@khanacademy/wonder-blocks-dropdown 2.7.6 → 2.8.0
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 +15 -0
- package/dist/es/index.js +75 -5
- package/dist/index.js +157 -39
- package/package.json +7 -7
- package/src/components/__tests__/dropdown-core.test.js +3 -5
- package/src/components/__tests__/multi-select.test.js +48 -0
- package/src/components/__tests__/single-select.test.js +28 -3
- package/src/components/dropdown-core.js +84 -4
- package/src/components/multi-select.js +1 -0
- package/src/components/single-select.js +1 -0
- package/src/util/__tests__/helpers.test.js +73 -0
- package/src/util/helpers.js +44 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# @khanacademy/wonder-blocks-dropdown
|
|
2
2
|
|
|
3
|
+
## 2.8.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- ee6fc773: Added keyboard support to search items when the dropdown is focused, included "Enter" as a key to trigger actions with the "option" role
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [ee6fc773]
|
|
12
|
+
- @khanacademy/wonder-blocks-clickable@2.3.0
|
|
13
|
+
- @khanacademy/wonder-blocks-button@3.0.1
|
|
14
|
+
- @khanacademy/wonder-blocks-icon-button@3.4.9
|
|
15
|
+
- @khanacademy/wonder-blocks-modal@2.3.4
|
|
16
|
+
- @khanacademy/wonder-blocks-search-field@1.0.7
|
|
17
|
+
|
|
3
18
|
## 2.7.6
|
|
4
19
|
|
|
5
20
|
### Patch Changes
|
package/dist/es/index.js
CHANGED
|
@@ -664,6 +664,26 @@ function DropdownPopper({
|
|
|
664
664
|
}), modalHost);
|
|
665
665
|
}
|
|
666
666
|
|
|
667
|
+
function getStringForKey(key) {
|
|
668
|
+
if (key.length === 1 || !/^[A-Z]/i.test(key)) {
|
|
669
|
+
return key;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
return "";
|
|
673
|
+
}
|
|
674
|
+
function debounce(callback, wait) {
|
|
675
|
+
let timeout;
|
|
676
|
+
return function executedFunction(...args) {
|
|
677
|
+
const later = () => {
|
|
678
|
+
clearTimeout(timeout);
|
|
679
|
+
callback.apply(void 0, args);
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
clearTimeout(timeout);
|
|
683
|
+
timeout = setTimeout(later, wait);
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
|
|
667
687
|
const VIRTUALIZE_THRESHOLD = 125;
|
|
668
688
|
const StyledSpan = addStyle("span");
|
|
669
689
|
|
|
@@ -734,6 +754,12 @@ class DropdownCore extends React.Component {
|
|
|
734
754
|
} = this.props;
|
|
735
755
|
const keyCode = event.which || event.keyCode;
|
|
736
756
|
|
|
757
|
+
if (getStringForKey(event.key)) {
|
|
758
|
+
event.stopPropagation();
|
|
759
|
+
this.textSuggestion += event.key;
|
|
760
|
+
this.handleKeyDownDebounced(this.textSuggestion);
|
|
761
|
+
}
|
|
762
|
+
|
|
737
763
|
if (!open) {
|
|
738
764
|
if (keyCode === keyCodes.down) {
|
|
739
765
|
event.preventDefault();
|
|
@@ -801,6 +827,39 @@ class DropdownCore extends React.Component {
|
|
|
801
827
|
}
|
|
802
828
|
};
|
|
803
829
|
|
|
830
|
+
this.handleKeyDownDebounceResult = key => {
|
|
831
|
+
const foundIndex = this.props.items.filter(item => item.focusable).findIndex(({
|
|
832
|
+
component
|
|
833
|
+
}) => {
|
|
834
|
+
var _component$props;
|
|
835
|
+
|
|
836
|
+
if (SeparatorItem.isClassOf(component)) {
|
|
837
|
+
return false;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
const label = (_component$props = component.props) == null ? void 0 : _component$props.label.toLowerCase();
|
|
841
|
+
return label.startsWith(key.toLowerCase());
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
if (foundIndex >= 0) {
|
|
845
|
+
const isClosed = !this.props.open;
|
|
846
|
+
|
|
847
|
+
if (isClosed) {
|
|
848
|
+
this.props.onOpenChanged(true);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
this.focusedIndex = foundIndex;
|
|
852
|
+
this.scheduleToFocusCurrentItem(node => {
|
|
853
|
+
if (this.props.selectionType === "single" && isClosed && node) {
|
|
854
|
+
node.click();
|
|
855
|
+
this.props.onOpenChanged(false);
|
|
856
|
+
}
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
this.textSuggestion = "";
|
|
861
|
+
};
|
|
862
|
+
|
|
804
863
|
this.handleClickFocus = index => {
|
|
805
864
|
this.itemsClicked = true;
|
|
806
865
|
this.focusedIndex = index;
|
|
@@ -847,6 +906,8 @@ class DropdownCore extends React.Component {
|
|
|
847
906
|
}, props.labels)
|
|
848
907
|
};
|
|
849
908
|
this.virtualizedListRef = React.createRef();
|
|
909
|
+
this.handleKeyDownDebounced = debounce(this.handleKeyDownDebounceResult, 500);
|
|
910
|
+
this.textSuggestion = "";
|
|
850
911
|
}
|
|
851
912
|
|
|
852
913
|
componentDidMount() {
|
|
@@ -941,15 +1002,17 @@ class DropdownCore extends React.Component {
|
|
|
941
1002
|
document.removeEventListener("touchend", this.handleInteract);
|
|
942
1003
|
}
|
|
943
1004
|
|
|
944
|
-
scheduleToFocusCurrentItem() {
|
|
1005
|
+
scheduleToFocusCurrentItem(onFocus) {
|
|
945
1006
|
if (this.shouldVirtualizeList()) {
|
|
946
|
-
this.props.schedule.animationFrame(() =>
|
|
1007
|
+
this.props.schedule.animationFrame(() => {
|
|
1008
|
+
this.focusCurrentItem(onFocus);
|
|
1009
|
+
});
|
|
947
1010
|
} else {
|
|
948
|
-
this.focusCurrentItem();
|
|
1011
|
+
this.focusCurrentItem(onFocus);
|
|
949
1012
|
}
|
|
950
1013
|
}
|
|
951
1014
|
|
|
952
|
-
focusCurrentItem() {
|
|
1015
|
+
focusCurrentItem(onFocus) {
|
|
953
1016
|
const focusedItemRef = this.state.itemRefs[this.focusedIndex];
|
|
954
1017
|
|
|
955
1018
|
if (focusedItemRef) {
|
|
@@ -962,6 +1025,10 @@ class DropdownCore extends React.Component {
|
|
|
962
1025
|
if (node) {
|
|
963
1026
|
node.focus();
|
|
964
1027
|
this.focusedOriginalIndex = focusedItemRef.originalIndex;
|
|
1028
|
+
|
|
1029
|
+
if (onFocus) {
|
|
1030
|
+
onFocus(node);
|
|
1031
|
+
}
|
|
965
1032
|
}
|
|
966
1033
|
}
|
|
967
1034
|
}
|
|
@@ -1206,7 +1273,8 @@ DropdownCore.defaultProps = {
|
|
|
1206
1273
|
noResults: defaultLabels.noResults,
|
|
1207
1274
|
someSelected: defaultLabels.someSelected
|
|
1208
1275
|
},
|
|
1209
|
-
light: false
|
|
1276
|
+
light: false,
|
|
1277
|
+
selectionType: "single"
|
|
1210
1278
|
};
|
|
1211
1279
|
const styles$3 = StyleSheet.create({
|
|
1212
1280
|
menuWrapper: {
|
|
@@ -1909,6 +1977,7 @@ class SingleSelect extends React.Component {
|
|
|
1909
1977
|
const opener = this.renderOpener(allChildren.length);
|
|
1910
1978
|
return React.createElement(DropdownCore$1, {
|
|
1911
1979
|
role: "listbox",
|
|
1980
|
+
selectionType: "single",
|
|
1912
1981
|
alignment: alignment,
|
|
1913
1982
|
dropdownStyle: [isFilterable && filterableDropdownStyle, selectDropdownStyle, dropdownStyle],
|
|
1914
1983
|
initialFocusedIndex: this.selectedIndex,
|
|
@@ -2227,6 +2296,7 @@ class MultiSelect extends React.Component {
|
|
|
2227
2296
|
open: open,
|
|
2228
2297
|
opener: opener,
|
|
2229
2298
|
openerElement: this.state.openerElement,
|
|
2299
|
+
selectionType: "multi",
|
|
2230
2300
|
style: style,
|
|
2231
2301
|
className: className,
|
|
2232
2302
|
onSearchTextChanged: isFilterable ? this.handleSearchTextChanged : null,
|
package/dist/index.js
CHANGED
|
@@ -82,7 +82,7 @@ module.exports =
|
|
|
82
82
|
/******/
|
|
83
83
|
/******/
|
|
84
84
|
/******/ // Load entry module and return exports
|
|
85
|
-
/******/ return __webpack_require__(__webpack_require__.s =
|
|
85
|
+
/******/ return __webpack_require__(__webpack_require__.s = 35);
|
|
86
86
|
/******/ })
|
|
87
87
|
/************************************************************************/
|
|
88
88
|
/******/ ([
|
|
@@ -258,7 +258,7 @@ module.exports = require("@khanacademy/wonder-blocks-clickable");
|
|
|
258
258
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
|
|
259
259
|
/* harmony import */ var aphrodite__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4);
|
|
260
260
|
/* harmony import */ var aphrodite__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(aphrodite__WEBPACK_IMPORTED_MODULE_1__);
|
|
261
|
-
/* harmony import */ var react_router_dom__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(
|
|
261
|
+
/* harmony import */ var react_router_dom__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(24);
|
|
262
262
|
/* harmony import */ var react_router_dom__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(react_router_dom__WEBPACK_IMPORTED_MODULE_2__);
|
|
263
263
|
/* harmony import */ var react_router__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(16);
|
|
264
264
|
/* harmony import */ var react_router__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(react_router__WEBPACK_IMPORTED_MODULE_3__);
|
|
@@ -437,8 +437,8 @@ const styles = aphrodite__WEBPACK_IMPORTED_MODULE_1__["StyleSheet"].create({
|
|
|
437
437
|
/* harmony import */ var _khanacademy_wonder_blocks_clickable__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(10);
|
|
438
438
|
/* harmony import */ var _khanacademy_wonder_blocks_clickable__WEBPACK_IMPORTED_MODULE_6___default = /*#__PURE__*/__webpack_require__.n(_khanacademy_wonder_blocks_clickable__WEBPACK_IMPORTED_MODULE_6__);
|
|
439
439
|
/* harmony import */ var _util_constants_js__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(2);
|
|
440
|
-
/* harmony import */ var _check_js__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(
|
|
441
|
-
/* harmony import */ var _checkbox_js__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(
|
|
440
|
+
/* harmony import */ var _check_js__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(25);
|
|
441
|
+
/* harmony import */ var _checkbox_js__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(26);
|
|
442
442
|
function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
|
|
443
443
|
|
|
444
444
|
|
|
@@ -671,17 +671,19 @@ DropdownOpener.defaultProps = {
|
|
|
671
671
|
/* harmony import */ var _khanacademy_wonder_blocks_spacing__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(_khanacademy_wonder_blocks_spacing__WEBPACK_IMPORTED_MODULE_5__);
|
|
672
672
|
/* harmony import */ var _khanacademy_wonder_blocks_core__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(5);
|
|
673
673
|
/* harmony import */ var _khanacademy_wonder_blocks_core__WEBPACK_IMPORTED_MODULE_6___default = /*#__PURE__*/__webpack_require__.n(_khanacademy_wonder_blocks_core__WEBPACK_IMPORTED_MODULE_6__);
|
|
674
|
-
/* harmony import */ var _khanacademy_wonder_blocks_search_field__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(
|
|
674
|
+
/* harmony import */ var _khanacademy_wonder_blocks_search_field__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(27);
|
|
675
675
|
/* harmony import */ var _khanacademy_wonder_blocks_search_field__WEBPACK_IMPORTED_MODULE_7___default = /*#__PURE__*/__webpack_require__.n(_khanacademy_wonder_blocks_search_field__WEBPACK_IMPORTED_MODULE_7__);
|
|
676
676
|
/* harmony import */ var _khanacademy_wonder_blocks_typography__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(9);
|
|
677
677
|
/* harmony import */ var _khanacademy_wonder_blocks_typography__WEBPACK_IMPORTED_MODULE_8___default = /*#__PURE__*/__webpack_require__.n(_khanacademy_wonder_blocks_typography__WEBPACK_IMPORTED_MODULE_8__);
|
|
678
678
|
/* harmony import */ var _khanacademy_wonder_blocks_timing__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(17);
|
|
679
679
|
/* harmony import */ var _khanacademy_wonder_blocks_timing__WEBPACK_IMPORTED_MODULE_9___default = /*#__PURE__*/__webpack_require__.n(_khanacademy_wonder_blocks_timing__WEBPACK_IMPORTED_MODULE_9__);
|
|
680
|
-
/* harmony import */ var _dropdown_core_virtualized_js__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(
|
|
680
|
+
/* harmony import */ var _dropdown_core_virtualized_js__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(28);
|
|
681
681
|
/* harmony import */ var _separator_item_js__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(6);
|
|
682
682
|
/* harmony import */ var _util_constants_js__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(2);
|
|
683
|
-
/* harmony import */ var _dropdown_popper_js__WEBPACK_IMPORTED_MODULE_13__ = __webpack_require__(
|
|
684
|
-
/* harmony import */ var
|
|
683
|
+
/* harmony import */ var _dropdown_popper_js__WEBPACK_IMPORTED_MODULE_13__ = __webpack_require__(30);
|
|
684
|
+
/* harmony import */ var _util_helpers_js__WEBPACK_IMPORTED_MODULE_14__ = __webpack_require__(20);
|
|
685
|
+
/* harmony import */ var _util_dropdown_menu_styles_js__WEBPACK_IMPORTED_MODULE_15__ = __webpack_require__(15);
|
|
686
|
+
/* eslint-disable max-lines */
|
|
685
687
|
// A menu that consists of action items
|
|
686
688
|
|
|
687
689
|
|
|
@@ -698,6 +700,7 @@ DropdownOpener.defaultProps = {
|
|
|
698
700
|
|
|
699
701
|
|
|
700
702
|
|
|
703
|
+
|
|
701
704
|
/**
|
|
702
705
|
* The number of options to apply the virtualized list to.
|
|
703
706
|
*
|
|
@@ -795,7 +798,15 @@ class DropdownCore extends react__WEBPACK_IMPORTED_MODULE_0__["Component"] {
|
|
|
795
798
|
open,
|
|
796
799
|
searchText
|
|
797
800
|
} = this.props;
|
|
798
|
-
const keyCode = event.which || event.keyCode; //
|
|
801
|
+
const keyCode = event.which || event.keyCode; // Listen for the keydown events if we are using ASCII characters.
|
|
802
|
+
|
|
803
|
+
if (Object(_util_helpers_js__WEBPACK_IMPORTED_MODULE_14__[/* getStringForKey */ "b"])(event.key)) {
|
|
804
|
+
event.stopPropagation();
|
|
805
|
+
this.textSuggestion += event.key; // Trigger the filter logic only after the debounce is resolved.
|
|
806
|
+
|
|
807
|
+
this.handleKeyDownDebounced(this.textSuggestion);
|
|
808
|
+
} // If menu isn't open and user presses down, open the menu
|
|
809
|
+
|
|
799
810
|
|
|
800
811
|
if (!open) {
|
|
801
812
|
if (keyCode === _util_constants_js__WEBPACK_IMPORTED_MODULE_12__[/* keyCodes */ "f"].down) {
|
|
@@ -877,6 +888,47 @@ class DropdownCore extends react__WEBPACK_IMPORTED_MODULE_0__["Component"] {
|
|
|
877
888
|
}
|
|
878
889
|
};
|
|
879
890
|
|
|
891
|
+
this.handleKeyDownDebounceResult = key => {
|
|
892
|
+
const foundIndex = this.props.items.filter(item => item.focusable).findIndex(({
|
|
893
|
+
component
|
|
894
|
+
}) => {
|
|
895
|
+
var _component$props;
|
|
896
|
+
|
|
897
|
+
if (_separator_item_js__WEBPACK_IMPORTED_MODULE_11__[/* default */ "a"].isClassOf(component)) {
|
|
898
|
+
return false;
|
|
899
|
+
} // Flow doesn't know that the component is an OptionItem
|
|
900
|
+
// $FlowIgnore[incompatible-use]
|
|
901
|
+
|
|
902
|
+
|
|
903
|
+
const label = (_component$props = component.props) == null ? void 0 : _component$props.label.toLowerCase();
|
|
904
|
+
return label.startsWith(key.toLowerCase());
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
if (foundIndex >= 0) {
|
|
908
|
+
const isClosed = !this.props.open;
|
|
909
|
+
|
|
910
|
+
if (isClosed) {
|
|
911
|
+
// Open the menu to be able to focus on the item that matches
|
|
912
|
+
// the text suggested.
|
|
913
|
+
this.props.onOpenChanged(true);
|
|
914
|
+
} // Update the focus reference.
|
|
915
|
+
|
|
916
|
+
|
|
917
|
+
this.focusedIndex = foundIndex;
|
|
918
|
+
this.scheduleToFocusCurrentItem(node => {
|
|
919
|
+
// Force click only if the dropdown is closed and we are using
|
|
920
|
+
// the SingleSelect component.
|
|
921
|
+
if (this.props.selectionType === "single" && isClosed && node) {
|
|
922
|
+
node.click();
|
|
923
|
+
this.props.onOpenChanged(false);
|
|
924
|
+
}
|
|
925
|
+
});
|
|
926
|
+
} // Otherwise, reset current text
|
|
927
|
+
|
|
928
|
+
|
|
929
|
+
this.textSuggestion = "";
|
|
930
|
+
};
|
|
931
|
+
|
|
880
932
|
this.handleClickFocus = index => {
|
|
881
933
|
// Turn itemsClicked on so pressing up or down would focus the
|
|
882
934
|
// appropriate item in handleKeyDown
|
|
@@ -927,7 +979,12 @@ class DropdownCore extends react__WEBPACK_IMPORTED_MODULE_0__["Component"] {
|
|
|
927
979
|
...props.labels
|
|
928
980
|
}
|
|
929
981
|
};
|
|
930
|
-
this.virtualizedListRef = /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["createRef"]();
|
|
982
|
+
this.virtualizedListRef = /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["createRef"](); // We debounce the keydown handler to get the ASCII chars because it's
|
|
983
|
+
// called on every keydown
|
|
984
|
+
|
|
985
|
+
this.handleKeyDownDebounced = Object(_util_helpers_js__WEBPACK_IMPORTED_MODULE_14__[/* debounce */ "a"])(this.handleKeyDownDebounceResult, // Leaving enough time for the user to type a valid query (e.g. jul)
|
|
986
|
+
500);
|
|
987
|
+
this.textSuggestion = "";
|
|
931
988
|
}
|
|
932
989
|
|
|
933
990
|
componentDidMount() {
|
|
@@ -1043,17 +1100,24 @@ class DropdownCore extends react__WEBPACK_IMPORTED_MODULE_0__["Component"] {
|
|
|
1043
1100
|
document.removeEventListener("touchend", this.handleInteract);
|
|
1044
1101
|
}
|
|
1045
1102
|
|
|
1046
|
-
scheduleToFocusCurrentItem() {
|
|
1103
|
+
scheduleToFocusCurrentItem(onFocus) {
|
|
1047
1104
|
if (this.shouldVirtualizeList()) {
|
|
1048
1105
|
// wait for windowed items to be recalculated
|
|
1049
|
-
this.props.schedule.animationFrame(() =>
|
|
1106
|
+
this.props.schedule.animationFrame(() => {
|
|
1107
|
+
this.focusCurrentItem(onFocus);
|
|
1108
|
+
});
|
|
1050
1109
|
} else {
|
|
1051
1110
|
// immediately focus the current item if we're not virtualizing
|
|
1052
|
-
this.focusCurrentItem();
|
|
1111
|
+
this.focusCurrentItem(onFocus);
|
|
1053
1112
|
}
|
|
1054
1113
|
}
|
|
1114
|
+
/**
|
|
1115
|
+
* Focus on the current item.
|
|
1116
|
+
* @param [onFocus] - Callback to be called when the item is focused.
|
|
1117
|
+
*/
|
|
1118
|
+
|
|
1055
1119
|
|
|
1056
|
-
focusCurrentItem() {
|
|
1120
|
+
focusCurrentItem(onFocus) {
|
|
1057
1121
|
const focusedItemRef = this.state.itemRefs[this.focusedIndex];
|
|
1058
1122
|
|
|
1059
1123
|
if (focusedItemRef) {
|
|
@@ -1073,6 +1137,11 @@ class DropdownCore extends react__WEBPACK_IMPORTED_MODULE_0__["Component"] {
|
|
|
1073
1137
|
// To be used if the set of focusable items in the menu changes
|
|
1074
1138
|
|
|
1075
1139
|
this.focusedOriginalIndex = focusedItemRef.originalIndex;
|
|
1140
|
+
|
|
1141
|
+
if (onFocus) {
|
|
1142
|
+
// Call the callback with the node that was focused.
|
|
1143
|
+
onFocus(node);
|
|
1144
|
+
}
|
|
1076
1145
|
}
|
|
1077
1146
|
}
|
|
1078
1147
|
}
|
|
@@ -1289,7 +1358,7 @@ class DropdownCore extends react__WEBPACK_IMPORTED_MODULE_0__["Component"] {
|
|
|
1289
1358
|
|
|
1290
1359
|
const openerStyle = openerElement && window.getComputedStyle(openerElement);
|
|
1291
1360
|
const minDropdownWidth = openerStyle ? openerStyle.getPropertyValue("width") : 0;
|
|
1292
|
-
const maxDropdownHeight = Object(
|
|
1361
|
+
const maxDropdownHeight = Object(_util_dropdown_menu_styles_js__WEBPACK_IMPORTED_MODULE_15__[/* getDropdownMenuHeight */ "b"])(this.props.items);
|
|
1293
1362
|
return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["createElement"](_khanacademy_wonder_blocks_core__WEBPACK_IMPORTED_MODULE_6__["View"] // Stop propagation to prevent the mouseup listener on the
|
|
1294
1363
|
// document from closing the menu.
|
|
1295
1364
|
, {
|
|
@@ -1298,7 +1367,7 @@ class DropdownCore extends react__WEBPACK_IMPORTED_MODULE_0__["Component"] {
|
|
|
1298
1367
|
testId: "dropdown-core-container"
|
|
1299
1368
|
}, this.props.isFilterable && this.renderSearchField(), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["createElement"](_khanacademy_wonder_blocks_core__WEBPACK_IMPORTED_MODULE_6__["View"], {
|
|
1300
1369
|
role: this.props.role,
|
|
1301
|
-
style: [styles.listboxOrMenu, Object(
|
|
1370
|
+
style: [styles.listboxOrMenu, Object(_util_dropdown_menu_styles_js__WEBPACK_IMPORTED_MODULE_15__[/* generateDropdownMenuStyles */ "a"])(minDropdownWidth, maxDropdownHeight)]
|
|
1302
1371
|
}, listRenderer), this.maybeRenderNoResults());
|
|
1303
1372
|
}
|
|
1304
1373
|
|
|
@@ -1365,7 +1434,8 @@ DropdownCore.defaultProps = {
|
|
|
1365
1434
|
noResults: _util_constants_js__WEBPACK_IMPORTED_MODULE_12__[/* defaultLabels */ "d"].noResults,
|
|
1366
1435
|
someSelected: _util_constants_js__WEBPACK_IMPORTED_MODULE_12__[/* defaultLabels */ "d"].someSelected
|
|
1367
1436
|
},
|
|
1368
|
-
light: false
|
|
1437
|
+
light: false,
|
|
1438
|
+
selectionType: "single"
|
|
1369
1439
|
};
|
|
1370
1440
|
const styles = aphrodite__WEBPACK_IMPORTED_MODULE_2__["StyleSheet"].create({
|
|
1371
1441
|
menuWrapper: {
|
|
@@ -1729,6 +1799,52 @@ module.exports = require("react-window");
|
|
|
1729
1799
|
/* 20 */
|
|
1730
1800
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
1731
1801
|
|
|
1802
|
+
"use strict";
|
|
1803
|
+
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "b", function() { return getStringForKey; });
|
|
1804
|
+
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return debounce; });
|
|
1805
|
+
/**
|
|
1806
|
+
* Checks if a given key is a valid ASCII value.
|
|
1807
|
+
*
|
|
1808
|
+
* @param {string} key The key that is being typed in.
|
|
1809
|
+
* @returns A valid string representation of the given key.
|
|
1810
|
+
*/
|
|
1811
|
+
function getStringForKey(key) {
|
|
1812
|
+
// If the key is of length 1, it is an ASCII value.
|
|
1813
|
+
// Otherwise, if there are no ASCII characters in the key name,
|
|
1814
|
+
// it is a Unicode character.
|
|
1815
|
+
// See https://www.w3.org/TR/uievents-key/
|
|
1816
|
+
if (key.length === 1 || !/^[A-Z]/i.test(key)) {
|
|
1817
|
+
return key;
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
return "";
|
|
1821
|
+
}
|
|
1822
|
+
/**
|
|
1823
|
+
*
|
|
1824
|
+
* @param {fn} callback The function that will be executed after the debounce is resolved.
|
|
1825
|
+
* @param {number} wait The period of time that will be executed the debounced
|
|
1826
|
+
* function.
|
|
1827
|
+
* @returns The function that will be executed after the wait period is
|
|
1828
|
+
* fulfilled.
|
|
1829
|
+
*/
|
|
1830
|
+
|
|
1831
|
+
function debounce(callback, wait) {
|
|
1832
|
+
let timeout;
|
|
1833
|
+
return function executedFunction(...args) {
|
|
1834
|
+
const later = () => {
|
|
1835
|
+
clearTimeout(timeout);
|
|
1836
|
+
callback.apply(void 0, args);
|
|
1837
|
+
};
|
|
1838
|
+
|
|
1839
|
+
clearTimeout(timeout);
|
|
1840
|
+
timeout = setTimeout(later, wait);
|
|
1841
|
+
};
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
/***/ }),
|
|
1845
|
+
/* 21 */
|
|
1846
|
+
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
1847
|
+
|
|
1732
1848
|
"use strict";
|
|
1733
1849
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return ActionMenu; });
|
|
1734
1850
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(0);
|
|
@@ -1741,7 +1857,7 @@ module.exports = require("react-window");
|
|
|
1741
1857
|
/* harmony import */ var _action_item_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(11);
|
|
1742
1858
|
/* harmony import */ var _option_item_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(12);
|
|
1743
1859
|
/* harmony import */ var _dropdown_core_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(14);
|
|
1744
|
-
/* harmony import */ var _action_menu_opener_core_js__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(
|
|
1860
|
+
/* harmony import */ var _action_menu_opener_core_js__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(33);
|
|
1745
1861
|
function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
|
|
1746
1862
|
|
|
1747
1863
|
|
|
@@ -1951,7 +2067,7 @@ const styles = aphrodite__WEBPACK_IMPORTED_MODULE_2__["StyleSheet"].create({
|
|
|
1951
2067
|
});
|
|
1952
2068
|
|
|
1953
2069
|
/***/ }),
|
|
1954
|
-
/*
|
|
2070
|
+
/* 22 */
|
|
1955
2071
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
1956
2072
|
|
|
1957
2073
|
"use strict";
|
|
@@ -2191,6 +2307,7 @@ class SingleSelect extends react__WEBPACK_IMPORTED_MODULE_0__["Component"] {
|
|
|
2191
2307
|
const opener = this.renderOpener(allChildren.length);
|
|
2192
2308
|
return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["createElement"](_dropdown_core_js__WEBPACK_IMPORTED_MODULE_2__[/* default */ "a"], {
|
|
2193
2309
|
role: "listbox",
|
|
2310
|
+
selectionType: "single",
|
|
2194
2311
|
alignment: alignment,
|
|
2195
2312
|
dropdownStyle: [isFilterable && _util_constants_js__WEBPACK_IMPORTED_MODULE_5__[/* filterableDropdownStyle */ "e"], _util_constants_js__WEBPACK_IMPORTED_MODULE_5__[/* selectDropdownStyle */ "g"], dropdownStyle],
|
|
2196
2313
|
initialFocusedIndex: this.selectedIndex,
|
|
@@ -2216,7 +2333,7 @@ SingleSelect.defaultProps = {
|
|
|
2216
2333
|
};
|
|
2217
2334
|
|
|
2218
2335
|
/***/ }),
|
|
2219
|
-
/*
|
|
2336
|
+
/* 23 */
|
|
2220
2337
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
2221
2338
|
|
|
2222
2339
|
"use strict";
|
|
@@ -2593,6 +2710,7 @@ class MultiSelect extends react__WEBPACK_IMPORTED_MODULE_0__["Component"] {
|
|
|
2593
2710
|
open: open,
|
|
2594
2711
|
opener: opener,
|
|
2595
2712
|
openerElement: this.state.openerElement,
|
|
2713
|
+
selectionType: "multi",
|
|
2596
2714
|
style: style,
|
|
2597
2715
|
className: className,
|
|
2598
2716
|
onSearchTextChanged: isFilterable ? this.handleSearchTextChanged : null,
|
|
@@ -2616,13 +2734,13 @@ MultiSelect.defaultProps = {
|
|
|
2616
2734
|
};
|
|
2617
2735
|
|
|
2618
2736
|
/***/ }),
|
|
2619
|
-
/*
|
|
2737
|
+
/* 24 */
|
|
2620
2738
|
/***/ (function(module, exports) {
|
|
2621
2739
|
|
|
2622
2740
|
module.exports = require("react-router-dom");
|
|
2623
2741
|
|
|
2624
2742
|
/***/ }),
|
|
2625
|
-
/*
|
|
2743
|
+
/* 25 */
|
|
2626
2744
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
2627
2745
|
|
|
2628
2746
|
"use strict";
|
|
@@ -2679,7 +2797,7 @@ const styles = aphrodite__WEBPACK_IMPORTED_MODULE_1__["StyleSheet"].create({
|
|
|
2679
2797
|
});
|
|
2680
2798
|
|
|
2681
2799
|
/***/ }),
|
|
2682
|
-
/*
|
|
2800
|
+
/* 26 */
|
|
2683
2801
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
2684
2802
|
|
|
2685
2803
|
"use strict";
|
|
@@ -2772,13 +2890,13 @@ const styles = aphrodite__WEBPACK_IMPORTED_MODULE_1__["StyleSheet"].create({
|
|
|
2772
2890
|
});
|
|
2773
2891
|
|
|
2774
2892
|
/***/ }),
|
|
2775
|
-
/*
|
|
2893
|
+
/* 27 */
|
|
2776
2894
|
/***/ (function(module, exports) {
|
|
2777
2895
|
|
|
2778
2896
|
module.exports = require("@khanacademy/wonder-blocks-search-field");
|
|
2779
2897
|
|
|
2780
2898
|
/***/ }),
|
|
2781
|
-
/*
|
|
2899
|
+
/* 28 */
|
|
2782
2900
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
2783
2901
|
|
|
2784
2902
|
"use strict";
|
|
@@ -2790,7 +2908,7 @@ module.exports = require("@khanacademy/wonder-blocks-search-field");
|
|
|
2790
2908
|
/* harmony import */ var react_window__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(react_window__WEBPACK_IMPORTED_MODULE_2__);
|
|
2791
2909
|
/* harmony import */ var _khanacademy_wonder_blocks_timing__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(17);
|
|
2792
2910
|
/* harmony import */ var _khanacademy_wonder_blocks_timing__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(_khanacademy_wonder_blocks_timing__WEBPACK_IMPORTED_MODULE_3__);
|
|
2793
|
-
/* harmony import */ var _dropdown_core_virtualized_item_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(
|
|
2911
|
+
/* harmony import */ var _dropdown_core_virtualized_item_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(29);
|
|
2794
2912
|
/* harmony import */ var _separator_item_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(6);
|
|
2795
2913
|
/* harmony import */ var _util_constants_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(2);
|
|
2796
2914
|
/* harmony import */ var _util_dropdown_menu_styles_js__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(15);
|
|
@@ -2971,7 +3089,7 @@ class DropdownCoreVirtualized extends react__WEBPACK_IMPORTED_MODULE_0__["Compon
|
|
|
2971
3089
|
/* harmony default export */ __webpack_exports__["a"] = (Object(_khanacademy_wonder_blocks_timing__WEBPACK_IMPORTED_MODULE_3__["withActionScheduler"])(DropdownCoreVirtualized));
|
|
2972
3090
|
|
|
2973
3091
|
/***/ }),
|
|
2974
|
-
/*
|
|
3092
|
+
/* 29 */
|
|
2975
3093
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
2976
3094
|
|
|
2977
3095
|
"use strict";
|
|
@@ -3025,7 +3143,7 @@ class DropdownVirtualizedItem extends react__WEBPACK_IMPORTED_MODULE_0__["Compon
|
|
|
3025
3143
|
/* harmony default export */ __webpack_exports__["a"] = (DropdownVirtualizedItem);
|
|
3026
3144
|
|
|
3027
3145
|
/***/ }),
|
|
3028
|
-
/*
|
|
3146
|
+
/* 30 */
|
|
3029
3147
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
3030
3148
|
|
|
3031
3149
|
"use strict";
|
|
@@ -3034,9 +3152,9 @@ class DropdownVirtualizedItem extends react__WEBPACK_IMPORTED_MODULE_0__["Compon
|
|
|
3034
3152
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
|
|
3035
3153
|
/* harmony import */ var react_dom__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(7);
|
|
3036
3154
|
/* harmony import */ var react_dom__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react_dom__WEBPACK_IMPORTED_MODULE_1__);
|
|
3037
|
-
/* harmony import */ var react_popper__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(
|
|
3155
|
+
/* harmony import */ var react_popper__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(31);
|
|
3038
3156
|
/* harmony import */ var react_popper__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(react_popper__WEBPACK_IMPORTED_MODULE_2__);
|
|
3039
|
-
/* harmony import */ var _khanacademy_wonder_blocks_modal__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(
|
|
3157
|
+
/* harmony import */ var _khanacademy_wonder_blocks_modal__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(32);
|
|
3040
3158
|
/* harmony import */ var _khanacademy_wonder_blocks_modal__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(_khanacademy_wonder_blocks_modal__WEBPACK_IMPORTED_MODULE_3__);
|
|
3041
3159
|
|
|
3042
3160
|
|
|
@@ -3101,19 +3219,19 @@ function DropdownPopper({
|
|
|
3101
3219
|
}
|
|
3102
3220
|
|
|
3103
3221
|
/***/ }),
|
|
3104
|
-
/*
|
|
3222
|
+
/* 31 */
|
|
3105
3223
|
/***/ (function(module, exports) {
|
|
3106
3224
|
|
|
3107
3225
|
module.exports = require("react-popper");
|
|
3108
3226
|
|
|
3109
3227
|
/***/ }),
|
|
3110
|
-
/*
|
|
3228
|
+
/* 32 */
|
|
3111
3229
|
/***/ (function(module, exports) {
|
|
3112
3230
|
|
|
3113
3231
|
module.exports = require("@khanacademy/wonder-blocks-modal");
|
|
3114
3232
|
|
|
3115
3233
|
/***/ }),
|
|
3116
|
-
/*
|
|
3234
|
+
/* 33 */
|
|
3117
3235
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
3118
3236
|
|
|
3119
3237
|
"use strict";
|
|
@@ -3132,7 +3250,7 @@ module.exports = require("@khanacademy/wonder-blocks-modal");
|
|
|
3132
3250
|
/* harmony import */ var _khanacademy_wonder_blocks_icon__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(_khanacademy_wonder_blocks_icon__WEBPACK_IMPORTED_MODULE_5__);
|
|
3133
3251
|
/* harmony import */ var _khanacademy_wonder_blocks_spacing__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(3);
|
|
3134
3252
|
/* harmony import */ var _khanacademy_wonder_blocks_spacing__WEBPACK_IMPORTED_MODULE_6___default = /*#__PURE__*/__webpack_require__.n(_khanacademy_wonder_blocks_spacing__WEBPACK_IMPORTED_MODULE_6__);
|
|
3135
|
-
/* harmony import */ var _khanacademy_wonder_blocks_layout__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(
|
|
3253
|
+
/* harmony import */ var _khanacademy_wonder_blocks_layout__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(34);
|
|
3136
3254
|
/* harmony import */ var _khanacademy_wonder_blocks_layout__WEBPACK_IMPORTED_MODULE_7___default = /*#__PURE__*/__webpack_require__.n(_khanacademy_wonder_blocks_layout__WEBPACK_IMPORTED_MODULE_7__);
|
|
3137
3255
|
/* harmony import */ var _util_constants_js__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(2);
|
|
3138
3256
|
function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
|
|
@@ -3288,13 +3406,13 @@ const _generateStyles = color => {
|
|
|
3288
3406
|
};
|
|
3289
3407
|
|
|
3290
3408
|
/***/ }),
|
|
3291
|
-
/*
|
|
3409
|
+
/* 34 */
|
|
3292
3410
|
/***/ (function(module, exports) {
|
|
3293
3411
|
|
|
3294
3412
|
module.exports = require("@khanacademy/wonder-blocks-layout");
|
|
3295
3413
|
|
|
3296
3414
|
/***/ }),
|
|
3297
|
-
/*
|
|
3415
|
+
/* 35 */
|
|
3298
3416
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
3299
3417
|
|
|
3300
3418
|
"use strict";
|
|
@@ -3308,13 +3426,13 @@ __webpack_require__.r(__webpack_exports__);
|
|
|
3308
3426
|
/* harmony import */ var _components_separator_item_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(6);
|
|
3309
3427
|
/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "SeparatorItem", function() { return _components_separator_item_js__WEBPACK_IMPORTED_MODULE_2__["a"]; });
|
|
3310
3428
|
|
|
3311
|
-
/* harmony import */ var _components_action_menu_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(
|
|
3429
|
+
/* harmony import */ var _components_action_menu_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(21);
|
|
3312
3430
|
/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "ActionMenu", function() { return _components_action_menu_js__WEBPACK_IMPORTED_MODULE_3__["a"]; });
|
|
3313
3431
|
|
|
3314
|
-
/* harmony import */ var _components_single_select_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(
|
|
3432
|
+
/* harmony import */ var _components_single_select_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(22);
|
|
3315
3433
|
/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "SingleSelect", function() { return _components_single_select_js__WEBPACK_IMPORTED_MODULE_4__["a"]; });
|
|
3316
3434
|
|
|
3317
|
-
/* harmony import */ var _components_multi_select_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(
|
|
3435
|
+
/* harmony import */ var _components_multi_select_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(23);
|
|
3318
3436
|
/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "MultiSelect", function() { return _components_multi_select_js__WEBPACK_IMPORTED_MODULE_5__["a"]; });
|
|
3319
3437
|
|
|
3320
3438
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@khanacademy/wonder-blocks-dropdown",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.0",
|
|
4
4
|
"design": "v1",
|
|
5
5
|
"description": "Dropdown variants for Wonder Blocks.",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -15,16 +15,16 @@
|
|
|
15
15
|
"access": "public"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@babel/runtime": "^7.
|
|
19
|
-
"@khanacademy/wonder-blocks-button": "^3.0.
|
|
20
|
-
"@khanacademy/wonder-blocks-clickable": "^2.
|
|
18
|
+
"@babel/runtime": "^7.18.6",
|
|
19
|
+
"@khanacademy/wonder-blocks-button": "^3.0.1",
|
|
20
|
+
"@khanacademy/wonder-blocks-clickable": "^2.3.0",
|
|
21
21
|
"@khanacademy/wonder-blocks-color": "^1.1.20",
|
|
22
22
|
"@khanacademy/wonder-blocks-core": "^4.3.2",
|
|
23
23
|
"@khanacademy/wonder-blocks-icon": "^1.2.29",
|
|
24
|
-
"@khanacademy/wonder-blocks-icon-button": "^3.4.
|
|
24
|
+
"@khanacademy/wonder-blocks-icon-button": "^3.4.9",
|
|
25
25
|
"@khanacademy/wonder-blocks-layout": "^1.4.10",
|
|
26
|
-
"@khanacademy/wonder-blocks-modal": "^2.3.
|
|
27
|
-
"@khanacademy/wonder-blocks-search-field": "^1.0.
|
|
26
|
+
"@khanacademy/wonder-blocks-modal": "^2.3.4",
|
|
27
|
+
"@khanacademy/wonder-blocks-search-field": "^1.0.7",
|
|
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"
|
|
@@ -541,7 +541,7 @@ describe("DropdownCore", () => {
|
|
|
541
541
|
expect(screen.getByText("No results")).toBeInTheDocument();
|
|
542
542
|
});
|
|
543
543
|
|
|
544
|
-
it("SearchField should be focused when opened and there's no selection",
|
|
544
|
+
it("SearchField should be focused when opened and there's no selection", () => {
|
|
545
545
|
// Arrange
|
|
546
546
|
|
|
547
547
|
// Act
|
|
@@ -562,9 +562,7 @@ describe("DropdownCore", () => {
|
|
|
562
562
|
);
|
|
563
563
|
|
|
564
564
|
// Assert
|
|
565
|
-
|
|
566
|
-
expect(screen.getByRole("textbox")).toHaveFocus();
|
|
567
|
-
});
|
|
565
|
+
expect(screen.getByRole("textbox")).toHaveFocus();
|
|
568
566
|
});
|
|
569
567
|
|
|
570
568
|
it("SearchField should trigger change when the user types in", () => {
|
|
@@ -739,7 +737,7 @@ describe("DropdownCore", () => {
|
|
|
739
737
|
});
|
|
740
738
|
|
|
741
739
|
describe("a11y > Live region", () => {
|
|
742
|
-
it("should render a live region announcing the number of options",
|
|
740
|
+
it("should render a live region announcing the number of options", () => {
|
|
743
741
|
// Arrange
|
|
744
742
|
|
|
745
743
|
// Act
|
|
@@ -19,6 +19,19 @@ const labels: $Shape<Labels> = {
|
|
|
19
19
|
};
|
|
20
20
|
|
|
21
21
|
describe("MultiSelect", () => {
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
window.scrollTo = jest.fn();
|
|
24
|
+
|
|
25
|
+
// We mock console.error() because React logs a bunch of errors pertaining
|
|
26
|
+
// to the use href="javascript:void(0);".
|
|
27
|
+
jest.spyOn(console, "error").mockImplementation(() => {});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
window.scrollTo.mockClear();
|
|
32
|
+
jest.spyOn(console, "error").mockReset();
|
|
33
|
+
});
|
|
34
|
+
|
|
22
35
|
describe("uncontrolled", () => {
|
|
23
36
|
const onChange = jest.fn();
|
|
24
37
|
const uncontrolledSingleSelect = (
|
|
@@ -1186,6 +1199,41 @@ describe("MultiSelect", () => {
|
|
|
1186
1199
|
screen.getByText(updatedLabels.selectNoneLabel),
|
|
1187
1200
|
).toBeInTheDocument();
|
|
1188
1201
|
});
|
|
1202
|
+
|
|
1203
|
+
describe("keyboard", () => {
|
|
1204
|
+
beforeEach(() => {
|
|
1205
|
+
// Required due to the `debounce` call.
|
|
1206
|
+
jest.useFakeTimers();
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1209
|
+
it("should find and focus an item using the keyboard", () => {
|
|
1210
|
+
// Arrange
|
|
1211
|
+
const onChangeMock = jest.fn();
|
|
1212
|
+
|
|
1213
|
+
render(
|
|
1214
|
+
<MultiSelect onChange={onChangeMock}>
|
|
1215
|
+
<OptionItem label="Mercury" value="mercury" />
|
|
1216
|
+
<OptionItem label="Venus" value="venus" />
|
|
1217
|
+
<OptionItem label="Mars" value="mars" />
|
|
1218
|
+
</MultiSelect>,
|
|
1219
|
+
);
|
|
1220
|
+
userEvent.tab();
|
|
1221
|
+
|
|
1222
|
+
// Act
|
|
1223
|
+
// find first occurrence
|
|
1224
|
+
userEvent.keyboard("v");
|
|
1225
|
+
jest.advanceTimersByTime(501);
|
|
1226
|
+
|
|
1227
|
+
// Assert
|
|
1228
|
+
const filteredOption = screen.getByRole("option", {
|
|
1229
|
+
name: /Venus/i,
|
|
1230
|
+
});
|
|
1231
|
+
// Verify that the element found is focused.
|
|
1232
|
+
expect(filteredOption).toHaveFocus();
|
|
1233
|
+
// And also verify that the listbox is opened.
|
|
1234
|
+
expect(screen.getByRole("listbox")).toBeInTheDocument();
|
|
1235
|
+
});
|
|
1236
|
+
});
|
|
1189
1237
|
});
|
|
1190
1238
|
|
|
1191
1239
|
describe("a11y > Live region", () => {
|
|
@@ -99,6 +99,10 @@ describe("SingleSelect", () => {
|
|
|
99
99
|
});
|
|
100
100
|
|
|
101
101
|
describe("keyboard", () => {
|
|
102
|
+
beforeEach(() => {
|
|
103
|
+
jest.useFakeTimers();
|
|
104
|
+
});
|
|
105
|
+
|
|
102
106
|
describe.each([{key: "{enter}"}, {key: "{space}"}])(
|
|
103
107
|
"$key",
|
|
104
108
|
({key}) => {
|
|
@@ -129,7 +133,7 @@ describe("SingleSelect", () => {
|
|
|
129
133
|
},
|
|
130
134
|
);
|
|
131
135
|
|
|
132
|
-
it("should
|
|
136
|
+
it("should select an item when pressing {enter}", () => {
|
|
133
137
|
// Arrange
|
|
134
138
|
render(uncontrolledSingleSelect);
|
|
135
139
|
userEvent.tab();
|
|
@@ -139,8 +143,8 @@ describe("SingleSelect", () => {
|
|
|
139
143
|
userEvent.keyboard("{enter}");
|
|
140
144
|
|
|
141
145
|
// Assert
|
|
142
|
-
expect(onChange).
|
|
143
|
-
expect(screen.
|
|
146
|
+
expect(onChange).toHaveBeenCalledWith("1");
|
|
147
|
+
expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
|
|
144
148
|
});
|
|
145
149
|
|
|
146
150
|
it("should select an item when pressing {space}", () => {
|
|
@@ -157,6 +161,27 @@ describe("SingleSelect", () => {
|
|
|
157
161
|
expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
|
|
158
162
|
});
|
|
159
163
|
|
|
164
|
+
it("should find and select an item using the keyboard", () => {
|
|
165
|
+
// Arrange
|
|
166
|
+
render(
|
|
167
|
+
<SingleSelect onChange={onChange} placeholder="Choose">
|
|
168
|
+
<OptionItem label="apple" value="apple" />
|
|
169
|
+
<OptionItem label="orange" value="orange" />
|
|
170
|
+
<OptionItem label="pear" value="pear" />
|
|
171
|
+
</SingleSelect>,
|
|
172
|
+
);
|
|
173
|
+
userEvent.tab();
|
|
174
|
+
|
|
175
|
+
// Act
|
|
176
|
+
// find first occurrence
|
|
177
|
+
userEvent.keyboard("or");
|
|
178
|
+
jest.advanceTimersByTime(501);
|
|
179
|
+
|
|
180
|
+
// Assert
|
|
181
|
+
expect(onChange).toHaveBeenCalledWith("orange");
|
|
182
|
+
expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
|
|
183
|
+
});
|
|
184
|
+
|
|
160
185
|
it("should dismiss the dropdown when pressing {escape}", () => {
|
|
161
186
|
// Arrange
|
|
162
187
|
render(uncontrolledSingleSelect);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable max-lines */
|
|
1
2
|
// @flow
|
|
2
3
|
// A menu that consists of action items
|
|
3
4
|
|
|
@@ -25,6 +26,7 @@ import SeparatorItem from "./separator-item.js";
|
|
|
25
26
|
import {defaultLabels, keyCodes} from "../util/constants.js";
|
|
26
27
|
import type {DropdownItem} from "../util/types.js";
|
|
27
28
|
import DropdownPopper from "./dropdown-popper.js";
|
|
29
|
+
import {debounce, getStringForKey} from "../util/helpers.js";
|
|
28
30
|
import {
|
|
29
31
|
generateDropdownMenuStyles,
|
|
30
32
|
getDropdownMenuHeight,
|
|
@@ -94,6 +96,11 @@ type DefaultProps = {|
|
|
|
94
96
|
* use when the item is used on a dark background.
|
|
95
97
|
*/
|
|
96
98
|
light: boolean,
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Used to determine if we can automatically select an item using the keyboard.
|
|
102
|
+
*/
|
|
103
|
+
selectionType: "single" | "multi",
|
|
97
104
|
|};
|
|
98
105
|
|
|
99
106
|
type DropdownAriaRole = "listbox" | "menu";
|
|
@@ -219,6 +226,10 @@ class DropdownCore extends React.Component<Props, State> {
|
|
|
219
226
|
current: null | React.ElementRef<typeof List>,
|
|
220
227
|
|};
|
|
221
228
|
|
|
229
|
+
handleKeyDownDebounced: (key: string) => void;
|
|
230
|
+
|
|
231
|
+
textSuggestion: string;
|
|
232
|
+
|
|
222
233
|
// Figure out if the same items are focusable. If an item has been added or
|
|
223
234
|
// removed, this method will return false.
|
|
224
235
|
static sameItemsFocusable(
|
|
@@ -245,6 +256,7 @@ class DropdownCore extends React.Component<Props, State> {
|
|
|
245
256
|
someSelected: defaultLabels.someSelected,
|
|
246
257
|
},
|
|
247
258
|
light: false,
|
|
259
|
+
selectionType: "single",
|
|
248
260
|
};
|
|
249
261
|
|
|
250
262
|
// This is here to avoid calling React.createRef on each rerender. Instead,
|
|
@@ -295,6 +307,15 @@ class DropdownCore extends React.Component<Props, State> {
|
|
|
295
307
|
};
|
|
296
308
|
|
|
297
309
|
this.virtualizedListRef = React.createRef();
|
|
310
|
+
|
|
311
|
+
// We debounce the keydown handler to get the ASCII chars because it's
|
|
312
|
+
// called on every keydown
|
|
313
|
+
this.handleKeyDownDebounced = debounce(
|
|
314
|
+
this.handleKeyDownDebounceResult,
|
|
315
|
+
// Leaving enough time for the user to type a valid query (e.g. jul)
|
|
316
|
+
500,
|
|
317
|
+
);
|
|
318
|
+
this.textSuggestion = "";
|
|
298
319
|
}
|
|
299
320
|
|
|
300
321
|
componentDidMount() {
|
|
@@ -419,17 +440,23 @@ class DropdownCore extends React.Component<Props, State> {
|
|
|
419
440
|
}
|
|
420
441
|
};
|
|
421
442
|
|
|
422
|
-
scheduleToFocusCurrentItem() {
|
|
443
|
+
scheduleToFocusCurrentItem(onFocus?: (node: void | HTMLElement) => void) {
|
|
423
444
|
if (this.shouldVirtualizeList()) {
|
|
424
445
|
// wait for windowed items to be recalculated
|
|
425
|
-
this.props.schedule.animationFrame(() =>
|
|
446
|
+
this.props.schedule.animationFrame(() => {
|
|
447
|
+
this.focusCurrentItem(onFocus);
|
|
448
|
+
});
|
|
426
449
|
} else {
|
|
427
450
|
// immediately focus the current item if we're not virtualizing
|
|
428
|
-
this.focusCurrentItem();
|
|
451
|
+
this.focusCurrentItem(onFocus);
|
|
429
452
|
}
|
|
430
453
|
}
|
|
431
454
|
|
|
432
|
-
|
|
455
|
+
/**
|
|
456
|
+
* Focus on the current item.
|
|
457
|
+
* @param [onFocus] - Callback to be called when the item is focused.
|
|
458
|
+
*/
|
|
459
|
+
focusCurrentItem(onFocus?: (node: HTMLElement) => void) {
|
|
433
460
|
const focusedItemRef = this.state.itemRefs[this.focusedIndex];
|
|
434
461
|
|
|
435
462
|
if (focusedItemRef) {
|
|
@@ -452,6 +479,11 @@ class DropdownCore extends React.Component<Props, State> {
|
|
|
452
479
|
// Keep track of the original index of the newly focused item.
|
|
453
480
|
// To be used if the set of focusable items in the menu changes
|
|
454
481
|
this.focusedOriginalIndex = focusedItemRef.originalIndex;
|
|
482
|
+
|
|
483
|
+
if (onFocus) {
|
|
484
|
+
// Call the callback with the node that was focused.
|
|
485
|
+
onFocus(node);
|
|
486
|
+
}
|
|
455
487
|
}
|
|
456
488
|
}
|
|
457
489
|
}
|
|
@@ -514,6 +546,15 @@ class DropdownCore extends React.Component<Props, State> {
|
|
|
514
546
|
handleKeyDown: (event: SyntheticKeyboardEvent<>) => void = (event) => {
|
|
515
547
|
const {onOpenChanged, open, searchText} = this.props;
|
|
516
548
|
const keyCode = event.which || event.keyCode;
|
|
549
|
+
|
|
550
|
+
// Listen for the keydown events if we are using ASCII characters.
|
|
551
|
+
if (getStringForKey(event.key)) {
|
|
552
|
+
event.stopPropagation();
|
|
553
|
+
this.textSuggestion += event.key;
|
|
554
|
+
// Trigger the filter logic only after the debounce is resolved.
|
|
555
|
+
this.handleKeyDownDebounced(this.textSuggestion);
|
|
556
|
+
}
|
|
557
|
+
|
|
517
558
|
// If menu isn't open and user presses down, open the menu
|
|
518
559
|
if (!open) {
|
|
519
560
|
if (keyCode === keyCodes.down) {
|
|
@@ -583,6 +624,45 @@ class DropdownCore extends React.Component<Props, State> {
|
|
|
583
624
|
}
|
|
584
625
|
};
|
|
585
626
|
|
|
627
|
+
handleKeyDownDebounceResult: (key: string) => void = (key) => {
|
|
628
|
+
const foundIndex = this.props.items
|
|
629
|
+
.filter((item) => item.focusable)
|
|
630
|
+
.findIndex(({component}) => {
|
|
631
|
+
if (SeparatorItem.isClassOf(component)) {
|
|
632
|
+
return false;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Flow doesn't know that the component is an OptionItem
|
|
636
|
+
// $FlowIgnore[incompatible-use]
|
|
637
|
+
const label = component.props?.label.toLowerCase();
|
|
638
|
+
|
|
639
|
+
return label.startsWith(key.toLowerCase());
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
if (foundIndex >= 0) {
|
|
643
|
+
const isClosed = !this.props.open;
|
|
644
|
+
if (isClosed) {
|
|
645
|
+
// Open the menu to be able to focus on the item that matches
|
|
646
|
+
// the text suggested.
|
|
647
|
+
this.props.onOpenChanged(true);
|
|
648
|
+
}
|
|
649
|
+
// Update the focus reference.
|
|
650
|
+
this.focusedIndex = foundIndex;
|
|
651
|
+
|
|
652
|
+
this.scheduleToFocusCurrentItem((node) => {
|
|
653
|
+
// Force click only if the dropdown is closed and we are using
|
|
654
|
+
// the SingleSelect component.
|
|
655
|
+
if (this.props.selectionType === "single" && isClosed && node) {
|
|
656
|
+
node.click();
|
|
657
|
+
this.props.onOpenChanged(false);
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// Otherwise, reset current text
|
|
663
|
+
this.textSuggestion = "";
|
|
664
|
+
};
|
|
665
|
+
|
|
586
666
|
handleClickFocus: (index: number) => void = (index) => {
|
|
587
667
|
// Turn itemsClicked on so pressing up or down would focus the
|
|
588
668
|
// appropriate item in handleKeyDown
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import {debounce, getStringForKey} from "../helpers.js";
|
|
3
|
+
|
|
4
|
+
describe("getStringForKey", () => {
|
|
5
|
+
it("should get a valid string", () => {
|
|
6
|
+
// Arrange
|
|
7
|
+
|
|
8
|
+
// Act
|
|
9
|
+
const key = getStringForKey("a");
|
|
10
|
+
|
|
11
|
+
// Assert
|
|
12
|
+
expect(key).toBe("a");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("should return empty if we use a glyph modifier key (e.g. Shift)", () => {
|
|
16
|
+
// Arrange
|
|
17
|
+
|
|
18
|
+
// Act
|
|
19
|
+
const key = getStringForKey("Shift");
|
|
20
|
+
|
|
21
|
+
// Assert
|
|
22
|
+
expect(key).toBe("");
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("debounce", () => {
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
jest.useFakeTimers();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("should call the debounced function", () => {
|
|
32
|
+
// Arrange
|
|
33
|
+
const callbackFnMock = jest.fn();
|
|
34
|
+
const debounced = debounce(callbackFnMock, 500);
|
|
35
|
+
|
|
36
|
+
// Act
|
|
37
|
+
debounced();
|
|
38
|
+
jest.advanceTimersByTime(501);
|
|
39
|
+
|
|
40
|
+
// Assert
|
|
41
|
+
expect(callbackFnMock).toHaveBeenCalled();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("should call the debounced function only once", () => {
|
|
45
|
+
// Arrange
|
|
46
|
+
const callbackFnMock = jest.fn();
|
|
47
|
+
const debounced = debounce(callbackFnMock, 500);
|
|
48
|
+
|
|
49
|
+
// Act
|
|
50
|
+
debounced();
|
|
51
|
+
debounced();
|
|
52
|
+
debounced();
|
|
53
|
+
jest.advanceTimersByTime(501);
|
|
54
|
+
|
|
55
|
+
// Assert
|
|
56
|
+
expect(callbackFnMock).toHaveBeenCalledTimes(1);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should execute the last call with the exact args", () => {
|
|
60
|
+
// Arrange
|
|
61
|
+
const callbackFnMock = jest.fn();
|
|
62
|
+
const debounced = debounce(callbackFnMock, 500);
|
|
63
|
+
|
|
64
|
+
// Act
|
|
65
|
+
debounced("a");
|
|
66
|
+
debounced("ab");
|
|
67
|
+
debounced("abc");
|
|
68
|
+
jest.advanceTimersByTime(501);
|
|
69
|
+
|
|
70
|
+
// Assert
|
|
71
|
+
expect(callbackFnMock).toHaveBeenCalledWith("abc");
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Checks if a given key is a valid ASCII value.
|
|
5
|
+
*
|
|
6
|
+
* @param {string} key The key that is being typed in.
|
|
7
|
+
* @returns A valid string representation of the given key.
|
|
8
|
+
*/
|
|
9
|
+
export function getStringForKey(key: string): string {
|
|
10
|
+
// If the key is of length 1, it is an ASCII value.
|
|
11
|
+
// Otherwise, if there are no ASCII characters in the key name,
|
|
12
|
+
// it is a Unicode character.
|
|
13
|
+
// See https://www.w3.org/TR/uievents-key/
|
|
14
|
+
if (key.length === 1 || !/^[A-Z]/i.test(key)) {
|
|
15
|
+
return key;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return "";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
*
|
|
23
|
+
* @param {fn} callback The function that will be executed after the debounce is resolved.
|
|
24
|
+
* @param {number} wait The period of time that will be executed the debounced
|
|
25
|
+
* function.
|
|
26
|
+
* @returns The function that will be executed after the wait period is
|
|
27
|
+
* fulfilled.
|
|
28
|
+
*/
|
|
29
|
+
export function debounce(
|
|
30
|
+
callback: (...args: any) => void,
|
|
31
|
+
wait: number,
|
|
32
|
+
): (...args: any) => void {
|
|
33
|
+
let timeout;
|
|
34
|
+
|
|
35
|
+
return function executedFunction(...args) {
|
|
36
|
+
const later = () => {
|
|
37
|
+
clearTimeout(timeout);
|
|
38
|
+
callback(...args);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
clearTimeout(timeout);
|
|
42
|
+
timeout = setTimeout(later, wait);
|
|
43
|
+
};
|
|
44
|
+
}
|