@instructure/ui-select 11.7.2-snapshot-47 → 11.7.2-snapshot-49

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.
Files changed (44) hide show
  1. package/CHANGELOG.md +12 -2
  2. package/es/Select/v2/Group/index.js +47 -0
  3. package/es/Select/v2/Group/props.js +26 -0
  4. package/es/Select/v2/Option/index.js +51 -0
  5. package/es/Select/v2/Option/props.js +26 -0
  6. package/es/Select/v2/index.js +704 -0
  7. package/es/Select/v2/props.js +35 -0
  8. package/es/Select/v2/styles.js +41 -0
  9. package/es/exports/b.js +26 -0
  10. package/lib/Select/v2/Group/index.js +53 -0
  11. package/lib/Select/v2/Group/props.js +31 -0
  12. package/lib/Select/v2/Option/index.js +57 -0
  13. package/lib/Select/v2/Option/props.js +31 -0
  14. package/lib/Select/v2/index.js +714 -0
  15. package/lib/Select/v2/props.js +40 -0
  16. package/lib/Select/v2/styles.js +47 -0
  17. package/lib/exports/b.js +26 -0
  18. package/package.json +29 -29
  19. package/src/Select/v2/Group/index.tsx +52 -0
  20. package/src/Select/v2/Group/props.ts +48 -0
  21. package/src/Select/v2/Option/index.tsx +56 -0
  22. package/src/Select/v2/Option/props.ts +82 -0
  23. package/src/Select/v2/README.md +1342 -0
  24. package/src/Select/v2/index.tsx +897 -0
  25. package/src/Select/v2/props.ts +333 -0
  26. package/src/Select/v2/styles.ts +48 -0
  27. package/src/exports/b.ts +31 -0
  28. package/tsconfig.build.tsbuildinfo +1 -1
  29. package/types/Select/v2/Group/index.d.ts +21 -0
  30. package/types/Select/v2/Group/index.d.ts.map +1 -0
  31. package/types/Select/v2/Group/props.d.ts +19 -0
  32. package/types/Select/v2/Group/props.d.ts.map +1 -0
  33. package/types/Select/v2/Option/index.d.ts +28 -0
  34. package/types/Select/v2/Option/index.d.ts.map +1 -0
  35. package/types/Select/v2/Option/props.d.ts +43 -0
  36. package/types/Select/v2/Option/props.d.ts.map +1 -0
  37. package/types/Select/v2/index.d.ts +138 -0
  38. package/types/Select/v2/index.d.ts.map +1 -0
  39. package/types/Select/v2/props.d.ts +206 -0
  40. package/types/Select/v2/props.d.ts.map +1 -0
  41. package/types/Select/v2/styles.d.ts +12 -0
  42. package/types/Select/v2/styles.d.ts.map +1 -0
  43. package/types/exports/b.d.ts +7 -0
  44. package/types/exports/b.d.ts.map +1 -0
@@ -0,0 +1,1342 @@
1
+ ---
2
+ describes: Select
3
+ ---
4
+
5
+ `Select` is an accessible, custom styled combobox component for inputting a variety of data types.
6
+
7
+ - It behaves similar to [Popover](Popover) but provides additional semantic markup and focus behavior as a form input.
8
+ - It should not be used for navigation or as a list of actions/functions. (see [Menu](Menu)).
9
+ - It can behave like a `<select>` element or implement autocomplete behavior.
10
+
11
+ > Notes:
12
+ >
13
+ > - Before implementing Select, see if a [SimpleSelect](SimpleSelect) will suffice.
14
+ > - The `id` prop on options must be globally unique, it will be translated to an `id` prop in the DOM.
15
+
16
+ #### Managing state for a Select
17
+
18
+ `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.
19
+
20
+ ```js
21
+ ---
22
+ type: example
23
+ ---
24
+ const SingleSelectExample = ({ options }) => {
25
+ const [inputValue, setInputValue] = useState(options[0].label)
26
+ const [isShowingOptions, setIsShowingOptions] = useState(false)
27
+ const [highlightedOptionId, setHighlightedOptionId] = useState(null)
28
+ const [selectedOptionId, setSelectedOptionId] = useState(options[0].id)
29
+ const [announcement, setAnnouncement] = useState(null)
30
+ const inputRef = useRef()
31
+
32
+ const focusInput = () => {
33
+ if (inputRef.current) {
34
+ inputRef.current.blur()
35
+ inputRef.current.focus()
36
+ }
37
+ }
38
+
39
+ const getOptionById = (queryId) => {
40
+ return options.find(({ id }) => id === queryId)
41
+ }
42
+
43
+ const handleShowOptions = (event) => {
44
+ setIsShowingOptions(true)
45
+ if (inputValue || selectedOptionId || options.length === 0) return
46
+
47
+ if ('key' in event) {
48
+ switch (event.key) {
49
+ case 'ArrowDown':
50
+ return handleHighlightOption(event, { id: options[0].id })
51
+ case 'ArrowUp':
52
+ return handleHighlightOption(event, {
53
+ id: options[options.length - 1].id
54
+ })
55
+ }
56
+ }
57
+ }
58
+
59
+ const handleHideOptions = (event) => {
60
+ const option = getOptionById(selectedOptionId)?.label
61
+ setIsShowingOptions(false)
62
+ setHighlightedOptionId(null)
63
+ setSelectedOptionId(selectedOptionId ? option : '')
64
+ setAnnouncement('List collapsed.')
65
+ }
66
+
67
+ const handleBlur = (event) => {
68
+ setHighlightedOptionId(null)
69
+ }
70
+
71
+ const handleHighlightOption = (event, { id }) => {
72
+ event.persist()
73
+ const optionsAvailable = `${options.length} options available.`
74
+ const nowOpen = !isShowingOptions
75
+ ? `List expanded. ${optionsAvailable}`
76
+ : ''
77
+ const option = getOptionById(id)?.label
78
+ setHighlightedOptionId(id)
79
+ setInputValue(inputValue)
80
+ setAnnouncement(`${option} ${nowOpen}`)
81
+ }
82
+
83
+ const handleSelectOption = (event, { id }) => {
84
+ focusInput()
85
+ const option = getOptionById(id)?.label
86
+ setSelectedOptionId(id)
87
+ setInputValue(option)
88
+ setIsShowingOptions(false)
89
+ setAnnouncement(`"${option}" selected. List collapsed.`)
90
+ }
91
+
92
+ return (
93
+ <div>
94
+ <Select
95
+ renderLabel="Single Select"
96
+ assistiveText="Use arrow keys to navigate options."
97
+ inputValue={inputValue}
98
+ isShowingOptions={isShowingOptions}
99
+ onBlur={handleBlur}
100
+ onRequestShowOptions={handleShowOptions}
101
+ onRequestHideOptions={handleHideOptions}
102
+ onRequestHighlightOption={handleHighlightOption}
103
+ onRequestSelectOption={handleSelectOption}
104
+ inputRef={(el) => {
105
+ inputRef.current = el
106
+ }}
107
+ >
108
+ {options.map((option) => {
109
+ return (
110
+ <Select.Option
111
+ id={option.id}
112
+ key={option.id}
113
+ isHighlighted={option.id === highlightedOptionId}
114
+ isSelected={option.id === selectedOptionId}
115
+ >
116
+ {option.label}
117
+ </Select.Option>
118
+ )
119
+ })}
120
+ </Select>
121
+ </div>
122
+ )
123
+ }
124
+ render(
125
+ <View>
126
+ <SingleSelectExample
127
+ options={[
128
+ { id: 'opt1', label: 'Alaska' },
129
+ { id: 'opt2', label: 'American Samoa' },
130
+ { id: 'opt3', label: 'Arizona' },
131
+ { id: 'opt4', label: 'Arkansas' },
132
+ { id: 'opt5', label: 'California' },
133
+ { id: 'opt6', label: 'Colorado' },
134
+ { id: 'opt7', label: 'Connecticut' },
135
+ { id: 'opt8', label: 'Delaware' },
136
+ { id: 'opt9', label: 'District Of Columbia' },
137
+ { id: 'opt10', label: 'Federated States Of Micronesia' },
138
+ { id: 'opt11', label: 'Florida' },
139
+ { id: 'opt12', label: 'Georgia (unavailable)' },
140
+ { id: 'opt13', label: 'Guam' },
141
+ { id: 'opt14', label: 'Hawaii' },
142
+ { id: 'opt15', label: 'Idaho' },
143
+ { id: 'opt16', label: 'Illinois' }
144
+ ]}
145
+ />
146
+ </View>
147
+ )
148
+ ```
149
+
150
+ #### Providing autocomplete behavior
151
+
152
+ It's best practice to always provide autocomplete functionality to help users make a selection. The example below demonstrates one method of filtering options based on user input, but this logic should be customized to what works best for the application.
153
+
154
+ > 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.
155
+
156
+ ```js
157
+ ---
158
+ type: example
159
+ ---
160
+ const AutocompleteExample = ({ options }) => {
161
+ const [inputValue, setInputValue] = useState('')
162
+ const [isShowingOptions, setIsShowingOptions] = useState(false)
163
+ const [highlightedOptionId, setHighlightedOptionId] = useState(null)
164
+ const [selectedOptionId, setSelectedOptionId] = useState(null)
165
+ const [filteredOptions, setFilteredOptions] = useState(options)
166
+ const [announcement, setAnnouncement] = useState(null)
167
+ const inputRef = useRef()
168
+
169
+ const focusInput = () => {
170
+ if (inputRef.current) {
171
+ inputRef.current.blur()
172
+ inputRef.current.focus()
173
+ }
174
+ }
175
+
176
+ const getOptionById = (queryId) => {
177
+ return options.find(({ id }) => id === queryId)
178
+ }
179
+
180
+ const getOptionsChangedMessage = (newOptions) => {
181
+ let message =
182
+ newOptions.length !== filteredOptions.length
183
+ ? `${newOptions.length} options available.` // options changed, announce new total
184
+ : null // options haven't changed, don't announce
185
+ if (message && newOptions.length > 0) {
186
+ // options still available
187
+ if (highlightedOptionId !== newOptions[0].id) {
188
+ // highlighted option hasn't been announced
189
+ const option = getOptionById(newOptions[0].id).label
190
+ message = `${option}. ${message}`
191
+ }
192
+ }
193
+ return message
194
+ }
195
+
196
+ const filterOptions = (value) => {
197
+ return options.filter((option) =>
198
+ option.label.toLowerCase().startsWith(value.toLowerCase())
199
+ )
200
+ }
201
+
202
+ const matchValue = () => {
203
+ // an option matching user input exists
204
+ if (filteredOptions.length === 1) {
205
+ const onlyOption = filteredOptions[0]
206
+ // automatically select the matching option
207
+ if (onlyOption.label.toLowerCase() === inputValue.toLowerCase()) {
208
+ setInputValue(onlyOption.label)
209
+ setSelectedOptionId(onlyOption.id)
210
+ setFilteredOptions(filterOptions(''))
211
+ }
212
+ }
213
+ // allow user to return to empty input and no selection
214
+ else if (inputValue.length === 0) {
215
+ setSelectedOptionId(null)
216
+ }
217
+ // no match found, return selected option label to input
218
+ else if (selectedOptionId) {
219
+ const selectedOption = getOptionById(selectedOptionId)
220
+ setInputValue(selectedOption.label)
221
+ }
222
+ // input value is from highlighted option, not user input
223
+ // clear input, reset options
224
+ else if (highlightedOptionId) {
225
+ if (inputValue === getOptionById(highlightedOptionId).label) {
226
+ setInputValue('')
227
+ setFilteredOptions(filterOptions(''))
228
+ }
229
+ }
230
+ }
231
+
232
+ const handleShowOptions = (event) => {
233
+ setIsShowingOptions(true)
234
+ setAnnouncement(
235
+ `List expanded. ${filteredOptions.length} options available.`
236
+ )
237
+ if (inputValue || selectedOptionId || options.length === 0) return
238
+
239
+ if ('key' in event) {
240
+ switch (event.key) {
241
+ case 'ArrowDown':
242
+ return handleHighlightOption(event, { id: options[0].id })
243
+ case 'ArrowUp':
244
+ return handleHighlightOption(event, {
245
+ id: options[options.length - 1].id
246
+ })
247
+ }
248
+ }
249
+ }
250
+
251
+ const handleHideOptions = (event) => {
252
+ setIsShowingOptions(false)
253
+ setHighlightedOptionId(false)
254
+ setAnnouncement('List collapsed.')
255
+ matchValue()
256
+ }
257
+
258
+ const handleBlur = (event) => {
259
+ setHighlightedOptionId(null)
260
+ }
261
+
262
+ const handleHighlightOption = (event, { id }) => {
263
+ event.persist()
264
+ const option = getOptionById(id)
265
+ if (!option) return // prevent highlighting of empty option
266
+ setHighlightedOptionId(id)
267
+ setInputValue(inputValue)
268
+ setAnnouncement(option.label)
269
+ }
270
+
271
+ const handleSelectOption = (event, { id }) => {
272
+ const option = getOptionById(id)
273
+ if (!option) return // prevent selecting of empty option
274
+ focusInput()
275
+ setSelectedOptionId(id)
276
+ setInputValue(option.label)
277
+ setIsShowingOptions(false)
278
+ setFilteredOptions(options)
279
+ setAnnouncement(`${option.label} selected. List collapsed.`)
280
+ }
281
+
282
+ const handleInputChange = (event) => {
283
+ const value = event.target.value
284
+ const newOptions = filterOptions(value)
285
+ setInputValue(value)
286
+ setFilteredOptions(newOptions)
287
+ setHighlightedOptionId(newOptions.length > 0 ? newOptions[0].id : null)
288
+ setIsShowingOptions(true)
289
+ setSelectedOptionId(value === '' ? null : selectedOptionId)
290
+ setAnnouncement(getOptionsChangedMessage(newOptions))
291
+ }
292
+
293
+ return (
294
+ <div>
295
+ <Select
296
+ renderLabel="Autocomplete"
297
+ assistiveText="Type or use arrow keys to navigate options."
298
+ placeholder="Start typing to search..."
299
+ inputValue={inputValue}
300
+ isShowingOptions={isShowingOptions}
301
+ onBlur={handleBlur}
302
+ onInputChange={handleInputChange}
303
+ onRequestShowOptions={handleShowOptions}
304
+ onRequestHideOptions={handleHideOptions}
305
+ onRequestHighlightOption={handleHighlightOption}
306
+ onRequestSelectOption={handleSelectOption}
307
+ renderBeforeInput={<User2InstUIIcon inline={false} />}
308
+ renderAfterInput={<SearchInstUIIcon inline={false} />}
309
+ inputRef={(el) => {
310
+ inputRef.current = el
311
+ }}
312
+ >
313
+ {filteredOptions.length > 0 ? (
314
+ filteredOptions.map((option) => {
315
+ return (
316
+ <Select.Option
317
+ id={option.id}
318
+ key={option.id}
319
+ isHighlighted={option.id === highlightedOptionId}
320
+ isSelected={option.id === selectedOptionId}
321
+ isDisabled={option.disabled}
322
+ renderBeforeLabel={<User2InstUIIcon />}
323
+ >
324
+ {!option.disabled
325
+ ? option.label
326
+ : `${option.label} (unavailable)`}
327
+ </Select.Option>
328
+ )
329
+ })
330
+ ) : (
331
+ <Select.Option id="empty-option" key="empty-option">
332
+ ---
333
+ </Select.Option>
334
+ )}
335
+ </Select>
336
+ </div>
337
+ )
338
+ }
339
+
340
+ render(
341
+ <View>
342
+ <AutocompleteExample
343
+ options={[
344
+ { id: 'opt0', label: 'Aaron Aaronson' },
345
+ { id: 'opt1', label: 'Amber Murphy' },
346
+ { id: 'opt2', label: 'Andrew Miller' },
347
+ { id: 'opt3', label: 'Barbara Ward' },
348
+ { id: 'opt4', label: 'Byron Cranston', disabled: true },
349
+ { id: 'opt5', label: 'Dennis Reynolds' },
350
+ { id: 'opt6', label: 'Dee Reynolds' },
351
+ { id: 'opt7', label: 'Ezra Betterthan' },
352
+ { id: 'opt8', label: 'Jeff Spicoli' },
353
+ { id: 'opt9', label: 'Joseph Smith' },
354
+ { id: 'opt10', label: 'Jasmine Diaz' },
355
+ { id: 'opt11', label: 'Martin Harris' },
356
+ { id: 'opt12', label: 'Michael Morgan', disabled: true },
357
+ { id: 'opt13', label: 'Michelle Rodriguez' },
358
+ { id: 'opt14', label: 'Ziggy Stardust' }
359
+ ]}
360
+ />
361
+ </View>
362
+ )
363
+ ```
364
+
365
+ #### Highlighting and selecting options
366
+
367
+ 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) with [AccessibleContent](AccessibleContent) for each selected option via the `renderBeforeInput` prop.
368
+
369
+ ```js
370
+ ---
371
+ type: example
372
+ ---
373
+ const MultipleSelectExample = ({ options }) => {
374
+ const [inputValue, setInputValue] = useState('')
375
+ const [isShowingOptions, setIsShowingOptions] = useState(false)
376
+ const [highlightedOptionId, setHighlightedOptionId] = useState(null)
377
+ const [selectedOptionId, setSelectedOptionId] = useState(['opt1', 'opt6'])
378
+ const [filteredOptions, setFilteredOptions] = useState(options)
379
+ const [announcement, setAnnouncement] = useState(null)
380
+ const inputRef = useRef()
381
+
382
+ const focusInput = () => {
383
+ if (inputRef.current) {
384
+ inputRef.current.blur()
385
+ inputRef.current.focus()
386
+ }
387
+ }
388
+
389
+ const getOptionById = (queryId) => {
390
+ return options.find(({ id }) => id === queryId)
391
+ }
392
+
393
+ const getOptionsChangedMessage = (newOptions) => {
394
+ let message =
395
+ newOptions.length !== filteredOptions.length
396
+ ? `${newOptions.length} options available.` // options changed, announce new total
397
+ : null // options haven't changed, don't announce
398
+ if (message && newOptions.length > 0) {
399
+ // options still available
400
+ if (highlightedOptionId !== newOptions[0].id) {
401
+ // highlighted option hasn't been announced
402
+ const option = getOptionById(newOptions[0].id).label
403
+ message = `${option}. ${message}`
404
+ }
405
+ }
406
+ return message
407
+ }
408
+
409
+ const filterOptions = (value) => {
410
+ return options.filter((option) =>
411
+ option.label.toLowerCase().startsWith(value.toLowerCase())
412
+ )
413
+ }
414
+
415
+ const matchValue = () => {
416
+ // an option matching user input exists
417
+ if (filteredOptions.length === 1) {
418
+ const onlyOption = filteredOptions[0]
419
+ // automatically select the matching option
420
+ if (onlyOption.label.toLowerCase() === inputValue.toLowerCase()) {
421
+ setInputValue('')
422
+ setSelectedOptionId([...selectedOptionId, onlyOption.id])
423
+ setFilteredOptions(filterOptions(''))
424
+ }
425
+ }
426
+ // input value is from highlighted option, not user input
427
+ // clear input, reset options
428
+ else if (highlightedOptionId) {
429
+ if (inputValue === getOptionById(highlightedOptionId).label) {
430
+ setInputValue('')
431
+ setFilteredOptions(filterOptions(''))
432
+ }
433
+ }
434
+ }
435
+
436
+ const handleShowOptions = (event) => {
437
+ setIsShowingOptions(true)
438
+
439
+ if (inputValue || options.length === 0) return
440
+
441
+ if ('key' in event) {
442
+ switch (event.key) {
443
+ case 'ArrowDown':
444
+ return handleHighlightOption(event, {
445
+ id: options.find((option) => !selectedOptionId.includes(option.id))
446
+ .id
447
+ })
448
+ case 'ArrowUp':
449
+ // Highlight last non-selected option
450
+ return handleHighlightOption(event, {
451
+ id: options[
452
+ options.findLastIndex(
453
+ (option) => !selectedOptionId.includes(option.id)
454
+ )
455
+ ].id
456
+ })
457
+ }
458
+ }
459
+ }
460
+
461
+ const handleHideOptions = (event) => {
462
+ setIsShowingOptions(false)
463
+ matchValue()
464
+ }
465
+
466
+ const handleBlur = (event) => {
467
+ setHighlightedOptionId(null)
468
+ }
469
+
470
+ const handleHighlightOption = (event, { id }) => {
471
+ event.persist()
472
+ const option = getOptionById(id)
473
+ if (!option) return // prevent highlighting empty option
474
+ setHighlightedOptionId(id)
475
+ setInputValue(inputValue)
476
+ setAnnouncement(option.label)
477
+ }
478
+
479
+ const handleSelectOption = (event, { id }) => {
480
+ const option = getOptionById(id)
481
+ if (!option) return // prevent selecting of empty option
482
+ focusInput()
483
+ setSelectedOptionId([...selectedOptionId, id])
484
+ setHighlightedOptionId(null)
485
+ setFilteredOptions(filterOptions(''))
486
+ setInputValue('')
487
+ setIsShowingOptions(false)
488
+ setAnnouncement(`${option.label} selected. List collapsed.`)
489
+ }
490
+
491
+ const handleInputChange = (event) => {
492
+ const value = event.target.value
493
+ const newOptions = filterOptions(value)
494
+ setInputValue(value)
495
+ setFilteredOptions(newOptions)
496
+ setHighlightedOptionId(newOptions.length > 0 ? newOptions[0].id : null)
497
+ setIsShowingOptions(true)
498
+ setAnnouncement(getOptionsChangedMessage(newOptions))
499
+ }
500
+
501
+ const handleKeyDown = (event) => {
502
+ if ('keyCode' in event && event.keyCode === 8) {
503
+ // when backspace key is pressed
504
+ if (inputValue === '' && selectedOptionId.length > 0) {
505
+ // remove last selected option, if input has no entered text
506
+ setHighlightedOptionId(null)
507
+ setSelectedOptionId(selectedOptionId.slice(0, -1))
508
+ }
509
+ }
510
+ }
511
+
512
+ // remove a selected option tag
513
+ const dismissTag = (e, tag) => {
514
+ // prevent closing of list
515
+ e.stopPropagation()
516
+ e.preventDefault()
517
+
518
+ const newSelection = selectedOptionId.filter((id) => id !== tag)
519
+
520
+ setSelectedOptionId(newSelection)
521
+ setHighlightedOptionId(null)
522
+ setAnnouncement(`${getOptionById(tag).label} removed`)
523
+
524
+ inputRef.current.focus()
525
+ }
526
+
527
+ const renderTags = () => {
528
+ return selectedOptionId.map((id, index) => (
529
+ <Tag
530
+ dismissible
531
+ key={id}
532
+ text={
533
+ <AccessibleContent alt={`Remove ${getOptionById(id).label}`}>
534
+ {getOptionById(id).label}
535
+ </AccessibleContent>
536
+ }
537
+ margin={
538
+ index > 0 ? 'xxx-small xx-small xxx-small 0' : '0 xx-small 0 0'
539
+ }
540
+ onClick={(e) => dismissTag(e, id)}
541
+ />
542
+ ))
543
+ }
544
+
545
+ return (
546
+ <div>
547
+ <Select
548
+ renderLabel="Multiple Select"
549
+ assistiveText="Type or use arrow keys to navigate options. Multiple selections allowed."
550
+ inputValue={inputValue}
551
+ isShowingOptions={isShowingOptions}
552
+ inputRef={(el) => {
553
+ inputRef.current = el
554
+ }}
555
+ onBlur={handleBlur}
556
+ onInputChange={handleInputChange}
557
+ onRequestShowOptions={handleShowOptions}
558
+ onRequestHideOptions={handleHideOptions}
559
+ onRequestHighlightOption={handleHighlightOption}
560
+ onRequestSelectOption={handleSelectOption}
561
+ onKeyDown={handleKeyDown}
562
+ renderBeforeInput={selectedOptionId.length > 0 ? renderTags() : null}
563
+ >
564
+ {filteredOptions.length > 0 ? (
565
+ filteredOptions.map((option, index) => {
566
+ if (selectedOptionId.indexOf(option.id) === -1) {
567
+ return (
568
+ <Select.Option
569
+ id={option.id}
570
+ key={option.id}
571
+ isHighlighted={option.id === highlightedOptionId}
572
+ >
573
+ {option.label}
574
+ </Select.Option>
575
+ )
576
+ }
577
+ })
578
+ ) : (
579
+ <Select.Option id="empty-option" key="empty-option">
580
+ ---
581
+ </Select.Option>
582
+ )}
583
+ </Select>
584
+ </div>
585
+ )
586
+ }
587
+
588
+ render(
589
+ <View>
590
+ <MultipleSelectExample
591
+ options={[
592
+ { id: 'opt1', label: 'Alaska' },
593
+ { id: 'opt2', label: 'American Samoa' },
594
+ { id: 'opt3', label: 'Arizona' },
595
+ { id: 'opt4', label: 'Arkansas' },
596
+ { id: 'opt5', label: 'California' },
597
+ { id: 'opt6', label: 'Colorado' },
598
+ { id: 'opt7', label: 'Connecticut' },
599
+ { id: 'opt8', label: 'Delaware' },
600
+ { id: 'opt9', label: 'District Of Columbia' },
601
+ { id: 'opt10', label: 'Federated States Of Micronesia' },
602
+ { id: 'opt11', label: 'Florida' },
603
+ { id: 'opt12', label: 'Georgia (unavailable)' },
604
+ { id: 'opt13', label: 'Guam' },
605
+ { id: 'opt14', label: 'Hawaii' },
606
+ { id: 'opt15', label: 'Idaho' },
607
+ { id: 'opt16', label: 'Illinois' }
608
+ ]}
609
+ />
610
+ </View>
611
+ )
612
+ ```
613
+
614
+ #### Composing option groups
615
+
616
+ 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.
617
+
618
+ ```js
619
+ ---
620
+ type: example
621
+ ---
622
+ const GroupSelectExample = ({ options }) => {
623
+ const [inputValue, setInputValue] = useState(options['Western'][0].label)
624
+ const [isShowingOptions, setIsShowingOptions] = useState(false)
625
+ const [highlightedOptionId, setHighlightedOptionId] = useState(null)
626
+ const [selectedOptionId, setSelectedOptionId] = useState(
627
+ options['Western'][0].id
628
+ )
629
+ const [announcement, setAnnouncement] = useState(null)
630
+ const inputRef = useRef()
631
+
632
+ const focusInput = () => {
633
+ if (inputRef.current) {
634
+ inputRef.current.blur()
635
+ inputRef.current.focus()
636
+ }
637
+ }
638
+
639
+ const getOptionById = (id) => {
640
+ let match = null
641
+ Object.keys(options).forEach((key, index) => {
642
+ for (let i = 0; i < options[key].length; i++) {
643
+ const option = options[key][i]
644
+ if (id === option.id) {
645
+ // return group property with the object just to make it easier
646
+ // to check which group the option belongs to
647
+ match = { ...option, group: key }
648
+ break
649
+ }
650
+ }
651
+ })
652
+ return match
653
+ }
654
+
655
+ const getGroupChangedMessage = (newOption) => {
656
+ const currentOption = getOptionById(highlightedOptionId)
657
+ const isNewGroup =
658
+ !currentOption || currentOption.group !== newOption.group
659
+ let message = isNewGroup ? `Group ${newOption.group} entered. ` : ''
660
+ message += newOption.label
661
+ return message
662
+ }
663
+
664
+ const handleShowOptions = (event) => {
665
+ setIsShowingOptions(true)
666
+ setHighlightedOptionId(null)
667
+ if (inputValue || selectedOptionId || Object.keys(options).length === 0) return
668
+
669
+ if ('key' in event) {
670
+ switch (event.key) {
671
+ case 'ArrowDown':
672
+ return handleHighlightOption(event, {
673
+ id: options[Object.keys(options)[0]][0].id
674
+ })
675
+ case 'ArrowUp':
676
+ return handleHighlightOption(event, {
677
+ id: Object.values(options).at(-1)?.at(-1)?.id
678
+ })
679
+ }
680
+ }
681
+ }
682
+
683
+ const handleHideOptions = (event) => {
684
+ setIsShowingOptions(false)
685
+ setHighlightedOptionId(null)
686
+ setInputValue(getOptionById(selectedOptionId)?.label)
687
+ }
688
+
689
+ const handleBlur = (event) => {
690
+ setHighlightedOptionId(null)
691
+ }
692
+
693
+ const handleHighlightOption = (event, { id }) => {
694
+ event.persist()
695
+ const newOption = getOptionById(id)
696
+ setHighlightedOptionId(id)
697
+ setInputValue(inputValue)
698
+ setAnnouncement(getGroupChangedMessage(newOption))
699
+ }
700
+
701
+ const handleSelectOption = (event, { id }) => {
702
+ focusInput()
703
+ setSelectedOptionId(id)
704
+ setInputValue(getOptionById(id).label)
705
+ setIsShowingOptions(false)
706
+ setAnnouncement(`${getOptionById(id).label} selected.`)
707
+ }
708
+
709
+ const renderLabel = (text, variant) => {
710
+ return (
711
+ <span>
712
+ <Badge
713
+ type="notification"
714
+ variant={variant}
715
+ standalone
716
+ margin="0 x-small xxx-small 0"
717
+ />
718
+ {text}
719
+ </span>
720
+ )
721
+ }
722
+
723
+ const renderGroup = () => {
724
+ return Object.keys(options).map((key, index) => {
725
+ const badgeVariant = key === 'Eastern' ? 'success' : 'primary'
726
+ return (
727
+ <Select.Group
728
+ key={index}
729
+ renderLabel={renderLabel(key, badgeVariant)}
730
+ >
731
+ {options[key].map((option) => (
732
+ <Select.Option
733
+ key={option.id}
734
+ id={option.id}
735
+ isHighlighted={option.id === highlightedOptionId}
736
+ isSelected={option.id === selectedOptionId}
737
+ >
738
+ {option.label}
739
+ </Select.Option>
740
+ ))}
741
+ </Select.Group>
742
+ )
743
+ })
744
+ }
745
+
746
+ return (
747
+ <div>
748
+ <Select
749
+ renderLabel="Group Select"
750
+ assistiveText="Type or use arrow keys to navigate options."
751
+ inputValue={inputValue}
752
+ isShowingOptions={isShowingOptions}
753
+ onBlur={handleBlur}
754
+ onRequestShowOptions={handleShowOptions}
755
+ onRequestHideOptions={handleHideOptions}
756
+ onRequestHighlightOption={handleHighlightOption}
757
+ onRequestSelectOption={handleSelectOption}
758
+ renderBeforeInput={
759
+ <Badge
760
+ type="notification"
761
+ variant={
762
+ getOptionById(selectedOptionId)?.group === 'Eastern'
763
+ ? 'success'
764
+ : 'primary'
765
+ }
766
+ standalone
767
+ margin="0 0 xxx-small 0"
768
+ />
769
+ }
770
+ inputRef={(el) => {
771
+ inputRef.current = el
772
+ }}
773
+ >
774
+ {renderGroup()}
775
+ </Select>
776
+ </div>
777
+ )
778
+ }
779
+
780
+ render(
781
+ <View>
782
+ <GroupSelectExample
783
+ options={{
784
+ Western: [
785
+ { id: 'opt5', label: 'Alaska' },
786
+ { id: 'opt6', label: 'California' },
787
+ { id: 'opt7', label: 'Colorado' },
788
+ { id: 'opt8', label: 'Idaho' }
789
+ ],
790
+ Eastern: [
791
+ { id: 'opt1', label: 'Alabama' },
792
+ { id: 'opt2', label: 'Connecticut' },
793
+ { id: 'opt3', label: 'Delaware' },
794
+ { id: 'opt4', label: 'Illinois' }
795
+ ]
796
+ }}
797
+ />
798
+ </View>
799
+ )
800
+ ```
801
+
802
+ ##### Using groups with autocomplete on Safari
803
+
804
+ 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:
805
+
806
+ ```js
807
+ ---
808
+ type: example
809
+ ---
810
+ const GroupSelectAutocompleteExample = ({ options }) => {
811
+ const [inputValue, setInputValue] = useState('')
812
+ const [isShowingOptions, setIsShowingOptions] = useState(false)
813
+ const [highlightedOptionId, setHighlightedOptionId] = useState(null)
814
+ const [selectedOptionId, setSelectedOptionId] = useState(null)
815
+ const [filteredOptions, setFilteredOptions] = useState(options)
816
+ const [announcement, setAnnouncement] = useState(null)
817
+ const inputRef = useRef()
818
+
819
+ const focusInput = () => {
820
+ if (inputRef.current) {
821
+ inputRef.current.blur()
822
+ inputRef.current.focus()
823
+ }
824
+ }
825
+
826
+ const getOptionById = (id) => {
827
+ return Object.values(options)
828
+ .flat()
829
+ .find((o) => o?.id === id)
830
+ }
831
+
832
+ const filterOptions = (value, options) => {
833
+ const filteredOptions = {}
834
+ Object.keys(options).forEach((key) => {
835
+ filteredOptions[key] = options[key]?.filter((option) =>
836
+ option.label.toLowerCase().includes(value.toLowerCase())
837
+ )
838
+ })
839
+ const optionsWithoutEmptyKeys = Object.keys(filteredOptions)
840
+ .filter((k) => filteredOptions[k].length > 0)
841
+ .reduce((a, k) => ({ ...a, [k]: filteredOptions[k] }), {})
842
+ return optionsWithoutEmptyKeys
843
+ }
844
+
845
+ const handleShowOptions = (event) => {
846
+ setIsShowingOptions(true)
847
+ setHighlightedOptionId(null)
848
+
849
+ if (inputValue || selectedOptionId || Object.keys(options).length === 0) return
850
+
851
+ if ('key' in event) {
852
+ switch (event.key) {
853
+ case 'ArrowDown':
854
+ return handleHighlightOption(event, {
855
+ id: options[Object.keys(options)[0]][0].id
856
+ })
857
+ case 'ArrowUp':
858
+ return handleHighlightOption(event, {
859
+ id: Object.values(options).at(-1)?.at(-1)?.id
860
+ })
861
+ }
862
+ }
863
+ }
864
+
865
+ const handleHideOptions = (event) => {
866
+ setIsShowingOptions(false)
867
+ setHighlightedOptionId(null)
868
+ }
869
+
870
+ const handleBlur = (event) => {
871
+ setHighlightedOptionId(null)
872
+ }
873
+
874
+ const handleHighlightOption = (event, { id }) => {
875
+ event.persist()
876
+ const option = getOptionById(id)
877
+ setTimeout(() => {
878
+ setAnnouncement(option.label)
879
+ }, 0)
880
+ setHighlightedOptionId(id)
881
+ }
882
+
883
+ const handleSelectOption = (event, { id }) => {
884
+ const option = getOptionById(id)
885
+ if (!option) return // prevent selecting of empty option
886
+ focusInput()
887
+ setSelectedOptionId(id)
888
+ setInputValue(option.label)
889
+ setIsShowingOptions(false)
890
+ setFilteredOptions(options)
891
+ setAnnouncement(option.label)
892
+ }
893
+
894
+ const handleInputChange = (event) => {
895
+ const value = event.target.value
896
+ const newOptions = filterOptions(value, options)
897
+ setInputValue(value)
898
+ setFilteredOptions(newOptions)
899
+ setHighlightedOptionId(newOptions.length > 0 ? newOptions[0].id : null)
900
+ setIsShowingOptions(true)
901
+ setSelectedOptionId(value === '' ? null : selectedOptionId)
902
+ }
903
+
904
+ const renderGroup = () => {
905
+ return Object.keys(filteredOptions).map((key, index) => {
906
+ return (
907
+ <Select.Group key={index} renderLabel={key}>
908
+ {filteredOptions[key].map((option) => (
909
+ <Select.Option
910
+ key={option.id}
911
+ id={option.id}
912
+ isHighlighted={option.id === highlightedOptionId}
913
+ isSelected={option.id === selectedOptionId}
914
+ >
915
+ {option.label}
916
+ </Select.Option>
917
+ ))}
918
+ </Select.Group>
919
+ )
920
+ })
921
+ }
922
+
923
+ const renderScreenReaderHelper = () => {
924
+ return (
925
+ window.safari && (
926
+ <ScreenReaderContent>
927
+ <span role="alert" aria-live="assertive">
928
+ {announcement}
929
+ </span>
930
+ </ScreenReaderContent>
931
+ )
932
+ )
933
+ }
934
+
935
+ return (
936
+ <div>
937
+ <Select
938
+ placeholder="Start typing to search..."
939
+ renderLabel="Group Select with autocomplete"
940
+ assistiveText="Type or use arrow keys to navigate options."
941
+ inputValue={inputValue}
942
+ isShowingOptions={isShowingOptions}
943
+ onBlur={handleBlur}
944
+ onInputChange={handleInputChange}
945
+ onRequestShowOptions={handleShowOptions}
946
+ onRequestHideOptions={handleHideOptions}
947
+ onRequestHighlightOption={handleHighlightOption}
948
+ onRequestSelectOption={handleSelectOption}
949
+ inputRef={(el) => {
950
+ inputRef.current = el
951
+ }}
952
+ >
953
+ {renderGroup()}
954
+ </Select>
955
+ {renderScreenReaderHelper()}
956
+ </div>
957
+ )
958
+ }
959
+
960
+ render(
961
+ <View>
962
+ <GroupSelectAutocompleteExample
963
+ options={{
964
+ Western: [
965
+ { id: 'opt5', label: 'Alaska' },
966
+ { id: 'opt6', label: 'California' },
967
+ { id: 'opt7', label: 'Colorado' },
968
+ { id: 'opt8', label: 'Idaho' }
969
+ ],
970
+ Eastern: [
971
+ { id: 'opt1', label: 'Alabama' },
972
+ { id: 'opt2', label: 'Connecticut' },
973
+ { id: 'opt3', label: 'Delaware' },
974
+ { id: '4', label: 'Illinois' }
975
+ ]
976
+ }}
977
+ />
978
+ </View>
979
+ )
980
+ ```
981
+
982
+ #### Asynchronous option loading
983
+
984
+ 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.
985
+
986
+ ```js
987
+ ---
988
+ type: example
989
+ ---
990
+ const AsyncExample = ({ options }) => {
991
+ const [inputValue, setInputValue] = useState('')
992
+ const [isShowingOptions, setIsShowingOptions] = useState(false)
993
+ const [isLoading, setIsLoading] = useState(false)
994
+ const [highlightedOptionId, setHighlightedOptionId] = useState(null)
995
+ const [selectedOptionId, setSelectedOptionId] = useState(null)
996
+ const [selectedOptionLabel, setSelectedOptionLabel] = useState('')
997
+ const [filteredOptions, setFilteredOptions] = useState([])
998
+ const [announcement, setAnnouncement] = useState(null)
999
+ const inputRef = useRef()
1000
+
1001
+ const focusInput = () => {
1002
+ if (inputRef.current) {
1003
+ inputRef.current.blur()
1004
+ inputRef.current.focus()
1005
+ }
1006
+ }
1007
+
1008
+ let timeoutId = null
1009
+
1010
+ const getOptionById = (queryId) => {
1011
+ return filteredOptions.find(({ id }) => id === queryId)
1012
+ }
1013
+
1014
+ const filterOptions = (value) => {
1015
+ return options.filter((option) =>
1016
+ option.label.toLowerCase().startsWith(value.toLowerCase())
1017
+ )
1018
+ }
1019
+
1020
+ const matchValue = () => {
1021
+ // an option matching user input exists
1022
+ if (filteredOptions.length === 1) {
1023
+ const onlyOption = filteredOptions[0]
1024
+ // automatically select the matching option
1025
+ if (onlyOption.label.toLowerCase() === inputValue.toLowerCase()) {
1026
+ setInputValue(onlyOption.label)
1027
+ setSelectedOptionId(onlyOption.id)
1028
+ return
1029
+ }
1030
+ }
1031
+ // allow user to return to empty input and no selection
1032
+ if (inputValue.length === 0) {
1033
+ setSelectedOptionId(null)
1034
+ setFilteredOptions([])
1035
+ return
1036
+ }
1037
+ // no match found, return selected option label to input
1038
+ if (selectedOptionId) {
1039
+ setInputValue(selectedOptionLabel)
1040
+ return
1041
+ }
1042
+ }
1043
+
1044
+ const handleShowOptions = (event) => {
1045
+ setIsShowingOptions(true)
1046
+ }
1047
+
1048
+ const handleHideOptions = (event) => {
1049
+ setIsShowingOptions(false)
1050
+ setHighlightedOptionId(null)
1051
+ setAnnouncement('List collapsed.')
1052
+ matchValue()
1053
+ }
1054
+
1055
+ const handleBlur = (event) => {
1056
+ setHighlightedOptionId(null)
1057
+ }
1058
+
1059
+ const handleHighlightOption = (event, { id }) => {
1060
+ event.persist()
1061
+ const option = getOptionById(id)
1062
+ if (!option) return // prevent highlighting of empty option
1063
+
1064
+ setHighlightedOptionId(id)
1065
+ setInputValue(inputValue)
1066
+ setAnnouncement(option.label)
1067
+ }
1068
+
1069
+ const handleSelectOption = (event, { id }) => {
1070
+ const option = getOptionById(id)
1071
+ if (!option) return // prevent selecting of empty option
1072
+ focusInput()
1073
+ setSelectedOptionId(id)
1074
+ setSelectedOptionLabel(option.label)
1075
+ setInputValue(option.label)
1076
+ setIsShowingOptions(false)
1077
+ setAnnouncement(`${option.label} selected. List collapsed.`)
1078
+ setFilteredOptions([getOptionById(id)])
1079
+ }
1080
+
1081
+ const handleInputChange = (event) => {
1082
+ const value = event.target.value
1083
+ clearTimeout(timeoutId)
1084
+
1085
+ if (!value || value === '') {
1086
+ setIsLoading(false)
1087
+ setInputValue(value)
1088
+ setIsShowingOptions(true)
1089
+ setSelectedOptionId(null)
1090
+ setSelectedOptionLabel(null)
1091
+ setFilteredOptions([])
1092
+ } else {
1093
+ setIsLoading(true)
1094
+ setInputValue(value)
1095
+ setIsShowingOptions(true)
1096
+ setFilteredOptions([])
1097
+ setHighlightedOptionId(null)
1098
+ setAnnouncement('Loading options.')
1099
+
1100
+ timeoutId = setTimeout(() => {
1101
+ const newOptions = filterOptions(value)
1102
+ setFilteredOptions(newOptions)
1103
+ setIsLoading(false)
1104
+ setAnnouncement(`${newOptions.length} options available.`)
1105
+ }, 1500)
1106
+ }
1107
+ }
1108
+
1109
+ return (
1110
+ <div>
1111
+ <Select
1112
+ renderLabel="Async Select"
1113
+ assistiveText="Type to search"
1114
+ inputValue={inputValue}
1115
+ isShowingOptions={isShowingOptions}
1116
+ onBlur={handleBlur}
1117
+ onInputChange={handleInputChange}
1118
+ onRequestShowOptions={handleShowOptions}
1119
+ onRequestHideOptions={handleHideOptions}
1120
+ onRequestHighlightOption={handleHighlightOption}
1121
+ onRequestSelectOption={handleSelectOption}
1122
+ inputRef={(el) => {
1123
+ inputRef.current = el
1124
+ }}
1125
+ >
1126
+ {filteredOptions.length > 0 ? (
1127
+ filteredOptions.map((option) => {
1128
+ return (
1129
+ <Select.Option
1130
+ id={option.id}
1131
+ key={option.id}
1132
+ isHighlighted={option.id === highlightedOptionId}
1133
+ isSelected={option.id === selectedOptionId}
1134
+ isDisabled={option.disabled}
1135
+ renderBeforeLabel={<User2InstUIIcon />}
1136
+ >
1137
+ {option.label}
1138
+ </Select.Option>
1139
+ )
1140
+ })
1141
+ ) : (
1142
+ <Select.Option id="empty-option" key="empty-option">
1143
+ {isLoading ? (
1144
+ <Spinner renderTitle="Loading" size="x-small" />
1145
+ ) : inputValue !== '' ? (
1146
+ 'No results'
1147
+ ) : (
1148
+ 'Type to search'
1149
+ )}
1150
+ </Select.Option>
1151
+ )}
1152
+ </Select>
1153
+ </div>
1154
+ )
1155
+ }
1156
+
1157
+ render(
1158
+ <View>
1159
+ <AsyncExample
1160
+ options={[
1161
+ { id: 'opt0', label: 'Aaron Aaronson' },
1162
+ { id: 'opt1', label: 'Amber Murphy' },
1163
+ { id: 'opt2', label: 'Andrew Miller' },
1164
+ { id: 'opt3', label: 'Barbara Ward' },
1165
+ { id: 'opt4', label: 'Byron Cranston', disabled: true },
1166
+ { id: 'opt5', label: 'Dennis Reynolds' },
1167
+ { id: 'opt6', label: 'Dee Reynolds' },
1168
+ { id: 'opt7', label: 'Ezra Betterthan' },
1169
+ { id: 'opt8', label: 'Jeff Spicoli' },
1170
+ { id: 'opt9', label: 'Joseph Smith' },
1171
+ { id: 'opt10', label: 'Jasmine Diaz' },
1172
+ { id: 'opt11', label: 'Martin Harris' },
1173
+ { id: 'opt12', label: 'Michael Morgan', disabled: true },
1174
+ { id: 'opt13', label: 'Michelle Rodriguez' },
1175
+ { id: 'opt14', label: 'Ziggy Stardust' }
1176
+ ]}
1177
+ />
1178
+ </View>
1179
+ )
1180
+ ```
1181
+
1182
+ ### Icons
1183
+
1184
+ 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 ]`.
1185
+
1186
+ ```js
1187
+ ---
1188
+ type: example
1189
+ ---
1190
+ const SingleSelectExample = ({ options }) => {
1191
+ const [inputValue, setInputValue] = useState(options[0].label)
1192
+ const [isShowingOptions, setIsShowingOptions] = useState(false)
1193
+ const [highlightedOptionId, setHighlightedOptionId] = useState(null)
1194
+ const [selectedOptionId, setSelectedOptionId] = useState(options[0].id)
1195
+ const [announcement, setAnnouncement] = useState(null)
1196
+ const inputRef = useRef()
1197
+
1198
+ const focusInput = () => {
1199
+ if (inputRef.current) {
1200
+ inputRef.current.blur()
1201
+ inputRef.current.focus()
1202
+ }
1203
+ }
1204
+
1205
+ const getOptionById = (queryId) => {
1206
+ return options.find(({ id }) => id === queryId)
1207
+ }
1208
+
1209
+ const handleShowOptions = (event) => {
1210
+ setIsShowingOptions(true)
1211
+
1212
+ if (inputValue || selectedOptionId || options.length === 0) return
1213
+
1214
+ if ('key' in event) {
1215
+ switch (event.key) {
1216
+ case 'ArrowDown':
1217
+ return handleHighlightOption(event, { id: options[0].id })
1218
+ case 'ArrowUp':
1219
+ return handleHighlightOption(event, {
1220
+ id: options[options.length - 1].id
1221
+ })
1222
+ }
1223
+ }
1224
+ }
1225
+
1226
+ const handleHideOptions = (event) => {
1227
+ const option = getOptionById(selectedOptionId)?.label
1228
+ setIsShowingOptions(false)
1229
+ setHighlightedOptionId(null)
1230
+ setInputValue(selectedOptionId ? option : '')
1231
+ setAnnouncement('List collapsed.')
1232
+ }
1233
+
1234
+ const handleBlur = (event) => {
1235
+ setHighlightedOptionId(null)
1236
+ }
1237
+
1238
+ const handleHighlightOption = (event, { id }) => {
1239
+ event.persist()
1240
+ const optionsAvailable = `${options.length} options available.`
1241
+ const nowOpen = !isShowingOptions
1242
+ ? `List expanded. ${optionsAvailable}`
1243
+ : ''
1244
+ const option = getOptionById(id).label
1245
+ setHighlightedOptionId(id)
1246
+ setInputValue(inputValue)
1247
+ setAnnouncement(`${option} ${nowOpen}`)
1248
+ }
1249
+
1250
+ const handleSelectOption = (event, { id }) => {
1251
+ const option = getOptionById(id).label
1252
+ focusInput()
1253
+ setSelectedOptionId(id)
1254
+ setInputValue(option)
1255
+ setIsShowingOptions(false)
1256
+ setAnnouncement(`"${option}" selected. List collapsed.`)
1257
+ }
1258
+
1259
+ return (
1260
+ <div>
1261
+ <Select
1262
+ renderLabel="Option Icons"
1263
+ assistiveText="Use arrow keys to navigate options."
1264
+ inputValue={inputValue}
1265
+ isShowingOptions={isShowingOptions}
1266
+ onBlur={handleBlur}
1267
+ onRequestShowOptions={handleShowOptions}
1268
+ onRequestHideOptions={handleHideOptions}
1269
+ onRequestHighlightOption={handleHighlightOption}
1270
+ onRequestSelectOption={handleSelectOption}
1271
+ inputRef={(el) => {
1272
+ inputRef.current = el
1273
+ }}
1274
+ >
1275
+ {options.map((option) => {
1276
+ return (
1277
+ <Select.Option
1278
+ id={option.id}
1279
+ key={option.id}
1280
+ isHighlighted={option.id === highlightedOptionId}
1281
+ isSelected={option.id === selectedOptionId}
1282
+ renderBeforeLabel={option.renderBeforeLabel}
1283
+ >
1284
+ {option.label}
1285
+ </Select.Option>
1286
+ )
1287
+ })}
1288
+ </Select>
1289
+ </div>
1290
+ )
1291
+ }
1292
+
1293
+ render(
1294
+ <View>
1295
+ <SingleSelectExample
1296
+ options={[
1297
+ {
1298
+ id: 'opt1',
1299
+ label: 'Text',
1300
+ renderBeforeLabel: 'XY'
1301
+ },
1302
+ {
1303
+ id: 'opt2',
1304
+ label: 'Icon',
1305
+ renderBeforeLabel: <CheckInstUIIcon />
1306
+ },
1307
+ {
1308
+ id: 'opt3',
1309
+ label: 'Colored Icon',
1310
+ renderBeforeLabel: (props) => {
1311
+ let color = 'infoColor'
1312
+ if (props.isHighlighted) color = 'baseColor'
1313
+ if (props.isSelected) color = 'inverseColor'
1314
+ if (props.isDisabled) color = 'disabledBaseColor'
1315
+ return <VerifiedInstUIIcon color={color}/>
1316
+ }
1317
+ }
1318
+ ]}
1319
+ />
1320
+ </View>
1321
+ )
1322
+ ```
1323
+
1324
+ #### Providing assistive text for screen readers
1325
+
1326
+ It's important to ensure screen reader users receive instruction and feedback while interacting with a `Select`, but screen reader support for the `combobox` role varies. The `assistiveText` prop should always be used to explain how a keyboard user can make a selection. Additionally, a live region should be updated with feedback as the component is interacted with, such as when options are filtered or highlighted. Using an [Alert](Alert) with the `screenReaderOnly` prop is the easiest way to do this.
1327
+
1328
+ > Note: This component uses a native `input` field to render the selected value. When it's included in a native HTML `form`, the text value will be sent to the backend instead of anything specified in the `value` field of the `Select.Option`-s. We do not recommend to use this component this way, rather write your own code that collects information and sends it to the backend.
1329
+
1330
+ ```js
1331
+ ---
1332
+ type: embed
1333
+ ---
1334
+ <Guidelines>
1335
+ <Figure recommendation="a11y" title="Accessibility">
1336
+ <Figure.Item>To ensure Select is accessible for iOS VoiceOver users, the input field’s focus must be blurred and then reapplied after selecting an option and closing the listbox. The examples above demonstrate this behavior.
1337
+ </Figure.Item>
1338
+ <Figure.Item>If no option is selected initially, pressing the down arrow should open the listbox and move focus to the first option, while pressing up should move focus to the last item. You can see this behavior in the examples above.
1339
+ </Figure.Item>
1340
+ </Figure>
1341
+ </Guidelines>
1342
+ ```