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) 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)) 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..4cd44383cb --- /dev/null +++ b/docs/src/examples/components/Dropdown/Types/DropdownExampleClearable.shorthand.steps.ts @@ -0,0 +1,15 @@ +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'), +] + +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" /> + ShorthandValue[]) + search?: boolean | ((items: ShorthandCollection, searchQuery: string) => ShorthandCollection) /** Component for the search input query. */ searchInput?: ShorthandValue @@ -156,7 +165,7 @@ export interface DropdownProps extends UIComponentProps, Dropdo content: false, }), activeSelectedIndex: PropTypes.number, + clearable: PropTypes.bool, + clearIndicator: customPropTypes.itemShorthand, defaultActiveSelectedIndex: PropTypes.number, defaultSearchQuery: PropTypes.string, defaultValue: PropTypes.oneOfType([ @@ -226,6 +237,7 @@ class Dropdown extends AutoControlledComponent, Dropdo static defaultProps: DropdownProps = { as: 'div', + clearIndicator: 'close', itemToString: item => { if (!item || React.isValidElement(item)) { return '' @@ -263,8 +275,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 +313,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, + 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', + styles: styles.toggleIndicator, + }, + overrideProps: (predefinedProps: IndicatorProps) => ({ + onClick: (e, indicatorProps: IndicatorProps) => { + _.invoke(predefinedProps, 'onClick', e, indicatorProps) + getToggleButtonProps().onClick(e) + }, + }), + })} {this.renderItemsList( styles, variables, @@ -392,7 +433,7 @@ class Dropdown extends AutoControlledComponent, 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: { @@ -510,7 +551,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 @@ -570,10 +611,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)) { @@ -627,7 +668,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), }) @@ -729,7 +770,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 }) } @@ -742,12 +783,21 @@ 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() } } + private handleClear = () => { + const initialState = this.getInitialAutoControlledState(this.props) + + this.setState({ value: initialState.value }) + + this.tryFocusSearchInput() + this.tryFocusTriggerButton() + } + private handleContainerClick = () => { this.tryFocusSearchInput() } @@ -797,7 +847,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), } @@ -834,7 +884,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 @@ -894,7 +944,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) { @@ -932,9 +982,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 +993,15 @@ class Dropdown extends AutoControlledComponent, Dropdo return itemToString(value) } + + private isValueEmpty = (value: ShorthandValue | ShorthandCollection) => { + 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..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,6 +58,8 @@ const dropdownStyles: ComponentSlotStylesInput ({ display: 'flex', flexWrap: 'wrap', @@ -117,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