Skip to content
This repository was archived by the owner on Mar 4, 2020. It is now read-only.

Commit 0d68612

Browse files
committed
feat(dropdown): autocontrolled mode for open state
- introduced `open`, `defaultOpen` props - introduced `onOpenChange` event handled
1 parent d237dbd commit 0d68612

File tree

5 files changed

+100
-58
lines changed

5 files changed

+100
-58
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import * as React from 'react'
2+
import { Dropdown, Flex, Text } from '@stardust-ui/react'
3+
4+
const inputItems = ['Bruce Wayne', 'Natasha Romanoff', 'Steven Strange', 'Alfred Pennyworth']
5+
6+
class DropdownExampleControlled extends React.Component {
7+
state = { open: false }
8+
9+
render() {
10+
const open = this.state.open
11+
return (
12+
<Flex gap="gap.large" vAlign="center">
13+
<Dropdown
14+
open={open}
15+
onOpenChange={this.handleOpenChange}
16+
items={inputItems}
17+
placeholder="Select your hero"
18+
/>
19+
<Text weight="semibold" content={`Dropdown open state is: "${open}"`} />
20+
</Flex>
21+
)
22+
}
23+
24+
handleOpenChange = (e, { open }) => {
25+
this.setState({ open })
26+
}
27+
}
28+
29+
export default DropdownExampleControlled

docs/src/examples/components/Dropdown/Usage/index.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,13 @@ import * as React from 'react'
22
import ComponentExample from 'docs/src/components/ComponentDoc/ComponentExample'
33
import ExampleSection from 'docs/src/components/ComponentDoc/ExampleSection'
44

5-
const Variations = () => (
6-
<ExampleSection title="Variations">
5+
const Usage = () => (
6+
<ExampleSection title="Usage">
7+
<ComponentExample
8+
title="Controlled"
9+
description="A dropdown can handle open state in controlled mode."
10+
examplePath="components/Dropdown/Usage/DropdownExampleControlled"
11+
/>
712
<ComponentExample
813
title="Render callbacks"
914
description="You can customize rendered elements with render callbacks."
@@ -12,4 +17,4 @@ const Variations = () => (
1217
</ExampleSection>
1318
)
1419

15-
export default Variations
20+
export default Usage

docs/src/examples/components/Popup/Types/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const Types = () => (
1212
/>
1313
<ComponentExample
1414
title="Controlled"
15-
description="Note that if Popup is controlled, then its 'open' prop value could be changed either by parent component, or by user actions (e.g. key press) - thus it is necessary to handle 'onOpenChanged' event. Try to type some text into popup's input field and press ESC to see the effect."
15+
description="Note that if Popup is controlled, then its 'open' prop value could be changed either by parent component, or by user actions (e.g. key press) - thus it is necessary to handle 'onOpenChange' event. Try to type some text into popup's input field and press ESC to see the effect."
1616
examplePath="components/Popup/Types/PopupControlledExample"
1717
/>
1818
<ComponentExample

packages/react/src/components/Dropdown/Dropdown.tsx

Lines changed: 61 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ export interface DropdownProps extends UIComponentProps<DropdownProps, DropdownS
6262
/** The initial value for the index of the currently active selected item, in a multiple selection. */
6363
defaultActiveSelectedIndex?: number
6464

65+
/** Initial value for 'open' in uncontrolled mode */
66+
defaultOpen?: boolean
67+
6568
/** The initial value for the search query, if the dropdown is also a search. */
6669
defaultSearchQuery?: string
6770

@@ -114,6 +117,13 @@ export interface DropdownProps extends UIComponentProps<DropdownProps, DropdownS
114117
/** A message to be displayed in the list when dropdown has no available items to show. */
115118
noResultsMessage?: ShorthandValue
116119

120+
/**
121+
* Callback for change in dropdown open value.
122+
* @param {SyntheticEvent} event - React's original SyntheticEvent.
123+
* @param {Object} data - All props and the new open flag value in the edit text.
124+
*/
125+
onOpenChange?: ComponentEventHandler<DropdownProps>
126+
117127
/**
118128
* Callback for change in dropdown search query value.
119129
* @param {SyntheticEvent} event - React's original SyntheticEvent.
@@ -128,6 +138,9 @@ export interface DropdownProps extends UIComponentProps<DropdownProps, DropdownS
128138
*/
129139
onSelectedChange?: ComponentEventHandler<DropdownProps>
130140

141+
/** Defines whether dropdown is displayed. */
142+
open?: boolean
143+
131144
/** A placeholder message for the input field. */
132145
placeholder?: string
133146

@@ -172,8 +185,8 @@ export interface DropdownState {
172185
activeSelectedIndex: number
173186
defaultHighlightedIndex: number
174187
focused: boolean
175-
isOpen?: boolean
176-
searchQuery?: string
188+
open: boolean
189+
searchQuery: string
177190
value: ShorthandValue | ShorthandCollection
178191
}
179192

@@ -204,6 +217,7 @@ class Dropdown extends AutoControlledComponent<Extendable<DropdownProps>, Dropdo
204217
clearable: PropTypes.bool,
205218
clearIndicator: customPropTypes.itemShorthand,
206219
defaultActiveSelectedIndex: PropTypes.number,
220+
defaultOpen: PropTypes.bool,
207221
defaultSearchQuery: PropTypes.string,
208222
defaultValue: PropTypes.oneOfType([
209223
customPropTypes.itemShorthand,
@@ -219,8 +233,10 @@ class Dropdown extends AutoControlledComponent<Extendable<DropdownProps>, Dropdo
219233
loadingMessage: customPropTypes.itemShorthand,
220234
multiple: PropTypes.bool,
221235
noResultsMessage: customPropTypes.itemShorthand,
236+
onOpenChange: PropTypes.func,
222237
onSearchQueryChange: PropTypes.func,
223238
onSelectedChange: PropTypes.func,
239+
open: PropTypes.bool,
224240
placeholder: PropTypes.string,
225241
renderItem: PropTypes.func,
226242
renderSelectedItem: PropTypes.func,
@@ -250,7 +266,7 @@ class Dropdown extends AutoControlledComponent<Extendable<DropdownProps>, Dropdo
250266
triggerButton: {},
251267
}
252268

253-
static autoControlledProps = ['activeSelectedIndex', 'searchQuery', 'value']
269+
static autoControlledProps = ['activeSelectedIndex', 'open', 'searchQuery', 'value']
254270

255271
static Item = DropdownItem
256272
static SearchInput = DropdownSearchInput
@@ -262,6 +278,7 @@ class Dropdown extends AutoControlledComponent<Extendable<DropdownProps>, Dropdo
262278
// used on single selection to open the dropdown with the selected option as highlighted.
263279
defaultHighlightedIndex: this.props.multiple ? undefined : null,
264280
focused: false,
281+
open: false,
265282
searchQuery: search ? '' : undefined,
266283
value: multiple ? [] : null,
267284
}
@@ -284,11 +301,12 @@ class Dropdown extends AutoControlledComponent<Extendable<DropdownProps>, Dropdo
284301
itemToString,
285302
toggleIndicator,
286303
} = this.props
287-
const { defaultHighlightedIndex, searchQuery, value } = this.state
304+
const { defaultHighlightedIndex, open, searchQuery, value } = this.state
288305

289306
return (
290307
<ElementType className={classes.root} {...unhandledProps}>
291308
<Downshift
309+
isOpen={open}
292310
onChange={this.handleSelectedChange}
293311
onInputValueChange={this.handleSearchQueryChange}
294312
inputValue={search ? searchQuery : null}
@@ -305,7 +323,6 @@ class Dropdown extends AutoControlledComponent<Extendable<DropdownProps>, Dropdo
305323
getMenuProps,
306324
getRootProps,
307325
getToggleButtonProps,
308-
isOpen,
309326
toggleMenu,
310327
highlightedIndex,
311328
selectItemAtIndex,
@@ -320,7 +337,7 @@ class Dropdown extends AutoControlledComponent<Extendable<DropdownProps>, Dropdo
320337
<Ref innerRef={innerRef}>
321338
<div
322339
className={cx(Dropdown.slotClassNames.container, classes.container)}
323-
onClick={search && !isOpen ? this.handleContainerClick : undefined}
340+
onClick={search && !open ? this.handleContainerClick : undefined}
324341
>
325342
<div
326343
ref={this.selectedItemsRef}
@@ -354,7 +371,7 @@ class Dropdown extends AutoControlledComponent<Extendable<DropdownProps>, Dropdo
354371
})
355372
: Indicator.create(toggleIndicator, {
356373
defaultProps: {
357-
direction: isOpen ? 'top' : 'bottom',
374+
direction: open ? 'top' : 'bottom',
358375
styles: styles.toggleIndicator,
359376
},
360377
overrideProps: (predefinedProps: IndicatorProps) => ({
@@ -367,7 +384,7 @@ class Dropdown extends AutoControlledComponent<Extendable<DropdownProps>, Dropdo
367384
{this.renderItemsList(
368385
styles,
369386
variables,
370-
isOpen,
387+
open,
371388
highlightedIndex,
372389
toggleMenu,
373390
selectItemAtIndex,
@@ -458,7 +475,7 @@ class Dropdown extends AutoControlledComponent<Extendable<DropdownProps>, Dropdo
458475
private renderItemsList(
459476
styles: ComponentSlotStylesInput,
460477
variables: ComponentVariablesInput,
461-
isOpen: boolean,
478+
open: boolean,
462479
highlightedIndex: number,
463480
toggleMenu: () => void,
464481
selectItemAtIndex: (index: number) => void,
@@ -501,8 +518,8 @@ class Dropdown extends AutoControlledComponent<Extendable<DropdownProps>, Dropdo
501518
{...accessibilityMenuProps}
502519
styles={styles.list}
503520
tabIndex={search ? undefined : -1} // needs to be focused when trigger button is activated.
504-
aria-hidden={!isOpen}
505-
items={isOpen ? this.renderItems(styles, variables, getItemProps, highlightedIndex) : []}
521+
aria-hidden={!open}
522+
items={open ? this.renderItems(styles, variables, getItemProps, highlightedIndex) : []}
506523
/>
507524
</Ref>
508525
)
@@ -576,13 +593,7 @@ class Dropdown extends AutoControlledComponent<Extendable<DropdownProps>, Dropdo
576593
}
577594

578595
private handleSearchQueryChange = (searchQuery: string) => {
579-
this.trySetState({ searchQuery })
580-
_.invoke(
581-
this.props,
582-
'onSearchQueryChange',
583-
{}, // we don't have event for it, but want to keep the event handling interface, event is empty.
584-
{ ...this.props, searchQuery },
585-
)
596+
this.setStateAndInvokeHandler({ searchQuery }, 'onSearchQueryChange')
586597
}
587598

588599
private handleDownshiftStateChanges = (
@@ -602,8 +613,8 @@ class Dropdown extends AutoControlledComponent<Extendable<DropdownProps>, Dropdo
602613
}
603614

604615
private handleStateChange = (changes: StateChangeOptions<ShorthandValue>) => {
605-
if (changes.isOpen !== undefined && changes.isOpen !== this.state.isOpen) {
606-
this.setState({ isOpen: changes.isOpen })
616+
if (changes.isOpen !== undefined && changes.isOpen !== this.state.open) {
617+
this.setStateAndInvokeHandler({ open: changes.isOpen }, 'onOpenChange')
607618
}
608619

609620
if (changes.isOpen && !this.props.search) {
@@ -664,19 +675,18 @@ class Dropdown extends AutoControlledComponent<Extendable<DropdownProps>, Dropdo
664675
item: ShorthandValue,
665676
rtl: boolean,
666677
) => ({
667-
onRemove: (e: React.SyntheticEvent, DropdownSelectedItemProps: DropdownSelectedItemProps) => {
668-
this.handleSelectedItemRemove(e, item, predefinedProps, DropdownSelectedItemProps)
678+
onRemove: (e: React.SyntheticEvent, dropdownSelectedItemProps: DropdownSelectedItemProps) => {
679+
this.handleSelectedItemRemove(e, item, predefinedProps, dropdownSelectedItemProps)
669680
},
670-
onClick: (e: React.SyntheticEvent, DropdownSelectedItemProps: DropdownSelectedItemProps) => {
681+
onClick: (e: React.SyntheticEvent, dropdownSelectedItemProps: DropdownSelectedItemProps) => {
671682
const { value } = this.state as { value: ShorthandCollection }
672-
this.trySetState({
673-
activeSelectedIndex: value.indexOf(item),
674-
})
683+
684+
this.trySetState({ activeSelectedIndex: value.indexOf(item) })
675685
e.stopPropagation()
676-
_.invoke(predefinedProps, 'onClick', e, DropdownSelectedItemProps)
686+
_.invoke(predefinedProps, 'onClick', e, dropdownSelectedItemProps)
677687
},
678-
onKeyDown: (e: React.SyntheticEvent, DropdownSelectedItemProps: DropdownSelectedItemProps) => {
679-
this.handleSelectedItemKeyDown(e, item, predefinedProps, DropdownSelectedItemProps, rtl)
688+
onKeyDown: (e: React.SyntheticEvent, dropdownSelectedItemProps: DropdownSelectedItemProps) => {
689+
this.handleSelectedItemKeyDown(e, item, predefinedProps, dropdownSelectedItemProps, rtl)
680690
},
681691
})
682692

@@ -846,12 +856,14 @@ class Dropdown extends AutoControlledComponent<Extendable<DropdownProps>, Dropdo
846856

847857
private handleSelectedChange = (item: ShorthandValue) => {
848858
const { items, multiple, getA11ySelectionMessage } = this.props
849-
const newState = {
850-
value: multiple ? [...(this.state.value as ShorthandCollection), item] : item,
851-
searchQuery: this.getSelectedItemAsString(item),
852-
}
853859

854-
this.trySetState(newState)
860+
this.setStateAndInvokeHandler(
861+
{
862+
value: multiple ? [...(this.state.value as ShorthandCollection), item] : item,
863+
searchQuery: this.getSelectedItemAsString(item),
864+
},
865+
'onSelectedChange',
866+
)
855867

856868
if (!multiple) {
857869
this.setState({ defaultHighlightedIndex: items.indexOf(item) })
@@ -870,9 +882,6 @@ class Dropdown extends AutoControlledComponent<Extendable<DropdownProps>, Dropdo
870882
}
871883

872884
this.tryFocusTriggerButton()
873-
874-
// we don't have event for it, but want to keep the event handling interface, event is empty.
875-
_.invoke(this.props, 'onSelectedChange', {}, { ...this.props, ...newState })
876885
}
877886

878887
private handleSelectedItemKeyDown(
@@ -896,21 +905,15 @@ class Dropdown extends AutoControlledComponent<Extendable<DropdownProps>, Dropdo
896905
break
897906
case previousKey:
898907
if (value.length > 0 && !_.isNil(activeSelectedIndex) && activeSelectedIndex > 0) {
899-
this.trySetState({
900-
activeSelectedIndex: activeSelectedIndex - 1,
901-
})
908+
this.trySetState({ activeSelectedIndex: activeSelectedIndex - 1 })
902909
}
903910
break
904911
case nextKey:
905912
if (value.length > 0 && !_.isNil(activeSelectedIndex)) {
906913
if (activeSelectedIndex < value.length - 1) {
907-
this.trySetState({
908-
activeSelectedIndex: activeSelectedIndex + 1,
909-
})
914+
this.trySetState({ activeSelectedIndex: activeSelectedIndex + 1 })
910915
} else {
911-
this.trySetState({
912-
activeSelectedIndex: null,
913-
})
916+
this.trySetState({ activeSelectedIndex: null })
914917
if (this.props.search) {
915918
e.preventDefault() // prevents caret to forward one position in input.
916919
this.inputRef.current.focus()
@@ -932,9 +935,7 @@ class Dropdown extends AutoControlledComponent<Extendable<DropdownProps>, Dropdo
932935
predefinedProps: DropdownSelectedItemProps,
933936
DropdownSelectedItemProps: DropdownSelectedItemProps,
934937
) {
935-
this.trySetState({
936-
activeSelectedIndex: null,
937-
})
938+
this.trySetState({ activeSelectedIndex: null })
938939
this.removeItemFromValue(item)
939940
this.tryFocusSearchInput()
940941
this.tryFocusTriggerButton()
@@ -953,14 +954,21 @@ class Dropdown extends AutoControlledComponent<Extendable<DropdownProps>, Dropdo
953954
poppedItem = value.pop()
954955
}
955956

956-
this.trySetState({ value })
957-
958957
if (getA11ySelectionMessage && getA11ySelectionMessage.onRemove) {
959958
this.setA11yStatus(getA11ySelectionMessage.onRemove(poppedItem))
960959
}
961960

962-
// we don't have event for it, but want to keep the event handling interface, event is empty.
963-
_.invoke(this.props, 'onSelectedChange', {}, { ...this.props, value })
961+
this.setStateAndInvokeHandler({ value }, 'onSelectedChange')
962+
}
963+
964+
/**
965+
* Calls trySetState (for autoControlledProps) and invokes event handler exposed to user.
966+
* We don't have the event object for most events coming from Downshift se we send an empty event
967+
* because we want to keep the event handling interface
968+
*/
969+
private setStateAndInvokeHandler = (newState: Partial<DropdownState>, eventName: string) => {
970+
this.trySetState(newState)
971+
_.invoke(this.props, eventName, {}, { ...this.props, ...newState })
964972
}
965973

966974
private tryFocusTriggerButton = () => {

packages/react/src/themes/teams/components/Dropdown/dropdownStyles.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ const dropdownStyles: ComponentSlotStylesInput<DropdownPropsAndState, DropdownVa
122122
width: getWidth(p, v),
123123
top: 'calc(100% + 2px)', // leave room for container + its border
124124
background: v.listBackgroundColor,
125-
...(p.isOpen && {
125+
...(p.open && {
126126
boxShadow: v.listBoxShadow,
127127
padding: v.listPadding,
128128
}),

0 commit comments

Comments
 (0)