@instructure/ui-select 10.16.1 → 10.16.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,25 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [10.16.3](https://github.com/instructure/instructure-ui/compare/v10.16.1...v10.16.3) (2025-04-30)
7
+
8
+
9
+ ### Bug Fixes
10
+
11
+ * **ui-time-select,ui-simple-select,ui-select:** make Select accessible for iOS VoiceOver ([b501a7b](https://github.com/instructure/instructure-ui/commit/b501a7b38bfa491298085a173a49a1baa0a19b29))
12
+
13
+
14
+
15
+
16
+
17
+ ## [10.16.2](https://github.com/instructure/instructure-ui/compare/v10.16.1...v10.16.2) (2025-04-22)
18
+
19
+ **Note:** Version bump only for package @instructure/ui-select
20
+
21
+
22
+
23
+
24
+
6
25
  ## [10.16.1](https://github.com/instructure/instructure-ui/compare/v10.16.0...v10.16.1) (2025-04-22)
7
26
 
8
27
 
@@ -130,6 +130,9 @@ let Select = (_dec = withDeterministicId(), _dec2 = withStyle(generateStyle, gen
130
130
  focus() {
131
131
  this._input && this._input.focus();
132
132
  }
133
+ blur() {
134
+ this._input && this._input.blur();
135
+ }
133
136
  get childrenArray() {
134
137
  return Children.toArray(this.props.children);
135
138
  }
@@ -567,7 +570,12 @@ let Select = (_dec = withDeterministicId(), _dec2 = withStyle(generateStyle, gen
567
570
  shouldNotWrap,
568
571
  display: isInline ? 'inline-block' : 'block',
569
572
  renderBeforeInput: this.handleRenderBeforeInput(),
570
- renderAfterInput: this.handleRenderAfterInput(),
573
+ // On iOS VoiceOver, if there is a custom element instead of the changing up and down arrow button
574
+ // the listbox closes on a swipe, so a DOM change is enforced by the key change
575
+ // that seems to inform VoiceOver to behave the correct way
576
+ renderAfterInput: utils.isAndroidOrIOS() && renderAfterInput !== void 0 ? _jsx("span", {
577
+ children: this.handleRenderAfterInput()
578
+ }, this.props.isShowingOptions ? 'open' : 'closed') : this.handleRenderAfterInput(),
571
579
  // If `inputValue` is provided, we need to pass a default onChange handler,
572
580
  // because TextInput `value` is a controlled prop,
573
581
  // and onChange is not required for Select
@@ -621,8 +629,12 @@ let Select = (_dec = withDeterministicId(), _dec2 = withStyle(generateStyle, gen
621
629
  children: assistiveText
622
630
  }), _jsx(Popover, {
623
631
  constrain: constrain,
624
- placement: placement,
625
- mountNode: mountNode,
632
+ placement: placement
633
+ // On iOS VoiceOver, the Popover is mounted right after the input
634
+ // in order to be able to navigate through the list items with a swipe.
635
+ // The swipe would result in closing the listbox if mounted elsewhere.
636
+ ,
637
+ mountNode: mountNode !== void 0 ? mountNode : utils.isAndroidOrIOS() ? this.ref : void 0,
626
638
  positionTarget: this._inputContainer,
627
639
  isShowingContent: isShowingOptions,
628
640
  shouldReturnFocus: false,
@@ -142,6 +142,9 @@ let Select = exports.Select = (_dec = (0, _withDeterministicId.withDeterministic
142
142
  focus() {
143
143
  this._input && this._input.focus();
144
144
  }
145
+ blur() {
146
+ this._input && this._input.blur();
147
+ }
145
148
  get childrenArray() {
146
149
  return _react.Children.toArray(this.props.children);
147
150
  }
@@ -579,7 +582,12 @@ let Select = exports.Select = (_dec = (0, _withDeterministicId.withDeterministic
579
582
  shouldNotWrap,
580
583
  display: isInline ? 'inline-block' : 'block',
581
584
  renderBeforeInput: this.handleRenderBeforeInput(),
582
- renderAfterInput: this.handleRenderAfterInput(),
585
+ // On iOS VoiceOver, if there is a custom element instead of the changing up and down arrow button
586
+ // the listbox closes on a swipe, so a DOM change is enforced by the key change
587
+ // that seems to inform VoiceOver to behave the correct way
588
+ renderAfterInput: utils.isAndroidOrIOS() && renderAfterInput !== void 0 ? (0, _jsxRuntime.jsx)("span", {
589
+ children: this.handleRenderAfterInput()
590
+ }, this.props.isShowingOptions ? 'open' : 'closed') : this.handleRenderAfterInput(),
583
591
  // If `inputValue` is provided, we need to pass a default onChange handler,
584
592
  // because TextInput `value` is a controlled prop,
585
593
  // and onChange is not required for Select
@@ -633,8 +641,12 @@ let Select = exports.Select = (_dec = (0, _withDeterministicId.withDeterministic
633
641
  children: assistiveText
634
642
  }), (0, _jsxRuntime.jsx)(_Popover.Popover, {
635
643
  constrain: constrain,
636
- placement: placement,
637
- mountNode: mountNode,
644
+ placement: placement
645
+ // On iOS VoiceOver, the Popover is mounted right after the input
646
+ // in order to be able to navigate through the list items with a swipe.
647
+ // The swipe would result in closing the listbox if mounted elsewhere.
648
+ ,
649
+ mountNode: mountNode !== void 0 ? mountNode : utils.isAndroidOrIOS() ? this.ref : void 0,
638
650
  positionTarget: this._inputContainer,
639
651
  isShowingContent: isShowingOptions,
640
652
  shouldReturnFocus: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@instructure/ui-select",
3
- "version": "10.16.1",
3
+ "version": "10.16.3",
4
4
  "description": "A component for select and autocomplete behavior.",
5
5
  "author": "Instructure, Inc. Engineering and Product Design",
6
6
  "module": "./es/index.js",
@@ -23,12 +23,12 @@
23
23
  },
24
24
  "license": "MIT",
25
25
  "devDependencies": {
26
- "@instructure/ui-axe-check": "10.16.1",
27
- "@instructure/ui-babel-preset": "10.16.1",
28
- "@instructure/ui-color-utils": "10.16.1",
29
- "@instructure/ui-scripts": "10.16.1",
30
- "@instructure/ui-test-utils": "10.16.1",
31
- "@instructure/ui-themes": "10.16.1",
26
+ "@instructure/ui-axe-check": "10.16.3",
27
+ "@instructure/ui-babel-preset": "10.16.3",
28
+ "@instructure/ui-color-utils": "10.16.3",
29
+ "@instructure/ui-scripts": "10.16.3",
30
+ "@instructure/ui-test-utils": "10.16.3",
31
+ "@instructure/ui-themes": "10.16.3",
32
32
  "@testing-library/jest-dom": "^6.6.3",
33
33
  "@testing-library/react": "^16.0.1",
34
34
  "@testing-library/user-event": "^14.5.2",
@@ -36,22 +36,22 @@
36
36
  },
37
37
  "dependencies": {
38
38
  "@babel/runtime": "^7.26.0",
39
- "@instructure/emotion": "10.16.1",
40
- "@instructure/shared-types": "10.16.1",
41
- "@instructure/ui-dom-utils": "10.16.1",
42
- "@instructure/ui-form-field": "10.16.1",
43
- "@instructure/ui-icons": "10.16.1",
44
- "@instructure/ui-options": "10.16.1",
45
- "@instructure/ui-popover": "10.16.1",
46
- "@instructure/ui-position": "10.16.1",
47
- "@instructure/ui-prop-types": "10.16.1",
48
- "@instructure/ui-react-utils": "10.16.1",
49
- "@instructure/ui-selectable": "10.16.1",
50
- "@instructure/ui-testable": "10.16.1",
51
- "@instructure/ui-text-input": "10.16.1",
52
- "@instructure/ui-utils": "10.16.1",
53
- "@instructure/ui-view": "10.16.1",
54
- "@instructure/uid": "10.16.1",
39
+ "@instructure/emotion": "10.16.3",
40
+ "@instructure/shared-types": "10.16.3",
41
+ "@instructure/ui-dom-utils": "10.16.3",
42
+ "@instructure/ui-form-field": "10.16.3",
43
+ "@instructure/ui-icons": "10.16.3",
44
+ "@instructure/ui-options": "10.16.3",
45
+ "@instructure/ui-popover": "10.16.3",
46
+ "@instructure/ui-position": "10.16.3",
47
+ "@instructure/ui-prop-types": "10.16.3",
48
+ "@instructure/ui-react-utils": "10.16.3",
49
+ "@instructure/ui-selectable": "10.16.3",
50
+ "@instructure/ui-testable": "10.16.3",
51
+ "@instructure/ui-text-input": "10.16.3",
52
+ "@instructure/ui-utils": "10.16.3",
53
+ "@instructure/ui-view": "10.16.3",
54
+ "@instructure/uid": "10.16.3",
55
55
  "prop-types": "^15.8.1"
56
56
  },
57
57
  "peerDependencies": {
@@ -26,6 +26,14 @@ describes: Select
26
26
  selectedOptionId: this.props.options[0].id,
27
27
  announcement: null
28
28
  }
29
+ inputElement = null
30
+
31
+ focusInput = () => {
32
+ this.inputElement.blur()
33
+ if (this.inputElement) {
34
+ this.inputElement.focus()
35
+ }
36
+ }
29
37
 
30
38
  getOptionById(queryId) {
31
39
  return this.props.options.find(({ id }) => id === queryId)
@@ -69,6 +77,7 @@ describes: Select
69
77
  }
70
78
 
71
79
  handleSelectOption = (event, { id }) => {
80
+ this.focusInput()
72
81
  const option = this.getOptionById(id).label
73
82
  this.setState({
74
83
  selectedOptionId: id,
@@ -99,6 +108,9 @@ describes: Select
99
108
  onRequestHideOptions={this.handleHideOptions}
100
109
  onRequestHighlightOption={this.handleHighlightOption}
101
110
  onRequestSelectOption={this.handleSelectOption}
111
+ inputRef={(el) => {
112
+ this.inputElement = el
113
+ }}
102
114
  >
103
115
  {this.props.options.map((option) => {
104
116
  return (
@@ -158,6 +170,14 @@ describes: Select
158
170
  const [highlightedOptionId, setHighlightedOptionId] = useState(null)
159
171
  const [selectedOptionId, setSelectedOptionId] = useState(options[0].id)
160
172
  const [announcement, setAnnouncement] = useState(null)
173
+ const [inputElement, setInputElement] = useState(null)
174
+
175
+ const focusInput = () => {
176
+ inputElement.blur()
177
+ if (inputElement) {
178
+ inputElement.focus()
179
+ }
180
+ }
161
181
 
162
182
  const getOptionById = (queryId) => {
163
183
  return options.find(({ id }) => id === queryId)
@@ -193,6 +213,7 @@ describes: Select
193
213
 
194
214
  const handleSelectOption = (event, { id }) => {
195
215
  const option = getOptionById(id).label
216
+ focusInput()
196
217
  setSelectedOptionId(id)
197
218
  setInputValue(option)
198
219
  setIsShowingOptions(false)
@@ -211,6 +232,9 @@ describes: Select
211
232
  onRequestHideOptions={handleHideOptions}
212
233
  onRequestHighlightOption={handleHighlightOption}
213
234
  onRequestSelectOption={handleSelectOption}
235
+ inputRef={(el) => {
236
+ setInputElement(el)
237
+ }}
214
238
  >
215
239
  {options.map((option) => {
216
240
  return (
@@ -277,6 +301,7 @@ It's best practice to always provide autocomplete functionality to help users ma
277
301
  filteredOptions: this.props.options,
278
302
  announcement: null
279
303
  }
304
+ inputElement = null
280
305
 
281
306
  getOptionById(queryId) {
282
307
  return this.props.options.find(({ id }) => id === queryId)
@@ -304,6 +329,13 @@ It's best practice to always provide autocomplete functionality to help users ma
304
329
  )
305
330
  }
306
331
 
332
+ focusInput = () => {
333
+ this.inputElement.blur()
334
+ if (this.inputElement) {
335
+ this.inputElement.focus()
336
+ }
337
+ }
338
+
307
339
  matchValue() {
308
340
  const {
309
341
  filteredOptions,
@@ -380,6 +412,7 @@ It's best practice to always provide autocomplete functionality to help users ma
380
412
  handleSelectOption = (event, { id }) => {
381
413
  const option = this.getOptionById(id)
382
414
  if (!option) return // prevent selecting of empty option
415
+ this.focusInput()
383
416
  this.setState({
384
417
  selectedOptionId: id,
385
418
  inputValue: option.label,
@@ -428,6 +461,9 @@ It's best practice to always provide autocomplete functionality to help users ma
428
461
  onRequestSelectOption={this.handleSelectOption}
429
462
  renderBeforeInput={<IconUserSolid inline={false} />}
430
463
  renderAfterInput={<IconSearchLine inline={false} />}
464
+ inputRef={(el) => {
465
+ this.inputElement = el
466
+ }}
431
467
  >
432
468
  {filteredOptions.length > 0 ? (
433
469
  filteredOptions.map((option) => {
@@ -499,6 +535,14 @@ It's best practice to always provide autocomplete functionality to help users ma
499
535
  const [selectedOptionId, setSelectedOptionId] = useState(null)
500
536
  const [filteredOptions, setFilteredOptions] = useState(options)
501
537
  const [announcement, setAnnouncement] = useState(null)
538
+ const [inputElement, setInputElement] = useState(null)
539
+
540
+ const focusInput = () => {
541
+ inputElement.blur()
542
+ if (inputElement) {
543
+ inputElement.focus()
544
+ }
545
+ }
502
546
 
503
547
  const getOptionById = (queryId) => {
504
548
  return options.find(({ id }) => id === queryId)
@@ -586,6 +630,7 @@ It's best practice to always provide autocomplete functionality to help users ma
586
630
  const handleSelectOption = (event, { id }) => {
587
631
  const option = getOptionById(id)
588
632
  if (!option) return // prevent selecting of empty option
633
+ focusInput()
589
634
  setSelectedOptionId(id)
590
635
  setInputValue(option.label)
591
636
  setIsShowingOptions(false)
@@ -620,6 +665,9 @@ It's best practice to always provide autocomplete functionality to help users ma
620
665
  onRequestSelectOption={handleSelectOption}
621
666
  renderBeforeInput={<IconUserSolid inline={false} />}
622
667
  renderAfterInput={<IconSearchLine inline={false} />}
668
+ inputRef={(el) => {
669
+ setInputElement(el)
670
+ }}
623
671
  >
624
672
  {filteredOptions.length > 0 ? (
625
673
  filteredOptions.map((option) => {
@@ -697,6 +745,15 @@ To mark an option as "highlighted", use the option's `isHighlighted` prop. Note
697
745
  announcement: null
698
746
  }
699
747
 
748
+ inputRef = null
749
+
750
+ focusInput = () => {
751
+ this.inputRef.blur()
752
+ if (this.inputRef) {
753
+ this.inputRef.focus()
754
+ }
755
+ }
756
+
700
757
  getOptionById(queryId) {
701
758
  return this.props.options.find(({ id }) => id === queryId)
702
759
  }
@@ -784,6 +841,7 @@ To mark an option as "highlighted", use the option's `isHighlighted` prop. Note
784
841
  }
785
842
 
786
843
  handleSelectOption = (event, { id }) => {
844
+ this.focusInput()
787
845
  const option = this.getOptionById(id)
788
846
  if (!option) return // prevent selecting of empty option
789
847
  this.setState((state) => ({
@@ -956,7 +1014,14 @@ To mark an option as "highlighted", use the option's `isHighlighted` prop. Note
956
1014
  const [selectedOptionId, setSelectedOptionId] = useState(['opt1', 'opt6'])
957
1015
  const [filteredOptions, setFilteredOptions] = useState(options)
958
1016
  const [announcement, setAnnouncement] = useState(null)
959
- const inputRef = useRef(null)
1017
+ const [inputElement, setInputElement] = useState(null)
1018
+
1019
+ const focusInput = () => {
1020
+ inputElement.blur()
1021
+ if (inputElement) {
1022
+ inputElement.focus()
1023
+ }
1024
+ }
960
1025
 
961
1026
  const getOptionById = (queryId) => {
962
1027
  return options.find(({ id }) => id === queryId)
@@ -1030,6 +1095,7 @@ To mark an option as "highlighted", use the option's `isHighlighted` prop. Note
1030
1095
  const handleSelectOption = (event, { id }) => {
1031
1096
  const option = getOptionById(id)
1032
1097
  if (!option) return // prevent selecting of empty option
1098
+ focusInput()
1033
1099
  setSelectedOptionId([...selectedOptionId, id])
1034
1100
  setHighlightedOptionId(null)
1035
1101
  setFilteredOptions(filterOptions(''))
@@ -1080,8 +1146,8 @@ To mark an option as "highlighted", use the option's `isHighlighted` prop. Note
1080
1146
  dismissible
1081
1147
  key={id}
1082
1148
  text={
1083
- <AccessibleContent alt={`Remove ${this.getOptionById(id).label}`}>
1084
- {this.getOptionById(id).label}
1149
+ <AccessibleContent alt={`Remove ${getOptionById(id).label}`}>
1150
+ {getOptionById(id).label}
1085
1151
  </AccessibleContent>
1086
1152
  }
1087
1153
  margin={
@@ -1099,7 +1165,9 @@ To mark an option as "highlighted", use the option's `isHighlighted` prop. Note
1099
1165
  assistiveText="Type or use arrow keys to navigate options. Multiple selections allowed."
1100
1166
  inputValue={inputValue}
1101
1167
  isShowingOptions={isShowingOptions}
1102
- inputRef={(el) => (inputRef.current = el)}
1168
+ inputRef={(el) => {
1169
+ setInputElement(el)
1170
+ }}
1103
1171
  onBlur={handleBlur}
1104
1172
  onInputChange={handleInputChange}
1105
1173
  onRequestShowOptions={handleShowOptions}
@@ -1180,6 +1248,15 @@ In addition to `<Select.Option />` Select also accepts `<Select.Group />` as chi
1180
1248
  announcement: null
1181
1249
  }
1182
1250
 
1251
+ inputElement = null
1252
+
1253
+ focusInput = () => {
1254
+ this.inputElement.blur()
1255
+ if (this.inputElement) {
1256
+ this.inputElement.focus()
1257
+ }
1258
+ }
1259
+
1183
1260
  getOptionById(id) {
1184
1261
  const { options } = this.props
1185
1262
  let match = null
@@ -1240,6 +1317,7 @@ In addition to `<Select.Option />` Select also accepts `<Select.Group />` as chi
1240
1317
  }
1241
1318
 
1242
1319
  handleSelectOption = (event, { id }) => {
1320
+ this.focusInput()
1243
1321
  this.setState({
1244
1322
  selectedOptionId: id,
1245
1323
  inputValue: this.getOptionById(id).label,
@@ -1322,6 +1400,9 @@ In addition to `<Select.Option />` Select also accepts `<Select.Group />` as chi
1322
1400
  margin="0 0 xxx-small 0"
1323
1401
  />
1324
1402
  }
1403
+ inputRef={(el) => {
1404
+ this.inputElement = el
1405
+ }}
1325
1406
  >
1326
1407
  {this.renderGroup()}
1327
1408
  </Select>
@@ -1368,6 +1449,14 @@ In addition to `<Select.Option />` Select also accepts `<Select.Group />` as chi
1368
1449
  options['Western'][0].id
1369
1450
  )
1370
1451
  const [announcement, setAnnouncement] = useState(null)
1452
+ const [inputElement, setInputElement] = useState(null)
1453
+
1454
+ const focusInput = () => {
1455
+ inputElement.blur()
1456
+ if (inputElement) {
1457
+ inputElement.focus()
1458
+ }
1459
+ }
1371
1460
 
1372
1461
  const getOptionById = (id) => {
1373
1462
  let match = null
@@ -1418,6 +1507,7 @@ In addition to `<Select.Option />` Select also accepts `<Select.Group />` as chi
1418
1507
  }
1419
1508
 
1420
1509
  const handleSelectOption = (event, { id }) => {
1510
+ focusInput()
1421
1511
  setSelectedOptionId(id)
1422
1512
  setInputValue(getOptionById(id).label)
1423
1513
  setIsShowingOptions(false)
@@ -1485,6 +1575,9 @@ In addition to `<Select.Option />` Select also accepts `<Select.Group />` as chi
1485
1575
  margin="0 0 xxx-small 0"
1486
1576
  />
1487
1577
  }
1578
+ inputRef={(el) => {
1579
+ setInputElement(el)
1580
+ }}
1488
1581
  >
1489
1582
  {renderGroup()}
1490
1583
  </Select>
@@ -1536,6 +1629,15 @@ Due to a WebKit bug if you are using `Select.Group` with autocomplete, the scree
1536
1629
  announcement: null
1537
1630
  }
1538
1631
 
1632
+ inputElement = null
1633
+
1634
+ focusInput = () => {
1635
+ this.inputElement.blur()
1636
+ if (this.inputElement) {
1637
+ this.inputElement.focus()
1638
+ }
1639
+ }
1640
+
1539
1641
  getOptionById(id) {
1540
1642
  const options = this.props.options
1541
1643
  return Object.values(options)
@@ -1593,6 +1695,7 @@ Due to a WebKit bug if you are using `Select.Group` with autocomplete, the scree
1593
1695
  handleSelectOption = (event, { id }) => {
1594
1696
  const option = this.getOptionById(id)
1595
1697
  if (!option) return // prevent selecting of empty option
1698
+ this.focusInput()
1596
1699
  this.setState({
1597
1700
  selectedOptionId: id,
1598
1701
  inputValue: option.label,
@@ -1672,6 +1775,9 @@ Due to a WebKit bug if you are using `Select.Group` with autocomplete, the scree
1672
1775
  onRequestHideOptions={this.handleHideOptions}
1673
1776
  onRequestHighlightOption={this.handleHighlightOption}
1674
1777
  onRequestSelectOption={this.handleSelectOption}
1778
+ inputRef={(el) => {
1779
+ this.inputElement = el
1780
+ }}
1675
1781
  >
1676
1782
  {this.renderGroup()}
1677
1783
  </Select>
@@ -1711,6 +1817,14 @@ Due to a WebKit bug if you are using `Select.Group` with autocomplete, the scree
1711
1817
  const [selectedOptionId, setSelectedOptionId] = useState(null)
1712
1818
  const [filteredOptions, setFilteredOptions] = useState(options)
1713
1819
  const [announcement, setAnnouncement] = useState(null)
1820
+ const [inputElement, setInputElement] = useState(null)
1821
+
1822
+ const focusInput = () => {
1823
+ inputElement.blur()
1824
+ if (inputElement) {
1825
+ inputElement.focus()
1826
+ }
1827
+ }
1714
1828
 
1715
1829
  const getOptionById = (id) => {
1716
1830
  return Object.values(options)
@@ -1757,6 +1871,7 @@ Due to a WebKit bug if you are using `Select.Group` with autocomplete, the scree
1757
1871
  const handleSelectOption = (event, { id }) => {
1758
1872
  const option = getOptionById(id)
1759
1873
  if (!option) return // prevent selecting of empty option
1874
+ focusInput()
1760
1875
  setSelectedOptionId(id)
1761
1876
  setInputValue(option.label)
1762
1877
  setIsShowingOptions(false)
@@ -1819,6 +1934,9 @@ Due to a WebKit bug if you are using `Select.Group` with autocomplete, the scree
1819
1934
  onRequestHideOptions={handleHideOptions}
1820
1935
  onRequestHighlightOption={handleHighlightOption}
1821
1936
  onRequestSelectOption={handleSelectOption}
1937
+ inputRef={(el) => {
1938
+ setInputElement(el)
1939
+ }}
1822
1940
  >
1823
1941
  {renderGroup()}
1824
1942
  </Select>
@@ -1866,6 +1984,15 @@ If no results match the user's search, it's recommended to leave `isShowingOptio
1866
1984
  announcement: null
1867
1985
  }
1868
1986
 
1987
+ inputElement = null
1988
+
1989
+ focusInput = () => {
1990
+ this.inputElement.blur()
1991
+ if (this.inputElement) {
1992
+ this.inputElement.focus()
1993
+ }
1994
+ }
1995
+
1869
1996
  timeoutId = null
1870
1997
 
1871
1998
  getOptionById(queryId) {
@@ -1941,6 +2068,7 @@ If no results match the user's search, it's recommended to leave `isShowingOptio
1941
2068
  handleSelectOption = (event, { id }) => {
1942
2069
  const option = this.getOptionById(id)
1943
2070
  if (!option) return // prevent selecting of empty option
2071
+ this.focusInput()
1944
2072
  this.setState({
1945
2073
  selectedOptionId: id,
1946
2074
  selectedOptionLabel: option.label,
@@ -2009,6 +2137,9 @@ If no results match the user's search, it's recommended to leave `isShowingOptio
2009
2137
  onRequestHideOptions={this.handleHideOptions}
2010
2138
  onRequestHighlightOption={this.handleHighlightOption}
2011
2139
  onRequestSelectOption={this.handleSelectOption}
2140
+ inputRef={(el) => {
2141
+ this.inputElement = el
2142
+ }}
2012
2143
  >
2013
2144
  {filteredOptions.length > 0 ? (
2014
2145
  filteredOptions.map((option) => {
@@ -2086,6 +2217,14 @@ If no results match the user's search, it's recommended to leave `isShowingOptio
2086
2217
  const [selectedOptionLabel, setSelectedOptionLabel] = useState('')
2087
2218
  const [filteredOptions, setFilteredOptions] = useState([])
2088
2219
  const [announcement, setAnnouncement] = useState(null)
2220
+ const [inputElement, setInputElement] = useState(null)
2221
+
2222
+ const focusInput = () => {
2223
+ inputElement.blur()
2224
+ if (inputElement) {
2225
+ inputElement.focus()
2226
+ }
2227
+ }
2089
2228
 
2090
2229
  let timeoutId = null
2091
2230
 
@@ -2151,6 +2290,7 @@ If no results match the user's search, it's recommended to leave `isShowingOptio
2151
2290
  const handleSelectOption = (event, { id }) => {
2152
2291
  const option = getOptionById(id)
2153
2292
  if (!option) return // prevent selecting of empty option
2293
+ focusInput()
2154
2294
  setSelectedOptionId(id)
2155
2295
  setSelectedOptionLabel(option.label)
2156
2296
  setInputValue(option.label)
@@ -2200,6 +2340,9 @@ If no results match the user's search, it's recommended to leave `isShowingOptio
2200
2340
  onRequestHideOptions={handleHideOptions}
2201
2341
  onRequestHighlightOption={handleHighlightOption}
2202
2342
  onRequestSelectOption={handleSelectOption}
2343
+ inputRef={(el) => {
2344
+ setInputElement(el)
2345
+ }}
2203
2346
  >
2204
2347
  {filteredOptions.length > 0 ? (
2205
2348
  filteredOptions.map((option) => {
@@ -2279,6 +2422,14 @@ To display icons (or other elements) before or after an option, pass it via the
2279
2422
  selectedOptionId: this.props.options[0].id,
2280
2423
  announcement: null
2281
2424
  }
2425
+ inputElement = null
2426
+
2427
+ focusInput = () => {
2428
+ this.inputElement.blur()
2429
+ if (this.inputElement) {
2430
+ this.inputElement.focus()
2431
+ }
2432
+ }
2282
2433
 
2283
2434
  getOptionById(queryId) {
2284
2435
  return this.props.options.find(({ id }) => id === queryId)
@@ -2323,6 +2474,7 @@ To display icons (or other elements) before or after an option, pass it via the
2323
2474
 
2324
2475
  handleSelectOption = (event, { id }) => {
2325
2476
  const option = this.getOptionById(id).label
2477
+ this.focusInput()
2326
2478
  this.setState({
2327
2479
  selectedOptionId: id,
2328
2480
  inputValue: option,
@@ -2352,6 +2504,9 @@ To display icons (or other elements) before or after an option, pass it via the
2352
2504
  onRequestHideOptions={this.handleHideOptions}
2353
2505
  onRequestHighlightOption={this.handleHighlightOption}
2354
2506
  onRequestSelectOption={this.handleSelectOption}
2507
+ inputRef={(el) => {
2508
+ this.inputElement = el
2509
+ }}
2355
2510
  >
2356
2511
  {this.props.options.map((option) => {
2357
2512
  return (
@@ -2417,6 +2572,14 @@ To display icons (or other elements) before or after an option, pass it via the
2417
2572
  const [highlightedOptionId, setHighlightedOptionId] = useState(null)
2418
2573
  const [selectedOptionId, setSelectedOptionId] = useState(options[0].id)
2419
2574
  const [announcement, setAnnouncement] = useState(null)
2575
+ const [inputElement, setInputElement] = useState(null)
2576
+
2577
+ const focusInput = () => {
2578
+ inputElement.blur()
2579
+ if (inputElement) {
2580
+ inputElement.focus()
2581
+ }
2582
+ }
2420
2583
 
2421
2584
  const getOptionById = (queryId) => {
2422
2585
  return options.find(({ id }) => id === queryId)
@@ -2452,6 +2615,7 @@ To display icons (or other elements) before or after an option, pass it via the
2452
2615
 
2453
2616
  const handleSelectOption = (event, { id }) => {
2454
2617
  const option = getOptionById(id).label
2618
+ focusInput()
2455
2619
  setSelectedOptionId(id)
2456
2620
  setInputValue(option)
2457
2621
  setIsShowingOptions(false)
@@ -2470,6 +2634,9 @@ To display icons (or other elements) before or after an option, pass it via the
2470
2634
  onRequestHideOptions={handleHideOptions}
2471
2635
  onRequestHighlightOption={handleHighlightOption}
2472
2636
  onRequestSelectOption={handleSelectOption}
2637
+ inputRef={(el) => {
2638
+ setInputElement(el)
2639
+ }}
2473
2640
  >
2474
2641
  {options.map((option) => {
2475
2642
  return (
@@ -2532,3 +2699,15 @@ To display icons (or other elements) before or after an option, pass it via the
2532
2699
  It's important to ensure screen reader users receive instruction and feedback while interacting with a `Select`, but screen reader support for the `combobox` role varies. The `assistiveText` prop should always be used to explain how a keyboard user can make a selection. Additionally, a live region should be updated with feedback as the component is interacted with, such as when options are filtered or highlighted. Using an [Alert](#Alert) with the `screenReaderOnly` prop is the easiest way to do this.
2533
2700
 
2534
2701
  > Note: This component uses a native `input` field to render the selected value. When it's included in a native HTML `form`, the text value will be sent to the backend instead of anything specified in the `value` field of the `Select.Option`-s. We do not recommend to use this component this way, rather write your own code that collects information and sends it to the backend.
2702
+
2703
+ ```js
2704
+ ---
2705
+ type: embed
2706
+ ---
2707
+ <Guidelines>
2708
+ <Figure recommendation="a11y" title="Accessibility">
2709
+ <Figure.Item>To ensure Select is accessible for iOS VoiceOver users, the input field’s focus must be blurred and then reapplied after selecting an option and closing the listbox. The examples above demonstrate this behavior.
2710
+ </Figure.Item>
2711
+ </Figure>
2712
+ </Guidelines>
2713
+ ```