diff --git a/README.md b/README.md index 19a127ce..4b82976e 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ Currently, the following components are supported: - ListBoxSelection - ListItem - Loading +- MultiSelect - Modal - Notification - ToastNotification diff --git a/src/components/Checkbox/Checkbox.svelte b/src/components/Checkbox/Checkbox.svelte index f1200a6a..688dd8e9 100644 --- a/src/components/Checkbox/Checkbox.svelte +++ b/src/components/Checkbox/Checkbox.svelte @@ -6,6 +6,8 @@ export let disabled = false; export let id = Math.random(); export let labelText = ''; + export let name = ''; + export let readonly = false; export let hideLabel = false; export let title = ''; export let style = undefined; @@ -37,7 +39,9 @@ {indeterminate} {disabled} {checked} - {id} /> + {name} + {id} + {readonly} /> diff --git a/src/components/ComboBox/ComboBox.svelte b/src/components/ComboBox/ComboBox.svelte index 0c7b6e1a..99779d14 100644 --- a/src/components/ComboBox/ComboBox.svelte +++ b/src/components/ComboBox/ComboBox.svelte @@ -30,8 +30,9 @@ ListBoxSelection } from '../ListBox'; + let selectedId = undefined; let inputRef = undefined; - let inputValue = undefined; + let inputValue = ''; let highlightedIndex = -1; function change(direction) { @@ -49,20 +50,21 @@ afterUpdate(() => { if (open) { inputRef.focus(); + filteredItems = items.filter(item => shouldFilterItem(item, value)); } else { highlightedIndex = -1; inputValue = selectedItem ? selectedItem.text : ''; } }); - $: selectedItem = items[selectedIndex]; - $: inputValue = selectedItem ? selectedItem.text : undefined; - $: value = inputValue; $: ariaLabel = $$props['aria-label'] || 'Choose an item'; $: menuId = `menu-${id}`; $: comboId = `combo-${id}`; $: highlightedId = items[highlightedIndex] ? items[highlightedIndex].id : undefined; $: filteredItems = items.filter(item => shouldFilterItem(item, value)); + $: selectedItem = items[selectedIndex]; + $: inputValue = selectedItem ? selectedItem.text : undefined; + $: value = inputValue; {#each filteredItems as item, i (item.id || i)} { - selectedIndex = i; + selectedId = item.id; + selectedIndex = items.map(({ id }) => id).indexOf(filteredItems[i].id); open = false; }} on:mouseenter={() => { diff --git a/src/components/ListBox/ListBox.svelte b/src/components/ListBox/ListBox.svelte index 826d9168..c9cb3e38 100644 --- a/src/components/ListBox/ListBox.svelte +++ b/src/components/ListBox/ListBox.svelte @@ -1,6 +1,7 @@
diff --git a/src/components/ListBox/ListBoxMenu.svelte b/src/components/ListBox/ListBoxMenu.svelte index 8d166cd7..d02f18ea 100644 --- a/src/components/ListBox/ListBoxMenu.svelte +++ b/src/components/ListBox/ListBoxMenu.svelte @@ -1,7 +1,7 @@ -
+
diff --git a/src/components/ListBox/ListBoxSelection.svelte b/src/components/ListBox/ListBoxSelection.svelte index 5b87edab..38b15107 100644 --- a/src/components/ListBox/ListBoxSelection.svelte +++ b/src/components/ListBox/ListBoxSelection.svelte @@ -7,21 +7,28 @@ export let translateWithId = id => defaultTranslations[id]; export let style = undefined; - import { createEventDispatcher } from 'svelte'; + import { createEventDispatcher, getContext } from 'svelte'; import Close16 from 'carbon-icons-svelte/lib/Close16'; import { cx } from '../../lib'; - const dispatch = createEventDispatcher(); - const defaultTranslations = { [translationIds.clearAll]: 'Clear all selected items', [translationIds.clearSelection]: 'Clear selected item' }; + const dispatch = createEventDispatcher(); + const ctx = getContext('MultiSelect'); + + let selectionRef = undefined; + + $: if (ctx && selectionRef) { + ctx.declareRef({ key: 'selection', ref: selectionRef }); + } $: description = selectionCount ? translateWithId('clearAll') : translateWithId('clearSelection');
+ import Layout from '../../internal/ui/Layout.svelte'; + import Button from '../Button'; + import MultiSelect from './MultiSelect.svelte'; + + let value = ''; + + let items = [ + { id: 'option-0', text: 'Option 1' }, + { id: 'option-1', text: 'Option 2' }, + { id: 'option-2', text: 'Option 3' }, + { id: 'option-3', text: 'Option 4' }, + { + id: 'option-4', + text: 'An example option that is really long to show what should be done to handle long text' + } + ]; + + let selectedIds = []; + + + +
+ +
+
+ +
+
diff --git a/src/components/MultiSelect/MultiSelect.stories.js b/src/components/MultiSelect/MultiSelect.stories.js new file mode 100644 index 00000000..52982d72 --- /dev/null +++ b/src/components/MultiSelect/MultiSelect.stories.js @@ -0,0 +1,38 @@ +import { withKnobs, select, boolean, text } from '@storybook/addon-knobs'; +import Component from './MultiSelect.Story.svelte'; + +export default { title: 'MultiSelect', decorators: [withKnobs] }; + +const types = { + 'Default (default)': 'default', + 'Inline (inline)': 'inline' +}; + +const sizes = { + 'Extra large size (xl)': 'xl', + 'Regular size (lg)': '', + 'Small size (sm)': 'sm' +}; + +export const Default = () => ({ + Component, + props: { + id: 'multiselect', + titleText: text('Title (titleText)', 'Multiselect Title'), + helperText: text('Helper text (helperText)', 'This is not helper text'), + filterable: boolean('Filterable (filterable)', false), + selectionFeedback: select( + 'Selection feedback (selectionFeedback)', + ['top', 'fixed', 'top-after-reopen'], + 'top-after-reopen' + ), + disabled: boolean('Disabled (disabled)', false), + light: boolean('Light variant (light)', false), + useTitleInItem: boolean('Show tooltip on hover', false), + type: select('UI type (Only for ``) (type)', types, 'default'), + size: select('Field size (size)', sizes, '') || undefined, + label: text('Label (label)', 'MultiSelect Label'), + invalid: boolean('Show form validation UI (invalid)', false), + invalidText: text('Form validation UI content (invalidText)', 'Invalid Selection') + } +}); diff --git a/src/components/MultiSelect/MultiSelect.svelte b/src/components/MultiSelect/MultiSelect.svelte new file mode 100644 index 00000000..79b9cad5 --- /dev/null +++ b/src/components/MultiSelect/MultiSelect.svelte @@ -0,0 +1,294 @@ + + + { + if (open && multiSelectRef && !multiSelectRef.contains(target)) { + open = false; + } + }} /> + +
+ {#if titleText} + + {/if} + {#if !inline && helperText} +
+ {helperText} +
+ {/if} + 0 && '--multi-select--selected')} + {id} + {disabled} + {invalid} + {invalidText} + {open} + {light} + {size}> + {#if invalid} + + {/if} + { + if (filterable) { + open = true; + inputRef.focus(); + } else { + open = !open; + } + }} + on:keydown={({ key }) => { + if (filterable) { + return; + } + if (key === ' ') { + open = !open; + } else if (key === 'Tab') { + if (selectionRef && checked.length > 0) { + selectionRef.focus(); + } else { + open = false; + fieldRef.blur(); + } + } else if (key === 'ArrowDown') { + change(1); + } else if (key === 'ArrowUp') { + change(-1); + } else if (key === 'Enter') { + if (highlightedIndex > -1) { + sortedItems[highlightedIndex].checked = !sortedItems[highlightedIndex].checked; + } + } + }} + on:blur={({ relatedTarget }) => { + if (relatedTarget && relatedTarget.getAttribute('role') !== 'button') { + fieldRef.focus(); + } + }} + {id} + {disabled} + {translateWithId}> + {#if checked.length > 0} + { + sortedItems = sortedItems.map(item => ({ ...item, checked: false })); + fieldRef.blur(); + }} + {translateWithId} + {disabled} /> + {/if} + {#if filterable} + { + inputValue = target.value; + }} + on:keydown + on:keydown|stopPropagation={({ key }) => { + if (key === 'Enter') { + if (highlightedIndex > -1) { + sortedItems[highlightedIndex].checked = !sortedItems[highlightedIndex].checked; + } + } else if (key === 'Tab') { + open = false; + } else if (key === 'ArrowDown') { + change(1); + } else if (key === 'ArrowUp') { + change(-1); + } + }} + on:focus + on:blur + on:blur={({ relatedTarget }) => { + if (relatedTarget && relatedTarget.getAttribute('role') !== 'button') { + inputRef.focus(); + } + }} + {disabled} + {placeholder} + {id} + value={inputValue} /> + {#if invalid} + + {/if} + {#if inputValue} + { + inputValue = ''; + open = false; + }} + {translateWithId} + {disabled} + {open} /> + {/if} + { + open = !open; + }} + {translateWithId} + {open} /> + {/if} + {#if !filterable} + {label} + + {/if} + + {#if open} + + {#each filterable ? filteredItems : sortedItems as item, i (item.id || i)} + { + sortedItems = sortedItems.map(_ => + _.id === item.id ? { ..._, checked: !_.checked } : _ + ); + fieldRef.focus(); + }} + on:mouseenter={() => { + highlightedIndex = i; + }}> + + + {/each} + + {/if} + +
diff --git a/src/components/MultiSelect/index.js b/src/components/MultiSelect/index.js new file mode 100644 index 00000000..5164a2b4 --- /dev/null +++ b/src/components/MultiSelect/index.js @@ -0,0 +1,3 @@ +import MultiSelect from './MultiSelect.svelte'; + +export default MultiSelect; diff --git a/src/index.js b/src/index.js index ba5d4617..7ce8755c 100644 --- a/src/index.js +++ b/src/index.js @@ -32,6 +32,7 @@ import ListBox, { } from './components/ListBox'; import ListItem from './components/ListItem'; import Loading from './components/Loading'; +import MultiSelect from './components/MultiSelect'; import Modal from './components/Modal'; import { ToastNotification, @@ -123,6 +124,7 @@ export { Icon, IconSkeleton, InlineLoading, + MultiSelect, Modal, InlineNotification, Link,