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

Commit ea30ca0

Browse files
authored
feat(Dropdown): add clearable prop (#885)
* feat(Dropdown): add `clearable` prop * update steps * add toc * fix focusing * fix `handleClear` method * extract styles * use ShorthandCollection * add changelog entry * use overrideProps
1 parent 8f590a4 commit ea30ca0

File tree

7 files changed

+152
-41
lines changed

7 files changed

+152
-41
lines changed

.github/test-a-feature.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ Test a feature
1717
- [Important mentions:](#important-mentions)
1818
- [Run Screener tests](#run-screener-tests)
1919
- [Local run command](#local-run-command)
20+
- [Behavior tets](#behavior-tets)
21+
- [Adding test(s)](#adding-tests)
22+
- [Running test(s)](#running-tests)
23+
- [Troubleshooting](#troubleshooting)
24+
- [I am not sure if my line under `@specification` was process correctly](#i-am-not-sure-if-my-line-under-specification-was-process-correctly)
25+
- [I am not sure if my line was executed](#i-am-not-sure-if-my-line-was-executed)
26+
- [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)
27+
- [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)
2028

2129
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
2230

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
2020
### Features
2121
- Export `arrow-up`,`arrow-down` and `chat` SVG icon @VyshnaviDasari ([#873](https://github.com/stardust-ui/react/pull/873))
2222
- Export `FocusZone`'s utilities @sophieH29 ([#876](https://github.com/stardust-ui/react/pull/876))
23+
- Add `clearable` prop for `Dropdown` @layershifter ([#885](https://github.com/stardust-ui/react/pull/885))
2324

2425
### Fixes
2526
- Properly handle falsy values provided as `Flex` and `Flex.Item` children @kuzhelov ([#890](https://github.com/stardust-ui/react/pull/890))
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { Dropdown } from '@stardust-ui/react'
2+
3+
const selectors = {
4+
clearIndicator: `.${Dropdown.slotClassNames.clearIndicator}`,
5+
triggerButton: `.${Dropdown.slotClassNames.triggerButton}`,
6+
item: (itemIndex: number) => `.${Dropdown.slotClassNames.itemsList} li:nth-child(${itemIndex})`,
7+
}
8+
9+
const steps = [
10+
steps => steps.click(selectors.triggerButton).snapshot('Shows list'),
11+
steps => steps.click(selectors.item(3)).snapshot('Selects an item'),
12+
steps => steps.click(selectors.clearIndicator).snapshot('Clears the value'),
13+
]
14+
15+
export default steps
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Dropdown } from '@stardust-ui/react'
2+
import * as React from 'react'
3+
4+
const inputItems = [
5+
'Bruce Wayne',
6+
'Natasha Romanoff',
7+
'Steven Strange',
8+
'Alfred Pennyworth',
9+
`Scarlett O'Hara`,
10+
'Imperator Furiosa',
11+
'Bruce Banner',
12+
'Peter Parker',
13+
'Selina Kyle',
14+
]
15+
16+
const DropdownClearableExample = () => (
17+
<Dropdown clearable items={inputItems} placeholder="Select your hero" />
18+
)
19+
20+
export default DropdownClearableExample

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ const Types = () => (
2424
description="A dropdown can be searchable and allow a multiple selection."
2525
examplePath="components/Dropdown/Types/DropdownExampleSearchMultiple"
2626
/>
27+
<ComponentExample
28+
title="Clearable"
29+
description="A dropdown can be clearable and let users remove their selection."
30+
examplePath="components/Dropdown/Types/DropdownExampleClearable"
31+
/>
2732
<ComponentExample
2833
title="Inline"
2934
description="A dropdown can be used inline with text."

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

Lines changed: 81 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
ShorthandRenderFunction,
99
ShorthandValue,
1010
ComponentEventHandler,
11+
ShorthandCollection,
1112
} from '../../types'
1213
import { ComponentSlotStylesInput, ComponentVariablesInput } from '../../themes/types'
1314
import Downshift, {
@@ -29,7 +30,7 @@ import {
2930
UIComponentProps,
3031
} from '../../lib'
3132
import keyboardKey from 'keyboard-key'
32-
import Indicator from '../Indicator/Indicator'
33+
import Indicator, { IndicatorProps } from '../Indicator/Indicator'
3334
import List from '../List/List'
3435
import Ref from '../Ref/Ref'
3536
import DropdownItem from './DropdownItem'
@@ -38,9 +39,11 @@ import DropdownSearchInput, { DropdownSearchInputProps } from './DropdownSearchI
3839
import Button from '../Button/Button'
3940
import { screenReaderContainerStyles } from '../../lib/accessibility/Styles/accessibilityStyles'
4041
import ListItem from '../List/ListItem'
42+
import Icon, { IconProps } from '../Icon/Icon'
4143

4244
export interface DropdownSlotClassNames {
4345
container: string
46+
clearIndicator: string
4447
triggerButton: string
4548
itemsList: string
4649
selectedItems: string
@@ -50,14 +53,20 @@ export interface DropdownProps extends UIComponentProps<DropdownProps, DropdownS
5053
/** The index of the currently active selected item, if dropdown has a multiple selection. */
5154
activeSelectedIndex?: number
5255

56+
/** A dropdown can be clearable and let users remove their selection. */
57+
clearable?: boolean
58+
59+
/** A slot for a clearing indicator. */
60+
clearIndicator?: ShorthandValue
61+
5362
/** The initial value for the index of the currently active selected item, in a multiple selection. */
5463
defaultActiveSelectedIndex?: number
5564

5665
/** The initial value for the search query, if the dropdown is also a search. */
5766
defaultSearchQuery?: string
5867

5968
/** The initial value or value array, if the array has multiple selection. */
60-
defaultValue?: ShorthandValue | ShorthandValue[]
69+
defaultValue?: ShorthandValue | ShorthandCollection
6170

6271
/** A dropdown can take the width of its container. */
6372
fluid?: boolean
@@ -86,7 +95,7 @@ export interface DropdownProps extends UIComponentProps<DropdownProps, DropdownS
8695
inline?: boolean
8796

8897
/** Array of props for generating list options (Dropdown.Item[]) and selected item labels(Dropdown.SelectedItem[]), if it's a multiple selection. */
89-
items?: ShorthandValue[]
98+
items?: ShorthandCollection
9099

91100
/**
92101
* Function to be passed to create string from selected item, if it's a shorthand object. Used when dropdown also has a search function.
@@ -141,7 +150,7 @@ export interface DropdownProps extends UIComponentProps<DropdownProps, DropdownS
141150
renderSelectedItem?: ShorthandRenderFunction
142151

143152
/** A dropdown can have a search field instead of trigger button. Can receive a custom search function that will replace the default equivalent. */
144-
search?: boolean | ((items: ShorthandValue[], searchQuery: string) => ShorthandValue[])
153+
search?: boolean | ((items: ShorthandCollection, searchQuery: string) => ShorthandCollection)
145154

146155
/** Component for the search input query. */
147156
searchInput?: ShorthandValue
@@ -156,7 +165,7 @@ export interface DropdownProps extends UIComponentProps<DropdownProps, DropdownS
156165
triggerButton?: ShorthandValue
157166

158167
/** Sets currently selected value(s) (controlled mode). */
159-
value?: ShorthandValue | ShorthandValue[]
168+
value?: ShorthandValue | ShorthandCollection
160169
}
161170

162171
export interface DropdownState {
@@ -165,7 +174,7 @@ export interface DropdownState {
165174
focused: boolean
166175
isOpen?: boolean
167176
searchQuery?: string
168-
value: ShorthandValue | ShorthandValue[]
177+
value: ShorthandValue | ShorthandCollection
169178
}
170179

171180
/**
@@ -192,6 +201,8 @@ class Dropdown extends AutoControlledComponent<Extendable<DropdownProps>, Dropdo
192201
content: false,
193202
}),
194203
activeSelectedIndex: PropTypes.number,
204+
clearable: PropTypes.bool,
205+
clearIndicator: customPropTypes.itemShorthand,
195206
defaultActiveSelectedIndex: PropTypes.number,
196207
defaultSearchQuery: PropTypes.string,
197208
defaultValue: PropTypes.oneOfType([
@@ -226,6 +237,7 @@ class Dropdown extends AutoControlledComponent<Extendable<DropdownProps>, Dropdo
226237

227238
static defaultProps: DropdownProps = {
228239
as: 'div',
240+
clearIndicator: 'close',
229241
itemToString: item => {
230242
if (!item || React.isValidElement(item)) {
231243
return ''
@@ -263,8 +275,16 @@ class Dropdown extends AutoControlledComponent<Extendable<DropdownProps>, Dropdo
263275
unhandledProps,
264276
rtl,
265277
}: RenderResultConfig<DropdownProps>) {
266-
const { search, multiple, getA11yStatusMessage, itemToString, toggleIndicator } = this.props
267-
const { defaultHighlightedIndex, searchQuery } = this.state
278+
const {
279+
clearable,
280+
clearIndicator,
281+
search,
282+
multiple,
283+
getA11yStatusMessage,
284+
itemToString,
285+
toggleIndicator,
286+
} = this.props
287+
const { defaultHighlightedIndex, searchQuery, value } = this.state
268288

269289
return (
270290
<ElementType className={classes.root} {...unhandledProps}>
@@ -293,6 +313,8 @@ class Dropdown extends AutoControlledComponent<Extendable<DropdownProps>, Dropdo
293313
{ refKey: 'innerRef' },
294314
{ suppressRefError: true },
295315
)
316+
const showClearIndicator = clearable && !this.isValueEmpty(value)
317+
296318
return (
297319
<Ref innerRef={innerRef}>
298320
<div
@@ -315,13 +337,32 @@ class Dropdown extends AutoControlledComponent<Extendable<DropdownProps>, Dropdo
315337
)
316338
: this.renderTriggerButton(styles, rtl, getToggleButtonProps)}
317339
</div>
318-
{Indicator.create(toggleIndicator, {
319-
defaultProps: {
320-
direction: isOpen ? 'top' : 'bottom',
321-
onClick: getToggleButtonProps().onClick,
322-
styles: styles.toggleIndicator,
323-
},
324-
})}
340+
{showClearIndicator
341+
? Icon.create(clearIndicator, {
342+
defaultProps: {
343+
className: Dropdown.slotClassNames.clearIndicator,
344+
styles: styles.clearIndicator,
345+
xSpacing: 'none',
346+
},
347+
overrideProps: (predefinedProps: IconProps) => ({
348+
onClick: (e, iconProps: IconProps) => {
349+
_.invoke(predefinedProps, 'onClick', e, iconProps)
350+
this.handleClear()
351+
},
352+
}),
353+
})
354+
: Indicator.create(toggleIndicator, {
355+
defaultProps: {
356+
direction: isOpen ? 'top' : 'bottom',
357+
styles: styles.toggleIndicator,
358+
},
359+
overrideProps: (predefinedProps: IndicatorProps) => ({
360+
onClick: (e, indicatorProps: IndicatorProps) => {
361+
_.invoke(predefinedProps, 'onClick', e, indicatorProps)
362+
getToggleButtonProps().onClick(e)
363+
},
364+
}),
365+
})}
325366
{this.renderItemsList(
326367
styles,
327368
variables,
@@ -392,7 +433,7 @@ class Dropdown extends AutoControlledComponent<Extendable<DropdownProps>, Dropdo
392433
const { searchQuery, value } = this.state
393434

394435
const noPlaceholder =
395-
searchQuery.length > 0 || (multiple && (value as ShorthandValue[]).length > 0)
436+
searchQuery.length > 0 || (multiple && (value as ShorthandCollection).length > 0)
396437

397438
return DropdownSearchInput.create(searchInput || {}, {
398439
defaultProps: {
@@ -510,7 +551,7 @@ class Dropdown extends AutoControlledComponent<Extendable<DropdownProps>, Dropdo
510551

511552
private renderSelectedItems(variables, rtl: boolean) {
512553
const { renderSelectedItem } = this.props
513-
const value = this.state.value as ShorthandValue[]
554+
const value = this.state.value as ShorthandCollection
514555

515556
if (value.length === 0) {
516557
return null
@@ -570,10 +611,10 @@ class Dropdown extends AutoControlledComponent<Extendable<DropdownProps>, Dropdo
570611
}
571612
}
572613

573-
private getItemsFilteredBySearchQuery = (): ShorthandValue[] => {
614+
private getItemsFilteredBySearchQuery = (): ShorthandCollection => {
574615
const { items, itemToString, multiple, search } = this.props
575616
const { searchQuery, value } = this.state
576-
const filteredItems = multiple ? _.difference(items, value as ShorthandValue[]) : items
617+
const filteredItems = multiple ? _.difference(items, value as ShorthandCollection) : items
577618

578619
if (search) {
579620
if (_.isFunction(search)) {
@@ -627,7 +668,7 @@ class Dropdown extends AutoControlledComponent<Extendable<DropdownProps>, Dropdo
627668
this.handleSelectedItemRemove(e, item, predefinedProps, DropdownSelectedItemProps)
628669
},
629670
onClick: (e: React.SyntheticEvent, DropdownSelectedItemProps: DropdownSelectedItemProps) => {
630-
const { value } = this.state as { value: ShorthandValue[] }
671+
const { value } = this.state as { value: ShorthandCollection }
631672
this.trySetState({
632673
activeSelectedIndex: value.indexOf(item),
633674
})
@@ -729,7 +770,7 @@ class Dropdown extends AutoControlledComponent<Extendable<DropdownProps>, Dropdo
729770
) {
730771
return
731772
}
732-
const { value } = this.state as { value: ShorthandValue[] }
773+
const { value } = this.state as { value: ShorthandCollection }
733774
if (value.length > 0) {
734775
this.trySetState({ activeSelectedIndex: value.length - 1 })
735776
}
@@ -742,12 +783,21 @@ class Dropdown extends AutoControlledComponent<Extendable<DropdownProps>, Dropdo
742783
if (
743784
multiple &&
744785
(searchQuery === '' || this.inputRef.current.selectionStart === 0) &&
745-
(value as ShorthandValue[]).length > 0
786+
(value as ShorthandCollection).length > 0
746787
) {
747788
this.removeItemFromValue()
748789
}
749790
}
750791

792+
private handleClear = () => {
793+
const initialState = this.getInitialAutoControlledState(this.props)
794+
795+
this.setState({ value: initialState.value })
796+
797+
this.tryFocusSearchInput()
798+
this.tryFocusTriggerButton()
799+
}
800+
751801
private handleContainerClick = () => {
752802
this.tryFocusSearchInput()
753803
}
@@ -797,7 +847,7 @@ class Dropdown extends AutoControlledComponent<Extendable<DropdownProps>, Dropdo
797847
private handleSelectedChange = (item: ShorthandValue) => {
798848
const { items, multiple, getA11ySelectionMessage } = this.props
799849
const newState = {
800-
value: multiple ? [...(this.state.value as ShorthandValue[]), item] : item,
850+
value: multiple ? [...(this.state.value as ShorthandCollection), item] : item,
801851
searchQuery: this.getSelectedItemAsString(item),
802852
}
803853

@@ -834,7 +884,7 @@ class Dropdown extends AutoControlledComponent<Extendable<DropdownProps>, Dropdo
834884
) {
835885
const { activeSelectedIndex, value } = this.state as {
836886
activeSelectedIndex: number
837-
value: ShorthandValue[]
887+
value: ShorthandCollection
838888
}
839889
const previousKey = rtl ? keyboardKey.ArrowRight : keyboardKey.ArrowLeft
840890
const nextKey = rtl ? keyboardKey.ArrowLeft : keyboardKey.ArrowRight
@@ -894,7 +944,7 @@ class Dropdown extends AutoControlledComponent<Extendable<DropdownProps>, Dropdo
894944

895945
private removeItemFromValue(item?: ShorthandValue) {
896946
const { getA11ySelectionMessage } = this.props
897-
let value = this.state.value as ShorthandValue[]
947+
let value = this.state.value as ShorthandCollection
898948
let poppedItem = item
899949

900950
if (poppedItem) {
@@ -932,9 +982,8 @@ class Dropdown extends AutoControlledComponent<Extendable<DropdownProps>, Dropdo
932982
*/
933983
private getSelectedItemAsString = (value: ShorthandValue): string => {
934984
const { itemToString, multiple, placeholder } = this.props
935-
const isValueEmpty = _.isArray(value) ? value.length < 1 : !value
936985

937-
if (isValueEmpty) {
986+
if (this.isValueEmpty(value)) {
938987
return placeholder
939988
}
940989

@@ -944,10 +993,15 @@ class Dropdown extends AutoControlledComponent<Extendable<DropdownProps>, Dropdo
944993

945994
return itemToString(value)
946995
}
996+
997+
private isValueEmpty = (value: ShorthandValue | ShorthandCollection) => {
998+
return _.isArray(value) ? value.length < 1 : !value
999+
}
9471000
}
9481001

9491002
Dropdown.slotClassNames = {
9501003
container: `${Dropdown.className}__container`,
1004+
clearIndicator: `${Dropdown.className}__clear-indicator`,
9511005
triggerButton: `${Dropdown.className}__trigger-button`,
9521006
itemsList: `${Dropdown.className}__items-list`,
9531007
selectedItems: `${Dropdown.className}__selected-items`,

0 commit comments

Comments
 (0)