@khanacademy/wonder-blocks-dropdown 2.6.7 → 2.6.8
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 +6 -0
- package/dist/es/index.js +202 -79
- package/dist/index.js +310 -174
- package/package.json +1 -1
- package/src/__tests__/__snapshots__/generated-snapshot.test.js.snap +1 -1
- package/src/__tests__/generated-snapshot.test.js +1 -1
- package/src/components/__tests__/action-menu.test.js +126 -166
- package/src/components/__tests__/dropdown-core-virtualized.test.js +20 -35
- package/src/components/__tests__/dropdown-core.test.js +724 -652
- package/src/components/__tests__/dropdown-popper.test.js +51 -0
- package/src/components/__tests__/multi-select.test.js +717 -518
- package/src/components/__tests__/single-select.test.js +52 -41
- package/src/components/action-item.js +2 -0
- package/src/components/dropdown-core.js +159 -90
- package/src/components/dropdown-popper.js +105 -0
- package/src/components/single-select.md +1 -1
- package/src/components/single-select.stories.js +49 -1
package/CHANGELOG.md
CHANGED
package/dist/es/index.js
CHANGED
|
@@ -11,11 +11,11 @@ import { addStyle, View } from '@khanacademy/wonder-blocks-core';
|
|
|
11
11
|
import _objectWithoutPropertiesLoose from '@babel/runtime/helpers/objectWithoutPropertiesLoose';
|
|
12
12
|
import Icon, { icons } from '@khanacademy/wonder-blocks-icon';
|
|
13
13
|
import ReactDOM from 'react-dom';
|
|
14
|
-
import { Popper } from 'react-popper';
|
|
15
14
|
import { VariableSizeList } from 'react-window';
|
|
16
|
-
import { maybeGetPortalMountedModalHostElement } from '@khanacademy/wonder-blocks-modal';
|
|
17
15
|
import { withActionScheduler } from '@khanacademy/wonder-blocks-timing';
|
|
18
16
|
import IconButton from '@khanacademy/wonder-blocks-icon-button';
|
|
17
|
+
import { Popper } from 'react-popper';
|
|
18
|
+
import { maybeGetPortalMountedModalHostElement } from '@khanacademy/wonder-blocks-modal';
|
|
19
19
|
import { Strut } from '@khanacademy/wonder-blocks-layout';
|
|
20
20
|
|
|
21
21
|
const keyCodes = {
|
|
@@ -299,7 +299,7 @@ const styles$7 = StyleSheet.create({
|
|
|
299
299
|
}
|
|
300
300
|
});
|
|
301
301
|
|
|
302
|
-
const _excluded$
|
|
302
|
+
const _excluded$5 = ["disabled", "label", "role", "selected", "testId", "style", "value", "onClick", "onToggle", "variant"];
|
|
303
303
|
|
|
304
304
|
/**
|
|
305
305
|
* For option items that can be selected in a dropdown, selection denoted either
|
|
@@ -346,7 +346,7 @@ class OptionItem extends React.Component {
|
|
|
346
346
|
testId,
|
|
347
347
|
style
|
|
348
348
|
} = _this$props,
|
|
349
|
-
sharedProps = _objectWithoutPropertiesLoose(_this$props, _excluded$
|
|
349
|
+
sharedProps = _objectWithoutPropertiesLoose(_this$props, _excluded$5);
|
|
350
350
|
|
|
351
351
|
const ClickableBehavior = getClickableBehavior();
|
|
352
352
|
const CheckComponent = this.getCheckComponent();
|
|
@@ -909,7 +909,76 @@ class DropdownCoreVirtualized extends React.Component {
|
|
|
909
909
|
|
|
910
910
|
var DropdownCoreVirtualized$1 = withActionScheduler(DropdownCoreVirtualized);
|
|
911
911
|
|
|
912
|
-
const
|
|
912
|
+
const modifiers = [{
|
|
913
|
+
name: "preventOverflow",
|
|
914
|
+
options: {
|
|
915
|
+
rootBoundary: "viewport",
|
|
916
|
+
// Allows to overlap the popper in case there's no more vertical
|
|
917
|
+
// room in the viewport.
|
|
918
|
+
altAxis: true,
|
|
919
|
+
// Also needed to make sure the Popper will be displayed correctly
|
|
920
|
+
// in different contexts (e.g inside a Modal)
|
|
921
|
+
tether: false
|
|
922
|
+
}
|
|
923
|
+
}];
|
|
924
|
+
|
|
925
|
+
/**
|
|
926
|
+
* A wrapper for PopperJS that renders the children inside a portal.
|
|
927
|
+
*/
|
|
928
|
+
function DropdownPopper({
|
|
929
|
+
children,
|
|
930
|
+
alignment = "left",
|
|
931
|
+
onPopperElement,
|
|
932
|
+
referenceElement
|
|
933
|
+
}) {
|
|
934
|
+
// If we are in a modal, we find where we should be portalling the menu by
|
|
935
|
+
// using the helper function from the modal package on the opener element.
|
|
936
|
+
// If we are not in a modal, we use body as the location to portal to.
|
|
937
|
+
const modalHost = maybeGetPortalMountedModalHostElement(referenceElement) || document.querySelector("body");
|
|
938
|
+
|
|
939
|
+
if (!modalHost) {
|
|
940
|
+
return null;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
return /*#__PURE__*/ReactDOM.createPortal( /*#__PURE__*/React.createElement(Popper, {
|
|
944
|
+
innerRef: node => {
|
|
945
|
+
if (node && onPopperElement) {
|
|
946
|
+
onPopperElement(node);
|
|
947
|
+
}
|
|
948
|
+
},
|
|
949
|
+
referenceElement: referenceElement,
|
|
950
|
+
strategy: "fixed",
|
|
951
|
+
placement: alignment === "left" ? "bottom-start" : "bottom-end",
|
|
952
|
+
modifiers: modifiers
|
|
953
|
+
}, ({
|
|
954
|
+
placement,
|
|
955
|
+
ref,
|
|
956
|
+
style,
|
|
957
|
+
hasPopperEscaped,
|
|
958
|
+
isReferenceHidden
|
|
959
|
+
}) => {
|
|
960
|
+
const shouldHidePopper = !!(hasPopperEscaped || isReferenceHidden);
|
|
961
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
962
|
+
ref: ref,
|
|
963
|
+
style: style,
|
|
964
|
+
"data-test-id": "dropdown-popper",
|
|
965
|
+
"data-placement": placement
|
|
966
|
+
}, children(shouldHidePopper));
|
|
967
|
+
}), modalHost);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
/**
|
|
971
|
+
* The number of options to apply the virtualized list to.
|
|
972
|
+
*
|
|
973
|
+
* NOTE: The threshold is defined taking into account performance
|
|
974
|
+
* implications (e.g. process input events for users should not be longer
|
|
975
|
+
* than 100ms).
|
|
976
|
+
* @see https://web.dev/rail/?utm_source=devtools#goals-and-guidelines
|
|
977
|
+
*
|
|
978
|
+
* TODO(juan, WB-1263): Improve performance by refactoring this component.
|
|
979
|
+
*/
|
|
980
|
+
|
|
981
|
+
const VIRTUALIZE_THRESHOLD = 125;
|
|
913
982
|
|
|
914
983
|
/**
|
|
915
984
|
* A core dropdown component that takes an opener and children to display as
|
|
@@ -1092,6 +1161,18 @@ class DropdownCore extends React.Component {
|
|
|
1092
1161
|
}
|
|
1093
1162
|
};
|
|
1094
1163
|
|
|
1164
|
+
this.handleItemClick = (focusIndex, item) => {
|
|
1165
|
+
this.handleClickFocus(focusIndex);
|
|
1166
|
+
|
|
1167
|
+
if (item.component.props.onClick) {
|
|
1168
|
+
item.component.props.onClick();
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
if (item.populatedProps.onClick) {
|
|
1172
|
+
item.populatedProps.onClick();
|
|
1173
|
+
}
|
|
1174
|
+
};
|
|
1175
|
+
|
|
1095
1176
|
this.resetFocusedIndex();
|
|
1096
1177
|
this.state = {
|
|
1097
1178
|
prevItems: this.props.items,
|
|
@@ -1101,7 +1182,7 @@ class DropdownCore extends React.Component {
|
|
|
1101
1182
|
noResults: defaultLabels.noResults
|
|
1102
1183
|
}, props.labels)
|
|
1103
1184
|
};
|
|
1104
|
-
this.
|
|
1185
|
+
this.virtualizedListRef = /*#__PURE__*/React.createRef();
|
|
1105
1186
|
}
|
|
1106
1187
|
|
|
1107
1188
|
componentDidMount() {
|
|
@@ -1225,8 +1306,13 @@ class DropdownCore extends React.Component {
|
|
|
1225
1306
|
}
|
|
1226
1307
|
|
|
1227
1308
|
scheduleToFocusCurrentItem() {
|
|
1228
|
-
|
|
1229
|
-
|
|
1309
|
+
if (this.shouldVirtualizeList()) {
|
|
1310
|
+
// wait for windowed items to be recalculated
|
|
1311
|
+
this.props.schedule.animationFrame(() => this.focusCurrentItem());
|
|
1312
|
+
} else {
|
|
1313
|
+
// immediately focus the current item if we're not virtualizing
|
|
1314
|
+
this.focusCurrentItem();
|
|
1315
|
+
}
|
|
1230
1316
|
}
|
|
1231
1317
|
|
|
1232
1318
|
focusCurrentItem() {
|
|
@@ -1234,12 +1320,12 @@ class DropdownCore extends React.Component {
|
|
|
1234
1320
|
|
|
1235
1321
|
if (fousedItemRef) {
|
|
1236
1322
|
// force react-window to scroll to ensure the focused item is visible
|
|
1237
|
-
if (this.
|
|
1323
|
+
if (this.virtualizedListRef.current) {
|
|
1238
1324
|
// Our focused index does not include disabled items, but the
|
|
1239
1325
|
// react-window index system does include the disabled items
|
|
1240
1326
|
// in the count. So we need to use "originalIndex", which
|
|
1241
1327
|
// does account for disabled items.
|
|
1242
|
-
this.
|
|
1328
|
+
this.virtualizedListRef.current.scrollToItem(fousedItemRef.originalIndex);
|
|
1243
1329
|
}
|
|
1244
1330
|
|
|
1245
1331
|
const node = ReactDOM.findDOMNode(fousedItemRef.ref.current);
|
|
@@ -1323,13 +1409,86 @@ class DropdownCore extends React.Component {
|
|
|
1323
1409
|
|
|
1324
1410
|
return null;
|
|
1325
1411
|
}
|
|
1412
|
+
/**
|
|
1413
|
+
* Handles click events for each item in the dropdown.
|
|
1414
|
+
*/
|
|
1415
|
+
|
|
1416
|
+
|
|
1417
|
+
/**
|
|
1418
|
+
* Determines which rendering strategy we are going to apply to the options
|
|
1419
|
+
* list.
|
|
1420
|
+
*/
|
|
1421
|
+
shouldVirtualizeList() {
|
|
1422
|
+
// Verify if the list is long enough to be virtualized (passes the
|
|
1423
|
+
// threshold).
|
|
1424
|
+
return this.props.items.length > VIRTUALIZE_THRESHOLD;
|
|
1425
|
+
}
|
|
1426
|
+
/**
|
|
1427
|
+
* Renders the non-virtualized list of items.
|
|
1428
|
+
*/
|
|
1429
|
+
|
|
1430
|
+
|
|
1431
|
+
renderList() {
|
|
1432
|
+
let focusCounter = 0;
|
|
1433
|
+
const itemRole = this.getItemRole(); // if we don't need to virtualize, we can render the list directly
|
|
1434
|
+
|
|
1435
|
+
return this.props.items.map((item, index) => {
|
|
1436
|
+
if (SeparatorItem.isClassOf(item.component)) {
|
|
1437
|
+
return item.component;
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
const {
|
|
1441
|
+
component,
|
|
1442
|
+
focusable,
|
|
1443
|
+
populatedProps
|
|
1444
|
+
} = item;
|
|
1445
|
+
|
|
1446
|
+
if (focusable) {
|
|
1447
|
+
focusCounter += 1;
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
const focusIndex = focusCounter - 1; // The reference to the item is used to restore focus.
|
|
1451
|
+
|
|
1452
|
+
const currentRef = this.state.itemRefs[focusIndex] ? this.state.itemRefs[focusIndex].ref : null; // Render the SearchField component.
|
|
1453
|
+
|
|
1454
|
+
if (SearchTextInput.isClassOf(component)) {
|
|
1455
|
+
return /*#__PURE__*/React.cloneElement(component, _extends({}, populatedProps, {
|
|
1456
|
+
key: "search-text-input",
|
|
1457
|
+
// pass the current ref down to the input element
|
|
1458
|
+
itemRef: currentRef,
|
|
1459
|
+
// override to avoid losing focus when pressing a key
|
|
1460
|
+
onClick: () => {
|
|
1461
|
+
this.handleClickFocus(0);
|
|
1462
|
+
this.focusCurrentItem();
|
|
1463
|
+
},
|
|
1464
|
+
// apply custom styles
|
|
1465
|
+
style: searchInputStyle
|
|
1466
|
+
}));
|
|
1467
|
+
} // Render OptionItem and/or ActionItem elements.
|
|
1468
|
+
|
|
1469
|
+
|
|
1470
|
+
return /*#__PURE__*/React.cloneElement(component, _extends({}, populatedProps, {
|
|
1471
|
+
key: index,
|
|
1472
|
+
onClick: () => {
|
|
1473
|
+
this.handleItemClick(focusIndex, item);
|
|
1474
|
+
},
|
|
1475
|
+
// Only pass the ref if the item is focusable.
|
|
1476
|
+
ref: focusable ? currentRef : null,
|
|
1477
|
+
role: itemRole
|
|
1478
|
+
}));
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1326
1481
|
/**
|
|
1327
1482
|
* Process the items and wrap them into an array that react-window can
|
|
1328
|
-
* interpret
|
|
1483
|
+
* interpret.
|
|
1484
|
+
*
|
|
1485
|
+
* NOTE: The main difference with the collection in renderList() is that we
|
|
1486
|
+
* massage the items to be able to clone them later in
|
|
1487
|
+
* DropdownVirtualizedItem, where as renderList() clones the items directly.
|
|
1329
1488
|
*/
|
|
1330
1489
|
|
|
1331
1490
|
|
|
1332
|
-
|
|
1491
|
+
parseVirtualizedItems() {
|
|
1333
1492
|
let focusCounter = 0;
|
|
1334
1493
|
const itemRole = this.getItemRole();
|
|
1335
1494
|
return this.props.items.map((item, index) => {
|
|
@@ -1358,21 +1517,26 @@ class DropdownCore extends React.Component {
|
|
|
1358
1517
|
role: itemRole,
|
|
1359
1518
|
ref: item.focusable ? this.state.itemRefs[focusIndex] ? this.state.itemRefs[focusIndex].ref : null : null,
|
|
1360
1519
|
onClick: () => {
|
|
1361
|
-
this.
|
|
1362
|
-
|
|
1363
|
-
if (item.component.props.onClick) {
|
|
1364
|
-
item.component.props.onClick();
|
|
1365
|
-
}
|
|
1366
|
-
|
|
1367
|
-
if (item.populatedProps.onClick) {
|
|
1368
|
-
item.populatedProps.onClick();
|
|
1369
|
-
}
|
|
1520
|
+
this.handleItemClick(focusIndex, item);
|
|
1370
1521
|
}
|
|
1371
1522
|
});
|
|
1372
1523
|
});
|
|
1373
1524
|
}
|
|
1525
|
+
/**
|
|
1526
|
+
* Render the items using a virtualized list
|
|
1527
|
+
*/
|
|
1528
|
+
|
|
1529
|
+
|
|
1530
|
+
renderVirtualizedList() {
|
|
1531
|
+
// preprocess items data to pass it to the renderer
|
|
1532
|
+
const virtualizedItems = this.parseVirtualizedItems();
|
|
1533
|
+
return /*#__PURE__*/React.createElement(DropdownCoreVirtualized$1, {
|
|
1534
|
+
data: virtualizedItems,
|
|
1535
|
+
listRef: this.virtualizedListRef
|
|
1536
|
+
});
|
|
1537
|
+
}
|
|
1374
1538
|
|
|
1375
|
-
|
|
1539
|
+
renderDropdownMenu(listRenderer, isReferenceHidden) {
|
|
1376
1540
|
const {
|
|
1377
1541
|
dropdownStyle,
|
|
1378
1542
|
light,
|
|
@@ -1381,9 +1545,7 @@ class DropdownCore extends React.Component {
|
|
|
1381
1545
|
// It's only used if the element exists in the DOM
|
|
1382
1546
|
|
|
1383
1547
|
const openerStyle = openerElement && window.getComputedStyle(openerElement);
|
|
1384
|
-
const minDropdownWidth = openerStyle ? openerStyle.getPropertyValue("width") : 0;
|
|
1385
|
-
|
|
1386
|
-
const itemsList = this.parseItemsList();
|
|
1548
|
+
const minDropdownWidth = openerStyle ? openerStyle.getPropertyValue("width") : 0;
|
|
1387
1549
|
return /*#__PURE__*/React.createElement(View // Stop propagation to prevent the mouseup listener on the
|
|
1388
1550
|
// document from closing the menu.
|
|
1389
1551
|
, {
|
|
@@ -1392,66 +1554,27 @@ class DropdownCore extends React.Component {
|
|
|
1392
1554
|
style: [styles$3.dropdown, light && styles$3.light, isReferenceHidden && styles$3.hidden, {
|
|
1393
1555
|
minWidth: minDropdownWidth
|
|
1394
1556
|
}, dropdownStyle]
|
|
1395
|
-
},
|
|
1396
|
-
data: itemsList,
|
|
1397
|
-
listRef: this.listRef
|
|
1398
|
-
}), this.maybeRenderNoResults());
|
|
1557
|
+
}, listRenderer, this.maybeRenderNoResults());
|
|
1399
1558
|
}
|
|
1400
1559
|
|
|
1401
1560
|
renderDropdown() {
|
|
1402
1561
|
const {
|
|
1403
1562
|
alignment,
|
|
1404
1563
|
openerElement
|
|
1405
|
-
} = this.props; //
|
|
1406
|
-
//
|
|
1407
|
-
//
|
|
1408
|
-
//
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
referenceElement: this.props.openerElement,
|
|
1420
|
-
strategy: "fixed",
|
|
1421
|
-
placement: alignment === "left" ? "bottom-start" : "bottom-end",
|
|
1422
|
-
modifiers: [{
|
|
1423
|
-
name: "preventOverflow",
|
|
1424
|
-
options: {
|
|
1425
|
-
rootBoundary: "viewport",
|
|
1426
|
-
// Allows to overlap the popper in case there's
|
|
1427
|
-
// no more vertical room in the viewport.
|
|
1428
|
-
altAxis: true,
|
|
1429
|
-
// Also needed to make sure the Popper will be
|
|
1430
|
-
// displayed correctly in different contexts
|
|
1431
|
-
// (e.g inside a Modal)
|
|
1432
|
-
tether: false
|
|
1433
|
-
}
|
|
1434
|
-
}]
|
|
1435
|
-
}, ({
|
|
1436
|
-
placement,
|
|
1437
|
-
ref,
|
|
1438
|
-
style,
|
|
1439
|
-
hasPopperEscaped,
|
|
1440
|
-
isReferenceHidden
|
|
1441
|
-
}) => {
|
|
1442
|
-
// For some reason react-popper includes `pointerEvents: "none"`
|
|
1443
|
-
// in the `style` it passes to us, but only when running the tests.
|
|
1444
|
-
const restStyle = _objectWithoutPropertiesLoose(style, _excluded$5);
|
|
1445
|
-
|
|
1446
|
-
return /*#__PURE__*/React.createElement("div", {
|
|
1447
|
-
ref: ref,
|
|
1448
|
-
style: restStyle,
|
|
1449
|
-
"data-placement": placement
|
|
1450
|
-
}, this.renderItems(hasPopperEscaped || isReferenceHidden));
|
|
1451
|
-
}), modalHost);
|
|
1452
|
-
}
|
|
1453
|
-
|
|
1454
|
-
return null;
|
|
1564
|
+
} = this.props; // Preprocess the items that are used inside the Popper instance. By
|
|
1565
|
+
// doing this, we optimize the list to be processed only one time
|
|
1566
|
+
// instead of every time popper changes.
|
|
1567
|
+
// NOTE: This improves the performance impact of the dropdown by
|
|
1568
|
+
// reducing the execution time up to 2.5X.
|
|
1569
|
+
|
|
1570
|
+
const listRenderer = this.shouldVirtualizeList() ? this.renderVirtualizedList() : this.renderList();
|
|
1571
|
+
return /*#__PURE__*/React.createElement(DropdownPopper, {
|
|
1572
|
+
alignment: alignment,
|
|
1573
|
+
onPopperElement: popperElement => {
|
|
1574
|
+
this.popperElement = popperElement;
|
|
1575
|
+
},
|
|
1576
|
+
referenceElement: openerElement
|
|
1577
|
+
}, isReferenceHidden => this.renderDropdownMenu(listRenderer, isReferenceHidden));
|
|
1455
1578
|
}
|
|
1456
1579
|
|
|
1457
1580
|
render() {
|