diff --git a/CHANGELOG.md b/CHANGELOG.md index 3afeca8ffe..29fcf04c9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Add `hasMention`, `isImportant`, `hasMentionColor` and `isImportantColor` in ChatMessage variables in Teams theme @mnajdova ([#841](https://github.com/stardust-ui/react/pull/841)) - Add `actionMenu` prop to `ChatMessage` component @layershifter ([#811](https://github.com/stardust-ui/react/pull/811)) - Add `rtl` field in the `SvgIconFuncArg`, and used it in Teams theme's number-list icon ([#851](https://github.com/stardust-ui/react/pull/851)) +- Add single search flavor for `Dropdown` component @Bugaa92 ([#839](https://github.com/stardust-ui/react/pull/839)) ### Fixes - Fix `Dropdown` component styles regression @Bugaa92 ([#824](https://github.com/stardust-ui/react/pull/824)) @@ -96,7 +97,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Fixes - Make `headerMedia` visible for screen readers in `ListItem` @layershifter ([#772](https://github.com/stardust-ui/react/pull/772)) - Cleanup for `Dropdown` examples' accessibility and added localisation example. @silviuavram ([#771](https://github.com/stardust-ui/react/pull/771)) -- Fix highlighted selected option in single selection `Dropdown` when opened @silviuavram ([#726](https://github.com/stardust-ui/react/pull/726)) +- Fix highlighted selected option in single selection `Dropdown` when opened @silviuavram ([#726](https://github.com/stardust-ui/react/pull/726)) ## [v0.18.0](https://github.com/stardust-ui/react/tree/v0.18.0) (2019-01-24) diff --git a/docs/src/examples/components/Dropdown/State/DropdownExampleLoading.shorthand.tsx b/docs/src/examples/components/Dropdown/State/DropdownExampleLoading.shorthand.tsx index 1504c449ad..1737460030 100644 --- a/docs/src/examples/components/Dropdown/State/DropdownExampleLoading.shorthand.tsx +++ b/docs/src/examples/components/Dropdown/State/DropdownExampleLoading.shorthand.tsx @@ -8,9 +8,9 @@ const DropdownExampleLoading: React.FC<{ knobs: { loading: boolean } }> = ({ kno loading={knobs.loading} loadingMessage="Loading..." multiple + search items={inputItems} placeholder="Start typing a name" - search /> ) diff --git a/docs/src/examples/components/Dropdown/Types/DropdownExample.shorthand.steps.ts b/docs/src/examples/components/Dropdown/Types/DropdownExample.shorthand.steps.ts new file mode 100644 index 0000000000..f62edd01fd --- /dev/null +++ b/docs/src/examples/components/Dropdown/Types/DropdownExample.shorthand.steps.ts @@ -0,0 +1,17 @@ +import { Dropdown } from '@stardust-ui/react' + +const selectors = { + triggerButton: `.${Dropdown.slotClassNames.triggerButton}`, + dropdownItem: (itemIndex = 1) => + `.${Dropdown.slotClassNames.itemsList} li:nth-child(${itemIndex})`, +} + +const steps = [ + steps => steps.click(selectors.triggerButton).snapshot('Shows list'), + steps => steps.click(selectors.dropdownItem(3)).snapshot('Selects an item'), + steps => steps.click(selectors.triggerButton).snapshot('Opens with selected item highlighted'), + steps => steps.hover(selectors.dropdownItem(2)).snapshot('Highlights another item'), + steps => steps.click(selectors.triggerButton).snapshot('Closes the list'), +] + +export default steps diff --git a/docs/src/examples/components/Dropdown/Types/DropdownExampleSingleSelection.shorthand.tsx b/docs/src/examples/components/Dropdown/Types/DropdownExample.shorthand.tsx similarity index 82% rename from docs/src/examples/components/Dropdown/Types/DropdownExampleSingleSelection.shorthand.tsx rename to docs/src/examples/components/Dropdown/Types/DropdownExample.shorthand.tsx index cf208dda39..294f37f537 100644 --- a/docs/src/examples/components/Dropdown/Types/DropdownExampleSingleSelection.shorthand.tsx +++ b/docs/src/examples/components/Dropdown/Types/DropdownExample.shorthand.tsx @@ -15,11 +15,9 @@ const inputItems = [ const DropdownExample = () => ( `${item} has been selected.`, - }} - placeholder="Select your hero" items={inputItems} + placeholder="Select your hero" + getA11ySelectionMessage={{ onAdd: item => `${item} has been selected.` }} /> ) diff --git a/docs/src/examples/components/Dropdown/Types/DropdownExampleMultipleSearch.shorthand.tsx b/docs/src/examples/components/Dropdown/Types/DropdownExampleMultipleSearch.shorthand.tsx deleted file mode 100644 index 6bcf2363cb..0000000000 --- a/docs/src/examples/components/Dropdown/Types/DropdownExampleMultipleSearch.shorthand.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import * as React from 'react' -import { Dropdown } from '@stardust-ui/react' - -const inputItems = [ - 'Bruce Wayne', - 'Natasha Romanoff', - 'Steven Strange', - 'Alfred Pennyworth', - `Scarlett O'Hara`, - 'Imperator Furiosa', - 'Bruce Banner', - 'Peter Parker', - 'Selina Kyle', -] - -const DropdownExample = () => ( - -) - -const getA11ySelectionMessage = { - onAdd: item => `${item} has been selected.`, - onRemove: item => `${item} has been removed.`, -} -export default DropdownExample diff --git a/docs/src/examples/components/Dropdown/Types/DropdownExampleSearch.shorthand.tsx b/docs/src/examples/components/Dropdown/Types/DropdownExampleSearch.shorthand.tsx new file mode 100644 index 0000000000..7ddf627eeb --- /dev/null +++ b/docs/src/examples/components/Dropdown/Types/DropdownExampleSearch.shorthand.tsx @@ -0,0 +1,36 @@ +import * as React from 'react' +import { Dropdown, Header } from '@stardust-ui/react' + +const inputItems = [ + 'Bruce Wayne', + 'Natasha Romanoff', + 'Steven Strange', + 'Alfred Pennyworth', + `Scarlett O'Hara`, + 'Imperator Furiosa', + 'Bruce Banner', + 'Peter Parker', + 'Selina Kyle', +] + +const DropdownExampleSearch = () => ( + <> +
Single Search:
+ +
Multiple Search:
+ + +) + +export default DropdownExampleSearch diff --git a/docs/src/examples/components/Dropdown/Types/DropdownExampleSingleSelection.shorthand.steps.ts b/docs/src/examples/components/Dropdown/Types/DropdownExampleSingleSelection.shorthand.steps.ts deleted file mode 100644 index 119cc75fb6..0000000000 --- a/docs/src/examples/components/Dropdown/Types/DropdownExampleSingleSelection.shorthand.steps.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Dropdown, Button, List } from '@stardust-ui/react' - -const steps = [ - steps => steps.click(`.${Dropdown.className} .${Button.className}`).snapshot('Shows list'), - steps => steps.click(`.${List.className} li:nth-child(3)`).snapshot('Selects an element'), - steps => - steps - .click(`.${Dropdown.className} .${Button.className}`) - .snapshot('Opens with selected element highlighted'), - steps => steps.hover(`.${List.className} li:nth-child(2)`).snapshot('Highlights another element'), -] - -export default steps diff --git a/docs/src/examples/components/Dropdown/Types/index.tsx b/docs/src/examples/components/Dropdown/Types/index.tsx index 9b11169a48..71bfbd6f9e 100644 --- a/docs/src/examples/components/Dropdown/Types/index.tsx +++ b/docs/src/examples/components/Dropdown/Types/index.tsx @@ -5,14 +5,14 @@ import ExampleSection from 'docs/src/components/ComponentDoc/ExampleSection' const Types = () => ( ) diff --git a/docs/src/examples/components/Dropdown/Usage/DropdownExampleRender.shorthand.tsx b/docs/src/examples/components/Dropdown/Usage/DropdownExampleRender.shorthand.tsx index 5b57e8e11a..8eed523db2 100644 --- a/docs/src/examples/components/Dropdown/Usage/DropdownExampleRender.shorthand.tsx +++ b/docs/src/examples/components/Dropdown/Usage/DropdownExampleRender.shorthand.tsx @@ -15,8 +15,9 @@ const inputItems = [ const DropdownExampleRender: React.FC = () => ( ( @@ -24,7 +25,6 @@ const DropdownExampleRender: React.FC = () => ( renderSelectedItem={(SelectedItem: typeof Dropdown.SelectedItem, props) => ( )} - search /> ) diff --git a/docs/src/examples/components/Dropdown/Variations/DropdownExampleMultipleSearchFluid.shorthand.tsx b/docs/src/examples/components/Dropdown/Variations/DropdownExampleMultipleSearchFluid.shorthand.tsx index 2ebb415de7..c776fddc62 100644 --- a/docs/src/examples/components/Dropdown/Variations/DropdownExampleMultipleSearchFluid.shorthand.tsx +++ b/docs/src/examples/components/Dropdown/Variations/DropdownExampleMultipleSearchFluid.shorthand.tsx @@ -12,15 +12,16 @@ const inputItems = [ 'Peter Parker', 'Selina Kyle', ] -const DropdownExample = () => ( + +const DropdownExampleMultipleSearchFluid = () => ( ) @@ -29,4 +30,4 @@ const getA11ySelectionMessage = { onRemove: item => `${item} has been removed.`, } -export default DropdownExample +export default DropdownExampleMultipleSearchFluid diff --git a/docs/src/examples/components/Dropdown/Variations/DropdownExampleMultipleSearchFrenchLanguage.shorthand.tsx b/docs/src/examples/components/Dropdown/Variations/DropdownExampleMultipleSearchFrenchLanguage.shorthand.tsx index 41f971a84c..00080491c4 100644 --- a/docs/src/examples/components/Dropdown/Variations/DropdownExampleMultipleSearchFrenchLanguage.shorthand.tsx +++ b/docs/src/examples/components/Dropdown/Variations/DropdownExampleMultipleSearchFrenchLanguage.shorthand.tsx @@ -19,15 +19,15 @@ const inputItems = [ }, })) -const DropdownExample = () => ( +const DropdownExampleMultipleSearchFrenchLanguage = () => ( ) @@ -56,4 +56,5 @@ const getA11ySelectionMessage = { onAdd: item => `${item.header} a été choisi.`, onRemove: item => `${item.header} a été éliminé.`, } -export default DropdownExample + +export default DropdownExampleMultipleSearchFrenchLanguage diff --git a/docs/src/examples/components/Dropdown/Variations/DropdownExampleMultipleSearchImageAndContent.shorthand.tsx b/docs/src/examples/components/Dropdown/Variations/DropdownExampleMultipleSearchImageAndContent.shorthand.tsx index 1c0cf16bed..41fe747a74 100644 --- a/docs/src/examples/components/Dropdown/Variations/DropdownExampleMultipleSearchImageAndContent.shorthand.tsx +++ b/docs/src/examples/components/Dropdown/Variations/DropdownExampleMultipleSearchImageAndContent.shorthand.tsx @@ -49,14 +49,14 @@ const inputItems = [ }, ] -const DropdownExample = () => ( +const DropdownExampleMultipleSearchImageAndContent = () => ( ) @@ -65,4 +65,4 @@ const getA11ySelectionMessage = { onRemove: item => `${item.header} has been removed.`, } -export default DropdownExample +export default DropdownExampleMultipleSearchImageAndContent diff --git a/packages/react/src/components/Dropdown/Dropdown.tsx b/packages/react/src/components/Dropdown/Dropdown.tsx index 7468608220..b874965abc 100644 --- a/packages/react/src/components/Dropdown/Dropdown.tsx +++ b/packages/react/src/components/Dropdown/Dropdown.tsx @@ -41,6 +41,8 @@ import ListItem from '../List/ListItem' export interface DropdownSlotClassNames { container: string + triggerButton: string + itemsList: string selectedItems: string } @@ -218,11 +220,7 @@ class Dropdown extends AutoControlledComponent, Dropdo } // targets DropdownItem shorthand objects - if ((item as any).header) { - return (item as any).header - } - - return `${item}` + return (item as any).header || String(item) }, toggleIndicator: {}, triggerButton: {}, @@ -242,7 +240,7 @@ class Dropdown extends AutoControlledComponent, Dropdo searchQuery: search ? '' : undefined, value: multiple ? [] : null, // used on single selection to open the dropdown with the selected option as highlighted. - defaultHighlightedIndex: !this.props.search && !this.props.multiple ? null : undefined, + defaultHighlightedIndex: this.props.multiple ? undefined : null, } } @@ -263,9 +261,7 @@ class Dropdown extends AutoControlledComponent, Dropdo inputValue={search ? searchQuery : null} stateReducer={this.handleDownshiftStateChanges} itemToString={itemToString} - // If it's single search, don't pass anything. Pass a null otherwise, as Downshift does - // not handle selection by default for single/multiple selection and multiple search. - selectedItem={search && !multiple ? undefined : null} + selectedItem={null} getA11yStatusMessage={getA11yStatusMessage} defaultHighlightedIndex={defaultHighlightedIndex} onStateChange={this.handleStateChange} @@ -289,7 +285,7 @@ class Dropdown extends AutoControlledComponent, Dropdo
, Dropdo styles: ComponentSlotStylesInput, getToggleButtonProps: (options?: GetToggleButtonPropsOptions) => any, ): JSX.Element { - const { placeholder, itemToString, multiple, triggerButton } = this.props - const { value } = this.state - const content = value && !multiple ? itemToString(value) : placeholder + const content = this.getSelectedItemAsString(this.state.value) + return ( - {Button.create(triggerButton, { + {Button.create(this.props.triggerButton, { defaultProps: { + className: Dropdown.slotClassNames.triggerButton, content, fluid: true, styles: styles.button, @@ -438,6 +434,7 @@ class Dropdown extends AutoControlledComponent, Dropdo }} > , Dropdo private getItemsFilteredBySearchQuery = (): ShorthandValue[] => { const { items, itemToString, multiple, search } = this.props const { searchQuery, value } = this.state - let filteredItems = items - - if (multiple) { - filteredItems = _.difference(filteredItems, value as ShorthandValue[]) - } + const filteredItems = multiple ? _.difference(items, value as ShorthandValue[]) : items if (search) { if (_.isFunction(search)) { @@ -695,8 +688,8 @@ class Dropdown extends AutoControlledComponent, Dropdo } } - private handleContainerClick = (isOpen: boolean) => { - !isOpen && this.inputRef.current.focus() + private handleContainerClick = () => { + this.tryFocusSearchInput() } private handleListKeyDown = ( @@ -716,7 +709,7 @@ class Dropdown extends AutoControlledComponent, Dropdo return case keyboardKey.Escape: accessibilityInputPropsKeyDown(e) - this.buttonRef.current.focus() + this.tryFocusTriggerButton() return default: accessibilityInputPropsKeyDown(e) @@ -725,12 +718,15 @@ class Dropdown extends AutoControlledComponent, Dropdo } private handleSelectedChange = (item: ShorthandValue) => { - const { items, multiple, getA11ySelectionMessage, search } = this.props - const newValue = multiple ? [...(this.state.value as ShorthandValue[]), item] : item + const { items, multiple, getA11ySelectionMessage } = this.props + const newState = { + value: multiple ? [...(this.state.value as ShorthandValue[]), item] : item, + searchQuery: this.getSelectedItemAsString(item), + } - this.trySetState({ value: newValue, searchQuery: '' }) + this.trySetState(newState) - if (!search && !multiple) { + if (!multiple) { this.setState({ defaultHighlightedIndex: items.indexOf(item) }) } @@ -746,22 +742,16 @@ class Dropdown extends AutoControlledComponent, Dropdo ) } - if (!search) { - this.buttonRef.current.focus() - } + this.tryFocusTriggerButton() // we don't have event for it, but want to keep the event handling interface, event is empty. - _.invoke( - this.props, - 'onSelectedChange', - {}, - { ...this.props, searchQuery: '', value: newValue }, - ) + _.invoke(this.props, 'onSelectedChange', {}, { ...this.props, ...newState }) } private handleSelectedItemRemove(e: React.SyntheticEvent, item: ShorthandValue) { this.removeItemFromValue(item) - this.inputRef.current.focus() + this.tryFocusSearchInput() + this.tryFocusTriggerButton() e.stopPropagation() } @@ -785,10 +775,44 @@ class Dropdown extends AutoControlledComponent, Dropdo // we don't have event for it, but want to keep the event handling interface, event is empty. _.invoke(this.props, 'onSelectedChange', {}, { ...this.props, value }) } + + private tryFocusTriggerButton = () => { + if (!this.props.search) { + this.buttonRef.current.focus() + } + } + + private tryFocusSearchInput = () => { + if (this.props.search) { + this.inputRef.current.focus() + } + } + + /** + * If there is no value we use the placeholder value + * otherwise, for single selection we convert the value with itemToString + * and for multiple selection we return empty string, the values are rendered by renderSelectedItems + */ + private getSelectedItemAsString = (value: ShorthandValue): string => { + const { itemToString, multiple, placeholder } = this.props + const isValueEmpty = _.isArray(value) ? value.length < 1 : !value + + if (isValueEmpty) { + return placeholder + } + + if (multiple) { + return '' + } + + return itemToString(value) + } } Dropdown.slotClassNames = { container: `${Dropdown.className}__container`, + triggerButton: `${Dropdown.className}__trigger-button`, + itemsList: `${Dropdown.className}__items-list`, selectedItems: `${Dropdown.className}__selected-items`, } diff --git a/packages/react/src/components/Dropdown/DropdownSelectedItem.tsx b/packages/react/src/components/Dropdown/DropdownSelectedItem.tsx index bf82a1525c..55d95b4a33 100644 --- a/packages/react/src/components/Dropdown/DropdownSelectedItem.tsx +++ b/packages/react/src/components/Dropdown/DropdownSelectedItem.tsx @@ -15,6 +15,10 @@ import { import { Image, Icon, Label } from '../..' import { IconProps } from '../Icon/Icon' +export interface DropdownSelectedItemSlotClassNames { + removeIcon: string +} + export interface DropdownSelectedItemProps extends UIComponentProps { /** Header of the selected item. */ header?: string @@ -48,9 +52,8 @@ export interface DropdownSelectedItemProps extends UIComponentProps, any> { static displayName = 'DropdownSelectedItem' - static create: Function - + static slotClassNames: DropdownSelectedItemSlotClassNames static className = 'ui-dropdown__selected-item' static propTypes = { @@ -97,6 +100,7 @@ class DropdownSelectedItem extends UIComponent