@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/CHANGELOG.md +26 -0
- package/dist/components/listbox.d.ts +85 -0
- package/dist/components/multi-select.d.ts +2 -2
- package/dist/components/option-item.d.ts +22 -0
- package/dist/es/index.js +313 -50
- package/dist/hooks/use-listbox.d.ts +73 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +312 -48
- package/dist/util/selection.d.ts +2 -0
- package/dist/util/types.d.ts +7 -1
- package/package.json +11 -11
- package/src/components/__tests__/listbox.test.tsx +425 -0
- package/src/components/listbox.tsx +176 -0
- package/src/components/multi-select.tsx +16 -22
- package/src/components/option-item.tsx +127 -15
- package/src/hooks/use-listbox.tsx +224 -0
- package/src/index.ts +2 -0
- package/src/util/__tests__/selection.test.ts +50 -0
- package/src/util/selection.ts +16 -0
- package/src/util/types.ts +12 -3
- package/tsconfig-build.tsbuildinfo +1 -1
package/dist/index.js
CHANGED
|
@@ -118,13 +118,13 @@ class ActionItem extends React__namespace.Component {
|
|
|
118
118
|
style,
|
|
119
119
|
testId
|
|
120
120
|
} = this.props;
|
|
121
|
-
const defaultStyle = [styles$
|
|
121
|
+
const defaultStyle = [styles$9.wrapper, style];
|
|
122
122
|
const labelComponent = typeof label === "string" ? React__namespace.createElement(wonderBlocksTypography.LabelMedium, {
|
|
123
123
|
lang: lang,
|
|
124
|
-
style: styles$
|
|
124
|
+
style: styles$9.label
|
|
125
125
|
}, label) : React__namespace.cloneElement(label, _extends({
|
|
126
126
|
lang,
|
|
127
|
-
style: styles$
|
|
127
|
+
style: styles$9.label
|
|
128
128
|
}, label.props));
|
|
129
129
|
return React__namespace.createElement(wonderBlocksCell.CompactCell, {
|
|
130
130
|
disabled: disabled,
|
|
@@ -132,7 +132,7 @@ class ActionItem extends React__namespace.Component {
|
|
|
132
132
|
rootStyle: defaultStyle,
|
|
133
133
|
leftAccessory: leftAccessory,
|
|
134
134
|
rightAccessory: rightAccessory,
|
|
135
|
-
style: [styles$
|
|
135
|
+
style: [styles$9.shared, indent && styles$9.indent],
|
|
136
136
|
role: role,
|
|
137
137
|
testId: testId,
|
|
138
138
|
title: labelComponent,
|
|
@@ -149,7 +149,7 @@ ActionItem.defaultProps = {
|
|
|
149
149
|
role: "menuitem"
|
|
150
150
|
};
|
|
151
151
|
ActionItem.__IS_ACTION_ITEM__ = true;
|
|
152
|
-
const styles$
|
|
152
|
+
const styles$9 = aphrodite.StyleSheet.create({
|
|
153
153
|
wrapper: {
|
|
154
154
|
minHeight: DROPDOWN_ITEM_HEIGHT,
|
|
155
155
|
touchAction: "manipulation",
|
|
@@ -206,10 +206,10 @@ const Check = function Check(props) {
|
|
|
206
206
|
return React__namespace.createElement(wonderBlocksIcon.PhosphorIcon, {
|
|
207
207
|
icon: checkIcon__default["default"],
|
|
208
208
|
size: "small",
|
|
209
|
-
style: [styles$
|
|
209
|
+
style: [styles$8.bounds, !selected && styles$8.hide]
|
|
210
210
|
});
|
|
211
211
|
};
|
|
212
|
-
const styles$
|
|
212
|
+
const styles$8 = aphrodite.StyleSheet.create({
|
|
213
213
|
bounds: {
|
|
214
214
|
alignSelf: "center",
|
|
215
215
|
height: tokens.spacing.medium_16,
|
|
@@ -233,7 +233,7 @@ const Checkbox = function Checkbox(props) {
|
|
|
233
233
|
} = props;
|
|
234
234
|
return React__namespace.createElement(wonderBlocksCore.View, {
|
|
235
235
|
className: "checkbox",
|
|
236
|
-
style: [styles$
|
|
236
|
+
style: [styles$7.checkbox, selected && !disabled && styles$7.noBorder, disabled && styles$7.disabledCheckbox]
|
|
237
237
|
}, selected && React__namespace.createElement(wonderBlocksIcon.PhosphorIcon, {
|
|
238
238
|
icon: checkIcon__default["default"],
|
|
239
239
|
size: "small",
|
|
@@ -242,10 +242,10 @@ const Checkbox = function Checkbox(props) {
|
|
|
242
242
|
width: tokens.spacing.small_12,
|
|
243
243
|
height: tokens.spacing.small_12,
|
|
244
244
|
margin: tokens.spacing.xxxxSmall_2
|
|
245
|
-
}, disabled && selected && styles$
|
|
245
|
+
}, disabled && selected && styles$7.disabledCheckFormatting]
|
|
246
246
|
}));
|
|
247
247
|
};
|
|
248
|
-
const styles$
|
|
248
|
+
const styles$7 = aphrodite.StyleSheet.create({
|
|
249
249
|
checkbox: {
|
|
250
250
|
alignSelf: "center",
|
|
251
251
|
minHeight: tokens.spacing.medium_16,
|
|
@@ -270,7 +270,8 @@ const styles$6 = aphrodite.StyleSheet.create({
|
|
|
270
270
|
}
|
|
271
271
|
});
|
|
272
272
|
|
|
273
|
-
const _excluded$5 = ["disabled", "label", "
|
|
273
|
+
const _excluded$5 = ["disabled", "label", "selected", "testId", "leftAccessory", "horizontalRule", "parentComponent", "rightAccessory", "style", "subtitle1", "subtitle2", "value", "onClick", "onToggle", "variant", "role"];
|
|
274
|
+
const StyledListItem = wonderBlocksCore.addStyle("li");
|
|
274
275
|
class OptionItem extends React__namespace.Component {
|
|
275
276
|
constructor(...args) {
|
|
276
277
|
super(...args);
|
|
@@ -296,31 +297,32 @@ class OptionItem extends React__namespace.Component {
|
|
|
296
297
|
return Checkbox;
|
|
297
298
|
}
|
|
298
299
|
}
|
|
299
|
-
|
|
300
|
+
renderCell() {
|
|
300
301
|
const _this$props = this.props,
|
|
301
302
|
{
|
|
302
303
|
disabled,
|
|
303
304
|
label,
|
|
304
|
-
role,
|
|
305
305
|
selected,
|
|
306
306
|
testId,
|
|
307
|
-
style,
|
|
308
307
|
leftAccessory,
|
|
309
308
|
horizontalRule,
|
|
309
|
+
parentComponent,
|
|
310
310
|
rightAccessory,
|
|
311
|
+
style,
|
|
311
312
|
subtitle1,
|
|
312
|
-
subtitle2
|
|
313
|
+
subtitle2,
|
|
314
|
+
role
|
|
313
315
|
} = _this$props,
|
|
314
316
|
sharedProps = _objectWithoutPropertiesLoose(_this$props, _excluded$5);
|
|
315
317
|
const CheckComponent = this.getCheckComponent();
|
|
316
|
-
const defaultStyle = [styles$
|
|
318
|
+
const defaultStyle = [styles$6.item, style];
|
|
317
319
|
return React__namespace.createElement(wonderBlocksCell.DetailCell, _extends({
|
|
318
320
|
disabled: disabled,
|
|
319
321
|
horizontalRule: horizontalRule,
|
|
320
|
-
rootStyle: defaultStyle,
|
|
321
|
-
style: styles$
|
|
322
|
-
"aria-selected": selected ? "true" : "false",
|
|
323
|
-
role: role,
|
|
322
|
+
rootStyle: parentComponent === "listbox" ? styles$6.listboxItem : defaultStyle,
|
|
323
|
+
style: styles$6.itemContainer,
|
|
324
|
+
"aria-selected": parentComponent !== "listbox" && selected ? "true" : "false",
|
|
325
|
+
role: parentComponent !== "listbox" ? role : undefined,
|
|
324
326
|
testId: testId,
|
|
325
327
|
leftAccessory: React__namespace.createElement(React__namespace.Fragment, null, leftAccessory ? React__namespace.createElement(wonderBlocksCore.View, {
|
|
326
328
|
style: {
|
|
@@ -340,17 +342,42 @@ class OptionItem extends React__namespace.Component {
|
|
|
340
342
|
className: "subtitle"
|
|
341
343
|
}, subtitle1) : undefined,
|
|
342
344
|
title: React__namespace.createElement(wonderBlocksTypography.LabelMedium, {
|
|
343
|
-
style: styles$
|
|
345
|
+
style: styles$6.label
|
|
344
346
|
}, label),
|
|
345
347
|
subtitle2: subtitle2 ? React__namespace.createElement(wonderBlocksTypography.LabelSmall, {
|
|
346
348
|
className: "subtitle"
|
|
347
349
|
}, subtitle2) : undefined,
|
|
348
|
-
onClick: this.handleClick
|
|
350
|
+
onClick: parentComponent !== "listbox" ? this.handleClick : undefined
|
|
349
351
|
}, sharedProps));
|
|
350
352
|
}
|
|
353
|
+
render() {
|
|
354
|
+
const {
|
|
355
|
+
disabled,
|
|
356
|
+
focused,
|
|
357
|
+
parentComponent,
|
|
358
|
+
role,
|
|
359
|
+
selected
|
|
360
|
+
} = this.props;
|
|
361
|
+
if (parentComponent === "listbox") {
|
|
362
|
+
return React__namespace.createElement(StyledListItem, {
|
|
363
|
+
onMouseDown: e => {
|
|
364
|
+
e.preventDefault();
|
|
365
|
+
},
|
|
366
|
+
onClick: this.handleClick,
|
|
367
|
+
style: [styles$6.reset, styles$6.item, focused && styles$6.itemFocused, disabled && styles$6.itemDisabled],
|
|
368
|
+
role: role,
|
|
369
|
+
"aria-selected": selected ? "true" : "false",
|
|
370
|
+
"aria-disabled": disabled ? "true" : "false",
|
|
371
|
+
id: this.props.id,
|
|
372
|
+
tabIndex: -1
|
|
373
|
+
}, this.renderCell());
|
|
374
|
+
}
|
|
375
|
+
return this.renderCell();
|
|
376
|
+
}
|
|
351
377
|
}
|
|
352
378
|
OptionItem.defaultProps = {
|
|
353
379
|
disabled: false,
|
|
380
|
+
focused: false,
|
|
354
381
|
horizontalRule: "none",
|
|
355
382
|
onToggle: () => void 0,
|
|
356
383
|
role: "option",
|
|
@@ -362,14 +389,33 @@ const {
|
|
|
362
389
|
white,
|
|
363
390
|
offBlack
|
|
364
391
|
} = tokens.color;
|
|
365
|
-
const
|
|
392
|
+
const focusedStyle = {
|
|
393
|
+
borderRadius: tokens.spacing.xxxSmall_4,
|
|
394
|
+
outline: `${tokens.spacing.xxxxSmall_2}px solid ${tokens.color.blue}`,
|
|
395
|
+
outlineOffset: -tokens.spacing.xxxxSmall_2
|
|
396
|
+
};
|
|
397
|
+
const styles$6 = aphrodite.StyleSheet.create({
|
|
398
|
+
reset: {
|
|
399
|
+
margin: 0,
|
|
400
|
+
padding: 0,
|
|
401
|
+
border: 0,
|
|
402
|
+
background: "none",
|
|
403
|
+
outline: "none",
|
|
404
|
+
fontSize: "100%",
|
|
405
|
+
verticalAlign: "baseline",
|
|
406
|
+
textAlign: "left",
|
|
407
|
+
textDecoration: "none",
|
|
408
|
+
listStyle: "none",
|
|
409
|
+
cursor: "pointer"
|
|
410
|
+
},
|
|
411
|
+
listboxItem: {
|
|
412
|
+
backgroundColor: "transparent",
|
|
413
|
+
color: "inherit"
|
|
414
|
+
},
|
|
366
415
|
item: {
|
|
416
|
+
backgroundColor: tokens.color.white,
|
|
367
417
|
minHeight: "unset",
|
|
368
|
-
":focus":
|
|
369
|
-
borderRadius: tokens.spacing.xxxSmall_4,
|
|
370
|
-
outline: `${tokens.spacing.xxxxSmall_2}px solid ${tokens.color.blue}`,
|
|
371
|
-
outlineOffset: -tokens.spacing.xxxxSmall_2
|
|
372
|
-
},
|
|
418
|
+
":focus": focusedStyle,
|
|
373
419
|
":focus-visible": {
|
|
374
420
|
overflow: "visible"
|
|
375
421
|
},
|
|
@@ -377,6 +423,16 @@ const styles$5 = aphrodite.StyleSheet.create({
|
|
|
377
423
|
color: white,
|
|
378
424
|
background: blue
|
|
379
425
|
},
|
|
426
|
+
[":active[aria-selected=false]"]: {},
|
|
427
|
+
[":hover[aria-disabled=true]"]: {
|
|
428
|
+
cursor: "not-allowed"
|
|
429
|
+
},
|
|
430
|
+
[":is([aria-disabled=true])"]: {
|
|
431
|
+
color: tokens.color.offBlack32,
|
|
432
|
+
":focus-visible": {
|
|
433
|
+
outline: "none"
|
|
434
|
+
}
|
|
435
|
+
},
|
|
380
436
|
["@media not (hover: hover)"]: {
|
|
381
437
|
[":hover[aria-disabled=false]"]: {
|
|
382
438
|
color: white,
|
|
@@ -412,6 +468,10 @@ const styles$5 = aphrodite.StyleSheet.create({
|
|
|
412
468
|
color: tokens.mix(tokens.color.fadedBlue16, white)
|
|
413
469
|
}
|
|
414
470
|
},
|
|
471
|
+
itemFocused: focusedStyle,
|
|
472
|
+
itemDisabled: {
|
|
473
|
+
outlineColor: tokens.color.offBlack32
|
|
474
|
+
},
|
|
415
475
|
itemContainer: {
|
|
416
476
|
minHeight: "unset",
|
|
417
477
|
padding: `${tokens.spacing.xSmall_8 + tokens.spacing.xxxxSmall_2}px ${tokens.spacing.xSmall_8}px`,
|
|
@@ -435,13 +495,13 @@ class SeparatorItem extends React__namespace.Component {
|
|
|
435
495
|
}
|
|
436
496
|
render() {
|
|
437
497
|
return React__namespace.createElement(wonderBlocksCore.View, {
|
|
438
|
-
style: [styles$
|
|
498
|
+
style: [styles$5.separator, this.props.style],
|
|
439
499
|
"aria-hidden": "true"
|
|
440
500
|
});
|
|
441
501
|
}
|
|
442
502
|
}
|
|
443
503
|
SeparatorItem.__IS_SEPARATOR_ITEM__ = true;
|
|
444
|
-
const styles$
|
|
504
|
+
const styles$5 = aphrodite.StyleSheet.create({
|
|
445
505
|
separator: {
|
|
446
506
|
boxShadow: `0 -1px ${tokens.color.offBlack16}`,
|
|
447
507
|
height: 1,
|
|
@@ -1107,7 +1167,7 @@ class DropdownCore extends React__namespace.Component {
|
|
|
1107
1167
|
const numResults = items.length;
|
|
1108
1168
|
if (numResults === 0) {
|
|
1109
1169
|
return React__namespace.createElement(wonderBlocksTypography.LabelMedium, {
|
|
1110
|
-
style: styles$
|
|
1170
|
+
style: styles$4.noResult,
|
|
1111
1171
|
testId: "dropdown-core-no-results"
|
|
1112
1172
|
}, noResults);
|
|
1113
1173
|
}
|
|
@@ -1179,7 +1239,7 @@ class DropdownCore extends React__namespace.Component {
|
|
|
1179
1239
|
onChange: this.handleSearchTextChanged,
|
|
1180
1240
|
placeholder: labels.filter,
|
|
1181
1241
|
ref: this.searchFieldRef,
|
|
1182
|
-
style: styles$
|
|
1242
|
+
style: styles$4.searchInputStyle,
|
|
1183
1243
|
value: searchText || ""
|
|
1184
1244
|
});
|
|
1185
1245
|
}
|
|
@@ -1197,11 +1257,11 @@ class DropdownCore extends React__namespace.Component {
|
|
|
1197
1257
|
const minDropdownWidth = openerStyle ? openerStyle.getPropertyValue("width") : 0;
|
|
1198
1258
|
return React__namespace.createElement(wonderBlocksCore.View, {
|
|
1199
1259
|
onMouseUp: this.handleDropdownMouseUp,
|
|
1200
|
-
style: [styles$
|
|
1260
|
+
style: [styles$4.dropdown, light && styles$4.light, isReferenceHidden && styles$4.hidden, dropdownStyle],
|
|
1201
1261
|
testId: "dropdown-core-container"
|
|
1202
1262
|
}, isFilterable && this.renderSearchField(), React__namespace.createElement(wonderBlocksCore.View, {
|
|
1203
1263
|
role: role,
|
|
1204
|
-
style: [styles$
|
|
1264
|
+
style: [styles$4.listboxOrMenu, {
|
|
1205
1265
|
minWidth: minDropdownWidth
|
|
1206
1266
|
}],
|
|
1207
1267
|
"aria-invalid": role === "listbox" ? ariaInvalid : undefined,
|
|
@@ -1235,7 +1295,7 @@ class DropdownCore extends React__namespace.Component {
|
|
|
1235
1295
|
"aria-live": "polite",
|
|
1236
1296
|
"aria-atomic": "true",
|
|
1237
1297
|
"aria-relevant": "additions text",
|
|
1238
|
-
style: styles$
|
|
1298
|
+
style: styles$4.srOnly,
|
|
1239
1299
|
"data-testid": "dropdown-live-region"
|
|
1240
1300
|
}, open && labels.someResults(totalItems));
|
|
1241
1301
|
}
|
|
@@ -1249,7 +1309,7 @@ class DropdownCore extends React__namespace.Component {
|
|
|
1249
1309
|
return React__namespace.createElement(wonderBlocksCore.View, {
|
|
1250
1310
|
onKeyDown: this.handleKeyDown,
|
|
1251
1311
|
onKeyUp: this.handleKeyUp,
|
|
1252
|
-
style: [styles$
|
|
1312
|
+
style: [styles$4.menuWrapper, style],
|
|
1253
1313
|
className: className
|
|
1254
1314
|
}, this.renderLiveRegion(), opener, open && this.renderDropdown());
|
|
1255
1315
|
}
|
|
@@ -1267,7 +1327,7 @@ DropdownCore.defaultProps = {
|
|
|
1267
1327
|
light: false,
|
|
1268
1328
|
selectionType: "single"
|
|
1269
1329
|
};
|
|
1270
|
-
const styles$
|
|
1330
|
+
const styles$4 = aphrodite.StyleSheet.create({
|
|
1271
1331
|
menuWrapper: {
|
|
1272
1332
|
width: "fit-content"
|
|
1273
1333
|
},
|
|
@@ -1400,11 +1460,11 @@ const sharedStyles = aphrodite.StyleSheet.create({
|
|
|
1400
1460
|
position: "absolute"
|
|
1401
1461
|
}
|
|
1402
1462
|
});
|
|
1403
|
-
const styles$
|
|
1463
|
+
const styles$3 = {};
|
|
1404
1464
|
const _generateStyles$1 = localColor => {
|
|
1405
1465
|
const buttonType = localColor;
|
|
1406
|
-
if (styles$
|
|
1407
|
-
return styles$
|
|
1466
|
+
if (styles$3[buttonType]) {
|
|
1467
|
+
return styles$3[buttonType];
|
|
1408
1468
|
}
|
|
1409
1469
|
const {
|
|
1410
1470
|
offBlack32
|
|
@@ -1436,8 +1496,8 @@ const _generateStyles$1 = localColor => {
|
|
|
1436
1496
|
cursor: "default"
|
|
1437
1497
|
}
|
|
1438
1498
|
};
|
|
1439
|
-
styles$
|
|
1440
|
-
return styles$
|
|
1499
|
+
styles$3[buttonType] = aphrodite.StyleSheet.create(newStyles);
|
|
1500
|
+
return styles$3[buttonType];
|
|
1441
1501
|
};
|
|
1442
1502
|
|
|
1443
1503
|
const _excluded$3 = ["text", "opened"];
|
|
@@ -1573,7 +1633,7 @@ class ActionMenu extends React__namespace.Component {
|
|
|
1573
1633
|
items: items,
|
|
1574
1634
|
openerElement: this.openerElement,
|
|
1575
1635
|
onOpenChanged: this.handleOpenChanged,
|
|
1576
|
-
dropdownStyle: [styles$
|
|
1636
|
+
dropdownStyle: [styles$2.menuTopSpace, dropdownStyle]
|
|
1577
1637
|
});
|
|
1578
1638
|
}
|
|
1579
1639
|
}
|
|
@@ -1581,7 +1641,7 @@ ActionMenu.defaultProps = {
|
|
|
1581
1641
|
alignment: "left",
|
|
1582
1642
|
disabled: false
|
|
1583
1643
|
};
|
|
1584
|
-
const styles$
|
|
1644
|
+
const styles$2 = aphrodite.StyleSheet.create({
|
|
1585
1645
|
caret: {
|
|
1586
1646
|
marginLeft: 4
|
|
1587
1647
|
},
|
|
@@ -1633,7 +1693,7 @@ class SelectOpener extends React__namespace.Component {
|
|
|
1633
1693
|
pressed
|
|
1634
1694
|
} = state;
|
|
1635
1695
|
const iconColor = light ? disabled || pressed ? "currentColor" : tokens__namespace.color.white : disabled ? tokens__namespace.color.offBlack32 : tokens__namespace.color.offBlack64;
|
|
1636
|
-
const style = [styles.shared, stateStyles.default, disabled && stateStyles.disabled, !disabled && (pressed ? stateStyles.active : (hovered || focused) && stateStyles.focus)];
|
|
1696
|
+
const style = [styles$1.shared, stateStyles.default, disabled && stateStyles.disabled, !disabled && (pressed ? stateStyles.active : (hovered || focused) && stateStyles.focus)];
|
|
1637
1697
|
return React__namespace.createElement(StyledButton, _extends({}, sharedProps, {
|
|
1638
1698
|
"aria-expanded": open ? "true" : "false",
|
|
1639
1699
|
"aria-haspopup": "listbox",
|
|
@@ -1643,12 +1703,12 @@ class SelectOpener extends React__namespace.Component {
|
|
|
1643
1703
|
style: style,
|
|
1644
1704
|
type: "button"
|
|
1645
1705
|
}, childrenProps), React__namespace.createElement(wonderBlocksTypography.LabelMedium, {
|
|
1646
|
-
style: styles.text
|
|
1706
|
+
style: styles$1.text
|
|
1647
1707
|
}, children || "\u00A0"), React__namespace.createElement(wonderBlocksIcon.PhosphorIcon, {
|
|
1648
1708
|
icon: caretDownIcon__default["default"],
|
|
1649
1709
|
color: iconColor,
|
|
1650
1710
|
size: "small",
|
|
1651
|
-
style: styles.caret,
|
|
1711
|
+
style: styles$1.caret,
|
|
1652
1712
|
"aria-hidden": "true"
|
|
1653
1713
|
}));
|
|
1654
1714
|
});
|
|
@@ -1663,7 +1723,7 @@ SelectOpener.defaultProps = {
|
|
|
1663
1723
|
light: false,
|
|
1664
1724
|
isPlaceholder: false
|
|
1665
1725
|
};
|
|
1666
|
-
const styles = aphrodite.StyleSheet.create({
|
|
1726
|
+
const styles$1 = aphrodite.StyleSheet.create({
|
|
1667
1727
|
shared: {
|
|
1668
1728
|
position: "relative",
|
|
1669
1729
|
display: "inline-flex",
|
|
@@ -2260,8 +2320,212 @@ MultiSelect.defaultProps = {
|
|
|
2260
2320
|
selectedValues: []
|
|
2261
2321
|
};
|
|
2262
2322
|
|
|
2323
|
+
function updateMultipleSelection(previousSelection, value = "") {
|
|
2324
|
+
if (!previousSelection) {
|
|
2325
|
+
return [value];
|
|
2326
|
+
}
|
|
2327
|
+
return previousSelection.includes(value) ? previousSelection.filter(item => item !== value) : [...previousSelection, value];
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
function useListbox({
|
|
2331
|
+
children: options,
|
|
2332
|
+
disabled,
|
|
2333
|
+
id,
|
|
2334
|
+
selectionType = "single",
|
|
2335
|
+
value
|
|
2336
|
+
}) {
|
|
2337
|
+
const selectedValueIndex = React__namespace.useMemo(() => {
|
|
2338
|
+
const firstValue = Array.isArray(value) ? value[0] : value;
|
|
2339
|
+
if (!firstValue || firstValue === "") {
|
|
2340
|
+
return 0;
|
|
2341
|
+
}
|
|
2342
|
+
return options.findIndex(item => item.props.value === firstValue);
|
|
2343
|
+
}, [options, value]);
|
|
2344
|
+
const [focusedIndex, setFocusedIndex] = React__namespace.useState(selectedValueIndex);
|
|
2345
|
+
const [isListboxFocused, setIsListboxFocused] = React__namespace.useState(false);
|
|
2346
|
+
const [selected, setSelected] = React__namespace.useState(value);
|
|
2347
|
+
const focusItem = index => {
|
|
2348
|
+
setFocusedIndex(index);
|
|
2349
|
+
};
|
|
2350
|
+
const focusPreviousItem = React__namespace.useCallback(() => {
|
|
2351
|
+
if (focusedIndex === 0) {
|
|
2352
|
+
focusItem(options.length - 1);
|
|
2353
|
+
} else {
|
|
2354
|
+
focusItem(focusedIndex - 1);
|
|
2355
|
+
}
|
|
2356
|
+
}, [options, focusedIndex]);
|
|
2357
|
+
const focusNextItem = React__namespace.useCallback(() => {
|
|
2358
|
+
if (focusedIndex === options.length - 1) {
|
|
2359
|
+
focusItem(0);
|
|
2360
|
+
} else {
|
|
2361
|
+
focusItem(focusedIndex + 1);
|
|
2362
|
+
}
|
|
2363
|
+
}, [options, focusedIndex]);
|
|
2364
|
+
const selectOption = React__namespace.useCallback(index => {
|
|
2365
|
+
const optionItem = options[index];
|
|
2366
|
+
if (optionItem.props.disabled) {
|
|
2367
|
+
return;
|
|
2368
|
+
}
|
|
2369
|
+
if (selectionType === "single") {
|
|
2370
|
+
setSelected(optionItem.props.value);
|
|
2371
|
+
} else {
|
|
2372
|
+
setSelected(prevSelected => {
|
|
2373
|
+
const newSelectedValue = updateMultipleSelection(prevSelected, optionItem.props.value);
|
|
2374
|
+
return newSelectedValue;
|
|
2375
|
+
});
|
|
2376
|
+
}
|
|
2377
|
+
}, [options, selectionType]);
|
|
2378
|
+
const handleKeyDown = React__namespace.useCallback(event => {
|
|
2379
|
+
const {
|
|
2380
|
+
key
|
|
2381
|
+
} = event;
|
|
2382
|
+
switch (key) {
|
|
2383
|
+
case "ArrowUp":
|
|
2384
|
+
event.preventDefault();
|
|
2385
|
+
focusPreviousItem();
|
|
2386
|
+
return;
|
|
2387
|
+
case "ArrowDown":
|
|
2388
|
+
event.preventDefault();
|
|
2389
|
+
focusNextItem();
|
|
2390
|
+
return;
|
|
2391
|
+
case "Home":
|
|
2392
|
+
event.preventDefault();
|
|
2393
|
+
focusItem(0);
|
|
2394
|
+
return;
|
|
2395
|
+
case "End":
|
|
2396
|
+
event.preventDefault();
|
|
2397
|
+
focusItem(options.length - 1);
|
|
2398
|
+
return;
|
|
2399
|
+
case "Enter":
|
|
2400
|
+
case " ":
|
|
2401
|
+
event.preventDefault();
|
|
2402
|
+
selectOption(focusedIndex);
|
|
2403
|
+
return;
|
|
2404
|
+
}
|
|
2405
|
+
}, [focusNextItem, focusPreviousItem, focusedIndex, options, selectOption]);
|
|
2406
|
+
const handleKeyUp = React__namespace.useCallback(event => {
|
|
2407
|
+
switch (event.key) {
|
|
2408
|
+
case " ":
|
|
2409
|
+
event.preventDefault();
|
|
2410
|
+
return;
|
|
2411
|
+
}
|
|
2412
|
+
}, []);
|
|
2413
|
+
const handleFocus = React__namespace.useCallback(() => {
|
|
2414
|
+
if (!disabled) {
|
|
2415
|
+
setIsListboxFocused(true);
|
|
2416
|
+
}
|
|
2417
|
+
}, [disabled]);
|
|
2418
|
+
const handleBlur = React__namespace.useCallback(() => {
|
|
2419
|
+
if (!disabled) {
|
|
2420
|
+
setIsListboxFocused(false);
|
|
2421
|
+
}
|
|
2422
|
+
}, [disabled]);
|
|
2423
|
+
const handleClick = React__namespace.useCallback(value => {
|
|
2424
|
+
const index = options.findIndex(item => item.props.value === value);
|
|
2425
|
+
const isOptionDisabled = options[index].props.disabled;
|
|
2426
|
+
if (disabled || isOptionDisabled) {
|
|
2427
|
+
return;
|
|
2428
|
+
}
|
|
2429
|
+
focusItem(index);
|
|
2430
|
+
selectOption(index);
|
|
2431
|
+
}, [disabled, options, selectOption]);
|
|
2432
|
+
const renderList = React__namespace.useMemo(() => {
|
|
2433
|
+
return options.map((component, index) => {
|
|
2434
|
+
const isSelected = (selected == null ? void 0 : selected.includes(component.props.value)) || false;
|
|
2435
|
+
const optionId = id ? `${id}-option-${index}` : `option-${index}`;
|
|
2436
|
+
return React__namespace.cloneElement(component, {
|
|
2437
|
+
key: index,
|
|
2438
|
+
focused: isListboxFocused && index === focusedIndex,
|
|
2439
|
+
disabled: component.props.disabled || disabled || false,
|
|
2440
|
+
selected: isSelected,
|
|
2441
|
+
variant: selectionType === "single" ? "check" : "checkbox",
|
|
2442
|
+
parentComponent: "listbox",
|
|
2443
|
+
id: optionId,
|
|
2444
|
+
onToggle: () => {
|
|
2445
|
+
handleClick(component.props.value);
|
|
2446
|
+
},
|
|
2447
|
+
role: "option"
|
|
2448
|
+
});
|
|
2449
|
+
});
|
|
2450
|
+
}, [options, selected, id, isListboxFocused, focusedIndex, disabled, selectionType, handleClick]);
|
|
2451
|
+
return {
|
|
2452
|
+
isListboxFocused,
|
|
2453
|
+
focusedIndex,
|
|
2454
|
+
renderList,
|
|
2455
|
+
selected,
|
|
2456
|
+
handleKeyDown,
|
|
2457
|
+
handleKeyUp,
|
|
2458
|
+
handleFocus,
|
|
2459
|
+
handleBlur
|
|
2460
|
+
};
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
function Listbox(props) {
|
|
2464
|
+
const {
|
|
2465
|
+
children,
|
|
2466
|
+
disabled,
|
|
2467
|
+
id,
|
|
2468
|
+
onChange,
|
|
2469
|
+
selectionType = "single",
|
|
2470
|
+
style,
|
|
2471
|
+
tabIndex = 0,
|
|
2472
|
+
testId,
|
|
2473
|
+
value,
|
|
2474
|
+
"aria-label": ariaLabel,
|
|
2475
|
+
"aria-labelledby": ariaLabelledby
|
|
2476
|
+
} = props;
|
|
2477
|
+
const ids = wonderBlocksCore.useUniqueIdWithMock("listbox");
|
|
2478
|
+
const uniqueId = id != null ? id : ids.get("id");
|
|
2479
|
+
const {
|
|
2480
|
+
focusedIndex,
|
|
2481
|
+
isListboxFocused,
|
|
2482
|
+
renderList,
|
|
2483
|
+
selected,
|
|
2484
|
+
handleKeyDown,
|
|
2485
|
+
handleKeyUp,
|
|
2486
|
+
handleFocus,
|
|
2487
|
+
handleBlur
|
|
2488
|
+
} = useListbox({
|
|
2489
|
+
children,
|
|
2490
|
+
disabled,
|
|
2491
|
+
id: uniqueId,
|
|
2492
|
+
selectionType,
|
|
2493
|
+
value
|
|
2494
|
+
});
|
|
2495
|
+
React__namespace.useEffect(() => {
|
|
2496
|
+
if (selected && selected !== value) {
|
|
2497
|
+
onChange == null ? void 0 : onChange(selected);
|
|
2498
|
+
}
|
|
2499
|
+
}, [onChange, selected, value]);
|
|
2500
|
+
return React__namespace.createElement(wonderBlocksCore.View, {
|
|
2501
|
+
role: "listbox",
|
|
2502
|
+
"aria-disabled": disabled,
|
|
2503
|
+
id: uniqueId,
|
|
2504
|
+
style: [styles.listbox, style, disabled && styles.disabled],
|
|
2505
|
+
tabIndex: tabIndex,
|
|
2506
|
+
onKeyDown: handleKeyDown,
|
|
2507
|
+
onKeyUp: handleKeyUp,
|
|
2508
|
+
onFocus: handleFocus,
|
|
2509
|
+
onBlur: handleBlur,
|
|
2510
|
+
testId: testId,
|
|
2511
|
+
"aria-activedescendant": isListboxFocused ? renderList[focusedIndex].props.id : undefined,
|
|
2512
|
+
"aria-label": ariaLabel,
|
|
2513
|
+
"aria-labelledby": ariaLabelledby,
|
|
2514
|
+
"aria-multiselectable": selectionType === "multiple"
|
|
2515
|
+
}, renderList);
|
|
2516
|
+
}
|
|
2517
|
+
const styles = aphrodite.StyleSheet.create({
|
|
2518
|
+
listbox: {
|
|
2519
|
+
outline: "none"
|
|
2520
|
+
},
|
|
2521
|
+
disabled: {
|
|
2522
|
+
color: tokens.color.offBlack64
|
|
2523
|
+
}
|
|
2524
|
+
});
|
|
2525
|
+
|
|
2263
2526
|
exports.ActionItem = ActionItem;
|
|
2264
2527
|
exports.ActionMenu = ActionMenu;
|
|
2528
|
+
exports.Listbox = Listbox;
|
|
2265
2529
|
exports.MultiSelect = MultiSelect;
|
|
2266
2530
|
exports.OptionItem = OptionItem;
|
|
2267
2531
|
exports.SeparatorItem = SeparatorItem;
|
package/dist/util/types.d.ts
CHANGED
|
@@ -26,4 +26,10 @@ export type OpenerProps = ClickableState & {
|
|
|
26
26
|
text: OptionLabel;
|
|
27
27
|
opened: boolean;
|
|
28
28
|
};
|
|
29
|
-
export type
|
|
29
|
+
export type OptionItemComponent = React.ReactElement<PropsFor<typeof OptionItem>>;
|
|
30
|
+
export type OptionItemComponentArray = OptionItemComponent[];
|
|
31
|
+
/**
|
|
32
|
+
* Allows optional values to be passed to the listbox.
|
|
33
|
+
*/
|
|
34
|
+
export type MaybeString = string | null | undefined;
|
|
35
|
+
export type MaybeValueOrValues = MaybeString | Array<MaybeString>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@khanacademy/wonder-blocks-dropdown",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.3.1",
|
|
4
4
|
"design": "v1",
|
|
5
5
|
"description": "Dropdown variants for Wonder Blocks.",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -16,16 +16,16 @@
|
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"@babel/runtime": "^7.18.6",
|
|
19
|
-
"@khanacademy/wonder-blocks-cell": "^3.3.
|
|
20
|
-
"@khanacademy/wonder-blocks-clickable": "^4.2.
|
|
21
|
-
"@khanacademy/wonder-blocks-core": "^6.4.
|
|
22
|
-
"@khanacademy/wonder-blocks-icon": "^4.1.
|
|
23
|
-
"@khanacademy/wonder-blocks-layout": "^2.0.
|
|
24
|
-
"@khanacademy/wonder-blocks-modal": "^5.1.
|
|
25
|
-
"@khanacademy/wonder-blocks-search-field": "^2.2.
|
|
26
|
-
"@khanacademy/wonder-blocks-timing": "^
|
|
19
|
+
"@khanacademy/wonder-blocks-cell": "^3.3.6",
|
|
20
|
+
"@khanacademy/wonder-blocks-clickable": "^4.2.2",
|
|
21
|
+
"@khanacademy/wonder-blocks-core": "^6.4.1",
|
|
22
|
+
"@khanacademy/wonder-blocks-icon": "^4.1.1",
|
|
23
|
+
"@khanacademy/wonder-blocks-layout": "^2.0.33",
|
|
24
|
+
"@khanacademy/wonder-blocks-modal": "^5.1.3",
|
|
25
|
+
"@khanacademy/wonder-blocks-search-field": "^2.2.11",
|
|
26
|
+
"@khanacademy/wonder-blocks-timing": "^5.0.0",
|
|
27
27
|
"@khanacademy/wonder-blocks-tokens": "^1.3.0",
|
|
28
|
-
"@khanacademy/wonder-blocks-typography": "^2.1.
|
|
28
|
+
"@khanacademy/wonder-blocks-typography": "^2.1.12"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
31
|
"@phosphor-icons/core": "^2.0.2",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"react-window": "^1.8.5"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@khanacademy/wonder-blocks-button": "^6.3.
|
|
42
|
+
"@khanacademy/wonder-blocks-button": "^6.3.2",
|
|
43
43
|
"@khanacademy/wb-dev-build-settings": "^1.0.0"
|
|
44
44
|
}
|
|
45
45
|
}
|