diff --git a/CHANGELOG.md b/CHANGELOG.md index 92eb4f1e8e..092dfe18e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Adding status behavior @kolaps33 ([#880](https://github.com/stardust-ui/react/pull/880)) - Add basic animation library for Teams theme @bhamlefty @mnajdova ([#871](https://github.com/stardust-ui/react/pull/871) - Export `accept` and `urgent` SVG icons to the Teams Theme @joheredi([#929](https://github.com/stardust-ui/react/pull/929)) +- Add `open`, `defaultOpen` and `onOpenChange` props for `Dropdown` component (controlled mode) @Bugaa92 ([#900](https://github.com/stardust-ui/react/pull/900)) ### Fixes - Display correctly images in portrait mode inside `Avatar` @layershifter ([#899](https://github.com/stardust-ui/react/pull/899)) diff --git a/docs/src/examples/components/Dropdown/Usage/DropdownExampleControlled.shorthand.tsx b/docs/src/examples/components/Dropdown/Usage/DropdownExampleControlled.shorthand.tsx new file mode 100644 index 0000000000..591107085a --- /dev/null +++ b/docs/src/examples/components/Dropdown/Usage/DropdownExampleControlled.shorthand.tsx @@ -0,0 +1,29 @@ +import * as React from 'react' +import { Dropdown, Flex, Text } from '@stardust-ui/react' + +const inputItems = ['Bruce Wayne', 'Natasha Romanoff', 'Steven Strange', 'Alfred Pennyworth'] + +class DropdownExampleControlled extends React.Component { + state = { open: false } + + handleOpenChange = (e, { open }) => { + this.setState({ open }) + } + + render() { + const open = this.state.open + return ( + + + + + ) + } +} + +export default DropdownExampleControlled diff --git a/docs/src/examples/components/Dropdown/Usage/index.tsx b/docs/src/examples/components/Dropdown/Usage/index.tsx index 392794dac3..1f36099ebc 100644 --- a/docs/src/examples/components/Dropdown/Usage/index.tsx +++ b/docs/src/examples/components/Dropdown/Usage/index.tsx @@ -2,8 +2,13 @@ import * as React from 'react' import ComponentExample from 'docs/src/components/ComponentDoc/ComponentExample' import ExampleSection from 'docs/src/components/ComponentDoc/ExampleSection' -const Variations = () => ( - +const Usage = () => ( + + ( ) -export default Variations +export default Usage diff --git a/docs/src/examples/components/Popup/Types/index.tsx b/docs/src/examples/components/Popup/Types/index.tsx index 06310be153..26582e6889 100644 --- a/docs/src/examples/components/Popup/Types/index.tsx +++ b/docs/src/examples/components/Popup/Types/index.tsx @@ -12,7 +12,7 @@ const Types = () => ( /> + /** * Callback for change in dropdown search query value. * @param {SyntheticEvent} event - React's original SyntheticEvent. @@ -128,6 +138,9 @@ export interface DropdownProps extends UIComponentProps + /** Defines whether dropdown is displayed. */ + open?: boolean + /** A placeholder message for the input field. */ placeholder?: string @@ -172,8 +185,8 @@ export interface DropdownState { activeSelectedIndex: number defaultHighlightedIndex: number focused: boolean - isOpen?: boolean - searchQuery?: string + open: boolean + searchQuery: string value: ShorthandValue | ShorthandCollection } @@ -204,6 +217,7 @@ class Dropdown extends AutoControlledComponent, Dropdo clearable: PropTypes.bool, clearIndicator: customPropTypes.itemShorthand, defaultActiveSelectedIndex: PropTypes.number, + defaultOpen: PropTypes.bool, defaultSearchQuery: PropTypes.string, defaultValue: PropTypes.oneOfType([ customPropTypes.itemShorthand, @@ -219,8 +233,10 @@ class Dropdown extends AutoControlledComponent, Dropdo loadingMessage: customPropTypes.itemShorthand, multiple: PropTypes.bool, noResultsMessage: customPropTypes.itemShorthand, + onOpenChange: PropTypes.func, onSearchQueryChange: PropTypes.func, onSelectedChange: PropTypes.func, + open: PropTypes.bool, placeholder: PropTypes.string, renderItem: PropTypes.func, renderSelectedItem: PropTypes.func, @@ -250,7 +266,7 @@ class Dropdown extends AutoControlledComponent, Dropdo triggerButton: {}, } - static autoControlledProps = ['activeSelectedIndex', 'searchQuery', 'value'] + static autoControlledProps = ['activeSelectedIndex', 'open', 'searchQuery', 'value'] static Item = DropdownItem static SearchInput = DropdownSearchInput @@ -262,6 +278,7 @@ class Dropdown extends AutoControlledComponent, Dropdo // used on single selection to open the dropdown with the selected option as highlighted. defaultHighlightedIndex: this.props.multiple ? undefined : null, focused: false, + open: false, searchQuery: search ? '' : undefined, value: multiple ? [] : null, } @@ -284,11 +301,12 @@ class Dropdown extends AutoControlledComponent, Dropdo itemToString, toggleIndicator, } = this.props - const { defaultHighlightedIndex, searchQuery, value } = this.state + const { defaultHighlightedIndex, open, searchQuery, value } = this.state return ( , Dropdo getMenuProps, getRootProps, getToggleButtonProps, - isOpen, toggleMenu, highlightedIndex, selectItemAtIndex, @@ -320,7 +337,7 @@ class Dropdown extends AutoControlledComponent, Dropdo
, Dropdo }) : Indicator.create(toggleIndicator, { defaultProps: { - direction: isOpen ? 'top' : 'bottom', + direction: open ? 'top' : 'bottom', styles: styles.toggleIndicator, }, overrideProps: (predefinedProps: IndicatorProps) => ({ @@ -367,7 +384,6 @@ class Dropdown extends AutoControlledComponent, Dropdo {this.renderItemsList( styles, variables, - isOpen, highlightedIndex, toggleMenu, selectItemAtIndex, @@ -458,7 +474,6 @@ class Dropdown extends AutoControlledComponent, Dropdo private renderItemsList( styles: ComponentSlotStylesInput, variables: ComponentVariablesInput, - isOpen: boolean, highlightedIndex: number, toggleMenu: () => void, selectItemAtIndex: (index: number) => void, @@ -467,6 +482,7 @@ class Dropdown extends AutoControlledComponent, Dropdo getInputProps: (options?: GetInputPropsOptions) => any, ) { const { search } = this.props + const { open } = this.state const { innerRef, ...accessibilityMenuProps } = getMenuProps( { refKey: 'innerRef' }, { suppressRefError: true }, @@ -501,8 +517,8 @@ class Dropdown extends AutoControlledComponent, Dropdo {...accessibilityMenuProps} styles={styles.list} tabIndex={search ? undefined : -1} // needs to be focused when trigger button is activated. - aria-hidden={!isOpen} - items={isOpen ? this.renderItems(styles, variables, getItemProps, highlightedIndex) : []} + aria-hidden={!open} + items={open ? this.renderItems(styles, variables, getItemProps, highlightedIndex) : []} /> ) @@ -576,13 +592,7 @@ class Dropdown extends AutoControlledComponent, Dropdo } private handleSearchQueryChange = (searchQuery: string) => { - this.trySetState({ searchQuery }) - _.invoke( - this.props, - 'onSearchQueryChange', - {}, // we don't have event for it, but want to keep the event handling interface, event is empty. - { ...this.props, searchQuery }, - ) + this.trySetStateAndInvokeHandler('onSearchQueryChange', null, { searchQuery }) } private handleDownshiftStateChanges = ( @@ -602,8 +612,8 @@ class Dropdown extends AutoControlledComponent, Dropdo } private handleStateChange = (changes: StateChangeOptions) => { - if (changes.isOpen !== undefined && changes.isOpen !== this.state.isOpen) { - this.setState({ isOpen: changes.isOpen }) + if (changes.isOpen !== undefined && changes.isOpen !== this.state.open) { + this.trySetStateAndInvokeHandler('onOpenChange', null, { open: changes.isOpen }) } if (changes.isOpen && !this.props.search) { @@ -664,19 +674,18 @@ class Dropdown extends AutoControlledComponent, Dropdo item: ShorthandValue, rtl: boolean, ) => ({ - onRemove: (e: React.SyntheticEvent, DropdownSelectedItemProps: DropdownSelectedItemProps) => { - this.handleSelectedItemRemove(e, item, predefinedProps, DropdownSelectedItemProps) + onRemove: (e: React.SyntheticEvent, dropdownSelectedItemProps: DropdownSelectedItemProps) => { + this.handleSelectedItemRemove(e, item, predefinedProps, dropdownSelectedItemProps) }, - onClick: (e: React.SyntheticEvent, DropdownSelectedItemProps: DropdownSelectedItemProps) => { + onClick: (e: React.SyntheticEvent, dropdownSelectedItemProps: DropdownSelectedItemProps) => { const { value } = this.state as { value: ShorthandCollection } - this.trySetState({ - activeSelectedIndex: value.indexOf(item), - }) + + this.trySetState({ activeSelectedIndex: value.indexOf(item) }) e.stopPropagation() - _.invoke(predefinedProps, 'onClick', e, DropdownSelectedItemProps) + _.invoke(predefinedProps, 'onClick', e, dropdownSelectedItemProps) }, - onKeyDown: (e: React.SyntheticEvent, DropdownSelectedItemProps: DropdownSelectedItemProps) => { - this.handleSelectedItemKeyDown(e, item, predefinedProps, DropdownSelectedItemProps, rtl) + onKeyDown: (e: React.SyntheticEvent, dropdownSelectedItemProps: DropdownSelectedItemProps) => { + this.handleSelectedItemKeyDown(e, item, predefinedProps, dropdownSelectedItemProps, rtl) }, }) @@ -846,12 +855,11 @@ class Dropdown extends AutoControlledComponent, Dropdo private handleSelectedChange = (item: ShorthandValue) => { const { items, multiple, getA11ySelectionMessage } = this.props - const newState = { + + this.trySetStateAndInvokeHandler('onSelectedChange', null, { value: multiple ? [...(this.state.value as ShorthandCollection), item] : item, searchQuery: this.getSelectedItemAsString(item), - } - - this.trySetState(newState) + }) if (!multiple) { this.setState({ defaultHighlightedIndex: items.indexOf(item) }) @@ -870,9 +878,6 @@ class Dropdown extends AutoControlledComponent, Dropdo } 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, ...newState }) } private handleSelectedItemKeyDown( @@ -896,21 +901,15 @@ class Dropdown extends AutoControlledComponent, Dropdo break case previousKey: if (value.length > 0 && !_.isNil(activeSelectedIndex) && activeSelectedIndex > 0) { - this.trySetState({ - activeSelectedIndex: activeSelectedIndex - 1, - }) + this.trySetState({ activeSelectedIndex: activeSelectedIndex - 1 }) } break case nextKey: if (value.length > 0 && !_.isNil(activeSelectedIndex)) { if (activeSelectedIndex < value.length - 1) { - this.trySetState({ - activeSelectedIndex: activeSelectedIndex + 1, - }) + this.trySetState({ activeSelectedIndex: activeSelectedIndex + 1 }) } else { - this.trySetState({ - activeSelectedIndex: null, - }) + this.trySetState({ activeSelectedIndex: null }) if (this.props.search) { e.preventDefault() // prevents caret to forward one position in input. this.inputRef.current.focus() @@ -932,9 +931,7 @@ class Dropdown extends AutoControlledComponent, Dropdo predefinedProps: DropdownSelectedItemProps, DropdownSelectedItemProps: DropdownSelectedItemProps, ) { - this.trySetState({ - activeSelectedIndex: null, - }) + this.trySetState({ activeSelectedIndex: null }) this.removeItemFromValue(item) this.tryFocusSearchInput() this.tryFocusTriggerButton() @@ -953,14 +950,25 @@ class Dropdown extends AutoControlledComponent, Dropdo poppedItem = value.pop() } - this.trySetState({ value }) - if (getA11ySelectionMessage && getA11ySelectionMessage.onRemove) { this.setA11yStatus(getA11ySelectionMessage.onRemove(poppedItem)) } - // 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 }) + this.trySetStateAndInvokeHandler('onSelectedChange', null, { value }) + } + + /** + * Calls trySetState (for autoControlledProps) and invokes event handler exposed to user. + * We don't have the event object for most events coming from Downshift se we send an empty event + * because we want to keep the event handling interface + */ + private trySetStateAndInvokeHandler = ( + handlerName: keyof DropdownProps, + event: React.SyntheticEvent, + newState: Partial, + ) => { + this.trySetState(newState) + _.invoke(this.props, handlerName, event, { ...this.props, ...newState }) } private tryFocusTriggerButton = () => { diff --git a/packages/react/src/themes/teams/components/Dropdown/dropdownStyles.ts b/packages/react/src/themes/teams/components/Dropdown/dropdownStyles.ts index 18d9ca09a9..d6a7c808b4 100644 --- a/packages/react/src/themes/teams/components/Dropdown/dropdownStyles.ts +++ b/packages/react/src/themes/teams/components/Dropdown/dropdownStyles.ts @@ -122,7 +122,7 @@ const dropdownStyles: ComponentSlotStylesInput