From 8834fc1edb9a2c2df692aed8c6186ab61d5f7547 Mon Sep 17 00:00:00 2001 From: Alexandru Buliga Date: Thu, 14 Feb 2019 12:51:20 +0100 Subject: [PATCH 1/4] feat(dropdown): autocontrolled mode for open state - introduced `open`, `defaultOpen` props - introduced `onOpenChange` event handled --- .../DropdownExampleControlled.shorthand.tsx | 29 +++++ .../components/Dropdown/Usage/index.tsx | 11 +- .../examples/components/Popup/Types/index.tsx | 2 +- .../src/components/Dropdown/Dropdown.tsx | 114 ++++++++++-------- .../components/Dropdown/dropdownStyles.ts | 2 +- 5 files changed, 100 insertions(+), 58 deletions(-) create mode 100644 docs/src/examples/components/Dropdown/Usage/DropdownExampleControlled.shorthand.tsx 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..df71fb5aa8 --- /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 } + + render() { + const open = this.state.open + return ( + + + + + ) + } + + handleOpenChange = (e, { open }) => { + this.setState({ open }) + } +} + +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,7 @@ class Dropdown extends AutoControlledComponent, Dropdo {this.renderItemsList( styles, variables, - isOpen, + open, highlightedIndex, toggleMenu, selectItemAtIndex, @@ -458,7 +475,7 @@ class Dropdown extends AutoControlledComponent, Dropdo private renderItemsList( styles: ComponentSlotStylesInput, variables: ComponentVariablesInput, - isOpen: boolean, + open: boolean, highlightedIndex: number, toggleMenu: () => void, selectItemAtIndex: (index: number) => void, @@ -501,8 +518,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 +593,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.setStateAndInvokeHandler({ searchQuery }, 'onSearchQueryChange') } private handleDownshiftStateChanges = ( @@ -602,8 +613,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.setStateAndInvokeHandler({ open: changes.isOpen }, 'onOpenChange') } if (changes.isOpen && !this.props.search) { @@ -664,19 +675,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 +856,14 @@ class Dropdown extends AutoControlledComponent, Dropdo private handleSelectedChange = (item: ShorthandValue) => { const { items, multiple, getA11ySelectionMessage } = this.props - const newState = { - value: multiple ? [...(this.state.value as ShorthandCollection), item] : item, - searchQuery: this.getSelectedItemAsString(item), - } - this.trySetState(newState) + this.setStateAndInvokeHandler( + { + value: multiple ? [...(this.state.value as ShorthandCollection), item] : item, + searchQuery: this.getSelectedItemAsString(item), + }, + 'onSelectedChange', + ) if (!multiple) { this.setState({ defaultHighlightedIndex: items.indexOf(item) }) @@ -870,9 +882,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 +905,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 +935,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 +954,21 @@ 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.setStateAndInvokeHandler({ value }, 'onSelectedChange') + } + + /** + * 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 setStateAndInvokeHandler = (newState: Partial, eventName: string) => { + this.trySetState(newState) + _.invoke(this.props, eventName, {}, { ...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 Date: Thu, 14 Feb 2019 15:26:35 +0100 Subject: [PATCH 2/4] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) 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)) From ac31952f7e019ede5f6d7be1bd985b5fe0952d95 Mon Sep 17 00:00:00 2001 From: Alexandru Buliga Date: Tue, 19 Feb 2019 14:05:54 +0100 Subject: [PATCH 3/4] addressed comments --- .../src/components/Dropdown/Dropdown.tsx | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/packages/react/src/components/Dropdown/Dropdown.tsx b/packages/react/src/components/Dropdown/Dropdown.tsx index 50e645de3d..af146d2feb 100644 --- a/packages/react/src/components/Dropdown/Dropdown.tsx +++ b/packages/react/src/components/Dropdown/Dropdown.tsx @@ -593,7 +593,7 @@ class Dropdown extends AutoControlledComponent, Dropdo } private handleSearchQueryChange = (searchQuery: string) => { - this.setStateAndInvokeHandler({ searchQuery }, 'onSearchQueryChange') + this.trySetStateAndInvokeHandler('onSearchQueryChange', null, { searchQuery }) } private handleDownshiftStateChanges = ( @@ -614,7 +614,7 @@ class Dropdown extends AutoControlledComponent, Dropdo private handleStateChange = (changes: StateChangeOptions) => { if (changes.isOpen !== undefined && changes.isOpen !== this.state.open) { - this.setStateAndInvokeHandler({ open: changes.isOpen }, 'onOpenChange') + this.trySetStateAndInvokeHandler('onOpenChange', null, { open: changes.isOpen }) } if (changes.isOpen && !this.props.search) { @@ -857,13 +857,10 @@ class Dropdown extends AutoControlledComponent, Dropdo private handleSelectedChange = (item: ShorthandValue) => { const { items, multiple, getA11ySelectionMessage } = this.props - this.setStateAndInvokeHandler( - { - value: multiple ? [...(this.state.value as ShorthandCollection), item] : item, - searchQuery: this.getSelectedItemAsString(item), - }, - 'onSelectedChange', - ) + this.trySetStateAndInvokeHandler('onSelectedChange', null, { + value: multiple ? [...(this.state.value as ShorthandCollection), item] : item, + searchQuery: this.getSelectedItemAsString(item), + }) if (!multiple) { this.setState({ defaultHighlightedIndex: items.indexOf(item) }) @@ -958,7 +955,7 @@ class Dropdown extends AutoControlledComponent, Dropdo this.setA11yStatus(getA11ySelectionMessage.onRemove(poppedItem)) } - this.setStateAndInvokeHandler({ value }, 'onSelectedChange') + this.trySetStateAndInvokeHandler('onSelectedChange', null, { value }) } /** @@ -966,9 +963,13 @@ class Dropdown extends AutoControlledComponent, Dropdo * 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 setStateAndInvokeHandler = (newState: Partial, eventName: string) => { + private trySetStateAndInvokeHandler = ( + handlerName: keyof DropdownProps, + event: React.SyntheticEvent, + newState: Partial, + ) => { this.trySetState(newState) - _.invoke(this.props, eventName, {}, { ...this.props, ...newState }) + _.invoke(this.props, handlerName, event, { ...this.props, ...newState }) } private tryFocusTriggerButton = () => { From dbcd0dc1caa4fb338268241209f92755bdef616b Mon Sep 17 00:00:00 2001 From: Alexandru Buliga Date: Wed, 20 Feb 2019 13:00:07 +0100 Subject: [PATCH 4/4] - removed redundant open flag - reordered handlers and render function --- .../Usage/DropdownExampleControlled.shorthand.tsx | 8 ++++---- packages/react/src/components/Dropdown/Dropdown.tsx | 3 +-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/src/examples/components/Dropdown/Usage/DropdownExampleControlled.shorthand.tsx b/docs/src/examples/components/Dropdown/Usage/DropdownExampleControlled.shorthand.tsx index df71fb5aa8..591107085a 100644 --- a/docs/src/examples/components/Dropdown/Usage/DropdownExampleControlled.shorthand.tsx +++ b/docs/src/examples/components/Dropdown/Usage/DropdownExampleControlled.shorthand.tsx @@ -6,6 +6,10 @@ const inputItems = ['Bruce Wayne', 'Natasha Romanoff', 'Steven Strange', 'Alfred class DropdownExampleControlled extends React.Component { state = { open: false } + handleOpenChange = (e, { open }) => { + this.setState({ open }) + } + render() { const open = this.state.open return ( @@ -20,10 +24,6 @@ class DropdownExampleControlled extends React.Component { ) } - - handleOpenChange = (e, { open }) => { - this.setState({ open }) - } } export default DropdownExampleControlled diff --git a/packages/react/src/components/Dropdown/Dropdown.tsx b/packages/react/src/components/Dropdown/Dropdown.tsx index af146d2feb..af79faad1d 100644 --- a/packages/react/src/components/Dropdown/Dropdown.tsx +++ b/packages/react/src/components/Dropdown/Dropdown.tsx @@ -384,7 +384,6 @@ class Dropdown extends AutoControlledComponent, Dropdo {this.renderItemsList( styles, variables, - open, highlightedIndex, toggleMenu, selectItemAtIndex, @@ -475,7 +474,6 @@ class Dropdown extends AutoControlledComponent, Dropdo private renderItemsList( styles: ComponentSlotStylesInput, variables: ComponentVariablesInput, - open: boolean, highlightedIndex: number, toggleMenu: () => void, selectItemAtIndex: (index: number) => void, @@ -484,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 },