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