From 431268ca09d7d181fbea8d583e39dbe7d6999f3a Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Mon, 10 Dec 2018 13:17:22 +0200 Subject: [PATCH 01/62] crude implementation --- ...opdownExampleSingleSelection.shorthand.tsx | 50 +++++++++++++++++++ .../components/Dropdown/Types/index.tsx | 5 ++ src/components/Dropdown/Dropdown.tsx | 48 ++++++++++++++---- .../components/Dropdown/dropdownStyles.ts | 16 ++++++ 4 files changed, 109 insertions(+), 10 deletions(-) create mode 100644 docs/src/examples/components/Dropdown/Types/DropdownExampleSingleSelection.shorthand.tsx diff --git a/docs/src/examples/components/Dropdown/Types/DropdownExampleSingleSelection.shorthand.tsx b/docs/src/examples/components/Dropdown/Types/DropdownExampleSingleSelection.shorthand.tsx new file mode 100644 index 0000000000..e6968e8313 --- /dev/null +++ b/docs/src/examples/components/Dropdown/Types/DropdownExampleSingleSelection.shorthand.tsx @@ -0,0 +1,50 @@ +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', +] + +class DropdownExample extends React.Component { + render() { + return ( + <Dropdown + getA11yStatusMessage={getA11yStatusMessage} + noResultsMessage="We couldn't find any matches." + placeholder="Start typing a name" + items={inputItems} + /> + ) + } +} + +const getA11yStatusMessage = ({ + isOpen, + itemToString, + previousResultCount, + resultCount, + selectedItem, +}) => { + if (!isOpen) { + return selectedItem ? itemToString(selectedItem) : '' + } + if (!resultCount) { + return 'No results are available.' + } + if (resultCount !== previousResultCount) { + return `${resultCount} result${ + resultCount === 1 ? ' is' : 's are' + } available, use up and down arrow keys to navigate. Press Enter key to select.` + } + return '' +} + +export default DropdownExample diff --git a/docs/src/examples/components/Dropdown/Types/index.tsx b/docs/src/examples/components/Dropdown/Types/index.tsx index 6a93a98f50..dc2a7499ef 100644 --- a/docs/src/examples/components/Dropdown/Types/index.tsx +++ b/docs/src/examples/components/Dropdown/Types/index.tsx @@ -4,6 +4,11 @@ import ExampleSection from 'docs/src/components/ComponentDoc/ExampleSection' const Types = () => ( <ExampleSection title="Types"> + <ComponentExample + title="Single Selection" + description="A dropdown with single selection." + examplePath="components/Dropdown/Types/DropdownExampleSingleSelection.shorthand" + /> <ComponentExample title="Multiple Search" description="A dropdown with multiple selection and search." diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index bb7aded10a..e302fb9a49 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -30,6 +30,7 @@ import DropdownItem, { DropdownItemProps } from './DropdownItem' import DropdownLabel, { DropdownLabelProps } from './DropdownLabel' import DropdownSearchInput from './DropdownSearchInput' import { DropdownSearchInputProps } from 'semantic-ui-react' +import Button from '../Button/Button' // TODO: To be replaced when Downshift will add highlightedItem in their interface. export interface A11yStatusMessageOptions<Item> extends DownshiftA11yStatusMessageOptions<Item> { @@ -210,11 +211,11 @@ export default class Dropdown extends AutoControlledComponent< <ElementType className={classes.root} {...rest}> <Downshift onChange={this.handleSelectedChange} - inputValue={searchQuery} + inputValue={search ? searchQuery : undefined} stateReducer={this.handleDownshiftStateChanges} itemToString={itemToString} // Downshift does not support multiple selection. We will handle everything and pass it selected as null in this case. - selectedItem={multiple ? null : undefined} + selectedItem={search && !multiple ? undefined : null} getA11yStatusMessage={getA11yStatusMessage} > {({ @@ -233,13 +234,14 @@ export default class Dropdown extends AutoControlledComponent< onClick={this.handleContainerClick.bind(this, isOpen)} > {multiple && this.renderSelectedItems(styles)} - {search && - this.renderSearchInput( - getRootProps, - getInputProps, - highlightedIndex, - selectItemAtIndex, - )} + {search + ? this.renderSearchInput( + getRootProps, + getInputProps, + highlightedIndex, + selectItemAtIndex, + ) + : this.renderTriggerButton(getToggleButtonProps, styles)} {toggleButton && this.renderToggleButton(getToggleButtonProps, styles, isOpen)} {this.renderItemsList( styles, @@ -257,6 +259,32 @@ export default class Dropdown extends AutoControlledComponent< ) } + private renderTriggerButton( + getToggleButtonProps: (options?: GetToggleButtonPropsOptions) => any, + styles: ComponentSlotStylesInput, + ): JSX.Element { + const { placeholder, itemToString } = this.props + const { value } = this.state + return ( + <Button + content={value ? itemToString(value) : placeholder} + fluid + styles={styles.button} + {...getToggleButtonProps({ + onFocus: () => { + this.setState({ focused: true }) + }, + onBlur: () => { + this.setState({ focused: false }) + }, + onClick: e => { + e.stopPropagation() + }, + })} + /> + ) + } + private renderSearchInput( getRootProps: (options?: GetMenuPropsOptions, otherOptions?: GetPropsCommonOptions) => any, getInputProps: (options?: GetInputPropsOptions) => any, @@ -611,7 +639,7 @@ export default class Dropdown extends AutoControlledComponent< value, }) if (getA11ySelectionMessage && getA11ySelectionMessage.onRemove) { - this.setA11yStatus(getA11ySelectionMessage.onRemove(item)) + this.setA11yStatus(getA11ySelectionMessage.onRemove(poppedItem)) } // we don't have event for it, but want to keep the event handling interface, event is empty. diff --git a/src/themes/teams/components/Dropdown/dropdownStyles.ts b/src/themes/teams/components/Dropdown/dropdownStyles.ts index 62436c7499..5a2120e940 100644 --- a/src/themes/teams/components/Dropdown/dropdownStyles.ts +++ b/src/themes/teams/components/Dropdown/dropdownStyles.ts @@ -37,6 +37,22 @@ const dropdownStyles: ComponentSlotStylesInput<DropdownProps, DropdownVariables> }), }), + button: (): ICSSInJSStyle => { + const transparentColorStyle = { + backgroundColor: 'transparent', + borderColor: 'transparent', + } + return { + boxShadow: '0 0 0 0', + margin: '0', + justifyContent: 'left', + ...transparentColorStyle, + ':hover': transparentColorStyle, + ':focus': transparentColorStyle, + ':active': transparentColorStyle, + } + }, + label: (): ICSSInJSStyle => ({ margin: '.4rem 0 0 .4rem', }), From 2ee1b5e3ec9431f0437ba9736f969c59b56853ff Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Tue, 11 Dec 2018 12:14:28 +0200 Subject: [PATCH 02/62] fixed the toggle and added/refactored styles --- ...opdownExampleSingleSelection.shorthand.tsx | 3 +- src/components/Dropdown/Dropdown.tsx | 80 ++++++++++--------- .../Dropdown/DropdownSearchInput.tsx | 44 +++++----- .../components/Dropdown/dropdownItemStyles.ts | 4 +- .../Dropdown/dropdownSearchInputStyles.ts | 7 +- .../components/Dropdown/dropdownStyles.ts | 38 +++++---- .../components/Dropdown/dropdownVariables.ts | 45 ++++++----- 7 files changed, 116 insertions(+), 105 deletions(-) diff --git a/docs/src/examples/components/Dropdown/Types/DropdownExampleSingleSelection.shorthand.tsx b/docs/src/examples/components/Dropdown/Types/DropdownExampleSingleSelection.shorthand.tsx index e6968e8313..1094c540a4 100644 --- a/docs/src/examples/components/Dropdown/Types/DropdownExampleSingleSelection.shorthand.tsx +++ b/docs/src/examples/components/Dropdown/Types/DropdownExampleSingleSelection.shorthand.tsx @@ -18,8 +18,7 @@ class DropdownExample extends React.Component { return ( <Dropdown getA11yStatusMessage={getA11yStatusMessage} - noResultsMessage="We couldn't find any matches." - placeholder="Start typing a name" + placeholder="Select your hero" items={inputItems} /> ) diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index e302fb9a49..ef90f67ca4 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -208,38 +208,46 @@ export default class Dropdown extends AutoControlledComponent< const { searchQuery } = this.state return ( - <ElementType className={classes.root} {...rest}> - <Downshift - onChange={this.handleSelectedChange} - inputValue={search ? searchQuery : undefined} - stateReducer={this.handleDownshiftStateChanges} - itemToString={itemToString} - // Downshift does not support multiple selection. We will handle everything and pass it selected as null in this case. - selectedItem={search && !multiple ? undefined : null} - getA11yStatusMessage={getA11yStatusMessage} - > - {({ - getInputProps, - getItemProps, - getMenuProps, - getRootProps, - getToggleButtonProps, - isOpen, - highlightedIndex, - selectItemAtIndex, - }) => { - return ( - <div - className={classes.containerDiv} - onClick={this.handleContainerClick.bind(this, isOpen)} + <Downshift + onChange={this.handleSelectedChange} + inputValue={search ? searchQuery : undefined} + stateReducer={this.handleDownshiftStateChanges} + itemToString={itemToString} + // Downshift does not support multiple selection. We will handle everything and pass it selected as null in this case. + selectedItem={search && !multiple ? undefined : null} + getA11yStatusMessage={getA11yStatusMessage} + > + {({ + getInputProps, + getItemProps, + getMenuProps, + getRootProps, + getToggleButtonProps, + isOpen, + highlightedIndex, + selectItemAtIndex, + }) => { + const accessibilityRootProps = getRootProps( + { refKey: 'innerRef' }, + { suppressRefError: true }, + ) + // const getExpandedRootProps = () => rootAccessibilityProps + const { innerRef, ...accessibilityRootPropsRest } = accessibilityRootProps + return ( + <Ref innerRef={innerRef}> + <ElementType + className={classes.root} + onClick={multiple ? this.handleContainerClick.bind(this, isOpen) : undefined} + {...rest} > {multiple && this.renderSelectedItems(styles)} {search ? this.renderSearchInput( - getRootProps, + accessibilityRootPropsRest, getInputProps, highlightedIndex, selectItemAtIndex, + variables, ) : this.renderTriggerButton(getToggleButtonProps, styles)} {toggleButton && this.renderToggleButton(getToggleButtonProps, styles, isOpen)} @@ -251,11 +259,11 @@ export default class Dropdown extends AutoControlledComponent< isOpen, highlightedIndex, )} - </div> - ) - }} - </Downshift> - </ElementType> + </ElementType> + </Ref> + ) + }} + </Downshift> ) } @@ -286,7 +294,7 @@ export default class Dropdown extends AutoControlledComponent< } private renderSearchInput( - getRootProps: (options?: GetMenuPropsOptions, otherOptions?: GetPropsCommonOptions) => any, + accessibilityComboboxProps: Object, getInputProps: (options?: GetInputPropsOptions) => any, highlightedIndex: number, selectItemAtIndex: ( @@ -294,6 +302,7 @@ export default class Dropdown extends AutoControlledComponent< otherStateToSet?: Partial<StateChangeOptions<any>>, cb?: () => void, ) => void, + variables, ): JSX.Element { const { searchInput, multiple, placeholder } = this.props const { searchQuery, value } = this.state @@ -304,6 +313,7 @@ export default class Dropdown extends AutoControlledComponent< return DropdownSearchInput.create(searchInput || {}, { defaultProps: { placeholder: noPlaceholder ? '' : placeholder, + variables, inputRef: (inputNode: HTMLElement) => { this.inputNode = inputNode }, @@ -313,7 +323,7 @@ export default class Dropdown extends AutoControlledComponent< predefinedProps, highlightedIndex, selectItemAtIndex, - getRootProps, + accessibilityComboboxProps, getInputProps, ), }) @@ -524,7 +534,7 @@ export default class Dropdown extends AutoControlledComponent< otherStateToSet?: Partial<StateChangeOptions<any>>, cb?: () => void, ) => void, - getRootProps: (options?: GetMenuPropsOptions, otherOptions?: GetPropsCommonOptions) => any, + accessibilityComboboxProps: Object, getInputProps: (options?: GetInputPropsOptions) => any, ) => { const handleInputBlur = ( @@ -564,9 +574,7 @@ export default class Dropdown extends AutoControlledComponent< }), }, // same story as above for getRootProps. - accessibilityWrapperProps: { - ...getRootProps({ refKey: 'innerRef' }, { suppressRefError: true }), - }, + accessibilityComboboxProps, onFocus: (e: React.SyntheticEvent, searchInputProps: DropdownSearchInputProps) => { this.setState({ focused: true }) diff --git a/src/components/Dropdown/DropdownSearchInput.tsx b/src/components/Dropdown/DropdownSearchInput.tsx index 63cec50f1d..6a4a4db94c 100644 --- a/src/components/Dropdown/DropdownSearchInput.tsx +++ b/src/components/Dropdown/DropdownSearchInput.tsx @@ -6,7 +6,6 @@ import { UIComponent, RenderResultConfig, createShorthandFactory, commonPropType import { ComponentEventHandler, ReactProps } from '../../../types/utils' import { UIComponentProps } from '../../lib/commonPropInterfaces' import Input from '../Input/Input' -import Ref from '../Ref/Ref' export interface DropdownSearchInputProps extends UIComponentProps<DropdownSearchInputProps> { /** @@ -68,7 +67,7 @@ class DropdownSearchInput extends UIComponent<ReactProps<DropdownSearchInputProp content: false, }), accessibilityInputProps: PropTypes.object, - accessibilityWrapperProps: PropTypes.object, + accessibilityComboboxProps: PropTypes.object, inputRef: PropTypes.func, onFocus: PropTypes.func, onInputBlur: PropTypes.func, @@ -98,29 +97,26 @@ class DropdownSearchInput extends UIComponent<ReactProps<DropdownSearchInputProp } public renderComponent({ rest, styles }: RenderResultConfig<DropdownSearchInputProps>) { - const { accessibilityWrapperProps, accessibilityInputProps, placeholder } = this.props - const { innerRef, ...accessibilityWrapperPropsRest } = accessibilityWrapperProps + const { accessibilityComboboxProps, accessibilityInputProps, placeholder } = this.props return ( - <Ref innerRef={innerRef}> - <Input - inputRef={this.handleInputRef} - onFocus={this.handleFocus} - onKeyUp={this.handleKeyUp} - wrapper={{ - styles: styles.wrapper, - ...accessibilityWrapperPropsRest, - }} - input={{ - type: 'text', - styles: styles.input, - placeholder, - onBlur: this.handleInputBlur, - onKeyDown: this.handleInputKeyDown, - ...accessibilityInputProps, - }} - {...rest} - /> - </Ref> + <Input + inputRef={this.handleInputRef} + onFocus={this.handleFocus} + onKeyUp={this.handleKeyUp} + wrapper={{ + styles: styles.combobox, + ...accessibilityComboboxProps, + }} + input={{ + type: 'text', + styles: styles.input, + placeholder, + onBlur: this.handleInputBlur, + onKeyDown: this.handleInputKeyDown, + ...accessibilityInputProps, + }} + {...rest} + /> ) } } diff --git a/src/themes/teams/components/Dropdown/dropdownItemStyles.ts b/src/themes/teams/components/Dropdown/dropdownItemStyles.ts index bf5347c4cd..4e28dbe216 100644 --- a/src/themes/teams/components/Dropdown/dropdownItemStyles.ts +++ b/src/themes/teams/components/Dropdown/dropdownItemStyles.ts @@ -9,8 +9,8 @@ const dropdownItemStyles: ComponentSlotStylesInput<DropdownItemProps, DropdownVa ...(active && { [`&.${ListItem.className}`]: { - backgroundColor: v.listItemHighlightedBackgroundColor, - color: v.listItemHighlightedTextColor, + backgroundColor: v.listItemBackgroundColorActive, + color: v.listItemColorActive, }, }), }), diff --git a/src/themes/teams/components/Dropdown/dropdownSearchInputStyles.ts b/src/themes/teams/components/Dropdown/dropdownSearchInputStyles.ts index 193a05a58c..e8a04e5c22 100644 --- a/src/themes/teams/components/Dropdown/dropdownSearchInputStyles.ts +++ b/src/themes/teams/components/Dropdown/dropdownSearchInputStyles.ts @@ -6,17 +6,18 @@ const dropdownSearchInputStyles: ComponentSlotStylesInput< DropdownSearchInputProps, DropdownVariables > = { - input: ({ variables: { backgroundColor } }): ICSSInJSStyle => ({ + input: ({ variables: { backgroundColor, comboboxPaddingInput } }): ICSSInJSStyle => ({ width: '100%', backgroundColor, + padding: comboboxPaddingInput, ':focus': { borderBottomColor: 'transparent', }, }), - wrapper: ({ variables: { editTextFlexBasis } }): ICSSInJSStyle => ({ - flexBasis: editTextFlexBasis, + combobox: ({ variables: { comboboxFlexBasis } }): ICSSInJSStyle => ({ + flexBasis: comboboxFlexBasis, flexGrow: 1, }), } diff --git a/src/themes/teams/components/Dropdown/dropdownStyles.ts b/src/themes/teams/components/Dropdown/dropdownStyles.ts index 5a2120e940..b16b484fe4 100644 --- a/src/themes/teams/components/Dropdown/dropdownStyles.ts +++ b/src/themes/teams/components/Dropdown/dropdownStyles.ts @@ -3,16 +3,16 @@ import { DropdownProps } from '../../../../components/Dropdown/Dropdown' import { DropdownVariables } from './dropdownVariables' const dropdownStyles: ComponentSlotStylesInput<DropdownProps, DropdownVariables> = { - containerDiv: ({ + root: ({ props: { focused, toggleButton, fluid }, variables: { backgroundColor, - containerDivBorderBottom, - containerDivBorderRadius, - containerDivBorderColor, - containerDivFocusBorderColor, - containerDivFocusBorderRadius, - containerDivColor, + borderBottom, + borderRadius, + borderColor, + borderColorFocus, + borderRadiusFocus, + color, toggleButtonSize, width, }, @@ -22,33 +22,39 @@ const dropdownStyles: ComponentSlotStylesInput<DropdownProps, DropdownVariables> outline: 0, border: 0, backgroundColor, - borderRadius: containerDivBorderRadius, - borderBottom: containerDivBorderBottom, - borderColor: containerDivBorderColor, - color: containerDivColor, + borderBottom, + borderColor, + borderRadius, + color, width: fluid ? '100%' : width, position: 'relative', ...(toggleButton && { paddingRight: toggleButtonSize, }), ...(focused && { - borderColor: containerDivFocusBorderColor, - borderRadius: containerDivFocusBorderRadius, + borderColor: borderColorFocus, + borderRadius: borderRadiusFocus, }), }), - button: (): ICSSInJSStyle => { + button: ({ variables: { comboboxPaddingButton } }): ICSSInJSStyle => { const transparentColorStyle = { backgroundColor: 'transparent', borderColor: 'transparent', } return { - boxShadow: '0 0 0 0', + boxShadow: 'none', margin: '0', justifyContent: 'left', + padding: comboboxPaddingButton, ...transparentColorStyle, ':hover': transparentColorStyle, - ':focus': transparentColorStyle, + ':focus': { + ...transparentColorStyle, + ':after': { + borderColor: 'transparent', + }, + }, ':active': transparentColorStyle, } }, diff --git a/src/themes/teams/components/Dropdown/dropdownVariables.ts b/src/themes/teams/components/Dropdown/dropdownVariables.ts index 32c59c7fd2..95319720d2 100644 --- a/src/themes/teams/components/Dropdown/dropdownVariables.ts +++ b/src/themes/teams/components/Dropdown/dropdownVariables.ts @@ -1,39 +1,40 @@ import { pxToRem } from '../../utils' export interface DropdownVariables { backgroundColor: string - containerDivBorderRadius: string - containerDivBorderBottom: string - containerDivBorderColor: string - containerDivColor: string - containerDivFocusBorderColor: string - containerDivFocusBorderRadius: string - editTextFlexBasis: string + borderBottom: string + borderColor: string + borderColorFocus: string + borderRadius: string + borderRadiusFocus: string + color: string + comboboxPaddingButton: string + comboboxPaddingInput: string + comboboxFlexBasis: string listItemBackgroundColor: string - listItemHighlightedBackgroundColor: string - listItemHighlightedTextColor: string + listItemBackgroundColorActive: string + listItemColorActive: string listMaxHeight: string toggleButtonSize: string width: string } -const [_2px_asRem, _3px_asRem] = [2, 3].map(v => pxToRem(v)) +const [_2px_asRem, _3px_asRem, _6px_asRem, _12px_asRem] = [2, 3, 6, 12].map(v => pxToRem(v)) export default (siteVars): DropdownVariables => ({ backgroundColor: siteVars.gray10, - - containerDivBorderRadius: _3px_asRem, - containerDivBorderBottom: `${_2px_asRem} solid transparent`, - containerDivBorderColor: 'transparent', - containerDivColor: siteVars.bodyColor, - containerDivFocusBorderColor: siteVars.brand, - containerDivFocusBorderRadius: `${_3px_asRem} ${_3px_asRem} ${_2px_asRem} ${_2px_asRem}`, - editTextFlexBasis: '100px', - + borderRadius: _3px_asRem, + borderBottom: `${_2px_asRem} solid transparent`, + borderColor: 'transparent', + borderColorFocus: siteVars.brand, + borderRadiusFocus: `${_3px_asRem} ${_3px_asRem} ${_2px_asRem} ${_2px_asRem}`, + color: siteVars.bodyColor, + comboboxPaddingButton: `0 ${_12px_asRem}`, + comboboxPaddingInput: `${_6px_asRem} ${_12px_asRem}`, + comboboxFlexBasis: '50px', listItemBackgroundColor: siteVars.white, - listItemHighlightedBackgroundColor: siteVars.brand, - listItemHighlightedTextColor: siteVars.white, + listItemBackgroundColorActive: siteVars.brand, + listItemColorActive: siteVars.white, listMaxHeight: '20rem', - toggleButtonSize: pxToRem(30), width: pxToRem(356), }) From b959dd990c412542c37cb8da997caa463f24569c Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Tue, 11 Dec 2018 13:17:36 +0200 Subject: [PATCH 03/62] fixed the filtering to work for all variants --- src/components/Dropdown/Dropdown.tsx | 42 +++++++++++++++------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index ef90f67ca4..22acc7cef6 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -231,7 +231,6 @@ export default class Dropdown extends AutoControlledComponent< { refKey: 'innerRef' }, { suppressRefError: true }, ) - // const getExpandedRootProps = () => rootAccessibilityProps const { innerRef, ...accessibilityRootPropsRest } = accessibilityRootProps return ( <Ref innerRef={innerRef}> @@ -374,8 +373,8 @@ export default class Dropdown extends AutoControlledComponent< getItemProps: (options: GetItemPropsOptions<ShorthandValue>) => any, highlightedIndex: number, ) { - const { items, noResultsMessage } = this.props - const filteredItems = this.getItemsFilteredBySearchQuery(items) + const { noResultsMessage } = this.props + const filteredItems = this.getFilteredItems() if (filteredItems.length > 0) { return filteredItems.map((item, index) => { @@ -457,25 +456,30 @@ export default class Dropdown extends AutoControlledComponent< } } - private getItemsFilteredBySearchQuery = (items: ShorthandValue[]): ShorthandValue[] => { - const { itemToString, multiple, search } = this.props + private getFilteredItems = (): ShorthandValue[] => { + const { items, itemToString, multiple, search } = this.props const { searchQuery, value } = this.state + let filteredItems = items - const nonSelectedItems = items.filter(item => - multiple ? (value as ShorthandValue[]).indexOf(item) === -1 : true, - ) - - const itemsMatchSearchQuery = nonSelectedItems.filter(item => - search - ? _.isFunction(search) - ? search(itemsMatchSearchQuery, searchQuery) - : itemToString(item) - .toLowerCase() - .indexOf(searchQuery.toLowerCase()) !== -1 - : true, - ) + if (!multiple && !search) { + return items.filter(item => item !== (value as ShorthandValue)) + } + if (multiple) { + filteredItems = filteredItems.filter(item => (value as ShorthandValue[]).indexOf(item) === -1) + } + if (search) { + if (_.isFunction(search)) { + return search(filteredItems, searchQuery) + } + return filteredItems.filter( + item => + itemToString(item) + .toLowerCase() + .indexOf(searchQuery.toLowerCase()) !== -1, + ) + } - return itemsMatchSearchQuery + return filteredItems } private setA11yStatus = (statusMessage: string) => { From c0482c54c0b5286e40f2ea531b5ae02321966f31 Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Tue, 11 Dec 2018 13:25:56 +0200 Subject: [PATCH 04/62] made the toggleButton tabbable by default --- src/components/Dropdown/Dropdown.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index 22acc7cef6..7a6ffe243d 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -337,7 +337,7 @@ export default class Dropdown extends AutoControlledComponent< <Icon name={`chevron ${isOpen ? 'up' : 'down'}`} as="button" - tabIndex="-1" + tabIndex="0" styles={styles.toggleButton} {...getToggleButtonProps()} /> From 5fe6ed74504e311d17cd655f09b6b8c8367aebf4 Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Tue, 11 Dec 2018 13:31:22 +0200 Subject: [PATCH 05/62] fixed style case active+focus --- src/themes/teams/components/Dropdown/dropdownStyles.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/themes/teams/components/Dropdown/dropdownStyles.ts b/src/themes/teams/components/Dropdown/dropdownStyles.ts index b16b484fe4..08b91d8ebf 100644 --- a/src/themes/teams/components/Dropdown/dropdownStyles.ts +++ b/src/themes/teams/components/Dropdown/dropdownStyles.ts @@ -54,6 +54,7 @@ const dropdownStyles: ComponentSlotStylesInput<DropdownProps, DropdownVariables> ':after': { borderColor: 'transparent', }, + ':active': transparentColorStyle, }, ':active': transparentColorStyle, } From 8a8882d6748e1390a93cdec61db977f3216e4b29 Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Tue, 11 Dec 2018 15:46:49 +0200 Subject: [PATCH 06/62] fixed wrong import --- src/components/Dropdown/Dropdown.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index 7a6ffe243d..564c11c38b 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -28,8 +28,7 @@ import Ref from '../Ref/Ref' import { UIComponentProps } from '../../lib/commonPropInterfaces' import DropdownItem, { DropdownItemProps } from './DropdownItem' import DropdownLabel, { DropdownLabelProps } from './DropdownLabel' -import DropdownSearchInput from './DropdownSearchInput' -import { DropdownSearchInputProps } from 'semantic-ui-react' +import DropdownSearchInput, { DropdownSearchInputProps } from './DropdownSearchInput' import Button from '../Button/Button' // TODO: To be replaced when Downshift will add highlightedItem in their interface. @@ -352,7 +351,7 @@ export default class Dropdown extends AutoControlledComponent< isOpen: boolean, highlightedIndex: number, ) { - const accessibilityMenuProps = getMenuProps({ refKey: 'innerRef' }) + const accessibilityMenuProps = getMenuProps({ refKey: 'innerRef' }, { suppressRefError: true }) const { innerRef, ...accessibilityMenuPropsRest } = accessibilityMenuProps return ( From 92ad9c0057d0dfac185c9b921766ef163c5fb43e Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Tue, 11 Dec 2018 17:09:29 +0200 Subject: [PATCH 07/62] fixed ariaLabel for trigger button --- src/components/Dropdown/Dropdown.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index 564c11c38b..ee465d6ba8 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -269,11 +269,12 @@ export default class Dropdown extends AutoControlledComponent< getToggleButtonProps: (options?: GetToggleButtonPropsOptions) => any, styles: ComponentSlotStylesInput, ): JSX.Element { - const { placeholder, itemToString } = this.props + const { placeholder, itemToString, multiple } = this.props const { value } = this.state + const content = multiple ? placeholder : value ? itemToString(value) : placeholder return ( <Button - content={value ? itemToString(value) : placeholder} + content={content} fluid styles={styles.button} {...getToggleButtonProps({ @@ -286,6 +287,7 @@ export default class Dropdown extends AutoControlledComponent< onClick: e => { e.stopPropagation() }, + 'aria-label': content, // TODO: add this to behaviour })} /> ) From 55e980740e237cc6f934adb7f2f3b9500a611912 Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Wed, 12 Dec 2018 18:14:05 +0200 Subject: [PATCH 08/62] added list focus and accessibility handling for it --- ...opdownExampleSingleSelection.shorthand.tsx | 3 + src/components/Dropdown/Dropdown.tsx | 121 ++++++++++++++---- 2 files changed, 99 insertions(+), 25 deletions(-) diff --git a/docs/src/examples/components/Dropdown/Types/DropdownExampleSingleSelection.shorthand.tsx b/docs/src/examples/components/Dropdown/Types/DropdownExampleSingleSelection.shorthand.tsx index 1094c540a4..1145fb1b6c 100644 --- a/docs/src/examples/components/Dropdown/Types/DropdownExampleSingleSelection.shorthand.tsx +++ b/docs/src/examples/components/Dropdown/Types/DropdownExampleSingleSelection.shorthand.tsx @@ -18,6 +18,9 @@ class DropdownExample extends React.Component { return ( <Dropdown getA11yStatusMessage={getA11yStatusMessage} + getA11ySelectionMessage={{ + onAdd: item => `${item} has been selected.`, + }} placeholder="Select your hero" items={inputItems} /> diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index ee465d6ba8..eb21b562d4 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -131,6 +131,8 @@ export default class Dropdown extends AutoControlledComponent< DropdownState > { private inputNode: HTMLElement + private listNode: HTMLElement + private buttonNode: HTMLElement static displayName = 'Dropdown' @@ -215,6 +217,11 @@ export default class Dropdown extends AutoControlledComponent< // Downshift does not support multiple selection. We will handle everything and pass it selected as null in this case. selectedItem={search && !multiple ? undefined : null} getA11yStatusMessage={getA11yStatusMessage} + onStateChange={changes => { + if (changes.isOpen && !search) { + this.listNode.focus() + } + }} > {({ getInputProps, @@ -223,6 +230,7 @@ export default class Dropdown extends AutoControlledComponent< getRootProps, getToggleButtonProps, isOpen, + toggleMenu, highlightedIndex, selectItemAtIndex, }) => { @@ -247,15 +255,18 @@ export default class Dropdown extends AutoControlledComponent< selectItemAtIndex, variables, ) - : this.renderTriggerButton(getToggleButtonProps, styles)} + : this.renderTriggerButton(styles, getToggleButtonProps)} {toggleButton && this.renderToggleButton(getToggleButtonProps, styles, isOpen)} {this.renderItemsList( styles, variables, - getMenuProps, - getItemProps, isOpen, highlightedIndex, + toggleMenu, + selectItemAtIndex, + getMenuProps, + getItemProps, + getInputProps, )} </ElementType> </Ref> @@ -266,30 +277,33 @@ export default class Dropdown extends AutoControlledComponent< } private renderTriggerButton( - getToggleButtonProps: (options?: GetToggleButtonPropsOptions) => any, styles: ComponentSlotStylesInput, + getToggleButtonProps: (options?: GetToggleButtonPropsOptions) => any, ): JSX.Element { const { placeholder, itemToString, multiple } = this.props const { value } = this.state const content = multiple ? placeholder : value ? itemToString(value) : placeholder return ( - <Button - content={content} - fluid - styles={styles.button} - {...getToggleButtonProps({ - onFocus: () => { - this.setState({ focused: true }) - }, - onBlur: () => { - this.setState({ focused: false }) - }, - onClick: e => { - e.stopPropagation() - }, - 'aria-label': content, // TODO: add this to behaviour - })} - /> + <Ref + innerRef={buttonNode => { + this.buttonNode = buttonNode + }} + > + <Button + content={content} + fluid + styles={styles.button} + {...getToggleButtonProps({ + onFocus: () => { + this.setState({ focused: true }) + }, + onBlur: () => { + this.setState({ focused: false }) + }, + 'aria-label': content, // TODO: add this to behaviour + })} + /> + </Ref> ) } @@ -348,19 +362,44 @@ export default class Dropdown extends AutoControlledComponent< private renderItemsList( styles: ComponentSlotStylesInput, variables: ComponentVariablesInput, - getMenuProps: (options?: GetMenuPropsOptions, otherOptions?: GetPropsCommonOptions) => any, - getItemProps: (options: GetItemPropsOptions<ShorthandValue>) => any, isOpen: boolean, highlightedIndex: number, + toggleMenu: () => void, + selectItemAtIndex: (index: number) => void, + getMenuProps: (options?: GetMenuPropsOptions, otherOptions?: GetPropsCommonOptions) => any, + getItemProps: (options: GetItemPropsOptions<ShorthandValue>) => any, + getInputProps: (options?: GetInputPropsOptions) => any, ) { - const accessibilityMenuProps = getMenuProps({ refKey: 'innerRef' }, { suppressRefError: true }) + const accessibilityMenuProps = { + ...getMenuProps({ refKey: 'innerRef' }, { suppressRefError: true }), + } + if (!this.props.search) { + const accessibilityInputProps = getInputProps() + accessibilityMenuProps['aria-activedescendant'] = + accessibilityInputProps['aria-activedescendant'] + accessibilityMenuProps['onKeyDown'] = e => { + this.handleListKeyDown( + e, + highlightedIndex, + accessibilityInputProps['onKeyDown'], + toggleMenu, + selectItemAtIndex, + ) + } + } const { innerRef, ...accessibilityMenuPropsRest } = accessibilityMenuProps return ( - <Ref innerRef={innerRef}> + <Ref + innerRef={(listNode: HTMLElement) => { + this.listNode = listNode + innerRef(listNode) + }} + > <List {...accessibilityMenuPropsRest} styles={styles.list} + tabIndex={-1} aria-hidden={!isOpen} items={isOpen ? this.renderItems(styles, variables, getItemProps, highlightedIndex) : []} /> @@ -452,6 +491,12 @@ export default class Dropdown extends AutoControlledComponent< { ...this.props, searchQuery: changes.inputValue }, ) return changes + case Downshift.stateChangeTypes.blurButton: + // Focus the list, by button click/enter/up/down/space. Downshift, by default, closes the list + // on trigger blur, but in this case it's custom behaviour, where we want to keep it open. + if (state.isOpen) { + return {} + } default: return changes } @@ -615,6 +660,32 @@ export default class Dropdown extends AutoControlledComponent< !isOpen && this.inputNode.focus() } + private handleListKeyDown = ( + e: React.SyntheticEvent, + highlightedIndex: number, + accessibilityInputPropsKeyDown: (e) => any, + toggleMenu: () => void, + selectItemAtIndex: (index: number) => void, + ) => { + switch (keyboardKey.getCode(e)) { + case keyboardKey.Tab: + if (_.isNil(highlightedIndex)) { + toggleMenu() + } else { + selectItemAtIndex(highlightedIndex) + } + return + case keyboardKey.Enter: + case keyboardKey.Escape: + accessibilityInputPropsKeyDown(e) + this.buttonNode.focus() + return + default: + accessibilityInputPropsKeyDown(e) + return + } + } + private handleSelectedChange = (item: ShorthandValue) => { const { multiple, getA11ySelectionMessage } = this.props const newValue = multiple ? [...(this.state.value as ShorthandValue[]), item] : item From 0f294b2451b596d6b709065ebf50f285b140ed0c Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Thu, 13 Dec 2018 10:14:21 +0200 Subject: [PATCH 09/62] fixed toggle button bug that appeared after trigger fix --- src/components/Dropdown/Dropdown.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index eb21b562d4..a0e850ee64 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -492,10 +492,10 @@ export default class Dropdown extends AutoControlledComponent< ) return changes case Downshift.stateChangeTypes.blurButton: - // Focus the list, by button click/enter/up/down/space. Downshift, by default, closes the list - // on trigger blur, but in this case it's custom behaviour, where we want to keep it open. - if (state.isOpen) { - return {} + // Downshift closes the list by default on trigger blur. It does not support the case when dropdown is + // single selection and focuses list on trigger click/up/down/space/enter. Treating that here. + if (state.isOpen && document.activeElement === this.listNode) { + return {} // won't change state in this case. } default: return changes @@ -599,7 +599,7 @@ export default class Dropdown extends AutoControlledComponent< e: React.SyntheticEvent, searchInputProps: DropdownSearchInputProps, ) => { - if (keyboardKey.getCode(e) === keyboardKey.Tab && highlightedIndex !== undefined) { + if (keyboardKey.getCode(e) === keyboardKey.Tab && _.isNil(highlightedIndex)) { selectItemAtIndex(highlightedIndex) } From 27396f122fdf19a422bc6e43f586ee130c86cd79 Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Thu, 13 Dec 2018 11:23:21 +0200 Subject: [PATCH 10/62] reverted change for filteredItems --- src/components/Dropdown/Dropdown.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index a0e850ee64..3b014c41bf 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -414,7 +414,7 @@ export default class Dropdown extends AutoControlledComponent< highlightedIndex: number, ) { const { noResultsMessage } = this.props - const filteredItems = this.getFilteredItems() + const filteredItems = this.getItemsFilteredBySearchQuery() if (filteredItems.length > 0) { return filteredItems.map((item, index) => { @@ -502,14 +502,11 @@ export default class Dropdown extends AutoControlledComponent< } } - private getFilteredItems = (): ShorthandValue[] => { + private getItemsFilteredBySearchQuery = (): ShorthandValue[] => { const { items, itemToString, multiple, search } = this.props const { searchQuery, value } = this.state let filteredItems = items - if (!multiple && !search) { - return items.filter(item => item !== (value as ShorthandValue)) - } if (multiple) { filteredItems = filteredItems.filter(item => (value as ShorthandValue[]).indexOf(item) === -1) } From 35d7e530c5a34ea22a1778d8523e0b083c0cc555 Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Thu, 13 Dec 2018 11:52:53 +0200 Subject: [PATCH 11/62] refactored the renderComponent to pass conformance --- src/components/Dropdown/Dropdown.tsx | 129 +++++++++--------- .../components/Dropdown/dropdownStyles.ts | 4 +- 2 files changed, 68 insertions(+), 65 deletions(-) diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index 3b014c41bf..7218a198b1 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -209,70 +209,71 @@ export default class Dropdown extends AutoControlledComponent< const { searchQuery } = this.state return ( - <Downshift - onChange={this.handleSelectedChange} - inputValue={search ? searchQuery : undefined} - stateReducer={this.handleDownshiftStateChanges} - itemToString={itemToString} - // Downshift does not support multiple selection. We will handle everything and pass it selected as null in this case. - selectedItem={search && !multiple ? undefined : null} - getA11yStatusMessage={getA11yStatusMessage} - onStateChange={changes => { - if (changes.isOpen && !search) { - this.listNode.focus() - } - }} - > - {({ - getInputProps, - getItemProps, - getMenuProps, - getRootProps, - getToggleButtonProps, - isOpen, - toggleMenu, - highlightedIndex, - selectItemAtIndex, - }) => { - const accessibilityRootProps = getRootProps( - { refKey: 'innerRef' }, - { suppressRefError: true }, - ) - const { innerRef, ...accessibilityRootPropsRest } = accessibilityRootProps - return ( - <Ref innerRef={innerRef}> - <ElementType - className={classes.root} - onClick={multiple ? this.handleContainerClick.bind(this, isOpen) : undefined} - {...rest} - > - {multiple && this.renderSelectedItems(styles)} - {search - ? this.renderSearchInput( - accessibilityRootPropsRest, - getInputProps, - highlightedIndex, - selectItemAtIndex, - variables, - ) - : this.renderTriggerButton(styles, getToggleButtonProps)} - {toggleButton && this.renderToggleButton(getToggleButtonProps, styles, isOpen)} - {this.renderItemsList( - styles, - variables, - isOpen, - highlightedIndex, - toggleMenu, - selectItemAtIndex, - getMenuProps, - getItemProps, - getInputProps, - )} - </ElementType> - </Ref> - ) - }} - </Downshift> + <ElementType className={classes.root} {...rest}> + <Downshift + onChange={this.handleSelectedChange} + inputValue={search ? searchQuery : undefined} + stateReducer={this.handleDownshiftStateChanges} + itemToString={itemToString} + // Downshift does not support multiple selection. We will handle everything and pass it selected as null in this case. + selectedItem={search && !multiple ? undefined : null} + getA11yStatusMessage={getA11yStatusMessage} + onStateChange={changes => { + if (changes.isOpen && !search) { + this.listNode.focus() + } + }} + > + {({ + getInputProps, + getItemProps, + getMenuProps, + getRootProps, + getToggleButtonProps, + isOpen, + toggleMenu, + highlightedIndex, + selectItemAtIndex, + }) => { + const accessibilityRootProps = getRootProps( + { refKey: 'innerRef' }, + { suppressRefError: true }, + ) + const { innerRef, ...accessibilityRootPropsRest } = accessibilityRootProps + return ( + <Ref innerRef={innerRef}> + <div + className={classes.container} + onClick={multiple ? this.handleContainerClick.bind(this, isOpen) : undefined} + > + {multiple && this.renderSelectedItems(styles)} + {search + ? this.renderSearchInput( + accessibilityRootPropsRest, + getInputProps, + highlightedIndex, + selectItemAtIndex, + variables, + ) + : this.renderTriggerButton(styles, getToggleButtonProps)} + {toggleButton && this.renderToggleButton(getToggleButtonProps, styles, isOpen)} + {this.renderItemsList( + styles, + variables, + isOpen, + highlightedIndex, + toggleMenu, + selectItemAtIndex, + getMenuProps, + getItemProps, + getInputProps, + )} + </div> + </Ref> + ) + }} + </Downshift> + </ElementType> ) } diff --git a/src/themes/teams/components/Dropdown/dropdownStyles.ts b/src/themes/teams/components/Dropdown/dropdownStyles.ts index 08b91d8ebf..81639105b9 100644 --- a/src/themes/teams/components/Dropdown/dropdownStyles.ts +++ b/src/themes/teams/components/Dropdown/dropdownStyles.ts @@ -3,7 +3,9 @@ import { DropdownProps } from '../../../../components/Dropdown/Dropdown' import { DropdownVariables } from './dropdownVariables' const dropdownStyles: ComponentSlotStylesInput<DropdownProps, DropdownVariables> = { - root: ({ + root: (): ICSSInJSStyle => ({}), + + container: ({ props: { focused, toggleButton, fluid }, variables: { backgroundColor, From fc64c6df648d7bb974d2addd55750a602f37dfb4 Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Thu, 13 Dec 2018 11:55:50 +0200 Subject: [PATCH 12/62] fixed button focus at selection --- src/components/Dropdown/Dropdown.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index 7218a198b1..ca43a881bf 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -673,7 +673,6 @@ export default class Dropdown extends AutoControlledComponent< selectItemAtIndex(highlightedIndex) } return - case keyboardKey.Enter: case keyboardKey.Escape: accessibilityInputPropsKeyDown(e) this.buttonNode.focus() @@ -685,7 +684,7 @@ export default class Dropdown extends AutoControlledComponent< } private handleSelectedChange = (item: ShorthandValue) => { - const { multiple, getA11ySelectionMessage } = this.props + const { multiple, getA11ySelectionMessage, search } = this.props const newValue = multiple ? [...(this.state.value as ShorthandValue[]), item] : item this.trySetState({ @@ -695,6 +694,9 @@ export default class Dropdown extends AutoControlledComponent< if (getA11ySelectionMessage && getA11ySelectionMessage.onAdd) { this.setA11yStatus(getA11ySelectionMessage.onAdd(item)) } + if (!search) { + this.buttonNode.focus() + } // 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: newValue }) From 8b24b0cb14525439cf35423ff372e4df1862b915 Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Thu, 13 Dec 2018 18:39:39 +0200 Subject: [PATCH 13/62] merged ref code review improvement --- src/components/Dropdown/Dropdown.tsx | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index ca43a881bf..2a01dc3538 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -132,7 +132,7 @@ export default class Dropdown extends AutoControlledComponent< > { private inputNode: HTMLElement private listNode: HTMLElement - private buttonNode: HTMLElement + private buttonRef = React.createRef<HTMLElement>() static displayName = 'Dropdown' @@ -285,11 +285,7 @@ export default class Dropdown extends AutoControlledComponent< const { value } = this.state const content = multiple ? placeholder : value ? itemToString(value) : placeholder return ( - <Ref - innerRef={buttonNode => { - this.buttonNode = buttonNode - }} - > + <Ref innerRef={this.buttonRef}> <Button content={content} fluid @@ -675,7 +671,7 @@ export default class Dropdown extends AutoControlledComponent< return case keyboardKey.Escape: accessibilityInputPropsKeyDown(e) - this.buttonNode.focus() + this.buttonRef.current.focus() return default: accessibilityInputPropsKeyDown(e) @@ -695,7 +691,7 @@ export default class Dropdown extends AutoControlledComponent< this.setA11yStatus(getA11ySelectionMessage.onAdd(item)) } if (!search) { - this.buttonNode.focus() + this.buttonRef.current.focus() } // we don't have event for it, but want to keep the event handling interface, event is empty. From a2b3424d236ecb3a7ed8f6e94ff26973d4fac205 Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Thu, 13 Dec 2018 19:17:00 +0200 Subject: [PATCH 14/62] removed unneeded code --- src/components/Dropdown/Dropdown.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index 2a01dc3538..bf88d1e7b5 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -415,13 +415,7 @@ export default class Dropdown extends AutoControlledComponent< if (filteredItems.length > 0) { return filteredItems.map((item, index) => { - let itemAsListItem = item - if (typeof item === 'object') { - itemAsListItem = _.pickBy(item, (value, key) => - _.includes(['key', ...DropdownItem.handledProps], key), - ) - } - return DropdownItem.create(itemAsListItem, { + return DropdownItem.create(item, { defaultProps: { active: highlightedIndex === index, variables, From bc88b891cab9b5581db93fbc53ed7de3a6c842a3 Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Fri, 14 Dec 2018 11:37:30 +0200 Subject: [PATCH 15/62] removed ternary condition --- src/components/Dropdown/Dropdown.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index bf88d1e7b5..0ce06179ff 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -283,7 +283,14 @@ export default class Dropdown extends AutoControlledComponent< ): JSX.Element { const { placeholder, itemToString, multiple } = this.props const { value } = this.state - const content = multiple ? placeholder : value ? itemToString(value) : placeholder + let content + + if (multiple) { + content = placeholder + } else { + content = value ? itemToString(value) : placeholder + } + return ( <Ref innerRef={this.buttonRef}> <Button From eabbbe10ece8a42c231424660908346514dacdc0 Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Wed, 2 Jan 2019 17:11:49 +0200 Subject: [PATCH 16/62] replaced some heights to make toggle button appear centered --- src/themes/teams/components/Dropdown/dropdownStyles.ts | 2 ++ src/themes/teams/components/Dropdown/dropdownVariables.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/themes/teams/components/Dropdown/dropdownStyles.ts b/src/themes/teams/components/Dropdown/dropdownStyles.ts index 81639105b9..446b22fb4a 100644 --- a/src/themes/teams/components/Dropdown/dropdownStyles.ts +++ b/src/themes/teams/components/Dropdown/dropdownStyles.ts @@ -1,6 +1,7 @@ import { ComponentSlotStylesInput, ICSSInJSStyle } from '../../../types' import { DropdownProps } from '../../../../components/Dropdown/Dropdown' import { DropdownVariables } from './dropdownVariables' +import { pxToRem } from '../../utils' const dropdownStyles: ComponentSlotStylesInput<DropdownProps, DropdownVariables> = { root: (): ICSSInJSStyle => ({}), @@ -50,6 +51,7 @@ const dropdownStyles: ComponentSlotStylesInput<DropdownProps, DropdownVariables> justifyContent: 'left', padding: comboboxPaddingButton, ...transparentColorStyle, + height: pxToRem(30), ':hover': transparentColorStyle, ':focus': { ...transparentColorStyle, diff --git a/src/themes/teams/components/Dropdown/dropdownVariables.ts b/src/themes/teams/components/Dropdown/dropdownVariables.ts index 95319720d2..d92e33e739 100644 --- a/src/themes/teams/components/Dropdown/dropdownVariables.ts +++ b/src/themes/teams/components/Dropdown/dropdownVariables.ts @@ -35,6 +35,6 @@ export default (siteVars): DropdownVariables => ({ listItemBackgroundColorActive: siteVars.brand, listItemColorActive: siteVars.white, listMaxHeight: '20rem', - toggleButtonSize: pxToRem(30), + toggleButtonSize: pxToRem(32), width: pxToRem(356), }) From 50700a40c2770648110ba23c3ee3203cc181dec0 Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Wed, 2 Jan 2019 19:05:39 +0200 Subject: [PATCH 17/62] improvements on styles from code review --- src/components/Dropdown/Dropdown.tsx | 14 ++++++++------ src/components/Dropdown/DropdownSearchInput.tsx | 3 +++ .../Dropdown/dropdownSearchInputStyles.ts | 8 +++++++- .../teams/components/Dropdown/dropdownStyles.ts | 11 +++++------ 4 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index 0ce06179ff..f3a8f9d6bc 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -304,7 +304,6 @@ export default class Dropdown extends AutoControlledComponent< onBlur: () => { this.setState({ focused: false }) }, - 'aria-label': content, // TODO: add this to behaviour })} /> </Ref> @@ -322,7 +321,7 @@ export default class Dropdown extends AutoControlledComponent< ) => void, variables, ): JSX.Element { - const { searchInput, multiple, placeholder } = this.props + const { searchInput, multiple, placeholder, toggleButton } = this.props const { searchQuery, value } = this.state const noPlaceholder = @@ -331,6 +330,7 @@ export default class Dropdown extends AutoControlledComponent< return DropdownSearchInput.create(searchInput || {}, { defaultProps: { placeholder: noPlaceholder ? '' : placeholder, + hasToggleButton: !!toggleButton, variables, inputRef: (inputNode: HTMLElement) => { this.inputNode = inputNode @@ -352,13 +352,15 @@ export default class Dropdown extends AutoControlledComponent< styles: ComponentSlotStylesInput, isOpen: boolean, ) { + const { onClick, onBlur, onKeyDown, onKeyUp } = getToggleButtonProps() return ( <Icon name={`chevron ${isOpen ? 'up' : 'down'}`} - as="button" - tabIndex="0" styles={styles.toggleButton} - {...getToggleButtonProps()} + onClick={onClick} + onBlur={onBlur} + onKeyDown={onKeyDown} + onKeyUp={onKeyUp} /> ) } @@ -594,7 +596,7 @@ export default class Dropdown extends AutoControlledComponent< e: React.SyntheticEvent, searchInputProps: DropdownSearchInputProps, ) => { - if (keyboardKey.getCode(e) === keyboardKey.Tab && _.isNil(highlightedIndex)) { + if (keyboardKey.getCode(e) === keyboardKey.Tab && !_.isNil(highlightedIndex)) { selectItemAtIndex(highlightedIndex) } diff --git a/src/components/Dropdown/DropdownSearchInput.tsx b/src/components/Dropdown/DropdownSearchInput.tsx index 6a4a4db94c..a6ed33276a 100644 --- a/src/components/Dropdown/DropdownSearchInput.tsx +++ b/src/components/Dropdown/DropdownSearchInput.tsx @@ -8,6 +8,8 @@ import { UIComponentProps } from '../../lib/commonPropInterfaces' import Input from '../Input/Input' export interface DropdownSearchInputProps extends UIComponentProps<DropdownSearchInputProps> { + /** Informs the search input about an existing toggle button. */ + hasToggleButton?: boolean /** * Ref callback with an input DOM node. * @@ -68,6 +70,7 @@ class DropdownSearchInput extends UIComponent<ReactProps<DropdownSearchInputProp }), accessibilityInputProps: PropTypes.object, accessibilityComboboxProps: PropTypes.object, + hasToggleButton: PropTypes.bool, inputRef: PropTypes.func, onFocus: PropTypes.func, onInputBlur: PropTypes.func, diff --git a/src/themes/teams/components/Dropdown/dropdownSearchInputStyles.ts b/src/themes/teams/components/Dropdown/dropdownSearchInputStyles.ts index e8a04e5c22..bd496b3eb2 100644 --- a/src/themes/teams/components/Dropdown/dropdownSearchInputStyles.ts +++ b/src/themes/teams/components/Dropdown/dropdownSearchInputStyles.ts @@ -16,9 +16,15 @@ const dropdownSearchInputStyles: ComponentSlotStylesInput< }, }), - combobox: ({ variables: { comboboxFlexBasis } }): ICSSInJSStyle => ({ + combobox: ({ + variables: { comboboxFlexBasis, toggleButtonSize }, + props: { hasToggleButton }, + }): ICSSInJSStyle => ({ flexBasis: comboboxFlexBasis, flexGrow: 1, + ...(hasToggleButton && { + paddingRight: toggleButtonSize, + }), }), } diff --git a/src/themes/teams/components/Dropdown/dropdownStyles.ts b/src/themes/teams/components/Dropdown/dropdownStyles.ts index 446b22fb4a..fbf29496cf 100644 --- a/src/themes/teams/components/Dropdown/dropdownStyles.ts +++ b/src/themes/teams/components/Dropdown/dropdownStyles.ts @@ -7,7 +7,7 @@ const dropdownStyles: ComponentSlotStylesInput<DropdownProps, DropdownVariables> root: (): ICSSInJSStyle => ({}), container: ({ - props: { focused, toggleButton, fluid }, + props: { focused, fluid }, variables: { backgroundColor, borderBottom, @@ -16,7 +16,6 @@ const dropdownStyles: ComponentSlotStylesInput<DropdownProps, DropdownVariables> borderColorFocus, borderRadiusFocus, color, - toggleButtonSize, width, }, }): ICSSInJSStyle => ({ @@ -31,9 +30,6 @@ const dropdownStyles: ComponentSlotStylesInput<DropdownProps, DropdownVariables> color, width: fluid ? '100%' : width, position: 'relative', - ...(toggleButton && { - paddingRight: toggleButtonSize, - }), ...(focused && { borderColor: borderColorFocus, borderRadius: borderRadiusFocus, @@ -85,9 +81,12 @@ const dropdownStyles: ComponentSlotStylesInput<DropdownProps, DropdownVariables> position: 'absolute', height: toggleButtonSize, width: toggleButtonSize, - border: 0, + cursor: 'pointer', backgroundColor: 'transparent', margin: 0, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', ...(fluid ? { right: 0 } : { left: `calc(${width} - ${toggleButtonSize})` }), }), } From 0d7a892b3ed7cd2a68742777a07d295e8494cc28 Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Thu, 3 Jan 2019 10:39:45 +0200 Subject: [PATCH 18/62] replaced Icon with unicode char for theme consistency --- src/components/Dropdown/Dropdown.tsx | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index f3a8f9d6bc..9a4753cb50 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -3,7 +3,11 @@ import * as PropTypes from 'prop-types' import * as _ from 'lodash' import { Extendable, ShorthandValue, ComponentEventHandler } from '../../../types/utils' -import { ComponentSlotStylesInput, ComponentVariablesInput } from '../../themes/types' +import { + ComponentSlotStylesInput, + ComponentVariablesInput, + ComponentSlotClasses, +} from '../../themes/types' import Downshift, { DownshiftState, StateChangeOptions, @@ -23,7 +27,6 @@ import { import keyboardKey from 'keyboard-key' import List from '../List/List' import Text from '../Text/Text' -import Icon from '../Icon/Icon' import Ref from '../Ref/Ref' import { UIComponentProps } from '../../lib/commonPropInterfaces' import DropdownItem, { DropdownItemProps } from './DropdownItem' @@ -256,7 +259,7 @@ export default class Dropdown extends AutoControlledComponent< variables, ) : this.renderTriggerButton(styles, getToggleButtonProps)} - {toggleButton && this.renderToggleButton(getToggleButtonProps, styles, isOpen)} + {toggleButton && this.renderToggleButton(getToggleButtonProps, classes, isOpen)} {this.renderItemsList( styles, variables, @@ -349,19 +352,14 @@ export default class Dropdown extends AutoControlledComponent< private renderToggleButton( getToggleButtonProps: (options?: GetToggleButtonPropsOptions) => any, - styles: ComponentSlotStylesInput, + classes: ComponentSlotClasses, isOpen: boolean, ) { - const { onClick, onBlur, onKeyDown, onKeyUp } = getToggleButtonProps() + const { onClick } = getToggleButtonProps() return ( - <Icon - name={`chevron ${isOpen ? 'up' : 'down'}`} - styles={styles.toggleButton} - onClick={onClick} - onBlur={onBlur} - onKeyDown={onKeyDown} - onKeyUp={onKeyUp} - /> + <span className={classes.toggleButton} onClick={onClick}> + {isOpen ? String.fromCharCode(9650) : String.fromCharCode(9660)} + </span> ) } From 1bdf7047961e72a8a546fd10e37460c6b750d388 Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Thu, 3 Jan 2019 11:58:50 +0200 Subject: [PATCH 19/62] some more review comments handled --- src/components/Dropdown/Dropdown.tsx | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index 9a4753cb50..d8a8395745 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -218,7 +218,8 @@ export default class Dropdown extends AutoControlledComponent< inputValue={search ? searchQuery : undefined} stateReducer={this.handleDownshiftStateChanges} itemToString={itemToString} - // Downshift does not support multiple selection. We will handle everything and pass it selected as null in this case. + // 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} getA11yStatusMessage={getA11yStatusMessage} onStateChange={changes => { @@ -286,13 +287,7 @@ export default class Dropdown extends AutoControlledComponent< ): JSX.Element { const { placeholder, itemToString, multiple } = this.props const { value } = this.state - let content - - if (multiple) { - content = placeholder - } else { - content = value ? itemToString(value) : placeholder - } + const content = value && !multiple ? itemToString(value) : placeholder return ( <Ref innerRef={this.buttonRef}> From 518dad57ef810368ba4c6572f3d034c2f2b11451 Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Thu, 3 Jan 2019 12:40:08 +0200 Subject: [PATCH 20/62] some more code improvements in Dropdown --- src/components/Dropdown/Dropdown.tsx | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index d8a8395745..83afe6b3c9 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -29,7 +29,7 @@ import List from '../List/List' import Text from '../Text/Text' import Ref from '../Ref/Ref' import { UIComponentProps } from '../../lib/commonPropInterfaces' -import DropdownItem, { DropdownItemProps } from './DropdownItem' +import DropdownItem from './DropdownItem' import DropdownLabel, { DropdownLabelProps } from './DropdownLabel' import DropdownSearchInput, { DropdownSearchInputProps } from './DropdownSearchInput' import Button from '../Button/Button' @@ -239,11 +239,10 @@ export default class Dropdown extends AutoControlledComponent< highlightedIndex, selectItemAtIndex, }) => { - const accessibilityRootProps = getRootProps( + const { innerRef, ...accessibilityRootPropsRest } = getRootProps( { refKey: 'innerRef' }, { suppressRefError: true }, ) - const { innerRef, ...accessibilityRootPropsRest } = accessibilityRootProps return ( <Ref innerRef={innerRef}> <div @@ -369,9 +368,8 @@ export default class Dropdown extends AutoControlledComponent< getItemProps: (options: GetItemPropsOptions<ShorthandValue>) => any, getInputProps: (options?: GetInputPropsOptions) => any, ) { - const accessibilityMenuProps = { - ...getMenuProps({ refKey: 'innerRef' }, { suppressRefError: true }), - } + const accessibilityMenuProps = getMenuProps({ refKey: 'innerRef' }, { suppressRefError: true }) + // If it's just a selection, some attributes and listeners from Downshift input need to go on the menu list. if (!this.props.search) { const accessibilityInputProps = getInputProps() accessibilityMenuProps['aria-activedescendant'] = @@ -387,7 +385,6 @@ export default class Dropdown extends AutoControlledComponent< } } const { innerRef, ...accessibilityMenuPropsRest } = accessibilityMenuProps - return ( <Ref innerRef={(listNode: HTMLElement) => { @@ -426,8 +423,7 @@ export default class Dropdown extends AutoControlledComponent< key: (item as any).header, }), }, - overrideProps: (predefinedProps: DropdownItemProps) => - this.handleItemOverrides(item, index, getItemProps), + overrideProps: () => this.handleItemOverrides(item, index, getItemProps), }) }) } From 3cdc8d8634ebb299ec9aa3e53f7807b5203c4ce2 Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Thu, 3 Jan 2019 12:40:28 +0200 Subject: [PATCH 21/62] replaced right margin with padding for combobox --- .../teams/components/Dropdown/dropdownSearchInputStyles.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/themes/teams/components/Dropdown/dropdownSearchInputStyles.ts b/src/themes/teams/components/Dropdown/dropdownSearchInputStyles.ts index bd496b3eb2..16c52581ff 100644 --- a/src/themes/teams/components/Dropdown/dropdownSearchInputStyles.ts +++ b/src/themes/teams/components/Dropdown/dropdownSearchInputStyles.ts @@ -23,7 +23,7 @@ const dropdownSearchInputStyles: ComponentSlotStylesInput< flexBasis: comboboxFlexBasis, flexGrow: 1, ...(hasToggleButton && { - paddingRight: toggleButtonSize, + marginRight: toggleButtonSize, }), }), } From bb670f98e67beccf3719a41e5dbb10e5692f6b77 Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Thu, 3 Jan 2019 12:59:42 +0200 Subject: [PATCH 22/62] used functional components in the examples --- ...ropdownExampleMultipleSearch.shorthand.tsx | 26 +++++++---------- ...opdownExampleSingleSelection.shorthand.tsx | 24 +++++++-------- ...wnExampleMultipleSearchFluid.shorthand.tsx | 29 ++++++++----------- ...ultipleSearchImageAndContent.shorthand.tsx | 26 +++++++---------- ...leMultipleSearchToggleButton.shorthand.tsx | 28 ++++++++---------- 5 files changed, 56 insertions(+), 77 deletions(-) diff --git a/docs/src/examples/components/Dropdown/Types/DropdownExampleMultipleSearch.shorthand.tsx b/docs/src/examples/components/Dropdown/Types/DropdownExampleMultipleSearch.shorthand.tsx index 4dc983cd0c..6aba57a1d9 100644 --- a/docs/src/examples/components/Dropdown/Types/DropdownExampleMultipleSearch.shorthand.tsx +++ b/docs/src/examples/components/Dropdown/Types/DropdownExampleMultipleSearch.shorthand.tsx @@ -13,21 +13,17 @@ const inputItems = [ 'Selina Kyle', ] -class DropdownExample extends React.Component { - render() { - return ( - <Dropdown - multiple - search - getA11ySelectionMessage={getA11ySelectionMessage} - getA11yStatusMessage={getA11yStatusMessage} - noResultsMessage="We couldn't find any matches." - placeholder="Start typing a name" - items={inputItems} - /> - ) - } -} +const DropdownExample = () => ( + <Dropdown + multiple + search + getA11ySelectionMessage={getA11ySelectionMessage} + getA11yStatusMessage={getA11yStatusMessage} + noResultsMessage="We couldn't find any matches." + placeholder="Start typing a name" + items={inputItems} + /> +) const getA11ySelectionMessage = { onAdd: item => `${item} has been selected.`, diff --git a/docs/src/examples/components/Dropdown/Types/DropdownExampleSingleSelection.shorthand.tsx b/docs/src/examples/components/Dropdown/Types/DropdownExampleSingleSelection.shorthand.tsx index 1145fb1b6c..c4f046d982 100644 --- a/docs/src/examples/components/Dropdown/Types/DropdownExampleSingleSelection.shorthand.tsx +++ b/docs/src/examples/components/Dropdown/Types/DropdownExampleSingleSelection.shorthand.tsx @@ -13,20 +13,16 @@ const inputItems = [ 'Selina Kyle', ] -class DropdownExample extends React.Component { - render() { - return ( - <Dropdown - getA11yStatusMessage={getA11yStatusMessage} - getA11ySelectionMessage={{ - onAdd: item => `${item} has been selected.`, - }} - placeholder="Select your hero" - items={inputItems} - /> - ) - } -} +const DropdownExample = () => ( + <Dropdown + getA11yStatusMessage={getA11yStatusMessage} + getA11ySelectionMessage={{ + onAdd: item => `${item} has been selected.`, + }} + placeholder="Select your hero" + items={inputItems} + /> +) const getA11yStatusMessage = ({ isOpen, diff --git a/docs/src/examples/components/Dropdown/Variations/DropdownExampleMultipleSearchFluid.shorthand.tsx b/docs/src/examples/components/Dropdown/Variations/DropdownExampleMultipleSearchFluid.shorthand.tsx index 7dd60f245a..5d1de96419 100644 --- a/docs/src/examples/components/Dropdown/Variations/DropdownExampleMultipleSearchFluid.shorthand.tsx +++ b/docs/src/examples/components/Dropdown/Variations/DropdownExampleMultipleSearchFluid.shorthand.tsx @@ -12,23 +12,18 @@ const inputItems = [ 'Peter Parker', 'Selina Kyle', ] - -class DropdownExample extends React.Component { - render() { - return ( - <Dropdown - multiple - getA11ySelectionMessage={getA11ySelectionMessage} - getA11yStatusMessage={getA11yStatusMessage} - noResultsMessage="We couldn't find any matches." - search - fluid - placeholder="Start typing a name" - items={inputItems} - /> - ) - } -} +const DropdownExample = () => ( + <Dropdown + multiple + getA11ySelectionMessage={getA11ySelectionMessage} + getA11yStatusMessage={getA11yStatusMessage} + noResultsMessage="We couldn't find any matches." + search + fluid + placeholder="Start typing a name" + items={inputItems} + /> +) const getA11ySelectionMessage = { onAdd: item => `${item} has been selected.`, diff --git a/docs/src/examples/components/Dropdown/Variations/DropdownExampleMultipleSearchImageAndContent.shorthand.tsx b/docs/src/examples/components/Dropdown/Variations/DropdownExampleMultipleSearchImageAndContent.shorthand.tsx index ed52d80f56..248055b67a 100644 --- a/docs/src/examples/components/Dropdown/Variations/DropdownExampleMultipleSearchImageAndContent.shorthand.tsx +++ b/docs/src/examples/components/Dropdown/Variations/DropdownExampleMultipleSearchImageAndContent.shorthand.tsx @@ -49,21 +49,17 @@ const inputItems = [ }, ] -class DropdownExample extends React.Component { - render() { - return ( - <Dropdown - multiple - getA11yStatusMessage={getA11yStatusMessage} - search - getA11ySelectionMessage={getA11ySelectionMessage} - noResultsMessage="We couldn't find any matches." - placeholder="Start typing a name" - items={inputItems} - /> - ) - } -} +const DropdownExample = () => ( + <Dropdown + multiple + getA11yStatusMessage={getA11yStatusMessage} + search + getA11ySelectionMessage={getA11ySelectionMessage} + noResultsMessage="We couldn't find any matches." + placeholder="Start typing a name" + items={inputItems} + /> +) const getA11ySelectionMessage = { onAdd: item => `${item.header} has been selected.`, diff --git a/docs/src/examples/components/Dropdown/Variations/DropdownExampleMultipleSearchToggleButton.shorthand.tsx b/docs/src/examples/components/Dropdown/Variations/DropdownExampleMultipleSearchToggleButton.shorthand.tsx index 1f85c9cc1b..3fb6470421 100644 --- a/docs/src/examples/components/Dropdown/Variations/DropdownExampleMultipleSearchToggleButton.shorthand.tsx +++ b/docs/src/examples/components/Dropdown/Variations/DropdownExampleMultipleSearchToggleButton.shorthand.tsx @@ -13,22 +13,18 @@ const inputItems = [ 'Selina Kyle', ] -class DropdownExample extends React.Component { - render() { - return ( - <Dropdown - multiple - getA11yStatusMessage={getA11yStatusMessage} - getA11ySelectionMessage={getA11ySelectionMessage} - noResultsMessage="We couldn't find any matches." - search - placeholder="Start typing a name" - toggleButton - items={inputItems} - /> - ) - } -} +const DropdownExample = () => ( + <Dropdown + multiple + getA11yStatusMessage={getA11yStatusMessage} + getA11ySelectionMessage={getA11ySelectionMessage} + noResultsMessage="We couldn't find any matches." + search + placeholder="Start typing a name" + toggleButton + items={inputItems} + /> +) const getA11ySelectionMessage = { onAdd: item => `${item} has been selected.`, From c851c132061f779b5ec3385c5c0743d015660b11 Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Thu, 3 Jan 2019 13:11:15 +0200 Subject: [PATCH 23/62] overriding downshift aria label for the toggle button --- src/components/Dropdown/Dropdown.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index 83afe6b3c9..1ac2100cae 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -301,6 +301,7 @@ export default class Dropdown extends AutoControlledComponent< onBlur: () => { this.setState({ focused: false }) }, + 'aria-label': content, })} /> </Ref> From 58c41a8ead6d83191832f11229522faa6ea23782 Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Thu, 3 Jan 2019 13:21:45 +0200 Subject: [PATCH 24/62] made the toggle button arrow non-selectable --- src/themes/teams/components/Dropdown/dropdownStyles.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/themes/teams/components/Dropdown/dropdownStyles.ts b/src/themes/teams/components/Dropdown/dropdownStyles.ts index fbf29496cf..a69d19057d 100644 --- a/src/themes/teams/components/Dropdown/dropdownStyles.ts +++ b/src/themes/teams/components/Dropdown/dropdownStyles.ts @@ -87,6 +87,7 @@ const dropdownStyles: ComponentSlotStylesInput<DropdownProps, DropdownVariables> display: 'flex', justifyContent: 'center', alignItems: 'center', + userSelect: 'none', ...(fluid ? { right: 0 } : { left: `calc(${width} - ${toggleButtonSize})` }), }), } From 96a61ab708f3aaeae5b1b2ec8481e3b2f63f37ee Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Thu, 3 Jan 2019 15:29:51 +0200 Subject: [PATCH 25/62] used a lodash util instead of filter --- src/components/Dropdown/Dropdown.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index 1ac2100cae..46dfe032b6 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -498,7 +498,7 @@ export default class Dropdown extends AutoControlledComponent< let filteredItems = items if (multiple) { - filteredItems = filteredItems.filter(item => (value as ShorthandValue[]).indexOf(item) === -1) + filteredItems = _.difference(filteredItems, value as ShorthandValue[]) } if (search) { if (_.isFunction(search)) { From 97758c282ca0a694a08a7a913e2bdfe608950f95 Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Fri, 4 Jan 2019 10:59:52 +0200 Subject: [PATCH 26/62] used buttonNode for consistency --- src/components/Dropdown/Dropdown.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index 46dfe032b6..6acfd8c61c 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -133,9 +133,9 @@ export default class Dropdown extends AutoControlledComponent< Extendable<DropdownProps>, DropdownState > { + private buttonNode: HTMLElement private inputNode: HTMLElement private listNode: HTMLElement - private buttonRef = React.createRef<HTMLElement>() static displayName = 'Dropdown' @@ -289,7 +289,11 @@ export default class Dropdown extends AutoControlledComponent< const content = value && !multiple ? itemToString(value) : placeholder return ( - <Ref innerRef={this.buttonRef}> + <Ref + innerRef={(buttonNode: HTMLElement) => { + this.buttonNode = buttonNode + }} + > <Button content={content} fluid @@ -664,7 +668,7 @@ export default class Dropdown extends AutoControlledComponent< return case keyboardKey.Escape: accessibilityInputPropsKeyDown(e) - this.buttonRef.current.focus() + this.buttonNode.focus() return default: accessibilityInputPropsKeyDown(e) @@ -684,7 +688,7 @@ export default class Dropdown extends AutoControlledComponent< this.setA11yStatus(getA11ySelectionMessage.onAdd(item)) } if (!search) { - this.buttonRef.current.focus() + this.buttonNode.focus() } // we don't have event for it, but want to keep the event handling interface, event is empty. From c28f7c57cbdb5f7bd53ff175be20098e85c473cb Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Fri, 4 Jan 2019 11:22:35 +0200 Subject: [PATCH 27/62] apply tabIndex only if dropdown is non-search --- src/components/Dropdown/Dropdown.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index 6acfd8c61c..7c8843bda4 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -374,8 +374,9 @@ export default class Dropdown extends AutoControlledComponent< getInputProps: (options?: GetInputPropsOptions) => any, ) { const accessibilityMenuProps = getMenuProps({ refKey: 'innerRef' }, { suppressRefError: true }) + const { search } = this.props // If it's just a selection, some attributes and listeners from Downshift input need to go on the menu list. - if (!this.props.search) { + if (!search) { const accessibilityInputProps = getInputProps() accessibilityMenuProps['aria-activedescendant'] = accessibilityInputProps['aria-activedescendant'] @@ -400,7 +401,7 @@ export default class Dropdown extends AutoControlledComponent< <List {...accessibilityMenuPropsRest} styles={styles.list} - tabIndex={-1} + tabIndex={search ? undefined : -1} // needs to be focused when trigger button is activated. aria-hidden={!isOpen} items={isOpen ? this.renderItems(styles, variables, getItemProps, highlightedIndex) : []} /> From 1563a6551ed04f116c78d0d649ebc39b66546e1e Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Fri, 4 Jan 2019 11:23:19 +0200 Subject: [PATCH 28/62] Revert "used buttonNode for consistency" This reverts commit 97758c282ca0a694a08a7a913e2bdfe608950f95. --- src/components/Dropdown/Dropdown.tsx | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index 7c8843bda4..83d6967feb 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -133,9 +133,9 @@ export default class Dropdown extends AutoControlledComponent< Extendable<DropdownProps>, DropdownState > { - private buttonNode: HTMLElement private inputNode: HTMLElement private listNode: HTMLElement + private buttonRef = React.createRef<HTMLElement>() static displayName = 'Dropdown' @@ -289,11 +289,7 @@ export default class Dropdown extends AutoControlledComponent< const content = value && !multiple ? itemToString(value) : placeholder return ( - <Ref - innerRef={(buttonNode: HTMLElement) => { - this.buttonNode = buttonNode - }} - > + <Ref innerRef={this.buttonRef}> <Button content={content} fluid @@ -669,7 +665,7 @@ export default class Dropdown extends AutoControlledComponent< return case keyboardKey.Escape: accessibilityInputPropsKeyDown(e) - this.buttonNode.focus() + this.buttonRef.current.focus() return default: accessibilityInputPropsKeyDown(e) @@ -689,7 +685,7 @@ export default class Dropdown extends AutoControlledComponent< this.setA11yStatus(getA11ySelectionMessage.onAdd(item)) } if (!search) { - this.buttonNode.focus() + this.buttonRef.current.focus() } // we don't have event for it, but want to keep the event handling interface, event is empty. From 5e4dc81e993af85bd8e91c0f2a755cf2aec6d79d Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Fri, 4 Jan 2019 12:51:59 +0200 Subject: [PATCH 29/62] refactored the Refs use in Dropdown --- src/components/Dropdown/Dropdown.tsx | 23 +++++++------- .../Dropdown/DropdownSearchInput.tsx | 24 +++++++-------- src/components/Input/Input.tsx | 30 ++++++++----------- 3 files changed, 35 insertions(+), 42 deletions(-) diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index 83d6967feb..98b3183ffc 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -23,6 +23,7 @@ import { RenderResultConfig, customPropTypes, commonPropTypes, + handleRef, } from '../../lib' import keyboardKey from 'keyboard-key' import List from '../List/List' @@ -133,9 +134,9 @@ export default class Dropdown extends AutoControlledComponent< Extendable<DropdownProps>, DropdownState > { - private inputNode: HTMLElement - private listNode: HTMLElement private buttonRef = React.createRef<HTMLElement>() + private inputRef = React.createRef<HTMLElement>() + private listRef = React.createRef<HTMLElement>() static displayName = 'Dropdown' @@ -224,7 +225,7 @@ export default class Dropdown extends AutoControlledComponent< getA11yStatusMessage={getA11yStatusMessage} onStateChange={changes => { if (changes.isOpen && !search) { - this.listNode.focus() + this.listRef.current.focus() } }} > @@ -330,9 +331,7 @@ export default class Dropdown extends AutoControlledComponent< placeholder: noPlaceholder ? '' : placeholder, hasToggleButton: !!toggleButton, variables, - inputRef: (inputNode: HTMLElement) => { - this.inputNode = inputNode - }, + inputRef: this.inputRef, }, overrideProps: (predefinedProps: DropdownSearchInputProps) => this.handleSearchInputOverrides( @@ -389,9 +388,9 @@ export default class Dropdown extends AutoControlledComponent< const { innerRef, ...accessibilityMenuPropsRest } = accessibilityMenuProps return ( <Ref - innerRef={(listNode: HTMLElement) => { - this.listNode = listNode - innerRef(listNode) + innerRef={(listElement: HTMLElement) => { + handleRef(this.listRef, listElement) + handleRef(innerRef, listElement) }} > <List @@ -485,7 +484,7 @@ export default class Dropdown extends AutoControlledComponent< case Downshift.stateChangeTypes.blurButton: // Downshift closes the list by default on trigger blur. It does not support the case when dropdown is // single selection and focuses list on trigger click/up/down/space/enter. Treating that here. - if (state.isOpen && document.activeElement === this.listNode) { + if (state.isOpen && document.activeElement === this.listRef.current) { return {} // won't change state in this case. } default: @@ -645,7 +644,7 @@ export default class Dropdown extends AutoControlledComponent< } private handleContainerClick = (isOpen: boolean) => { - !isOpen && this.inputNode.focus() + !isOpen && this.inputRef.current.focus() } private handleListKeyDown = ( @@ -694,7 +693,7 @@ export default class Dropdown extends AutoControlledComponent< private handleSelectedItemRemove(e: React.SyntheticEvent, item: ShorthandValue) { this.removeItemFromValue(item) - this.inputNode.focus() + this.inputRef.current.focus() e.stopPropagation() } diff --git a/src/components/Dropdown/DropdownSearchInput.tsx b/src/components/Dropdown/DropdownSearchInput.tsx index a6ed33276a..5d967a245d 100644 --- a/src/components/Dropdown/DropdownSearchInput.tsx +++ b/src/components/Dropdown/DropdownSearchInput.tsx @@ -10,12 +10,9 @@ import Input from '../Input/Input' export interface DropdownSearchInputProps extends UIComponentProps<DropdownSearchInputProps> { /** Informs the search input about an existing toggle button. */ hasToggleButton?: boolean - /** - * Ref callback with an input DOM node. - * - * @param {JSX.Element} node - input DOM node. - */ - inputRef?: (inputNode: HTMLElement) => void + + /** Ref for input DOM node. */ + inputRef?: React.Ref<HTMLElement> /** * Called on input element focus. @@ -71,7 +68,7 @@ class DropdownSearchInput extends UIComponent<ReactProps<DropdownSearchInputProp accessibilityInputProps: PropTypes.object, accessibilityComboboxProps: PropTypes.object, hasToggleButton: PropTypes.bool, - inputRef: PropTypes.func, + inputRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), onFocus: PropTypes.func, onInputBlur: PropTypes.func, onInputKeyDown: PropTypes.func, @@ -79,10 +76,6 @@ class DropdownSearchInput extends UIComponent<ReactProps<DropdownSearchInputProp placeholder: PropTypes.string, } - private handleInputRef = (inputNode: HTMLElement) => { - _.invoke(this.props, 'inputRef', inputNode) - } - private handleFocus = (e: React.SyntheticEvent) => { _.invoke(this.props, 'onFocus', e, this.props) } @@ -100,10 +93,15 @@ class DropdownSearchInput extends UIComponent<ReactProps<DropdownSearchInputProp } public renderComponent({ rest, styles }: RenderResultConfig<DropdownSearchInputProps>) { - const { accessibilityComboboxProps, accessibilityInputProps, placeholder } = this.props + const { + accessibilityComboboxProps, + accessibilityInputProps, + inputRef, + placeholder, + } = this.props return ( <Input - inputRef={this.handleInputRef} + inputRef={inputRef} onFocus={this.handleFocus} onKeyUp={this.handleKeyUp} wrapper={{ diff --git a/src/components/Input/Input.tsx b/src/components/Input/Input.tsx index 8eee55fa30..73da8e5f84 100644 --- a/src/components/Input/Input.tsx +++ b/src/components/Input/Input.tsx @@ -11,6 +11,7 @@ import { UIComponentProps, ChildrenComponentProps, commonPropTypes, + handleRef, } from '../../lib' import { ReactProps, ShorthandValue, ComponentEventHandler } from '../../../types/utils' import Icon from '../Icon/Icon' @@ -50,12 +51,8 @@ export interface InputProps extends UIComponentProps, ChildrenComponentProps { /** The HTML input type. */ type?: string - /** - * Ref callback with an input DOM node. - * - * @param {JSX.Element} node - input DOM node. - */ - inputRef?: (node: HTMLElement) => void + /** Ref for input DOM node. */ + inputRef?: React.Ref<HTMLElement> /** The value of the input. */ value?: React.ReactText @@ -77,7 +74,7 @@ export interface InputState { * - if input is search, then use "role='search'" */ class Input extends AutoControlledComponent<ReactProps<InputProps>, InputState> { - private inputDomElement: HTMLInputElement + private inputRef = React.createRef<HTMLElement>() static className = 'ui-input' @@ -93,7 +90,7 @@ class Input extends AutoControlledComponent<ReactProps<InputProps>, InputState> icon: customPropTypes.itemShorthand, iconPosition: PropTypes.oneOf(['start', 'end']), input: customPropTypes.itemShorthand, - inputRef: PropTypes.func, + inputRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), inline: PropTypes.bool, onChange: PropTypes.func, type: PropTypes.string, @@ -116,7 +113,7 @@ class Input extends AutoControlledComponent<ReactProps<InputProps>, InputState> styles, variables, }: RenderResultConfig<InputProps>) { - const { className, input, type, wrapper } = this.props + const { className, input, inputRef, type, wrapper } = this.props const { value = '' } = this.state const [htmlInputProps, rest] = partitionHTMLProps(restProps) @@ -125,7 +122,12 @@ class Input extends AutoControlledComponent<ReactProps<InputProps>, InputState> className: cx(Input.className, className), children: ( <> - <Ref innerRef={this.handleInputRef}> + <Ref + innerRef={(inputElement: HTMLElement) => { + handleRef(this.inputRef, inputElement) + handleRef(inputRef, inputElement) + }} + > {Slot.create(input || type, { defaultProps: { ...htmlInputProps, @@ -155,16 +157,10 @@ class Input extends AutoControlledComponent<ReactProps<InputProps>, InputState> }) } - private handleInputRef = (inputNode: HTMLElement) => { - this.inputDomElement = inputNode as HTMLInputElement - - _.invoke(this.props, 'inputRef', inputNode) - } - private handleIconOverrides = predefinedProps => ({ onClick: (e: React.SyntheticEvent) => { this.handleOnClear() - this.inputDomElement.focus() + this.inputRef.current.focus() _.invoke(predefinedProps, 'onClick', e, this.props) }, ...(predefinedProps.onClick && { tabIndex: '0' }), From 14532ef60093b65e9d8958f5130311969d11981d Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Mon, 10 Dec 2018 13:17:22 +0200 Subject: [PATCH 30/62] crude implementation --- ...opdownExampleSingleSelection.shorthand.tsx | 50 +++++++++++++++++++ .../components/Dropdown/Types/index.tsx | 5 ++ src/components/Dropdown/Dropdown.tsx | 48 ++++++++++++++---- .../components/Dropdown/dropdownStyles.ts | 16 ++++++ 4 files changed, 109 insertions(+), 10 deletions(-) create mode 100644 docs/src/examples/components/Dropdown/Types/DropdownExampleSingleSelection.shorthand.tsx diff --git a/docs/src/examples/components/Dropdown/Types/DropdownExampleSingleSelection.shorthand.tsx b/docs/src/examples/components/Dropdown/Types/DropdownExampleSingleSelection.shorthand.tsx new file mode 100644 index 0000000000..e6968e8313 --- /dev/null +++ b/docs/src/examples/components/Dropdown/Types/DropdownExampleSingleSelection.shorthand.tsx @@ -0,0 +1,50 @@ +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', +] + +class DropdownExample extends React.Component { + render() { + return ( + <Dropdown + getA11yStatusMessage={getA11yStatusMessage} + noResultsMessage="We couldn't find any matches." + placeholder="Start typing a name" + items={inputItems} + /> + ) + } +} + +const getA11yStatusMessage = ({ + isOpen, + itemToString, + previousResultCount, + resultCount, + selectedItem, +}) => { + if (!isOpen) { + return selectedItem ? itemToString(selectedItem) : '' + } + if (!resultCount) { + return 'No results are available.' + } + if (resultCount !== previousResultCount) { + return `${resultCount} result${ + resultCount === 1 ? ' is' : 's are' + } available, use up and down arrow keys to navigate. Press Enter key to select.` + } + return '' +} + +export default DropdownExample diff --git a/docs/src/examples/components/Dropdown/Types/index.tsx b/docs/src/examples/components/Dropdown/Types/index.tsx index 6a93a98f50..dc2a7499ef 100644 --- a/docs/src/examples/components/Dropdown/Types/index.tsx +++ b/docs/src/examples/components/Dropdown/Types/index.tsx @@ -4,6 +4,11 @@ import ExampleSection from 'docs/src/components/ComponentDoc/ExampleSection' const Types = () => ( <ExampleSection title="Types"> + <ComponentExample + title="Single Selection" + description="A dropdown with single selection." + examplePath="components/Dropdown/Types/DropdownExampleSingleSelection.shorthand" + /> <ComponentExample title="Multiple Search" description="A dropdown with multiple selection and search." diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index bb7aded10a..e302fb9a49 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -30,6 +30,7 @@ import DropdownItem, { DropdownItemProps } from './DropdownItem' import DropdownLabel, { DropdownLabelProps } from './DropdownLabel' import DropdownSearchInput from './DropdownSearchInput' import { DropdownSearchInputProps } from 'semantic-ui-react' +import Button from '../Button/Button' // TODO: To be replaced when Downshift will add highlightedItem in their interface. export interface A11yStatusMessageOptions<Item> extends DownshiftA11yStatusMessageOptions<Item> { @@ -210,11 +211,11 @@ export default class Dropdown extends AutoControlledComponent< <ElementType className={classes.root} {...rest}> <Downshift onChange={this.handleSelectedChange} - inputValue={searchQuery} + inputValue={search ? searchQuery : undefined} stateReducer={this.handleDownshiftStateChanges} itemToString={itemToString} // Downshift does not support multiple selection. We will handle everything and pass it selected as null in this case. - selectedItem={multiple ? null : undefined} + selectedItem={search && !multiple ? undefined : null} getA11yStatusMessage={getA11yStatusMessage} > {({ @@ -233,13 +234,14 @@ export default class Dropdown extends AutoControlledComponent< onClick={this.handleContainerClick.bind(this, isOpen)} > {multiple && this.renderSelectedItems(styles)} - {search && - this.renderSearchInput( - getRootProps, - getInputProps, - highlightedIndex, - selectItemAtIndex, - )} + {search + ? this.renderSearchInput( + getRootProps, + getInputProps, + highlightedIndex, + selectItemAtIndex, + ) + : this.renderTriggerButton(getToggleButtonProps, styles)} {toggleButton && this.renderToggleButton(getToggleButtonProps, styles, isOpen)} {this.renderItemsList( styles, @@ -257,6 +259,32 @@ export default class Dropdown extends AutoControlledComponent< ) } + private renderTriggerButton( + getToggleButtonProps: (options?: GetToggleButtonPropsOptions) => any, + styles: ComponentSlotStylesInput, + ): JSX.Element { + const { placeholder, itemToString } = this.props + const { value } = this.state + return ( + <Button + content={value ? itemToString(value) : placeholder} + fluid + styles={styles.button} + {...getToggleButtonProps({ + onFocus: () => { + this.setState({ focused: true }) + }, + onBlur: () => { + this.setState({ focused: false }) + }, + onClick: e => { + e.stopPropagation() + }, + })} + /> + ) + } + private renderSearchInput( getRootProps: (options?: GetMenuPropsOptions, otherOptions?: GetPropsCommonOptions) => any, getInputProps: (options?: GetInputPropsOptions) => any, @@ -611,7 +639,7 @@ export default class Dropdown extends AutoControlledComponent< value, }) if (getA11ySelectionMessage && getA11ySelectionMessage.onRemove) { - this.setA11yStatus(getA11ySelectionMessage.onRemove(item)) + this.setA11yStatus(getA11ySelectionMessage.onRemove(poppedItem)) } // we don't have event for it, but want to keep the event handling interface, event is empty. diff --git a/src/themes/teams/components/Dropdown/dropdownStyles.ts b/src/themes/teams/components/Dropdown/dropdownStyles.ts index 62436c7499..5a2120e940 100644 --- a/src/themes/teams/components/Dropdown/dropdownStyles.ts +++ b/src/themes/teams/components/Dropdown/dropdownStyles.ts @@ -37,6 +37,22 @@ const dropdownStyles: ComponentSlotStylesInput<DropdownProps, DropdownVariables> }), }), + button: (): ICSSInJSStyle => { + const transparentColorStyle = { + backgroundColor: 'transparent', + borderColor: 'transparent', + } + return { + boxShadow: '0 0 0 0', + margin: '0', + justifyContent: 'left', + ...transparentColorStyle, + ':hover': transparentColorStyle, + ':focus': transparentColorStyle, + ':active': transparentColorStyle, + } + }, + label: (): ICSSInJSStyle => ({ margin: '.4rem 0 0 .4rem', }), From 7e870fe38b24d4f6b731ea7a77bbdeec6c09d44d Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Tue, 11 Dec 2018 12:14:28 +0200 Subject: [PATCH 31/62] fixed the toggle and added/refactored styles --- ...opdownExampleSingleSelection.shorthand.tsx | 3 +- src/components/Dropdown/Dropdown.tsx | 80 ++++++++++--------- .../Dropdown/DropdownSearchInput.tsx | 44 +++++----- .../components/Dropdown/dropdownItemStyles.ts | 4 +- .../Dropdown/dropdownSearchInputStyles.ts | 7 +- .../components/Dropdown/dropdownStyles.ts | 38 +++++---- .../components/Dropdown/dropdownVariables.ts | 45 ++++++----- 7 files changed, 116 insertions(+), 105 deletions(-) diff --git a/docs/src/examples/components/Dropdown/Types/DropdownExampleSingleSelection.shorthand.tsx b/docs/src/examples/components/Dropdown/Types/DropdownExampleSingleSelection.shorthand.tsx index e6968e8313..1094c540a4 100644 --- a/docs/src/examples/components/Dropdown/Types/DropdownExampleSingleSelection.shorthand.tsx +++ b/docs/src/examples/components/Dropdown/Types/DropdownExampleSingleSelection.shorthand.tsx @@ -18,8 +18,7 @@ class DropdownExample extends React.Component { return ( <Dropdown getA11yStatusMessage={getA11yStatusMessage} - noResultsMessage="We couldn't find any matches." - placeholder="Start typing a name" + placeholder="Select your hero" items={inputItems} /> ) diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index e302fb9a49..ef90f67ca4 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -208,38 +208,46 @@ export default class Dropdown extends AutoControlledComponent< const { searchQuery } = this.state return ( - <ElementType className={classes.root} {...rest}> - <Downshift - onChange={this.handleSelectedChange} - inputValue={search ? searchQuery : undefined} - stateReducer={this.handleDownshiftStateChanges} - itemToString={itemToString} - // Downshift does not support multiple selection. We will handle everything and pass it selected as null in this case. - selectedItem={search && !multiple ? undefined : null} - getA11yStatusMessage={getA11yStatusMessage} - > - {({ - getInputProps, - getItemProps, - getMenuProps, - getRootProps, - getToggleButtonProps, - isOpen, - highlightedIndex, - selectItemAtIndex, - }) => { - return ( - <div - className={classes.containerDiv} - onClick={this.handleContainerClick.bind(this, isOpen)} + <Downshift + onChange={this.handleSelectedChange} + inputValue={search ? searchQuery : undefined} + stateReducer={this.handleDownshiftStateChanges} + itemToString={itemToString} + // Downshift does not support multiple selection. We will handle everything and pass it selected as null in this case. + selectedItem={search && !multiple ? undefined : null} + getA11yStatusMessage={getA11yStatusMessage} + > + {({ + getInputProps, + getItemProps, + getMenuProps, + getRootProps, + getToggleButtonProps, + isOpen, + highlightedIndex, + selectItemAtIndex, + }) => { + const accessibilityRootProps = getRootProps( + { refKey: 'innerRef' }, + { suppressRefError: true }, + ) + // const getExpandedRootProps = () => rootAccessibilityProps + const { innerRef, ...accessibilityRootPropsRest } = accessibilityRootProps + return ( + <Ref innerRef={innerRef}> + <ElementType + className={classes.root} + onClick={multiple ? this.handleContainerClick.bind(this, isOpen) : undefined} + {...rest} > {multiple && this.renderSelectedItems(styles)} {search ? this.renderSearchInput( - getRootProps, + accessibilityRootPropsRest, getInputProps, highlightedIndex, selectItemAtIndex, + variables, ) : this.renderTriggerButton(getToggleButtonProps, styles)} {toggleButton && this.renderToggleButton(getToggleButtonProps, styles, isOpen)} @@ -251,11 +259,11 @@ export default class Dropdown extends AutoControlledComponent< isOpen, highlightedIndex, )} - </div> - ) - }} - </Downshift> - </ElementType> + </ElementType> + </Ref> + ) + }} + </Downshift> ) } @@ -286,7 +294,7 @@ export default class Dropdown extends AutoControlledComponent< } private renderSearchInput( - getRootProps: (options?: GetMenuPropsOptions, otherOptions?: GetPropsCommonOptions) => any, + accessibilityComboboxProps: Object, getInputProps: (options?: GetInputPropsOptions) => any, highlightedIndex: number, selectItemAtIndex: ( @@ -294,6 +302,7 @@ export default class Dropdown extends AutoControlledComponent< otherStateToSet?: Partial<StateChangeOptions<any>>, cb?: () => void, ) => void, + variables, ): JSX.Element { const { searchInput, multiple, placeholder } = this.props const { searchQuery, value } = this.state @@ -304,6 +313,7 @@ export default class Dropdown extends AutoControlledComponent< return DropdownSearchInput.create(searchInput || {}, { defaultProps: { placeholder: noPlaceholder ? '' : placeholder, + variables, inputRef: (inputNode: HTMLElement) => { this.inputNode = inputNode }, @@ -313,7 +323,7 @@ export default class Dropdown extends AutoControlledComponent< predefinedProps, highlightedIndex, selectItemAtIndex, - getRootProps, + accessibilityComboboxProps, getInputProps, ), }) @@ -524,7 +534,7 @@ export default class Dropdown extends AutoControlledComponent< otherStateToSet?: Partial<StateChangeOptions<any>>, cb?: () => void, ) => void, - getRootProps: (options?: GetMenuPropsOptions, otherOptions?: GetPropsCommonOptions) => any, + accessibilityComboboxProps: Object, getInputProps: (options?: GetInputPropsOptions) => any, ) => { const handleInputBlur = ( @@ -564,9 +574,7 @@ export default class Dropdown extends AutoControlledComponent< }), }, // same story as above for getRootProps. - accessibilityWrapperProps: { - ...getRootProps({ refKey: 'innerRef' }, { suppressRefError: true }), - }, + accessibilityComboboxProps, onFocus: (e: React.SyntheticEvent, searchInputProps: DropdownSearchInputProps) => { this.setState({ focused: true }) diff --git a/src/components/Dropdown/DropdownSearchInput.tsx b/src/components/Dropdown/DropdownSearchInput.tsx index 63cec50f1d..6a4a4db94c 100644 --- a/src/components/Dropdown/DropdownSearchInput.tsx +++ b/src/components/Dropdown/DropdownSearchInput.tsx @@ -6,7 +6,6 @@ import { UIComponent, RenderResultConfig, createShorthandFactory, commonPropType import { ComponentEventHandler, ReactProps } from '../../../types/utils' import { UIComponentProps } from '../../lib/commonPropInterfaces' import Input from '../Input/Input' -import Ref from '../Ref/Ref' export interface DropdownSearchInputProps extends UIComponentProps<DropdownSearchInputProps> { /** @@ -68,7 +67,7 @@ class DropdownSearchInput extends UIComponent<ReactProps<DropdownSearchInputProp content: false, }), accessibilityInputProps: PropTypes.object, - accessibilityWrapperProps: PropTypes.object, + accessibilityComboboxProps: PropTypes.object, inputRef: PropTypes.func, onFocus: PropTypes.func, onInputBlur: PropTypes.func, @@ -98,29 +97,26 @@ class DropdownSearchInput extends UIComponent<ReactProps<DropdownSearchInputProp } public renderComponent({ rest, styles }: RenderResultConfig<DropdownSearchInputProps>) { - const { accessibilityWrapperProps, accessibilityInputProps, placeholder } = this.props - const { innerRef, ...accessibilityWrapperPropsRest } = accessibilityWrapperProps + const { accessibilityComboboxProps, accessibilityInputProps, placeholder } = this.props return ( - <Ref innerRef={innerRef}> - <Input - inputRef={this.handleInputRef} - onFocus={this.handleFocus} - onKeyUp={this.handleKeyUp} - wrapper={{ - styles: styles.wrapper, - ...accessibilityWrapperPropsRest, - }} - input={{ - type: 'text', - styles: styles.input, - placeholder, - onBlur: this.handleInputBlur, - onKeyDown: this.handleInputKeyDown, - ...accessibilityInputProps, - }} - {...rest} - /> - </Ref> + <Input + inputRef={this.handleInputRef} + onFocus={this.handleFocus} + onKeyUp={this.handleKeyUp} + wrapper={{ + styles: styles.combobox, + ...accessibilityComboboxProps, + }} + input={{ + type: 'text', + styles: styles.input, + placeholder, + onBlur: this.handleInputBlur, + onKeyDown: this.handleInputKeyDown, + ...accessibilityInputProps, + }} + {...rest} + /> ) } } diff --git a/src/themes/teams/components/Dropdown/dropdownItemStyles.ts b/src/themes/teams/components/Dropdown/dropdownItemStyles.ts index bf5347c4cd..4e28dbe216 100644 --- a/src/themes/teams/components/Dropdown/dropdownItemStyles.ts +++ b/src/themes/teams/components/Dropdown/dropdownItemStyles.ts @@ -9,8 +9,8 @@ const dropdownItemStyles: ComponentSlotStylesInput<DropdownItemProps, DropdownVa ...(active && { [`&.${ListItem.className}`]: { - backgroundColor: v.listItemHighlightedBackgroundColor, - color: v.listItemHighlightedTextColor, + backgroundColor: v.listItemBackgroundColorActive, + color: v.listItemColorActive, }, }), }), diff --git a/src/themes/teams/components/Dropdown/dropdownSearchInputStyles.ts b/src/themes/teams/components/Dropdown/dropdownSearchInputStyles.ts index 193a05a58c..e8a04e5c22 100644 --- a/src/themes/teams/components/Dropdown/dropdownSearchInputStyles.ts +++ b/src/themes/teams/components/Dropdown/dropdownSearchInputStyles.ts @@ -6,17 +6,18 @@ const dropdownSearchInputStyles: ComponentSlotStylesInput< DropdownSearchInputProps, DropdownVariables > = { - input: ({ variables: { backgroundColor } }): ICSSInJSStyle => ({ + input: ({ variables: { backgroundColor, comboboxPaddingInput } }): ICSSInJSStyle => ({ width: '100%', backgroundColor, + padding: comboboxPaddingInput, ':focus': { borderBottomColor: 'transparent', }, }), - wrapper: ({ variables: { editTextFlexBasis } }): ICSSInJSStyle => ({ - flexBasis: editTextFlexBasis, + combobox: ({ variables: { comboboxFlexBasis } }): ICSSInJSStyle => ({ + flexBasis: comboboxFlexBasis, flexGrow: 1, }), } diff --git a/src/themes/teams/components/Dropdown/dropdownStyles.ts b/src/themes/teams/components/Dropdown/dropdownStyles.ts index 5a2120e940..b16b484fe4 100644 --- a/src/themes/teams/components/Dropdown/dropdownStyles.ts +++ b/src/themes/teams/components/Dropdown/dropdownStyles.ts @@ -3,16 +3,16 @@ import { DropdownProps } from '../../../../components/Dropdown/Dropdown' import { DropdownVariables } from './dropdownVariables' const dropdownStyles: ComponentSlotStylesInput<DropdownProps, DropdownVariables> = { - containerDiv: ({ + root: ({ props: { focused, toggleButton, fluid }, variables: { backgroundColor, - containerDivBorderBottom, - containerDivBorderRadius, - containerDivBorderColor, - containerDivFocusBorderColor, - containerDivFocusBorderRadius, - containerDivColor, + borderBottom, + borderRadius, + borderColor, + borderColorFocus, + borderRadiusFocus, + color, toggleButtonSize, width, }, @@ -22,33 +22,39 @@ const dropdownStyles: ComponentSlotStylesInput<DropdownProps, DropdownVariables> outline: 0, border: 0, backgroundColor, - borderRadius: containerDivBorderRadius, - borderBottom: containerDivBorderBottom, - borderColor: containerDivBorderColor, - color: containerDivColor, + borderBottom, + borderColor, + borderRadius, + color, width: fluid ? '100%' : width, position: 'relative', ...(toggleButton && { paddingRight: toggleButtonSize, }), ...(focused && { - borderColor: containerDivFocusBorderColor, - borderRadius: containerDivFocusBorderRadius, + borderColor: borderColorFocus, + borderRadius: borderRadiusFocus, }), }), - button: (): ICSSInJSStyle => { + button: ({ variables: { comboboxPaddingButton } }): ICSSInJSStyle => { const transparentColorStyle = { backgroundColor: 'transparent', borderColor: 'transparent', } return { - boxShadow: '0 0 0 0', + boxShadow: 'none', margin: '0', justifyContent: 'left', + padding: comboboxPaddingButton, ...transparentColorStyle, ':hover': transparentColorStyle, - ':focus': transparentColorStyle, + ':focus': { + ...transparentColorStyle, + ':after': { + borderColor: 'transparent', + }, + }, ':active': transparentColorStyle, } }, diff --git a/src/themes/teams/components/Dropdown/dropdownVariables.ts b/src/themes/teams/components/Dropdown/dropdownVariables.ts index 32c59c7fd2..95319720d2 100644 --- a/src/themes/teams/components/Dropdown/dropdownVariables.ts +++ b/src/themes/teams/components/Dropdown/dropdownVariables.ts @@ -1,39 +1,40 @@ import { pxToRem } from '../../utils' export interface DropdownVariables { backgroundColor: string - containerDivBorderRadius: string - containerDivBorderBottom: string - containerDivBorderColor: string - containerDivColor: string - containerDivFocusBorderColor: string - containerDivFocusBorderRadius: string - editTextFlexBasis: string + borderBottom: string + borderColor: string + borderColorFocus: string + borderRadius: string + borderRadiusFocus: string + color: string + comboboxPaddingButton: string + comboboxPaddingInput: string + comboboxFlexBasis: string listItemBackgroundColor: string - listItemHighlightedBackgroundColor: string - listItemHighlightedTextColor: string + listItemBackgroundColorActive: string + listItemColorActive: string listMaxHeight: string toggleButtonSize: string width: string } -const [_2px_asRem, _3px_asRem] = [2, 3].map(v => pxToRem(v)) +const [_2px_asRem, _3px_asRem, _6px_asRem, _12px_asRem] = [2, 3, 6, 12].map(v => pxToRem(v)) export default (siteVars): DropdownVariables => ({ backgroundColor: siteVars.gray10, - - containerDivBorderRadius: _3px_asRem, - containerDivBorderBottom: `${_2px_asRem} solid transparent`, - containerDivBorderColor: 'transparent', - containerDivColor: siteVars.bodyColor, - containerDivFocusBorderColor: siteVars.brand, - containerDivFocusBorderRadius: `${_3px_asRem} ${_3px_asRem} ${_2px_asRem} ${_2px_asRem}`, - editTextFlexBasis: '100px', - + borderRadius: _3px_asRem, + borderBottom: `${_2px_asRem} solid transparent`, + borderColor: 'transparent', + borderColorFocus: siteVars.brand, + borderRadiusFocus: `${_3px_asRem} ${_3px_asRem} ${_2px_asRem} ${_2px_asRem}`, + color: siteVars.bodyColor, + comboboxPaddingButton: `0 ${_12px_asRem}`, + comboboxPaddingInput: `${_6px_asRem} ${_12px_asRem}`, + comboboxFlexBasis: '50px', listItemBackgroundColor: siteVars.white, - listItemHighlightedBackgroundColor: siteVars.brand, - listItemHighlightedTextColor: siteVars.white, + listItemBackgroundColorActive: siteVars.brand, + listItemColorActive: siteVars.white, listMaxHeight: '20rem', - toggleButtonSize: pxToRem(30), width: pxToRem(356), }) From ee95d06d9718701533ebed25763e12f4fe25100a Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Tue, 11 Dec 2018 13:17:36 +0200 Subject: [PATCH 32/62] fixed the filtering to work for all variants --- src/components/Dropdown/Dropdown.tsx | 42 +++++++++++++++------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index ef90f67ca4..22acc7cef6 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -231,7 +231,6 @@ export default class Dropdown extends AutoControlledComponent< { refKey: 'innerRef' }, { suppressRefError: true }, ) - // const getExpandedRootProps = () => rootAccessibilityProps const { innerRef, ...accessibilityRootPropsRest } = accessibilityRootProps return ( <Ref innerRef={innerRef}> @@ -374,8 +373,8 @@ export default class Dropdown extends AutoControlledComponent< getItemProps: (options: GetItemPropsOptions<ShorthandValue>) => any, highlightedIndex: number, ) { - const { items, noResultsMessage } = this.props - const filteredItems = this.getItemsFilteredBySearchQuery(items) + const { noResultsMessage } = this.props + const filteredItems = this.getFilteredItems() if (filteredItems.length > 0) { return filteredItems.map((item, index) => { @@ -457,25 +456,30 @@ export default class Dropdown extends AutoControlledComponent< } } - private getItemsFilteredBySearchQuery = (items: ShorthandValue[]): ShorthandValue[] => { - const { itemToString, multiple, search } = this.props + private getFilteredItems = (): ShorthandValue[] => { + const { items, itemToString, multiple, search } = this.props const { searchQuery, value } = this.state + let filteredItems = items - const nonSelectedItems = items.filter(item => - multiple ? (value as ShorthandValue[]).indexOf(item) === -1 : true, - ) - - const itemsMatchSearchQuery = nonSelectedItems.filter(item => - search - ? _.isFunction(search) - ? search(itemsMatchSearchQuery, searchQuery) - : itemToString(item) - .toLowerCase() - .indexOf(searchQuery.toLowerCase()) !== -1 - : true, - ) + if (!multiple && !search) { + return items.filter(item => item !== (value as ShorthandValue)) + } + if (multiple) { + filteredItems = filteredItems.filter(item => (value as ShorthandValue[]).indexOf(item) === -1) + } + if (search) { + if (_.isFunction(search)) { + return search(filteredItems, searchQuery) + } + return filteredItems.filter( + item => + itemToString(item) + .toLowerCase() + .indexOf(searchQuery.toLowerCase()) !== -1, + ) + } - return itemsMatchSearchQuery + return filteredItems } private setA11yStatus = (statusMessage: string) => { From bcd061ae18bbf5ce07897ea45b5ad88df333c78a Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Tue, 11 Dec 2018 13:25:56 +0200 Subject: [PATCH 33/62] made the toggleButton tabbable by default --- src/components/Dropdown/Dropdown.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index 22acc7cef6..7a6ffe243d 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -337,7 +337,7 @@ export default class Dropdown extends AutoControlledComponent< <Icon name={`chevron ${isOpen ? 'up' : 'down'}`} as="button" - tabIndex="-1" + tabIndex="0" styles={styles.toggleButton} {...getToggleButtonProps()} /> From 7d69d08c575131555aeddf10257a2f0f4a0aeff0 Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Tue, 11 Dec 2018 13:31:22 +0200 Subject: [PATCH 34/62] fixed style case active+focus --- src/themes/teams/components/Dropdown/dropdownStyles.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/themes/teams/components/Dropdown/dropdownStyles.ts b/src/themes/teams/components/Dropdown/dropdownStyles.ts index b16b484fe4..08b91d8ebf 100644 --- a/src/themes/teams/components/Dropdown/dropdownStyles.ts +++ b/src/themes/teams/components/Dropdown/dropdownStyles.ts @@ -54,6 +54,7 @@ const dropdownStyles: ComponentSlotStylesInput<DropdownProps, DropdownVariables> ':after': { borderColor: 'transparent', }, + ':active': transparentColorStyle, }, ':active': transparentColorStyle, } From 75ee900cb0bb160a59466529efd762377abf4ad5 Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Tue, 11 Dec 2018 15:46:49 +0200 Subject: [PATCH 35/62] fixed wrong import --- src/components/Dropdown/Dropdown.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index 7a6ffe243d..564c11c38b 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -28,8 +28,7 @@ import Ref from '../Ref/Ref' import { UIComponentProps } from '../../lib/commonPropInterfaces' import DropdownItem, { DropdownItemProps } from './DropdownItem' import DropdownLabel, { DropdownLabelProps } from './DropdownLabel' -import DropdownSearchInput from './DropdownSearchInput' -import { DropdownSearchInputProps } from 'semantic-ui-react' +import DropdownSearchInput, { DropdownSearchInputProps } from './DropdownSearchInput' import Button from '../Button/Button' // TODO: To be replaced when Downshift will add highlightedItem in their interface. @@ -352,7 +351,7 @@ export default class Dropdown extends AutoControlledComponent< isOpen: boolean, highlightedIndex: number, ) { - const accessibilityMenuProps = getMenuProps({ refKey: 'innerRef' }) + const accessibilityMenuProps = getMenuProps({ refKey: 'innerRef' }, { suppressRefError: true }) const { innerRef, ...accessibilityMenuPropsRest } = accessibilityMenuProps return ( From 868452599f977a20b233d22358f7a30d087af328 Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Tue, 11 Dec 2018 17:09:29 +0200 Subject: [PATCH 36/62] fixed ariaLabel for trigger button --- src/components/Dropdown/Dropdown.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index 564c11c38b..ee465d6ba8 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -269,11 +269,12 @@ export default class Dropdown extends AutoControlledComponent< getToggleButtonProps: (options?: GetToggleButtonPropsOptions) => any, styles: ComponentSlotStylesInput, ): JSX.Element { - const { placeholder, itemToString } = this.props + const { placeholder, itemToString, multiple } = this.props const { value } = this.state + const content = multiple ? placeholder : value ? itemToString(value) : placeholder return ( <Button - content={value ? itemToString(value) : placeholder} + content={content} fluid styles={styles.button} {...getToggleButtonProps({ @@ -286,6 +287,7 @@ export default class Dropdown extends AutoControlledComponent< onClick: e => { e.stopPropagation() }, + 'aria-label': content, // TODO: add this to behaviour })} /> ) From 8a6027177281f0e44ca5becfca02cb86d95dcb65 Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Wed, 12 Dec 2018 18:14:05 +0200 Subject: [PATCH 37/62] added list focus and accessibility handling for it --- ...opdownExampleSingleSelection.shorthand.tsx | 3 + src/components/Dropdown/Dropdown.tsx | 121 ++++++++++++++---- 2 files changed, 99 insertions(+), 25 deletions(-) diff --git a/docs/src/examples/components/Dropdown/Types/DropdownExampleSingleSelection.shorthand.tsx b/docs/src/examples/components/Dropdown/Types/DropdownExampleSingleSelection.shorthand.tsx index 1094c540a4..1145fb1b6c 100644 --- a/docs/src/examples/components/Dropdown/Types/DropdownExampleSingleSelection.shorthand.tsx +++ b/docs/src/examples/components/Dropdown/Types/DropdownExampleSingleSelection.shorthand.tsx @@ -18,6 +18,9 @@ class DropdownExample extends React.Component { return ( <Dropdown getA11yStatusMessage={getA11yStatusMessage} + getA11ySelectionMessage={{ + onAdd: item => `${item} has been selected.`, + }} placeholder="Select your hero" items={inputItems} /> diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index ee465d6ba8..eb21b562d4 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -131,6 +131,8 @@ export default class Dropdown extends AutoControlledComponent< DropdownState > { private inputNode: HTMLElement + private listNode: HTMLElement + private buttonNode: HTMLElement static displayName = 'Dropdown' @@ -215,6 +217,11 @@ export default class Dropdown extends AutoControlledComponent< // Downshift does not support multiple selection. We will handle everything and pass it selected as null in this case. selectedItem={search && !multiple ? undefined : null} getA11yStatusMessage={getA11yStatusMessage} + onStateChange={changes => { + if (changes.isOpen && !search) { + this.listNode.focus() + } + }} > {({ getInputProps, @@ -223,6 +230,7 @@ export default class Dropdown extends AutoControlledComponent< getRootProps, getToggleButtonProps, isOpen, + toggleMenu, highlightedIndex, selectItemAtIndex, }) => { @@ -247,15 +255,18 @@ export default class Dropdown extends AutoControlledComponent< selectItemAtIndex, variables, ) - : this.renderTriggerButton(getToggleButtonProps, styles)} + : this.renderTriggerButton(styles, getToggleButtonProps)} {toggleButton && this.renderToggleButton(getToggleButtonProps, styles, isOpen)} {this.renderItemsList( styles, variables, - getMenuProps, - getItemProps, isOpen, highlightedIndex, + toggleMenu, + selectItemAtIndex, + getMenuProps, + getItemProps, + getInputProps, )} </ElementType> </Ref> @@ -266,30 +277,33 @@ export default class Dropdown extends AutoControlledComponent< } private renderTriggerButton( - getToggleButtonProps: (options?: GetToggleButtonPropsOptions) => any, styles: ComponentSlotStylesInput, + getToggleButtonProps: (options?: GetToggleButtonPropsOptions) => any, ): JSX.Element { const { placeholder, itemToString, multiple } = this.props const { value } = this.state const content = multiple ? placeholder : value ? itemToString(value) : placeholder return ( - <Button - content={content} - fluid - styles={styles.button} - {...getToggleButtonProps({ - onFocus: () => { - this.setState({ focused: true }) - }, - onBlur: () => { - this.setState({ focused: false }) - }, - onClick: e => { - e.stopPropagation() - }, - 'aria-label': content, // TODO: add this to behaviour - })} - /> + <Ref + innerRef={buttonNode => { + this.buttonNode = buttonNode + }} + > + <Button + content={content} + fluid + styles={styles.button} + {...getToggleButtonProps({ + onFocus: () => { + this.setState({ focused: true }) + }, + onBlur: () => { + this.setState({ focused: false }) + }, + 'aria-label': content, // TODO: add this to behaviour + })} + /> + </Ref> ) } @@ -348,19 +362,44 @@ export default class Dropdown extends AutoControlledComponent< private renderItemsList( styles: ComponentSlotStylesInput, variables: ComponentVariablesInput, - getMenuProps: (options?: GetMenuPropsOptions, otherOptions?: GetPropsCommonOptions) => any, - getItemProps: (options: GetItemPropsOptions<ShorthandValue>) => any, isOpen: boolean, highlightedIndex: number, + toggleMenu: () => void, + selectItemAtIndex: (index: number) => void, + getMenuProps: (options?: GetMenuPropsOptions, otherOptions?: GetPropsCommonOptions) => any, + getItemProps: (options: GetItemPropsOptions<ShorthandValue>) => any, + getInputProps: (options?: GetInputPropsOptions) => any, ) { - const accessibilityMenuProps = getMenuProps({ refKey: 'innerRef' }, { suppressRefError: true }) + const accessibilityMenuProps = { + ...getMenuProps({ refKey: 'innerRef' }, { suppressRefError: true }), + } + if (!this.props.search) { + const accessibilityInputProps = getInputProps() + accessibilityMenuProps['aria-activedescendant'] = + accessibilityInputProps['aria-activedescendant'] + accessibilityMenuProps['onKeyDown'] = e => { + this.handleListKeyDown( + e, + highlightedIndex, + accessibilityInputProps['onKeyDown'], + toggleMenu, + selectItemAtIndex, + ) + } + } const { innerRef, ...accessibilityMenuPropsRest } = accessibilityMenuProps return ( - <Ref innerRef={innerRef}> + <Ref + innerRef={(listNode: HTMLElement) => { + this.listNode = listNode + innerRef(listNode) + }} + > <List {...accessibilityMenuPropsRest} styles={styles.list} + tabIndex={-1} aria-hidden={!isOpen} items={isOpen ? this.renderItems(styles, variables, getItemProps, highlightedIndex) : []} /> @@ -452,6 +491,12 @@ export default class Dropdown extends AutoControlledComponent< { ...this.props, searchQuery: changes.inputValue }, ) return changes + case Downshift.stateChangeTypes.blurButton: + // Focus the list, by button click/enter/up/down/space. Downshift, by default, closes the list + // on trigger blur, but in this case it's custom behaviour, where we want to keep it open. + if (state.isOpen) { + return {} + } default: return changes } @@ -615,6 +660,32 @@ export default class Dropdown extends AutoControlledComponent< !isOpen && this.inputNode.focus() } + private handleListKeyDown = ( + e: React.SyntheticEvent, + highlightedIndex: number, + accessibilityInputPropsKeyDown: (e) => any, + toggleMenu: () => void, + selectItemAtIndex: (index: number) => void, + ) => { + switch (keyboardKey.getCode(e)) { + case keyboardKey.Tab: + if (_.isNil(highlightedIndex)) { + toggleMenu() + } else { + selectItemAtIndex(highlightedIndex) + } + return + case keyboardKey.Enter: + case keyboardKey.Escape: + accessibilityInputPropsKeyDown(e) + this.buttonNode.focus() + return + default: + accessibilityInputPropsKeyDown(e) + return + } + } + private handleSelectedChange = (item: ShorthandValue) => { const { multiple, getA11ySelectionMessage } = this.props const newValue = multiple ? [...(this.state.value as ShorthandValue[]), item] : item From 5aa379e4c193e65cb1901992302220a1fc967e88 Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Thu, 13 Dec 2018 10:14:21 +0200 Subject: [PATCH 38/62] fixed toggle button bug that appeared after trigger fix --- src/components/Dropdown/Dropdown.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index eb21b562d4..a0e850ee64 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -492,10 +492,10 @@ export default class Dropdown extends AutoControlledComponent< ) return changes case Downshift.stateChangeTypes.blurButton: - // Focus the list, by button click/enter/up/down/space. Downshift, by default, closes the list - // on trigger blur, but in this case it's custom behaviour, where we want to keep it open. - if (state.isOpen) { - return {} + // Downshift closes the list by default on trigger blur. It does not support the case when dropdown is + // single selection and focuses list on trigger click/up/down/space/enter. Treating that here. + if (state.isOpen && document.activeElement === this.listNode) { + return {} // won't change state in this case. } default: return changes @@ -599,7 +599,7 @@ export default class Dropdown extends AutoControlledComponent< e: React.SyntheticEvent, searchInputProps: DropdownSearchInputProps, ) => { - if (keyboardKey.getCode(e) === keyboardKey.Tab && highlightedIndex !== undefined) { + if (keyboardKey.getCode(e) === keyboardKey.Tab && _.isNil(highlightedIndex)) { selectItemAtIndex(highlightedIndex) } From eeaf144ad4d49a9452b4a0760607e57a4419c16e Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Thu, 13 Dec 2018 11:23:21 +0200 Subject: [PATCH 39/62] reverted change for filteredItems --- src/components/Dropdown/Dropdown.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index a0e850ee64..3b014c41bf 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -414,7 +414,7 @@ export default class Dropdown extends AutoControlledComponent< highlightedIndex: number, ) { const { noResultsMessage } = this.props - const filteredItems = this.getFilteredItems() + const filteredItems = this.getItemsFilteredBySearchQuery() if (filteredItems.length > 0) { return filteredItems.map((item, index) => { @@ -502,14 +502,11 @@ export default class Dropdown extends AutoControlledComponent< } } - private getFilteredItems = (): ShorthandValue[] => { + private getItemsFilteredBySearchQuery = (): ShorthandValue[] => { const { items, itemToString, multiple, search } = this.props const { searchQuery, value } = this.state let filteredItems = items - if (!multiple && !search) { - return items.filter(item => item !== (value as ShorthandValue)) - } if (multiple) { filteredItems = filteredItems.filter(item => (value as ShorthandValue[]).indexOf(item) === -1) } From c2861334644d54900b9ffe68972a7c0edab068ee Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Thu, 13 Dec 2018 11:52:53 +0200 Subject: [PATCH 40/62] refactored the renderComponent to pass conformance --- src/components/Dropdown/Dropdown.tsx | 129 +++++++++--------- .../components/Dropdown/dropdownStyles.ts | 4 +- 2 files changed, 68 insertions(+), 65 deletions(-) diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index 3b014c41bf..7218a198b1 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -209,70 +209,71 @@ export default class Dropdown extends AutoControlledComponent< const { searchQuery } = this.state return ( - <Downshift - onChange={this.handleSelectedChange} - inputValue={search ? searchQuery : undefined} - stateReducer={this.handleDownshiftStateChanges} - itemToString={itemToString} - // Downshift does not support multiple selection. We will handle everything and pass it selected as null in this case. - selectedItem={search && !multiple ? undefined : null} - getA11yStatusMessage={getA11yStatusMessage} - onStateChange={changes => { - if (changes.isOpen && !search) { - this.listNode.focus() - } - }} - > - {({ - getInputProps, - getItemProps, - getMenuProps, - getRootProps, - getToggleButtonProps, - isOpen, - toggleMenu, - highlightedIndex, - selectItemAtIndex, - }) => { - const accessibilityRootProps = getRootProps( - { refKey: 'innerRef' }, - { suppressRefError: true }, - ) - const { innerRef, ...accessibilityRootPropsRest } = accessibilityRootProps - return ( - <Ref innerRef={innerRef}> - <ElementType - className={classes.root} - onClick={multiple ? this.handleContainerClick.bind(this, isOpen) : undefined} - {...rest} - > - {multiple && this.renderSelectedItems(styles)} - {search - ? this.renderSearchInput( - accessibilityRootPropsRest, - getInputProps, - highlightedIndex, - selectItemAtIndex, - variables, - ) - : this.renderTriggerButton(styles, getToggleButtonProps)} - {toggleButton && this.renderToggleButton(getToggleButtonProps, styles, isOpen)} - {this.renderItemsList( - styles, - variables, - isOpen, - highlightedIndex, - toggleMenu, - selectItemAtIndex, - getMenuProps, - getItemProps, - getInputProps, - )} - </ElementType> - </Ref> - ) - }} - </Downshift> + <ElementType className={classes.root} {...rest}> + <Downshift + onChange={this.handleSelectedChange} + inputValue={search ? searchQuery : undefined} + stateReducer={this.handleDownshiftStateChanges} + itemToString={itemToString} + // Downshift does not support multiple selection. We will handle everything and pass it selected as null in this case. + selectedItem={search && !multiple ? undefined : null} + getA11yStatusMessage={getA11yStatusMessage} + onStateChange={changes => { + if (changes.isOpen && !search) { + this.listNode.focus() + } + }} + > + {({ + getInputProps, + getItemProps, + getMenuProps, + getRootProps, + getToggleButtonProps, + isOpen, + toggleMenu, + highlightedIndex, + selectItemAtIndex, + }) => { + const accessibilityRootProps = getRootProps( + { refKey: 'innerRef' }, + { suppressRefError: true }, + ) + const { innerRef, ...accessibilityRootPropsRest } = accessibilityRootProps + return ( + <Ref innerRef={innerRef}> + <div + className={classes.container} + onClick={multiple ? this.handleContainerClick.bind(this, isOpen) : undefined} + > + {multiple && this.renderSelectedItems(styles)} + {search + ? this.renderSearchInput( + accessibilityRootPropsRest, + getInputProps, + highlightedIndex, + selectItemAtIndex, + variables, + ) + : this.renderTriggerButton(styles, getToggleButtonProps)} + {toggleButton && this.renderToggleButton(getToggleButtonProps, styles, isOpen)} + {this.renderItemsList( + styles, + variables, + isOpen, + highlightedIndex, + toggleMenu, + selectItemAtIndex, + getMenuProps, + getItemProps, + getInputProps, + )} + </div> + </Ref> + ) + }} + </Downshift> + </ElementType> ) } diff --git a/src/themes/teams/components/Dropdown/dropdownStyles.ts b/src/themes/teams/components/Dropdown/dropdownStyles.ts index 08b91d8ebf..81639105b9 100644 --- a/src/themes/teams/components/Dropdown/dropdownStyles.ts +++ b/src/themes/teams/components/Dropdown/dropdownStyles.ts @@ -3,7 +3,9 @@ import { DropdownProps } from '../../../../components/Dropdown/Dropdown' import { DropdownVariables } from './dropdownVariables' const dropdownStyles: ComponentSlotStylesInput<DropdownProps, DropdownVariables> = { - root: ({ + root: (): ICSSInJSStyle => ({}), + + container: ({ props: { focused, toggleButton, fluid }, variables: { backgroundColor, From 1ec202817b9beefcbebd862deb8410c4d78fd4d5 Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Thu, 13 Dec 2018 11:55:50 +0200 Subject: [PATCH 41/62] fixed button focus at selection --- src/components/Dropdown/Dropdown.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index 7218a198b1..ca43a881bf 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -673,7 +673,6 @@ export default class Dropdown extends AutoControlledComponent< selectItemAtIndex(highlightedIndex) } return - case keyboardKey.Enter: case keyboardKey.Escape: accessibilityInputPropsKeyDown(e) this.buttonNode.focus() @@ -685,7 +684,7 @@ export default class Dropdown extends AutoControlledComponent< } private handleSelectedChange = (item: ShorthandValue) => { - const { multiple, getA11ySelectionMessage } = this.props + const { multiple, getA11ySelectionMessage, search } = this.props const newValue = multiple ? [...(this.state.value as ShorthandValue[]), item] : item this.trySetState({ @@ -695,6 +694,9 @@ export default class Dropdown extends AutoControlledComponent< if (getA11ySelectionMessage && getA11ySelectionMessage.onAdd) { this.setA11yStatus(getA11ySelectionMessage.onAdd(item)) } + if (!search) { + this.buttonNode.focus() + } // 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: newValue }) From c19c12f7fda1134a507e7f34175a18222a0901ed Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Thu, 13 Dec 2018 18:39:39 +0200 Subject: [PATCH 42/62] merged ref code review improvement --- src/components/Dropdown/Dropdown.tsx | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index ca43a881bf..2a01dc3538 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -132,7 +132,7 @@ export default class Dropdown extends AutoControlledComponent< > { private inputNode: HTMLElement private listNode: HTMLElement - private buttonNode: HTMLElement + private buttonRef = React.createRef<HTMLElement>() static displayName = 'Dropdown' @@ -285,11 +285,7 @@ export default class Dropdown extends AutoControlledComponent< const { value } = this.state const content = multiple ? placeholder : value ? itemToString(value) : placeholder return ( - <Ref - innerRef={buttonNode => { - this.buttonNode = buttonNode - }} - > + <Ref innerRef={this.buttonRef}> <Button content={content} fluid @@ -675,7 +671,7 @@ export default class Dropdown extends AutoControlledComponent< return case keyboardKey.Escape: accessibilityInputPropsKeyDown(e) - this.buttonNode.focus() + this.buttonRef.current.focus() return default: accessibilityInputPropsKeyDown(e) @@ -695,7 +691,7 @@ export default class Dropdown extends AutoControlledComponent< this.setA11yStatus(getA11ySelectionMessage.onAdd(item)) } if (!search) { - this.buttonNode.focus() + this.buttonRef.current.focus() } // we don't have event for it, but want to keep the event handling interface, event is empty. From 862c4bd0783fda8920ab4f6002d694824bf41f33 Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Thu, 13 Dec 2018 19:17:00 +0200 Subject: [PATCH 43/62] removed unneeded code --- src/components/Dropdown/Dropdown.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index 2a01dc3538..bf88d1e7b5 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -415,13 +415,7 @@ export default class Dropdown extends AutoControlledComponent< if (filteredItems.length > 0) { return filteredItems.map((item, index) => { - let itemAsListItem = item - if (typeof item === 'object') { - itemAsListItem = _.pickBy(item, (value, key) => - _.includes(['key', ...DropdownItem.handledProps], key), - ) - } - return DropdownItem.create(itemAsListItem, { + return DropdownItem.create(item, { defaultProps: { active: highlightedIndex === index, variables, From 945bb954dd5c86906942a739e607eac7a0fa0c8d Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Fri, 14 Dec 2018 11:37:30 +0200 Subject: [PATCH 44/62] removed ternary condition --- src/components/Dropdown/Dropdown.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index bf88d1e7b5..0ce06179ff 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -283,7 +283,14 @@ export default class Dropdown extends AutoControlledComponent< ): JSX.Element { const { placeholder, itemToString, multiple } = this.props const { value } = this.state - const content = multiple ? placeholder : value ? itemToString(value) : placeholder + let content + + if (multiple) { + content = placeholder + } else { + content = value ? itemToString(value) : placeholder + } + return ( <Ref innerRef={this.buttonRef}> <Button From 0675513f77aa1a322846907cf89446a6f146bffd Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Wed, 2 Jan 2019 17:11:49 +0200 Subject: [PATCH 45/62] replaced some heights to make toggle button appear centered --- src/themes/teams/components/Dropdown/dropdownStyles.ts | 2 ++ src/themes/teams/components/Dropdown/dropdownVariables.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/themes/teams/components/Dropdown/dropdownStyles.ts b/src/themes/teams/components/Dropdown/dropdownStyles.ts index 81639105b9..446b22fb4a 100644 --- a/src/themes/teams/components/Dropdown/dropdownStyles.ts +++ b/src/themes/teams/components/Dropdown/dropdownStyles.ts @@ -1,6 +1,7 @@ import { ComponentSlotStylesInput, ICSSInJSStyle } from '../../../types' import { DropdownProps } from '../../../../components/Dropdown/Dropdown' import { DropdownVariables } from './dropdownVariables' +import { pxToRem } from '../../utils' const dropdownStyles: ComponentSlotStylesInput<DropdownProps, DropdownVariables> = { root: (): ICSSInJSStyle => ({}), @@ -50,6 +51,7 @@ const dropdownStyles: ComponentSlotStylesInput<DropdownProps, DropdownVariables> justifyContent: 'left', padding: comboboxPaddingButton, ...transparentColorStyle, + height: pxToRem(30), ':hover': transparentColorStyle, ':focus': { ...transparentColorStyle, diff --git a/src/themes/teams/components/Dropdown/dropdownVariables.ts b/src/themes/teams/components/Dropdown/dropdownVariables.ts index 95319720d2..d92e33e739 100644 --- a/src/themes/teams/components/Dropdown/dropdownVariables.ts +++ b/src/themes/teams/components/Dropdown/dropdownVariables.ts @@ -35,6 +35,6 @@ export default (siteVars): DropdownVariables => ({ listItemBackgroundColorActive: siteVars.brand, listItemColorActive: siteVars.white, listMaxHeight: '20rem', - toggleButtonSize: pxToRem(30), + toggleButtonSize: pxToRem(32), width: pxToRem(356), }) From c571d9217933189d6a4ab7adfe35fea0abede047 Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Wed, 2 Jan 2019 19:05:39 +0200 Subject: [PATCH 46/62] improvements on styles from code review --- src/components/Dropdown/Dropdown.tsx | 14 ++++++++------ src/components/Dropdown/DropdownSearchInput.tsx | 3 +++ .../Dropdown/dropdownSearchInputStyles.ts | 8 +++++++- .../teams/components/Dropdown/dropdownStyles.ts | 11 +++++------ 4 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index 0ce06179ff..f3a8f9d6bc 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -304,7 +304,6 @@ export default class Dropdown extends AutoControlledComponent< onBlur: () => { this.setState({ focused: false }) }, - 'aria-label': content, // TODO: add this to behaviour })} /> </Ref> @@ -322,7 +321,7 @@ export default class Dropdown extends AutoControlledComponent< ) => void, variables, ): JSX.Element { - const { searchInput, multiple, placeholder } = this.props + const { searchInput, multiple, placeholder, toggleButton } = this.props const { searchQuery, value } = this.state const noPlaceholder = @@ -331,6 +330,7 @@ export default class Dropdown extends AutoControlledComponent< return DropdownSearchInput.create(searchInput || {}, { defaultProps: { placeholder: noPlaceholder ? '' : placeholder, + hasToggleButton: !!toggleButton, variables, inputRef: (inputNode: HTMLElement) => { this.inputNode = inputNode @@ -352,13 +352,15 @@ export default class Dropdown extends AutoControlledComponent< styles: ComponentSlotStylesInput, isOpen: boolean, ) { + const { onClick, onBlur, onKeyDown, onKeyUp } = getToggleButtonProps() return ( <Icon name={`chevron ${isOpen ? 'up' : 'down'}`} - as="button" - tabIndex="0" styles={styles.toggleButton} - {...getToggleButtonProps()} + onClick={onClick} + onBlur={onBlur} + onKeyDown={onKeyDown} + onKeyUp={onKeyUp} /> ) } @@ -594,7 +596,7 @@ export default class Dropdown extends AutoControlledComponent< e: React.SyntheticEvent, searchInputProps: DropdownSearchInputProps, ) => { - if (keyboardKey.getCode(e) === keyboardKey.Tab && _.isNil(highlightedIndex)) { + if (keyboardKey.getCode(e) === keyboardKey.Tab && !_.isNil(highlightedIndex)) { selectItemAtIndex(highlightedIndex) } diff --git a/src/components/Dropdown/DropdownSearchInput.tsx b/src/components/Dropdown/DropdownSearchInput.tsx index 6a4a4db94c..a6ed33276a 100644 --- a/src/components/Dropdown/DropdownSearchInput.tsx +++ b/src/components/Dropdown/DropdownSearchInput.tsx @@ -8,6 +8,8 @@ import { UIComponentProps } from '../../lib/commonPropInterfaces' import Input from '../Input/Input' export interface DropdownSearchInputProps extends UIComponentProps<DropdownSearchInputProps> { + /** Informs the search input about an existing toggle button. */ + hasToggleButton?: boolean /** * Ref callback with an input DOM node. * @@ -68,6 +70,7 @@ class DropdownSearchInput extends UIComponent<ReactProps<DropdownSearchInputProp }), accessibilityInputProps: PropTypes.object, accessibilityComboboxProps: PropTypes.object, + hasToggleButton: PropTypes.bool, inputRef: PropTypes.func, onFocus: PropTypes.func, onInputBlur: PropTypes.func, diff --git a/src/themes/teams/components/Dropdown/dropdownSearchInputStyles.ts b/src/themes/teams/components/Dropdown/dropdownSearchInputStyles.ts index e8a04e5c22..bd496b3eb2 100644 --- a/src/themes/teams/components/Dropdown/dropdownSearchInputStyles.ts +++ b/src/themes/teams/components/Dropdown/dropdownSearchInputStyles.ts @@ -16,9 +16,15 @@ const dropdownSearchInputStyles: ComponentSlotStylesInput< }, }), - combobox: ({ variables: { comboboxFlexBasis } }): ICSSInJSStyle => ({ + combobox: ({ + variables: { comboboxFlexBasis, toggleButtonSize }, + props: { hasToggleButton }, + }): ICSSInJSStyle => ({ flexBasis: comboboxFlexBasis, flexGrow: 1, + ...(hasToggleButton && { + paddingRight: toggleButtonSize, + }), }), } diff --git a/src/themes/teams/components/Dropdown/dropdownStyles.ts b/src/themes/teams/components/Dropdown/dropdownStyles.ts index 446b22fb4a..fbf29496cf 100644 --- a/src/themes/teams/components/Dropdown/dropdownStyles.ts +++ b/src/themes/teams/components/Dropdown/dropdownStyles.ts @@ -7,7 +7,7 @@ const dropdownStyles: ComponentSlotStylesInput<DropdownProps, DropdownVariables> root: (): ICSSInJSStyle => ({}), container: ({ - props: { focused, toggleButton, fluid }, + props: { focused, fluid }, variables: { backgroundColor, borderBottom, @@ -16,7 +16,6 @@ const dropdownStyles: ComponentSlotStylesInput<DropdownProps, DropdownVariables> borderColorFocus, borderRadiusFocus, color, - toggleButtonSize, width, }, }): ICSSInJSStyle => ({ @@ -31,9 +30,6 @@ const dropdownStyles: ComponentSlotStylesInput<DropdownProps, DropdownVariables> color, width: fluid ? '100%' : width, position: 'relative', - ...(toggleButton && { - paddingRight: toggleButtonSize, - }), ...(focused && { borderColor: borderColorFocus, borderRadius: borderRadiusFocus, @@ -85,9 +81,12 @@ const dropdownStyles: ComponentSlotStylesInput<DropdownProps, DropdownVariables> position: 'absolute', height: toggleButtonSize, width: toggleButtonSize, - border: 0, + cursor: 'pointer', backgroundColor: 'transparent', margin: 0, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', ...(fluid ? { right: 0 } : { left: `calc(${width} - ${toggleButtonSize})` }), }), } From 277175eda5a82025e18603953ed12f08beb929cc Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Thu, 3 Jan 2019 10:39:45 +0200 Subject: [PATCH 47/62] replaced Icon with unicode char for theme consistency --- src/components/Dropdown/Dropdown.tsx | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index f3a8f9d6bc..9a4753cb50 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -3,7 +3,11 @@ import * as PropTypes from 'prop-types' import * as _ from 'lodash' import { Extendable, ShorthandValue, ComponentEventHandler } from '../../../types/utils' -import { ComponentSlotStylesInput, ComponentVariablesInput } from '../../themes/types' +import { + ComponentSlotStylesInput, + ComponentVariablesInput, + ComponentSlotClasses, +} from '../../themes/types' import Downshift, { DownshiftState, StateChangeOptions, @@ -23,7 +27,6 @@ import { import keyboardKey from 'keyboard-key' import List from '../List/List' import Text from '../Text/Text' -import Icon from '../Icon/Icon' import Ref from '../Ref/Ref' import { UIComponentProps } from '../../lib/commonPropInterfaces' import DropdownItem, { DropdownItemProps } from './DropdownItem' @@ -256,7 +259,7 @@ export default class Dropdown extends AutoControlledComponent< variables, ) : this.renderTriggerButton(styles, getToggleButtonProps)} - {toggleButton && this.renderToggleButton(getToggleButtonProps, styles, isOpen)} + {toggleButton && this.renderToggleButton(getToggleButtonProps, classes, isOpen)} {this.renderItemsList( styles, variables, @@ -349,19 +352,14 @@ export default class Dropdown extends AutoControlledComponent< private renderToggleButton( getToggleButtonProps: (options?: GetToggleButtonPropsOptions) => any, - styles: ComponentSlotStylesInput, + classes: ComponentSlotClasses, isOpen: boolean, ) { - const { onClick, onBlur, onKeyDown, onKeyUp } = getToggleButtonProps() + const { onClick } = getToggleButtonProps() return ( - <Icon - name={`chevron ${isOpen ? 'up' : 'down'}`} - styles={styles.toggleButton} - onClick={onClick} - onBlur={onBlur} - onKeyDown={onKeyDown} - onKeyUp={onKeyUp} - /> + <span className={classes.toggleButton} onClick={onClick}> + {isOpen ? String.fromCharCode(9650) : String.fromCharCode(9660)} + </span> ) } From 83fc34dfcfb45d150c2b43c6d277841d0a4c4f23 Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Thu, 3 Jan 2019 11:58:50 +0200 Subject: [PATCH 48/62] some more review comments handled --- src/components/Dropdown/Dropdown.tsx | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index 9a4753cb50..d8a8395745 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -218,7 +218,8 @@ export default class Dropdown extends AutoControlledComponent< inputValue={search ? searchQuery : undefined} stateReducer={this.handleDownshiftStateChanges} itemToString={itemToString} - // Downshift does not support multiple selection. We will handle everything and pass it selected as null in this case. + // 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} getA11yStatusMessage={getA11yStatusMessage} onStateChange={changes => { @@ -286,13 +287,7 @@ export default class Dropdown extends AutoControlledComponent< ): JSX.Element { const { placeholder, itemToString, multiple } = this.props const { value } = this.state - let content - - if (multiple) { - content = placeholder - } else { - content = value ? itemToString(value) : placeholder - } + const content = value && !multiple ? itemToString(value) : placeholder return ( <Ref innerRef={this.buttonRef}> From cf48f4b89dbb4a8df52c2ffe120a89617d055a88 Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Thu, 3 Jan 2019 12:40:08 +0200 Subject: [PATCH 49/62] some more code improvements in Dropdown --- src/components/Dropdown/Dropdown.tsx | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index d8a8395745..83afe6b3c9 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -29,7 +29,7 @@ import List from '../List/List' import Text from '../Text/Text' import Ref from '../Ref/Ref' import { UIComponentProps } from '../../lib/commonPropInterfaces' -import DropdownItem, { DropdownItemProps } from './DropdownItem' +import DropdownItem from './DropdownItem' import DropdownLabel, { DropdownLabelProps } from './DropdownLabel' import DropdownSearchInput, { DropdownSearchInputProps } from './DropdownSearchInput' import Button from '../Button/Button' @@ -239,11 +239,10 @@ export default class Dropdown extends AutoControlledComponent< highlightedIndex, selectItemAtIndex, }) => { - const accessibilityRootProps = getRootProps( + const { innerRef, ...accessibilityRootPropsRest } = getRootProps( { refKey: 'innerRef' }, { suppressRefError: true }, ) - const { innerRef, ...accessibilityRootPropsRest } = accessibilityRootProps return ( <Ref innerRef={innerRef}> <div @@ -369,9 +368,8 @@ export default class Dropdown extends AutoControlledComponent< getItemProps: (options: GetItemPropsOptions<ShorthandValue>) => any, getInputProps: (options?: GetInputPropsOptions) => any, ) { - const accessibilityMenuProps = { - ...getMenuProps({ refKey: 'innerRef' }, { suppressRefError: true }), - } + const accessibilityMenuProps = getMenuProps({ refKey: 'innerRef' }, { suppressRefError: true }) + // If it's just a selection, some attributes and listeners from Downshift input need to go on the menu list. if (!this.props.search) { const accessibilityInputProps = getInputProps() accessibilityMenuProps['aria-activedescendant'] = @@ -387,7 +385,6 @@ export default class Dropdown extends AutoControlledComponent< } } const { innerRef, ...accessibilityMenuPropsRest } = accessibilityMenuProps - return ( <Ref innerRef={(listNode: HTMLElement) => { @@ -426,8 +423,7 @@ export default class Dropdown extends AutoControlledComponent< key: (item as any).header, }), }, - overrideProps: (predefinedProps: DropdownItemProps) => - this.handleItemOverrides(item, index, getItemProps), + overrideProps: () => this.handleItemOverrides(item, index, getItemProps), }) }) } From b9d5412cc3430cb3ebd0b2777aab1927ce0b78e7 Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Thu, 3 Jan 2019 12:40:28 +0200 Subject: [PATCH 50/62] replaced right margin with padding for combobox --- .../teams/components/Dropdown/dropdownSearchInputStyles.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/themes/teams/components/Dropdown/dropdownSearchInputStyles.ts b/src/themes/teams/components/Dropdown/dropdownSearchInputStyles.ts index bd496b3eb2..16c52581ff 100644 --- a/src/themes/teams/components/Dropdown/dropdownSearchInputStyles.ts +++ b/src/themes/teams/components/Dropdown/dropdownSearchInputStyles.ts @@ -23,7 +23,7 @@ const dropdownSearchInputStyles: ComponentSlotStylesInput< flexBasis: comboboxFlexBasis, flexGrow: 1, ...(hasToggleButton && { - paddingRight: toggleButtonSize, + marginRight: toggleButtonSize, }), }), } From d2ae9e1fd821d75ad8aebdd9bfbe29d6da650a1d Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Thu, 3 Jan 2019 12:59:42 +0200 Subject: [PATCH 51/62] used functional components in the examples --- ...ropdownExampleMultipleSearch.shorthand.tsx | 26 +++++++---------- ...opdownExampleSingleSelection.shorthand.tsx | 24 +++++++-------- ...wnExampleMultipleSearchFluid.shorthand.tsx | 29 ++++++++----------- ...ultipleSearchImageAndContent.shorthand.tsx | 26 +++++++---------- ...leMultipleSearchToggleButton.shorthand.tsx | 28 ++++++++---------- 5 files changed, 56 insertions(+), 77 deletions(-) diff --git a/docs/src/examples/components/Dropdown/Types/DropdownExampleMultipleSearch.shorthand.tsx b/docs/src/examples/components/Dropdown/Types/DropdownExampleMultipleSearch.shorthand.tsx index 4dc983cd0c..6aba57a1d9 100644 --- a/docs/src/examples/components/Dropdown/Types/DropdownExampleMultipleSearch.shorthand.tsx +++ b/docs/src/examples/components/Dropdown/Types/DropdownExampleMultipleSearch.shorthand.tsx @@ -13,21 +13,17 @@ const inputItems = [ 'Selina Kyle', ] -class DropdownExample extends React.Component { - render() { - return ( - <Dropdown - multiple - search - getA11ySelectionMessage={getA11ySelectionMessage} - getA11yStatusMessage={getA11yStatusMessage} - noResultsMessage="We couldn't find any matches." - placeholder="Start typing a name" - items={inputItems} - /> - ) - } -} +const DropdownExample = () => ( + <Dropdown + multiple + search + getA11ySelectionMessage={getA11ySelectionMessage} + getA11yStatusMessage={getA11yStatusMessage} + noResultsMessage="We couldn't find any matches." + placeholder="Start typing a name" + items={inputItems} + /> +) const getA11ySelectionMessage = { onAdd: item => `${item} has been selected.`, diff --git a/docs/src/examples/components/Dropdown/Types/DropdownExampleSingleSelection.shorthand.tsx b/docs/src/examples/components/Dropdown/Types/DropdownExampleSingleSelection.shorthand.tsx index 1145fb1b6c..c4f046d982 100644 --- a/docs/src/examples/components/Dropdown/Types/DropdownExampleSingleSelection.shorthand.tsx +++ b/docs/src/examples/components/Dropdown/Types/DropdownExampleSingleSelection.shorthand.tsx @@ -13,20 +13,16 @@ const inputItems = [ 'Selina Kyle', ] -class DropdownExample extends React.Component { - render() { - return ( - <Dropdown - getA11yStatusMessage={getA11yStatusMessage} - getA11ySelectionMessage={{ - onAdd: item => `${item} has been selected.`, - }} - placeholder="Select your hero" - items={inputItems} - /> - ) - } -} +const DropdownExample = () => ( + <Dropdown + getA11yStatusMessage={getA11yStatusMessage} + getA11ySelectionMessage={{ + onAdd: item => `${item} has been selected.`, + }} + placeholder="Select your hero" + items={inputItems} + /> +) const getA11yStatusMessage = ({ isOpen, diff --git a/docs/src/examples/components/Dropdown/Variations/DropdownExampleMultipleSearchFluid.shorthand.tsx b/docs/src/examples/components/Dropdown/Variations/DropdownExampleMultipleSearchFluid.shorthand.tsx index 7dd60f245a..5d1de96419 100644 --- a/docs/src/examples/components/Dropdown/Variations/DropdownExampleMultipleSearchFluid.shorthand.tsx +++ b/docs/src/examples/components/Dropdown/Variations/DropdownExampleMultipleSearchFluid.shorthand.tsx @@ -12,23 +12,18 @@ const inputItems = [ 'Peter Parker', 'Selina Kyle', ] - -class DropdownExample extends React.Component { - render() { - return ( - <Dropdown - multiple - getA11ySelectionMessage={getA11ySelectionMessage} - getA11yStatusMessage={getA11yStatusMessage} - noResultsMessage="We couldn't find any matches." - search - fluid - placeholder="Start typing a name" - items={inputItems} - /> - ) - } -} +const DropdownExample = () => ( + <Dropdown + multiple + getA11ySelectionMessage={getA11ySelectionMessage} + getA11yStatusMessage={getA11yStatusMessage} + noResultsMessage="We couldn't find any matches." + search + fluid + placeholder="Start typing a name" + items={inputItems} + /> +) const getA11ySelectionMessage = { onAdd: item => `${item} has been selected.`, diff --git a/docs/src/examples/components/Dropdown/Variations/DropdownExampleMultipleSearchImageAndContent.shorthand.tsx b/docs/src/examples/components/Dropdown/Variations/DropdownExampleMultipleSearchImageAndContent.shorthand.tsx index ed52d80f56..248055b67a 100644 --- a/docs/src/examples/components/Dropdown/Variations/DropdownExampleMultipleSearchImageAndContent.shorthand.tsx +++ b/docs/src/examples/components/Dropdown/Variations/DropdownExampleMultipleSearchImageAndContent.shorthand.tsx @@ -49,21 +49,17 @@ const inputItems = [ }, ] -class DropdownExample extends React.Component { - render() { - return ( - <Dropdown - multiple - getA11yStatusMessage={getA11yStatusMessage} - search - getA11ySelectionMessage={getA11ySelectionMessage} - noResultsMessage="We couldn't find any matches." - placeholder="Start typing a name" - items={inputItems} - /> - ) - } -} +const DropdownExample = () => ( + <Dropdown + multiple + getA11yStatusMessage={getA11yStatusMessage} + search + getA11ySelectionMessage={getA11ySelectionMessage} + noResultsMessage="We couldn't find any matches." + placeholder="Start typing a name" + items={inputItems} + /> +) const getA11ySelectionMessage = { onAdd: item => `${item.header} has been selected.`, diff --git a/docs/src/examples/components/Dropdown/Variations/DropdownExampleMultipleSearchToggleButton.shorthand.tsx b/docs/src/examples/components/Dropdown/Variations/DropdownExampleMultipleSearchToggleButton.shorthand.tsx index 1f85c9cc1b..3fb6470421 100644 --- a/docs/src/examples/components/Dropdown/Variations/DropdownExampleMultipleSearchToggleButton.shorthand.tsx +++ b/docs/src/examples/components/Dropdown/Variations/DropdownExampleMultipleSearchToggleButton.shorthand.tsx @@ -13,22 +13,18 @@ const inputItems = [ 'Selina Kyle', ] -class DropdownExample extends React.Component { - render() { - return ( - <Dropdown - multiple - getA11yStatusMessage={getA11yStatusMessage} - getA11ySelectionMessage={getA11ySelectionMessage} - noResultsMessage="We couldn't find any matches." - search - placeholder="Start typing a name" - toggleButton - items={inputItems} - /> - ) - } -} +const DropdownExample = () => ( + <Dropdown + multiple + getA11yStatusMessage={getA11yStatusMessage} + getA11ySelectionMessage={getA11ySelectionMessage} + noResultsMessage="We couldn't find any matches." + search + placeholder="Start typing a name" + toggleButton + items={inputItems} + /> +) const getA11ySelectionMessage = { onAdd: item => `${item} has been selected.`, From 5882ab8e27bc2a61506f4cb3b359a7edb9969256 Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Thu, 3 Jan 2019 13:11:15 +0200 Subject: [PATCH 52/62] overriding downshift aria label for the toggle button --- src/components/Dropdown/Dropdown.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index 83afe6b3c9..1ac2100cae 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -301,6 +301,7 @@ export default class Dropdown extends AutoControlledComponent< onBlur: () => { this.setState({ focused: false }) }, + 'aria-label': content, })} /> </Ref> From 0350af66b3467ae75311a5cadd54c81ac06ea25d Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Thu, 3 Jan 2019 13:21:45 +0200 Subject: [PATCH 53/62] made the toggle button arrow non-selectable --- src/themes/teams/components/Dropdown/dropdownStyles.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/themes/teams/components/Dropdown/dropdownStyles.ts b/src/themes/teams/components/Dropdown/dropdownStyles.ts index fbf29496cf..a69d19057d 100644 --- a/src/themes/teams/components/Dropdown/dropdownStyles.ts +++ b/src/themes/teams/components/Dropdown/dropdownStyles.ts @@ -87,6 +87,7 @@ const dropdownStyles: ComponentSlotStylesInput<DropdownProps, DropdownVariables> display: 'flex', justifyContent: 'center', alignItems: 'center', + userSelect: 'none', ...(fluid ? { right: 0 } : { left: `calc(${width} - ${toggleButtonSize})` }), }), } From d13c08975963f8db13e1cbdd0ec195411abe70f5 Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Thu, 3 Jan 2019 15:29:51 +0200 Subject: [PATCH 54/62] used a lodash util instead of filter --- src/components/Dropdown/Dropdown.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index 1ac2100cae..46dfe032b6 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -498,7 +498,7 @@ export default class Dropdown extends AutoControlledComponent< let filteredItems = items if (multiple) { - filteredItems = filteredItems.filter(item => (value as ShorthandValue[]).indexOf(item) === -1) + filteredItems = _.difference(filteredItems, value as ShorthandValue[]) } if (search) { if (_.isFunction(search)) { From 40744a7e4ee213a296a0d72c586e5665739b6f7b Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Fri, 4 Jan 2019 10:59:52 +0200 Subject: [PATCH 55/62] used buttonNode for consistency --- src/components/Dropdown/Dropdown.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index 46dfe032b6..6acfd8c61c 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -133,9 +133,9 @@ export default class Dropdown extends AutoControlledComponent< Extendable<DropdownProps>, DropdownState > { + private buttonNode: HTMLElement private inputNode: HTMLElement private listNode: HTMLElement - private buttonRef = React.createRef<HTMLElement>() static displayName = 'Dropdown' @@ -289,7 +289,11 @@ export default class Dropdown extends AutoControlledComponent< const content = value && !multiple ? itemToString(value) : placeholder return ( - <Ref innerRef={this.buttonRef}> + <Ref + innerRef={(buttonNode: HTMLElement) => { + this.buttonNode = buttonNode + }} + > <Button content={content} fluid @@ -664,7 +668,7 @@ export default class Dropdown extends AutoControlledComponent< return case keyboardKey.Escape: accessibilityInputPropsKeyDown(e) - this.buttonRef.current.focus() + this.buttonNode.focus() return default: accessibilityInputPropsKeyDown(e) @@ -684,7 +688,7 @@ export default class Dropdown extends AutoControlledComponent< this.setA11yStatus(getA11ySelectionMessage.onAdd(item)) } if (!search) { - this.buttonRef.current.focus() + this.buttonNode.focus() } // we don't have event for it, but want to keep the event handling interface, event is empty. From 390999af195b09c7dd474c3b04d5ff78a8431b6c Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Fri, 4 Jan 2019 11:22:35 +0200 Subject: [PATCH 56/62] apply tabIndex only if dropdown is non-search --- src/components/Dropdown/Dropdown.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index 6acfd8c61c..7c8843bda4 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -374,8 +374,9 @@ export default class Dropdown extends AutoControlledComponent< getInputProps: (options?: GetInputPropsOptions) => any, ) { const accessibilityMenuProps = getMenuProps({ refKey: 'innerRef' }, { suppressRefError: true }) + const { search } = this.props // If it's just a selection, some attributes and listeners from Downshift input need to go on the menu list. - if (!this.props.search) { + if (!search) { const accessibilityInputProps = getInputProps() accessibilityMenuProps['aria-activedescendant'] = accessibilityInputProps['aria-activedescendant'] @@ -400,7 +401,7 @@ export default class Dropdown extends AutoControlledComponent< <List {...accessibilityMenuPropsRest} styles={styles.list} - tabIndex={-1} + tabIndex={search ? undefined : -1} // needs to be focused when trigger button is activated. aria-hidden={!isOpen} items={isOpen ? this.renderItems(styles, variables, getItemProps, highlightedIndex) : []} /> From 6c547066936b14df9baa93b3c5d17b370fa0ee91 Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Fri, 4 Jan 2019 11:23:19 +0200 Subject: [PATCH 57/62] Revert "used buttonNode for consistency" This reverts commit 97758c282ca0a694a08a7a913e2bdfe608950f95. --- src/components/Dropdown/Dropdown.tsx | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index 7c8843bda4..83d6967feb 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -133,9 +133,9 @@ export default class Dropdown extends AutoControlledComponent< Extendable<DropdownProps>, DropdownState > { - private buttonNode: HTMLElement private inputNode: HTMLElement private listNode: HTMLElement + private buttonRef = React.createRef<HTMLElement>() static displayName = 'Dropdown' @@ -289,11 +289,7 @@ export default class Dropdown extends AutoControlledComponent< const content = value && !multiple ? itemToString(value) : placeholder return ( - <Ref - innerRef={(buttonNode: HTMLElement) => { - this.buttonNode = buttonNode - }} - > + <Ref innerRef={this.buttonRef}> <Button content={content} fluid @@ -669,7 +665,7 @@ export default class Dropdown extends AutoControlledComponent< return case keyboardKey.Escape: accessibilityInputPropsKeyDown(e) - this.buttonNode.focus() + this.buttonRef.current.focus() return default: accessibilityInputPropsKeyDown(e) @@ -689,7 +685,7 @@ export default class Dropdown extends AutoControlledComponent< this.setA11yStatus(getA11ySelectionMessage.onAdd(item)) } if (!search) { - this.buttonNode.focus() + this.buttonRef.current.focus() } // we don't have event for it, but want to keep the event handling interface, event is empty. From 0c9767800a61ad8ac2bcb2d2fd048c6d11c69559 Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Fri, 4 Jan 2019 12:51:59 +0200 Subject: [PATCH 58/62] refactored the Refs use in Dropdown --- src/components/Dropdown/Dropdown.tsx | 23 +++++++------- .../Dropdown/DropdownSearchInput.tsx | 24 +++++++-------- src/components/Input/Input.tsx | 30 ++++++++----------- 3 files changed, 35 insertions(+), 42 deletions(-) diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index 83d6967feb..98b3183ffc 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -23,6 +23,7 @@ import { RenderResultConfig, customPropTypes, commonPropTypes, + handleRef, } from '../../lib' import keyboardKey from 'keyboard-key' import List from '../List/List' @@ -133,9 +134,9 @@ export default class Dropdown extends AutoControlledComponent< Extendable<DropdownProps>, DropdownState > { - private inputNode: HTMLElement - private listNode: HTMLElement private buttonRef = React.createRef<HTMLElement>() + private inputRef = React.createRef<HTMLElement>() + private listRef = React.createRef<HTMLElement>() static displayName = 'Dropdown' @@ -224,7 +225,7 @@ export default class Dropdown extends AutoControlledComponent< getA11yStatusMessage={getA11yStatusMessage} onStateChange={changes => { if (changes.isOpen && !search) { - this.listNode.focus() + this.listRef.current.focus() } }} > @@ -330,9 +331,7 @@ export default class Dropdown extends AutoControlledComponent< placeholder: noPlaceholder ? '' : placeholder, hasToggleButton: !!toggleButton, variables, - inputRef: (inputNode: HTMLElement) => { - this.inputNode = inputNode - }, + inputRef: this.inputRef, }, overrideProps: (predefinedProps: DropdownSearchInputProps) => this.handleSearchInputOverrides( @@ -389,9 +388,9 @@ export default class Dropdown extends AutoControlledComponent< const { innerRef, ...accessibilityMenuPropsRest } = accessibilityMenuProps return ( <Ref - innerRef={(listNode: HTMLElement) => { - this.listNode = listNode - innerRef(listNode) + innerRef={(listElement: HTMLElement) => { + handleRef(this.listRef, listElement) + handleRef(innerRef, listElement) }} > <List @@ -485,7 +484,7 @@ export default class Dropdown extends AutoControlledComponent< case Downshift.stateChangeTypes.blurButton: // Downshift closes the list by default on trigger blur. It does not support the case when dropdown is // single selection and focuses list on trigger click/up/down/space/enter. Treating that here. - if (state.isOpen && document.activeElement === this.listNode) { + if (state.isOpen && document.activeElement === this.listRef.current) { return {} // won't change state in this case. } default: @@ -645,7 +644,7 @@ export default class Dropdown extends AutoControlledComponent< } private handleContainerClick = (isOpen: boolean) => { - !isOpen && this.inputNode.focus() + !isOpen && this.inputRef.current.focus() } private handleListKeyDown = ( @@ -694,7 +693,7 @@ export default class Dropdown extends AutoControlledComponent< private handleSelectedItemRemove(e: React.SyntheticEvent, item: ShorthandValue) { this.removeItemFromValue(item) - this.inputNode.focus() + this.inputRef.current.focus() e.stopPropagation() } diff --git a/src/components/Dropdown/DropdownSearchInput.tsx b/src/components/Dropdown/DropdownSearchInput.tsx index a6ed33276a..5d967a245d 100644 --- a/src/components/Dropdown/DropdownSearchInput.tsx +++ b/src/components/Dropdown/DropdownSearchInput.tsx @@ -10,12 +10,9 @@ import Input from '../Input/Input' export interface DropdownSearchInputProps extends UIComponentProps<DropdownSearchInputProps> { /** Informs the search input about an existing toggle button. */ hasToggleButton?: boolean - /** - * Ref callback with an input DOM node. - * - * @param {JSX.Element} node - input DOM node. - */ - inputRef?: (inputNode: HTMLElement) => void + + /** Ref for input DOM node. */ + inputRef?: React.Ref<HTMLElement> /** * Called on input element focus. @@ -71,7 +68,7 @@ class DropdownSearchInput extends UIComponent<ReactProps<DropdownSearchInputProp accessibilityInputProps: PropTypes.object, accessibilityComboboxProps: PropTypes.object, hasToggleButton: PropTypes.bool, - inputRef: PropTypes.func, + inputRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), onFocus: PropTypes.func, onInputBlur: PropTypes.func, onInputKeyDown: PropTypes.func, @@ -79,10 +76,6 @@ class DropdownSearchInput extends UIComponent<ReactProps<DropdownSearchInputProp placeholder: PropTypes.string, } - private handleInputRef = (inputNode: HTMLElement) => { - _.invoke(this.props, 'inputRef', inputNode) - } - private handleFocus = (e: React.SyntheticEvent) => { _.invoke(this.props, 'onFocus', e, this.props) } @@ -100,10 +93,15 @@ class DropdownSearchInput extends UIComponent<ReactProps<DropdownSearchInputProp } public renderComponent({ rest, styles }: RenderResultConfig<DropdownSearchInputProps>) { - const { accessibilityComboboxProps, accessibilityInputProps, placeholder } = this.props + const { + accessibilityComboboxProps, + accessibilityInputProps, + inputRef, + placeholder, + } = this.props return ( <Input - inputRef={this.handleInputRef} + inputRef={inputRef} onFocus={this.handleFocus} onKeyUp={this.handleKeyUp} wrapper={{ diff --git a/src/components/Input/Input.tsx b/src/components/Input/Input.tsx index 8eee55fa30..73da8e5f84 100644 --- a/src/components/Input/Input.tsx +++ b/src/components/Input/Input.tsx @@ -11,6 +11,7 @@ import { UIComponentProps, ChildrenComponentProps, commonPropTypes, + handleRef, } from '../../lib' import { ReactProps, ShorthandValue, ComponentEventHandler } from '../../../types/utils' import Icon from '../Icon/Icon' @@ -50,12 +51,8 @@ export interface InputProps extends UIComponentProps, ChildrenComponentProps { /** The HTML input type. */ type?: string - /** - * Ref callback with an input DOM node. - * - * @param {JSX.Element} node - input DOM node. - */ - inputRef?: (node: HTMLElement) => void + /** Ref for input DOM node. */ + inputRef?: React.Ref<HTMLElement> /** The value of the input. */ value?: React.ReactText @@ -77,7 +74,7 @@ export interface InputState { * - if input is search, then use "role='search'" */ class Input extends AutoControlledComponent<ReactProps<InputProps>, InputState> { - private inputDomElement: HTMLInputElement + private inputRef = React.createRef<HTMLElement>() static className = 'ui-input' @@ -93,7 +90,7 @@ class Input extends AutoControlledComponent<ReactProps<InputProps>, InputState> icon: customPropTypes.itemShorthand, iconPosition: PropTypes.oneOf(['start', 'end']), input: customPropTypes.itemShorthand, - inputRef: PropTypes.func, + inputRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), inline: PropTypes.bool, onChange: PropTypes.func, type: PropTypes.string, @@ -116,7 +113,7 @@ class Input extends AutoControlledComponent<ReactProps<InputProps>, InputState> styles, variables, }: RenderResultConfig<InputProps>) { - const { className, input, type, wrapper } = this.props + const { className, input, inputRef, type, wrapper } = this.props const { value = '' } = this.state const [htmlInputProps, rest] = partitionHTMLProps(restProps) @@ -125,7 +122,12 @@ class Input extends AutoControlledComponent<ReactProps<InputProps>, InputState> className: cx(Input.className, className), children: ( <> - <Ref innerRef={this.handleInputRef}> + <Ref + innerRef={(inputElement: HTMLElement) => { + handleRef(this.inputRef, inputElement) + handleRef(inputRef, inputElement) + }} + > {Slot.create(input || type, { defaultProps: { ...htmlInputProps, @@ -155,16 +157,10 @@ class Input extends AutoControlledComponent<ReactProps<InputProps>, InputState> }) } - private handleInputRef = (inputNode: HTMLElement) => { - this.inputDomElement = inputNode as HTMLInputElement - - _.invoke(this.props, 'inputRef', inputNode) - } - private handleIconOverrides = predefinedProps => ({ onClick: (e: React.SyntheticEvent) => { this.handleOnClear() - this.inputDomElement.focus() + this.inputRef.current.focus() _.invoke(predefinedProps, 'onClick', e, this.props) }, ...(predefinedProps.onClick && { tabIndex: '0' }), From 49962603f79e529e24f8b052280efe6bae9d759f Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Fri, 4 Jan 2019 15:15:45 +0200 Subject: [PATCH 59/62] updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa76ff2292..a401fc3cdb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Add `color` prop to `Icon` component @Bugaa92 ([#651](https://github.com/stardust-ui/react/pull/651)) - Create a `base` theme with Text component example @almedint ([#618](https://github.com/stardust-ui/react/pull/618)) - Adding attachment behavior to handle space/enter key @kolaps33 ([#375](https://github.com/stardust-ui/react/pull/375)) +- Add Dropdown Single Selection variant @silviuavram ([#584](https://github.com/stardust-ui/react/pull/584)) ### Documentation - Add more accessibility descriptions to components and behaviors @jurokapsiar ([#648](https://github.com/stardust-ui/react/pull/648)) From 2b7f913171ca02eb56f5157f604e5ca5c490f09b Mon Sep 17 00:00:00 2001 From: manajdov <manajdov@microsoft.com> Date: Tue, 8 Jan 2019 11:46:38 +0100 Subject: [PATCH 60/62] -added white background on the list in the Dropdown for fixing transparent scrollbar --- src/themes/teams/components/Dropdown/dropdownStyles.ts | 6 +++++- src/themes/teams/components/Dropdown/dropdownVariables.ts | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/themes/teams/components/Dropdown/dropdownStyles.ts b/src/themes/teams/components/Dropdown/dropdownStyles.ts index a69d19057d..f06a82774f 100644 --- a/src/themes/teams/components/Dropdown/dropdownStyles.ts +++ b/src/themes/teams/components/Dropdown/dropdownStyles.ts @@ -64,13 +64,17 @@ const dropdownStyles: ComponentSlotStylesInput<DropdownProps, DropdownVariables> margin: '.4rem 0 0 .4rem', }), - list: ({ variables: { listMaxHeight, width }, props: { fluid } }): ICSSInJSStyle => ({ + list: ({ + variables: { listMaxHeight, width, listBackgroundColor }, + props: { fluid }, + }): ICSSInJSStyle => ({ position: 'absolute', zIndex: 1000, maxHeight: listMaxHeight, overflowY: 'auto', width: fluid ? '100%' : width, top: 'calc(100% + 2px)', // leave room for container + its border + background: listBackgroundColor, }), emptyListItem: ({ variables: { listItemBackgroundColor } }) => ({ diff --git a/src/themes/teams/components/Dropdown/dropdownVariables.ts b/src/themes/teams/components/Dropdown/dropdownVariables.ts index d92e33e739..489bbccc4a 100644 --- a/src/themes/teams/components/Dropdown/dropdownVariables.ts +++ b/src/themes/teams/components/Dropdown/dropdownVariables.ts @@ -10,6 +10,7 @@ export interface DropdownVariables { comboboxPaddingButton: string comboboxPaddingInput: string comboboxFlexBasis: string + listBackgroundColor: string listItemBackgroundColor: string listItemBackgroundColorActive: string listItemColorActive: string @@ -31,6 +32,7 @@ export default (siteVars): DropdownVariables => ({ comboboxPaddingButton: `0 ${_12px_asRem}`, comboboxPaddingInput: `${_6px_asRem} ${_12px_asRem}`, comboboxFlexBasis: '50px', + listBackgroundColor: siteVars.white, listItemBackgroundColor: siteVars.white, listItemBackgroundColorActive: siteVars.brand, listItemColorActive: siteVars.white, From 5d492294e13dd2d46f1d4cda7aa9047347ae38ca Mon Sep 17 00:00:00 2001 From: manajdov <manajdov@microsoft.com> Date: Tue, 8 Jan 2019 11:59:17 +0100 Subject: [PATCH 61/62] -fixed changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c875f78b88..071e6b5002 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Features - Add `on` and `mouseLeaveDelay` props to `Popup` component @mnajdova ([#622](https://github.com/stardust-ui/react/pull/622)) +- Add Dropdown Single Selection variant @silviuavram ([#584](https://github.com/stardust-ui/react/pull/584)) <!--------------------------------[ v0.16.0 ]------------------------------- --> ## [v0.16.0](https://github.com/stardust-ui/react/tree/v0.16.0) (2019-01-07) @@ -53,7 +54,6 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Add `color` prop to `Icon` component @Bugaa92 ([#651](https://github.com/stardust-ui/react/pull/651)) - Create a `base` theme with Text component example @almedint ([#618](https://github.com/stardust-ui/react/pull/618)) - Adding attachment behavior to handle space/enter key @kolaps33 ([#375](https://github.com/stardust-ui/react/pull/375)) -- Add Dropdown Single Selection variant @silviuavram ([#584](https://github.com/stardust-ui/react/pull/584)) ### Documentation - Add more accessibility descriptions to components and behaviors @jurokapsiar ([#648](https://github.com/stardust-ui/react/pull/648)) From f17b5b194dcea618aad7c48097ecc83fb6bccc18 Mon Sep 17 00:00:00 2001 From: Silviu Avram <siavram@microsoft.com> Date: Tue, 8 Jan 2019 15:28:41 +0100 Subject: [PATCH 62/62] removed '.shorthand' from examplePath --- docs/src/examples/components/Dropdown/Types/index.tsx | 4 ++-- docs/src/examples/components/Dropdown/Variations/index.tsx | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/src/examples/components/Dropdown/Types/index.tsx b/docs/src/examples/components/Dropdown/Types/index.tsx index dc2a7499ef..9b11169a48 100644 --- a/docs/src/examples/components/Dropdown/Types/index.tsx +++ b/docs/src/examples/components/Dropdown/Types/index.tsx @@ -7,12 +7,12 @@ const Types = () => ( <ComponentExample title="Single Selection" description="A dropdown with single selection." - examplePath="components/Dropdown/Types/DropdownExampleSingleSelection.shorthand" + examplePath="components/Dropdown/Types/DropdownExampleSingleSelection" /> <ComponentExample title="Multiple Search" description="A dropdown with multiple selection and search." - examplePath="components/Dropdown/Types/DropdownExampleMultipleSearch.shorthand" + examplePath="components/Dropdown/Types/DropdownExampleMultipleSearch" /> </ExampleSection> ) diff --git a/docs/src/examples/components/Dropdown/Variations/index.tsx b/docs/src/examples/components/Dropdown/Variations/index.tsx index cd3ff955f8..ca3ecfd32f 100644 --- a/docs/src/examples/components/Dropdown/Variations/index.tsx +++ b/docs/src/examples/components/Dropdown/Variations/index.tsx @@ -7,17 +7,17 @@ const Variations = () => ( <ComponentExample title="Multiple Search with Image and Content" description="A multiple search dropdown which items have header, content and image." - examplePath="components/Dropdown/Variations/DropdownExampleMultipleSearchImageAndContent.shorthand" + examplePath="components/Dropdown/Variations/DropdownExampleMultipleSearchImageAndContent" /> <ComponentExample title="Multiple Search Fluid" description="A multiple search dropdown that fits the width of the container." - examplePath="components/Dropdown/Variations/DropdownExampleMultipleSearchFluid.shorthand" + examplePath="components/Dropdown/Variations/DropdownExampleMultipleSearchFluid" /> <ComponentExample title="Multiple Search with Toggle Button" description="A multiple search dropdown with toggle button that shows/hides the items list." - examplePath="components/Dropdown/Variations/DropdownExampleMultipleSearchToggleButton.shorthand" + examplePath="components/Dropdown/Variations/DropdownExampleMultipleSearchToggleButton" /> </ExampleSection> )