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/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,