@khanacademy/wonder-blocks-dropdown 5.2.1 → 5.3.1

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/dist/es/index.js CHANGED
@@ -4,7 +4,7 @@ import { CompactCell, DetailCell } from '@khanacademy/wonder-blocks-cell';
4
4
  import * as tokens from '@khanacademy/wonder-blocks-tokens';
5
5
  import { spacing, color, mix, fade } from '@khanacademy/wonder-blocks-tokens';
6
6
  import { LabelMedium, LabelSmall, LabelLarge } from '@khanacademy/wonder-blocks-typography';
7
- import { View, addStyle } from '@khanacademy/wonder-blocks-core';
7
+ import { View, addStyle, useUniqueIdWithMock } from '@khanacademy/wonder-blocks-core';
8
8
  import { Strut } from '@khanacademy/wonder-blocks-layout';
9
9
  import { PhosphorIcon } from '@khanacademy/wonder-blocks-icon';
10
10
  import checkIcon from '@phosphor-icons/core/bold/check-bold.svg';
@@ -88,13 +88,13 @@ class ActionItem extends React.Component {
88
88
  style,
89
89
  testId
90
90
  } = this.props;
91
- const defaultStyle = [styles$8.wrapper, style];
91
+ const defaultStyle = [styles$9.wrapper, style];
92
92
  const labelComponent = typeof label === "string" ? React.createElement(LabelMedium, {
93
93
  lang: lang,
94
- style: styles$8.label
94
+ style: styles$9.label
95
95
  }, label) : React.cloneElement(label, _extends({
96
96
  lang,
97
- style: styles$8.label
97
+ style: styles$9.label
98
98
  }, label.props));
99
99
  return React.createElement(CompactCell, {
100
100
  disabled: disabled,
@@ -102,7 +102,7 @@ class ActionItem extends React.Component {
102
102
  rootStyle: defaultStyle,
103
103
  leftAccessory: leftAccessory,
104
104
  rightAccessory: rightAccessory,
105
- style: [styles$8.shared, indent && styles$8.indent],
105
+ style: [styles$9.shared, indent && styles$9.indent],
106
106
  role: role,
107
107
  testId: testId,
108
108
  title: labelComponent,
@@ -119,7 +119,7 @@ ActionItem.defaultProps = {
119
119
  role: "menuitem"
120
120
  };
121
121
  ActionItem.__IS_ACTION_ITEM__ = true;
122
- const styles$8 = StyleSheet.create({
122
+ const styles$9 = StyleSheet.create({
123
123
  wrapper: {
124
124
  minHeight: DROPDOWN_ITEM_HEIGHT,
125
125
  touchAction: "manipulation",
@@ -176,10 +176,10 @@ const Check = function Check(props) {
176
176
  return React.createElement(PhosphorIcon, {
177
177
  icon: checkIcon,
178
178
  size: "small",
179
- style: [styles$7.bounds, !selected && styles$7.hide]
179
+ style: [styles$8.bounds, !selected && styles$8.hide]
180
180
  });
181
181
  };
182
- const styles$7 = StyleSheet.create({
182
+ const styles$8 = StyleSheet.create({
183
183
  bounds: {
184
184
  alignSelf: "center",
185
185
  height: spacing.medium_16,
@@ -203,7 +203,7 @@ const Checkbox = function Checkbox(props) {
203
203
  } = props;
204
204
  return React.createElement(View, {
205
205
  className: "checkbox",
206
- style: [styles$6.checkbox, selected && !disabled && styles$6.noBorder, disabled && styles$6.disabledCheckbox]
206
+ style: [styles$7.checkbox, selected && !disabled && styles$7.noBorder, disabled && styles$7.disabledCheckbox]
207
207
  }, selected && React.createElement(PhosphorIcon, {
208
208
  icon: checkIcon,
209
209
  size: "small",
@@ -212,10 +212,10 @@ const Checkbox = function Checkbox(props) {
212
212
  width: spacing.small_12,
213
213
  height: spacing.small_12,
214
214
  margin: spacing.xxxxSmall_2
215
- }, disabled && selected && styles$6.disabledCheckFormatting]
215
+ }, disabled && selected && styles$7.disabledCheckFormatting]
216
216
  }));
217
217
  };
218
- const styles$6 = StyleSheet.create({
218
+ const styles$7 = StyleSheet.create({
219
219
  checkbox: {
220
220
  alignSelf: "center",
221
221
  minHeight: spacing.medium_16,
@@ -240,7 +240,8 @@ const styles$6 = StyleSheet.create({
240
240
  }
241
241
  });
242
242
 
243
- const _excluded$5 = ["disabled", "label", "role", "selected", "testId", "style", "leftAccessory", "horizontalRule", "rightAccessory", "subtitle1", "subtitle2", "value", "onClick", "onToggle", "variant"];
243
+ const _excluded$5 = ["disabled", "label", "selected", "testId", "leftAccessory", "horizontalRule", "parentComponent", "rightAccessory", "style", "subtitle1", "subtitle2", "value", "onClick", "onToggle", "variant", "role"];
244
+ const StyledListItem = addStyle("li");
244
245
  class OptionItem extends React.Component {
245
246
  constructor(...args) {
246
247
  super(...args);
@@ -266,31 +267,32 @@ class OptionItem extends React.Component {
266
267
  return Checkbox;
267
268
  }
268
269
  }
269
- render() {
270
+ renderCell() {
270
271
  const _this$props = this.props,
271
272
  {
272
273
  disabled,
273
274
  label,
274
- role,
275
275
  selected,
276
276
  testId,
277
- style,
278
277
  leftAccessory,
279
278
  horizontalRule,
279
+ parentComponent,
280
280
  rightAccessory,
281
+ style,
281
282
  subtitle1,
282
- subtitle2
283
+ subtitle2,
284
+ role
283
285
  } = _this$props,
284
286
  sharedProps = _objectWithoutPropertiesLoose(_this$props, _excluded$5);
285
287
  const CheckComponent = this.getCheckComponent();
286
- const defaultStyle = [styles$5.item, style];
288
+ const defaultStyle = [styles$6.item, style];
287
289
  return React.createElement(DetailCell, _extends({
288
290
  disabled: disabled,
289
291
  horizontalRule: horizontalRule,
290
- rootStyle: defaultStyle,
291
- style: styles$5.itemContainer,
292
- "aria-selected": selected ? "true" : "false",
293
- role: role,
292
+ rootStyle: parentComponent === "listbox" ? styles$6.listboxItem : defaultStyle,
293
+ style: styles$6.itemContainer,
294
+ "aria-selected": parentComponent !== "listbox" && selected ? "true" : "false",
295
+ role: parentComponent !== "listbox" ? role : undefined,
294
296
  testId: testId,
295
297
  leftAccessory: React.createElement(React.Fragment, null, leftAccessory ? React.createElement(View, {
296
298
  style: {
@@ -310,17 +312,42 @@ class OptionItem extends React.Component {
310
312
  className: "subtitle"
311
313
  }, subtitle1) : undefined,
312
314
  title: React.createElement(LabelMedium, {
313
- style: styles$5.label
315
+ style: styles$6.label
314
316
  }, label),
315
317
  subtitle2: subtitle2 ? React.createElement(LabelSmall, {
316
318
  className: "subtitle"
317
319
  }, subtitle2) : undefined,
318
- onClick: this.handleClick
320
+ onClick: parentComponent !== "listbox" ? this.handleClick : undefined
319
321
  }, sharedProps));
320
322
  }
323
+ render() {
324
+ const {
325
+ disabled,
326
+ focused,
327
+ parentComponent,
328
+ role,
329
+ selected
330
+ } = this.props;
331
+ if (parentComponent === "listbox") {
332
+ return React.createElement(StyledListItem, {
333
+ onMouseDown: e => {
334
+ e.preventDefault();
335
+ },
336
+ onClick: this.handleClick,
337
+ style: [styles$6.reset, styles$6.item, focused && styles$6.itemFocused, disabled && styles$6.itemDisabled],
338
+ role: role,
339
+ "aria-selected": selected ? "true" : "false",
340
+ "aria-disabled": disabled ? "true" : "false",
341
+ id: this.props.id,
342
+ tabIndex: -1
343
+ }, this.renderCell());
344
+ }
345
+ return this.renderCell();
346
+ }
321
347
  }
322
348
  OptionItem.defaultProps = {
323
349
  disabled: false,
350
+ focused: false,
324
351
  horizontalRule: "none",
325
352
  onToggle: () => void 0,
326
353
  role: "option",
@@ -332,14 +359,33 @@ const {
332
359
  white,
333
360
  offBlack
334
361
  } = color;
335
- const styles$5 = StyleSheet.create({
362
+ const focusedStyle = {
363
+ borderRadius: spacing.xxxSmall_4,
364
+ outline: `${spacing.xxxxSmall_2}px solid ${color.blue}`,
365
+ outlineOffset: -spacing.xxxxSmall_2
366
+ };
367
+ const styles$6 = StyleSheet.create({
368
+ reset: {
369
+ margin: 0,
370
+ padding: 0,
371
+ border: 0,
372
+ background: "none",
373
+ outline: "none",
374
+ fontSize: "100%",
375
+ verticalAlign: "baseline",
376
+ textAlign: "left",
377
+ textDecoration: "none",
378
+ listStyle: "none",
379
+ cursor: "pointer"
380
+ },
381
+ listboxItem: {
382
+ backgroundColor: "transparent",
383
+ color: "inherit"
384
+ },
336
385
  item: {
386
+ backgroundColor: color.white,
337
387
  minHeight: "unset",
338
- ":focus": {
339
- borderRadius: spacing.xxxSmall_4,
340
- outline: `${spacing.xxxxSmall_2}px solid ${color.blue}`,
341
- outlineOffset: -spacing.xxxxSmall_2
342
- },
388
+ ":focus": focusedStyle,
343
389
  ":focus-visible": {
344
390
  overflow: "visible"
345
391
  },
@@ -347,6 +393,16 @@ const styles$5 = StyleSheet.create({
347
393
  color: white,
348
394
  background: blue
349
395
  },
396
+ [":active[aria-selected=false]"]: {},
397
+ [":hover[aria-disabled=true]"]: {
398
+ cursor: "not-allowed"
399
+ },
400
+ [":is([aria-disabled=true])"]: {
401
+ color: color.offBlack32,
402
+ ":focus-visible": {
403
+ outline: "none"
404
+ }
405
+ },
350
406
  ["@media not (hover: hover)"]: {
351
407
  [":hover[aria-disabled=false]"]: {
352
408
  color: white,
@@ -382,6 +438,10 @@ const styles$5 = StyleSheet.create({
382
438
  color: mix(color.fadedBlue16, white)
383
439
  }
384
440
  },
441
+ itemFocused: focusedStyle,
442
+ itemDisabled: {
443
+ outlineColor: color.offBlack32
444
+ },
385
445
  itemContainer: {
386
446
  minHeight: "unset",
387
447
  padding: `${spacing.xSmall_8 + spacing.xxxxSmall_2}px ${spacing.xSmall_8}px`,
@@ -405,13 +465,13 @@ class SeparatorItem extends React.Component {
405
465
  }
406
466
  render() {
407
467
  return React.createElement(View, {
408
- style: [styles$4.separator, this.props.style],
468
+ style: [styles$5.separator, this.props.style],
409
469
  "aria-hidden": "true"
410
470
  });
411
471
  }
412
472
  }
413
473
  SeparatorItem.__IS_SEPARATOR_ITEM__ = true;
414
- const styles$4 = StyleSheet.create({
474
+ const styles$5 = StyleSheet.create({
415
475
  separator: {
416
476
  boxShadow: `0 -1px ${color.offBlack16}`,
417
477
  height: 1,
@@ -1077,7 +1137,7 @@ class DropdownCore extends React.Component {
1077
1137
  const numResults = items.length;
1078
1138
  if (numResults === 0) {
1079
1139
  return React.createElement(LabelMedium, {
1080
- style: styles$3.noResult,
1140
+ style: styles$4.noResult,
1081
1141
  testId: "dropdown-core-no-results"
1082
1142
  }, noResults);
1083
1143
  }
@@ -1149,7 +1209,7 @@ class DropdownCore extends React.Component {
1149
1209
  onChange: this.handleSearchTextChanged,
1150
1210
  placeholder: labels.filter,
1151
1211
  ref: this.searchFieldRef,
1152
- style: styles$3.searchInputStyle,
1212
+ style: styles$4.searchInputStyle,
1153
1213
  value: searchText || ""
1154
1214
  });
1155
1215
  }
@@ -1167,11 +1227,11 @@ class DropdownCore extends React.Component {
1167
1227
  const minDropdownWidth = openerStyle ? openerStyle.getPropertyValue("width") : 0;
1168
1228
  return React.createElement(View, {
1169
1229
  onMouseUp: this.handleDropdownMouseUp,
1170
- style: [styles$3.dropdown, light && styles$3.light, isReferenceHidden && styles$3.hidden, dropdownStyle],
1230
+ style: [styles$4.dropdown, light && styles$4.light, isReferenceHidden && styles$4.hidden, dropdownStyle],
1171
1231
  testId: "dropdown-core-container"
1172
1232
  }, isFilterable && this.renderSearchField(), React.createElement(View, {
1173
1233
  role: role,
1174
- style: [styles$3.listboxOrMenu, {
1234
+ style: [styles$4.listboxOrMenu, {
1175
1235
  minWidth: minDropdownWidth
1176
1236
  }],
1177
1237
  "aria-invalid": role === "listbox" ? ariaInvalid : undefined,
@@ -1205,7 +1265,7 @@ class DropdownCore extends React.Component {
1205
1265
  "aria-live": "polite",
1206
1266
  "aria-atomic": "true",
1207
1267
  "aria-relevant": "additions text",
1208
- style: styles$3.srOnly,
1268
+ style: styles$4.srOnly,
1209
1269
  "data-testid": "dropdown-live-region"
1210
1270
  }, open && labels.someResults(totalItems));
1211
1271
  }
@@ -1219,7 +1279,7 @@ class DropdownCore extends React.Component {
1219
1279
  return React.createElement(View, {
1220
1280
  onKeyDown: this.handleKeyDown,
1221
1281
  onKeyUp: this.handleKeyUp,
1222
- style: [styles$3.menuWrapper, style],
1282
+ style: [styles$4.menuWrapper, style],
1223
1283
  className: className
1224
1284
  }, this.renderLiveRegion(), opener, open && this.renderDropdown());
1225
1285
  }
@@ -1237,7 +1297,7 @@ DropdownCore.defaultProps = {
1237
1297
  light: false,
1238
1298
  selectionType: "single"
1239
1299
  };
1240
- const styles$3 = StyleSheet.create({
1300
+ const styles$4 = StyleSheet.create({
1241
1301
  menuWrapper: {
1242
1302
  width: "fit-content"
1243
1303
  },
@@ -1370,11 +1430,11 @@ const sharedStyles = StyleSheet.create({
1370
1430
  position: "absolute"
1371
1431
  }
1372
1432
  });
1373
- const styles$2 = {};
1433
+ const styles$3 = {};
1374
1434
  const _generateStyles$1 = localColor => {
1375
1435
  const buttonType = localColor;
1376
- if (styles$2[buttonType]) {
1377
- return styles$2[buttonType];
1436
+ if (styles$3[buttonType]) {
1437
+ return styles$3[buttonType];
1378
1438
  }
1379
1439
  const {
1380
1440
  offBlack32
@@ -1406,8 +1466,8 @@ const _generateStyles$1 = localColor => {
1406
1466
  cursor: "default"
1407
1467
  }
1408
1468
  };
1409
- styles$2[buttonType] = StyleSheet.create(newStyles);
1410
- return styles$2[buttonType];
1469
+ styles$3[buttonType] = StyleSheet.create(newStyles);
1470
+ return styles$3[buttonType];
1411
1471
  };
1412
1472
 
1413
1473
  const _excluded$3 = ["text", "opened"];
@@ -1543,7 +1603,7 @@ class ActionMenu extends React.Component {
1543
1603
  items: items,
1544
1604
  openerElement: this.openerElement,
1545
1605
  onOpenChanged: this.handleOpenChanged,
1546
- dropdownStyle: [styles$1.menuTopSpace, dropdownStyle]
1606
+ dropdownStyle: [styles$2.menuTopSpace, dropdownStyle]
1547
1607
  });
1548
1608
  }
1549
1609
  }
@@ -1551,7 +1611,7 @@ ActionMenu.defaultProps = {
1551
1611
  alignment: "left",
1552
1612
  disabled: false
1553
1613
  };
1554
- const styles$1 = StyleSheet.create({
1614
+ const styles$2 = StyleSheet.create({
1555
1615
  caret: {
1556
1616
  marginLeft: 4
1557
1617
  },
@@ -1603,7 +1663,7 @@ class SelectOpener extends React.Component {
1603
1663
  pressed
1604
1664
  } = state;
1605
1665
  const iconColor = light ? disabled || pressed ? "currentColor" : tokens.color.white : disabled ? tokens.color.offBlack32 : tokens.color.offBlack64;
1606
- const style = [styles.shared, stateStyles.default, disabled && stateStyles.disabled, !disabled && (pressed ? stateStyles.active : (hovered || focused) && stateStyles.focus)];
1666
+ const style = [styles$1.shared, stateStyles.default, disabled && stateStyles.disabled, !disabled && (pressed ? stateStyles.active : (hovered || focused) && stateStyles.focus)];
1607
1667
  return React.createElement(StyledButton, _extends({}, sharedProps, {
1608
1668
  "aria-expanded": open ? "true" : "false",
1609
1669
  "aria-haspopup": "listbox",
@@ -1613,12 +1673,12 @@ class SelectOpener extends React.Component {
1613
1673
  style: style,
1614
1674
  type: "button"
1615
1675
  }, childrenProps), React.createElement(LabelMedium, {
1616
- style: styles.text
1676
+ style: styles$1.text
1617
1677
  }, children || "\u00A0"), React.createElement(PhosphorIcon, {
1618
1678
  icon: caretDownIcon,
1619
1679
  color: iconColor,
1620
1680
  size: "small",
1621
- style: styles.caret,
1681
+ style: styles$1.caret,
1622
1682
  "aria-hidden": "true"
1623
1683
  }));
1624
1684
  });
@@ -1633,7 +1693,7 @@ SelectOpener.defaultProps = {
1633
1693
  light: false,
1634
1694
  isPlaceholder: false
1635
1695
  };
1636
- const styles = StyleSheet.create({
1696
+ const styles$1 = StyleSheet.create({
1637
1697
  shared: {
1638
1698
  position: "relative",
1639
1699
  display: "inline-flex",
@@ -2230,4 +2290,207 @@ MultiSelect.defaultProps = {
2230
2290
  selectedValues: []
2231
2291
  };
2232
2292
 
2233
- export { ActionItem, ActionMenu, MultiSelect, OptionItem, SeparatorItem, SingleSelect };
2293
+ function updateMultipleSelection(previousSelection, value = "") {
2294
+ if (!previousSelection) {
2295
+ return [value];
2296
+ }
2297
+ return previousSelection.includes(value) ? previousSelection.filter(item => item !== value) : [...previousSelection, value];
2298
+ }
2299
+
2300
+ function useListbox({
2301
+ children: options,
2302
+ disabled,
2303
+ id,
2304
+ selectionType = "single",
2305
+ value
2306
+ }) {
2307
+ const selectedValueIndex = React.useMemo(() => {
2308
+ const firstValue = Array.isArray(value) ? value[0] : value;
2309
+ if (!firstValue || firstValue === "") {
2310
+ return 0;
2311
+ }
2312
+ return options.findIndex(item => item.props.value === firstValue);
2313
+ }, [options, value]);
2314
+ const [focusedIndex, setFocusedIndex] = React.useState(selectedValueIndex);
2315
+ const [isListboxFocused, setIsListboxFocused] = React.useState(false);
2316
+ const [selected, setSelected] = React.useState(value);
2317
+ const focusItem = index => {
2318
+ setFocusedIndex(index);
2319
+ };
2320
+ const focusPreviousItem = React.useCallback(() => {
2321
+ if (focusedIndex === 0) {
2322
+ focusItem(options.length - 1);
2323
+ } else {
2324
+ focusItem(focusedIndex - 1);
2325
+ }
2326
+ }, [options, focusedIndex]);
2327
+ const focusNextItem = React.useCallback(() => {
2328
+ if (focusedIndex === options.length - 1) {
2329
+ focusItem(0);
2330
+ } else {
2331
+ focusItem(focusedIndex + 1);
2332
+ }
2333
+ }, [options, focusedIndex]);
2334
+ const selectOption = React.useCallback(index => {
2335
+ const optionItem = options[index];
2336
+ if (optionItem.props.disabled) {
2337
+ return;
2338
+ }
2339
+ if (selectionType === "single") {
2340
+ setSelected(optionItem.props.value);
2341
+ } else {
2342
+ setSelected(prevSelected => {
2343
+ const newSelectedValue = updateMultipleSelection(prevSelected, optionItem.props.value);
2344
+ return newSelectedValue;
2345
+ });
2346
+ }
2347
+ }, [options, selectionType]);
2348
+ const handleKeyDown = React.useCallback(event => {
2349
+ const {
2350
+ key
2351
+ } = event;
2352
+ switch (key) {
2353
+ case "ArrowUp":
2354
+ event.preventDefault();
2355
+ focusPreviousItem();
2356
+ return;
2357
+ case "ArrowDown":
2358
+ event.preventDefault();
2359
+ focusNextItem();
2360
+ return;
2361
+ case "Home":
2362
+ event.preventDefault();
2363
+ focusItem(0);
2364
+ return;
2365
+ case "End":
2366
+ event.preventDefault();
2367
+ focusItem(options.length - 1);
2368
+ return;
2369
+ case "Enter":
2370
+ case " ":
2371
+ event.preventDefault();
2372
+ selectOption(focusedIndex);
2373
+ return;
2374
+ }
2375
+ }, [focusNextItem, focusPreviousItem, focusedIndex, options, selectOption]);
2376
+ const handleKeyUp = React.useCallback(event => {
2377
+ switch (event.key) {
2378
+ case " ":
2379
+ event.preventDefault();
2380
+ return;
2381
+ }
2382
+ }, []);
2383
+ const handleFocus = React.useCallback(() => {
2384
+ if (!disabled) {
2385
+ setIsListboxFocused(true);
2386
+ }
2387
+ }, [disabled]);
2388
+ const handleBlur = React.useCallback(() => {
2389
+ if (!disabled) {
2390
+ setIsListboxFocused(false);
2391
+ }
2392
+ }, [disabled]);
2393
+ const handleClick = React.useCallback(value => {
2394
+ const index = options.findIndex(item => item.props.value === value);
2395
+ const isOptionDisabled = options[index].props.disabled;
2396
+ if (disabled || isOptionDisabled) {
2397
+ return;
2398
+ }
2399
+ focusItem(index);
2400
+ selectOption(index);
2401
+ }, [disabled, options, selectOption]);
2402
+ const renderList = React.useMemo(() => {
2403
+ return options.map((component, index) => {
2404
+ const isSelected = (selected == null ? void 0 : selected.includes(component.props.value)) || false;
2405
+ const optionId = id ? `${id}-option-${index}` : `option-${index}`;
2406
+ return React.cloneElement(component, {
2407
+ key: index,
2408
+ focused: isListboxFocused && index === focusedIndex,
2409
+ disabled: component.props.disabled || disabled || false,
2410
+ selected: isSelected,
2411
+ variant: selectionType === "single" ? "check" : "checkbox",
2412
+ parentComponent: "listbox",
2413
+ id: optionId,
2414
+ onToggle: () => {
2415
+ handleClick(component.props.value);
2416
+ },
2417
+ role: "option"
2418
+ });
2419
+ });
2420
+ }, [options, selected, id, isListboxFocused, focusedIndex, disabled, selectionType, handleClick]);
2421
+ return {
2422
+ isListboxFocused,
2423
+ focusedIndex,
2424
+ renderList,
2425
+ selected,
2426
+ handleKeyDown,
2427
+ handleKeyUp,
2428
+ handleFocus,
2429
+ handleBlur
2430
+ };
2431
+ }
2432
+
2433
+ function Listbox(props) {
2434
+ const {
2435
+ children,
2436
+ disabled,
2437
+ id,
2438
+ onChange,
2439
+ selectionType = "single",
2440
+ style,
2441
+ tabIndex = 0,
2442
+ testId,
2443
+ value,
2444
+ "aria-label": ariaLabel,
2445
+ "aria-labelledby": ariaLabelledby
2446
+ } = props;
2447
+ const ids = useUniqueIdWithMock("listbox");
2448
+ const uniqueId = id != null ? id : ids.get("id");
2449
+ const {
2450
+ focusedIndex,
2451
+ isListboxFocused,
2452
+ renderList,
2453
+ selected,
2454
+ handleKeyDown,
2455
+ handleKeyUp,
2456
+ handleFocus,
2457
+ handleBlur
2458
+ } = useListbox({
2459
+ children,
2460
+ disabled,
2461
+ id: uniqueId,
2462
+ selectionType,
2463
+ value
2464
+ });
2465
+ React.useEffect(() => {
2466
+ if (selected && selected !== value) {
2467
+ onChange == null ? void 0 : onChange(selected);
2468
+ }
2469
+ }, [onChange, selected, value]);
2470
+ return React.createElement(View, {
2471
+ role: "listbox",
2472
+ "aria-disabled": disabled,
2473
+ id: uniqueId,
2474
+ style: [styles.listbox, style, disabled && styles.disabled],
2475
+ tabIndex: tabIndex,
2476
+ onKeyDown: handleKeyDown,
2477
+ onKeyUp: handleKeyUp,
2478
+ onFocus: handleFocus,
2479
+ onBlur: handleBlur,
2480
+ testId: testId,
2481
+ "aria-activedescendant": isListboxFocused ? renderList[focusedIndex].props.id : undefined,
2482
+ "aria-label": ariaLabel,
2483
+ "aria-labelledby": ariaLabelledby,
2484
+ "aria-multiselectable": selectionType === "multiple"
2485
+ }, renderList);
2486
+ }
2487
+ const styles = StyleSheet.create({
2488
+ listbox: {
2489
+ outline: "none"
2490
+ },
2491
+ disabled: {
2492
+ color: color.offBlack64
2493
+ }
2494
+ });
2495
+
2496
+ export { ActionItem, ActionMenu, Listbox, MultiSelect, OptionItem, SeparatorItem, SingleSelect };
@@ -0,0 +1,73 @@
1
+ import * as React from "react";
2
+ import { MaybeValueOrValues, OptionItemComponent } from "../util/types";
3
+ type Props = {
4
+ /**
5
+ * The list of items to display in the listbox.
6
+ */
7
+ children: Array<OptionItemComponent>;
8
+ /**
9
+ * Whether the listbox is disabled.
10
+ */
11
+ disabled: boolean | undefined;
12
+ /**
13
+ * The unique identifier of the listbox element.
14
+ */
15
+ id: string;
16
+ /**
17
+ * The value of the currently selected items.
18
+ */
19
+ value?: MaybeValueOrValues;
20
+ /**
21
+ * The type of selection that the listbox supports.
22
+ */
23
+ selectionType: "single" | "multiple";
24
+ };
25
+ /**
26
+ * Hook for managing the state of a listbox.
27
+ *
28
+ * It manages how the options are rendered and how the listbox behaves.
29
+ *
30
+ * This includes:
31
+ * - Keyboard navigation.
32
+ * - Selection management.
33
+ */
34
+ export declare function useListbox({ children: options, disabled, id, selectionType, value, }: Props): {
35
+ isListboxFocused: boolean;
36
+ focusedIndex: number;
37
+ renderList: React.ReactElement<Pick<Readonly<import("../../../wonder-blocks-core/src/util/aria-types").AriaAttributes> & Readonly<{
38
+ role?: import("../../../wonder-blocks-core/src/util/aria-types").AriaRole | undefined;
39
+ }> & {
40
+ label: import("../util/types").OptionLabel;
41
+ labelAsText?: string | undefined;
42
+ value: string;
43
+ disabled: boolean;
44
+ onClick?: (() => unknown) | undefined;
45
+ onToggle: (value: string) => unknown;
46
+ selected: boolean;
47
+ focused: boolean;
48
+ role: "menuitem" | "option";
49
+ testId?: string | undefined;
50
+ variant?: "checkbox" | "check" | undefined;
51
+ style?: import("@khanacademy/wonder-blocks-core").StyleType;
52
+ parentComponent?: "listbox" | "dropdown" | undefined;
53
+ id?: string | undefined;
54
+ horizontalRule: import("../../../wonder-blocks-cell/src/util/types").HorizontalRuleVariant | undefined;
55
+ leftAccessory?: React.ReactNode;
56
+ rightAccessory?: React.ReactNode;
57
+ subtitle1?: import("../../../wonder-blocks-cell/src/util/types").TypographyText | undefined;
58
+ subtitle2?: import("../../../wonder-blocks-cell/src/util/types").TypographyText | undefined;
59
+ }, "style" | "label" | "id" | "value" | "onClick" | keyof import("../../../wonder-blocks-core/src/util/aria-types").AriaAttributes | "testId" | "leftAccessory" | "rightAccessory" | "labelAsText" | "variant" | "parentComponent" | "subtitle1" | "subtitle2"> & {
60
+ disabled?: boolean | undefined;
61
+ role?: "menuitem" | "option" | undefined;
62
+ selected?: boolean | undefined;
63
+ onToggle?: ((value: string) => unknown) | undefined;
64
+ focused?: boolean | undefined;
65
+ horizontalRule?: import("../../../wonder-blocks-cell/src/util/types").HorizontalRuleVariant | undefined;
66
+ } & {}, string | React.JSXElementConstructor<any>>[];
67
+ selected: string | import("../util/types").MaybeString[] | null | undefined;
68
+ handleKeyDown: (event: React.KeyboardEvent) => void;
69
+ handleKeyUp: (event: React.KeyboardEvent) => void;
70
+ handleFocus: () => void;
71
+ handleBlur: () => void;
72
+ };
73
+ export {};
package/dist/index.d.ts CHANGED
@@ -4,7 +4,8 @@ import SeparatorItem from "./components/separator-item";
4
4
  import ActionMenu from "./components/action-menu";
5
5
  import SingleSelect from "./components/single-select";
6
6
  import MultiSelect from "./components/multi-select";
7
+ import Listbox from "./components/listbox";
7
8
  import type { Labels } from "./components/multi-select";
8
9
  import type { SingleSelectLabels } from "./components/single-select";
9
- export { ActionItem, OptionItem, SeparatorItem, ActionMenu, SingleSelect, MultiSelect, };
10
+ export { ActionItem, OptionItem, SeparatorItem, ActionMenu, SingleSelect, MultiSelect, Listbox, };
10
11
  export type { Labels, SingleSelectLabels };