Skip to content

Commit 297ed42

Browse files
authored
refactor: Consolidate showDropdown props (#253) 🔨
* feat: Group logically related props together (#242) ⚡️ BREAKING CHANGE: Property changes | Description | Usage before | Usage after | | --------------------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------- | | Added a new `mode` prop | `simpleSelect={true}` / `simpleSelect` | `mode='simpleSelect'` | | Bundled text props into a single object | `placeholderText='My text'`<br>`noMatchesText='No matches'` | `texts={{ placeholder: 'My text', noMatches: 'No matches' }}` |
1 parent 3e255dd commit 297ed42

File tree

11 files changed

+86
-49
lines changed

11 files changed

+86
-49
lines changed

README.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ A lightweight and fast control to render a select component that can display hie
5656
- [radioSelect](#radioSelect)
5757
- [showPartiallySelected](#showpartiallyselected)
5858
- [showDropdown](#showDropdown)
59-
- [showDropdownAlways](#showDropdownAlways)
59+
- [initial](#initial)
60+
- [always](#always)
6061
- [form states (disabled|readOnly)](#formstates)
6162
- [id](#id)
6263
- [Styling and Customization](#styling-and-customization)
@@ -358,15 +359,17 @@ If set to true, shows checkboxes in a partial state when one, but not all of the
358359

359360
### showDropdown
360361

361-
Type: `bool` (default: `false`)
362+
Type: `string`
362363

363-
If set to true, shows the dropdown when rendered. This can be used to render the component with the dropdown open as its initial state.
364+
Let's you choose the rendered state of the dropdown.
364365

365-
### showDropdownAlways
366+
#### initial
366367

367-
Type: `bool`
368+
`showDropdown: initial` shows the dropdown when rendered. This can be used to render the component with the dropdown open as its initial state.
369+
370+
#### always
368371

369-
If set to true, always shows the dropdown when rendered, and toggling dropdown will be disabled.
372+
`showDropdown: always` shows the dropdown when rendered, and keeps it visible at all times. Toggling dropdown is disabled.
370373

371374
### form states (disabled|readOnly)
372375

__snapshots__/src/index.test.js.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,7 @@ Generated by [AVA](https://ava.li).
276276
onBlur={Function onBlur {}}
277277
onChange={Function onChange {}}
278278
onFocus={Function onFocus {}}
279+
showDropdown="default"
279280
texts={{}}
280281
>
281282
<div
@@ -414,6 +415,7 @@ Generated by [AVA](https://ava.li).
414415
onBlur={Function onBlur {}}
415416
onChange={Function onChange {}}
416417
onFocus={Function onFocus {}}
418+
showDropdown="default"
417419
texts={{}}
418420
>
419421
<div
-3 Bytes
Binary file not shown.

docs/src/stories/Options/index.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ class WithOptions extends PureComponent {
4545
showPartiallySelected,
4646
disabled,
4747
readOnly,
48+
showDropdown,
4849
} = this.state
4950

5051
return (
@@ -68,6 +69,18 @@ class WithOptions extends PureComponent {
6869
<option value="hierarchical">Hierarchical</option>
6970
</select>
7071
</div>
72+
<div style={{ marginBottom: '10px' }}>
73+
<label htmlFor={showDropdown}>ShowDropdown: </label>
74+
<select
75+
id="showDropdown"
76+
value={showDropdown}
77+
onChange={e => this.setState({ showDropdown: e.target.value })}
78+
>
79+
<option value="default">--</option>
80+
<option value="initial">Initial</option>
81+
<option value="always">Always</option>
82+
</select>
83+
</div>
7184
<Checkbox
7285
label="Clear search on selection"
7386
value="clearSearchOnChange"
@@ -109,6 +122,7 @@ class WithOptions extends PureComponent {
109122
showPartiallySelected={showPartiallySelected}
110123
disabled={disabled}
111124
readOnly={readOnly}
125+
showDropdown={showDropdown}
112126
/>
113127
</div>
114128
</div>

src/a11y/a11y.test.js

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,19 +44,20 @@ test.beforeEach(t => {
4444
]
4545
})
4646

47-
test('regular tree has no a11y exceptions', async t => {
47+
test('has no a11y exceptions', async t => {
4848
const { tree } = t.context
4949
const component = mountToDoc(
5050
<div>
51-
<DropdownTreeSelect data={tree} showDropDown texts={{ label: 'test' }} />
52-
<DropdownTreeSelect data={tree} showDropDown disabled texts={{ label: 'test' }} />
53-
<DropdownTreeSelect data={tree} showDropDown readOnly texts={{ label: 'test' }} />
54-
<DropdownTreeSelect data={tree} showDropDown mode="simpleSelect" texts={{ label: 'test' }} />
55-
<DropdownTreeSelect data={tree} showDropDown mode="radioSelect" texts={{ label: 'test' }} />
51+
<DropdownTreeSelect data={tree} showDropdown="initial" texts={{ label: 'test' }} />
52+
<DropdownTreeSelect data={tree} showDropdown="initial" disabled texts={{ label: 'test' }} />
53+
<DropdownTreeSelect data={tree} showDropdown="initial" readOnly texts={{ label: 'test' }} />
54+
<DropdownTreeSelect data={tree} showDropdown="initial" mode="simpleSelect" texts={{ label: 'test' }} />
55+
<DropdownTreeSelect data={tree} showDropdown="initial" mode="radioSelect" texts={{ label: 'test' }} />
56+
<DropdownTreeSelect data={tree} showDropdown="initial" mode="hierarchical" texts={{ label: 'test' }} />
5657
</div>
5758
)
5859
const domNode = component.getDOMNode()
5960
const { error, violations } = await run(domNode)
60-
t.is(error, null, JSON.stringify(error, null, 4))
61-
t.is(violations.length, 0, JSON.stringify(violations, null, 4))
61+
t.is(error, null, JSON.stringify(error, null, 2))
62+
t.is(violations.length, 0, JSON.stringify(violations, null, 2))
6263
})

src/index.js

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import TreeManager from './tree-manager'
1818
import keyboardNavigation from './tree-manager/keyboardNavigation'
1919

2020
import styles from './index.css'
21+
import { getAriaLabel } from './a11y'
2122

2223
const cx = cn.bind(styles)
2324

@@ -34,8 +35,7 @@ class DropdownTreeSelect extends Component {
3435
label: PropTypes.string,
3536
labelRemove: PropTypes.string,
3637
}),
37-
showDropdown: PropTypes.bool,
38-
showDropdownAlways: PropTypes.bool,
38+
showDropdown: PropTypes.oneOf(['default', 'initial', 'always']),
3939
className: PropTypes.string,
4040
onChange: PropTypes.func,
4141
onAction: PropTypes.func,
@@ -54,19 +54,19 @@ class DropdownTreeSelect extends Component {
5454
onBlur: () => {},
5555
onChange: () => {},
5656
texts: {},
57+
showDropdown: 'default',
5758
}
5859

5960
constructor(props) {
6061
super(props)
6162
this.state = {
62-
showDropdown: this.props.showDropdown || this.props.showDropdownAlways || false,
6363
searchModeOn: false,
6464
currentFocus: undefined,
6565
}
6666
this.clientId = props.id || clientIdGenerator.get(this)
6767
}
6868

69-
initNewProps = ({ data, mode, showPartiallySelected }) => {
69+
initNewProps = ({ data, mode, showDropdown, showPartiallySelected }) => {
7070
this.treeManager = new TreeManager({
7171
data,
7272
mode,
@@ -78,7 +78,10 @@ class DropdownTreeSelect extends Component {
7878
if (currentFocusNode) {
7979
currentFocusNode._focused = true
8080
}
81-
this.setState(this.treeManager.getTreeAndTags())
81+
this.setState({
82+
showDropdown: /initial|always/.test(showDropdown) || false,
83+
...this.treeManager.getTreeAndTags(),
84+
})
8285
}
8386

8487
resetSearchState = () => {
@@ -92,8 +95,7 @@ class DropdownTreeSelect extends Component {
9295
}
9396

9497
componentWillMount() {
95-
const { data, mode, showPartiallySelected } = this.props
96-
this.initNewProps({ data, mode, showPartiallySelected })
98+
this.initNewProps(this.props)
9799
}
98100

99101
componentWillUnmount() {
@@ -107,7 +109,7 @@ class DropdownTreeSelect extends Component {
107109
handleClick = (e, callback) => {
108110
this.setState(prevState => {
109111
// keep dropdown active when typing in search box
110-
const showDropdown = this.props.showDropdownAlways || this.keepDropdownActive || !prevState.showDropdown
112+
const showDropdown = this.props.showDropdown === 'always' || this.keepDropdownActive || !prevState.showDropdown
111113

112114
// register event listeners only if there is a state change
113115
if (showDropdown !== prevState.showDropdown) {
@@ -126,7 +128,7 @@ class DropdownTreeSelect extends Component {
126128
}
127129

128130
handleOutsideClick = e => {
129-
if (this.props.showDropdownAlways || !isOutsideClick(e, this.node)) {
131+
if (this.props.showDropdown === 'always' || !isOutsideClick(e, this.node)) {
130132
return
131133
}
132134

@@ -260,6 +262,17 @@ class DropdownTreeSelect extends Component {
260262
e.preventDefault()
261263
}
262264

265+
getAriaAttributes = () => {
266+
const { mode, texts } = this.props
267+
268+
if (mode !== 'radioSelect') return {}
269+
270+
return {
271+
role: 'radiogroup',
272+
...getAriaLabel(texts.label),
273+
}
274+
}
275+
263276
render() {
264277
const { disabled, readOnly, mode, texts } = this.props
265278
const { showDropdown, currentFocus } = this.state
@@ -298,7 +311,7 @@ class DropdownTreeSelect extends Component {
298311
/>
299312
</Trigger>
300313
{showDropdown && (
301-
<div className="dropdown-content">
314+
<div className="dropdown-content" {...this.getAriaAttributes()}>
302315
{this.state.allNodesHidden ? (
303316
<span className="no-matches">{texts.noMatches || 'No matches found'}</span>
304317
) : (

src/index.keyboardNav.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ test('can collapse on keyboardNavigation', t => {
9898
})
9999

100100
test('can navigate searchresult on keyboardNavigation', t => {
101-
const wrapper = mount(<DropdownTreeSelect data={tree} showDropdown />)
101+
const wrapper = mount(<DropdownTreeSelect data={tree} showDropdown="initial" />)
102102
wrapper.instance().onInputChange('bb')
103103
triggerOnKeyboardKeyDown(wrapper, ['b', 'ArrowDown', 'ArrowDown', 'ArrowDown'])
104104
t.deepEqual(wrapper.find('li.focused').text(), 'bbb 1')
@@ -147,7 +147,7 @@ test('can delete tags with backspace/delete on keyboardNavigation', t => {
147147
})
148148

149149
test('remembers current focus between prop updates', t => {
150-
const wrapper = mount(<DropdownTreeSelect data={tree} showDropdown />)
150+
const wrapper = mount(<DropdownTreeSelect data={tree} showDropdown="initial" />)
151151
t.false(wrapper.find('li.focused').exists())
152152
triggerOnKeyboardKeyDown(wrapper, ['ArrowDown'])
153153
t.deepEqual(wrapper.find('li.focused').text(), 'ccc 1')

src/index.test.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -76,27 +76,27 @@ test('renders default radio select state', t => {
7676

7777
test('shows dropdown', t => {
7878
const { tree } = t.context
79-
const wrapper = shallow(<DropdownTreeSelect id={dropdownId} data={tree} showDropdown />)
79+
const wrapper = shallow(<DropdownTreeSelect id={dropdownId} data={tree} showDropdown="initial" />)
8080
t.snapshot(toJson(wrapper))
8181
})
8282

8383
test('always shows dropdown', t => {
8484
const { tree } = t.context
85-
const wrapper = shallow(<DropdownTreeSelect id={dropdownId} data={tree} showDropdownAlways />)
85+
const wrapper = shallow(<DropdownTreeSelect id={dropdownId} data={tree} showDropdown="always" />)
8686
t.snapshot(toJson(wrapper))
8787
})
8888

89-
test('keeps dropdown open for showDropdownAlways', t => {
89+
test('keeps dropdown open for showDropdown: always', t => {
9090
const { tree } = t.context
91-
const wrapper = mount(<DropdownTreeSelect id={dropdownId} data={tree} showDropdownAlways />)
91+
const wrapper = mount(<DropdownTreeSelect id={dropdownId} data={tree} showDropdown="always" />)
9292
wrapper.instance().handleClick()
9393
t.true(wrapper.state().showDropdown)
9494
})
9595

9696
test('notifies on action', t => {
9797
const handler = spy()
9898
const { tree } = t.context
99-
const wrapper = mount(<DropdownTreeSelect id={dropdownId} data={tree} onAction={handler} showDropdown />)
99+
const wrapper = mount(<DropdownTreeSelect id={dropdownId} data={tree} onAction={handler} showDropdown="initial" />)
100100
wrapper.find('i.fa-ban').simulate('click')
101101
t.true(handler.calledWithExactly(node0, action))
102102
})
@@ -134,7 +134,7 @@ test('sets search mode on input change', t => {
134134

135135
test('hides dropdown onChange for simpleSelect', t => {
136136
const { tree } = t.context
137-
const wrapper = mount(<DropdownTreeSelect id={dropdownId} showDropdown data={tree} mode="simpleSelect" />)
137+
const wrapper = mount(<DropdownTreeSelect id={dropdownId} showDropdown="initial" data={tree} mode="simpleSelect" />)
138138
wrapper.instance().onCheckboxChange(node0._id, true)
139139
t.false(wrapper.state().searchModeOn)
140140
t.false(wrapper.state().allNodesHidden)
@@ -144,7 +144,7 @@ test('hides dropdown onChange for simpleSelect', t => {
144144
test('keeps dropdown open onChange for simpleSelect and keepOpenOnSelect', t => {
145145
const { tree } = t.context
146146
const wrapper = mount(
147-
<DropdownTreeSelect id={dropdownId} data={tree} showDropdown mode="simpleSelect" keepOpenOnSelect />
147+
<DropdownTreeSelect id={dropdownId} data={tree} showDropdown="initial" mode="simpleSelect" keepOpenOnSelect />
148148
)
149149
wrapper.instance().onCheckboxChange(node0._id, true)
150150
t.true(wrapper.state().showDropdown)

src/tree-manager/tests/radioSelect.test.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@ const dropdownId = 'rdts'
88
const tree = ['nodeA', 'nodeB', 'nodeC'].map(nv => ({ id: nv, label: nv, value: nv }))
99

1010
test('should render radio inputs with shared name', t => {
11-
const wrapper = mount(<DropdownTreeSelect id={dropdownId} data={tree} mode="radioSelect" showDropdown />)
11+
const wrapper = mount(<DropdownTreeSelect id={dropdownId} data={tree} mode="radioSelect" showDropdown="initial" />)
1212

1313
const inputs = wrapper.find('.dropdown-content').find(`input[type="radio"][name="${dropdownId}"]`)
1414
t.deepEqual(inputs.length, 3)
1515
})
1616

1717
test('hides dropdown onChange for radioSelect', t => {
18-
const wrapper = mount(<DropdownTreeSelect id={dropdownId} data={tree} showDropdown mode="radioSelect" />)
18+
const wrapper = mount(<DropdownTreeSelect id={dropdownId} data={tree} showDropdown="initial" mode="radioSelect" />)
1919
wrapper.instance().onCheckboxChange('nodeA', true)
2020
t.false(wrapper.state().searchModeOn)
2121
t.false(wrapper.state().allNodesHidden)
@@ -24,7 +24,7 @@ test('hides dropdown onChange for radioSelect', t => {
2424

2525
test('keeps dropdown open onChange for radioSelect and keepOpenOnSelect', t => {
2626
const wrapper = mount(
27-
<DropdownTreeSelect id={dropdownId} data={tree} showDropdown mode="radioSelect" keepOpenOnSelect />
27+
<DropdownTreeSelect id={dropdownId} data={tree} showDropdown="initial" mode="radioSelect" keepOpenOnSelect />
2828
)
2929
wrapper.instance().onCheckboxChange('nodeA', true)
3030
t.true(wrapper.state().showDropdown)

src/tree/index.js

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -128,12 +128,15 @@ class Tree extends Component {
128128
}
129129

130130
getAriaAttributes = () => {
131-
const { readOnly, mode } = this.props
132-
const attributes = {}
131+
const { mode } = this.props
132+
133+
const attributes = {
134+
/* https://www.w3.org/TR/wai-aria-1.1/#select
135+
* https://www.w3.org/TR/wai-aria-1.1/#tree */
136+
role: mode === 'simpleSelect' ? 'listbox' : 'tree',
137+
'aria-multiselectable': /multiSelect|hierarchical/.test(mode),
138+
}
133139

134-
attributes.role = mode === 'simpleSelect' ? 'listbox' : 'tree'
135-
attributes['aria-multiselectable'] = mode === 'multiSelect' ? 'true' : 'false'
136-
attributes['aria-readonly'] = readOnly ? 'true' : 'false'
137140
return attributes
138141
}
139142

0 commit comments

Comments
 (0)