From 1cc2977de4a2594e43a572a41ab82cdf3ed469bd Mon Sep 17 00:00:00 2001 From: Oleksandr Fediashov Date: Mon, 11 Feb 2019 17:35:42 +0200 Subject: [PATCH 1/9] feat(Dropdown): add `clearable` prop --- ...ropdownExampleClearable.shorthand.steps.ts | 16 +++++ .../DropdownExampleClearable.shorthand.tsx | 20 ++++++ .../components/Dropdown/Types/index.tsx | 5 ++ .../src/components/Dropdown/Dropdown.tsx | 62 +++++++++++++++---- .../components/Dropdown/dropdownStyles.ts | 16 +++++ 5 files changed, 108 insertions(+), 11 deletions(-) create mode 100644 docs/src/examples/components/Dropdown/Types/DropdownExampleClearable.shorthand.steps.ts create mode 100644 docs/src/examples/components/Dropdown/Types/DropdownExampleClearable.shorthand.tsx diff --git a/docs/src/examples/components/Dropdown/Types/DropdownExampleClearable.shorthand.steps.ts b/docs/src/examples/components/Dropdown/Types/DropdownExampleClearable.shorthand.steps.ts new file mode 100644 index 0000000000..7f559de10f --- /dev/null +++ b/docs/src/examples/components/Dropdown/Types/DropdownExampleClearable.shorthand.steps.ts @@ -0,0 +1,16 @@ +import { Dropdown } from '@stardust-ui/react' + +const selectors = { + clearIndicator: `.${Dropdown.slotClassNames.clearIndicator}`, + triggerButton: `.${Dropdown.slotClassNames.triggerButton}`, + item: (itemIndex: number) => `.${Dropdown.slotClassNames.itemsList} li:nth-child(${itemIndex})`, +} + +const steps = [ + steps => steps.click(selectors.triggerButton).snapshot('Shows list'), + steps => steps.click(selectors.item(3)).snapshot('Selects an item'), + steps => steps.click(selectors.clearIndicator).snapshot('Clears the value'), + steps => steps.click(selectors.triggerButton).snapshot('Closes the list'), +] + +export default steps diff --git a/docs/src/examples/components/Dropdown/Types/DropdownExampleClearable.shorthand.tsx b/docs/src/examples/components/Dropdown/Types/DropdownExampleClearable.shorthand.tsx new file mode 100644 index 0000000000..49280890df --- /dev/null +++ b/docs/src/examples/components/Dropdown/Types/DropdownExampleClearable.shorthand.tsx @@ -0,0 +1,20 @@ +import { Dropdown } from '@stardust-ui/react' +import * as React from 'react' + +const inputItems = [ + 'Bruce Wayne', + 'Natasha Romanoff', + 'Steven Strange', + 'Alfred Pennyworth', + `Scarlett O'Hara`, + 'Imperator Furiosa', + 'Bruce Banner', + 'Peter Parker', + 'Selina Kyle', +] + +const DropdownClearableExample = () => ( + +) + +export default DropdownClearableExample diff --git a/docs/src/examples/components/Dropdown/Types/index.tsx b/docs/src/examples/components/Dropdown/Types/index.tsx index 295af8189e..d4fba41a93 100644 --- a/docs/src/examples/components/Dropdown/Types/index.tsx +++ b/docs/src/examples/components/Dropdown/Types/index.tsx @@ -24,6 +24,11 @@ const Types = () => ( description="A dropdown can be searchable and allow a multiple selection." examplePath="components/Dropdown/Types/DropdownExampleSearchMultiple" /> + , Dropdo content: false, }), activeSelectedIndex: PropTypes.number, + clearable: PropTypes.bool, + clearIndicator: customPropTypes.itemShorthand, defaultActiveSelectedIndex: PropTypes.number, defaultSearchQuery: PropTypes.string, defaultValue: PropTypes.oneOfType([ @@ -226,6 +236,7 @@ class Dropdown extends AutoControlledComponent, Dropdo static defaultProps: DropdownProps = { as: 'div', + clearIndicator: 'close', itemToString: item => { if (!item || React.isValidElement(item)) { return '' @@ -263,8 +274,16 @@ class Dropdown extends AutoControlledComponent, Dropdo unhandledProps, rtl, }: RenderResultConfig) { - const { search, multiple, getA11yStatusMessage, itemToString, toggleIndicator } = this.props - const { defaultHighlightedIndex, searchQuery } = this.state + const { + clearable, + clearIndicator, + search, + multiple, + getA11yStatusMessage, + itemToString, + toggleIndicator, + } = this.props + const { defaultHighlightedIndex, searchQuery, value } = this.state return ( @@ -293,6 +312,8 @@ class Dropdown extends AutoControlledComponent, Dropdo { refKey: 'innerRef' }, { suppressRefError: true }, ) + const showClearIndicator = clearable && !this.isValueEmpty(value) + return (
, Dropdo ) : this.renderTriggerButton(styles, rtl, getToggleButtonProps)}
- {Indicator.create(toggleIndicator, { - defaultProps: { - direction: isOpen ? 'top' : 'bottom', - onClick: getToggleButtonProps().onClick, - styles: styles.toggleIndicator, - }, - })} + {showClearIndicator + ? Icon.create(clearIndicator, { + defaultProps: { + className: Dropdown.slotClassNames.clearIndicator, + onClick: this.handleClear, + styles: styles.clearIndicator, + xSpacing: 'none', + }, + }) + : Indicator.create(toggleIndicator, { + defaultProps: { + direction: isOpen ? 'top' : 'bottom', + onClick: getToggleButtonProps().onClick, + styles: styles.toggleIndicator, + }, + })} {this.renderItemsList( styles, variables, @@ -748,6 +778,12 @@ class Dropdown extends AutoControlledComponent, Dropdo } } + private handleClear = () => { + const { multiple } = this.props + + this.setState({ value: multiple ? [] : '' }) + } + private handleContainerClick = () => { this.tryFocusSearchInput() } @@ -932,9 +968,8 @@ class Dropdown extends AutoControlledComponent, Dropdo */ private getSelectedItemAsString = (value: ShorthandValue): string => { const { itemToString, multiple, placeholder } = this.props - const isValueEmpty = _.isArray(value) ? value.length < 1 : !value - if (isValueEmpty) { + if (this.isValueEmpty(value)) { return placeholder } @@ -944,10 +979,15 @@ class Dropdown extends AutoControlledComponent, Dropdo return itemToString(value) } + + private isValueEmpty = (value: ShorthandValue | ShorthandValue[]) => { + return _.isArray(value) ? value.length < 1 : !value + } } Dropdown.slotClassNames = { container: `${Dropdown.className}__container`, + clearIndicator: `${Dropdown.className}__clear-indicator`, triggerButton: `${Dropdown.className}__trigger-button`, itemsList: `${Dropdown.className}__items-list`, selectedItems: `${Dropdown.className}__selected-items`, diff --git a/packages/react/src/themes/teams/components/Dropdown/dropdownStyles.ts b/packages/react/src/themes/teams/components/Dropdown/dropdownStyles.ts index 38c06c13ac..99b6c6a170 100644 --- a/packages/react/src/themes/teams/components/Dropdown/dropdownStyles.ts +++ b/packages/react/src/themes/teams/components/Dropdown/dropdownStyles.ts @@ -40,6 +40,22 @@ const dropdownStyles: ComponentSlotStylesInput ({ + alignItems: 'center', + display: 'flex', + justifyContent: 'center', + + backgroundColor: 'transparent', + cursor: 'pointer', + userSelect: 'none', + + margin: 0, + position: 'absolute', + right: pxToRem(5), + height: v.toggleIndicatorSize, + width: v.toggleIndicatorSize, + }), + container: ({ props: p, variables: v }): ICSSInJSStyle => ({ display: 'flex', flexWrap: 'wrap', From afa62f34375ab01a74b915fa33f4e56d153dc128 Mon Sep 17 00:00:00 2001 From: Oleksandr Fediashov Date: Tue, 12 Feb 2019 13:27:55 +0200 Subject: [PATCH 2/9] update steps --- .../Dropdown/Types/DropdownExampleClearable.shorthand.steps.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/src/examples/components/Dropdown/Types/DropdownExampleClearable.shorthand.steps.ts b/docs/src/examples/components/Dropdown/Types/DropdownExampleClearable.shorthand.steps.ts index 7f559de10f..4cd44383cb 100644 --- a/docs/src/examples/components/Dropdown/Types/DropdownExampleClearable.shorthand.steps.ts +++ b/docs/src/examples/components/Dropdown/Types/DropdownExampleClearable.shorthand.steps.ts @@ -10,7 +10,6 @@ const steps = [ steps => steps.click(selectors.triggerButton).snapshot('Shows list'), steps => steps.click(selectors.item(3)).snapshot('Selects an item'), steps => steps.click(selectors.clearIndicator).snapshot('Clears the value'), - steps => steps.click(selectors.triggerButton).snapshot('Closes the list'), ] export default steps From 2f7bae2a28c20e368f1141d1d2f7d6337ab3bc32 Mon Sep 17 00:00:00 2001 From: Oleksandr Fediashov Date: Tue, 12 Feb 2019 15:53:21 +0200 Subject: [PATCH 3/9] add toc --- .github/test-a-feature.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/test-a-feature.md b/.github/test-a-feature.md index e0e92e08fc..f5a9543cd4 100644 --- a/.github/test-a-feature.md +++ b/.github/test-a-feature.md @@ -17,6 +17,14 @@ Test a feature - [Important mentions:](#important-mentions) - [Run Screener tests](#run-screener-tests) - [Local run command](#local-run-command) +- [Behavior tets](#behavior-tets) + - [Adding test(s)](#adding-tests) + - [Running test(s)](#running-tests) + - [Troubleshooting](#troubleshooting) + - [I am not sure if my line under `@specification` was process correctly](#i-am-not-sure-if-my-line-under-specification-was-process-correctly) + - [I am not sure if my line was executed](#i-am-not-sure-if-my-line-was-executed) + - [I want to add any description which should not be consider as unit test](#i-want-to-add-any-description-which-should-not-be-consider-as-unit-test) + - [I want to create unit tests in separate file not through the regex](#i-want-to-create-unit-tests-in-separate-file-not-through-the-regex) From 8acec0f30d2a0d22d01bb9f058851c43d2089372 Mon Sep 17 00:00:00 2001 From: Oleksandr Fediashov Date: Tue, 12 Feb 2019 15:53:38 +0200 Subject: [PATCH 4/9] fix focusing --- packages/react/src/components/Dropdown/Dropdown.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/react/src/components/Dropdown/Dropdown.tsx b/packages/react/src/components/Dropdown/Dropdown.tsx index a00780526a..7b49ef3bf1 100644 --- a/packages/react/src/components/Dropdown/Dropdown.tsx +++ b/packages/react/src/components/Dropdown/Dropdown.tsx @@ -782,6 +782,9 @@ class Dropdown extends AutoControlledComponent, Dropdo const { multiple } = this.props this.setState({ value: multiple ? [] : '' }) + + this.tryFocusSearchInput() + this.tryFocusTriggerButton() } private handleContainerClick = () => { From 346493d1e451340f85713e0d42de3f0e0ad52b21 Mon Sep 17 00:00:00 2001 From: Oleksandr Fediashov Date: Tue, 12 Feb 2019 15:56:35 +0200 Subject: [PATCH 5/9] fix `handleClear` method --- packages/react/src/components/Dropdown/Dropdown.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react/src/components/Dropdown/Dropdown.tsx b/packages/react/src/components/Dropdown/Dropdown.tsx index 7b49ef3bf1..833ff10d02 100644 --- a/packages/react/src/components/Dropdown/Dropdown.tsx +++ b/packages/react/src/components/Dropdown/Dropdown.tsx @@ -779,9 +779,9 @@ class Dropdown extends AutoControlledComponent, Dropdo } private handleClear = () => { - const { multiple } = this.props + const initialState = this.getInitialAutoControlledState(this.props) - this.setState({ value: multiple ? [] : '' }) + this.setState({ value: initialState.value }) this.tryFocusSearchInput() this.tryFocusTriggerButton() From b8c5aa6813dff6190e0d8dea4ee1f86993d1b02a Mon Sep 17 00:00:00 2001 From: Oleksandr Fediashov Date: Tue, 12 Feb 2019 15:59:42 +0200 Subject: [PATCH 6/9] extract styles --- .../components/Dropdown/dropdownStyles.ts | 50 ++++++++----------- 1 file changed, 21 insertions(+), 29 deletions(-) diff --git a/packages/react/src/themes/teams/components/Dropdown/dropdownStyles.ts b/packages/react/src/themes/teams/components/Dropdown/dropdownStyles.ts index 99b6c6a170..18d9ca09a9 100644 --- a/packages/react/src/themes/teams/components/Dropdown/dropdownStyles.ts +++ b/packages/react/src/themes/teams/components/Dropdown/dropdownStyles.ts @@ -1,4 +1,4 @@ -import { ComponentSlotStylesInput, ICSSInJSStyle } from '../../../types' +import { ComponentSlotStyle, ComponentSlotStylesInput, ICSSInJSStyle } from '../../../types' import { DropdownProps, DropdownState } from '../../../../components/Dropdown/Dropdown' import { DropdownVariables } from './dropdownVariables' import { pxToRem } from '../../../../lib' @@ -21,6 +21,24 @@ const transparentColorStyleObj: ICSSInJSStyle = { }, } +const getIndicatorStyles: ComponentSlotStyle = ({ + variables: v, +}): ICSSInJSStyle => ({ + alignItems: 'center', + display: 'flex', + justifyContent: 'center', + + backgroundColor: 'transparent', + cursor: 'pointer', + userSelect: 'none', + + margin: 0, + position: 'absolute', + right: pxToRem(5), + height: v.toggleIndicatorSize, + width: v.toggleIndicatorSize, +}) + const getWidth = (p: DropdownPropsAndState, v: DropdownVariables): string => { if (p.fluid) { return '100%' @@ -40,21 +58,7 @@ const dropdownStyles: ComponentSlotStylesInput ({ - alignItems: 'center', - display: 'flex', - justifyContent: 'center', - - backgroundColor: 'transparent', - cursor: 'pointer', - userSelect: 'none', - - margin: 0, - position: 'absolute', - right: pxToRem(5), - height: v.toggleIndicatorSize, - width: v.toggleIndicatorSize, - }), + clearIndicator: getIndicatorStyles, container: ({ props: p, variables: v }): ICSSInJSStyle => ({ display: 'flex', @@ -133,19 +137,7 @@ const dropdownStyles: ComponentSlotStylesInput ({ - position: 'absolute', - height: v.toggleIndicatorSize, - width: v.toggleIndicatorSize, - cursor: 'pointer', - backgroundColor: 'transparent', - margin: 0, - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - userSelect: 'none', - right: pxToRem(5), - }), + toggleIndicator: getIndicatorStyles, } export default dropdownStyles From 30967d0ec55362d03096df8b921de91671264d6f Mon Sep 17 00:00:00 2001 From: Oleksandr Fediashov Date: Tue, 12 Feb 2019 16:02:20 +0200 Subject: [PATCH 7/9] use ShorthandCollection --- .../src/components/Dropdown/Dropdown.tsx | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/packages/react/src/components/Dropdown/Dropdown.tsx b/packages/react/src/components/Dropdown/Dropdown.tsx index 833ff10d02..a1cc38886f 100644 --- a/packages/react/src/components/Dropdown/Dropdown.tsx +++ b/packages/react/src/components/Dropdown/Dropdown.tsx @@ -8,6 +8,7 @@ import { ShorthandRenderFunction, ShorthandValue, ComponentEventHandler, + ShorthandCollection, } from '../../types' import { ComponentSlotStylesInput, ComponentVariablesInput } from '../../themes/types' import Downshift, { @@ -65,7 +66,7 @@ export interface DropdownProps extends UIComponentProps ShorthandValue[]) + search?: boolean | ((items: ShorthandCollection, searchQuery: string) => ShorthandCollection) /** Component for the search input query. */ searchInput?: ShorthandValue @@ -164,7 +165,7 @@ export interface DropdownProps extends UIComponentProps, Dropdo const { searchQuery, value } = this.state const noPlaceholder = - searchQuery.length > 0 || (multiple && (value as ShorthandValue[]).length > 0) + searchQuery.length > 0 || (multiple && (value as ShorthandCollection).length > 0) return DropdownSearchInput.create(searchInput || {}, { defaultProps: { @@ -540,7 +541,7 @@ class Dropdown extends AutoControlledComponent, Dropdo private renderSelectedItems(variables, rtl: boolean) { const { renderSelectedItem } = this.props - const value = this.state.value as ShorthandValue[] + const value = this.state.value as ShorthandCollection if (value.length === 0) { return null @@ -600,10 +601,10 @@ class Dropdown extends AutoControlledComponent, Dropdo } } - private getItemsFilteredBySearchQuery = (): ShorthandValue[] => { + private getItemsFilteredBySearchQuery = (): ShorthandCollection => { const { items, itemToString, multiple, search } = this.props const { searchQuery, value } = this.state - const filteredItems = multiple ? _.difference(items, value as ShorthandValue[]) : items + const filteredItems = multiple ? _.difference(items, value as ShorthandCollection) : items if (search) { if (_.isFunction(search)) { @@ -657,7 +658,7 @@ class Dropdown extends AutoControlledComponent, Dropdo this.handleSelectedItemRemove(e, item, predefinedProps, DropdownSelectedItemProps) }, onClick: (e: React.SyntheticEvent, DropdownSelectedItemProps: DropdownSelectedItemProps) => { - const { value } = this.state as { value: ShorthandValue[] } + const { value } = this.state as { value: ShorthandCollection } this.trySetState({ activeSelectedIndex: value.indexOf(item), }) @@ -759,7 +760,7 @@ class Dropdown extends AutoControlledComponent, Dropdo ) { return } - const { value } = this.state as { value: ShorthandValue[] } + const { value } = this.state as { value: ShorthandCollection } if (value.length > 0) { this.trySetState({ activeSelectedIndex: value.length - 1 }) } @@ -772,7 +773,7 @@ class Dropdown extends AutoControlledComponent, Dropdo if ( multiple && (searchQuery === '' || this.inputRef.current.selectionStart === 0) && - (value as ShorthandValue[]).length > 0 + (value as ShorthandCollection).length > 0 ) { this.removeItemFromValue() } @@ -836,7 +837,7 @@ class Dropdown extends AutoControlledComponent, Dropdo private handleSelectedChange = (item: ShorthandValue) => { const { items, multiple, getA11ySelectionMessage } = this.props const newState = { - value: multiple ? [...(this.state.value as ShorthandValue[]), item] : item, + value: multiple ? [...(this.state.value as ShorthandCollection), item] : item, searchQuery: this.getSelectedItemAsString(item), } @@ -873,7 +874,7 @@ class Dropdown extends AutoControlledComponent, Dropdo ) { const { activeSelectedIndex, value } = this.state as { activeSelectedIndex: number - value: ShorthandValue[] + value: ShorthandCollection } const previousKey = rtl ? keyboardKey.ArrowRight : keyboardKey.ArrowLeft const nextKey = rtl ? keyboardKey.ArrowLeft : keyboardKey.ArrowRight @@ -933,7 +934,7 @@ class Dropdown extends AutoControlledComponent, Dropdo private removeItemFromValue(item?: ShorthandValue) { const { getA11ySelectionMessage } = this.props - let value = this.state.value as ShorthandValue[] + let value = this.state.value as ShorthandCollection let poppedItem = item if (poppedItem) { @@ -983,7 +984,7 @@ class Dropdown extends AutoControlledComponent, Dropdo return itemToString(value) } - private isValueEmpty = (value: ShorthandValue | ShorthandValue[]) => { + private isValueEmpty = (value: ShorthandValue | ShorthandCollection) => { return _.isArray(value) ? value.length < 1 : !value } } From 1c54f09a55bfe0437f497b36349ec1ecdfcc291b Mon Sep 17 00:00:00 2001 From: Oleksandr Fediashov Date: Wed, 13 Feb 2019 15:52:41 +0200 Subject: [PATCH 8/9] add changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6980d8b2b..a07fd1bbb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Features - Export `arrow-up`,`arrow-down` and `chat` SVG icon @VyshnaviDasari ([#873](https://github.com/stardust-ui/react/pull/873)) - Export `FocusZone`'s utilities @sophieH29 ([#876](https://github.com/stardust-ui/react/pull/876)) +- Add `clearable` prop for `Dropdown` @layershifter ([#885](https://github.com/stardust-ui/react/pull/885)) ### Fixes - Properly handle falsy values provided as `Flex` and `Flex.Item` children @kuzhelov ([#890](https://github.com/stardust-ui/react/pull/890)) From 230544ef58ee8b00a15a091b68d4d0af50bd55dc Mon Sep 17 00:00:00 2001 From: Oleksandr Fediashov Date: Wed, 13 Feb 2019 16:30:03 +0200 Subject: [PATCH 9/9] use overrideProps --- .../react/src/components/Dropdown/Dropdown.tsx | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/react/src/components/Dropdown/Dropdown.tsx b/packages/react/src/components/Dropdown/Dropdown.tsx index a1cc38886f..cff637d3eb 100644 --- a/packages/react/src/components/Dropdown/Dropdown.tsx +++ b/packages/react/src/components/Dropdown/Dropdown.tsx @@ -30,7 +30,7 @@ import { UIComponentProps, } from '../../lib' import keyboardKey from 'keyboard-key' -import Indicator from '../Indicator/Indicator' +import Indicator, { IndicatorProps } from '../Indicator/Indicator' import List from '../List/List' import Ref from '../Ref/Ref' import DropdownItem from './DropdownItem' @@ -39,7 +39,7 @@ import DropdownSearchInput, { DropdownSearchInputProps } from './DropdownSearchI import Button from '../Button/Button' import { screenReaderContainerStyles } from '../../lib/accessibility/Styles/accessibilityStyles' import ListItem from '../List/ListItem' -import Icon from '../Icon/Icon' +import Icon, { IconProps } from '../Icon/Icon' export interface DropdownSlotClassNames { container: string @@ -341,17 +341,27 @@ class Dropdown extends AutoControlledComponent, Dropdo ? Icon.create(clearIndicator, { defaultProps: { className: Dropdown.slotClassNames.clearIndicator, - onClick: this.handleClear, styles: styles.clearIndicator, xSpacing: 'none', }, + overrideProps: (predefinedProps: IconProps) => ({ + onClick: (e, iconProps: IconProps) => { + _.invoke(predefinedProps, 'onClick', e, iconProps) + this.handleClear() + }, + }), }) : Indicator.create(toggleIndicator, { defaultProps: { direction: isOpen ? 'top' : 'bottom', - onClick: getToggleButtonProps().onClick, styles: styles.toggleIndicator, }, + overrideProps: (predefinedProps: IndicatorProps) => ({ + onClick: (e, indicatorProps: IndicatorProps) => { + _.invoke(predefinedProps, 'onClick', e, indicatorProps) + getToggleButtonProps().onClick(e) + }, + }), })} {this.renderItemsList( styles,