@instructure/ui-select 10.19.2-snapshot-9 → 10.19.2-snapshot-11

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,12 +3,13 @@
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.19.2-snapshot-9](https://github.com/instructure/instructure-ui/compare/v10.19.1...v10.19.2-snapshot-9) (2025-06-13)
6
+ ## [10.19.2-snapshot-11](https://github.com/instructure/instructure-ui/compare/v10.19.1...v10.19.2-snapshot-11) (2025-06-13)
7
7
 
8
8
 
9
9
  ### Bug Fixes
10
10
 
11
11
  * **many:** update dependencies, browsersdb and moment timezone database ([3813636](https://github.com/instructure/instructure-ui/commit/3813636458c901ad4bc74a4d5ae015cb55defcb2))
12
+ * **ui-time-select,ui-simple-select,ui-select:** add missing keyboard interactions and fix duplicate SR announcements ([0f7ffa5](https://github.com/instructure/instructure-ui/commit/0f7ffa5b263b0b287ca1c2387e0b902189706cb2))
12
13
 
13
14
 
14
15
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@instructure/ui-select",
3
- "version": "10.19.2-snapshot-9",
3
+ "version": "10.19.2-snapshot-11",
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.19.2-snapshot-9",
27
- "@instructure/ui-babel-preset": "10.19.2-snapshot-9",
28
- "@instructure/ui-color-utils": "10.19.2-snapshot-9",
29
- "@instructure/ui-scripts": "10.19.2-snapshot-9",
30
- "@instructure/ui-test-utils": "10.19.2-snapshot-9",
31
- "@instructure/ui-themes": "10.19.2-snapshot-9",
26
+ "@instructure/ui-axe-check": "10.19.2-snapshot-11",
27
+ "@instructure/ui-babel-preset": "10.19.2-snapshot-11",
28
+ "@instructure/ui-color-utils": "10.19.2-snapshot-11",
29
+ "@instructure/ui-scripts": "10.19.2-snapshot-11",
30
+ "@instructure/ui-test-utils": "10.19.2-snapshot-11",
31
+ "@instructure/ui-themes": "10.19.2-snapshot-11",
32
32
  "@testing-library/jest-dom": "^6.6.3",
33
33
  "@testing-library/react": "^16.0.1",
34
34
  "@testing-library/user-event": "^14.6.1",
@@ -36,22 +36,22 @@
36
36
  },
37
37
  "dependencies": {
38
38
  "@babel/runtime": "^7.27.6",
39
- "@instructure/emotion": "10.19.2-snapshot-9",
40
- "@instructure/shared-types": "10.19.2-snapshot-9",
41
- "@instructure/ui-dom-utils": "10.19.2-snapshot-9",
42
- "@instructure/ui-form-field": "10.19.2-snapshot-9",
43
- "@instructure/ui-icons": "10.19.2-snapshot-9",
44
- "@instructure/ui-options": "10.19.2-snapshot-9",
45
- "@instructure/ui-popover": "10.19.2-snapshot-9",
46
- "@instructure/ui-position": "10.19.2-snapshot-9",
47
- "@instructure/ui-prop-types": "10.19.2-snapshot-9",
48
- "@instructure/ui-react-utils": "10.19.2-snapshot-9",
49
- "@instructure/ui-selectable": "10.19.2-snapshot-9",
50
- "@instructure/ui-testable": "10.19.2-snapshot-9",
51
- "@instructure/ui-text-input": "10.19.2-snapshot-9",
52
- "@instructure/ui-utils": "10.19.2-snapshot-9",
53
- "@instructure/ui-view": "10.19.2-snapshot-9",
54
- "@instructure/uid": "10.19.2-snapshot-9",
39
+ "@instructure/emotion": "10.19.2-snapshot-11",
40
+ "@instructure/shared-types": "10.19.2-snapshot-11",
41
+ "@instructure/ui-dom-utils": "10.19.2-snapshot-11",
42
+ "@instructure/ui-form-field": "10.19.2-snapshot-11",
43
+ "@instructure/ui-icons": "10.19.2-snapshot-11",
44
+ "@instructure/ui-options": "10.19.2-snapshot-11",
45
+ "@instructure/ui-popover": "10.19.2-snapshot-11",
46
+ "@instructure/ui-position": "10.19.2-snapshot-11",
47
+ "@instructure/ui-prop-types": "10.19.2-snapshot-11",
48
+ "@instructure/ui-react-utils": "10.19.2-snapshot-11",
49
+ "@instructure/ui-selectable": "10.19.2-snapshot-11",
50
+ "@instructure/ui-testable": "10.19.2-snapshot-11",
51
+ "@instructure/ui-text-input": "10.19.2-snapshot-11",
52
+ "@instructure/ui-utils": "10.19.2-snapshot-11",
53
+ "@instructure/ui-view": "10.19.2-snapshot-11",
54
+ "@instructure/uid": "10.19.2-snapshot-11",
55
55
  "prop-types": "^15.8.1"
56
56
  },
57
57
  "peerDependencies": {
@@ -40,14 +40,27 @@ describes: Select
40
40
  }
41
41
 
42
42
  handleShowOptions = (event) => {
43
+ const { options } = this.props
44
+ const { inputValue, selectedOptionId } = this.state
45
+
43
46
  this.setState({
44
47
  isShowingOptions: true
45
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
+ }
46
59
  }
47
60
 
48
61
  handleHideOptions = (event) => {
49
62
  const { selectedOptionId } = this.state
50
- const option = this.getOptionById(selectedOptionId).label
63
+ const option = this.getOptionById(selectedOptionId)?.label
51
64
  this.setState({
52
65
  isShowingOptions: false,
53
66
  highlightedOptionId: null,
@@ -68,17 +81,17 @@ describes: Select
68
81
  const nowOpen = !this.state.isShowingOptions
69
82
  ? `List expanded. ${optionsAvailable}`
70
83
  : ''
71
- const option = this.getOptionById(id).label
84
+ const option = this.getOptionById(id)?.label
72
85
  this.setState((state) => ({
73
86
  highlightedOptionId: id,
74
- inputValue: event.type === 'keydown' ? option : state.inputValue,
87
+ inputValue: state.inputValue,
75
88
  announcement: `${option} ${nowOpen}`
76
89
  }))
77
90
  }
78
91
 
79
92
  handleSelectOption = (event, { id }) => {
80
93
  this.focusInput()
81
- const option = this.getOptionById(id).label
94
+ const option = this.getOptionById(id)?.label
82
95
  this.setState({
83
96
  selectedOptionId: id,
84
97
  inputValue: option,
@@ -125,13 +138,6 @@ describes: Select
125
138
  )
126
139
  })}
127
140
  </Select>
128
- <Alert
129
- liveRegion={() => document.getElementById('flash-messages')}
130
- liveRegionPoliteness="assertive"
131
- screenReaderOnly
132
- >
133
- {announcement}
134
- </Alert>
135
141
  </div>
136
142
  )
137
143
  }
@@ -185,10 +191,20 @@ describes: Select
185
191
 
186
192
  const handleShowOptions = (event) => {
187
193
  setIsShowingOptions(true)
194
+ if (inputValue || selectedOptionId || options.length === 0) return
195
+
196
+ switch (event.key) {
197
+ case 'ArrowDown':
198
+ return handleHighlightOption(event, { id: options[0].id })
199
+ case 'ArrowUp':
200
+ return handleHighlightOption(event, {
201
+ id: options[options.length - 1].id
202
+ })
203
+ }
188
204
  }
189
205
 
190
206
  const handleHideOptions = (event) => {
191
- const option = getOptionById(selectedOptionId).label
207
+ const option = getOptionById(selectedOptionId)?.label
192
208
  setIsShowingOptions(false)
193
209
  setHighlightedOptionId(null)
194
210
  setSelectedOptionId(selectedOptionId ? option : '')
@@ -205,15 +221,15 @@ describes: Select
205
221
  const nowOpen = !isShowingOptions
206
222
  ? `List expanded. ${optionsAvailable}`
207
223
  : ''
208
- const option = getOptionById(id).label
224
+ const option = getOptionById(id)?.label
209
225
  setHighlightedOptionId(id)
210
- setInputValue(event.type === 'keydown' ? option : inputValue)
226
+ setInputValue(inputValue)
211
227
  setAnnouncement(`${option} ${nowOpen}`)
212
228
  }
213
229
 
214
230
  const handleSelectOption = (event, { id }) => {
215
- const option = getOptionById(id).label
216
231
  focusInput()
232
+ const option = getOptionById(id)?.label
217
233
  setSelectedOptionId(id)
218
234
  setInputValue(option)
219
235
  setIsShowingOptions(false)
@@ -249,13 +265,6 @@ describes: Select
249
265
  )
250
266
  })}
251
267
  </Select>
252
- <Alert
253
- liveRegion={() => document.getElementById('flash-messages')}
254
- liveRegionPoliteness="assertive"
255
- screenReaderOnly
256
- >
257
- {announcement}
258
- </Alert>
259
268
  </div>
260
269
  )
261
270
  }
@@ -378,10 +387,24 @@ It's best practice to always provide autocomplete functionality to help users ma
378
387
  }
379
388
 
380
389
  handleShowOptions = (event) => {
390
+ const { options } = this.props
391
+ const { inputValue, selectedOptionId } = this.state
392
+
381
393
  this.setState(({ filteredOptions }) => ({
382
394
  isShowingOptions: true,
383
395
  announcement: `List expanded. ${filteredOptions.length} options available.`
384
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
+ }
385
408
  }
386
409
 
387
410
  handleHideOptions = (event) => {
@@ -404,7 +427,7 @@ It's best practice to always provide autocomplete functionality to help users ma
404
427
  if (!option) return // prevent highlighting of empty option
405
428
  this.setState((state) => ({
406
429
  highlightedOptionId: id,
407
- inputValue: event.type === 'keydown' ? option.label : state.inputValue,
430
+ inputValue: state.inputValue,
408
431
  announcement: option.label
409
432
  }))
410
433
  }
@@ -490,13 +513,6 @@ It's best practice to always provide autocomplete functionality to help users ma
490
513
  </Select.Option>
491
514
  )}
492
515
  </Select>
493
- <Alert
494
- liveRegion={() => document.getElementById('flash-messages')}
495
- liveRegionPoliteness="assertive"
496
- screenReaderOnly
497
- >
498
- {announcement}
499
- </Alert>
500
516
  </div>
501
517
  )
502
518
  }
@@ -605,6 +621,16 @@ It's best practice to always provide autocomplete functionality to help users ma
605
621
  setAnnouncement(
606
622
  `List expanded. ${filteredOptions.length} options available.`
607
623
  )
624
+ if (inputValue || selectedOptionId || options.length === 0) return
625
+
626
+ switch (event.key) {
627
+ case 'ArrowDown':
628
+ return handleHighlightOption(event, { id: options[0].id })
629
+ case 'ArrowUp':
630
+ return handleHighlightOption(event, {
631
+ id: options[options.length - 1].id
632
+ })
633
+ }
608
634
  }
609
635
 
610
636
  const handleHideOptions = (event) => {
@@ -623,7 +649,7 @@ It's best practice to always provide autocomplete functionality to help users ma
623
649
  const option = getOptionById(id)
624
650
  if (!option) return // prevent highlighting of empty option
625
651
  setHighlightedOptionId(id)
626
- setInputValue(event.type === 'keydown' ? option.label : inputValue)
652
+ setInputValue(inputValue)
627
653
  setAnnouncement(option.label)
628
654
  }
629
655
 
@@ -694,13 +720,6 @@ It's best practice to always provide autocomplete functionality to help users ma
694
720
  </Select.Option>
695
721
  )}
696
722
  </Select>
697
- <Alert
698
- liveRegion={() => document.getElementById('flash-messages')}
699
- liveRegionPoliteness="assertive"
700
- screenReaderOnly
701
- >
702
- {announcement}
703
- </Alert>
704
723
  </div>
705
724
  )
706
725
  }
@@ -813,7 +832,29 @@ To mark an option as "highlighted", use the option's `isHighlighted` prop. Note
813
832
  }
814
833
 
815
834
  handleShowOptions = (event) => {
835
+ const { options } = this.props
836
+ const { inputValue, selectedOptionId } = this.state
837
+
816
838
  this.setState({ isShowingOptions: true })
839
+
840
+ if (inputValue || options.length === 0) return
841
+
842
+ switch (event.key) {
843
+ case 'ArrowDown':
844
+ return this.handleHighlightOption(event, {
845
+ id: options.find((option) => !selectedOptionId.includes(option.id))
846
+ .id
847
+ })
848
+ case 'ArrowUp':
849
+ // Highlight last non-selected option
850
+ return this.handleHighlightOption(event, {
851
+ id: options[
852
+ options.findLastIndex(
853
+ (option) => !selectedOptionId.includes(option.id)
854
+ )
855
+ ].id
856
+ })
857
+ }
817
858
  }
818
859
 
819
860
  handleHideOptions = (event) => {
@@ -835,7 +876,7 @@ To mark an option as "highlighted", use the option's `isHighlighted` prop. Note
835
876
  if (!option) return // prevent highlighting empty option
836
877
  this.setState((state) => ({
837
878
  highlightedOptionId: id,
838
- inputValue: event.type === 'keydown' ? option.label : state.inputValue,
879
+ inputValue: state.inputValue,
839
880
  announcement: option.label
840
881
  }))
841
882
  }
@@ -908,7 +949,7 @@ To mark an option as "highlighted", use the option's `isHighlighted` prop. Note
908
949
  key={id}
909
950
  text={
910
951
  <AccessibleContent alt={`Remove ${this.getOptionById(id).label}`}>
911
- {this.getOptionById(id).label}
952
+ {this.getOptionById(id)?.label}
912
953
  </AccessibleContent>
913
954
  }
914
955
  margin={
@@ -968,13 +1009,6 @@ To mark an option as "highlighted", use the option's `isHighlighted` prop. Note
968
1009
  </Select.Option>
969
1010
  )}
970
1011
  </Select>
971
- <Alert
972
- liveRegion={() => document.getElementById('flash-messages')}
973
- liveRegionPoliteness="assertive"
974
- screenReaderOnly
975
- >
976
- {announcement}
977
- </Alert>
978
1012
  </div>
979
1013
  )
980
1014
  }
@@ -1072,6 +1106,25 @@ To mark an option as "highlighted", use the option's `isHighlighted` prop. Note
1072
1106
 
1073
1107
  const handleShowOptions = (event) => {
1074
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
+ }
1075
1128
  }
1076
1129
 
1077
1130
  const handleHideOptions = (event) => {
@@ -1088,7 +1141,7 @@ To mark an option as "highlighted", use the option's `isHighlighted` prop. Note
1088
1141
  const option = getOptionById(id)
1089
1142
  if (!option) return // prevent highlighting empty option
1090
1143
  setHighlightedOptionId(id)
1091
- setInputValue(event.type === 'keydown' ? option.label : inputValue)
1144
+ setInputValue(inputValue)
1092
1145
  setAnnouncement(option.label)
1093
1146
  }
1094
1147
 
@@ -1197,13 +1250,6 @@ To mark an option as "highlighted", use the option's `isHighlighted` prop. Note
1197
1250
  </Select.Option>
1198
1251
  )}
1199
1252
  </Select>
1200
- <Alert
1201
- liveRegion={() => document.getElementById('flash-messages')}
1202
- liveRegionPoliteness="assertive"
1203
- screenReaderOnly
1204
- >
1205
- {announcement}
1206
- </Alert>
1207
1253
  </div>
1208
1254
  )
1209
1255
  }
@@ -1284,10 +1330,26 @@ In addition to `<Select.Option />` Select also accepts `<Select.Group />` as chi
1284
1330
  }
1285
1331
 
1286
1332
  handleShowOptions = (event) => {
1333
+ const { options } = this.props
1334
+ const { inputValue, selectedOptionId } = this.state
1335
+
1287
1336
  this.setState({
1288
1337
  isShowingOptions: true,
1289
1338
  highlightedOptionId: null
1290
1339
  })
1340
+
1341
+ if (inputValue || selectedOptionId || options.length === 0) return
1342
+
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
+ }
1291
1353
  }
1292
1354
 
1293
1355
  handleHideOptions = (event) => {
@@ -1295,7 +1357,7 @@ In addition to `<Select.Option />` Select also accepts `<Select.Group />` as chi
1295
1357
  this.setState({
1296
1358
  isShowingOptions: false,
1297
1359
  highlightedOptionId: null,
1298
- inputValue: this.getOptionById(selectedOptionId).label
1360
+ inputValue: this.getOptionById(selectedOptionId)?.label
1299
1361
  })
1300
1362
  }
1301
1363
 
@@ -1310,8 +1372,7 @@ In addition to `<Select.Option />` Select also accepts `<Select.Group />` as chi
1310
1372
  const newOption = this.getOptionById(id)
1311
1373
  this.setState((state) => ({
1312
1374
  highlightedOptionId: id,
1313
- inputValue:
1314
- event.type === 'keydown' ? newOption.label : state.inputValue,
1375
+ inputValue: state.inputValue,
1315
1376
  announcement: this.getGroupChangedMessage(newOption)
1316
1377
  }))
1317
1378
  }
@@ -1392,7 +1453,7 @@ In addition to `<Select.Option />` Select also accepts `<Select.Group />` as chi
1392
1453
  <Badge
1393
1454
  type="notification"
1394
1455
  variant={
1395
- this.getOptionById(selectedOptionId).group === 'Eastern'
1456
+ this.getOptionById(selectedOptionId)?.group === 'Eastern'
1396
1457
  ? 'success'
1397
1458
  : 'primary'
1398
1459
  }
@@ -1406,13 +1467,6 @@ In addition to `<Select.Option />` Select also accepts `<Select.Group />` as chi
1406
1467
  >
1407
1468
  {this.renderGroup()}
1408
1469
  </Select>
1409
- <Alert
1410
- liveRegion={() => document.getElementById('flash-messages')}
1411
- liveRegionPoliteness="assertive"
1412
- screenReaderOnly
1413
- >
1414
- {announcement}
1415
- </Alert>
1416
1470
  </div>
1417
1471
  )
1418
1472
  }
@@ -1432,7 +1486,7 @@ In addition to `<Select.Option />` Select also accepts `<Select.Group />` as chi
1432
1486
  { id: 'opt1', label: 'Alabama' },
1433
1487
  { id: 'opt2', label: 'Connecticut' },
1434
1488
  { id: 'opt3', label: 'Delaware' },
1435
- { id: '4', label: 'Illinois' }
1489
+ { id: 'opt4', label: 'Illinois' }
1436
1490
  ]
1437
1491
  }}
1438
1492
  />
@@ -1486,12 +1540,24 @@ In addition to `<Select.Option />` Select also accepts `<Select.Group />` as chi
1486
1540
  const handleShowOptions = (event) => {
1487
1541
  setIsShowingOptions(true)
1488
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
+ }
1489
1555
  }
1490
1556
 
1491
1557
  const handleHideOptions = (event) => {
1492
1558
  setIsShowingOptions(false)
1493
1559
  setHighlightedOptionId(null)
1494
- setInputValue(getOptionById(selectedOptionId).label)
1560
+ setInputValue(getOptionById(selectedOptionId)?.label)
1495
1561
  }
1496
1562
 
1497
1563
  const handleBlur = (event) => {
@@ -1502,7 +1568,7 @@ In addition to `<Select.Option />` Select also accepts `<Select.Group />` as chi
1502
1568
  event.persist()
1503
1569
  const newOption = getOptionById(id)
1504
1570
  setHighlightedOptionId(id)
1505
- setInputValue(event.type === 'keydown' ? newOption.label : inputValue)
1571
+ setInputValue(inputValue)
1506
1572
  setAnnouncement(getGroupChangedMessage(newOption))
1507
1573
  }
1508
1574
 
@@ -1567,7 +1633,7 @@ In addition to `<Select.Option />` Select also accepts `<Select.Group />` as chi
1567
1633
  <Badge
1568
1634
  type="notification"
1569
1635
  variant={
1570
- getOptionById(selectedOptionId).group === 'Eastern'
1636
+ getOptionById(selectedOptionId)?.group === 'Eastern'
1571
1637
  ? 'success'
1572
1638
  : 'primary'
1573
1639
  }
@@ -1581,13 +1647,6 @@ In addition to `<Select.Option />` Select also accepts `<Select.Group />` as chi
1581
1647
  >
1582
1648
  {renderGroup()}
1583
1649
  </Select>
1584
- <Alert
1585
- liveRegion={() => document.getElementById('flash-messages')}
1586
- liveRegionPoliteness="assertive"
1587
- screenReaderOnly
1588
- >
1589
- {announcement}
1590
- </Alert>
1591
1650
  </div>
1592
1651
  )
1593
1652
  }
@@ -1606,7 +1665,7 @@ In addition to `<Select.Option />` Select also accepts `<Select.Group />` as chi
1606
1665
  { id: 'opt1', label: 'Alabama' },
1607
1666
  { id: 'opt2', label: 'Connecticut' },
1608
1667
  { id: 'opt3', label: 'Delaware' },
1609
- { id: '4', label: 'Illinois' }
1668
+ { id: 'opt4', label: 'Illinois' }
1610
1669
  ]
1611
1670
  }}
1612
1671
  />
@@ -1659,10 +1718,26 @@ Due to a WebKit bug if you are using `Select.Group` with autocomplete, the scree
1659
1718
  }
1660
1719
 
1661
1720
  handleShowOptions = (event) => {
1721
+ const { options } = this.props
1722
+ const { inputValue, selectedOptionId } = this.state
1723
+
1662
1724
  this.setState({
1663
1725
  isShowingOptions: true,
1664
1726
  highlightedOptionId: null
1665
1727
  })
1728
+
1729
+ if (inputValue || selectedOptionId || options.length === 0) return
1730
+
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
+ }
1666
1741
  }
1667
1742
 
1668
1743
  handleHideOptions = (event) => {
@@ -1848,6 +1923,19 @@ Due to a WebKit bug if you are using `Select.Group` with autocomplete, the scree
1848
1923
  const handleShowOptions = (event) => {
1849
1924
  setIsShowingOptions(true)
1850
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
+ }
1851
1939
  }
1852
1940
 
1853
1941
  const handleHideOptions = (event) => {
@@ -2060,7 +2148,7 @@ If no results match the user's search, it's recommended to leave `isShowingOptio
2060
2148
  if (!option) return // prevent highlighting of empty option
2061
2149
  this.setState((state) => ({
2062
2150
  highlightedOptionId: id,
2063
- inputValue: event.type === 'keydown' ? option.label : state.inputValue,
2151
+ inputValue: state.inputValue,
2064
2152
  announcement: option.label
2065
2153
  }))
2066
2154
  }
@@ -2170,13 +2258,6 @@ If no results match the user's search, it's recommended to leave `isShowingOptio
2170
2258
  </Select.Option>
2171
2259
  )}
2172
2260
  </Select>
2173
- <Alert
2174
- liveRegion={() => document.getElementById('flash-messages')}
2175
- liveRegionPoliteness="assertive"
2176
- screenReaderOnly
2177
- >
2178
- {announcement}
2179
- </Alert>
2180
2261
  </div>
2181
2262
  )
2182
2263
  }
@@ -2283,7 +2364,7 @@ If no results match the user's search, it's recommended to leave `isShowingOptio
2283
2364
  if (!option) return // prevent highlighting of empty option
2284
2365
 
2285
2366
  setHighlightedOptionId(id)
2286
- setInputValue(event.type === 'keydown' ? option.label : inputValue)
2367
+ setInputValue(inputValue)
2287
2368
  setAnnouncement(option.label)
2288
2369
  }
2289
2370
 
@@ -2373,13 +2454,6 @@ If no results match the user's search, it's recommended to leave `isShowingOptio
2373
2454
  </Select.Option>
2374
2455
  )}
2375
2456
  </Select>
2376
- <Alert
2377
- liveRegion={() => document.getElementById('flash-messages')}
2378
- liveRegionPoliteness="assertive"
2379
- screenReaderOnly
2380
- >
2381
- {announcement}
2382
- </Alert>
2383
2457
  </div>
2384
2458
  )
2385
2459
  }
@@ -2436,14 +2510,28 @@ To display icons (or other elements) before or after an option, pass it via the
2436
2510
  }
2437
2511
 
2438
2512
  handleShowOptions = (event) => {
2513
+ const { options } = this.props
2514
+ const { inputValue, selectedOptionId } = this.state
2515
+
2439
2516
  this.setState({
2440
2517
  isShowingOptions: true
2441
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
+ }
2442
2530
  }
2443
2531
 
2444
2532
  handleHideOptions = (event) => {
2445
2533
  const { selectedOptionId } = this.state
2446
- const option = this.getOptionById(selectedOptionId).label
2534
+ const option = this.getOptionById(selectedOptionId)?.label
2447
2535
  this.setState({
2448
2536
  isShowingOptions: false,
2449
2537
  highlightedOptionId: null,
@@ -2467,7 +2555,7 @@ To display icons (or other elements) before or after an option, pass it via the
2467
2555
  const option = this.getOptionById(id).label
2468
2556
  this.setState((state) => ({
2469
2557
  highlightedOptionId: id,
2470
- inputValue: event.type === 'keydown' ? option : state.inputValue,
2558
+ inputValue: state.inputValue,
2471
2559
  announcement: `${option} ${nowOpen}`
2472
2560
  }))
2473
2561
  }
@@ -2522,13 +2610,6 @@ To display icons (or other elements) before or after an option, pass it via the
2522
2610
  )
2523
2611
  })}
2524
2612
  </Select>
2525
- <Alert
2526
- liveRegion={() => document.getElementById('flash-messages')}
2527
- liveRegionPoliteness="assertive"
2528
- screenReaderOnly
2529
- >
2530
- {announcement}
2531
- </Alert>
2532
2613
  </div>
2533
2614
  )
2534
2615
  }
@@ -2587,10 +2668,21 @@ To display icons (or other elements) before or after an option, pass it via the
2587
2668
 
2588
2669
  const handleShowOptions = (event) => {
2589
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
+ }
2590
2682
  }
2591
2683
 
2592
2684
  const handleHideOptions = (event) => {
2593
- const option = getOptionById(selectedOptionId).label
2685
+ const option = getOptionById(selectedOptionId)?.label
2594
2686
  setIsShowingOptions(false)
2595
2687
  setHighlightedOptionId(null)
2596
2688
  setInputValue(selectedOptionId ? option : '')
@@ -2609,7 +2701,7 @@ To display icons (or other elements) before or after an option, pass it via the
2609
2701
  : ''
2610
2702
  const option = getOptionById(id).label
2611
2703
  setHighlightedOptionId(id)
2612
- setInputValue(event.type === 'keydown' ? option : inputValue)
2704
+ setInputValue(inputValue)
2613
2705
  setAnnouncement(`${option} ${nowOpen}`)
2614
2706
  }
2615
2707
 
@@ -2652,13 +2744,6 @@ To display icons (or other elements) before or after an option, pass it via the
2652
2744
  )
2653
2745
  })}
2654
2746
  </Select>
2655
- <Alert
2656
- liveRegion={() => document.getElementById('flash-messages')}
2657
- liveRegionPoliteness="assertive"
2658
- screenReaderOnly
2659
- >
2660
- {announcement}
2661
- </Alert>
2662
2747
  </div>
2663
2748
  )
2664
2749
  }
@@ -2708,6 +2793,8 @@ type: embed
2708
2793
  <Figure recommendation="a11y" title="Accessibility">
2709
2794
  <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
2795
  </Figure.Item>
2796
+ <Figure.Item>If no option is selected initially, pressing the down arrow should open the listbox and move focus to the first option, while pressing up should move focus to the last item. You can see this behavior in the examples above.
2797
+ </Figure.Item>
2711
2798
  </Figure>
2712
2799
  </Guidelines>
2713
2800
  ```