@khanacademy/wonder-blocks-dropdown 2.7.2 → 2.7.5

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
@@ -1,5 +1,29 @@
1
1
  # @khanacademy/wonder-blocks-dropdown
2
2
 
3
+ ## 2.7.5
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [85c31780]
8
+ - @khanacademy/wonder-blocks-button@3.0.0
9
+
10
+ ## 2.7.4
11
+
12
+ ### Patch Changes
13
+
14
+ - Updated dependencies [83486dba]
15
+ - @khanacademy/wonder-blocks-icon@1.2.29
16
+ - @khanacademy/wonder-blocks-button@2.11.7
17
+ - @khanacademy/wonder-blocks-icon-button@3.4.8
18
+ - @khanacademy/wonder-blocks-modal@2.3.3
19
+ - @khanacademy/wonder-blocks-search-field@1.0.6
20
+
21
+ ## 2.7.3
22
+
23
+ ### Patch Changes
24
+
25
+ - d0a76cf5: Add live region to announce the number of options (fixing an iOS issue)
26
+
3
27
  ## 2.7.2
4
28
 
5
29
  ### Patch Changes
package/dist/es/index.js CHANGED
@@ -719,6 +719,7 @@ function DropdownPopper({
719
719
  }
720
720
 
721
721
  const VIRTUALIZE_THRESHOLD = 125;
722
+ const StyledSpan = addStyle("span");
722
723
 
723
724
  class DropdownCore extends React.Component {
724
725
  static sameItemsFocusable(prevItems, currentItems) {
@@ -1201,6 +1202,24 @@ class DropdownCore extends React.Component {
1201
1202
  }, isReferenceHidden => this.renderDropdownMenu(listRenderer, isReferenceHidden));
1202
1203
  }
1203
1204
 
1205
+ renderLiveRegion() {
1206
+ const {
1207
+ items,
1208
+ open
1209
+ } = this.props;
1210
+ const {
1211
+ labels
1212
+ } = this.state;
1213
+ const totalItems = this.hasSearchBox() ? items.length - 1 : items.length;
1214
+ return React.createElement(StyledSpan, {
1215
+ "aria-live": "polite",
1216
+ "aria-atomic": "true",
1217
+ "aria-relevant": "additions text",
1218
+ style: styles$3.srOnly,
1219
+ "data-test-id": "dropdown-live-region"
1220
+ }, open && labels.someSelected(totalItems));
1221
+ }
1222
+
1204
1223
  render() {
1205
1224
  const {
1206
1225
  open,
@@ -1213,7 +1232,7 @@ class DropdownCore extends React.Component {
1213
1232
  onKeyUp: this.handleKeyUp,
1214
1233
  style: [styles$3.menuWrapper, style],
1215
1234
  className: className
1216
- }, opener, open && this.renderDropdown());
1235
+ }, this.renderLiveRegion(), opener, open && this.renderDropdown());
1217
1236
  }
1218
1237
 
1219
1238
  }
@@ -1221,7 +1240,8 @@ class DropdownCore extends React.Component {
1221
1240
  DropdownCore.defaultProps = {
1222
1241
  alignment: "left",
1223
1242
  labels: {
1224
- noResults: defaultLabels.noResults
1243
+ noResults: defaultLabels.noResults,
1244
+ someSelected: defaultLabels.someSelected
1225
1245
  },
1226
1246
  light: false
1227
1247
  };
@@ -1249,6 +1269,16 @@ const styles$3 = StyleSheet.create({
1249
1269
  color: Color.offBlack64,
1250
1270
  alignSelf: "center",
1251
1271
  marginTop: Spacing.xxSmall_6
1272
+ },
1273
+ srOnly: {
1274
+ border: 0,
1275
+ clip: "rect(0,0,0,0)",
1276
+ height: 1,
1277
+ margin: -1,
1278
+ overflow: "hidden",
1279
+ padding: 0,
1280
+ position: "absolute",
1281
+ width: 1
1252
1282
  }
1253
1283
  });
1254
1284
  var DropdownCore$1 = withActionScheduler(DropdownCore);
@@ -2252,7 +2282,8 @@ class MultiSelect extends React.Component {
2252
2282
  searchText
2253
2283
  } = this.state;
2254
2284
  const {
2255
- noResults
2285
+ noResults,
2286
+ someSelected
2256
2287
  } = this.state.labels;
2257
2288
  const allChildren = React.Children.toArray(children).filter(Boolean);
2258
2289
  const numOptions = allChildren.length;
@@ -2273,7 +2304,8 @@ class MultiSelect extends React.Component {
2273
2304
  onSearchTextChanged: isFilterable ? this.handleSearchTextChanged : null,
2274
2305
  searchText: isFilterable ? searchText : "",
2275
2306
  labels: {
2276
- noResults
2307
+ noResults,
2308
+ someSelected
2277
2309
  }
2278
2310
  });
2279
2311
  }
package/dist/index.js CHANGED
@@ -787,6 +787,7 @@ DropdownOpener.defaultProps = {
787
787
  */
788
788
 
789
789
  const VIRTUALIZE_THRESHOLD = 125;
790
+ const StyledSpan = Object(_khanacademy_wonder_blocks_core__WEBPACK_IMPORTED_MODULE_6__["addStyle"])("span");
790
791
 
791
792
  /**
792
793
  * A core dropdown component that takes an opener and children to display as
@@ -1401,6 +1402,24 @@ class DropdownCore extends react__WEBPACK_IMPORTED_MODULE_0__["Component"] {
1401
1402
  }, isReferenceHidden => this.renderDropdownMenu(listRenderer, isReferenceHidden));
1402
1403
  }
1403
1404
 
1405
+ renderLiveRegion() {
1406
+ const {
1407
+ items,
1408
+ open
1409
+ } = this.props;
1410
+ const {
1411
+ labels
1412
+ } = this.state;
1413
+ const totalItems = this.hasSearchBox() ? items.length - 1 : items.length;
1414
+ return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["createElement"](StyledSpan, {
1415
+ "aria-live": "polite",
1416
+ "aria-atomic": "true",
1417
+ "aria-relevant": "additions text",
1418
+ style: styles.srOnly,
1419
+ "data-test-id": "dropdown-live-region"
1420
+ }, open && labels.someSelected(totalItems));
1421
+ }
1422
+
1404
1423
  render() {
1405
1424
  const {
1406
1425
  open,
@@ -1413,7 +1432,7 @@ class DropdownCore extends react__WEBPACK_IMPORTED_MODULE_0__["Component"] {
1413
1432
  onKeyUp: this.handleKeyUp,
1414
1433
  style: [styles.menuWrapper, style],
1415
1434
  className: className
1416
- }, opener, open && this.renderDropdown());
1435
+ }, this.renderLiveRegion(), opener, open && this.renderDropdown());
1417
1436
  }
1418
1437
 
1419
1438
  }
@@ -1421,7 +1440,8 @@ class DropdownCore extends react__WEBPACK_IMPORTED_MODULE_0__["Component"] {
1421
1440
  DropdownCore.defaultProps = {
1422
1441
  alignment: "left",
1423
1442
  labels: {
1424
- noResults: _util_constants_js__WEBPACK_IMPORTED_MODULE_12__[/* defaultLabels */ "e"].noResults
1443
+ noResults: _util_constants_js__WEBPACK_IMPORTED_MODULE_12__[/* defaultLabels */ "e"].noResults,
1444
+ someSelected: _util_constants_js__WEBPACK_IMPORTED_MODULE_12__[/* defaultLabels */ "e"].someSelected
1425
1445
  },
1426
1446
  light: false
1427
1447
  };
@@ -1450,6 +1470,16 @@ const styles = aphrodite__WEBPACK_IMPORTED_MODULE_2__["StyleSheet"].create({
1450
1470
  color: _khanacademy_wonder_blocks_color__WEBPACK_IMPORTED_MODULE_4___default.a.offBlack64,
1451
1471
  alignSelf: "center",
1452
1472
  marginTop: _khanacademy_wonder_blocks_spacing__WEBPACK_IMPORTED_MODULE_5___default.a.xxSmall_6
1473
+ },
1474
+ srOnly: {
1475
+ border: 0,
1476
+ clip: "rect(0,0,0,0)",
1477
+ height: 1,
1478
+ margin: -1,
1479
+ overflow: "hidden",
1480
+ padding: 0,
1481
+ position: "absolute",
1482
+ width: 1
1453
1483
  }
1454
1484
  });
1455
1485
  /* harmony default export */ __webpack_exports__["a"] = (Object(_khanacademy_wonder_blocks_timing__WEBPACK_IMPORTED_MODULE_8__["withActionScheduler"])(DropdownCore));
@@ -2632,7 +2662,8 @@ class MultiSelect extends react__WEBPACK_IMPORTED_MODULE_0__["Component"] {
2632
2662
  searchText
2633
2663
  } = this.state;
2634
2664
  const {
2635
- noResults
2665
+ noResults,
2666
+ someSelected
2636
2667
  } = this.state.labels;
2637
2668
  const allChildren = react__WEBPACK_IMPORTED_MODULE_0__["Children"].toArray(children).filter(Boolean);
2638
2669
  const numOptions = allChildren.length;
@@ -2653,7 +2684,8 @@ class MultiSelect extends react__WEBPACK_IMPORTED_MODULE_0__["Component"] {
2653
2684
  onSearchTextChanged: isFilterable ? this.handleSearchTextChanged : null,
2654
2685
  searchText: isFilterable ? searchText : "",
2655
2686
  labels: {
2656
- noResults
2687
+ noResults,
2688
+ someSelected
2657
2689
  }
2658
2690
  });
2659
2691
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@khanacademy/wonder-blocks-dropdown",
3
- "version": "2.7.2",
3
+ "version": "2.7.5",
4
4
  "design": "v1",
5
5
  "description": "Dropdown variants for Wonder Blocks.",
6
6
  "main": "dist/index.js",
@@ -16,15 +16,15 @@
16
16
  },
17
17
  "dependencies": {
18
18
  "@babel/runtime": "^7.16.3",
19
- "@khanacademy/wonder-blocks-button": "^2.11.6",
19
+ "@khanacademy/wonder-blocks-button": "^3.0.0",
20
20
  "@khanacademy/wonder-blocks-clickable": "^2.2.7",
21
21
  "@khanacademy/wonder-blocks-color": "^1.1.20",
22
22
  "@khanacademy/wonder-blocks-core": "^4.3.2",
23
- "@khanacademy/wonder-blocks-icon": "^1.2.28",
24
- "@khanacademy/wonder-blocks-icon-button": "^3.4.7",
23
+ "@khanacademy/wonder-blocks-icon": "^1.2.29",
24
+ "@khanacademy/wonder-blocks-icon-button": "^3.4.8",
25
25
  "@khanacademy/wonder-blocks-layout": "^1.4.10",
26
- "@khanacademy/wonder-blocks-modal": "^2.3.2",
27
- "@khanacademy/wonder-blocks-search-field": "^1.0.5",
26
+ "@khanacademy/wonder-blocks-modal": "^2.3.3",
27
+ "@khanacademy/wonder-blocks-search-field": "^1.0.6",
28
28
  "@khanacademy/wonder-blocks-spacing": "^3.0.5",
29
29
  "@khanacademy/wonder-blocks-timing": "^2.1.0",
30
30
  "@khanacademy/wonder-blocks-typography": "^1.1.32"
@@ -43,6 +43,25 @@ exports[`wonder-blocks-dropdown example 1 1`] = `
43
43
  }
44
44
  }
45
45
  >
46
+ <span
47
+ aria-atomic="true"
48
+ aria-live="polite"
49
+ aria-relevant="additions text"
50
+ className=""
51
+ data-test-id="dropdown-live-region"
52
+ style={
53
+ Object {
54
+ "border": 0,
55
+ "clip": "rect(0,0,0,0)",
56
+ "height": 1,
57
+ "margin": -1,
58
+ "overflow": "hidden",
59
+ "padding": 0,
60
+ "position": "absolute",
61
+ "width": 1,
62
+ }
63
+ }
64
+ />
46
65
  <button
47
66
  aria-expanded="false"
48
67
  aria-haspopup="menu"
@@ -246,6 +265,25 @@ exports[`wonder-blocks-dropdown example 2 1`] = `
246
265
  }
247
266
  }
248
267
  >
268
+ <span
269
+ aria-atomic="true"
270
+ aria-live="polite"
271
+ aria-relevant="additions text"
272
+ className=""
273
+ data-test-id="dropdown-live-region"
274
+ style={
275
+ Object {
276
+ "border": 0,
277
+ "clip": "rect(0,0,0,0)",
278
+ "height": 1,
279
+ "margin": -1,
280
+ "overflow": "hidden",
281
+ "padding": 0,
282
+ "position": "absolute",
283
+ "width": 1,
284
+ }
285
+ }
286
+ />
249
287
  <button
250
288
  aria-expanded="false"
251
289
  aria-haspopup="menu"
@@ -427,6 +465,25 @@ exports[`wonder-blocks-dropdown example 3 1`] = `
427
465
  }
428
466
  }
429
467
  >
468
+ <span
469
+ aria-atomic="true"
470
+ aria-live="polite"
471
+ aria-relevant="additions text"
472
+ className=""
473
+ data-test-id="dropdown-live-region"
474
+ style={
475
+ Object {
476
+ "border": 0,
477
+ "clip": "rect(0,0,0,0)",
478
+ "height": 1,
479
+ "margin": -1,
480
+ "overflow": "hidden",
481
+ "padding": 0,
482
+ "position": "absolute",
483
+ "width": 1,
484
+ }
485
+ }
486
+ />
430
487
  <button
431
488
  aria-expanded="false"
432
489
  aria-haspopup="menu"
@@ -608,6 +665,25 @@ exports[`wonder-blocks-dropdown example 4 1`] = `
608
665
  }
609
666
  }
610
667
  >
668
+ <span
669
+ aria-atomic="true"
670
+ aria-live="polite"
671
+ aria-relevant="additions text"
672
+ className=""
673
+ data-test-id="dropdown-live-region"
674
+ style={
675
+ Object {
676
+ "border": 0,
677
+ "clip": "rect(0,0,0,0)",
678
+ "height": 1,
679
+ "margin": -1,
680
+ "overflow": "hidden",
681
+ "padding": 0,
682
+ "position": "absolute",
683
+ "width": 1,
684
+ }
685
+ }
686
+ />
611
687
  <button
612
688
  aria-expanded="false"
613
689
  aria-haspopup="menu"
@@ -789,6 +865,25 @@ exports[`wonder-blocks-dropdown example 5 1`] = `
789
865
  }
790
866
  }
791
867
  >
868
+ <span
869
+ aria-atomic="true"
870
+ aria-live="polite"
871
+ aria-relevant="additions text"
872
+ className=""
873
+ data-test-id="dropdown-live-region"
874
+ style={
875
+ Object {
876
+ "border": 0,
877
+ "clip": "rect(0,0,0,0)",
878
+ "height": 1,
879
+ "margin": -1,
880
+ "overflow": "hidden",
881
+ "padding": 0,
882
+ "position": "absolute",
883
+ "width": 1,
884
+ }
885
+ }
886
+ />
792
887
  <button
793
888
  aria-expanded="false"
794
889
  aria-haspopup="menu"
@@ -971,6 +1066,25 @@ exports[`wonder-blocks-dropdown example 6 1`] = `
971
1066
  }
972
1067
  }
973
1068
  >
1069
+ <span
1070
+ aria-atomic="true"
1071
+ aria-live="polite"
1072
+ aria-relevant="additions text"
1073
+ className=""
1074
+ data-test-id="dropdown-live-region"
1075
+ style={
1076
+ Object {
1077
+ "border": 0,
1078
+ "clip": "rect(0,0,0,0)",
1079
+ "height": 1,
1080
+ "margin": -1,
1081
+ "overflow": "hidden",
1082
+ "padding": 0,
1083
+ "position": "absolute",
1084
+ "width": 1,
1085
+ }
1086
+ }
1087
+ />
974
1088
  <button
975
1089
  aria-expanded="false"
976
1090
  aria-haspopup="menu"
@@ -1231,6 +1345,25 @@ exports[`wonder-blocks-dropdown example 7 1`] = `
1231
1345
  }
1232
1346
  }
1233
1347
  >
1348
+ <span
1349
+ aria-atomic="true"
1350
+ aria-live="polite"
1351
+ aria-relevant="additions text"
1352
+ className=""
1353
+ data-test-id="dropdown-live-region"
1354
+ style={
1355
+ Object {
1356
+ "border": 0,
1357
+ "clip": "rect(0,0,0,0)",
1358
+ "height": 1,
1359
+ "margin": -1,
1360
+ "overflow": "hidden",
1361
+ "padding": 0,
1362
+ "position": "absolute",
1363
+ "width": 1,
1364
+ }
1365
+ }
1366
+ />
1234
1367
  <span
1235
1368
  className=""
1236
1369
  data-test-id="teacher-menu-custom-opener"
@@ -1309,6 +1442,25 @@ exports[`wonder-blocks-dropdown example 8 1`] = `
1309
1442
  }
1310
1443
  }
1311
1444
  >
1445
+ <span
1446
+ aria-atomic="true"
1447
+ aria-live="polite"
1448
+ aria-relevant="additions text"
1449
+ className=""
1450
+ data-test-id="dropdown-live-region"
1451
+ style={
1452
+ Object {
1453
+ "border": 0,
1454
+ "clip": "rect(0,0,0,0)",
1455
+ "height": 1,
1456
+ "margin": -1,
1457
+ "overflow": "hidden",
1458
+ "padding": 0,
1459
+ "position": "absolute",
1460
+ "width": 1,
1461
+ }
1462
+ }
1463
+ />
1312
1464
  <button
1313
1465
  aria-expanded="false"
1314
1466
  aria-haspopup="listbox"
@@ -1446,6 +1598,25 @@ exports[`wonder-blocks-dropdown example 9 1`] = `
1446
1598
  }
1447
1599
  }
1448
1600
  >
1601
+ <span
1602
+ aria-atomic="true"
1603
+ aria-live="polite"
1604
+ aria-relevant="additions text"
1605
+ className=""
1606
+ data-test-id="dropdown-live-region"
1607
+ style={
1608
+ Object {
1609
+ "border": 0,
1610
+ "clip": "rect(0,0,0,0)",
1611
+ "height": 1,
1612
+ "margin": -1,
1613
+ "overflow": "hidden",
1614
+ "padding": 0,
1615
+ "position": "absolute",
1616
+ "width": 1,
1617
+ }
1618
+ }
1619
+ />
1449
1620
  <button
1450
1621
  aria-expanded="false"
1451
1622
  aria-haspopup="listbox"
@@ -1582,6 +1753,25 @@ exports[`wonder-blocks-dropdown example 10 1`] = `
1582
1753
  }
1583
1754
  }
1584
1755
  >
1756
+ <span
1757
+ aria-atomic="true"
1758
+ aria-live="polite"
1759
+ aria-relevant="additions text"
1760
+ className=""
1761
+ data-test-id="dropdown-live-region"
1762
+ style={
1763
+ Object {
1764
+ "border": 0,
1765
+ "clip": "rect(0,0,0,0)",
1766
+ "height": 1,
1767
+ "margin": -1,
1768
+ "overflow": "hidden",
1769
+ "padding": 0,
1770
+ "position": "absolute",
1771
+ "width": 1,
1772
+ }
1773
+ }
1774
+ />
1585
1775
  <button
1586
1776
  aria-expanded="false"
1587
1777
  aria-haspopup="listbox"
@@ -1744,6 +1934,25 @@ exports[`wonder-blocks-dropdown example 11 1`] = `
1744
1934
  }
1745
1935
  }
1746
1936
  >
1937
+ <span
1938
+ aria-atomic="true"
1939
+ aria-live="polite"
1940
+ aria-relevant="additions text"
1941
+ className=""
1942
+ data-test-id="dropdown-live-region"
1943
+ style={
1944
+ Object {
1945
+ "border": 0,
1946
+ "clip": "rect(0,0,0,0)",
1947
+ "height": 1,
1948
+ "margin": -1,
1949
+ "overflow": "hidden",
1950
+ "padding": 0,
1951
+ "position": "absolute",
1952
+ "width": 1,
1953
+ }
1954
+ }
1955
+ />
1747
1956
  <button
1748
1957
  aria-expanded="false"
1749
1958
  aria-haspopup="listbox"
@@ -1881,6 +2090,25 @@ exports[`wonder-blocks-dropdown example 12 1`] = `
1881
2090
  }
1882
2091
  }
1883
2092
  >
2093
+ <span
2094
+ aria-atomic="true"
2095
+ aria-live="polite"
2096
+ aria-relevant="additions text"
2097
+ className=""
2098
+ data-test-id="dropdown-live-region"
2099
+ style={
2100
+ Object {
2101
+ "border": 0,
2102
+ "clip": "rect(0,0,0,0)",
2103
+ "height": 1,
2104
+ "margin": -1,
2105
+ "overflow": "hidden",
2106
+ "padding": 0,
2107
+ "position": "absolute",
2108
+ "width": 1,
2109
+ }
2110
+ }
2111
+ />
1884
2112
  <button
1885
2113
  aria-expanded="false"
1886
2114
  aria-haspopup="listbox"
@@ -2036,6 +2264,25 @@ exports[`wonder-blocks-dropdown example 13 1`] = `
2036
2264
  }
2037
2265
  }
2038
2266
  >
2267
+ <span
2268
+ aria-atomic="true"
2269
+ aria-live="polite"
2270
+ aria-relevant="additions text"
2271
+ className=""
2272
+ data-test-id="dropdown-live-region"
2273
+ style={
2274
+ Object {
2275
+ "border": 0,
2276
+ "clip": "rect(0,0,0,0)",
2277
+ "height": 1,
2278
+ "margin": -1,
2279
+ "overflow": "hidden",
2280
+ "padding": 0,
2281
+ "position": "absolute",
2282
+ "width": 1,
2283
+ }
2284
+ }
2285
+ />
2039
2286
  <button
2040
2287
  aria-expanded="false"
2041
2288
  aria-haspopup="listbox"
@@ -2174,6 +2421,25 @@ exports[`wonder-blocks-dropdown example 14 1`] = `
2174
2421
  }
2175
2422
  }
2176
2423
  >
2424
+ <span
2425
+ aria-atomic="true"
2426
+ aria-live="polite"
2427
+ aria-relevant="additions text"
2428
+ className=""
2429
+ data-test-id="dropdown-live-region"
2430
+ style={
2431
+ Object {
2432
+ "border": 0,
2433
+ "clip": "rect(0,0,0,0)",
2434
+ "height": 1,
2435
+ "margin": -1,
2436
+ "overflow": "hidden",
2437
+ "padding": 0,
2438
+ "position": "absolute",
2439
+ "width": 1,
2440
+ }
2441
+ }
2442
+ />
2177
2443
  <button
2178
2444
  aria-expanded="false"
2179
2445
  aria-haspopup="listbox"
@@ -2389,6 +2655,25 @@ exports[`wonder-blocks-dropdown example 15 1`] = `
2389
2655
  }
2390
2656
  }
2391
2657
  >
2658
+ <span
2659
+ aria-atomic="true"
2660
+ aria-live="polite"
2661
+ aria-relevant="additions text"
2662
+ className=""
2663
+ data-test-id="dropdown-live-region"
2664
+ style={
2665
+ Object {
2666
+ "border": 0,
2667
+ "clip": "rect(0,0,0,0)",
2668
+ "height": 1,
2669
+ "margin": -1,
2670
+ "overflow": "hidden",
2671
+ "padding": 0,
2672
+ "position": "absolute",
2673
+ "width": 1,
2674
+ }
2675
+ }
2676
+ />
2392
2677
  <h2
2393
2678
  className=""
2394
2679
  data-test-id="single-select-custom-opener"
@@ -2474,6 +2759,25 @@ exports[`wonder-blocks-dropdown example 16 1`] = `
2474
2759
  }
2475
2760
  }
2476
2761
  >
2762
+ <span
2763
+ aria-atomic="true"
2764
+ aria-live="polite"
2765
+ aria-relevant="additions text"
2766
+ className=""
2767
+ data-test-id="dropdown-live-region"
2768
+ style={
2769
+ Object {
2770
+ "border": 0,
2771
+ "clip": "rect(0,0,0,0)",
2772
+ "height": 1,
2773
+ "margin": -1,
2774
+ "overflow": "hidden",
2775
+ "padding": 0,
2776
+ "position": "absolute",
2777
+ "width": 1,
2778
+ }
2779
+ }
2780
+ />
2477
2781
  <button
2478
2782
  aria-expanded="false"
2479
2783
  aria-haspopup="listbox"
@@ -2610,6 +2914,25 @@ exports[`wonder-blocks-dropdown example 17 1`] = `
2610
2914
  }
2611
2915
  }
2612
2916
  >
2917
+ <span
2918
+ aria-atomic="true"
2919
+ aria-live="polite"
2920
+ aria-relevant="additions text"
2921
+ className=""
2922
+ data-test-id="dropdown-live-region"
2923
+ style={
2924
+ Object {
2925
+ "border": 0,
2926
+ "clip": "rect(0,0,0,0)",
2927
+ "height": 1,
2928
+ "margin": -1,
2929
+ "overflow": "hidden",
2930
+ "padding": 0,
2931
+ "position": "absolute",
2932
+ "width": 1,
2933
+ }
2934
+ }
2935
+ />
2613
2936
  <button
2614
2937
  aria-expanded="false"
2615
2938
  aria-haspopup="listbox"
@@ -2747,6 +3070,25 @@ exports[`wonder-blocks-dropdown example 18 1`] = `
2747
3070
  }
2748
3071
  }
2749
3072
  >
3073
+ <span
3074
+ aria-atomic="true"
3075
+ aria-live="polite"
3076
+ aria-relevant="additions text"
3077
+ className=""
3078
+ data-test-id="dropdown-live-region"
3079
+ style={
3080
+ Object {
3081
+ "border": 0,
3082
+ "clip": "rect(0,0,0,0)",
3083
+ "height": 1,
3084
+ "margin": -1,
3085
+ "overflow": "hidden",
3086
+ "padding": 0,
3087
+ "position": "absolute",
3088
+ "width": 1,
3089
+ }
3090
+ }
3091
+ />
2750
3092
  <button
2751
3093
  aria-expanded="false"
2752
3094
  aria-haspopup="listbox"
@@ -2883,6 +3225,25 @@ exports[`wonder-blocks-dropdown example 19 1`] = `
2883
3225
  }
2884
3226
  }
2885
3227
  >
3228
+ <span
3229
+ aria-atomic="true"
3230
+ aria-live="polite"
3231
+ aria-relevant="additions text"
3232
+ className=""
3233
+ data-test-id="dropdown-live-region"
3234
+ style={
3235
+ Object {
3236
+ "border": 0,
3237
+ "clip": "rect(0,0,0,0)",
3238
+ "height": 1,
3239
+ "margin": -1,
3240
+ "overflow": "hidden",
3241
+ "padding": 0,
3242
+ "position": "absolute",
3243
+ "width": 1,
3244
+ }
3245
+ }
3246
+ />
2886
3247
  <button
2887
3248
  aria-expanded="false"
2888
3249
  aria-haspopup="listbox"
@@ -3114,6 +3475,25 @@ exports[`wonder-blocks-dropdown example 21 1`] = `
3114
3475
  }
3115
3476
  }
3116
3477
  >
3478
+ <span
3479
+ aria-atomic="true"
3480
+ aria-live="polite"
3481
+ aria-relevant="additions text"
3482
+ className=""
3483
+ data-test-id="dropdown-live-region"
3484
+ style={
3485
+ Object {
3486
+ "border": 0,
3487
+ "clip": "rect(0,0,0,0)",
3488
+ "height": 1,
3489
+ "margin": -1,
3490
+ "overflow": "hidden",
3491
+ "padding": 0,
3492
+ "position": "absolute",
3493
+ "width": 1,
3494
+ }
3495
+ }
3496
+ />
3117
3497
  <button
3118
3498
  aria-expanded="false"
3119
3499
  aria-haspopup="listbox"
@@ -3269,6 +3649,25 @@ exports[`wonder-blocks-dropdown example 22 1`] = `
3269
3649
  }
3270
3650
  }
3271
3651
  >
3652
+ <span
3653
+ aria-atomic="true"
3654
+ aria-live="polite"
3655
+ aria-relevant="additions text"
3656
+ className=""
3657
+ data-test-id="dropdown-live-region"
3658
+ style={
3659
+ Object {
3660
+ "border": 0,
3661
+ "clip": "rect(0,0,0,0)",
3662
+ "height": 1,
3663
+ "margin": -1,
3664
+ "overflow": "hidden",
3665
+ "padding": 0,
3666
+ "position": "absolute",
3667
+ "width": 1,
3668
+ }
3669
+ }
3670
+ />
3272
3671
  <button
3273
3672
  aria-expanded="false"
3274
3673
  aria-haspopup="listbox"
@@ -3407,6 +3806,25 @@ exports[`wonder-blocks-dropdown example 23 1`] = `
3407
3806
  }
3408
3807
  }
3409
3808
  >
3809
+ <span
3810
+ aria-atomic="true"
3811
+ aria-live="polite"
3812
+ aria-relevant="additions text"
3813
+ className=""
3814
+ data-test-id="dropdown-live-region"
3815
+ style={
3816
+ Object {
3817
+ "border": 0,
3818
+ "clip": "rect(0,0,0,0)",
3819
+ "height": 1,
3820
+ "margin": -1,
3821
+ "overflow": "hidden",
3822
+ "padding": 0,
3823
+ "position": "absolute",
3824
+ "width": 1,
3825
+ }
3826
+ }
3827
+ />
3410
3828
  <button
3411
3829
  aria-expanded="false"
3412
3830
  aria-haspopup="listbox"
@@ -3543,6 +3961,25 @@ exports[`wonder-blocks-dropdown example 24 1`] = `
3543
3961
  }
3544
3962
  }
3545
3963
  >
3964
+ <span
3965
+ aria-atomic="true"
3966
+ aria-live="polite"
3967
+ aria-relevant="additions text"
3968
+ className=""
3969
+ data-test-id="dropdown-live-region"
3970
+ style={
3971
+ Object {
3972
+ "border": 0,
3973
+ "clip": "rect(0,0,0,0)",
3974
+ "height": 1,
3975
+ "margin": -1,
3976
+ "overflow": "hidden",
3977
+ "padding": 0,
3978
+ "position": "absolute",
3979
+ "width": 1,
3980
+ }
3981
+ }
3982
+ />
3546
3983
  <button
3547
3984
  aria-expanded="false"
3548
3985
  aria-haspopup="listbox"
@@ -3777,6 +4214,25 @@ exports[`wonder-blocks-dropdown example 25 1`] = `
3777
4214
  }
3778
4215
  }
3779
4216
  >
4217
+ <span
4218
+ aria-atomic="true"
4219
+ aria-live="polite"
4220
+ aria-relevant="additions text"
4221
+ className=""
4222
+ data-test-id="dropdown-live-region"
4223
+ style={
4224
+ Object {
4225
+ "border": 0,
4226
+ "clip": "rect(0,0,0,0)",
4227
+ "height": 1,
4228
+ "margin": -1,
4229
+ "overflow": "hidden",
4230
+ "padding": 0,
4231
+ "position": "absolute",
4232
+ "width": 1,
4233
+ }
4234
+ }
4235
+ />
3780
4236
  <button
3781
4237
  aria-expanded="false"
3782
4238
  aria-haspopup="listbox"
@@ -3894,6 +4350,25 @@ exports[`wonder-blocks-dropdown example 26 1`] = `
3894
4350
  }
3895
4351
  }
3896
4352
  >
4353
+ <span
4354
+ aria-atomic="true"
4355
+ aria-live="polite"
4356
+ aria-relevant="additions text"
4357
+ className=""
4358
+ data-test-id="dropdown-live-region"
4359
+ style={
4360
+ Object {
4361
+ "border": 0,
4362
+ "clip": "rect(0,0,0,0)",
4363
+ "height": 1,
4364
+ "margin": -1,
4365
+ "overflow": "hidden",
4366
+ "padding": 0,
4367
+ "position": "absolute",
4368
+ "width": 1,
4369
+ }
4370
+ }
4371
+ />
3897
4372
  <h2
3898
4373
  className=""
3899
4374
  data-test-id="multi-select-custom-opener"
@@ -3960,6 +4435,25 @@ exports[`wonder-blocks-dropdown example 27 1`] = `
3960
4435
  }
3961
4436
  }
3962
4437
  >
4438
+ <span
4439
+ aria-atomic="true"
4440
+ aria-live="polite"
4441
+ aria-relevant="additions text"
4442
+ className=""
4443
+ data-test-id="dropdown-live-region"
4444
+ style={
4445
+ Object {
4446
+ "border": 0,
4447
+ "clip": "rect(0,0,0,0)",
4448
+ "height": 1,
4449
+ "margin": -1,
4450
+ "overflow": "hidden",
4451
+ "padding": 0,
4452
+ "position": "absolute",
4453
+ "width": 1,
4454
+ }
4455
+ }
4456
+ />
3963
4457
  <button
3964
4458
  aria-expanded="false"
3965
4459
  aria-haspopup="listbox"
@@ -2,8 +2,8 @@
2
2
  import * as React from "react";
3
3
 
4
4
  import type {StoryComponentType} from "@storybook/react";
5
- import ActionMenu from "./action-menu.js";
6
- import ActionItem from "./action-item.js";
5
+ import ActionMenu from "../action-menu.js";
6
+ import ActionItem from "../action-item.js";
7
7
 
8
8
  export default {
9
9
  title: "Dropdown / ActionMenu",
@@ -7,7 +7,7 @@ import {View} from "@khanacademy/wonder-blocks-core";
7
7
  import type {Labels} from "@khanacademy/wonder-blocks-dropdown";
8
8
  import type {StoryComponentType} from "@storybook/react";
9
9
 
10
- import {MultiSelect, OptionItem} from "../index.js";
10
+ import {MultiSelect, OptionItem} from "../../index.js";
11
11
 
12
12
  export default {
13
13
  title: "Dropdown / MultiSelect",
@@ -11,7 +11,7 @@ import {Body} from "@khanacademy/wonder-blocks-typography";
11
11
 
12
12
  import type {StoryComponentType} from "@storybook/react";
13
13
 
14
- import {SingleSelect, OptionItem} from "../index.js";
14
+ import {SingleSelect, OptionItem} from "../../index.js";
15
15
 
16
16
  export default {
17
17
  title: "Dropdown / SingleSelect",
@@ -31,6 +31,19 @@ const items = [
31
31
  },
32
32
  ];
33
33
 
34
+ const searchFieldItem = {
35
+ component: (
36
+ <SearchTextInput
37
+ testId="search-text-input"
38
+ key="search-text-input"
39
+ onChange={jest.fn()}
40
+ searchText={""}
41
+ />
42
+ ),
43
+ focusable: true,
44
+ populatedProps: {},
45
+ };
46
+
34
47
  describe("DropdownCore", () => {
35
48
  it("should throw for invalid role", () => {
36
49
  // Arrange
@@ -342,18 +355,7 @@ describe("DropdownCore", () => {
342
355
  searchText=""
343
356
  // mock the items
344
357
  items={[
345
- {
346
- component: (
347
- <SearchTextInput
348
- testId="search"
349
- key="search-text-input"
350
- onChange={jest.fn()}
351
- searchText={""}
352
- />
353
- ),
354
- focusable: true,
355
- populatedProps: {},
356
- },
358
+ searchFieldItem,
357
359
  {
358
360
  component: (
359
361
  <OptionItem
@@ -754,21 +756,7 @@ describe("DropdownCore", () => {
754
756
  onSearchTextChanged={jest.fn()}
755
757
  searchText=""
756
758
  // mock the items
757
- items={[
758
- {
759
- component: (
760
- <SearchTextInput
761
- testId="search-text-input"
762
- key="search-text-input"
763
- onChange={jest.fn()}
764
- searchText={""}
765
- />
766
- ),
767
- focusable: true,
768
- populatedProps: {},
769
- },
770
- ...optionItems,
771
- ]}
759
+ items={[searchFieldItem, ...optionItems]}
772
760
  role="listbox"
773
761
  open={true}
774
762
  // mock the opener elements
@@ -797,21 +785,7 @@ describe("DropdownCore", () => {
797
785
  onSearchTextChanged={jest.fn()}
798
786
  searchText=""
799
787
  // mock the items
800
- items={[
801
- {
802
- component: (
803
- <SearchTextInput
804
- testId="search-text-input"
805
- key="search-text-input"
806
- onChange={jest.fn()}
807
- searchText={""}
808
- />
809
- ),
810
- focusable: true,
811
- populatedProps: {},
812
- },
813
- ...optionItems,
814
- ]}
788
+ items={[searchFieldItem, ...optionItems]}
815
789
  role="listbox"
816
790
  open={true}
817
791
  // mock the opener elements
@@ -833,4 +807,53 @@ describe("DropdownCore", () => {
833
807
  });
834
808
  });
835
809
  });
810
+
811
+ describe("a11y > Live region", () => {
812
+ it("should render a live region announcing the number of options", async () => {
813
+ // Arrange
814
+
815
+ // Act
816
+ const {container} = render(
817
+ <DropdownCore
818
+ initialFocusedIndex={undefined}
819
+ onSearchTextChanged={jest.fn()}
820
+ // mock the items (3 options)
821
+ items={items}
822
+ role="listbox"
823
+ open={true}
824
+ // mock the opener elements
825
+ opener={<button />}
826
+ openerElement={null}
827
+ onOpenChanged={jest.fn()}
828
+ />,
829
+ );
830
+
831
+ // Assert
832
+ expect(container).toHaveTextContent("3 items");
833
+ });
834
+
835
+ it("shouldn't include the search field as part of the options", async () => {
836
+ // Arrange
837
+
838
+ // Act
839
+ const {container} = render(
840
+ <DropdownCore
841
+ initialFocusedIndex={undefined}
842
+ onSearchTextChanged={jest.fn()}
843
+ searchText=""
844
+ // mock the items (3 options + search field)
845
+ items={[searchFieldItem, ...items]}
846
+ role="listbox"
847
+ open={true}
848
+ // mock the opener elements
849
+ opener={<button />}
850
+ openerElement={null}
851
+ onOpenChanged={jest.fn()}
852
+ />,
853
+ );
854
+
855
+ // Assert
856
+ expect(container).toHaveTextContent("3 items");
857
+ });
858
+ });
836
859
  });
@@ -4,6 +4,8 @@ import * as React from "react";
4
4
  import {render, screen} from "@testing-library/react";
5
5
  import userEvent from "@testing-library/user-event";
6
6
 
7
+ import {ngettext} from "@khanacademy/wonder-blocks-i18n";
8
+
7
9
  import OptionItem from "../option-item.js";
8
10
  import MultiSelect from "../multi-select.js";
9
11
 
@@ -1187,4 +1189,59 @@ describe("MultiSelect", () => {
1187
1189
  ).toBeInTheDocument();
1188
1190
  });
1189
1191
  });
1192
+
1193
+ describe("a11y > Live region", () => {
1194
+ it("should announce the number of options when the listbox is open", async () => {
1195
+ // Arrange
1196
+ const labels: $Shape<Labels> = {
1197
+ someSelected: (numOptions: number): string =>
1198
+ ngettext("%(num)s school", "%(num)s schools", numOptions),
1199
+ };
1200
+
1201
+ // Act
1202
+ const {container} = render(
1203
+ <MultiSelect
1204
+ onChange={jest.fn()}
1205
+ isFilterable={true}
1206
+ labels={labels}
1207
+ opened={true}
1208
+ >
1209
+ <OptionItem label="school 1" value="1" />
1210
+ <OptionItem label="school 2" value="2" />
1211
+ <OptionItem label="school 3" value="3" />
1212
+ </MultiSelect>,
1213
+ );
1214
+
1215
+ // Assert
1216
+ expect(container).toHaveTextContent("3 schools");
1217
+ });
1218
+
1219
+ it("should change the number of options after using the search filter", async () => {
1220
+ // Arrange
1221
+ const labels: $Shape<Labels> = {
1222
+ someSelected: (numOptions: number): string =>
1223
+ ngettext("%(num)s planet", "%(num)s planets", numOptions),
1224
+ };
1225
+
1226
+ const {container} = render(
1227
+ <MultiSelect
1228
+ onChange={jest.fn()}
1229
+ isFilterable={true}
1230
+ shortcuts={true}
1231
+ labels={labels}
1232
+ opened={true}
1233
+ >
1234
+ <OptionItem label="Earth" value="earth" />
1235
+ <OptionItem label="Venus" value="venus" />
1236
+ <OptionItem label="Mars" value="mars" />
1237
+ </MultiSelect>,
1238
+ );
1239
+
1240
+ // Act
1241
+ userEvent.paste(screen.getByRole("textbox"), "Ear");
1242
+
1243
+ // Assert
1244
+ expect(container).toHaveTextContent("1 planet");
1245
+ });
1246
+ });
1190
1247
  });
@@ -692,4 +692,34 @@ describe("SingleSelect", () => {
692
692
  expect(dropdownMenu).toHaveStyle("max-height: 200px");
693
693
  });
694
694
  });
695
+
696
+ describe("a11y > Live region", () => {
697
+ it("should change the number of options after using the search filter", async () => {
698
+ // Arrange
699
+ render(
700
+ <SingleSelect
701
+ onChange={onChange}
702
+ placeholder="Choose"
703
+ isFilterable={true}
704
+ opened={true}
705
+ >
706
+ <OptionItem label="item 0" value="0" />
707
+ <OptionItem label="item 1" value="1" />
708
+ <OptionItem label="item 2" value="2" />
709
+ </SingleSelect>,
710
+ );
711
+
712
+ // Act
713
+ userEvent.paste(screen.getByRole("textbox"), "item 0");
714
+
715
+ // Assert
716
+ const liveRegionText = screen.getByTestId(
717
+ "dropdown-live-region",
718
+ ).textContent;
719
+
720
+ // TODO(WB-1318): Change this assertion to `1 item` after adding the
721
+ // `labels` prop to the component.
722
+ expect(liveRegionText).toEqual("1 items");
723
+ });
724
+ });
695
725
  });
@@ -10,7 +10,7 @@ import {VariableSizeList as List} from "react-window";
10
10
  import Color, {fade} from "@khanacademy/wonder-blocks-color";
11
11
 
12
12
  import Spacing from "@khanacademy/wonder-blocks-spacing";
13
- import {View} from "@khanacademy/wonder-blocks-core";
13
+ import {addStyle, View} from "@khanacademy/wonder-blocks-core";
14
14
  import {LabelMedium} from "@khanacademy/wonder-blocks-typography";
15
15
  import {withActionScheduler} from "@khanacademy/wonder-blocks-timing";
16
16
 
@@ -42,8 +42,17 @@ import {
42
42
  */
43
43
  const VIRTUALIZE_THRESHOLD = 125;
44
44
 
45
+ const StyledSpan = addStyle("span");
46
+
45
47
  type Labels = {|
46
48
  noResults: string,
49
+ /**
50
+ * The number total of items available.
51
+ *
52
+ * NOTE: We are reusing the same label for both the total number of items
53
+ * and the number of selected items.
54
+ */
55
+ someSelected: (numOptions: number) => string,
47
56
  |};
48
57
 
49
58
  // we need to define a DefaultProps type to allow the HOC expose the default
@@ -212,6 +221,7 @@ class DropdownCore extends React.Component<Props, State> {
212
221
  alignment: "left",
213
222
  labels: {
214
223
  noResults: defaultLabels.noResults,
224
+ someSelected: defaultLabels.someSelected,
215
225
  },
216
226
  light: false,
217
227
  };
@@ -837,6 +847,26 @@ class DropdownCore extends React.Component<Props, State> {
837
847
  );
838
848
  }
839
849
 
850
+ renderLiveRegion(): React.Node {
851
+ const {items, open} = this.props;
852
+ const {labels} = this.state;
853
+ const totalItems = this.hasSearchBox()
854
+ ? items.length - 1
855
+ : items.length;
856
+
857
+ return (
858
+ <StyledSpan
859
+ aria-live="polite"
860
+ aria-atomic="true"
861
+ aria-relevant="additions text"
862
+ style={styles.srOnly}
863
+ data-test-id="dropdown-live-region"
864
+ >
865
+ {open && labels.someSelected(totalItems)}
866
+ </StyledSpan>
867
+ );
868
+ }
869
+
840
870
  render(): React.Node {
841
871
  const {open, opener, style, className} = this.props;
842
872
 
@@ -847,6 +877,7 @@ class DropdownCore extends React.Component<Props, State> {
847
877
  style={[styles.menuWrapper, style]}
848
878
  className={className}
849
879
  >
880
+ {this.renderLiveRegion()}
850
881
  {opener}
851
882
  {open && this.renderDropdown()}
852
883
  </View>
@@ -884,6 +915,17 @@ const styles = StyleSheet.create({
884
915
  alignSelf: "center",
885
916
  marginTop: Spacing.xxSmall_6,
886
917
  },
918
+
919
+ srOnly: {
920
+ border: 0,
921
+ clip: "rect(0,0,0,0)",
922
+ height: 1,
923
+ margin: -1,
924
+ overflow: "hidden",
925
+ padding: 0,
926
+ position: "absolute",
927
+ width: 1,
928
+ },
887
929
  });
888
930
 
889
931
  type ExportProps = WithoutActionScheduler<
@@ -556,7 +556,7 @@ export default class MultiSelect extends React.Component<Props, State> {
556
556
  isFilterable,
557
557
  } = this.props;
558
558
  const {open, searchText} = this.state;
559
- const {noResults} = this.state.labels;
559
+ const {noResults, someSelected} = this.state.labels;
560
560
 
561
561
  const allChildren = React.Children.toArray(children).filter(Boolean);
562
562
  const numOptions = allChildren.length;
@@ -590,6 +590,7 @@ export default class MultiSelect extends React.Component<Props, State> {
590
590
  searchText={isFilterable ? searchText : ""}
591
591
  labels={{
592
592
  noResults,
593
+ someSelected,
593
594
  }}
594
595
  />
595
596
  );