@instructure/ui-select 10.4.2-snapshot-11 → 10.4.2-snapshot-15

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.
@@ -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
- type: example
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
- class SingleSelectExample extends React.Component {
23
- state = {
24
- inputValue: this.props.options[0].label,
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
- getOptionById (queryId) {
32
- return this.props.options.find(({ id }) => id === queryId)
33
- }
31
+ handleShowOptions = (event) => {
32
+ this.setState({
33
+ isShowingOptions: true
34
+ })
35
+ }
34
36
 
35
- handleShowOptions = (event) => {
36
- this.setState({
37
- isShowingOptions: true
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
- handleHideOptions = (event) => {
42
- const { selectedOptionId } = this.state
43
- const option = this.getOptionById(selectedOptionId).label
44
- this.setState({
45
- isShowingOptions: false,
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
- handleBlur = (event) => {
53
- this.setState({
54
- highlightedOptionId: null
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
- handleHighlightOption = (event, { id }) => {
59
- event.persist()
60
- const optionsAvailable = `${this.props.options.length} options available.`
61
- const nowOpen = !this.state.isShowingOptions ? `List expanded. ${optionsAvailable}` : ''
62
- const option = this.getOptionById(id).label
63
- this.setState((state) => ({
64
- highlightedOptionId: id,
65
- inputValue: event.type === 'keydown' ? option : state.inputValue,
66
- announcement: `${option} ${nowOpen}`
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
- handleSelectOption = (event, { id }) => {
71
- const option = this.getOptionById(id).label
72
- this.setState({
73
- selectedOptionId: id,
74
- inputValue: option,
75
- isShowingOptions: false,
76
- announcement: `"${option}" selected. List collapsed.`
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
- const {
82
- inputValue,
83
- isShowingOptions,
84
- highlightedOptionId,
85
- selectedOptionId,
86
- announcement
87
- } = this.state
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={this.handleBlur}
97
- onRequestShowOptions={this.handleShowOptions}
98
- onRequestHideOptions={this.handleHideOptions}
99
- onRequestHighlightOption={this.handleHighlightOption}
100
- onRequestSelectOption={this.handleSelectOption}
206
+ onBlur={handleBlur}
207
+ onRequestShowOptions={handleShowOptions}
208
+ onRequestHideOptions={handleHideOptions}
209
+ onRequestHighlightOption={handleHighlightOption}
210
+ onRequestSelectOption={handleSelectOption}
101
211
  >
102
- {this.props.options.map((option) => {
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
- { option.label }
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
- { announcement }
230
+ {announcement}
121
231
  </Alert>
122
232
  </div>
123
233
  )
124
234
  }
125
- }
126
-
127
- render(
128
- <View>
129
- <SingleSelectExample
130
- options={[
131
- { id: 'opt1', label: 'Alaska' },
132
- { id: 'opt2', label: 'American Samoa' },
133
- { id: 'opt3', label: 'Arizona' },
134
- { id: 'opt4', label: 'Arkansas' },
135
- { id: 'opt5', label: 'California' },
136
- { id: 'opt6', label: 'Colorado' },
137
- { id: 'opt7', label: 'Connecticut' },
138
- { id: 'opt8', label: 'Delaware' },
139
- { id: 'opt9', label: 'District Of Columbia' },
140
- { id: 'opt10', label: 'Federated States Of Micronesia' },
141
- { id: 'opt11', label: 'Florida' },
142
- { id: 'opt12', label: 'Georgia (unavailable)' },
143
- { id: 'opt13', label: 'Guam' },
144
- { id: 'opt14', label: 'Hawaii' },
145
- { id: 'opt15', label: 'Idaho' },
146
- { id: 'opt16', label: 'Illinois' }
147
- ]}
148
- />
149
- </View>
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
- type: example
162
- ---
163
-
164
- class AutocompleteExample extends React.Component {
165
- state = {
166
- inputValue: '',
167
- isShowingOptions: false,
168
- highlightedOptionId: null,
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
- getOptionById (queryId) {
175
- return this.props.options.find(({ id }) => id === queryId)
176
- }
278
+ getOptionById(queryId) {
279
+ return this.props.options.find(({ id }) => id === queryId)
280
+ }
177
281
 
178
- getOptionsChangedMessage (newOptions) {
179
- let message = newOptions.length !== this.state.filteredOptions.length
180
- ? `${newOptions.length} options available.` // options changed, announce new total
181
- : null // options haven't changed, don't announce
182
- if (message && newOptions.length > 0) {
183
- // options still available
184
- if (this.state.highlightedOptionId !== newOptions[0].id) {
185
- // highlighted option hasn't been announced
186
- const option = this.getOptionById(newOptions[0].id).label
187
- message = `${option}. ${message}`
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
- filterOptions = (value) => {
194
- return this.props.options.filter(option => (
195
- option.label.toLowerCase().startsWith(value.toLowerCase())
196
- ))
197
- }
298
+ filterOptions = (value) => {
299
+ return this.props.options.filter((option) =>
300
+ option.label.toLowerCase().startsWith(value.toLowerCase())
301
+ )
302
+ }
198
303
 
199
- matchValue () {
200
- const {
201
- filteredOptions,
202
- inputValue,
203
- highlightedOptionId,
204
- selectedOptionId
205
- } = this.state
206
-
207
- // an option matching user input exists
208
- if (filteredOptions.length === 1) {
209
- const onlyOption = filteredOptions[0]
210
- // automatically select the matching option
211
- if (onlyOption.label.toLowerCase() === inputValue.toLowerCase()) {
212
- return {
213
- inputValue: onlyOption.label,
214
- selectedOptionId: onlyOption.id,
215
- filteredOptions: this.filterOptions('')
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
- // allow user to return to empty input and no selection
220
- if (inputValue.length === 0) {
221
- return { selectedOptionId: null }
222
- }
223
- // no match found, return selected option label to input
224
- if (selectedOptionId) {
225
- const selectedOption = this.getOptionById(selectedOptionId)
226
- return { inputValue: selectedOption.label }
227
- }
228
- // input value is from highlighted option, not user input
229
- // clear input, reset options
230
- if (highlightedOptionId) {
231
- if (inputValue === this.getOptionById(highlightedOptionId).label) {
232
- return {
233
- inputValue: '',
234
- filteredOptions: this.filterOptions('')
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
- handleShowOptions = (event) => {
241
- this.setState(({ filteredOptions }) => ({
242
- isShowingOptions: true,
243
- announcement: `List expanded. ${filteredOptions.length} options available.`
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
- handleHideOptions = (event) => {
248
- const { selectedOptionId, inputValue } = this.state
249
- this.setState({
250
- isShowingOptions: false,
251
- highlightedOptionId: null,
252
- announcement: 'List collapsed.',
253
- ...this.matchValue()
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
- handleBlur = (event) => {
258
- this.setState({ highlightedOptionId: null })
259
- }
362
+ handleBlur = (event) => {
363
+ this.setState({ highlightedOptionId: null })
364
+ }
260
365
 
261
- handleHighlightOption = (event, { id }) => {
262
- event.persist()
263
- const option = this.getOptionById(id)
264
- if (!option) return // prevent highlighting of empty option
265
- this.setState((state) => ({
266
- highlightedOptionId: id,
267
- inputValue: event.type === 'keydown' ? option.label : state.inputValue,
268
- announcement: option.label
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
- handleSelectOption = (event, { id }) => {
273
- const option = this.getOptionById(id)
274
- if (!option) return // prevent selecting of empty option
275
- this.setState({
276
- selectedOptionId: id,
277
- inputValue: option.label,
278
- isShowingOptions: false,
279
- filteredOptions: this.props.options,
280
- announcement: `${option.label} selected. List collapsed.`
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
- handleInputChange = (event) => {
285
- const value = event.target.value
286
- const newOptions = this.filterOptions(value)
287
- this.setState((state) => ({
288
- inputValue: value,
289
- filteredOptions: newOptions,
290
- highlightedOptionId: newOptions.length > 0 ? newOptions[0].id : null,
291
- isShowingOptions: true,
292
- selectedOptionId: value === '' ? null : state.selectedOptionId,
293
- announcement: this.getOptionsChangedMessage(newOptions)
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
- render () {
298
- const {
299
- inputValue,
300
- isShowingOptions,
301
- highlightedOptionId,
302
- selectedOptionId,
303
- filteredOptions,
304
- announcement
305
- } = this.state
402
+ render() {
403
+ const {
404
+ inputValue,
405
+ isShowingOptions,
406
+ highlightedOptionId,
407
+ selectedOptionId,
408
+ filteredOptions,
409
+ announcement
410
+ } = this.state
306
411
 
307
- return (
308
- <div>
309
- <Select
310
- renderLabel="Autocomplete"
311
- assistiveText="Type or use arrow keys to navigate options."
312
- placeholder="Start typing to search..."
313
- inputValue={inputValue}
314
- isShowingOptions={isShowingOptions}
315
- onBlur={this.handleBlur}
316
- onInputChange={this.handleInputChange}
317
- onRequestShowOptions={this.handleShowOptions}
318
- onRequestHideOptions={this.handleHideOptions}
319
- onRequestHighlightOption={this.handleHighlightOption}
320
- onRequestSelectOption={this.handleSelectOption}
321
- renderBeforeInput={<IconUserSolid inline={false} />}
322
- renderAfterInput={<IconSearchLine inline={false} />}
323
- >
324
- {filteredOptions.length > 0 ? filteredOptions.map((option) => {
325
- return (
326
- <Select.Option
327
- id={option.id}
328
- key={option.id}
329
- isHighlighted={option.id === highlightedOptionId}
330
- isSelected={option.id === selectedOptionId}
331
- isDisabled={option.disabled}
332
- renderBeforeLabel={!option.disabled ? IconUserSolid : IconUserLine}
333
- >
334
- {!option.disabled
335
- ? option.label
336
- : `${option.label} (unavailable)`
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
- <Select.Option
342
- id="empty-option"
343
- key="empty-option"
344
- >
345
- ---
346
- </Select.Option>
347
- )}
348
- </Select>
349
- <Alert
350
- liveRegion={() => document.getElementById('flash-messages')}
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
- getOptionById (queryId) {
406
- return this.props.options.find(({ id }) => id === queryId)
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
- getOptionsChangedMessage (newOptions) {
410
- let message = newOptions.length !== this.state.filteredOptions.length
411
- ? `${newOptions.length} options available.` // options changed, announce new total
412
- : null // options haven't changed, don't announce
413
- if (message && newOptions.length > 0) {
414
- // options still available
415
- if (this.state.highlightedOptionId !== newOptions[0].id) {
416
- // highlighted option hasn't been announced
417
- const option = this.getOptionById(newOptions[0].id).label
418
- message = `${option}. ${message}`
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
- filterOptions = (value) => {
425
- const { selectedOptionId } = this.state
426
- return this.props.options.filter(option => (option.label.toLowerCase().startsWith(value.toLowerCase())
427
- ))
428
- }
520
+ const filterOptions = (value) => {
521
+ return options.filter((option) =>
522
+ option.label.toLowerCase().startsWith(value.toLowerCase())
523
+ )
524
+ }
429
525
 
430
- matchValue () {
431
- const {
432
- filteredOptions,
433
- inputValue,
434
- highlightedOptionId,
435
- selectedOptionId
436
- } = this.state
437
-
438
- // an option matching user input exists
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
- // input value is from highlighted option, not user input
451
- // clear input, reset options
452
- if (highlightedOptionId) {
453
- if (inputValue === this.getOptionById(highlightedOptionId).label) {
454
- return {
455
- inputValue: '',
456
- filteredOptions: this.filterOptions('')
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
- handleBlur = (event) => {
474
- this.setState({
475
- highlightedOptionId: null
476
- })
477
- }
556
+ const handleShowOptions = (event) => {
557
+ setIsShowingOptions(true)
558
+ setAnnouncement(
559
+ `List expanded. ${filteredOptions.length} options available.`
560
+ )
561
+ }
478
562
 
479
- handleHighlightOption = (event, { id }) => {
480
- event.persist()
481
- const option = this.getOptionById(id)
482
- if (!option) return // prevent highlighting empty option
483
- this.setState((state) => ({
484
- highlightedOptionId: id,
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
- handleSelectOption = (event, { id }) => {
491
- const option = this.getOptionById(id)
492
- if (!option) return // prevent selecting of empty option
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
- handleInputChange = (event) => {
504
- const value = event.target.value
505
- const newOptions = this.filterOptions(value)
506
- this.setState({
507
- inputValue: value,
508
- filteredOptions: newOptions,
509
- highlightedOptionId: newOptions.length > 0 ? newOptions[0].id : null,
510
- isShowingOptions: true,
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
- handleKeyDown = (event) => {
516
- const { selectedOptionId, inputValue } = this.state
517
- if (event.keyCode === 8) {
518
- // when backspace key is pressed
519
- if (inputValue === '' && selectedOptionId.length > 0) {
520
- // remove last selected option, if input has no entered text
521
- this.setState((state) => ({
522
- highlightedOptionId: null,
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
- render () {
559
- const {
560
- inputValue,
561
- isShowingOptions,
562
- highlightedOptionId,
563
- selectedOptionId,
564
- filteredOptions,
565
- announcement
566
- } = this.state
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="Multiple Select"
572
- assistiveText="Type or use arrow keys to navigate options. Multiple selections allowed."
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
- inputRef={(el) => this.inputRef = el}
576
- onBlur={this.handleBlur}
577
- onInputChange={this.handleInputChange}
578
- onRequestShowOptions={this.handleShowOptions}
579
- onRequestHideOptions={this.handleHideOptions}
580
- onRequestHighlightOption={this.handleHighlightOption}
581
- onRequestSelectOption={this.handleSelectOption}
582
- onKeyDown={this.handleKeyDown}
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 ? filteredOptions.map((option, index) => {
586
- if (selectedOptionId.indexOf(option.id) === -1) {
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
- { option.label }
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
- { announcement }
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
- #### Composing option groups
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
- 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.
682
+ #### Highlighting and selecting options
647
683
 
648
- ```javascript
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
- class GroupSelectExample extends React.Component {
654
- state = {
655
- inputValue: this.props.options['Western'][0].label,
656
- isShowingOptions: false,
657
- highlightedOptionId: null,
658
- selectedOptionId: this.props.options['Western'][0].id,
659
- announcement: null
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
- getOptionById (id) {
663
- const { options } = this.props
664
- let match = null
665
- Object.keys(options).forEach((key, index) => {
666
- for (let i = 0; i < options[key].length; i++) {
667
- const option = options[key][i]
668
- if (id === option.id) {
669
- // return group property with the object just to make it easier
670
- // to check which group the option belongs to
671
- match = { ...option, group: key }
672
- break
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
- return match
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
- handleShowOptions = (event) => {
688
- this.setState({
689
- isShowingOptions: true,
690
- highlightedOptionId: null
691
- })
692
- }
717
+ filterOptions = (value) => {
718
+ return this.props.options.filter((option) =>
719
+ option.label.toLowerCase().startsWith(value.toLowerCase())
720
+ )
721
+ }
693
722
 
694
- handleHideOptions = (event) => {
695
- const { selectedOptionId } = this.state
696
- this.setState({
697
- isShowingOptions: false,
698
- highlightedOptionId: null,
699
- inputValue: this.getOptionById(selectedOptionId).label
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
- handleBlur = (event) => {
704
- this.setState({
705
- highlightedOptionId: null
706
- })
707
- }
755
+ handleShowOptions = (event) => {
756
+ this.setState({ isShowingOptions: true })
757
+ }
708
758
 
709
- handleHighlightOption = (event, { id }) => {
710
- event.persist()
711
- const newOption = this.getOptionById(id)
712
- this.setState((state) => ({
713
- highlightedOptionId: id,
714
- inputValue: event.type === 'keydown' ? newOption.label : state.inputValue,
715
- announcement: this.getGroupChangedMessage(newOption)
716
- }))
717
- }
759
+ handleHideOptions = (event) => {
760
+ this.setState({
761
+ isShowingOptions: false,
762
+ ...this.matchValue()
763
+ })
764
+ }
718
765
 
719
- handleSelectOption = (event, { id }) => {
720
- this.setState({
721
- selectedOptionId: id,
722
- inputValue: this.getOptionById(id).label,
723
- isShowingOptions: false,
724
- announcement: `${this.getOptionById(id).label} selected.`
725
- })
726
- }
766
+ handleBlur = (event) => {
767
+ this.setState({
768
+ highlightedOptionId: null
769
+ })
770
+ }
727
771
 
728
- renderLabel (text, variant) {
729
- return (
730
- <span>
731
- <Badge
732
- type="notification"
733
- variant={variant}
734
- standalone
735
- margin="0 x-small xxx-small 0"
736
- />
737
- { text }
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
- renderGroup () {
743
- const { options } = this.props
744
- const { highlightedOptionId, selectedOptionId } = this.state
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
- <Select.Group key={index} renderLabel={this.renderLabel(key, badgeVariant)}>
750
- {options[key].map((option) => (
751
- <Select.Option
752
- key={option.id}
753
- id={option.id}
754
- isHighlighted={option.id === highlightedOptionId}
755
- isSelected={option.id === selectedOptionId}
756
- >
757
- { option.label }
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.Group>
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
- const {
767
- inputValue,
768
- isShowingOptions,
769
- highlightedOptionId,
770
- selectedOptionId,
771
- filteredOptions,
772
- announcement
773
- } = this.state
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={this.handleBlur}
783
- onRequestShowOptions={this.handleShowOptions}
784
- onRequestHideOptions={this.handleHideOptions}
785
- onRequestHighlightOption={this.handleHighlightOption}
786
- onRequestSelectOption={this.handleSelectOption}
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={this.getOptionById(selectedOptionId).group === 'Eastern'
791
- ? 'success'
792
- : 'primary'
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
- {this.renderGroup()}
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
- { announcement }
1483
+ {announcement}
807
1484
  </Alert>
808
1485
  </div>
809
1486
  )
810
1487
  }
811
- }
812
-
813
- render(
814
- <View>
815
- <GroupSelectExample
816
- options={{
817
- 'Western': [
818
- { id: 'opt5', label: 'Alaska' },
819
- { id: 'opt6', label: 'California' },
820
- { id: 'opt7', label: 'Colorado' },
821
- { id: 'opt8', label: 'Idaho' }
822
- ],
823
- 'Eastern': [
824
- { id: 'opt1', label: 'Alabama' },
825
- { id: 'opt2', label: 'Connecticut' },
826
- { id: 'opt3', label: 'Delaware' },
827
- { id: '4', label: 'Illinois' }
828
- ]
829
- }}
830
- />
831
- </View>
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
- type: example
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
- class GroupSelectAutocompleteExample extends React.Component {
845
- state = {
846
- inputValue: '',
847
- isShowingOptions: false,
848
- highlightedOptionId: null,
849
- selectedOptionId: null,
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
- getOptionById (id) {
855
- const options = this.props.options
856
- return Object.values(options)
857
- .flat()
858
- .find((o) => o?.id === id);
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
- filterOptions (value, options) {
862
- const filteredOptions = {};
863
- Object.keys(options).forEach((key) => {
864
- filteredOptions[key] = options[key]?.filter(
865
- (option) =>
866
- option.label.toLowerCase().includes(value.toLowerCase())
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
- handleHideOptions = (event) => {
883
- const { selectedOptionId } = this.state
884
- this.setState({
885
- isShowingOptions: false,
886
- highlightedOptionId: null
887
- })
888
- }
1553
+ handleHideOptions = (event) => {
1554
+ const { selectedOptionId } = this.state
1555
+ this.setState({
1556
+ isShowingOptions: false,
1557
+ highlightedOptionId: null
1558
+ })
1559
+ }
889
1560
 
890
- handleBlur = (event) => {
891
- this.setState({
892
- highlightedOptionId: null
893
- })
894
- }
1561
+ handleBlur = (event) => {
1562
+ this.setState({
1563
+ highlightedOptionId: null
1564
+ })
1565
+ }
895
1566
 
896
- handleHighlightOption = (event, { id }) => {
897
- event.persist()
898
- const option = this.getOptionById(id)
899
- setTimeout(() => {
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
- }, 0)
904
- this.setState((state) => ({
905
- highlightedOptionId: id,
906
- }))
907
- }
1602
+ }
908
1603
 
909
- handleSelectOption = (event, { id }) => {
910
- const option = this.getOptionById(id)
911
- if (!option) return // prevent selecting of empty option
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
- handleInputChange = (event) => {
922
- const value = event.target.value
923
- const newOptions = this.filterOptions(value, this.props.options)
924
- this.setState((state) => ({
925
- inputValue: value,
926
- filteredOptions: newOptions,
927
- highlightedOptionId: newOptions.length > 0 ? newOptions[0].id : null,
928
- isShowingOptions: true,
929
- selectedOptionId: value === '' ? null : state.selectedOptionId,
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
- renderGroup () {
934
- const filteredOptions = this.state.filteredOptions
935
- const { highlightedOptionId, selectedOptionId } = this.state
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
- <Select.Group key={index} renderLabel={key}>
940
- {filteredOptions[key].map((option) => (
941
- <Select.Option
942
- key={option.id}
943
- id={option.id}
944
- isHighlighted={option.id === highlightedOptionId}
945
- isSelected={option.id === selectedOptionId}
946
- >
947
- { option.label }
948
- </Select.Option>
949
- ))}
950
- </Select.Group>
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
- renderScreenReaderHelper () {
956
- const announcement = this.state.announcement
957
- return window.safari && (
958
- <ScreenReaderContent>
959
- <span role="alert" aria-live="assertive">{announcement}</span>
960
- </ScreenReaderContent>
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
- render () {
965
- const {
966
- inputValue,
967
- isShowingOptions,
968
- highlightedOptionId,
969
- selectedOptionId,
970
- filteredOptions
971
- } = this.state
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={this.handleBlur}
982
- onInputChange={this.handleInputChange}
983
- onRequestShowOptions={this.handleShowOptions}
984
- onRequestHideOptions={this.handleHideOptions}
985
- onRequestHighlightOption={this.handleHighlightOption}
986
- onRequestSelectOption={this.handleSelectOption}
1803
+ onBlur={handleBlur}
1804
+ onInputChange={handleInputChange}
1805
+ onRequestShowOptions={handleShowOptions}
1806
+ onRequestHideOptions={handleHideOptions}
1807
+ onRequestHighlightOption={handleHighlightOption}
1808
+ onRequestSelectOption={handleSelectOption}
987
1809
  >
988
- {this.renderGroup()}
1810
+ {renderGroup()}
989
1811
  </Select>
990
- {this.renderScreenReaderHelper()}
1812
+ {renderScreenReaderHelper()}
991
1813
  </div>
992
1814
  )
993
1815
  }
994
- }
995
-
996
- render(
997
- <View>
998
- <GroupSelectAutocompleteExample
999
- options={{
1000
- 'Western': [
1001
- { id: 'opt5', label: 'Alaska' },
1002
- { id: 'opt6', label: 'California' },
1003
- { id: 'opt7', label: 'Colorado' },
1004
- { id: 'opt8', label: 'Idaho' }
1005
- ],
1006
- 'Eastern': [
1007
- { id: 'opt1', label: 'Alabama' },
1008
- { id: 'opt2', label: 'Connecticut' },
1009
- { id: 'opt3', label: 'Delaware' },
1010
- { id: '4', label: 'Illinois' }
1011
- ]
1012
- }}
1013
- />
1014
- </View>
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
- type: example
1025
- ---
1026
-
1027
- class AsyncExample extends React.Component {
1028
- state = {
1029
- inputValue: '',
1030
- isShowingOptions: false,
1031
- isLoading: false,
1032
- highlightedOptionId: null,
1033
- selectedOptionId: null,
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
- timeoutId = null
1856
+ timeoutId = null
1040
1857
 
1041
- getOptionById (queryId) {
1042
- return this.state.filteredOptions.find(({ id }) => id === queryId)
1043
- }
1858
+ getOptionById(queryId) {
1859
+ return this.state.filteredOptions.find(({ id }) => id === queryId)
1860
+ }
1044
1861
 
1045
- filterOptions = (value) => {
1046
- return this.props.options.filter(option => (
1047
- option.label.toLowerCase().startsWith(value.toLowerCase())
1048
- ))
1049
- }
1862
+ filterOptions = (value) => {
1863
+ return this.props.options.filter((option) =>
1864
+ option.label.toLowerCase().startsWith(value.toLowerCase())
1865
+ )
1866
+ }
1050
1867
 
1051
- matchValue () {
1052
- const {
1053
- filteredOptions,
1054
- inputValue,
1055
- selectedOptionId,
1056
- selectedOptionLabel
1057
- } = this.state
1058
-
1059
- // an option matching user input exists
1060
- if (filteredOptions.length === 1) {
1061
- const onlyOption = filteredOptions[0]
1062
- // automatically select the matching option
1063
- if (onlyOption.label.toLowerCase() === inputValue.toLowerCase()) {
1064
- return {
1065
- inputValue: onlyOption.label,
1066
- selectedOptionId: onlyOption.id
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
- handleBlur = (event) => {
1097
- this.setState({ highlightedOptionId: null })
1098
- }
1897
+ handleShowOptions = (event) => {
1898
+ this.setState(({ filteredOptions }) => ({
1899
+ isShowingOptions: true
1900
+ }))
1901
+ }
1099
1902
 
1100
- handleHighlightOption = (event, { id }) => {
1101
- event.persist()
1102
- const option = this.getOptionById(id)
1103
- if (!option) return // prevent highlighting of empty option
1104
- this.setState((state) => ({
1105
- highlightedOptionId: id,
1106
- inputValue: event.type === 'keydown' ? option.label : state.inputValue,
1107
- announcement: option.label
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
- handleSelectOption = (event, { id }) => {
1112
- const option = this.getOptionById(id)
1113
- if (!option) return // prevent selecting of empty option
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
- handleInputChange = (event) => {
1125
- const value = event.target.value
1126
- clearTimeout(this.timeoutId)
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
- if (!value || value === '') {
1928
+ handleSelectOption = (event, { id }) => {
1929
+ const option = this.getOptionById(id)
1930
+ if (!option) return // prevent selecting of empty option
1129
1931
  this.setState({
1130
- isLoading: false,
1131
- inputValue: value,
1132
- isShowingOptions: true,
1133
- selectedOptionId: null,
1134
- selectedOptionLabel: null,
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
- this.timeoutId = setTimeout(() => {
1148
- const newOptions = this.filterOptions(value)
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
- announcement: `${newOptions.length} options available.`
1948
+ inputValue: value,
1949
+ isShowingOptions: true,
1950
+ selectedOptionId: null,
1951
+ selectedOptionLabel: null,
1952
+ filteredOptions: []
1153
1953
  })
1154
- }, 1500)
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
- const {
1160
- inputValue,
1161
- isShowingOptions,
1162
- isLoading,
1163
- highlightedOptionId,
1164
- selectedOptionId,
1165
- filteredOptions,
1166
- announcement
1167
- } = this.state
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={this.handleBlur}
1177
- onInputChange={this.handleInputChange}
1178
- onRequestShowOptions={this.handleShowOptions}
1179
- onRequestHideOptions={this.handleHideOptions}
1180
- onRequestHighlightOption={this.handleHighlightOption}
1181
- onRequestSelectOption={this.handleSelectOption}
2184
+ onBlur={handleBlur}
2185
+ onInputChange={handleInputChange}
2186
+ onRequestShowOptions={handleShowOptions}
2187
+ onRequestHideOptions={handleHideOptions}
2188
+ onRequestHighlightOption={handleHighlightOption}
2189
+ onRequestSelectOption={handleSelectOption}
1182
2190
  >
1183
- {filteredOptions.length > 0 ? filteredOptions.map((option) => {
1184
- return (
1185
- <Select.Option
1186
- id={option.id}
1187
- key={option.id}
1188
- isHighlighted={option.id === highlightedOptionId}
1189
- isSelected={option.id === selectedOptionId}
1190
- isDisabled={option.disabled}
1191
- renderBeforeLabel={!option.disabled ? IconUserSolid : IconUserLine}
1192
- >
1193
- {option.label}
1194
- </Select.Option>
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
- ? <Spinner renderTitle="Loading" size="x-small" />
1200
- : inputValue !== '' ? 'No results' : 'Type to search'}
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
- { announcement }
2225
+ {announcement}
1210
2226
  </Alert>
1211
2227
  </div>
1212
2228
  )
1213
2229
  }
1214
- }
1215
-
1216
- render(
1217
- <View>
1218
- <AsyncExample
1219
- options={[
1220
- { id: 'opt0', label: 'Aaron Aaronson' },
1221
- { id: 'opt1', label: 'Amber Murphy' },
1222
- { id: 'opt2', label: 'Andrew Miller' },
1223
- { id: 'opt3', label: 'Barbara Ward' },
1224
- { id: 'opt4', label: 'Byron Cranston', disabled: true },
1225
- { id: 'opt5', label: 'Dennis Reynolds' },
1226
- { id: 'opt6', label: 'Dee Reynolds' },
1227
- { id: 'opt7', label: 'Ezra Betterthan' },
1228
- { id: 'opt8', label: 'Jeff Spicoli' },
1229
- { id: 'opt9', label: 'Joseph Smith' },
1230
- { id: 'opt10', label: 'Jasmine Diaz' },
1231
- { id: 'opt11', label: 'Martin Harris' },
1232
- { id: 'opt12', label: 'Michael Morgan', disabled: true },
1233
- { id: 'opt13', label: 'Michelle Rodriguez' },
1234
- { id: 'opt14', label: 'Ziggy Stardust' },
1235
- ]}
1236
- />
1237
- </View>
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
- ```javascript
1246
- ---
1247
- type: example
1248
- ---
1249
- class SingleSelectExample extends React.Component {
1250
- state = {
1251
- inputValue: this.props.options[0].label,
1252
- isShowingOptions: false,
1253
- highlightedOptionId: null,
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
- getOptionById (queryId) {
1259
- return this.props.options.find(({ id }) => id === queryId)
1260
- }
2270
+ getOptionById(queryId) {
2271
+ return this.props.options.find(({ id }) => id === queryId)
2272
+ }
1261
2273
 
1262
- handleShowOptions = (event) => {
1263
- this.setState({
1264
- isShowingOptions: true
1265
- })
1266
- }
2274
+ handleShowOptions = (event) => {
2275
+ this.setState({
2276
+ isShowingOptions: true
2277
+ })
2278
+ }
1267
2279
 
1268
- handleHideOptions = (event) => {
1269
- const { selectedOptionId } = this.state
1270
- const option = this.getOptionById(selectedOptionId).label
1271
- this.setState({
1272
- isShowingOptions: false,
1273
- highlightedOptionId: null,
1274
- inputValue: selectedOptionId ? option : '',
1275
- announcement: 'List collapsed.'
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
- handleBlur = (event) => {
1280
- this.setState({
1281
- highlightedOptionId: null
1282
- })
1283
- }
2291
+ handleBlur = (event) => {
2292
+ this.setState({
2293
+ highlightedOptionId: null
2294
+ })
2295
+ }
1284
2296
 
1285
- handleHighlightOption = (event, { id }) => {
1286
- event.persist()
1287
- const optionsAvailable = `${this.props.options.length} options available.`
1288
- const nowOpen = !this.state.isShowingOptions ? `List expanded. ${optionsAvailable}` : ''
1289
- const option = this.getOptionById(id).label
1290
- this.setState((state) => ({
1291
- highlightedOptionId: id,
1292
- inputValue: event.type === 'keydown' ? option : state.inputValue,
1293
- announcement: `${option} ${nowOpen}`
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
- handleSelectOption = (event, { id }) => {
1298
- const option = this.getOptionById(id).label
1299
- this.setState({
1300
- selectedOptionId: id,
1301
- inputValue: option,
1302
- isShowingOptions: false,
1303
- announcement: `"${option}" selected. List collapsed.`
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
- const {
1309
- inputValue,
1310
- isShowingOptions,
1311
- highlightedOptionId,
1312
- selectedOptionId,
1313
- announcement
1314
- } = this.state
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={this.handleBlur}
1324
- onRequestShowOptions={this.handleShowOptions}
1325
- onRequestHideOptions={this.handleHideOptions}
1326
- onRequestHighlightOption={this.handleHighlightOption}
1327
- onRequestSelectOption={this.handleSelectOption}
2455
+ onBlur={handleBlur}
2456
+ onRequestShowOptions={handleShowOptions}
2457
+ onRequestHideOptions={handleHideOptions}
2458
+ onRequestHighlightOption={handleHighlightOption}
2459
+ onRequestSelectOption={handleSelectOption}
1328
2460
  >
1329
- {this.props.options.map((option) => {
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
- { option.label }
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
- { announcement }
2480
+ {announcement}
1349
2481
  </Alert>
1350
2482
  </div>
1351
2483
  )
1352
2484
  }
1353
- }
1354
2485
 
1355
- render(
1356
- <View>
1357
- <SingleSelectExample
1358
- options={[
1359
- {
1360
- id: 'opt1',
1361
- label: 'Text',
1362
- renderBeforeLabel: 'XY'
1363
- },
1364
- {
1365
- id: 'opt2',
1366
- label: 'Icon',
1367
- renderBeforeLabel: <IconCheckSolid />
1368
- },
1369
- {
1370
- id: 'opt3',
1371
- label: 'Colored Icon',
1372
- renderBeforeLabel: (props) => {
1373
- let color = 'brand'
1374
- if (props.isHighlighted) color = 'primary-inverse'
1375
- if (props.isSelected) color = 'primary'
1376
- if (props.isDisabled) color = 'warning'
1377
- return <IconInstructureSolid color={color} />
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
- </View>
1383
- )
1384
- ```
2511
+ ]}
2512
+ />
2513
+ </View>
2514
+ )
2515
+ ```
1385
2516
 
1386
2517
  #### Providing assistive text for screen readers
1387
2518