Skip to content

Commit da11b17

Browse files
authored
SelectDropdown: cleanup code and migrate CSS to webpack (#35086)
* SelectDropdown: replace methods bound in constructor with arrow fns * SelectDropdown: instanceId doesn't need to be in state and be used for keys Changes `instanceId` to be an instance property assigned during construction. No need to put it in state, as it never changes. Also, don't use `instanceId` to create `key` values. These don't need to be globally unique. Use them only for DOM element IDs that are used to link elements together with ARIA attributes. * SelectDropdown: remove props argument of getInitialSelectedItem We can always read from `this.props`. Even in constructor, because we called `super( props )`. * SelectDropdown: comment and improve the getInitialSelectedItem logic Add comments about what the method does, and fix a bug where a separator item (specified as `null` in `options`) can potentially crash the `find`. * SelectDropdown: toggle visibility of dropdown with visibility: hidden Removes need for smart `tabIndex` logic: hidden elements are automatically excluded from tab order. `visibility: hidden`, as opposed to `display: none`, still takes the element into account when doing layout. Therefore, the dropdown header will continue to have the width of the widest option in the list. * SelectDropdown: use arrow functions where appropriate * SelectDropdown: use modern refs for focusing item links Replace string ref with a modern one, and create a `focusLink` instance method to provide API for the parent component that wants to set focus. Replace access to internal `itemRef.refs.itemLink` property. * SelectDropdown: replace string ref to dropdownContainer with modern one * SelectDropdown: replace string itemRefs with modern ones Use an array of refs to focusable items, set them with a callback, move the `refIndex` increment inline to calling expression. * SelectDropdown: cloned children don't need a key `key` is needed only when passing array of children. In this case, the children are written as JSX and `key` doesn't need to be added on cloning and mapping 1:1. * SelectDropdown: only DropdownItem children need to be cloned and amended Only `DropdownItem` needs a ref (it's focusable and the ref is used to move focus on up and down keyboard navigation) and an `onClick` handler. Separator and Label are neither focusable nor clickable. * SelectDropdown: ignore clicks on Label and Separator, add a11y role Clicks on label and separator should not bubble to the parent element and cause the dropdown to close. This patch adds a missing handler to the separator component (label is already OK). Also adds an a11y role to specify that the element is not interactive despite having an `onClick` handler. * SelectDropdown: simplify setting initial state Can be done by assinging an instance property instead of full constructor. Also, `getInitialSelectedItem` already handles the case where `options` prop is not present or empty. * SelectDropdown: remove unneeded componentWillReceiveProps Closing the popup when receiving new props doesn't seem to make much sense. And the initial selected value is set only on initial mount and further changes of the prop are ignored. That's the common behavior of initial-ish props on uncontrolled components. For example, native `<input defaultValue="x" />` renders input box with "x" and doesn't change the value on further rerenders with a different prop. * SelectDropdown: no need to look at initial selected item in getSelectedText or Icon In the `getSelectedText` and `getSelectedIcon` getters, the currently selected value is always in `this.state.selected`. No need to default to the initial value. Also, use `_.get` instead of `_.result`, as `icon` and `label` are not functions. * SelectDropdown: remove ESLint-offending and non-helpful JSDoc comment * SelectDropdown: fix a11y issues by adding roles and correct ARIA attrs * SelectDropdown: use the classNames helper better * SelectDropdown: use Lodash noop for event handler defaults * SelectDropdown: reorg the code that moves focus, use fewer lodash funcs * SelectDropdown: export Item, Separator and Label as main component fields * SelectDropdown: rework docs and devdocs example to use the SelectDropdown._ convention * SelectDropdown: migrate CSS to webpack * Update usages of SelectDropdown to use the SelectDropdown.Item convention * Remove import of SelectDropdown stylesheet from EditorPublishData * SelectDropdown: return undefined from getInitialSelectedItem * SelectDropdown: update unit tests Some of them were quite awful, spying on internal method calls or mocking the whole component instance instead of testing on the real component and checking its state and JSDOM rendering. * SelectDropdown: disable some ESLint warnings in docs example * SelectDropdown: hide the dropdown options when the dropdown is closed Otherwise, the options element is clickable although it has `visibility: hidden` and captures clicks on controls that are underneath it.
1 parent 09d98af commit da11b17

File tree

21 files changed

+281
-494
lines changed

21 files changed

+281
-494
lines changed

assets/stylesheets/_components.scss

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,5 @@
1515
@import 'components/main/style';
1616
@import 'components/popover/style';
1717
@import 'components/segmented-control/style';
18-
@import 'components/select-dropdown/style';
1918
@import 'components/tooltip/style';
2019
@import 'layout/sidebar/style';

client/components/section-nav/tabs.jsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import { debounce } from 'lodash';
1111
/**
1212
* Internal Dependencies
1313
*/
14-
import DropdownItem from 'components/select-dropdown/item';
1514
import SelectDropdown from 'components/select-dropdown';
1615
import { getWindowInnerWidth } from 'lib/viewport';
1716
import afterLayoutFlush from 'lib/after-layout-flush';
@@ -120,9 +119,9 @@ class NavTabs extends Component {
120119
return null;
121120
}
122121
return (
123-
<DropdownItem { ...child.props } key={ 'navTabsDropdown-' + index }>
122+
<SelectDropdown.Item { ...child.props } key={ 'navTabsDropdown-' + index }>
124123
{ child.props.children }
125-
</DropdownItem>
124+
</SelectDropdown.Item>
126125
);
127126
} );
128127

client/components/select-dropdown/README.md

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,17 @@ A good example for this case is navigation. Sometimes the option that is selecte
1616
```js
1717
import React from 'react';
1818
import SelectDropdown from 'components/select-dropdown';
19-
import DropdownItem from 'components/select-dropdown/item';
2019

2120
export default class extends React.Component {
2221
// ...
2322

2423
render() {
2524
return (
2625
<SelectDropdown selectedText="Published">
27-
<DropdownItem selected={ true } path="/published">Published</DropdownItem>
28-
<DropdownItem path="/scheduled">Scheduled</DropdownItem>
29-
<DropdownItem path="/drafts">Drafts</DropdownItem>
30-
<DropdownItem path="/trashed">Trashed</DropdownItem>
26+
<SelectDropdown.Item selected path="/published">Published</SelectDropdown.Item>
27+
<SelectDropdown.Item path="/scheduled">Scheduled</SelectDropdown.Item>
28+
<SelectDropdown.Item path="/drafts">Drafts</SelectDropdown.Item>
29+
<SelectDropdown.Item path="/trashed">Trashed</SelectDropdown.Item>
3130
</SelectDropdown>
3231
);
3332
}
@@ -92,24 +91,22 @@ Optional bool to disable dropdown item.
9291

9392
`onClick`
9493

95-
Optional callback that will be applied when a `DropdownItem` has been clicked. This could be used for updating a parent's state, tracking analytics, etc.
94+
Optional callback that will be applied when a `SelectDropdown.Item` has been clicked. This could be used for updating a parent's state, tracking analytics, etc.
9695

9796
### Label
9897

99-
An item "label" can be added like as a sibling to `DropdownItem`. The purpose
100-
of this `DropdownLabel` component is used to display a static item, for example, to group
98+
An item "label" can be added like as a sibling to `SelectDropdown.Item`. The purpose
99+
of this `SelectDropdown.Label` component is used to display a static item, for example, to group
101100
items.
102101

103102
### Separator
104103

105-
As a sibling to `DropdownItem`, an item "separator" or horizontal line can be used to visually separate items.
104+
As a sibling to `SelectDropdown.Item`, an item "separator" or horizontal line can be used to visually separate items.
106105

107106
![separator example screenshot](https://cldup.com/CWEH2K9PUf.png)
108107

109108
```js
110109
import SelectDropdown from 'components/select-dropdown';
111-
import DropdownItem from 'components/select-dropdown/item';
112-
import DropdownSeparator from 'components/select-dropdown/separator';
113110

114111
export default class extends React.Component {
115112

@@ -118,12 +115,12 @@ export default class extends React.Component {
118115
render() {
119116
return (
120117
<SelectDropdown selectedText="Published">
121-
<DropdownLabel><em>Post status<em></DropdownLabel>
122-
<DropdownItem selected={ true } path="/published">Published</DropdownItem>
123-
<DropdownItem path="/scheduled">Scheduled</DropdownItem>
124-
<DropdownItem path="/drafts">Drafts</DropdownItem>
125-
<DropdownSeparator />
126-
<DropdownItem path="/trashed">Trashed</DropdownItem>
118+
<SelectDropdown.Label><em>Post status<em></SelectDropdown.Label>
119+
<SelectDropdown.Item selected path="/published">Published</SelectDropdown.Item>
120+
<SelectDropdown.Item path="/scheduled">Scheduled</SelectDropdown.Item>
121+
<SelectDropdown.Item path="/drafts">Drafts</SelectDropdown.Item>
122+
<SelectDropdown.Separator />
123+
<SelectDropdown.Item path="/trashed">Trashed</SelectDropdown.Item>
127124
</SelectDropdown>
128125
);
129126
}
@@ -142,6 +139,7 @@ A good example for this case is a form element. You don't want to have to write
142139
143140
```js
144141
import SelectDropdown from 'components/select-dropdown';
142+
145143
var options = [
146144
{ label: 'Post status', isLabel: true },
147145
{ value: 'published', label: 'Published' },
@@ -203,7 +201,7 @@ Optional callback that will be run after the dropdown is opened or closed. An ev
203201

204202
### Label
205203

206-
Adding `isLabel` key set to `true` into the item object will create a `DropdownLabel` component.
204+
Adding `isLabel` key set to `true` into the item object will create a `SelectDropdown.Label` component.
207205

208206
```js
209207
var options = [

client/components/select-dropdown/docs/example.jsx

Lines changed: 41 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,6 @@ import Gridicon from 'gridicons';
1111
* Internal dependencies
1212
*/
1313
import SelectDropdown from 'components/select-dropdown';
14-
import DropdownItem from 'components/select-dropdown/item';
15-
import DropdownLabel from 'components/select-dropdown/label';
16-
import DropdownSeparator from 'components/select-dropdown/separator';
1714

1815
class SelectDropdownExample extends React.PureComponent {
1916
static displayName = 'SelectDropdownExample';
@@ -29,18 +26,12 @@ class SelectDropdownExample extends React.PureComponent {
2926
],
3027
};
3128

32-
constructor( props ) {
33-
super( props );
34-
35-
const initialState = {
36-
childSelected: 'Published',
37-
selectedCount: 10,
38-
compactButtons: false,
39-
selectedIcon: <Gridicon icon="align-image-left" size={ 18 } />,
40-
};
41-
42-
this.state = initialState;
43-
}
29+
state = {
30+
childSelected: 'Published',
31+
selectedCount: 10,
32+
compactButtons: false,
33+
selectedIcon: <Gridicon icon="align-image-left" size={ 18 } />,
34+
};
4435

4536
toggleButtons = () => {
4637
this.setState( { compactButtons: ! this.state.compactButtons } );
@@ -51,9 +42,9 @@ class SelectDropdownExample extends React.PureComponent {
5142

5243
return (
5344
<div className="docs__select-dropdown-container">
54-
<a className="docs__design-toggle button" onClick={ this.toggleButtons }>
45+
<button className="docs__design-toggle button" onClick={ this.toggleButtons }>
5546
{ toggleButtonsText }
56-
</a>
47+
</button>
5748

5849
<h3>Items passed as options prop</h3>
5950
<SelectDropdown
@@ -69,43 +60,43 @@ class SelectDropdownExample extends React.PureComponent {
6960
selectedText={ this.state.childSelected }
7061
selectedCount={ this.state.selectedCount }
7162
>
72-
<DropdownLabel>
63+
<SelectDropdown.Label>
7364
<strong>Statuses</strong>
74-
</DropdownLabel>
65+
</SelectDropdown.Label>
7566

76-
<DropdownItem
67+
<SelectDropdown.Item
7768
count={ 10 }
7869
selected={ this.state.childSelected === 'Published' }
7970
onClick={ this.getSelectItemHandler( 'Published', 10 ) }
8071
>
8172
Published
82-
</DropdownItem>
73+
</SelectDropdown.Item>
8374

84-
<DropdownItem
75+
<SelectDropdown.Item
8576
count={ 4 }
8677
selected={ this.state.childSelected === 'Scheduled' }
8778
onClick={ this.getSelectItemHandler( 'Scheduled', 4 ) }
8879
>
8980
Scheduled
90-
</DropdownItem>
81+
</SelectDropdown.Item>
9182

92-
<DropdownItem
83+
<SelectDropdown.Item
9384
count={ 3343 }
9485
selected={ this.state.childSelected === 'Drafts' }
9586
onClick={ this.getSelectItemHandler( 'Drafts', 3343 ) }
9687
>
9788
Drafts
98-
</DropdownItem>
89+
</SelectDropdown.Item>
9990

100-
<DropdownSeparator />
91+
<SelectDropdown.Separator />
10192

102-
<DropdownItem
93+
<SelectDropdown.Item
10394
count={ 3 }
10495
selected={ this.state.childSelected === 'Trashed' }
10596
onClick={ this.getSelectItemHandler( 'Trashed', 3 ) }
10697
>
10798
Trashed
108-
</DropdownItem>
99+
</SelectDropdown.Item>
109100
</SelectDropdown>
110101

111102
<h3 style={ { marginTop: 20 } }>With Icons in Items Passed as Options</h3>
@@ -143,11 +134,11 @@ class SelectDropdownExample extends React.PureComponent {
143134
selectedText={ this.state.childSelected }
144135
selectedIcon={ this.state.selectedIcon }
145136
>
146-
<DropdownLabel>
137+
<SelectDropdown.Label>
147138
<strong>Statuses</strong>
148-
</DropdownLabel>
139+
</SelectDropdown.Label>
149140

150-
<DropdownItem
141+
<SelectDropdown.Item
151142
selected={ this.state.childSelected === 'Published' }
152143
icon={ <Gridicon icon="align-image-left" size={ 18 } /> }
153144
onClick={ this.getSelectItemHandler(
@@ -157,9 +148,9 @@ class SelectDropdownExample extends React.PureComponent {
157148
) }
158149
>
159150
Published
160-
</DropdownItem>
151+
</SelectDropdown.Item>
161152

162-
<DropdownItem
153+
<SelectDropdown.Item
163154
selected={ this.state.childSelected === 'Scheduled' }
164155
icon={ <Gridicon icon="calendar" size={ 18 } /> }
165156
onClick={ this.getSelectItemHandler(
@@ -169,9 +160,9 @@ class SelectDropdownExample extends React.PureComponent {
169160
) }
170161
>
171162
Scheduled
172-
</DropdownItem>
163+
</SelectDropdown.Item>
173164

174-
<DropdownItem
165+
<SelectDropdown.Item
175166
selected={ this.state.childSelected === 'Drafts' }
176167
icon={ <Gridicon icon="create" size={ 18 } /> }
177168
onClick={ this.getSelectItemHandler(
@@ -181,11 +172,11 @@ class SelectDropdownExample extends React.PureComponent {
181172
) }
182173
>
183174
Drafts
184-
</DropdownItem>
175+
</SelectDropdown.Item>
185176

186-
<DropdownSeparator />
177+
<SelectDropdown.Separator />
187178

188-
<DropdownItem
179+
<SelectDropdown.Item
189180
selected={ this.state.childSelected === 'Trashed' }
190181
icon={ <Gridicon icon="trash" size={ 18 } /> }
191182
onClick={ this.getSelectItemHandler(
@@ -195,7 +186,7 @@ class SelectDropdownExample extends React.PureComponent {
195186
) }
196187
>
197188
Trashed
198-
</DropdownItem>
189+
</SelectDropdown.Item>
199190
</SelectDropdown>
200191

201192
<h3 style={ { marginTop: 20 } }>max-width: 220px;</h3>
@@ -206,17 +197,17 @@ class SelectDropdownExample extends React.PureComponent {
206197
selectedText="Published publish publish publish"
207198
selectedCount={ 10 }
208199
>
209-
<DropdownLabel>
200+
<SelectDropdown.Label>
210201
<strong>Statuses</strong>
211-
</DropdownLabel>
212-
<DropdownItem count={ 10 } selected={ true }>
202+
</SelectDropdown.Label>
203+
<SelectDropdown.Item count={ 10 } selected={ true }>
213204
Published publish publish publish
214-
</DropdownItem>
215-
<DropdownItem count={ 4 }> Scheduled scheduled</DropdownItem>
216-
<DropdownItem count={ 3343 }>Drafts</DropdownItem>
217-
<DropdownItem disabled={ true }>Disabled Item</DropdownItem>
218-
<DropdownSeparator />
219-
<DropdownItem count={ 3 }>Trashed</DropdownItem>
205+
</SelectDropdown.Item>
206+
<SelectDropdown.Item count={ 4 }> Scheduled scheduled</SelectDropdown.Item>
207+
<SelectDropdown.Item count={ 3343 }>Drafts</SelectDropdown.Item>
208+
<SelectDropdown.Item disabled={ true }>Disabled Item</SelectDropdown.Item>
209+
<SelectDropdown.Separator />
210+
<SelectDropdown.Item count={ 3 }>Trashed</SelectDropdown.Item>
220211
</SelectDropdown>
221212

222213
<h3 style={ { marginTop: 20 } }>Disabled State</h3>
@@ -244,10 +235,12 @@ class SelectDropdownExample extends React.PureComponent {
244235
selectedIcon: icon,
245236
} );
246237

238+
// eslint-disable-next-line no-console
247239
console.log( 'Select Dropdown Item (selected):', childSelected );
248240
};
249241

250242
onDropdownSelect( option ) {
243+
// eslint-disable-next-line no-console
251244
console.log( 'Select Dropdown (selected):', option );
252245
}
253246
}

0 commit comments

Comments
 (0)