@instructure/ui-select 10.4.2-snapshot-11 → 10.4.2-snapshot-13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1 -1
- package/package.json +24 -24
- package/src/Select/README.md +2210 -1079
package/src/Select/README.md
CHANGED
|
@@ -14,77 +14,187 @@ describes: Select
|
|
|
14
14
|
|
|
15
15
|
`Select` is a controlled-only component. The consuming app or component must manage any state needed. A variety of request callbacks are provided as prompts for state updates. `onRequestShowOptions`, for example, is fired when `Select` thinks the `isShowingOptions` prop should be updated to `true`. Of course, the consumer can always choose how to react to these callbacks.
|
|
16
16
|
|
|
17
|
-
```javascript
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
- ```javascript
|
|
18
|
+
class SingleSelectExample extends React.Component {
|
|
19
|
+
state = {
|
|
20
|
+
inputValue: this.props.options[0].label,
|
|
21
|
+
isShowingOptions: false,
|
|
22
|
+
highlightedOptionId: null,
|
|
23
|
+
selectedOptionId: this.props.options[0].id,
|
|
24
|
+
announcement: null
|
|
25
|
+
}
|
|
21
26
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
isShowingOptions: false,
|
|
26
|
-
highlightedOptionId: null,
|
|
27
|
-
selectedOptionId: this.props.options[0].id,
|
|
28
|
-
announcement: null
|
|
29
|
-
}
|
|
27
|
+
getOptionById(queryId) {
|
|
28
|
+
return this.props.options.find(({ id }) => id === queryId)
|
|
29
|
+
}
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
handleShowOptions = (event) => {
|
|
32
|
+
this.setState({
|
|
33
|
+
isShowingOptions: true
|
|
34
|
+
})
|
|
35
|
+
}
|
|
34
36
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
handleHideOptions = (event) => {
|
|
38
|
+
const { selectedOptionId } = this.state
|
|
39
|
+
const option = this.getOptionById(selectedOptionId).label
|
|
40
|
+
this.setState({
|
|
41
|
+
isShowingOptions: false,
|
|
42
|
+
highlightedOptionId: null,
|
|
43
|
+
inputValue: selectedOptionId ? option : '',
|
|
44
|
+
announcement: 'List collapsed.'
|
|
45
|
+
})
|
|
46
|
+
}
|
|
40
47
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
highlightedOptionId: null,
|
|
47
|
-
inputValue: selectedOptionId ? option : '',
|
|
48
|
-
announcement: 'List collapsed.'
|
|
49
|
-
})
|
|
50
|
-
}
|
|
48
|
+
handleBlur = (event) => {
|
|
49
|
+
this.setState({
|
|
50
|
+
highlightedOptionId: null
|
|
51
|
+
})
|
|
52
|
+
}
|
|
51
53
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
handleHighlightOption = (event, { id }) => {
|
|
55
|
+
event.persist()
|
|
56
|
+
const optionsAvailable = `${this.props.options.length} options available.`
|
|
57
|
+
const nowOpen = !this.state.isShowingOptions
|
|
58
|
+
? `List expanded. ${optionsAvailable}`
|
|
59
|
+
: ''
|
|
60
|
+
const option = this.getOptionById(id).label
|
|
61
|
+
this.setState((state) => ({
|
|
62
|
+
highlightedOptionId: id,
|
|
63
|
+
inputValue: event.type === 'keydown' ? option : state.inputValue,
|
|
64
|
+
announcement: `${option} ${nowOpen}`
|
|
65
|
+
}))
|
|
66
|
+
}
|
|
57
67
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}))
|
|
68
|
-
}
|
|
68
|
+
handleSelectOption = (event, { id }) => {
|
|
69
|
+
const option = this.getOptionById(id).label
|
|
70
|
+
this.setState({
|
|
71
|
+
selectedOptionId: id,
|
|
72
|
+
inputValue: option,
|
|
73
|
+
isShowingOptions: false,
|
|
74
|
+
announcement: `"${option}" selected. List collapsed.`
|
|
75
|
+
})
|
|
76
|
+
}
|
|
69
77
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
+
render() {
|
|
79
|
+
const {
|
|
80
|
+
inputValue,
|
|
81
|
+
isShowingOptions,
|
|
82
|
+
highlightedOptionId,
|
|
83
|
+
selectedOptionId,
|
|
84
|
+
announcement
|
|
85
|
+
} = this.state
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<div>
|
|
89
|
+
<Select
|
|
90
|
+
renderLabel="Single Select"
|
|
91
|
+
assistiveText="Use arrow keys to navigate options."
|
|
92
|
+
inputValue={inputValue}
|
|
93
|
+
isShowingOptions={isShowingOptions}
|
|
94
|
+
onBlur={this.handleBlur}
|
|
95
|
+
onRequestShowOptions={this.handleShowOptions}
|
|
96
|
+
onRequestHideOptions={this.handleHideOptions}
|
|
97
|
+
onRequestHighlightOption={this.handleHighlightOption}
|
|
98
|
+
onRequestSelectOption={this.handleSelectOption}
|
|
99
|
+
>
|
|
100
|
+
{this.props.options.map((option) => {
|
|
101
|
+
return (
|
|
102
|
+
<Select.Option
|
|
103
|
+
id={option.id}
|
|
104
|
+
key={option.id}
|
|
105
|
+
isHighlighted={option.id === highlightedOptionId}
|
|
106
|
+
isSelected={option.id === selectedOptionId}
|
|
107
|
+
>
|
|
108
|
+
{option.label}
|
|
109
|
+
</Select.Option>
|
|
110
|
+
)
|
|
111
|
+
})}
|
|
112
|
+
</Select>
|
|
113
|
+
<Alert
|
|
114
|
+
liveRegion={() => document.getElementById('flash-messages')}
|
|
115
|
+
liveRegionPoliteness="assertive"
|
|
116
|
+
screenReaderOnly
|
|
117
|
+
>
|
|
118
|
+
{announcement}
|
|
119
|
+
</Alert>
|
|
120
|
+
</div>
|
|
121
|
+
)
|
|
122
|
+
}
|
|
78
123
|
}
|
|
79
124
|
|
|
80
|
-
render
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
125
|
+
render(
|
|
126
|
+
<View>
|
|
127
|
+
<SingleSelectExample
|
|
128
|
+
options={[
|
|
129
|
+
{ id: 'opt1', label: 'Alaska' },
|
|
130
|
+
{ id: 'opt2', label: 'American Samoa' },
|
|
131
|
+
{ id: 'opt3', label: 'Arizona' },
|
|
132
|
+
{ id: 'opt4', label: 'Arkansas' },
|
|
133
|
+
{ id: 'opt5', label: 'California' },
|
|
134
|
+
{ id: 'opt6', label: 'Colorado' },
|
|
135
|
+
{ id: 'opt7', label: 'Connecticut' },
|
|
136
|
+
{ id: 'opt8', label: 'Delaware' },
|
|
137
|
+
{ id: 'opt9', label: 'District Of Columbia' },
|
|
138
|
+
{ id: 'opt10', label: 'Federated States Of Micronesia' },
|
|
139
|
+
{ id: 'opt11', label: 'Florida' },
|
|
140
|
+
{ id: 'opt12', label: 'Georgia (unavailable)' },
|
|
141
|
+
{ id: 'opt13', label: 'Guam' },
|
|
142
|
+
{ id: 'opt14', label: 'Hawaii' },
|
|
143
|
+
{ id: 'opt15', label: 'Idaho' },
|
|
144
|
+
{ id: 'opt16', label: 'Illinois' }
|
|
145
|
+
]}
|
|
146
|
+
/>
|
|
147
|
+
</View>
|
|
148
|
+
)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
- ```js
|
|
152
|
+
const SingleSelectExample = ({ options }) => {
|
|
153
|
+
const [inputValue, setInputValue] = useState(options[0].label)
|
|
154
|
+
const [isShowingOptions, setIsShowingOptions] = useState(false)
|
|
155
|
+
const [highlightedOptionId, setHighlightedOptionId] = useState(null)
|
|
156
|
+
const [selectedOptionId, setSelectedOptionId] = useState(options[0].id)
|
|
157
|
+
const [announcement, setAnnouncement] = useState(null)
|
|
158
|
+
|
|
159
|
+
const getOptionById = (queryId) => {
|
|
160
|
+
return options.find(({ id }) => id === queryId)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const handleShowOptions = (event) => {
|
|
164
|
+
setIsShowingOptions(true)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const handleHideOptions = (event) => {
|
|
168
|
+
const option = getOptionById(selectedOptionId).label
|
|
169
|
+
setIsShowingOptions(false)
|
|
170
|
+
setHighlightedOptionId(null)
|
|
171
|
+
setSelectedOptionId(selectedOptionId ? option : '')
|
|
172
|
+
setAnnouncement('List collapsed.')
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const handleBlur = (event) => {
|
|
176
|
+
setHighlightedOptionId(null)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const handleHighlightOption = (event, { id }) => {
|
|
180
|
+
event.persist()
|
|
181
|
+
const optionsAvailable = `${options.length} options available.`
|
|
182
|
+
const nowOpen = !isShowingOptions
|
|
183
|
+
? `List expanded. ${optionsAvailable}`
|
|
184
|
+
: ''
|
|
185
|
+
const option = getOptionById(id).label
|
|
186
|
+
setHighlightedOptionId(id)
|
|
187
|
+
setInputValue(event.type === 'keydown' ? option : inputValue)
|
|
188
|
+
setAnnouncement(`${option} ${nowOpen}`)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const handleSelectOption = (event, { id }) => {
|
|
192
|
+
const option = getOptionById(id).label
|
|
193
|
+
setSelectedOptionId(id)
|
|
194
|
+
setInputValue(option)
|
|
195
|
+
setIsShowingOptions(false)
|
|
196
|
+
setAnnouncement(`"${option}" selected. List collapsed.`)
|
|
197
|
+
}
|
|
88
198
|
|
|
89
199
|
return (
|
|
90
200
|
<div>
|
|
@@ -93,13 +203,13 @@ class SingleSelectExample extends React.Component {
|
|
|
93
203
|
assistiveText="Use arrow keys to navigate options."
|
|
94
204
|
inputValue={inputValue}
|
|
95
205
|
isShowingOptions={isShowingOptions}
|
|
96
|
-
onBlur={
|
|
97
|
-
onRequestShowOptions={
|
|
98
|
-
onRequestHideOptions={
|
|
99
|
-
onRequestHighlightOption={
|
|
100
|
-
onRequestSelectOption={
|
|
206
|
+
onBlur={handleBlur}
|
|
207
|
+
onRequestShowOptions={handleShowOptions}
|
|
208
|
+
onRequestHideOptions={handleHideOptions}
|
|
209
|
+
onRequestHighlightOption={handleHighlightOption}
|
|
210
|
+
onRequestSelectOption={handleSelectOption}
|
|
101
211
|
>
|
|
102
|
-
{
|
|
212
|
+
{options.map((option) => {
|
|
103
213
|
return (
|
|
104
214
|
<Select.Option
|
|
105
215
|
id={option.id}
|
|
@@ -107,7 +217,7 @@ class SingleSelectExample extends React.Component {
|
|
|
107
217
|
isHighlighted={option.id === highlightedOptionId}
|
|
108
218
|
isSelected={option.id === selectedOptionId}
|
|
109
219
|
>
|
|
110
|
-
{
|
|
220
|
+
{option.label}
|
|
111
221
|
</Select.Option>
|
|
112
222
|
)
|
|
113
223
|
})}
|
|
@@ -117,38 +227,36 @@ class SingleSelectExample extends React.Component {
|
|
|
117
227
|
liveRegionPoliteness="assertive"
|
|
118
228
|
screenReaderOnly
|
|
119
229
|
>
|
|
120
|
-
{
|
|
230
|
+
{announcement}
|
|
121
231
|
</Alert>
|
|
122
232
|
</div>
|
|
123
233
|
)
|
|
124
234
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
)
|
|
151
|
-
```
|
|
235
|
+
render(
|
|
236
|
+
<View>
|
|
237
|
+
<SingleSelectExample
|
|
238
|
+
options={[
|
|
239
|
+
{ id: 'opt1', label: 'Alaska' },
|
|
240
|
+
{ id: 'opt2', label: 'American Samoa' },
|
|
241
|
+
{ id: 'opt3', label: 'Arizona' },
|
|
242
|
+
{ id: 'opt4', label: 'Arkansas' },
|
|
243
|
+
{ id: 'opt5', label: 'California' },
|
|
244
|
+
{ id: 'opt6', label: 'Colorado' },
|
|
245
|
+
{ id: 'opt7', label: 'Connecticut' },
|
|
246
|
+
{ id: 'opt8', label: 'Delaware' },
|
|
247
|
+
{ id: 'opt9', label: 'District Of Columbia' },
|
|
248
|
+
{ id: 'opt10', label: 'Federated States Of Micronesia' },
|
|
249
|
+
{ id: 'opt11', label: 'Florida' },
|
|
250
|
+
{ id: 'opt12', label: 'Georgia (unavailable)' },
|
|
251
|
+
{ id: 'opt13', label: 'Guam' },
|
|
252
|
+
{ id: 'opt14', label: 'Hawaii' },
|
|
253
|
+
{ id: 'opt15', label: 'Idaho' },
|
|
254
|
+
{ id: 'opt16', label: 'Illinois' }
|
|
255
|
+
]}
|
|
256
|
+
/>
|
|
257
|
+
</View>
|
|
258
|
+
)
|
|
259
|
+
```
|
|
152
260
|
|
|
153
261
|
#### Providing autocomplete behavior
|
|
154
262
|
|
|
@@ -156,449 +264,381 @@ It's best practice to always provide autocomplete functionality to help users ma
|
|
|
156
264
|
|
|
157
265
|
> Note: Select makes some conditional assumptions about keyboard behavior. For example, if the list is NOT showing, up/down arrow keys and the space key, will show the list. Otherwise, the arrows will navigate options and the space key will type a space character.
|
|
158
266
|
|
|
159
|
-
```javascript
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
selectedOptionId: null,
|
|
170
|
-
filteredOptions: this.props.options,
|
|
171
|
-
announcement: null
|
|
172
|
-
}
|
|
267
|
+
- ```javascript
|
|
268
|
+
class AutocompleteExample extends React.Component {
|
|
269
|
+
state = {
|
|
270
|
+
inputValue: '',
|
|
271
|
+
isShowingOptions: false,
|
|
272
|
+
highlightedOptionId: null,
|
|
273
|
+
selectedOptionId: null,
|
|
274
|
+
filteredOptions: this.props.options,
|
|
275
|
+
announcement: null
|
|
276
|
+
}
|
|
173
277
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
278
|
+
getOptionById(queryId) {
|
|
279
|
+
return this.props.options.find(({ id }) => id === queryId)
|
|
280
|
+
}
|
|
177
281
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
282
|
+
getOptionsChangedMessage(newOptions) {
|
|
283
|
+
let message =
|
|
284
|
+
newOptions.length !== this.state.filteredOptions.length
|
|
285
|
+
? `${newOptions.length} options available.` // options changed, announce new total
|
|
286
|
+
: null // options haven't changed, don't announce
|
|
287
|
+
if (message && newOptions.length > 0) {
|
|
288
|
+
// options still available
|
|
289
|
+
if (this.state.highlightedOptionId !== newOptions[0].id) {
|
|
290
|
+
// highlighted option hasn't been announced
|
|
291
|
+
const option = this.getOptionById(newOptions[0].id).label
|
|
292
|
+
message = `${option}. ${message}`
|
|
293
|
+
}
|
|
188
294
|
}
|
|
295
|
+
return message
|
|
189
296
|
}
|
|
190
|
-
return message
|
|
191
|
-
}
|
|
192
297
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
298
|
+
filterOptions = (value) => {
|
|
299
|
+
return this.props.options.filter((option) =>
|
|
300
|
+
option.label.toLowerCase().startsWith(value.toLowerCase())
|
|
301
|
+
)
|
|
302
|
+
}
|
|
198
303
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
304
|
+
matchValue() {
|
|
305
|
+
const {
|
|
306
|
+
filteredOptions,
|
|
307
|
+
inputValue,
|
|
308
|
+
highlightedOptionId,
|
|
309
|
+
selectedOptionId
|
|
310
|
+
} = this.state
|
|
311
|
+
|
|
312
|
+
// an option matching user input exists
|
|
313
|
+
if (filteredOptions.length === 1) {
|
|
314
|
+
const onlyOption = filteredOptions[0]
|
|
315
|
+
// automatically select the matching option
|
|
316
|
+
if (onlyOption.label.toLowerCase() === inputValue.toLowerCase()) {
|
|
317
|
+
return {
|
|
318
|
+
inputValue: onlyOption.label,
|
|
319
|
+
selectedOptionId: onlyOption.id,
|
|
320
|
+
filteredOptions: this.filterOptions('')
|
|
321
|
+
}
|
|
216
322
|
}
|
|
217
323
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
324
|
+
// allow user to return to empty input and no selection
|
|
325
|
+
if (inputValue.length === 0) {
|
|
326
|
+
return { selectedOptionId: null }
|
|
327
|
+
}
|
|
328
|
+
// no match found, return selected option label to input
|
|
329
|
+
if (selectedOptionId) {
|
|
330
|
+
const selectedOption = this.getOptionById(selectedOptionId)
|
|
331
|
+
return { inputValue: selectedOption.label }
|
|
332
|
+
}
|
|
333
|
+
// input value is from highlighted option, not user input
|
|
334
|
+
// clear input, reset options
|
|
335
|
+
if (highlightedOptionId) {
|
|
336
|
+
if (inputValue === this.getOptionById(highlightedOptionId).label) {
|
|
337
|
+
return {
|
|
338
|
+
inputValue: '',
|
|
339
|
+
filteredOptions: this.filterOptions('')
|
|
340
|
+
}
|
|
235
341
|
}
|
|
236
342
|
}
|
|
237
343
|
}
|
|
238
|
-
}
|
|
239
344
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
345
|
+
handleShowOptions = (event) => {
|
|
346
|
+
this.setState(({ filteredOptions }) => ({
|
|
347
|
+
isShowingOptions: true,
|
|
348
|
+
announcement: `List expanded. ${filteredOptions.length} options available.`
|
|
349
|
+
}))
|
|
350
|
+
}
|
|
246
351
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
352
|
+
handleHideOptions = (event) => {
|
|
353
|
+
const { selectedOptionId, inputValue } = this.state
|
|
354
|
+
this.setState({
|
|
355
|
+
isShowingOptions: false,
|
|
356
|
+
highlightedOptionId: null,
|
|
357
|
+
announcement: 'List collapsed.',
|
|
358
|
+
...this.matchValue()
|
|
359
|
+
})
|
|
360
|
+
}
|
|
256
361
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
362
|
+
handleBlur = (event) => {
|
|
363
|
+
this.setState({ highlightedOptionId: null })
|
|
364
|
+
}
|
|
260
365
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
366
|
+
handleHighlightOption = (event, { id }) => {
|
|
367
|
+
event.persist()
|
|
368
|
+
const option = this.getOptionById(id)
|
|
369
|
+
if (!option) return // prevent highlighting of empty option
|
|
370
|
+
this.setState((state) => ({
|
|
371
|
+
highlightedOptionId: id,
|
|
372
|
+
inputValue: event.type === 'keydown' ? option.label : state.inputValue,
|
|
373
|
+
announcement: option.label
|
|
374
|
+
}))
|
|
375
|
+
}
|
|
271
376
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
377
|
+
handleSelectOption = (event, { id }) => {
|
|
378
|
+
const option = this.getOptionById(id)
|
|
379
|
+
if (!option) return // prevent selecting of empty option
|
|
380
|
+
this.setState({
|
|
381
|
+
selectedOptionId: id,
|
|
382
|
+
inputValue: option.label,
|
|
383
|
+
isShowingOptions: false,
|
|
384
|
+
filteredOptions: this.props.options,
|
|
385
|
+
announcement: `${option.label} selected. List collapsed.`
|
|
386
|
+
})
|
|
387
|
+
}
|
|
283
388
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
389
|
+
handleInputChange = (event) => {
|
|
390
|
+
const value = event.target.value
|
|
391
|
+
const newOptions = this.filterOptions(value)
|
|
392
|
+
this.setState((state) => ({
|
|
393
|
+
inputValue: value,
|
|
394
|
+
filteredOptions: newOptions,
|
|
395
|
+
highlightedOptionId: newOptions.length > 0 ? newOptions[0].id : null,
|
|
396
|
+
isShowingOptions: true,
|
|
397
|
+
selectedOptionId: value === '' ? null : state.selectedOptionId,
|
|
398
|
+
announcement: this.getOptionsChangedMessage(newOptions)
|
|
399
|
+
}))
|
|
400
|
+
}
|
|
296
401
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
402
|
+
render() {
|
|
403
|
+
const {
|
|
404
|
+
inputValue,
|
|
405
|
+
isShowingOptions,
|
|
406
|
+
highlightedOptionId,
|
|
407
|
+
selectedOptionId,
|
|
408
|
+
filteredOptions,
|
|
409
|
+
announcement
|
|
410
|
+
} = this.state
|
|
306
411
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
412
|
+
return (
|
|
413
|
+
<div>
|
|
414
|
+
<Select
|
|
415
|
+
renderLabel="Autocomplete"
|
|
416
|
+
assistiveText="Type or use arrow keys to navigate options."
|
|
417
|
+
placeholder="Start typing to search..."
|
|
418
|
+
inputValue={inputValue}
|
|
419
|
+
isShowingOptions={isShowingOptions}
|
|
420
|
+
onBlur={this.handleBlur}
|
|
421
|
+
onInputChange={this.handleInputChange}
|
|
422
|
+
onRequestShowOptions={this.handleShowOptions}
|
|
423
|
+
onRequestHideOptions={this.handleHideOptions}
|
|
424
|
+
onRequestHighlightOption={this.handleHighlightOption}
|
|
425
|
+
onRequestSelectOption={this.handleSelectOption}
|
|
426
|
+
renderBeforeInput={<IconUserSolid inline={false} />}
|
|
427
|
+
renderAfterInput={<IconSearchLine inline={false} />}
|
|
428
|
+
>
|
|
429
|
+
{filteredOptions.length > 0 ? (
|
|
430
|
+
filteredOptions.map((option) => {
|
|
431
|
+
return (
|
|
432
|
+
<Select.Option
|
|
433
|
+
id={option.id}
|
|
434
|
+
key={option.id}
|
|
435
|
+
isHighlighted={option.id === highlightedOptionId}
|
|
436
|
+
isSelected={option.id === selectedOptionId}
|
|
437
|
+
isDisabled={option.disabled}
|
|
438
|
+
renderBeforeLabel={
|
|
439
|
+
!option.disabled ? IconUserSolid : IconUserLine
|
|
440
|
+
}
|
|
441
|
+
>
|
|
442
|
+
{!option.disabled
|
|
443
|
+
? option.label
|
|
444
|
+
: `${option.label} (unavailable)`}
|
|
445
|
+
</Select.Option>
|
|
446
|
+
)
|
|
447
|
+
})
|
|
448
|
+
) : (
|
|
449
|
+
<Select.Option id="empty-option" key="empty-option">
|
|
450
|
+
---
|
|
338
451
|
</Select.Option>
|
|
339
|
-
)
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
</
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
liveRegionPoliteness="assertive"
|
|
352
|
-
screenReaderOnly
|
|
353
|
-
>
|
|
354
|
-
{ announcement }
|
|
355
|
-
</Alert>
|
|
356
|
-
</div>
|
|
357
|
-
)
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
render(
|
|
362
|
-
<View>
|
|
363
|
-
<AutocompleteExample
|
|
364
|
-
options={[
|
|
365
|
-
{ id: 'opt0', label: 'Aaron Aaronson' },
|
|
366
|
-
{ id: 'opt1', label: 'Amber Murphy' },
|
|
367
|
-
{ id: 'opt2', label: 'Andrew Miller' },
|
|
368
|
-
{ id: 'opt3', label: 'Barbara Ward' },
|
|
369
|
-
{ id: 'opt4', label: 'Byron Cranston', disabled: true },
|
|
370
|
-
{ id: 'opt5', label: 'Dennis Reynolds' },
|
|
371
|
-
{ id: 'opt6', label: 'Dee Reynolds' },
|
|
372
|
-
{ id: 'opt7', label: 'Ezra Betterthan' },
|
|
373
|
-
{ id: 'opt8', label: 'Jeff Spicoli' },
|
|
374
|
-
{ id: 'opt9', label: 'Joseph Smith' },
|
|
375
|
-
{ id: 'opt10', label: 'Jasmine Diaz' },
|
|
376
|
-
{ id: 'opt11', label: 'Martin Harris' },
|
|
377
|
-
{ id: 'opt12', label: 'Michael Morgan', disabled: true },
|
|
378
|
-
{ id: 'opt13', label: 'Michelle Rodriguez' },
|
|
379
|
-
{ id: 'opt14', label: 'Ziggy Stardust' }
|
|
380
|
-
]}
|
|
381
|
-
/>
|
|
382
|
-
</View>
|
|
383
|
-
)
|
|
384
|
-
```
|
|
385
|
-
|
|
386
|
-
#### Highlighting and selecting options
|
|
387
|
-
|
|
388
|
-
To mark an option as "highlighted", use the option's `isHighlighted` prop. Note that only one highlighted option is permitted. Similarly, use `isSelected` to mark an option or multiple options as "selected". When allowing multiple selections, it's best to render a [Tag](#Tag) for each selected option via the `renderBeforeInput` prop.
|
|
389
|
-
|
|
390
|
-
```javascript
|
|
391
|
-
---
|
|
392
|
-
type: example
|
|
393
|
-
---
|
|
394
|
-
|
|
395
|
-
class MultipleSelectExample extends React.Component {
|
|
396
|
-
state = {
|
|
397
|
-
inputValue: '',
|
|
398
|
-
isShowingOptions: false,
|
|
399
|
-
highlightedOptionId: null,
|
|
400
|
-
selectedOptionId: ['opt1', 'opt6'],
|
|
401
|
-
filteredOptions: this.props.options,
|
|
402
|
-
announcement: null
|
|
452
|
+
)}
|
|
453
|
+
</Select>
|
|
454
|
+
<Alert
|
|
455
|
+
liveRegion={() => document.getElementById('flash-messages')}
|
|
456
|
+
liveRegionPoliteness="assertive"
|
|
457
|
+
screenReaderOnly
|
|
458
|
+
>
|
|
459
|
+
{announcement}
|
|
460
|
+
</Alert>
|
|
461
|
+
</div>
|
|
462
|
+
)
|
|
463
|
+
}
|
|
403
464
|
}
|
|
404
465
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
466
|
+
render(
|
|
467
|
+
<View>
|
|
468
|
+
<AutocompleteExample
|
|
469
|
+
options={[
|
|
470
|
+
{ id: 'opt0', label: 'Aaron Aaronson' },
|
|
471
|
+
{ id: 'opt1', label: 'Amber Murphy' },
|
|
472
|
+
{ id: 'opt2', label: 'Andrew Miller' },
|
|
473
|
+
{ id: 'opt3', label: 'Barbara Ward' },
|
|
474
|
+
{ id: 'opt4', label: 'Byron Cranston', disabled: true },
|
|
475
|
+
{ id: 'opt5', label: 'Dennis Reynolds' },
|
|
476
|
+
{ id: 'opt6', label: 'Dee Reynolds' },
|
|
477
|
+
{ id: 'opt7', label: 'Ezra Betterthan' },
|
|
478
|
+
{ id: 'opt8', label: 'Jeff Spicoli' },
|
|
479
|
+
{ id: 'opt9', label: 'Joseph Smith' },
|
|
480
|
+
{ id: 'opt10', label: 'Jasmine Diaz' },
|
|
481
|
+
{ id: 'opt11', label: 'Martin Harris' },
|
|
482
|
+
{ id: 'opt12', label: 'Michael Morgan', disabled: true },
|
|
483
|
+
{ id: 'opt13', label: 'Michelle Rodriguez' },
|
|
484
|
+
{ id: 'opt14', label: 'Ziggy Stardust' }
|
|
485
|
+
]}
|
|
486
|
+
/>
|
|
487
|
+
</View>
|
|
488
|
+
)
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
- ```js
|
|
492
|
+
const AutocompleteExample = ({ options }) => {
|
|
493
|
+
const [inputValue, setInputValue] = useState('')
|
|
494
|
+
const [isShowingOptions, setIsShowingOptions] = useState(false)
|
|
495
|
+
const [highlightedOptionId, setHighlightedOptionId] = useState(null)
|
|
496
|
+
const [selectedOptionId, setSelectedOptionId] = useState(null)
|
|
497
|
+
const [filteredOptions, setFilteredOptions] = useState(options)
|
|
498
|
+
const [announcement, setAnnouncement] = useState(null)
|
|
499
|
+
|
|
500
|
+
const getOptionById = (queryId) => {
|
|
501
|
+
return options.find(({ id }) => id === queryId)
|
|
502
|
+
}
|
|
408
503
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
504
|
+
const getOptionsChangedMessage = (newOptions) => {
|
|
505
|
+
let message =
|
|
506
|
+
newOptions.length !== filteredOptions.length
|
|
507
|
+
? `${newOptions.length} options available.` // options changed, announce new total
|
|
508
|
+
: null // options haven't changed, don't announce
|
|
509
|
+
if (message && newOptions.length > 0) {
|
|
510
|
+
// options still available
|
|
511
|
+
if (highlightedOptionId !== newOptions[0].id) {
|
|
512
|
+
// highlighted option hasn't been announced
|
|
513
|
+
const option = getOptionById(newOptions[0].id).label
|
|
514
|
+
message = `${option}. ${message}`
|
|
515
|
+
}
|
|
419
516
|
}
|
|
517
|
+
return message
|
|
420
518
|
}
|
|
421
|
-
return message
|
|
422
|
-
}
|
|
423
519
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
520
|
+
const filterOptions = (value) => {
|
|
521
|
+
return options.filter((option) =>
|
|
522
|
+
option.label.toLowerCase().startsWith(value.toLowerCase())
|
|
523
|
+
)
|
|
524
|
+
}
|
|
429
525
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
filteredOptions
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
if (filteredOptions.length === 1) {
|
|
440
|
-
const onlyOption = filteredOptions[0]
|
|
441
|
-
// automatically select the matching option
|
|
442
|
-
if (onlyOption.label.toLowerCase() === inputValue.toLowerCase()) {
|
|
443
|
-
return {
|
|
444
|
-
inputValue: '',
|
|
445
|
-
selectedOptionId: [...selectedOptionId, onlyOption.id],
|
|
446
|
-
filteredOptions: this.filterOptions('')
|
|
526
|
+
const matchValue = () => {
|
|
527
|
+
// an option matching user input exists
|
|
528
|
+
if (filteredOptions.length === 1) {
|
|
529
|
+
const onlyOption = filteredOptions[0]
|
|
530
|
+
// automatically select the matching option
|
|
531
|
+
if (onlyOption.label.toLowerCase() === inputValue.toLowerCase()) {
|
|
532
|
+
setInputValue(onlyOption.label)
|
|
533
|
+
setSelectedOptionId(onlyOption.id)
|
|
534
|
+
setFilteredOptions(filterOptions(''))
|
|
447
535
|
}
|
|
448
536
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
537
|
+
// allow user to return to empty input and no selection
|
|
538
|
+
else if (inputValue.length === 0) {
|
|
539
|
+
setSelectedOptionId(null)
|
|
540
|
+
}
|
|
541
|
+
// no match found, return selected option label to input
|
|
542
|
+
else if (selectedOptionId) {
|
|
543
|
+
const selectedOption = getOptionById(selectedOptionId)
|
|
544
|
+
setInputValue(selectedOption.label)
|
|
545
|
+
}
|
|
546
|
+
// input value is from highlighted option, not user input
|
|
547
|
+
// clear input, reset options
|
|
548
|
+
else if (highlightedOptionId) {
|
|
549
|
+
if (inputValue === getOptionById(highlightedOptionId).label) {
|
|
550
|
+
setInputValue('')
|
|
551
|
+
setFilteredOptions(filterOptions(''))
|
|
457
552
|
}
|
|
458
553
|
}
|
|
459
554
|
}
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
handleShowOptions = (event) => {
|
|
463
|
-
this.setState({ isShowingOptions: true })
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
handleHideOptions = (event) => {
|
|
467
|
-
this.setState({
|
|
468
|
-
isShowingOptions: false,
|
|
469
|
-
...this.matchValue()
|
|
470
|
-
})
|
|
471
|
-
}
|
|
472
555
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
556
|
+
const handleShowOptions = (event) => {
|
|
557
|
+
setIsShowingOptions(true)
|
|
558
|
+
setAnnouncement(
|
|
559
|
+
`List expanded. ${filteredOptions.length} options available.`
|
|
560
|
+
)
|
|
561
|
+
}
|
|
478
562
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
inputValue: event.type === 'keydown' ? option.label : state.inputValue,
|
|
486
|
-
announcement: option.label
|
|
487
|
-
}))
|
|
488
|
-
}
|
|
563
|
+
const handleHideOptions = (event) => {
|
|
564
|
+
setIsShowingOptions(false)
|
|
565
|
+
setHighlightedOptionId(false)
|
|
566
|
+
setAnnouncement('List collapsed.')
|
|
567
|
+
matchValue()
|
|
568
|
+
}
|
|
489
569
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
this.setState((state) => ({
|
|
494
|
-
selectedOptionId: [...state.selectedOptionId, id],
|
|
495
|
-
highlightedOptionId: null,
|
|
496
|
-
filteredOptions: this.filterOptions(''),
|
|
497
|
-
inputValue: '',
|
|
498
|
-
isShowingOptions: false,
|
|
499
|
-
announcement: `${option.label} selected. List collapsed.`
|
|
500
|
-
}))
|
|
501
|
-
}
|
|
570
|
+
const handleBlur = (event) => {
|
|
571
|
+
setHighlightedOptionId(null)
|
|
572
|
+
}
|
|
502
573
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
announcement: this.getOptionsChangedMessage(newOptions)
|
|
512
|
-
})
|
|
513
|
-
}
|
|
574
|
+
const handleHighlightOption = (event, { id }) => {
|
|
575
|
+
event.persist()
|
|
576
|
+
const option = getOptionById(id)
|
|
577
|
+
if (!option) return // prevent highlighting of empty option
|
|
578
|
+
setHighlightedOptionId(id)
|
|
579
|
+
setInputValue(event.type === 'keydown' ? option.label : inputValue)
|
|
580
|
+
setAnnouncement(option.label)
|
|
581
|
+
}
|
|
514
582
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
selectedOptionId: state.selectedOptionId.slice(0, -1)
|
|
524
|
-
}))
|
|
525
|
-
}
|
|
583
|
+
const handleSelectOption = (event, { id }) => {
|
|
584
|
+
const option = getOptionById(id)
|
|
585
|
+
if (!option) return // prevent selecting of empty option
|
|
586
|
+
setSelectedOptionId(id)
|
|
587
|
+
setInputValue(option.label)
|
|
588
|
+
setIsShowingOptions(false)
|
|
589
|
+
setFilteredOptions(options)
|
|
590
|
+
setAnnouncement(`${option.label} selected. List collapsed.`)
|
|
526
591
|
}
|
|
527
|
-
}
|
|
528
|
-
// remove a selected option tag
|
|
529
|
-
dismissTag (e, tag) {
|
|
530
|
-
// prevent closing of list
|
|
531
|
-
e.stopPropagation()
|
|
532
|
-
e.preventDefault()
|
|
533
|
-
|
|
534
|
-
const newSelection = this.state.selectedOptionId.filter((id) => id !== tag)
|
|
535
|
-
this.setState({
|
|
536
|
-
selectedOptionId: newSelection,
|
|
537
|
-
highlightedOptionId: null,
|
|
538
|
-
announcement: `${this.getOptionById(tag).label} removed`,
|
|
539
|
-
}, () => {
|
|
540
|
-
this.inputRef.focus()
|
|
541
|
-
})
|
|
542
|
-
}
|
|
543
|
-
// render tags when multiple options are selected
|
|
544
|
-
renderTags () {
|
|
545
|
-
const { selectedOptionId } = this.state
|
|
546
|
-
return selectedOptionId.map((id, index) => (
|
|
547
|
-
<Tag
|
|
548
|
-
dismissible
|
|
549
|
-
key={id}
|
|
550
|
-
title={`Remove ${this.getOptionById(id).label}`}
|
|
551
|
-
text={this.getOptionById(id).label}
|
|
552
|
-
margin={index > 0 ? 'xxx-small 0 xxx-small xx-small' : 'xxx-small 0'}
|
|
553
|
-
onClick={(e) => this.dismissTag(e, id)}
|
|
554
|
-
/>
|
|
555
|
-
))
|
|
556
|
-
}
|
|
557
592
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
593
|
+
const handleInputChange = (event) => {
|
|
594
|
+
const value = event.target.value
|
|
595
|
+
const newOptions = filterOptions(value)
|
|
596
|
+
setInputValue(value)
|
|
597
|
+
setFilteredOptions(newOptions)
|
|
598
|
+
setHighlightedOptionId(newOptions.length > 0 ? newOptions[0].id : null)
|
|
599
|
+
setIsShowingOptions(true)
|
|
600
|
+
setSelectedOptionId(value === '' ? null : selectedOptionId)
|
|
601
|
+
setAnnouncement(getOptionsChangedMessage(newOptions))
|
|
602
|
+
}
|
|
567
603
|
|
|
568
604
|
return (
|
|
569
605
|
<div>
|
|
570
606
|
<Select
|
|
571
|
-
renderLabel="
|
|
572
|
-
assistiveText="Type or use arrow keys to navigate options.
|
|
607
|
+
renderLabel="Autocomplete"
|
|
608
|
+
assistiveText="Type or use arrow keys to navigate options."
|
|
609
|
+
placeholder="Start typing to search..."
|
|
573
610
|
inputValue={inputValue}
|
|
574
611
|
isShowingOptions={isShowingOptions}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
renderBeforeInput={selectedOptionId.length > 0 ? this.renderTags() : null}
|
|
612
|
+
onBlur={handleBlur}
|
|
613
|
+
onInputChange={handleInputChange}
|
|
614
|
+
onRequestShowOptions={handleShowOptions}
|
|
615
|
+
onRequestHideOptions={handleHideOptions}
|
|
616
|
+
onRequestHighlightOption={handleHighlightOption}
|
|
617
|
+
onRequestSelectOption={handleSelectOption}
|
|
618
|
+
renderBeforeInput={<IconUserSolid inline={false} />}
|
|
619
|
+
renderAfterInput={<IconSearchLine inline={false} />}
|
|
584
620
|
>
|
|
585
|
-
{filteredOptions.length > 0 ?
|
|
586
|
-
|
|
621
|
+
{filteredOptions.length > 0 ? (
|
|
622
|
+
filteredOptions.map((option) => {
|
|
587
623
|
return (
|
|
588
624
|
<Select.Option
|
|
589
625
|
id={option.id}
|
|
590
626
|
key={option.id}
|
|
591
627
|
isHighlighted={option.id === highlightedOptionId}
|
|
628
|
+
isSelected={option.id === selectedOptionId}
|
|
629
|
+
isDisabled={option.disabled}
|
|
630
|
+
renderBeforeLabel={
|
|
631
|
+
!option.disabled ? IconUserSolid : IconUserLine
|
|
632
|
+
}
|
|
592
633
|
>
|
|
593
|
-
{
|
|
634
|
+
{!option.disabled
|
|
635
|
+
? option.label
|
|
636
|
+
: `${option.label} (unavailable)`}
|
|
594
637
|
</Select.Option>
|
|
595
638
|
)
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
<Select.Option
|
|
599
|
-
id="empty-option"
|
|
600
|
-
key="empty-option"
|
|
601
|
-
>
|
|
639
|
+
})
|
|
640
|
+
) : (
|
|
641
|
+
<Select.Option id="empty-option" key="empty-option">
|
|
602
642
|
---
|
|
603
643
|
</Select.Option>
|
|
604
644
|
)}
|
|
@@ -608,169 +648,805 @@ class MultipleSelectExample extends React.Component {
|
|
|
608
648
|
liveRegionPoliteness="assertive"
|
|
609
649
|
screenReaderOnly
|
|
610
650
|
>
|
|
611
|
-
{
|
|
651
|
+
{announcement}
|
|
612
652
|
</Alert>
|
|
613
653
|
</div>
|
|
614
654
|
)
|
|
615
655
|
}
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
render(
|
|
619
|
-
<View>
|
|
620
|
-
<MultipleSelectExample
|
|
621
|
-
options={[
|
|
622
|
-
{ id: 'opt1', label: 'Alaska' },
|
|
623
|
-
{ id: 'opt2', label: 'American Samoa' },
|
|
624
|
-
{ id: 'opt3', label: 'Arizona' },
|
|
625
|
-
{ id: 'opt4', label: 'Arkansas' },
|
|
626
|
-
{ id: 'opt5', label: 'California' },
|
|
627
|
-
{ id: 'opt6', label: 'Colorado' },
|
|
628
|
-
{ id: 'opt7', label: 'Connecticut' },
|
|
629
|
-
{ id: 'opt8', label: 'Delaware' },
|
|
630
|
-
{ id: 'opt9', label: 'District Of Columbia' },
|
|
631
|
-
{ id: 'opt10', label: 'Federated States Of Micronesia' },
|
|
632
|
-
{ id: 'opt11', label: 'Florida' },
|
|
633
|
-
{ id: 'opt12', label: 'Georgia (unavailable)' },
|
|
634
|
-
{ id: 'opt13', label: 'Guam' },
|
|
635
|
-
{ id: 'opt14', label: 'Hawaii' },
|
|
636
|
-
{ id: 'opt15', label: 'Idaho' },
|
|
637
|
-
{ id: 'opt16', label: 'Illinois' }
|
|
638
|
-
]}
|
|
639
|
-
/>
|
|
640
|
-
</View>
|
|
641
|
-
)
|
|
642
|
-
```
|
|
643
656
|
|
|
644
|
-
|
|
657
|
+
render(
|
|
658
|
+
<View>
|
|
659
|
+
<AutocompleteExample
|
|
660
|
+
options={[
|
|
661
|
+
{ id: 'opt0', label: 'Aaron Aaronson' },
|
|
662
|
+
{ id: 'opt1', label: 'Amber Murphy' },
|
|
663
|
+
{ id: 'opt2', label: 'Andrew Miller' },
|
|
664
|
+
{ id: 'opt3', label: 'Barbara Ward' },
|
|
665
|
+
{ id: 'opt4', label: 'Byron Cranston', disabled: true },
|
|
666
|
+
{ id: 'opt5', label: 'Dennis Reynolds' },
|
|
667
|
+
{ id: 'opt6', label: 'Dee Reynolds' },
|
|
668
|
+
{ id: 'opt7', label: 'Ezra Betterthan' },
|
|
669
|
+
{ id: 'opt8', label: 'Jeff Spicoli' },
|
|
670
|
+
{ id: 'opt9', label: 'Joseph Smith' },
|
|
671
|
+
{ id: 'opt10', label: 'Jasmine Diaz' },
|
|
672
|
+
{ id: 'opt11', label: 'Martin Harris' },
|
|
673
|
+
{ id: 'opt12', label: 'Michael Morgan', disabled: true },
|
|
674
|
+
{ id: 'opt13', label: 'Michelle Rodriguez' },
|
|
675
|
+
{ id: 'opt14', label: 'Ziggy Stardust' }
|
|
676
|
+
]}
|
|
677
|
+
/>
|
|
678
|
+
</View>
|
|
679
|
+
)
|
|
680
|
+
```
|
|
645
681
|
|
|
646
|
-
|
|
682
|
+
#### Highlighting and selecting options
|
|
647
683
|
|
|
648
|
-
|
|
649
|
-
---
|
|
650
|
-
type: example
|
|
651
|
-
---
|
|
684
|
+
To mark an option as "highlighted", use the option's `isHighlighted` prop. Note that only one highlighted option is permitted. Similarly, use `isSelected` to mark an option or multiple options as "selected". When allowing multiple selections, it's best to render a [Tag](#Tag) for each selected option via the `renderBeforeInput` prop.
|
|
652
685
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
686
|
+
- ```javascript
|
|
687
|
+
class MultipleSelectExample extends React.Component {
|
|
688
|
+
state = {
|
|
689
|
+
inputValue: '',
|
|
690
|
+
isShowingOptions: false,
|
|
691
|
+
highlightedOptionId: null,
|
|
692
|
+
selectedOptionId: ['opt1', 'opt6'],
|
|
693
|
+
filteredOptions: this.props.options,
|
|
694
|
+
announcement: null
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
getOptionById(queryId) {
|
|
698
|
+
return this.props.options.find(({ id }) => id === queryId)
|
|
699
|
+
}
|
|
661
700
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
//
|
|
671
|
-
|
|
672
|
-
|
|
701
|
+
getOptionsChangedMessage(newOptions) {
|
|
702
|
+
let message =
|
|
703
|
+
newOptions.length !== this.state.filteredOptions.length
|
|
704
|
+
? `${newOptions.length} options available.` // options changed, announce new total
|
|
705
|
+
: null // options haven't changed, don't announce
|
|
706
|
+
if (message && newOptions.length > 0) {
|
|
707
|
+
// options still available
|
|
708
|
+
if (this.state.highlightedOptionId !== newOptions[0].id) {
|
|
709
|
+
// highlighted option hasn't been announced
|
|
710
|
+
const option = this.getOptionById(newOptions[0].id).label
|
|
711
|
+
message = `${option}. ${message}`
|
|
673
712
|
}
|
|
674
713
|
}
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
getGroupChangedMessage (newOption) {
|
|
680
|
-
const currentOption = this.getOptionById(this.state.highlightedOptionId)
|
|
681
|
-
const isNewGroup = !currentOption || currentOption.group !== newOption.group
|
|
682
|
-
let message = isNewGroup ? `Group ${newOption.group} entered. ` : ''
|
|
683
|
-
message += newOption.label
|
|
684
|
-
return message
|
|
685
|
-
}
|
|
714
|
+
return message
|
|
715
|
+
}
|
|
686
716
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
}
|
|
692
|
-
}
|
|
717
|
+
filterOptions = (value) => {
|
|
718
|
+
return this.props.options.filter((option) =>
|
|
719
|
+
option.label.toLowerCase().startsWith(value.toLowerCase())
|
|
720
|
+
)
|
|
721
|
+
}
|
|
693
722
|
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
723
|
+
matchValue() {
|
|
724
|
+
const {
|
|
725
|
+
filteredOptions,
|
|
726
|
+
inputValue,
|
|
727
|
+
highlightedOptionId,
|
|
728
|
+
selectedOptionId
|
|
729
|
+
} = this.state
|
|
730
|
+
|
|
731
|
+
// an option matching user input exists
|
|
732
|
+
if (filteredOptions.length === 1) {
|
|
733
|
+
const onlyOption = filteredOptions[0]
|
|
734
|
+
// automatically select the matching option
|
|
735
|
+
if (onlyOption.label.toLowerCase() === inputValue.toLowerCase()) {
|
|
736
|
+
return {
|
|
737
|
+
inputValue: '',
|
|
738
|
+
selectedOptionId: [...selectedOptionId, onlyOption.id],
|
|
739
|
+
filteredOptions: this.filterOptions('')
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
// input value is from highlighted option, not user input
|
|
744
|
+
// clear input, reset options
|
|
745
|
+
if (highlightedOptionId) {
|
|
746
|
+
if (inputValue === this.getOptionById(highlightedOptionId).label) {
|
|
747
|
+
return {
|
|
748
|
+
inputValue: '',
|
|
749
|
+
filteredOptions: this.filterOptions('')
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
702
754
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
})
|
|
707
|
-
}
|
|
755
|
+
handleShowOptions = (event) => {
|
|
756
|
+
this.setState({ isShowingOptions: true })
|
|
757
|
+
}
|
|
708
758
|
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
announcement: this.getGroupChangedMessage(newOption)
|
|
716
|
-
}))
|
|
717
|
-
}
|
|
759
|
+
handleHideOptions = (event) => {
|
|
760
|
+
this.setState({
|
|
761
|
+
isShowingOptions: false,
|
|
762
|
+
...this.matchValue()
|
|
763
|
+
})
|
|
764
|
+
}
|
|
718
765
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
announcement: `${this.getOptionById(id).label} selected.`
|
|
725
|
-
})
|
|
726
|
-
}
|
|
766
|
+
handleBlur = (event) => {
|
|
767
|
+
this.setState({
|
|
768
|
+
highlightedOptionId: null
|
|
769
|
+
})
|
|
770
|
+
}
|
|
727
771
|
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
</span>
|
|
739
|
-
)
|
|
740
|
-
}
|
|
772
|
+
handleHighlightOption = (event, { id }) => {
|
|
773
|
+
event.persist()
|
|
774
|
+
const option = this.getOptionById(id)
|
|
775
|
+
if (!option) return // prevent highlighting empty option
|
|
776
|
+
this.setState((state) => ({
|
|
777
|
+
highlightedOptionId: id,
|
|
778
|
+
inputValue: event.type === 'keydown' ? option.label : state.inputValue,
|
|
779
|
+
announcement: option.label
|
|
780
|
+
}))
|
|
781
|
+
}
|
|
741
782
|
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
783
|
+
handleSelectOption = (event, { id }) => {
|
|
784
|
+
const option = this.getOptionById(id)
|
|
785
|
+
if (!option) return // prevent selecting of empty option
|
|
786
|
+
this.setState((state) => ({
|
|
787
|
+
selectedOptionId: [...state.selectedOptionId, id],
|
|
788
|
+
highlightedOptionId: null,
|
|
789
|
+
filteredOptions: this.filterOptions(''),
|
|
790
|
+
inputValue: '',
|
|
791
|
+
isShowingOptions: false,
|
|
792
|
+
announcement: `${option.label} selected. List collapsed.`
|
|
793
|
+
}))
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
handleInputChange = (event) => {
|
|
797
|
+
const value = event.target.value
|
|
798
|
+
const newOptions = this.filterOptions(value)
|
|
799
|
+
this.setState({
|
|
800
|
+
inputValue: value,
|
|
801
|
+
filteredOptions: newOptions,
|
|
802
|
+
highlightedOptionId: newOptions.length > 0 ? newOptions[0].id : null,
|
|
803
|
+
isShowingOptions: true,
|
|
804
|
+
announcement: this.getOptionsChangedMessage(newOptions)
|
|
805
|
+
})
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
handleKeyDown = (event) => {
|
|
809
|
+
const { selectedOptionId, inputValue } = this.state
|
|
810
|
+
if (event.keyCode === 8) {
|
|
811
|
+
// when backspace key is pressed
|
|
812
|
+
if (inputValue === '' && selectedOptionId.length > 0) {
|
|
813
|
+
// remove last selected option, if input has no entered text
|
|
814
|
+
this.setState((state) => ({
|
|
815
|
+
highlightedOptionId: null,
|
|
816
|
+
selectedOptionId: state.selectedOptionId.slice(0, -1)
|
|
817
|
+
}))
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
// remove a selected option tag
|
|
822
|
+
dismissTag(e, tag) {
|
|
823
|
+
// prevent closing of list
|
|
824
|
+
e.stopPropagation()
|
|
825
|
+
e.preventDefault()
|
|
826
|
+
|
|
827
|
+
const newSelection = this.state.selectedOptionId.filter(
|
|
828
|
+
(id) => id !== tag
|
|
829
|
+
)
|
|
830
|
+
this.setState(
|
|
831
|
+
{
|
|
832
|
+
selectedOptionId: newSelection,
|
|
833
|
+
highlightedOptionId: null,
|
|
834
|
+
announcement: `${this.getOptionById(tag).label} removed`
|
|
835
|
+
},
|
|
836
|
+
() => {
|
|
837
|
+
this.inputRef.focus()
|
|
838
|
+
}
|
|
839
|
+
)
|
|
840
|
+
}
|
|
841
|
+
// render tags when multiple options are selected
|
|
842
|
+
renderTags() {
|
|
843
|
+
const { selectedOptionId } = this.state
|
|
844
|
+
return selectedOptionId.map((id, index) => (
|
|
845
|
+
<Tag
|
|
846
|
+
dismissible
|
|
847
|
+
key={id}
|
|
848
|
+
title={`Remove ${this.getOptionById(id).label}`}
|
|
849
|
+
text={this.getOptionById(id).label}
|
|
850
|
+
margin={index > 0 ? 'xxx-small 0 xxx-small xx-small' : 'xxx-small 0'}
|
|
851
|
+
onClick={(e) => this.dismissTag(e, id)}
|
|
852
|
+
/>
|
|
853
|
+
))
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
render() {
|
|
857
|
+
const {
|
|
858
|
+
inputValue,
|
|
859
|
+
isShowingOptions,
|
|
860
|
+
highlightedOptionId,
|
|
861
|
+
selectedOptionId,
|
|
862
|
+
filteredOptions,
|
|
863
|
+
announcement
|
|
864
|
+
} = this.state
|
|
745
865
|
|
|
746
|
-
return Object.keys(options).map((key, index) => {
|
|
747
|
-
const badgeVariant = key === 'Eastern' ? 'success' : 'primary'
|
|
748
866
|
return (
|
|
749
|
-
<
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
867
|
+
<div>
|
|
868
|
+
<Select
|
|
869
|
+
renderLabel="Multiple Select"
|
|
870
|
+
assistiveText="Type or use arrow keys to navigate options. Multiple selections allowed."
|
|
871
|
+
inputValue={inputValue}
|
|
872
|
+
isShowingOptions={isShowingOptions}
|
|
873
|
+
inputRef={(el) => (this.inputRef = el)}
|
|
874
|
+
onBlur={this.handleBlur}
|
|
875
|
+
onInputChange={this.handleInputChange}
|
|
876
|
+
onRequestShowOptions={this.handleShowOptions}
|
|
877
|
+
onRequestHideOptions={this.handleHideOptions}
|
|
878
|
+
onRequestHighlightOption={this.handleHighlightOption}
|
|
879
|
+
onRequestSelectOption={this.handleSelectOption}
|
|
880
|
+
onKeyDown={this.handleKeyDown}
|
|
881
|
+
renderBeforeInput={
|
|
882
|
+
selectedOptionId.length > 0 ? this.renderTags() : null
|
|
883
|
+
}
|
|
884
|
+
>
|
|
885
|
+
{filteredOptions.length > 0 ? (
|
|
886
|
+
filteredOptions.map((option, index) => {
|
|
887
|
+
if (selectedOptionId.indexOf(option.id) === -1) {
|
|
888
|
+
return (
|
|
889
|
+
<Select.Option
|
|
890
|
+
id={option.id}
|
|
891
|
+
key={option.id}
|
|
892
|
+
isHighlighted={option.id === highlightedOptionId}
|
|
893
|
+
>
|
|
894
|
+
{option.label}
|
|
895
|
+
</Select.Option>
|
|
896
|
+
)
|
|
897
|
+
}
|
|
898
|
+
})
|
|
899
|
+
) : (
|
|
900
|
+
<Select.Option id="empty-option" key="empty-option">
|
|
901
|
+
---
|
|
902
|
+
</Select.Option>
|
|
903
|
+
)}
|
|
904
|
+
</Select>
|
|
905
|
+
<Alert
|
|
906
|
+
liveRegion={() => document.getElementById('flash-messages')}
|
|
907
|
+
liveRegionPoliteness="assertive"
|
|
908
|
+
screenReaderOnly
|
|
909
|
+
>
|
|
910
|
+
{announcement}
|
|
911
|
+
</Alert>
|
|
912
|
+
</div>
|
|
913
|
+
)
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
render(
|
|
918
|
+
<View>
|
|
919
|
+
<MultipleSelectExample
|
|
920
|
+
options={[
|
|
921
|
+
{ id: 'opt1', label: 'Alaska' },
|
|
922
|
+
{ id: 'opt2', label: 'American Samoa' },
|
|
923
|
+
{ id: 'opt3', label: 'Arizona' },
|
|
924
|
+
{ id: 'opt4', label: 'Arkansas' },
|
|
925
|
+
{ id: 'opt5', label: 'California' },
|
|
926
|
+
{ id: 'opt6', label: 'Colorado' },
|
|
927
|
+
{ id: 'opt7', label: 'Connecticut' },
|
|
928
|
+
{ id: 'opt8', label: 'Delaware' },
|
|
929
|
+
{ id: 'opt9', label: 'District Of Columbia' },
|
|
930
|
+
{ id: 'opt10', label: 'Federated States Of Micronesia' },
|
|
931
|
+
{ id: 'opt11', label: 'Florida' },
|
|
932
|
+
{ id: 'opt12', label: 'Georgia (unavailable)' },
|
|
933
|
+
{ id: 'opt13', label: 'Guam' },
|
|
934
|
+
{ id: 'opt14', label: 'Hawaii' },
|
|
935
|
+
{ id: 'opt15', label: 'Idaho' },
|
|
936
|
+
{ id: 'opt16', label: 'Illinois' }
|
|
937
|
+
]}
|
|
938
|
+
/>
|
|
939
|
+
</View>
|
|
940
|
+
)
|
|
941
|
+
```
|
|
942
|
+
|
|
943
|
+
- ```js
|
|
944
|
+
const MultipleSelectExample = ({ options }) => {
|
|
945
|
+
const [inputValue, setInputValue] = useState('')
|
|
946
|
+
const [isShowingOptions, setIsShowingOptions] = useState(false)
|
|
947
|
+
const [highlightedOptionId, setHighlightedOptionId] = useState(null)
|
|
948
|
+
const [selectedOptionId, setSelectedOptionId] = useState(['opt1', 'opt6'])
|
|
949
|
+
const [filteredOptions, setFilteredOptions] = useState(options)
|
|
950
|
+
const [announcement, setAnnouncement] = useState(null)
|
|
951
|
+
const inputRef = useRef(null)
|
|
952
|
+
|
|
953
|
+
const getOptionById = (queryId) => {
|
|
954
|
+
return options.find(({ id }) => id === queryId)
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
const getOptionsChangedMessage = (newOptions) => {
|
|
958
|
+
let message =
|
|
959
|
+
newOptions.length !== filteredOptions.length
|
|
960
|
+
? `${newOptions.length} options available.` // options changed, announce new total
|
|
961
|
+
: null // options haven't changed, don't announce
|
|
962
|
+
if (message && newOptions.length > 0) {
|
|
963
|
+
// options still available
|
|
964
|
+
if (highlightedOptionId !== newOptions[0].id) {
|
|
965
|
+
// highlighted option hasn't been announced
|
|
966
|
+
const option = getOptionById(newOptions[0].id).label
|
|
967
|
+
message = `${option}. ${message}`
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
return message
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
const filterOptions = (value) => {
|
|
974
|
+
return options.filter((option) =>
|
|
975
|
+
option.label.toLowerCase().startsWith(value.toLowerCase())
|
|
976
|
+
)
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
const matchValue = () => {
|
|
980
|
+
// an option matching user input exists
|
|
981
|
+
if (filteredOptions.length === 1) {
|
|
982
|
+
const onlyOption = filteredOptions[0]
|
|
983
|
+
// automatically select the matching option
|
|
984
|
+
if (onlyOption.label.toLowerCase() === inputValue.toLowerCase()) {
|
|
985
|
+
setInputValue('')
|
|
986
|
+
setSelectedOptionId([...selectedOptionId, onlyOption.id])
|
|
987
|
+
setFilteredOptions(filterOptions(''))
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
// input value is from highlighted option, not user input
|
|
991
|
+
// clear input, reset options
|
|
992
|
+
else if (highlightedOptionId) {
|
|
993
|
+
if (inputValue === getOptionById(highlightedOptionId).label) {
|
|
994
|
+
setInputValue('')
|
|
995
|
+
setFilteredOptions(filterOptions(''))
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
const handleShowOptions = (event) => {
|
|
1001
|
+
setIsShowingOptions(true)
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
const handleHideOptions = (event) => {
|
|
1005
|
+
setIsShowingOptions(false)
|
|
1006
|
+
matchValue()
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
const handleBlur = (event) => {
|
|
1010
|
+
setHighlightedOptionId(null)
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
const handleHighlightOption = (event, { id }) => {
|
|
1014
|
+
event.persist()
|
|
1015
|
+
const option = getOptionById(id)
|
|
1016
|
+
if (!option) return // prevent highlighting empty option
|
|
1017
|
+
setHighlightedOptionId(id)
|
|
1018
|
+
setInputValue(event.type === 'keydown' ? option.label : inputValue)
|
|
1019
|
+
setAnnouncement(option.label)
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
const handleSelectOption = (event, { id }) => {
|
|
1023
|
+
const option = getOptionById(id)
|
|
1024
|
+
if (!option) return // prevent selecting of empty option
|
|
1025
|
+
setSelectedOptionId([...selectedOptionId, id])
|
|
1026
|
+
setHighlightedOptionId(null)
|
|
1027
|
+
setFilteredOptions(filterOptions(''))
|
|
1028
|
+
setInputValue('')
|
|
1029
|
+
setIsShowingOptions(false)
|
|
1030
|
+
setAnnouncement(`${option.label} selected. List collapsed.`)
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
const handleInputChange = (event) => {
|
|
1034
|
+
const value = event.target.value
|
|
1035
|
+
const newOptions = filterOptions(value)
|
|
1036
|
+
setInputValue(value)
|
|
1037
|
+
setFilteredOptions(newOptions)
|
|
1038
|
+
sethHighlightedOptionId(newOptions.length > 0 ? newOptions[0].id : null)
|
|
1039
|
+
setIsShowingOptions(true)
|
|
1040
|
+
setAnnouncement(getOptionsChangedMessage(newOptions))
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
const handleKeyDown = (event) => {
|
|
1044
|
+
if (event.keyCode === 8) {
|
|
1045
|
+
// when backspace key is pressed
|
|
1046
|
+
if (inputValue === '' && selectedOptionId.length > 0) {
|
|
1047
|
+
// remove last selected option, if input has no entered text
|
|
1048
|
+
setHighlightedOptionId(null)
|
|
1049
|
+
setSelectedOptionId(selectedOptionId.slice(0, -1))
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// remove a selected option tag
|
|
1055
|
+
const dismissTag = (e, tag) => {
|
|
1056
|
+
// prevent closing of list
|
|
1057
|
+
e.stopPropagation()
|
|
1058
|
+
e.preventDefault()
|
|
1059
|
+
|
|
1060
|
+
const newSelection = selectedOptionId.filter((id) => id !== tag)
|
|
1061
|
+
|
|
1062
|
+
setSelectedOptionId(newSelection)
|
|
1063
|
+
setHighlightedOptionId(null)
|
|
1064
|
+
setAnnouncement(`${getOptionById(tag).label} removed`)
|
|
1065
|
+
|
|
1066
|
+
inputRef.current.focus()
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
const renderTags = () => {
|
|
1070
|
+
return selectedOptionId.map((id, index) => (
|
|
1071
|
+
<Tag
|
|
1072
|
+
dismissible
|
|
1073
|
+
key={id}
|
|
1074
|
+
title={`Remove ${getOptionById(id).label}`}
|
|
1075
|
+
text={getOptionById(id).label}
|
|
1076
|
+
margin={index > 0 ? 'xxx-small 0 xxx-small xx-small' : 'xxx-small 0'}
|
|
1077
|
+
onClick={(e) => dismissTag(e, id)}
|
|
1078
|
+
/>
|
|
1079
|
+
))
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
return (
|
|
1083
|
+
<div>
|
|
1084
|
+
<Select
|
|
1085
|
+
renderLabel="Multiple Select"
|
|
1086
|
+
assistiveText="Type or use arrow keys to navigate options. Multiple selections allowed."
|
|
1087
|
+
inputValue={inputValue}
|
|
1088
|
+
isShowingOptions={isShowingOptions}
|
|
1089
|
+
inputRef={(el) => (inputRef.current = el)}
|
|
1090
|
+
onBlur={handleBlur}
|
|
1091
|
+
onInputChange={handleInputChange}
|
|
1092
|
+
onRequestShowOptions={handleShowOptions}
|
|
1093
|
+
onRequestHideOptions={handleHideOptions}
|
|
1094
|
+
onRequestHighlightOption={handleHighlightOption}
|
|
1095
|
+
onRequestSelectOption={handleSelectOption}
|
|
1096
|
+
onKeyDown={handleKeyDown}
|
|
1097
|
+
renderBeforeInput={selectedOptionId.length > 0 ? renderTags() : null}
|
|
1098
|
+
>
|
|
1099
|
+
{filteredOptions.length > 0 ? (
|
|
1100
|
+
filteredOptions.map((option, index) => {
|
|
1101
|
+
if (selectedOptionId.indexOf(option.id) === -1) {
|
|
1102
|
+
return (
|
|
1103
|
+
<Select.Option
|
|
1104
|
+
id={option.id}
|
|
1105
|
+
key={option.id}
|
|
1106
|
+
isHighlighted={option.id === highlightedOptionId}
|
|
1107
|
+
>
|
|
1108
|
+
{option.label}
|
|
1109
|
+
</Select.Option>
|
|
1110
|
+
)
|
|
1111
|
+
}
|
|
1112
|
+
})
|
|
1113
|
+
) : (
|
|
1114
|
+
<Select.Option id="empty-option" key="empty-option">
|
|
1115
|
+
---
|
|
758
1116
|
</Select.Option>
|
|
759
|
-
)
|
|
760
|
-
</Select
|
|
1117
|
+
)}
|
|
1118
|
+
</Select>
|
|
1119
|
+
<Alert
|
|
1120
|
+
liveRegion={() => document.getElementById('flash-messages')}
|
|
1121
|
+
liveRegionPoliteness="assertive"
|
|
1122
|
+
screenReaderOnly
|
|
1123
|
+
>
|
|
1124
|
+
{announcement}
|
|
1125
|
+
</Alert>
|
|
1126
|
+
</div>
|
|
1127
|
+
)
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
render(
|
|
1131
|
+
<View>
|
|
1132
|
+
<MultipleSelectExample
|
|
1133
|
+
options={[
|
|
1134
|
+
{ id: 'opt1', label: 'Alaska' },
|
|
1135
|
+
{ id: 'opt2', label: 'American Samoa' },
|
|
1136
|
+
{ id: 'opt3', label: 'Arizona' },
|
|
1137
|
+
{ id: 'opt4', label: 'Arkansas' },
|
|
1138
|
+
{ id: 'opt5', label: 'California' },
|
|
1139
|
+
{ id: 'opt6', label: 'Colorado' },
|
|
1140
|
+
{ id: 'opt7', label: 'Connecticut' },
|
|
1141
|
+
{ id: 'opt8', label: 'Delaware' },
|
|
1142
|
+
{ id: 'opt9', label: 'District Of Columbia' },
|
|
1143
|
+
{ id: 'opt10', label: 'Federated States Of Micronesia' },
|
|
1144
|
+
{ id: 'opt11', label: 'Florida' },
|
|
1145
|
+
{ id: 'opt12', label: 'Georgia (unavailable)' },
|
|
1146
|
+
{ id: 'opt13', label: 'Guam' },
|
|
1147
|
+
{ id: 'opt14', label: 'Hawaii' },
|
|
1148
|
+
{ id: 'opt15', label: 'Idaho' },
|
|
1149
|
+
{ id: 'opt16', label: 'Illinois' }
|
|
1150
|
+
]}
|
|
1151
|
+
/>
|
|
1152
|
+
</View>
|
|
1153
|
+
)
|
|
1154
|
+
```
|
|
1155
|
+
|
|
1156
|
+
#### Composing option groups
|
|
1157
|
+
|
|
1158
|
+
In addition to `<Select.Option />` Select also accepts `<Select.Group />` as children. This is meant to serve the same purpose as `<optgroup>` elements. Group only requires you provide a label via its `renderLabel` prop. Groups and their associated options also accept icons or other stylistic additions if needed.
|
|
1159
|
+
|
|
1160
|
+
- ```javascript
|
|
1161
|
+
class GroupSelectExample extends React.Component {
|
|
1162
|
+
state = {
|
|
1163
|
+
inputValue: this.props.options['Western'][0].label,
|
|
1164
|
+
isShowingOptions: false,
|
|
1165
|
+
highlightedOptionId: null,
|
|
1166
|
+
selectedOptionId: this.props.options['Western'][0].id,
|
|
1167
|
+
announcement: null
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
getOptionById(id) {
|
|
1171
|
+
const { options } = this.props
|
|
1172
|
+
let match = null
|
|
1173
|
+
Object.keys(options).forEach((key, index) => {
|
|
1174
|
+
for (let i = 0; i < options[key].length; i++) {
|
|
1175
|
+
const option = options[key][i]
|
|
1176
|
+
if (id === option.id) {
|
|
1177
|
+
// return group property with the object just to make it easier
|
|
1178
|
+
// to check which group the option belongs to
|
|
1179
|
+
match = { ...option, group: key }
|
|
1180
|
+
break
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
})
|
|
1184
|
+
return match
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
getGroupChangedMessage(newOption) {
|
|
1188
|
+
const currentOption = this.getOptionById(this.state.highlightedOptionId)
|
|
1189
|
+
const isNewGroup =
|
|
1190
|
+
!currentOption || currentOption.group !== newOption.group
|
|
1191
|
+
let message = isNewGroup ? `Group ${newOption.group} entered. ` : ''
|
|
1192
|
+
message += newOption.label
|
|
1193
|
+
return message
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
handleShowOptions = (event) => {
|
|
1197
|
+
this.setState({
|
|
1198
|
+
isShowingOptions: true,
|
|
1199
|
+
highlightedOptionId: null
|
|
1200
|
+
})
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
handleHideOptions = (event) => {
|
|
1204
|
+
const { selectedOptionId } = this.state
|
|
1205
|
+
this.setState({
|
|
1206
|
+
isShowingOptions: false,
|
|
1207
|
+
highlightedOptionId: null,
|
|
1208
|
+
inputValue: this.getOptionById(selectedOptionId).label
|
|
1209
|
+
})
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
handleBlur = (event) => {
|
|
1213
|
+
this.setState({
|
|
1214
|
+
highlightedOptionId: null
|
|
1215
|
+
})
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
handleHighlightOption = (event, { id }) => {
|
|
1219
|
+
event.persist()
|
|
1220
|
+
const newOption = this.getOptionById(id)
|
|
1221
|
+
this.setState((state) => ({
|
|
1222
|
+
highlightedOptionId: id,
|
|
1223
|
+
inputValue:
|
|
1224
|
+
event.type === 'keydown' ? newOption.label : state.inputValue,
|
|
1225
|
+
announcement: this.getGroupChangedMessage(newOption)
|
|
1226
|
+
}))
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
handleSelectOption = (event, { id }) => {
|
|
1230
|
+
this.setState({
|
|
1231
|
+
selectedOptionId: id,
|
|
1232
|
+
inputValue: this.getOptionById(id).label,
|
|
1233
|
+
isShowingOptions: false,
|
|
1234
|
+
announcement: `${this.getOptionById(id).label} selected.`
|
|
1235
|
+
})
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
renderLabel(text, variant) {
|
|
1239
|
+
return (
|
|
1240
|
+
<span>
|
|
1241
|
+
<Badge
|
|
1242
|
+
type="notification"
|
|
1243
|
+
variant={variant}
|
|
1244
|
+
standalone
|
|
1245
|
+
margin="0 x-small xxx-small 0"
|
|
1246
|
+
/>
|
|
1247
|
+
{text}
|
|
1248
|
+
</span>
|
|
761
1249
|
)
|
|
762
|
-
}
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
renderGroup() {
|
|
1253
|
+
const { options } = this.props
|
|
1254
|
+
const { highlightedOptionId, selectedOptionId } = this.state
|
|
1255
|
+
|
|
1256
|
+
return Object.keys(options).map((key, index) => {
|
|
1257
|
+
const badgeVariant = key === 'Eastern' ? 'success' : 'primary'
|
|
1258
|
+
return (
|
|
1259
|
+
<Select.Group
|
|
1260
|
+
key={index}
|
|
1261
|
+
renderLabel={this.renderLabel(key, badgeVariant)}
|
|
1262
|
+
>
|
|
1263
|
+
{options[key].map((option) => (
|
|
1264
|
+
<Select.Option
|
|
1265
|
+
key={option.id}
|
|
1266
|
+
id={option.id}
|
|
1267
|
+
isHighlighted={option.id === highlightedOptionId}
|
|
1268
|
+
isSelected={option.id === selectedOptionId}
|
|
1269
|
+
>
|
|
1270
|
+
{option.label}
|
|
1271
|
+
</Select.Option>
|
|
1272
|
+
))}
|
|
1273
|
+
</Select.Group>
|
|
1274
|
+
)
|
|
1275
|
+
})
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
render() {
|
|
1279
|
+
const {
|
|
1280
|
+
inputValue,
|
|
1281
|
+
isShowingOptions,
|
|
1282
|
+
highlightedOptionId,
|
|
1283
|
+
selectedOptionId,
|
|
1284
|
+
filteredOptions,
|
|
1285
|
+
announcement
|
|
1286
|
+
} = this.state
|
|
1287
|
+
|
|
1288
|
+
return (
|
|
1289
|
+
<div>
|
|
1290
|
+
<Select
|
|
1291
|
+
renderLabel="Group Select"
|
|
1292
|
+
assistiveText="Type or use arrow keys to navigate options."
|
|
1293
|
+
inputValue={inputValue}
|
|
1294
|
+
isShowingOptions={isShowingOptions}
|
|
1295
|
+
onBlur={this.handleBlur}
|
|
1296
|
+
onRequestShowOptions={this.handleShowOptions}
|
|
1297
|
+
onRequestHideOptions={this.handleHideOptions}
|
|
1298
|
+
onRequestHighlightOption={this.handleHighlightOption}
|
|
1299
|
+
onRequestSelectOption={this.handleSelectOption}
|
|
1300
|
+
renderBeforeInput={
|
|
1301
|
+
<Badge
|
|
1302
|
+
type="notification"
|
|
1303
|
+
variant={
|
|
1304
|
+
this.getOptionById(selectedOptionId).group === 'Eastern'
|
|
1305
|
+
? 'success'
|
|
1306
|
+
: 'primary'
|
|
1307
|
+
}
|
|
1308
|
+
standalone
|
|
1309
|
+
margin="0 0 xxx-small 0"
|
|
1310
|
+
/>
|
|
1311
|
+
}
|
|
1312
|
+
>
|
|
1313
|
+
{this.renderGroup()}
|
|
1314
|
+
</Select>
|
|
1315
|
+
<Alert
|
|
1316
|
+
liveRegion={() => document.getElementById('flash-messages')}
|
|
1317
|
+
liveRegionPoliteness="assertive"
|
|
1318
|
+
screenReaderOnly
|
|
1319
|
+
>
|
|
1320
|
+
{announcement}
|
|
1321
|
+
</Alert>
|
|
1322
|
+
</div>
|
|
1323
|
+
)
|
|
1324
|
+
}
|
|
763
1325
|
}
|
|
764
1326
|
|
|
765
|
-
render
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
1327
|
+
render(
|
|
1328
|
+
<View>
|
|
1329
|
+
<GroupSelectExample
|
|
1330
|
+
options={{
|
|
1331
|
+
Western: [
|
|
1332
|
+
{ id: 'opt5', label: 'Alaska' },
|
|
1333
|
+
{ id: 'opt6', label: 'California' },
|
|
1334
|
+
{ id: 'opt7', label: 'Colorado' },
|
|
1335
|
+
{ id: 'opt8', label: 'Idaho' }
|
|
1336
|
+
],
|
|
1337
|
+
Eastern: [
|
|
1338
|
+
{ id: 'opt1', label: 'Alabama' },
|
|
1339
|
+
{ id: 'opt2', label: 'Connecticut' },
|
|
1340
|
+
{ id: 'opt3', label: 'Delaware' },
|
|
1341
|
+
{ id: '4', label: 'Illinois' }
|
|
1342
|
+
]
|
|
1343
|
+
}}
|
|
1344
|
+
/>
|
|
1345
|
+
</View>
|
|
1346
|
+
)
|
|
1347
|
+
```
|
|
1348
|
+
|
|
1349
|
+
- ```js
|
|
1350
|
+
const GroupSelectExample = ({ options }) => {
|
|
1351
|
+
const [inputValue, setInputValue] = useState(options['Western'][0].label)
|
|
1352
|
+
const [isShowingOptions, setIsShowingOptions] = useState(false)
|
|
1353
|
+
const [highlightedOptionId, setHighlightedOptionId] = useState(null)
|
|
1354
|
+
const [selectedOptionId, setSelectedOptionId] = useState(
|
|
1355
|
+
options['Western'][0].id
|
|
1356
|
+
)
|
|
1357
|
+
const [announcement, setAnnouncement] = useState(null)
|
|
1358
|
+
|
|
1359
|
+
const getOptionById = (id) => {
|
|
1360
|
+
let match = null
|
|
1361
|
+
Object.keys(options).forEach((key, index) => {
|
|
1362
|
+
for (let i = 0; i < options[key].length; i++) {
|
|
1363
|
+
const option = options[key][i]
|
|
1364
|
+
if (id === option.id) {
|
|
1365
|
+
// return group property with the object just to make it easier
|
|
1366
|
+
// to check which group the option belongs to
|
|
1367
|
+
match = { ...option, group: key }
|
|
1368
|
+
break
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
})
|
|
1372
|
+
return match
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
const getGroupChangedMessage = (newOption) => {
|
|
1376
|
+
const currentOption = getOptionById(highlightedOptionId)
|
|
1377
|
+
const isNewGroup =
|
|
1378
|
+
!currentOption || currentOption.group !== newOption.group
|
|
1379
|
+
let message = isNewGroup ? `Group ${newOption.group} entered. ` : ''
|
|
1380
|
+
message += newOption.label
|
|
1381
|
+
return message
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
const handleShowOptions = (event) => {
|
|
1385
|
+
setIsShowingOptions(true)
|
|
1386
|
+
setHighlightedOptionId(null)
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
const handleHideOptions = (event) => {
|
|
1390
|
+
setIsShowingOptions(false)
|
|
1391
|
+
setHighlightedOptionId(null)
|
|
1392
|
+
setInputValue(getOptionById(selectedOptionId).label)
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
const handleBlur = (event) => {
|
|
1396
|
+
setHighlightedOptionId(null)
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
const handleHighlightOption = (event, { id }) => {
|
|
1400
|
+
event.persist()
|
|
1401
|
+
const newOption = getOptionById(id)
|
|
1402
|
+
setHighlightedOptionId(id)
|
|
1403
|
+
setInputValue(event.type === 'keydown' ? newOption.label : inputValue)
|
|
1404
|
+
setAnnouncement(getGroupChangedMessage(newOption))
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
const handleSelectOption = (event, { id }) => {
|
|
1408
|
+
setSelectedOptionId(id)
|
|
1409
|
+
setInputValue(getOptionById(id).label)
|
|
1410
|
+
setIsShowingOptions(false)
|
|
1411
|
+
setAnnouncement(`${getOptionById(id).label} selected.`)
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
const renderLabel = (text, variant) => {
|
|
1415
|
+
return (
|
|
1416
|
+
<span>
|
|
1417
|
+
<Badge
|
|
1418
|
+
type="notification"
|
|
1419
|
+
variant={variant}
|
|
1420
|
+
standalone
|
|
1421
|
+
margin="0 x-small xxx-small 0"
|
|
1422
|
+
/>
|
|
1423
|
+
{text}
|
|
1424
|
+
</span>
|
|
1425
|
+
)
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
const renderGroup = () => {
|
|
1429
|
+
return Object.keys(options).map((key, index) => {
|
|
1430
|
+
const badgeVariant = key === 'Eastern' ? 'success' : 'primary'
|
|
1431
|
+
return (
|
|
1432
|
+
<Select.Group
|
|
1433
|
+
key={index}
|
|
1434
|
+
renderLabel={renderLabel(key, badgeVariant)}
|
|
1435
|
+
>
|
|
1436
|
+
{options[key].map((option) => (
|
|
1437
|
+
<Select.Option
|
|
1438
|
+
key={option.id}
|
|
1439
|
+
id={option.id}
|
|
1440
|
+
isHighlighted={option.id === highlightedOptionId}
|
|
1441
|
+
isSelected={option.id === selectedOptionId}
|
|
1442
|
+
>
|
|
1443
|
+
{option.label}
|
|
1444
|
+
</Select.Option>
|
|
1445
|
+
))}
|
|
1446
|
+
</Select.Group>
|
|
1447
|
+
)
|
|
1448
|
+
})
|
|
1449
|
+
}
|
|
774
1450
|
|
|
775
1451
|
return (
|
|
776
1452
|
<div>
|
|
@@ -779,196 +1455,342 @@ class GroupSelectExample extends React.Component {
|
|
|
779
1455
|
assistiveText="Type or use arrow keys to navigate options."
|
|
780
1456
|
inputValue={inputValue}
|
|
781
1457
|
isShowingOptions={isShowingOptions}
|
|
782
|
-
onBlur={
|
|
783
|
-
onRequestShowOptions={
|
|
784
|
-
onRequestHideOptions={
|
|
785
|
-
onRequestHighlightOption={
|
|
786
|
-
onRequestSelectOption={
|
|
1458
|
+
onBlur={handleBlur}
|
|
1459
|
+
onRequestShowOptions={handleShowOptions}
|
|
1460
|
+
onRequestHideOptions={handleHideOptions}
|
|
1461
|
+
onRequestHighlightOption={handleHighlightOption}
|
|
1462
|
+
onRequestSelectOption={handleSelectOption}
|
|
787
1463
|
renderBeforeInput={
|
|
788
1464
|
<Badge
|
|
789
1465
|
type="notification"
|
|
790
|
-
variant={
|
|
791
|
-
|
|
792
|
-
|
|
1466
|
+
variant={
|
|
1467
|
+
getOptionById(selectedOptionId).group === 'Eastern'
|
|
1468
|
+
? 'success'
|
|
1469
|
+
: 'primary'
|
|
793
1470
|
}
|
|
794
1471
|
standalone
|
|
795
1472
|
margin="0 0 xxx-small 0"
|
|
796
1473
|
/>
|
|
797
1474
|
}
|
|
798
1475
|
>
|
|
799
|
-
{
|
|
1476
|
+
{renderGroup()}
|
|
800
1477
|
</Select>
|
|
801
1478
|
<Alert
|
|
802
1479
|
liveRegion={() => document.getElementById('flash-messages')}
|
|
803
1480
|
liveRegionPoliteness="assertive"
|
|
804
1481
|
screenReaderOnly
|
|
805
1482
|
>
|
|
806
|
-
{
|
|
1483
|
+
{announcement}
|
|
807
1484
|
</Alert>
|
|
808
1485
|
</div>
|
|
809
1486
|
)
|
|
810
1487
|
}
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
```
|
|
1488
|
+
|
|
1489
|
+
render(
|
|
1490
|
+
<View>
|
|
1491
|
+
<GroupSelectExample
|
|
1492
|
+
options={{
|
|
1493
|
+
Western: [
|
|
1494
|
+
{ id: 'opt5', label: 'Alaska' },
|
|
1495
|
+
{ id: 'opt6', label: 'California' },
|
|
1496
|
+
{ id: 'opt7', label: 'Colorado' },
|
|
1497
|
+
{ id: 'opt8', label: 'Idaho' }
|
|
1498
|
+
],
|
|
1499
|
+
Eastern: [
|
|
1500
|
+
{ id: 'opt1', label: 'Alabama' },
|
|
1501
|
+
{ id: 'opt2', label: 'Connecticut' },
|
|
1502
|
+
{ id: 'opt3', label: 'Delaware' },
|
|
1503
|
+
{ id: '4', label: 'Illinois' }
|
|
1504
|
+
]
|
|
1505
|
+
}}
|
|
1506
|
+
/>
|
|
1507
|
+
</View>
|
|
1508
|
+
)
|
|
1509
|
+
```
|
|
834
1510
|
|
|
835
1511
|
##### Using groups with autocomplete on Safari
|
|
836
1512
|
|
|
837
1513
|
Due to a WebKit bug if you are using `Select.Group` with autocomplete, the screenreader won't announce highlight/selection changes. This only seems to be an issue in Safari. Here is an example how you can work around that:
|
|
838
1514
|
|
|
839
|
-
```javascript
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
1515
|
+
- ```javascript
|
|
1516
|
+
class GroupSelectAutocompleteExample extends React.Component {
|
|
1517
|
+
state = {
|
|
1518
|
+
inputValue: '',
|
|
1519
|
+
isShowingOptions: false,
|
|
1520
|
+
highlightedOptionId: null,
|
|
1521
|
+
selectedOptionId: null,
|
|
1522
|
+
filteredOptions: this.props.options,
|
|
1523
|
+
announcement: null
|
|
1524
|
+
}
|
|
843
1525
|
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
filteredOptions: this.props.options,
|
|
851
|
-
announcement: null
|
|
852
|
-
}
|
|
1526
|
+
getOptionById(id) {
|
|
1527
|
+
const options = this.props.options
|
|
1528
|
+
return Object.values(options)
|
|
1529
|
+
.flat()
|
|
1530
|
+
.find((o) => o?.id === id)
|
|
1531
|
+
}
|
|
853
1532
|
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
1533
|
+
filterOptions(value, options) {
|
|
1534
|
+
const filteredOptions = {}
|
|
1535
|
+
Object.keys(options).forEach((key) => {
|
|
1536
|
+
filteredOptions[key] = options[key]?.filter((option) =>
|
|
1537
|
+
option.label.toLowerCase().includes(value.toLowerCase())
|
|
1538
|
+
)
|
|
1539
|
+
})
|
|
1540
|
+
const optionsWithoutEmptyKeys = Object.keys(filteredOptions)
|
|
1541
|
+
.filter((k) => filteredOptions[k].length > 0)
|
|
1542
|
+
.reduce((a, k) => ({ ...a, [k]: filteredOptions[k] }), {})
|
|
1543
|
+
return optionsWithoutEmptyKeys
|
|
1544
|
+
}
|
|
860
1545
|
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
);
|
|
868
|
-
});
|
|
869
|
-
const optionsWithoutEmptyKeys = Object.keys(filteredOptions)
|
|
870
|
-
.filter((k) => filteredOptions[k].length > 0)
|
|
871
|
-
.reduce((a, k) => ({ ...a, [k]: filteredOptions[k] }), {});
|
|
872
|
-
return optionsWithoutEmptyKeys;
|
|
873
|
-
};
|
|
874
|
-
|
|
875
|
-
handleShowOptions = (event) => {
|
|
876
|
-
this.setState({
|
|
877
|
-
isShowingOptions: true,
|
|
878
|
-
highlightedOptionId: null
|
|
879
|
-
})
|
|
880
|
-
}
|
|
1546
|
+
handleShowOptions = (event) => {
|
|
1547
|
+
this.setState({
|
|
1548
|
+
isShowingOptions: true,
|
|
1549
|
+
highlightedOptionId: null
|
|
1550
|
+
})
|
|
1551
|
+
}
|
|
881
1552
|
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
1553
|
+
handleHideOptions = (event) => {
|
|
1554
|
+
const { selectedOptionId } = this.state
|
|
1555
|
+
this.setState({
|
|
1556
|
+
isShowingOptions: false,
|
|
1557
|
+
highlightedOptionId: null
|
|
1558
|
+
})
|
|
1559
|
+
}
|
|
889
1560
|
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
1561
|
+
handleBlur = (event) => {
|
|
1562
|
+
this.setState({
|
|
1563
|
+
highlightedOptionId: null
|
|
1564
|
+
})
|
|
1565
|
+
}
|
|
895
1566
|
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
1567
|
+
handleHighlightOption = (event, { id }) => {
|
|
1568
|
+
event.persist()
|
|
1569
|
+
const option = this.getOptionById(id)
|
|
1570
|
+
setTimeout(() => {
|
|
1571
|
+
this.setState((state) => ({
|
|
1572
|
+
announcement: option.label
|
|
1573
|
+
}))
|
|
1574
|
+
}, 0)
|
|
900
1575
|
this.setState((state) => ({
|
|
1576
|
+
highlightedOptionId: id
|
|
1577
|
+
}))
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
handleSelectOption = (event, { id }) => {
|
|
1581
|
+
const option = this.getOptionById(id)
|
|
1582
|
+
if (!option) return // prevent selecting of empty option
|
|
1583
|
+
this.setState({
|
|
1584
|
+
selectedOptionId: id,
|
|
1585
|
+
inputValue: option.label,
|
|
1586
|
+
isShowingOptions: false,
|
|
1587
|
+
filteredOptions: this.props.options,
|
|
901
1588
|
announcement: option.label
|
|
1589
|
+
})
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
handleInputChange = (event) => {
|
|
1593
|
+
const value = event.target.value
|
|
1594
|
+
const newOptions = this.filterOptions(value, this.props.options)
|
|
1595
|
+
this.setState((state) => ({
|
|
1596
|
+
inputValue: value,
|
|
1597
|
+
filteredOptions: newOptions,
|
|
1598
|
+
highlightedOptionId: newOptions.length > 0 ? newOptions[0].id : null,
|
|
1599
|
+
isShowingOptions: true,
|
|
1600
|
+
selectedOptionId: value === '' ? null : state.selectedOptionId
|
|
902
1601
|
}))
|
|
903
|
-
}
|
|
904
|
-
this.setState((state) => ({
|
|
905
|
-
highlightedOptionId: id,
|
|
906
|
-
}))
|
|
907
|
-
}
|
|
1602
|
+
}
|
|
908
1603
|
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
this.setState({
|
|
913
|
-
selectedOptionId: id,
|
|
914
|
-
inputValue: option.label,
|
|
915
|
-
isShowingOptions: false,
|
|
916
|
-
filteredOptions: this.props.options,
|
|
917
|
-
announcement: option.label
|
|
918
|
-
})
|
|
919
|
-
}
|
|
1604
|
+
renderGroup() {
|
|
1605
|
+
const filteredOptions = this.state.filteredOptions
|
|
1606
|
+
const { highlightedOptionId, selectedOptionId } = this.state
|
|
920
1607
|
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
1608
|
+
return Object.keys(filteredOptions).map((key, index) => {
|
|
1609
|
+
return (
|
|
1610
|
+
<Select.Group key={index} renderLabel={key}>
|
|
1611
|
+
{filteredOptions[key].map((option) => (
|
|
1612
|
+
<Select.Option
|
|
1613
|
+
key={option.id}
|
|
1614
|
+
id={option.id}
|
|
1615
|
+
isHighlighted={option.id === highlightedOptionId}
|
|
1616
|
+
isSelected={option.id === selectedOptionId}
|
|
1617
|
+
>
|
|
1618
|
+
{option.label}
|
|
1619
|
+
</Select.Option>
|
|
1620
|
+
))}
|
|
1621
|
+
</Select.Group>
|
|
1622
|
+
)
|
|
1623
|
+
})
|
|
1624
|
+
}
|
|
932
1625
|
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
1626
|
+
renderScreenReaderHelper() {
|
|
1627
|
+
const announcement = this.state.announcement
|
|
1628
|
+
return (
|
|
1629
|
+
window.safari && (
|
|
1630
|
+
<ScreenReaderContent>
|
|
1631
|
+
<span role="alert" aria-live="assertive">
|
|
1632
|
+
{announcement}
|
|
1633
|
+
</span>
|
|
1634
|
+
</ScreenReaderContent>
|
|
1635
|
+
)
|
|
1636
|
+
)
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
render() {
|
|
1640
|
+
const {
|
|
1641
|
+
inputValue,
|
|
1642
|
+
isShowingOptions,
|
|
1643
|
+
highlightedOptionId,
|
|
1644
|
+
selectedOptionId,
|
|
1645
|
+
filteredOptions
|
|
1646
|
+
} = this.state
|
|
936
1647
|
|
|
937
|
-
return Object.keys(filteredOptions).map((key, index) => {
|
|
938
1648
|
return (
|
|
939
|
-
<
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
1649
|
+
<div>
|
|
1650
|
+
<Select
|
|
1651
|
+
placeholder="Start typing to search..."
|
|
1652
|
+
renderLabel="Group Select with autocomplete"
|
|
1653
|
+
assistiveText="Type or use arrow keys to navigate options."
|
|
1654
|
+
inputValue={inputValue}
|
|
1655
|
+
isShowingOptions={isShowingOptions}
|
|
1656
|
+
onBlur={this.handleBlur}
|
|
1657
|
+
onInputChange={this.handleInputChange}
|
|
1658
|
+
onRequestShowOptions={this.handleShowOptions}
|
|
1659
|
+
onRequestHideOptions={this.handleHideOptions}
|
|
1660
|
+
onRequestHighlightOption={this.handleHighlightOption}
|
|
1661
|
+
onRequestSelectOption={this.handleSelectOption}
|
|
1662
|
+
>
|
|
1663
|
+
{this.renderGroup()}
|
|
1664
|
+
</Select>
|
|
1665
|
+
{this.renderScreenReaderHelper()}
|
|
1666
|
+
</div>
|
|
951
1667
|
)
|
|
952
|
-
}
|
|
1668
|
+
}
|
|
953
1669
|
}
|
|
954
1670
|
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
1671
|
+
render(
|
|
1672
|
+
<View>
|
|
1673
|
+
<GroupSelectAutocompleteExample
|
|
1674
|
+
options={{
|
|
1675
|
+
Western: [
|
|
1676
|
+
{ id: 'opt5', label: 'Alaska' },
|
|
1677
|
+
{ id: 'opt6', label: 'California' },
|
|
1678
|
+
{ id: 'opt7', label: 'Colorado' },
|
|
1679
|
+
{ id: 'opt8', label: 'Idaho' }
|
|
1680
|
+
],
|
|
1681
|
+
Eastern: [
|
|
1682
|
+
{ id: 'opt1', label: 'Alabama' },
|
|
1683
|
+
{ id: 'opt2', label: 'Connecticut' },
|
|
1684
|
+
{ id: 'opt3', label: 'Delaware' },
|
|
1685
|
+
{ id: '4', label: 'Illinois' }
|
|
1686
|
+
]
|
|
1687
|
+
}}
|
|
1688
|
+
/>
|
|
1689
|
+
</View>
|
|
1690
|
+
)
|
|
1691
|
+
```
|
|
1692
|
+
|
|
1693
|
+
- ```js
|
|
1694
|
+
const GroupSelectAutocompleteExample = ({ options }) => {
|
|
1695
|
+
const [inputValue, setInputValue] = useState('')
|
|
1696
|
+
const [isShowingOptions, setIsShowingOptions] = useState(false)
|
|
1697
|
+
const [highlightedOptionId, setHighlightedOptionId] = useState(null)
|
|
1698
|
+
const [selectedOptionId, setSelectedOptionId] = useState(null)
|
|
1699
|
+
const [filteredOptions, setFilteredOptions] = useState(options)
|
|
1700
|
+
const [announcement, setAnnouncement] = useState(null)
|
|
1701
|
+
|
|
1702
|
+
const getOptionById = (id) => {
|
|
1703
|
+
return Object.values(options)
|
|
1704
|
+
.flat()
|
|
1705
|
+
.find((o) => o?.id === id)
|
|
1706
|
+
}
|
|
963
1707
|
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1708
|
+
const filterOptions = (value, options) => {
|
|
1709
|
+
const filteredOptions = {}
|
|
1710
|
+
Object.keys(options).forEach((key) => {
|
|
1711
|
+
filteredOptions[key] = options[key]?.filter((option) =>
|
|
1712
|
+
option.label.toLowerCase().includes(value.toLowerCase())
|
|
1713
|
+
)
|
|
1714
|
+
})
|
|
1715
|
+
const optionsWithoutEmptyKeys = Object.keys(filteredOptions)
|
|
1716
|
+
.filter((k) => filteredOptions[k].length > 0)
|
|
1717
|
+
.reduce((a, k) => ({ ...a, [k]: filteredOptions[k] }), {})
|
|
1718
|
+
return optionsWithoutEmptyKeys
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
const handleShowOptions = (event) => {
|
|
1722
|
+
setIsShowingOptions(true)
|
|
1723
|
+
setHighlightedOptionId(null)
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
const handleHideOptions = (event) => {
|
|
1727
|
+
setIsShowingOptions(false)
|
|
1728
|
+
setHighlightedOptionId(null)
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
const handleBlur = (event) => {
|
|
1732
|
+
setHighlightedOptionId(null)
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
const handleHighlightOption = (event, { id }) => {
|
|
1736
|
+
event.persist()
|
|
1737
|
+
const option = getOptionById(id)
|
|
1738
|
+
setTimeout(() => {
|
|
1739
|
+
setAnnouncement(option.label)
|
|
1740
|
+
}, 0)
|
|
1741
|
+
setHighlightedOptionId(id)
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
const handleSelectOption = (event, { id }) => {
|
|
1745
|
+
const option = getOptionById(id)
|
|
1746
|
+
if (!option) return // prevent selecting of empty option
|
|
1747
|
+
setSelectedOptionId(id)
|
|
1748
|
+
setInputValue(option.label)
|
|
1749
|
+
setIsShowingOptions(false)
|
|
1750
|
+
setFilteredOptions(options)
|
|
1751
|
+
setAnnouncement(option.label)
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
const handleInputChange = (event) => {
|
|
1755
|
+
const value = event.target.value
|
|
1756
|
+
const newOptions = filterOptions(value, options)
|
|
1757
|
+
setInputValue(value)
|
|
1758
|
+
setFilteredOptions(newOptions)
|
|
1759
|
+
setHighlightedOptionId(newOptions.length > 0 ? newOptions[0].id : null)
|
|
1760
|
+
setIsShowingOptions(true)
|
|
1761
|
+
setSelectedOptionId(value === '' ? null : selectedOptionId)
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
const renderGroup = () => {
|
|
1765
|
+
return Object.keys(filteredOptions).map((key, index) => {
|
|
1766
|
+
return (
|
|
1767
|
+
<Select.Group key={index} renderLabel={key}>
|
|
1768
|
+
{filteredOptions[key].map((option) => (
|
|
1769
|
+
<Select.Option
|
|
1770
|
+
key={option.id}
|
|
1771
|
+
id={option.id}
|
|
1772
|
+
isHighlighted={option.id === highlightedOptionId}
|
|
1773
|
+
isSelected={option.id === selectedOptionId}
|
|
1774
|
+
>
|
|
1775
|
+
{option.label}
|
|
1776
|
+
</Select.Option>
|
|
1777
|
+
))}
|
|
1778
|
+
</Select.Group>
|
|
1779
|
+
)
|
|
1780
|
+
})
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
const renderScreenReaderHelper = () => {
|
|
1784
|
+
return (
|
|
1785
|
+
window.safari && (
|
|
1786
|
+
<ScreenReaderContent>
|
|
1787
|
+
<span role="alert" aria-live="assertive">
|
|
1788
|
+
{announcement}
|
|
1789
|
+
</span>
|
|
1790
|
+
</ScreenReaderContent>
|
|
1791
|
+
)
|
|
1792
|
+
)
|
|
1793
|
+
}
|
|
972
1794
|
|
|
973
1795
|
return (
|
|
974
1796
|
<div>
|
|
@@ -978,193 +1800,379 @@ class GroupSelectAutocompleteExample extends React.Component {
|
|
|
978
1800
|
assistiveText="Type or use arrow keys to navigate options."
|
|
979
1801
|
inputValue={inputValue}
|
|
980
1802
|
isShowingOptions={isShowingOptions}
|
|
981
|
-
onBlur={
|
|
982
|
-
onInputChange={
|
|
983
|
-
onRequestShowOptions={
|
|
984
|
-
onRequestHideOptions={
|
|
985
|
-
onRequestHighlightOption={
|
|
986
|
-
onRequestSelectOption={
|
|
1803
|
+
onBlur={handleBlur}
|
|
1804
|
+
onInputChange={handleInputChange}
|
|
1805
|
+
onRequestShowOptions={handleShowOptions}
|
|
1806
|
+
onRequestHideOptions={handleHideOptions}
|
|
1807
|
+
onRequestHighlightOption={handleHighlightOption}
|
|
1808
|
+
onRequestSelectOption={handleSelectOption}
|
|
987
1809
|
>
|
|
988
|
-
{
|
|
1810
|
+
{renderGroup()}
|
|
989
1811
|
</Select>
|
|
990
|
-
{
|
|
1812
|
+
{renderScreenReaderHelper()}
|
|
991
1813
|
</div>
|
|
992
1814
|
)
|
|
993
1815
|
}
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
```
|
|
1816
|
+
|
|
1817
|
+
render(
|
|
1818
|
+
<View>
|
|
1819
|
+
<GroupSelectAutocompleteExample
|
|
1820
|
+
options={{
|
|
1821
|
+
Western: [
|
|
1822
|
+
{ id: 'opt5', label: 'Alaska' },
|
|
1823
|
+
{ id: 'opt6', label: 'California' },
|
|
1824
|
+
{ id: 'opt7', label: 'Colorado' },
|
|
1825
|
+
{ id: 'opt8', label: 'Idaho' }
|
|
1826
|
+
],
|
|
1827
|
+
Eastern: [
|
|
1828
|
+
{ id: 'opt1', label: 'Alabama' },
|
|
1829
|
+
{ id: 'opt2', label: 'Connecticut' },
|
|
1830
|
+
{ id: 'opt3', label: 'Delaware' },
|
|
1831
|
+
{ id: '4', label: 'Illinois' }
|
|
1832
|
+
]
|
|
1833
|
+
}}
|
|
1834
|
+
/>
|
|
1835
|
+
</View>
|
|
1836
|
+
)
|
|
1837
|
+
```
|
|
1017
1838
|
|
|
1018
1839
|
#### Asynchronous option loading
|
|
1019
1840
|
|
|
1020
1841
|
If no results match the user's search, it's recommended to leave `isShowingOptions` as `true` and to display an "empty option" as a way of communicating that there are no matches. Similarly, it's helpful to display a [Spinner](#Spinner) in an empty option while options load.
|
|
1021
1842
|
|
|
1022
|
-
```javascript
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
selectedOptionLabel: '',
|
|
1035
|
-
filteredOptions: [],
|
|
1036
|
-
announcement: null
|
|
1037
|
-
}
|
|
1843
|
+
- ```javascript
|
|
1844
|
+
class AsyncExample extends React.Component {
|
|
1845
|
+
state = {
|
|
1846
|
+
inputValue: '',
|
|
1847
|
+
isShowingOptions: false,
|
|
1848
|
+
isLoading: false,
|
|
1849
|
+
highlightedOptionId: null,
|
|
1850
|
+
selectedOptionId: null,
|
|
1851
|
+
selectedOptionLabel: '',
|
|
1852
|
+
filteredOptions: [],
|
|
1853
|
+
announcement: null
|
|
1854
|
+
}
|
|
1038
1855
|
|
|
1039
|
-
|
|
1856
|
+
timeoutId = null
|
|
1040
1857
|
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1858
|
+
getOptionById(queryId) {
|
|
1859
|
+
return this.state.filteredOptions.find(({ id }) => id === queryId)
|
|
1860
|
+
}
|
|
1044
1861
|
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1862
|
+
filterOptions = (value) => {
|
|
1863
|
+
return this.props.options.filter((option) =>
|
|
1864
|
+
option.label.toLowerCase().startsWith(value.toLowerCase())
|
|
1865
|
+
)
|
|
1866
|
+
}
|
|
1050
1867
|
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1868
|
+
matchValue() {
|
|
1869
|
+
const {
|
|
1870
|
+
filteredOptions,
|
|
1871
|
+
inputValue,
|
|
1872
|
+
selectedOptionId,
|
|
1873
|
+
selectedOptionLabel
|
|
1874
|
+
} = this.state
|
|
1875
|
+
|
|
1876
|
+
// an option matching user input exists
|
|
1877
|
+
if (filteredOptions.length === 1) {
|
|
1878
|
+
const onlyOption = filteredOptions[0]
|
|
1879
|
+
// automatically select the matching option
|
|
1880
|
+
if (onlyOption.label.toLowerCase() === inputValue.toLowerCase()) {
|
|
1881
|
+
return {
|
|
1882
|
+
inputValue: onlyOption.label,
|
|
1883
|
+
selectedOptionId: onlyOption.id
|
|
1884
|
+
}
|
|
1067
1885
|
}
|
|
1068
1886
|
}
|
|
1887
|
+
// allow user to return to empty input and no selection
|
|
1888
|
+
if (inputValue.length === 0) {
|
|
1889
|
+
return { selectedOptionId: null, filteredOptions: [] }
|
|
1890
|
+
}
|
|
1891
|
+
// no match found, return selected option label to input
|
|
1892
|
+
if (selectedOptionId) {
|
|
1893
|
+
return { inputValue: selectedOptionLabel }
|
|
1894
|
+
}
|
|
1069
1895
|
}
|
|
1070
|
-
// allow user to return to empty input and no selection
|
|
1071
|
-
if (inputValue.length === 0) {
|
|
1072
|
-
return { selectedOptionId: null, filteredOptions: [] }
|
|
1073
|
-
}
|
|
1074
|
-
// no match found, return selected option label to input
|
|
1075
|
-
if (selectedOptionId) {
|
|
1076
|
-
return { inputValue: selectedOptionLabel }
|
|
1077
|
-
}
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
handleShowOptions = (event) => {
|
|
1081
|
-
this.setState(({ filteredOptions }) => ({
|
|
1082
|
-
isShowingOptions: true
|
|
1083
|
-
}))
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
handleHideOptions = (event) => {
|
|
1087
|
-
const { selectedOptionId, inputValue } = this.state
|
|
1088
|
-
this.setState({
|
|
1089
|
-
isShowingOptions: false,
|
|
1090
|
-
highlightedOptionId: null,
|
|
1091
|
-
announcement: 'List collapsed.',
|
|
1092
|
-
...this.matchValue()
|
|
1093
|
-
})
|
|
1094
|
-
}
|
|
1095
1896
|
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1897
|
+
handleShowOptions = (event) => {
|
|
1898
|
+
this.setState(({ filteredOptions }) => ({
|
|
1899
|
+
isShowingOptions: true
|
|
1900
|
+
}))
|
|
1901
|
+
}
|
|
1099
1902
|
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1903
|
+
handleHideOptions = (event) => {
|
|
1904
|
+
const { selectedOptionId, inputValue } = this.state
|
|
1905
|
+
this.setState({
|
|
1906
|
+
isShowingOptions: false,
|
|
1907
|
+
highlightedOptionId: null,
|
|
1908
|
+
announcement: 'List collapsed.',
|
|
1909
|
+
...this.matchValue()
|
|
1910
|
+
})
|
|
1911
|
+
}
|
|
1110
1912
|
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
this.setState({
|
|
1115
|
-
selectedOptionId: id,
|
|
1116
|
-
selectedOptionLabel: option.label,
|
|
1117
|
-
inputValue: option.label,
|
|
1118
|
-
isShowingOptions: false,
|
|
1119
|
-
announcement: `${option.label} selected. List collapsed.`,
|
|
1120
|
-
filteredOptions: [this.getOptionById(id)]
|
|
1121
|
-
})
|
|
1122
|
-
}
|
|
1913
|
+
handleBlur = (event) => {
|
|
1914
|
+
this.setState({ highlightedOptionId: null })
|
|
1915
|
+
}
|
|
1123
1916
|
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1917
|
+
handleHighlightOption = (event, { id }) => {
|
|
1918
|
+
event.persist()
|
|
1919
|
+
const option = this.getOptionById(id)
|
|
1920
|
+
if (!option) return // prevent highlighting of empty option
|
|
1921
|
+
this.setState((state) => ({
|
|
1922
|
+
highlightedOptionId: id,
|
|
1923
|
+
inputValue: event.type === 'keydown' ? option.label : state.inputValue,
|
|
1924
|
+
announcement: option.label
|
|
1925
|
+
}))
|
|
1926
|
+
}
|
|
1127
1927
|
|
|
1128
|
-
|
|
1928
|
+
handleSelectOption = (event, { id }) => {
|
|
1929
|
+
const option = this.getOptionById(id)
|
|
1930
|
+
if (!option) return // prevent selecting of empty option
|
|
1129
1931
|
this.setState({
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
filteredOptions: []
|
|
1136
|
-
})
|
|
1137
|
-
} else {
|
|
1138
|
-
this.setState({
|
|
1139
|
-
isLoading: true,
|
|
1140
|
-
inputValue: value,
|
|
1141
|
-
isShowingOptions: true,
|
|
1142
|
-
filteredOptions: [],
|
|
1143
|
-
highlightedOptionId: null,
|
|
1144
|
-
announcement: 'Loading options.'
|
|
1932
|
+
selectedOptionId: id,
|
|
1933
|
+
selectedOptionLabel: option.label,
|
|
1934
|
+
inputValue: option.label,
|
|
1935
|
+
isShowingOptions: false,
|
|
1936
|
+
announcement: `${option.label} selected. List collapsed.`,
|
|
1937
|
+
filteredOptions: [this.getOptionById(id)]
|
|
1145
1938
|
})
|
|
1939
|
+
}
|
|
1146
1940
|
|
|
1147
|
-
|
|
1148
|
-
|
|
1941
|
+
handleInputChange = (event) => {
|
|
1942
|
+
const value = event.target.value
|
|
1943
|
+
clearTimeout(this.timeoutId)
|
|
1944
|
+
|
|
1945
|
+
if (!value || value === '') {
|
|
1149
1946
|
this.setState({
|
|
1150
|
-
filteredOptions: newOptions,
|
|
1151
1947
|
isLoading: false,
|
|
1152
|
-
|
|
1948
|
+
inputValue: value,
|
|
1949
|
+
isShowingOptions: true,
|
|
1950
|
+
selectedOptionId: null,
|
|
1951
|
+
selectedOptionLabel: null,
|
|
1952
|
+
filteredOptions: []
|
|
1153
1953
|
})
|
|
1154
|
-
}
|
|
1954
|
+
} else {
|
|
1955
|
+
this.setState({
|
|
1956
|
+
isLoading: true,
|
|
1957
|
+
inputValue: value,
|
|
1958
|
+
isShowingOptions: true,
|
|
1959
|
+
filteredOptions: [],
|
|
1960
|
+
highlightedOptionId: null,
|
|
1961
|
+
announcement: 'Loading options.'
|
|
1962
|
+
})
|
|
1963
|
+
|
|
1964
|
+
this.timeoutId = setTimeout(() => {
|
|
1965
|
+
const newOptions = this.filterOptions(value)
|
|
1966
|
+
this.setState({
|
|
1967
|
+
filteredOptions: newOptions,
|
|
1968
|
+
isLoading: false,
|
|
1969
|
+
announcement: `${newOptions.length} options available.`
|
|
1970
|
+
})
|
|
1971
|
+
}, 1500)
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
render() {
|
|
1976
|
+
const {
|
|
1977
|
+
inputValue,
|
|
1978
|
+
isShowingOptions,
|
|
1979
|
+
isLoading,
|
|
1980
|
+
highlightedOptionId,
|
|
1981
|
+
selectedOptionId,
|
|
1982
|
+
filteredOptions,
|
|
1983
|
+
announcement
|
|
1984
|
+
} = this.state
|
|
1985
|
+
|
|
1986
|
+
return (
|
|
1987
|
+
<div>
|
|
1988
|
+
<Select
|
|
1989
|
+
renderLabel="Async Select"
|
|
1990
|
+
assistiveText="Type to search"
|
|
1991
|
+
inputValue={inputValue}
|
|
1992
|
+
isShowingOptions={isShowingOptions}
|
|
1993
|
+
onBlur={this.handleBlur}
|
|
1994
|
+
onInputChange={this.handleInputChange}
|
|
1995
|
+
onRequestShowOptions={this.handleShowOptions}
|
|
1996
|
+
onRequestHideOptions={this.handleHideOptions}
|
|
1997
|
+
onRequestHighlightOption={this.handleHighlightOption}
|
|
1998
|
+
onRequestSelectOption={this.handleSelectOption}
|
|
1999
|
+
>
|
|
2000
|
+
{filteredOptions.length > 0 ? (
|
|
2001
|
+
filteredOptions.map((option) => {
|
|
2002
|
+
return (
|
|
2003
|
+
<Select.Option
|
|
2004
|
+
id={option.id}
|
|
2005
|
+
key={option.id}
|
|
2006
|
+
isHighlighted={option.id === highlightedOptionId}
|
|
2007
|
+
isSelected={option.id === selectedOptionId}
|
|
2008
|
+
isDisabled={option.disabled}
|
|
2009
|
+
renderBeforeLabel={
|
|
2010
|
+
!option.disabled ? IconUserSolid : IconUserLine
|
|
2011
|
+
}
|
|
2012
|
+
>
|
|
2013
|
+
{option.label}
|
|
2014
|
+
</Select.Option>
|
|
2015
|
+
)
|
|
2016
|
+
})
|
|
2017
|
+
) : (
|
|
2018
|
+
<Select.Option id="empty-option" key="empty-option">
|
|
2019
|
+
{isLoading ? (
|
|
2020
|
+
<Spinner renderTitle="Loading" size="x-small" />
|
|
2021
|
+
) : inputValue !== '' ? (
|
|
2022
|
+
'No results'
|
|
2023
|
+
) : (
|
|
2024
|
+
'Type to search'
|
|
2025
|
+
)}
|
|
2026
|
+
</Select.Option>
|
|
2027
|
+
)}
|
|
2028
|
+
</Select>
|
|
2029
|
+
<Alert
|
|
2030
|
+
liveRegion={() => document.getElementById('flash-messages')}
|
|
2031
|
+
liveRegionPoliteness="assertive"
|
|
2032
|
+
screenReaderOnly
|
|
2033
|
+
>
|
|
2034
|
+
{announcement}
|
|
2035
|
+
</Alert>
|
|
2036
|
+
</div>
|
|
2037
|
+
)
|
|
1155
2038
|
}
|
|
1156
2039
|
}
|
|
1157
2040
|
|
|
1158
|
-
render
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
2041
|
+
render(
|
|
2042
|
+
<View>
|
|
2043
|
+
<AsyncExample
|
|
2044
|
+
options={[
|
|
2045
|
+
{ id: 'opt0', label: 'Aaron Aaronson' },
|
|
2046
|
+
{ id: 'opt1', label: 'Amber Murphy' },
|
|
2047
|
+
{ id: 'opt2', label: 'Andrew Miller' },
|
|
2048
|
+
{ id: 'opt3', label: 'Barbara Ward' },
|
|
2049
|
+
{ id: 'opt4', label: 'Byron Cranston', disabled: true },
|
|
2050
|
+
{ id: 'opt5', label: 'Dennis Reynolds' },
|
|
2051
|
+
{ id: 'opt6', label: 'Dee Reynolds' },
|
|
2052
|
+
{ id: 'opt7', label: 'Ezra Betterthan' },
|
|
2053
|
+
{ id: 'opt8', label: 'Jeff Spicoli' },
|
|
2054
|
+
{ id: 'opt9', label: 'Joseph Smith' },
|
|
2055
|
+
{ id: 'opt10', label: 'Jasmine Diaz' },
|
|
2056
|
+
{ id: 'opt11', label: 'Martin Harris' },
|
|
2057
|
+
{ id: 'opt12', label: 'Michael Morgan', disabled: true },
|
|
2058
|
+
{ id: 'opt13', label: 'Michelle Rodriguez' },
|
|
2059
|
+
{ id: 'opt14', label: 'Ziggy Stardust' }
|
|
2060
|
+
]}
|
|
2061
|
+
/>
|
|
2062
|
+
</View>
|
|
2063
|
+
)
|
|
2064
|
+
```
|
|
2065
|
+
|
|
2066
|
+
- ```js
|
|
2067
|
+
const AsyncExample = ({ options }) => {
|
|
2068
|
+
const [inputValue, setInputValue] = useState('')
|
|
2069
|
+
const [isShowingOptions, setIsShowingOptions] = useState(false)
|
|
2070
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
2071
|
+
const [highlightedOptionId, setHighlightedOptionId] = useState(null)
|
|
2072
|
+
const [selectedOptionId, setSelectedOptionId] = useState(null)
|
|
2073
|
+
const [selectedOptionLabel, setSelectedOptionLabel] = useState('')
|
|
2074
|
+
const [filteredOptions, setFilteredOptions] = useState([])
|
|
2075
|
+
const [announcement, setAnnouncement] = useState(null)
|
|
2076
|
+
|
|
2077
|
+
let timeoutId = null
|
|
2078
|
+
|
|
2079
|
+
const getOptionById = (queryId) => {
|
|
2080
|
+
return filteredOptions.find(({ id }) => id === queryId)
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
const filterOptions = (value) => {
|
|
2084
|
+
return options.filter((option) =>
|
|
2085
|
+
option.label.toLowerCase().startsWith(value.toLowerCase())
|
|
2086
|
+
)
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
const matchValue = () => {
|
|
2090
|
+
// an option matching user input exists
|
|
2091
|
+
if (filteredOptions.length === 1) {
|
|
2092
|
+
const onlyOption = filteredOptions[0]
|
|
2093
|
+
// automatically select the matching option
|
|
2094
|
+
if (onlyOption.label.toLowerCase() === inputValue.toLowerCase()) {
|
|
2095
|
+
setInputValue(onlyOption.label)
|
|
2096
|
+
setSelectedOptionId(onlyOption.id)
|
|
2097
|
+
return
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
// allow user to return to empty input and no selection
|
|
2101
|
+
if (inputValue.length === 0) {
|
|
2102
|
+
setSelectedOptionId(null)
|
|
2103
|
+
setFilteredOptions([])
|
|
2104
|
+
return
|
|
2105
|
+
}
|
|
2106
|
+
// no match found, return selected option label to input
|
|
2107
|
+
if (selectedOptionId) {
|
|
2108
|
+
setInputValue(selectedOptionLabel)
|
|
2109
|
+
return
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
const handleShowOptions = (event) => {
|
|
2114
|
+
setIsShowingOptions(true)
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
const handleHideOptions = (event) => {
|
|
2118
|
+
setIsShowingOptions(false)
|
|
2119
|
+
setHighlightedOptionId(null)
|
|
2120
|
+
setAnnouncement('List collapsed.')
|
|
2121
|
+
matchValue()
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
const handleBlur = (event) => {
|
|
2125
|
+
setHighlightedOptionId(null)
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
const handleHighlightOption = (event, { id }) => {
|
|
2129
|
+
event.persist()
|
|
2130
|
+
const option = getOptionById(id)
|
|
2131
|
+
if (!option) return // prevent highlighting of empty option
|
|
2132
|
+
|
|
2133
|
+
setHighlightedOptionId(id)
|
|
2134
|
+
setInputValue(event.type === 'keydown' ? option.label : inputValue)
|
|
2135
|
+
setAnnouncement(option.label)
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
const handleSelectOption = (event, { id }) => {
|
|
2139
|
+
const option = getOptionById(id)
|
|
2140
|
+
if (!option) return // prevent selecting of empty option
|
|
2141
|
+
setSelectedOptionId(id)
|
|
2142
|
+
setSelectedOptionLabel(option.label)
|
|
2143
|
+
setInputValue(option.label)
|
|
2144
|
+
setIsShowingOptions(false)
|
|
2145
|
+
setAnnouncement(`${option.label} selected. List collapsed.`)
|
|
2146
|
+
setFilteredOptions([getOptionById(id)])
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
const handleInputChange = (event) => {
|
|
2150
|
+
const value = event.target.value
|
|
2151
|
+
clearTimeout(timeoutId)
|
|
2152
|
+
|
|
2153
|
+
if (!value || value === '') {
|
|
2154
|
+
setIsLoading(false)
|
|
2155
|
+
setInputValue(value)
|
|
2156
|
+
setIsShowingOptions(true)
|
|
2157
|
+
setSelectedOptionId(null)
|
|
2158
|
+
setSelectedOptionLabel(null)
|
|
2159
|
+
setFilteredOptions([])
|
|
2160
|
+
} else {
|
|
2161
|
+
setIsLoading(true)
|
|
2162
|
+
setInputValue(value)
|
|
2163
|
+
setIsShowingOptions(true)
|
|
2164
|
+
setFilteredOptions([])
|
|
2165
|
+
setHighlightedOptionId(null)
|
|
2166
|
+
setAnnouncement('Loading options.')
|
|
2167
|
+
|
|
2168
|
+
timeoutId = setTimeout(() => {
|
|
2169
|
+
const newOptions = filterOptions(value)
|
|
2170
|
+
setFilteredOptions(newOptions)
|
|
2171
|
+
setIsLoading(false)
|
|
2172
|
+
setAnnouncement(`${newOptions.length} options available.`)
|
|
2173
|
+
}, 1500)
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
1168
2176
|
|
|
1169
2177
|
return (
|
|
1170
2178
|
<div>
|
|
@@ -1173,31 +2181,39 @@ class AsyncExample extends React.Component {
|
|
|
1173
2181
|
assistiveText="Type to search"
|
|
1174
2182
|
inputValue={inputValue}
|
|
1175
2183
|
isShowingOptions={isShowingOptions}
|
|
1176
|
-
onBlur={
|
|
1177
|
-
onInputChange={
|
|
1178
|
-
onRequestShowOptions={
|
|
1179
|
-
onRequestHideOptions={
|
|
1180
|
-
onRequestHighlightOption={
|
|
1181
|
-
onRequestSelectOption={
|
|
2184
|
+
onBlur={handleBlur}
|
|
2185
|
+
onInputChange={handleInputChange}
|
|
2186
|
+
onRequestShowOptions={handleShowOptions}
|
|
2187
|
+
onRequestHideOptions={handleHideOptions}
|
|
2188
|
+
onRequestHighlightOption={handleHighlightOption}
|
|
2189
|
+
onRequestSelectOption={handleSelectOption}
|
|
1182
2190
|
>
|
|
1183
|
-
{filteredOptions.length > 0 ?
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
2191
|
+
{filteredOptions.length > 0 ? (
|
|
2192
|
+
filteredOptions.map((option) => {
|
|
2193
|
+
return (
|
|
2194
|
+
<Select.Option
|
|
2195
|
+
id={option.id}
|
|
2196
|
+
key={option.id}
|
|
2197
|
+
isHighlighted={option.id === highlightedOptionId}
|
|
2198
|
+
isSelected={option.id === selectedOptionId}
|
|
2199
|
+
isDisabled={option.disabled}
|
|
2200
|
+
renderBeforeLabel={
|
|
2201
|
+
!option.disabled ? IconUserSolid : IconUserLine
|
|
2202
|
+
}
|
|
2203
|
+
>
|
|
2204
|
+
{option.label}
|
|
2205
|
+
</Select.Option>
|
|
2206
|
+
)
|
|
2207
|
+
})
|
|
2208
|
+
) : (
|
|
1197
2209
|
<Select.Option id="empty-option" key="empty-option">
|
|
1198
|
-
{isLoading
|
|
1199
|
-
|
|
1200
|
-
|
|
2210
|
+
{isLoading ? (
|
|
2211
|
+
<Spinner renderTitle="Loading" size="x-small" />
|
|
2212
|
+
) : inputValue !== '' ? (
|
|
2213
|
+
'No results'
|
|
2214
|
+
) : (
|
|
2215
|
+
'Type to search'
|
|
2216
|
+
)}
|
|
1201
2217
|
</Select.Option>
|
|
1202
2218
|
)}
|
|
1203
2219
|
</Select>
|
|
@@ -1206,112 +2222,228 @@ class AsyncExample extends React.Component {
|
|
|
1206
2222
|
liveRegionPoliteness="assertive"
|
|
1207
2223
|
screenReaderOnly
|
|
1208
2224
|
>
|
|
1209
|
-
{
|
|
2225
|
+
{announcement}
|
|
1210
2226
|
</Alert>
|
|
1211
2227
|
</div>
|
|
1212
2228
|
)
|
|
1213
2229
|
}
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
```
|
|
2230
|
+
|
|
2231
|
+
render(
|
|
2232
|
+
<View>
|
|
2233
|
+
<AsyncExample
|
|
2234
|
+
options={[
|
|
2235
|
+
{ id: 'opt0', label: 'Aaron Aaronson' },
|
|
2236
|
+
{ id: 'opt1', label: 'Amber Murphy' },
|
|
2237
|
+
{ id: 'opt2', label: 'Andrew Miller' },
|
|
2238
|
+
{ id: 'opt3', label: 'Barbara Ward' },
|
|
2239
|
+
{ id: 'opt4', label: 'Byron Cranston', disabled: true },
|
|
2240
|
+
{ id: 'opt5', label: 'Dennis Reynolds' },
|
|
2241
|
+
{ id: 'opt6', label: 'Dee Reynolds' },
|
|
2242
|
+
{ id: 'opt7', label: 'Ezra Betterthan' },
|
|
2243
|
+
{ id: 'opt8', label: 'Jeff Spicoli' },
|
|
2244
|
+
{ id: 'opt9', label: 'Joseph Smith' },
|
|
2245
|
+
{ id: 'opt10', label: 'Jasmine Diaz' },
|
|
2246
|
+
{ id: 'opt11', label: 'Martin Harris' },
|
|
2247
|
+
{ id: 'opt12', label: 'Michael Morgan', disabled: true },
|
|
2248
|
+
{ id: 'opt13', label: 'Michelle Rodriguez' },
|
|
2249
|
+
{ id: 'opt14', label: 'Ziggy Stardust' }
|
|
2250
|
+
]}
|
|
2251
|
+
/>
|
|
2252
|
+
</View>
|
|
2253
|
+
)
|
|
2254
|
+
```
|
|
1240
2255
|
|
|
1241
2256
|
### Icons
|
|
1242
2257
|
|
|
1243
2258
|
To display icons (or other elements) before or after an option, pass it via the `renderBeforeLabel` and `renderAfterLabel` prop to `Select.Option`. You can pass a function as well, which will have a `props` parameter, so you can access the properties of that `Select.Option` (e.g. if it is currently `isHighlighted`). The available props are: `[ id, isDisabled, isSelected, isHighlighted, children ]`.
|
|
1244
2259
|
|
|
1245
|
-
```
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
selectedOptionId: this.props.options[0].id,
|
|
1255
|
-
announcement: null
|
|
1256
|
-
}
|
|
2260
|
+
- ```js
|
|
2261
|
+
class SingleSelectExample extends React.Component {
|
|
2262
|
+
state = {
|
|
2263
|
+
inputValue: this.props.options[0].label,
|
|
2264
|
+
isShowingOptions: false,
|
|
2265
|
+
highlightedOptionId: null,
|
|
2266
|
+
selectedOptionId: this.props.options[0].id,
|
|
2267
|
+
announcement: null
|
|
2268
|
+
}
|
|
1257
2269
|
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
2270
|
+
getOptionById(queryId) {
|
|
2271
|
+
return this.props.options.find(({ id }) => id === queryId)
|
|
2272
|
+
}
|
|
1261
2273
|
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
2274
|
+
handleShowOptions = (event) => {
|
|
2275
|
+
this.setState({
|
|
2276
|
+
isShowingOptions: true
|
|
2277
|
+
})
|
|
2278
|
+
}
|
|
1267
2279
|
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
2280
|
+
handleHideOptions = (event) => {
|
|
2281
|
+
const { selectedOptionId } = this.state
|
|
2282
|
+
const option = this.getOptionById(selectedOptionId).label
|
|
2283
|
+
this.setState({
|
|
2284
|
+
isShowingOptions: false,
|
|
2285
|
+
highlightedOptionId: null,
|
|
2286
|
+
inputValue: selectedOptionId ? option : '',
|
|
2287
|
+
announcement: 'List collapsed.'
|
|
2288
|
+
})
|
|
2289
|
+
}
|
|
1278
2290
|
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
2291
|
+
handleBlur = (event) => {
|
|
2292
|
+
this.setState({
|
|
2293
|
+
highlightedOptionId: null
|
|
2294
|
+
})
|
|
2295
|
+
}
|
|
1284
2296
|
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
2297
|
+
handleHighlightOption = (event, { id }) => {
|
|
2298
|
+
event.persist()
|
|
2299
|
+
const optionsAvailable = `${this.props.options.length} options available.`
|
|
2300
|
+
const nowOpen = !this.state.isShowingOptions
|
|
2301
|
+
? `List expanded. ${optionsAvailable}`
|
|
2302
|
+
: ''
|
|
2303
|
+
const option = this.getOptionById(id).label
|
|
2304
|
+
this.setState((state) => ({
|
|
2305
|
+
highlightedOptionId: id,
|
|
2306
|
+
inputValue: event.type === 'keydown' ? option : state.inputValue,
|
|
2307
|
+
announcement: `${option} ${nowOpen}`
|
|
2308
|
+
}))
|
|
2309
|
+
}
|
|
1296
2310
|
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
2311
|
+
handleSelectOption = (event, { id }) => {
|
|
2312
|
+
const option = this.getOptionById(id).label
|
|
2313
|
+
this.setState({
|
|
2314
|
+
selectedOptionId: id,
|
|
2315
|
+
inputValue: option,
|
|
2316
|
+
isShowingOptions: false,
|
|
2317
|
+
announcement: `"${option}" selected. List collapsed.`
|
|
2318
|
+
})
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
render() {
|
|
2322
|
+
const {
|
|
2323
|
+
inputValue,
|
|
2324
|
+
isShowingOptions,
|
|
2325
|
+
highlightedOptionId,
|
|
2326
|
+
selectedOptionId,
|
|
2327
|
+
announcement
|
|
2328
|
+
} = this.state
|
|
2329
|
+
|
|
2330
|
+
return (
|
|
2331
|
+
<div>
|
|
2332
|
+
<Select
|
|
2333
|
+
renderLabel="Option Icons"
|
|
2334
|
+
assistiveText="Use arrow keys to navigate options."
|
|
2335
|
+
inputValue={inputValue}
|
|
2336
|
+
isShowingOptions={isShowingOptions}
|
|
2337
|
+
onBlur={this.handleBlur}
|
|
2338
|
+
onRequestShowOptions={this.handleShowOptions}
|
|
2339
|
+
onRequestHideOptions={this.handleHideOptions}
|
|
2340
|
+
onRequestHighlightOption={this.handleHighlightOption}
|
|
2341
|
+
onRequestSelectOption={this.handleSelectOption}
|
|
2342
|
+
>
|
|
2343
|
+
{this.props.options.map((option) => {
|
|
2344
|
+
return (
|
|
2345
|
+
<Select.Option
|
|
2346
|
+
id={option.id}
|
|
2347
|
+
key={option.id}
|
|
2348
|
+
isHighlighted={option.id === highlightedOptionId}
|
|
2349
|
+
isSelected={option.id === selectedOptionId}
|
|
2350
|
+
renderBeforeLabel={option.renderBeforeLabel}
|
|
2351
|
+
>
|
|
2352
|
+
{option.label}
|
|
2353
|
+
</Select.Option>
|
|
2354
|
+
)
|
|
2355
|
+
})}
|
|
2356
|
+
</Select>
|
|
2357
|
+
<Alert
|
|
2358
|
+
liveRegion={() => document.getElementById('flash-messages')}
|
|
2359
|
+
liveRegionPoliteness="assertive"
|
|
2360
|
+
screenReaderOnly
|
|
2361
|
+
>
|
|
2362
|
+
{announcement}
|
|
2363
|
+
</Alert>
|
|
2364
|
+
</div>
|
|
2365
|
+
)
|
|
2366
|
+
}
|
|
1305
2367
|
}
|
|
1306
2368
|
|
|
1307
|
-
render
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
2369
|
+
render(
|
|
2370
|
+
<View>
|
|
2371
|
+
<SingleSelectExample
|
|
2372
|
+
options={[
|
|
2373
|
+
{
|
|
2374
|
+
id: 'opt1',
|
|
2375
|
+
label: 'Text',
|
|
2376
|
+
renderBeforeLabel: 'XY'
|
|
2377
|
+
},
|
|
2378
|
+
{
|
|
2379
|
+
id: 'opt2',
|
|
2380
|
+
label: 'Icon',
|
|
2381
|
+
renderBeforeLabel: <IconCheckSolid />
|
|
2382
|
+
},
|
|
2383
|
+
{
|
|
2384
|
+
id: 'opt3',
|
|
2385
|
+
label: 'Colored Icon',
|
|
2386
|
+
renderBeforeLabel: (props) => {
|
|
2387
|
+
let color = 'brand'
|
|
2388
|
+
if (props.isHighlighted) color = 'primary-inverse'
|
|
2389
|
+
if (props.isSelected) color = 'primary'
|
|
2390
|
+
if (props.isDisabled) color = 'warning'
|
|
2391
|
+
return <IconInstructureSolid color={color} />
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
]}
|
|
2395
|
+
/>
|
|
2396
|
+
</View>
|
|
2397
|
+
)
|
|
2398
|
+
```
|
|
2399
|
+
|
|
2400
|
+
- ```js
|
|
2401
|
+
const SingleSelectExample = ({ options }) => {
|
|
2402
|
+
const [inputValue, setInputValue] = useState(options[0].label)
|
|
2403
|
+
const [isShowingOptions, setIsShowingOptions] = useState(false)
|
|
2404
|
+
const [highlightedOptionId, setHighlightedOptionId] = useState(null)
|
|
2405
|
+
const [selectedOptionId, setSelectedOptionId] = useState(options[0].id)
|
|
2406
|
+
const [announcement, setAnnouncement] = useState(null)
|
|
2407
|
+
|
|
2408
|
+
const getOptionById = (queryId) => {
|
|
2409
|
+
return options.find(({ id }) => id === queryId)
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
const handleShowOptions = (event) => {
|
|
2413
|
+
setIsShowingOptions(true)
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2416
|
+
const handleHideOptions = (event) => {
|
|
2417
|
+
const option = getOptionById(selectedOptionId).label
|
|
2418
|
+
setIsShowingOptions(false)
|
|
2419
|
+
setHighlightedOptionId(null)
|
|
2420
|
+
setInputValue(selectedOptionId ? option : '')
|
|
2421
|
+
setAnnouncement('List collapsed.')
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
const handleBlur = (event) => {
|
|
2425
|
+
setHighlightedOptionId(null)
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2428
|
+
const handleHighlightOption = (event, { id }) => {
|
|
2429
|
+
event.persist()
|
|
2430
|
+
const optionsAvailable = `${options.length} options available.`
|
|
2431
|
+
const nowOpen = !isShowingOptions
|
|
2432
|
+
? `List expanded. ${optionsAvailable}`
|
|
2433
|
+
: ''
|
|
2434
|
+
const option = getOptionById(id).label
|
|
2435
|
+
setHighlightedOptionId(id)
|
|
2436
|
+
setInputValue(event.type === 'keydown' ? option : inputValue)
|
|
2437
|
+
setAnnouncement(`${option} ${nowOpen}`)
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
const handleSelectOption = (event, { id }) => {
|
|
2441
|
+
const option = getOptionById(id).label
|
|
2442
|
+
setSelectedOptionId(id)
|
|
2443
|
+
setInputValue(option)
|
|
2444
|
+
setIsShowingOptions(false)
|
|
2445
|
+
setAnnouncement(`"${option}" selected. List collapsed.`)
|
|
2446
|
+
}
|
|
1315
2447
|
|
|
1316
2448
|
return (
|
|
1317
2449
|
<div>
|
|
@@ -1320,13 +2452,13 @@ class SingleSelectExample extends React.Component {
|
|
|
1320
2452
|
assistiveText="Use arrow keys to navigate options."
|
|
1321
2453
|
inputValue={inputValue}
|
|
1322
2454
|
isShowingOptions={isShowingOptions}
|
|
1323
|
-
onBlur={
|
|
1324
|
-
onRequestShowOptions={
|
|
1325
|
-
onRequestHideOptions={
|
|
1326
|
-
onRequestHighlightOption={
|
|
1327
|
-
onRequestSelectOption={
|
|
2455
|
+
onBlur={handleBlur}
|
|
2456
|
+
onRequestShowOptions={handleShowOptions}
|
|
2457
|
+
onRequestHideOptions={handleHideOptions}
|
|
2458
|
+
onRequestHighlightOption={handleHighlightOption}
|
|
2459
|
+
onRequestSelectOption={handleSelectOption}
|
|
1328
2460
|
>
|
|
1329
|
-
{
|
|
2461
|
+
{options.map((option) => {
|
|
1330
2462
|
return (
|
|
1331
2463
|
<Select.Option
|
|
1332
2464
|
id={option.id}
|
|
@@ -1335,7 +2467,7 @@ class SingleSelectExample extends React.Component {
|
|
|
1335
2467
|
isSelected={option.id === selectedOptionId}
|
|
1336
2468
|
renderBeforeLabel={option.renderBeforeLabel}
|
|
1337
2469
|
>
|
|
1338
|
-
{
|
|
2470
|
+
{option.label}
|
|
1339
2471
|
</Select.Option>
|
|
1340
2472
|
)
|
|
1341
2473
|
})}
|
|
@@ -1345,43 +2477,42 @@ class SingleSelectExample extends React.Component {
|
|
|
1345
2477
|
liveRegionPoliteness="assertive"
|
|
1346
2478
|
screenReaderOnly
|
|
1347
2479
|
>
|
|
1348
|
-
{
|
|
2480
|
+
{announcement}
|
|
1349
2481
|
</Alert>
|
|
1350
2482
|
</div>
|
|
1351
2483
|
)
|
|
1352
2484
|
}
|
|
1353
|
-
}
|
|
1354
2485
|
|
|
1355
|
-
render(
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
2486
|
+
render(
|
|
2487
|
+
<View>
|
|
2488
|
+
<SingleSelectExample
|
|
2489
|
+
options={[
|
|
2490
|
+
{
|
|
2491
|
+
id: 'opt1',
|
|
2492
|
+
label: 'Text',
|
|
2493
|
+
renderBeforeLabel: 'XY'
|
|
2494
|
+
},
|
|
2495
|
+
{
|
|
2496
|
+
id: 'opt2',
|
|
2497
|
+
label: 'Icon',
|
|
2498
|
+
renderBeforeLabel: <IconCheckSolid />
|
|
2499
|
+
},
|
|
2500
|
+
{
|
|
2501
|
+
id: 'opt3',
|
|
2502
|
+
label: 'Colored Icon',
|
|
2503
|
+
renderBeforeLabel: (props) => {
|
|
2504
|
+
let color = 'brand'
|
|
2505
|
+
if (props.isHighlighted) color = 'primary-inverse'
|
|
2506
|
+
if (props.isSelected) color = 'primary'
|
|
2507
|
+
if (props.isDisabled) color = 'warning'
|
|
2508
|
+
return <IconInstructureSolid color={color} />
|
|
2509
|
+
}
|
|
1378
2510
|
}
|
|
1379
|
-
}
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
```
|
|
2511
|
+
]}
|
|
2512
|
+
/>
|
|
2513
|
+
</View>
|
|
2514
|
+
)
|
|
2515
|
+
```
|
|
1385
2516
|
|
|
1386
2517
|
#### Providing assistive text for screen readers
|
|
1387
2518
|
|