@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.
@@ -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
- - ```javascript
21
- class SingleSelectExample extends React.Component {
22
- state = {
23
- inputValue: this.props.options[0].label,
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
- - ```javascript
304
- class AutocompleteExample extends React.Component {
305
- state = {
306
- inputValue: '',
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
- - ```javascript
757
- class MultipleSelectExample extends React.Component {
758
- state = {
759
- inputValue: '',
760
- isShowingOptions: false,
761
- highlightedOptionId: null,
762
- selectedOptionId: ['opt1', 'opt6'],
763
- filteredOptions: this.props.options,
764
- announcement: null
765
- }
766
-
767
- inputRef = null
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 (this.inputRef) {
771
- this.inputRef.blur()
772
- this.inputRef.focus()
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 this.props.options.find(({ id }) => id === queryId)
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 !== this.state.filteredOptions.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 (this.state.highlightedOptionId !== newOptions[0].id) {
398
+ if (highlightedOptionId !== newOptions[0].id) {
788
399
  // highlighted option hasn't been announced
789
- const option = this.getOptionById(newOptions[0].id).label
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 this.props.options.filter((option) =>
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
- return {
816
- inputValue: '',
817
- selectedOptionId: [...selectedOptionId, onlyOption.id],
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 === this.getOptionById(highlightedOptionId).label) {
826
- return {
827
- inputValue: '',
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
- const { options } = this.props
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 this.handleHighlightOption(event, {
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 this.handleHighlightOption(event, {
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
- this.setState({
862
- isShowingOptions: false,
863
- ...this.matchValue()
864
- })
457
+ const handleHideOptions = (event) => {
458
+ setIsShowingOptions(false)
459
+ matchValue()
865
460
  }
866
461
 
867
- handleBlur = (event) => {
868
- this.setState({
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 = this.getOptionById(id)
468
+ const option = getOptionById(id)
876
469
  if (!option) return // prevent highlighting empty option
877
- this.setState((state) => ({
878
- highlightedOptionId: id,
879
- inputValue: state.inputValue,
880
- announcement: option.label
881
- }))
470
+ setHighlightedOptionId(id)
471
+ setInputValue(inputValue)
472
+ setAnnouncement(option.label)
882
473
  }
883
474
 
884
- handleSelectOption = (event, { id }) => {
885
- this.focusInput()
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
- this.setState((state) => ({
889
- selectedOptionId: [...state.selectedOptionId, id],
890
- highlightedOptionId: null,
891
- filteredOptions: this.filterOptions(''),
892
- inputValue: '',
893
- isShowingOptions: false,
894
- announcement: `${option.label} selected. List collapsed.`
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 = this.filterOptions(value)
901
- this.setState({
902
- inputValue: value,
903
- filteredOptions: newOptions,
904
- highlightedOptionId: newOptions.length > 0 ? newOptions[0].id : null,
905
- isShowingOptions: true,
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
- - ```javascript
1288
- class GroupSelectExample extends React.Component {
1289
- state = {
1290
- inputValue: this.props.options['Western'][0].label,
1291
- isShowingOptions: false,
1292
- highlightedOptionId: null,
1293
- selectedOptionId: this.props.options['Western'][0].id,
1294
- announcement: null
1295
- }
1296
-
1297
- inputElement = null
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
- focusInput = () => {
1300
- if (this.inputElement) {
1301
- this.inputElement.blur()
1302
- this.inputElement.focus()
1303
- }
628
+ const focusInput = () => {
629
+ if (inputRef.current) {
630
+ inputRef.current.blur()
631
+ inputRef.current.focus()
1304
632
  }
633
+ }
1305
634
 
1306
- getOptionById(id) {
1307
- const { options } = this.props
1308
- let match = null
1309
- Object.keys(options).forEach((key, index) => {
1310
- for (let i = 0; i < options[key].length; i++) {
1311
- const option = options[key][i]
1312
- if (id === option.id) {
1313
- // return group property with the object just to make it easier
1314
- // to check which group the option belongs to
1315
- match = { ...option, group: key }
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
- return match
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
- this.setState({
1337
- isShowingOptions: true,
1338
- highlightedOptionId: null
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
- if (inputValue || selectedOptionId || options.length === 0) return
660
+ const handleShowOptions = (event) => {
661
+ setIsShowingOptions(true)
662
+ setHighlightedOptionId(null)
663
+ if (inputValue || selectedOptionId || options.length === 0) return
1342
664
 
1343
- switch (event.key) {
1344
- case 'ArrowDown':
1345
- return this.handleHighlightOption(event, {
1346
- id: options[Object.keys(options)[0]][0].id
1347
- })
1348
- case 'ArrowUp':
1349
- return this.handleHighlightOption(event, {
1350
- id: Object.values(options).at(-1)?.at(-1)?.id
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
- handleHideOptions = (event) => {
1356
- const { selectedOptionId } = this.state
1357
- this.setState({
1358
- isShowingOptions: false,
1359
- highlightedOptionId: null,
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
- handleBlur = (event) => {
1365
- this.setState({
1366
- highlightedOptionId: null
1367
- })
1368
- }
683
+ const handleBlur = (event) => {
684
+ setHighlightedOptionId(null)
685
+ }
1369
686
 
1370
- handleHighlightOption = (event, { id }) => {
1371
- event.persist()
1372
- const newOption = this.getOptionById(id)
1373
- this.setState((state) => ({
1374
- highlightedOptionId: id,
1375
- inputValue: state.inputValue,
1376
- announcement: this.getGroupChangedMessage(newOption)
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
- handleSelectOption = (event, { id }) => {
1381
- this.focusInput()
1382
- this.setState({
1383
- selectedOptionId: id,
1384
- inputValue: this.getOptionById(id).label,
1385
- isShowingOptions: false,
1386
- announcement: `${this.getOptionById(id).label} selected.`
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
- renderLabel(text, variant) {
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
- <span>
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={variant}
755
+ variant={
756
+ getOptionById(selectedOptionId)?.group === 'Eastern'
757
+ ? 'success'
758
+ : 'primary'
759
+ }
1396
760
  standalone
1397
- margin="0 x-small xxx-small 0"
761
+ margin="0 0 xxx-small 0"
1398
762
  />
1399
- {text}
1400
- </span>
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
- renderGroup() {
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
- render() {
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
- return (
1441
- <div>
1442
- <Select
1443
- renderLabel="Group Select"
1444
- assistiveText="Type or use arrow keys to navigate options."
1445
- inputValue={inputValue}
1446
- isShowingOptions={isShowingOptions}
1447
- onBlur={this.handleBlur}
1448
- onRequestShowOptions={this.handleShowOptions}
1449
- onRequestHideOptions={this.handleHideOptions}
1450
- onRequestHighlightOption={this.handleHighlightOption}
1451
- onRequestSelectOption={this.handleSelectOption}
1452
- renderBeforeInput={
1453
- <Badge
1454
- type="notification"
1455
- variant={
1456
- this.getOptionById(selectedOptionId)?.group === 'Eastern'
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
- render(
1476
- <View>
1477
- <GroupSelectExample
1478
- options={{
1479
- Western: [
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
- - ```js
1498
- const GroupSelectExample = ({ options }) => {
1499
- const [inputValue, setInputValue] = useState(options['Western'][0].label)
1500
- const [isShowingOptions, setIsShowingOptions] = useState(false)
1501
- const [highlightedOptionId, setHighlightedOptionId] = useState(null)
1502
- const [selectedOptionId, setSelectedOptionId] = useState(
1503
- options['Western'][0].id
1504
- )
1505
- const [announcement, setAnnouncement] = useState(null)
1506
- const inputRef = useRef()
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
- const focusInput = () => {
1509
- if (inputRef.current) {
1510
- inputRef.current.blur()
1511
- inputRef.current.focus()
1512
- }
1513
- }
839
+ const handleShowOptions = (event) => {
840
+ setIsShowingOptions(true)
841
+ setHighlightedOptionId(null)
1514
842
 
1515
- const getOptionById = (id) => {
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
- const getGroupChangedMessage = (newOption) => {
1532
- const currentOption = getOptionById(highlightedOptionId)
1533
- const isNewGroup =
1534
- !currentOption || currentOption.group !== newOption.group
1535
- let message = isNewGroup ? `Group ${newOption.group} entered. ` : ''
1536
- message += newOption.label
1537
- return message
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
- const handleShowOptions = (event) => {
1541
- setIsShowingOptions(true)
1542
- setHighlightedOptionId(null)
1543
- if (inputValue || selectedOptionId || options.length === 0) return
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
- const handleHideOptions = (event) => {
1558
- setIsShowingOptions(false)
1559
- setHighlightedOptionId(null)
1560
- setInputValue(getOptionById(selectedOptionId)?.label)
1561
- }
862
+ const handleBlur = (event) => {
863
+ setHighlightedOptionId(null)
864
+ }
1562
865
 
1563
- const handleBlur = (event) => {
1564
- setHighlightedOptionId(null)
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
- const handleHighlightOption = (event, { id }) => {
1568
- event.persist()
1569
- const newOption = getOptionById(id)
1570
- setHighlightedOptionId(id)
1571
- setInputValue(inputValue)
1572
- setAnnouncement(getGroupChangedMessage(newOption))
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
- const handleSelectOption = (event, { id }) => {
1576
- focusInput()
1577
- setSelectedOptionId(id)
1578
- setInputValue(getOptionById(id).label)
1579
- setIsShowingOptions(false)
1580
- setAnnouncement(`${getOptionById(id).label} selected.`)
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
- const renderLabel = (text, variant) => {
896
+ const renderGroup = () => {
897
+ return Object.keys(filteredOptions).map((key, index) => {
1584
898
  return (
1585
- <span>
1586
- <Badge
1587
- type="notification"
1588
- variant={variant}
1589
- standalone
1590
- margin="0 x-small xxx-small 0"
1591
- />
1592
- {text}
1593
- </span>
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
- <div>
1622
- <Select
1623
- renderLabel="Group Select"
1624
- assistiveText="Type or use arrow keys to navigate options."
1625
- inputValue={inputValue}
1626
- isShowingOptions={isShowingOptions}
1627
- onBlur={handleBlur}
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
- render(
1655
- <View>
1656
- <GroupSelectExample
1657
- options={{
1658
- Western: [
1659
- { id: 'opt5', label: 'Alaska' },
1660
- { id: 'opt6', label: 'California' },
1661
- { id: 'opt7', label: 'Colorado' },
1662
- { id: 'opt8', label: 'Idaho' }
1663
- ],
1664
- Eastern: [
1665
- { id: 'opt1', label: 'Alabama' },
1666
- { id: 'opt2', label: 'Connecticut' },
1667
- { id: 'opt3', label: 'Delaware' },
1668
- { id: 'opt4', label: 'Illinois' }
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
- </View>
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
- ##### Using groups with autocomplete on Safari
974
+ #### Asynchronous option loading
1677
975
 
1678
- 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:
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
- - ```javascript
1681
- class GroupSelectAutocompleteExample extends React.Component {
1682
- state = {
1683
- inputValue: '',
1684
- isShowingOptions: false,
1685
- highlightedOptionId: null,
1686
- selectedOptionId: null,
1687
- filteredOptions: this.props.options,
1688
- announcement: null
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
- inputElement = null
1006
+ const filterOptions = (value) => {
1007
+ return options.filter((option) =>
1008
+ option.label.toLowerCase().startsWith(value.toLowerCase())
1009
+ )
1010
+ }
1692
1011
 
1693
- focusInput = () => {
1694
- if (this.inputElement) {
1695
- this.inputElement.blur()
1696
- this.inputElement.focus()
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
- getOptionById(id) {
1701
- const options = this.props.options
1702
- return Object.values(options)
1703
- .flat()
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
- filterOptions(value, options) {
1708
- const filteredOptions = {}
1709
- Object.keys(options).forEach((key) => {
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
- handleShowOptions = (event) => {
1721
- const { options } = this.props
1722
- const { inputValue, selectedOptionId } = this.state
1036
+ const handleShowOptions = (event) => {
1037
+ setIsShowingOptions(true)
1038
+ }
1723
1039
 
1724
- this.setState({
1725
- isShowingOptions: true,
1726
- highlightedOptionId: null
1727
- })
1040
+ const handleHideOptions = (event) => {
1041
+ setIsShowingOptions(false)
1042
+ setHighlightedOptionId(null)
1043
+ setAnnouncement('List collapsed.')
1044
+ matchValue()
1045
+ }
1728
1046
 
1729
- if (inputValue || selectedOptionId || options.length === 0) return
1047
+ const handleBlur = (event) => {
1048
+ setHighlightedOptionId(null)
1049
+ }
1730
1050
 
1731
- switch (event.key) {
1732
- case 'ArrowDown':
1733
- return this.handleHighlightOption(event, {
1734
- id: options[Object.keys(options)[0]][0].id
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
- handleHideOptions = (event) => {
1744
- const { selectedOptionId } = this.state
1745
- this.setState({
1746
- isShowingOptions: false,
1747
- highlightedOptionId: null
1748
- })
1749
- }
1056
+ setHighlightedOptionId(id)
1057
+ setInputValue(inputValue)
1058
+ setAnnouncement(option.label)
1059
+ }
1750
1060
 
1751
- handleBlur = (event) => {
1752
- this.setState({
1753
- highlightedOptionId: null
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
- handleHighlightOption = (event, { id }) => {
1758
- event.persist()
1759
- const option = this.getOptionById(id)
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
- handleSelectOption = (event, { id }) => {
1771
- const option = this.getOptionById(id)
1772
- if (!option) return // prevent selecting of empty option
1773
- this.focusInput()
1774
- this.setState({
1775
- selectedOptionId: id,
1776
- inputValue: option.label,
1777
- isShowingOptions: false,
1778
- filteredOptions: this.props.options,
1779
- announcement: option.label
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
- handleInputChange = (event) => {
1784
- const value = event.target.value
1785
- const newOptions = this.filterOptions(value, this.props.options)
1786
- this.setState((state) => ({
1787
- inputValue: value,
1788
- filteredOptions: newOptions,
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
- renderGroup() {
1796
- const filteredOptions = this.state.filteredOptions
1797
- const { highlightedOptionId, selectedOptionId } = this.state
1798
-
1799
- return Object.keys(filteredOptions).map((key, index) => {
1800
- return (
1801
- <Select.Group key={index} renderLabel={key}>
1802
- {filteredOptions[key].map((option) => (
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
- </Select.Group>
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
- renderScreenReaderHelper() {
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
- render() {
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
- return (
1840
- <div>
1841
- <Select
1842
- placeholder="Start typing to search..."
1843
- renderLabel="Group Select with autocomplete"
1844
- assistiveText="Type or use arrow keys to navigate options."
1845
- inputValue={inputValue}
1846
- isShowingOptions={isShowingOptions}
1847
- onBlur={this.handleBlur}
1848
- onInputChange={this.handleInputChange}
1849
- onRequestShowOptions={this.handleShowOptions}
1850
- onRequestHideOptions={this.handleHideOptions}
1851
- onRequestHighlightOption={this.handleHighlightOption}
1852
- onRequestSelectOption={this.handleSelectOption}
1853
- inputRef={(el) => {
1854
- this.inputElement = el
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
- render(
1866
- <View>
1867
- <GroupSelectAutocompleteExample
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
- const focusInput = () => {
1898
- if (inputRef.current) {
1899
- inputRef.current.blur()
1900
- inputRef.current.focus()
1901
- }
1902
- }
1203
+ const handleShowOptions = (event) => {
1204
+ setIsShowingOptions(true)
1903
1205
 
1904
- const getOptionById = (id) => {
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
- const filterOptions = (value, options) => {
1911
- const filteredOptions = {}
1912
- Object.keys(options).forEach((key) => {
1913
- filteredOptions[key] = options[key]?.filter((option) =>
1914
- option.label.toLowerCase().includes(value.toLowerCase())
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
- const handleShowOptions = (event) => {
1924
- setIsShowingOptions(true)
1925
- setHighlightedOptionId(null)
1926
-
1927
- if (inputValue || selectedOptionId || options.length === 0) return
1928
-
1929
- switch (event.key) {
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
- const handleHideOptions = (event) => {
1942
- setIsShowingOptions(false)
1943
- setHighlightedOptionId(null)
1944
- }
1226
+ const handleBlur = (event) => {
1227
+ setHighlightedOptionId(null)
1228
+ }
1945
1229
 
1946
- const handleBlur = (event) => {
1947
- setHighlightedOptionId(null)
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
- const handleHighlightOption = (event, { id }) => {
1951
- event.persist()
1952
- const option = getOptionById(id)
1953
- setTimeout(() => {
1954
- setAnnouncement(option.label)
1955
- }, 0)
1956
- setHighlightedOptionId(id)
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
- const handleSelectOption = (event, { id }) => {
1960
- const option = getOptionById(id)
1961
- if (!option) return // prevent selecting of empty option
1962
- focusInput()
1963
- setSelectedOptionId(id)
1964
- setInputValue(option.label)
1965
- setIsShowingOptions(false)
1966
- setFilteredOptions(options)
1967
- setAnnouncement(option.label)
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
- const handleInputChange = (event) => {
1971
- const value = event.target.value
1972
- const newOptions = filterOptions(value, options)
1973
- setInputValue(value)
1974
- setFilteredOptions(newOptions)
1975
- setHighlightedOptionId(newOptions.length > 0 ? newOptions[0].id : null)
1976
- setIsShowingOptions(true)
1977
- setSelectedOptionId(value === '' ? null : selectedOptionId)
1978
- }
1979
-
1980
- const renderGroup = () => {
1981
- return Object.keys(filteredOptions).map((key, index) => {
1982
- return (
1983
- <Select.Group key={index} renderLabel={key}>
1984
- {filteredOptions[key].map((option) => (
1985
- <Select.Option
1986
- key={option.id}
1987
- id={option.id}
1988
- isHighlighted={option.id === highlightedOptionId}
1989
- isSelected={option.id === selectedOptionId}
1990
- >
1991
- {option.label}
1992
- </Select.Option>
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
- // allow user to return to empty input and no selection
2116
- if (inputValue.length === 0) {
2117
- return { selectedOptionId: null, filteredOptions: [] }
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