@khanacademy/wonder-blocks-dropdown 2.7.5 → 2.7.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @khanacademy/wonder-blocks-dropdown
2
2
 
3
+ ## 2.7.6
4
+
5
+ ### Patch Changes
6
+
7
+ - 3007ecd7: Move the search field out of the listbox container
8
+
3
9
  ## 2.7.5
4
10
 
5
11
  ### Patch Changes
package/dist/es/index.js CHANGED
@@ -12,8 +12,8 @@ import _objectWithoutPropertiesLoose from '@babel/runtime/helpers/objectWithoutP
12
12
  import Icon, { icons } from '@khanacademy/wonder-blocks-icon';
13
13
  import ReactDOM from 'react-dom';
14
14
  import { VariableSizeList } from 'react-window';
15
- import { withActionScheduler } from '@khanacademy/wonder-blocks-timing';
16
15
  import SearchField from '@khanacademy/wonder-blocks-search-field';
16
+ import { withActionScheduler } from '@khanacademy/wonder-blocks-timing';
17
17
  import { Popper } from 'react-popper';
18
18
  import { maybeGetPortalMountedModalHostElement } from '@khanacademy/wonder-blocks-modal';
19
19
  import { Strut } from '@khanacademy/wonder-blocks-layout';
@@ -33,15 +33,9 @@ const selectDropdownStyle = {
33
33
  const filterableDropdownStyle = {
34
34
  minHeight: 100
35
35
  };
36
- const searchInputStyle = {
37
- margin: Spacing.xSmall_8,
38
- marginTop: Spacing.xxxSmall_4,
39
- minHeight: "auto"
40
- };
41
36
  const DROPDOWN_ITEM_HEIGHT = 40;
42
37
  const MAX_VISIBLE_ITEMS = 9;
43
38
  const SEPARATOR_ITEM_HEIGHT = 9;
44
- const SEARCH_ITEM_HEIGHT = DROPDOWN_ITEM_HEIGHT + searchInputStyle.margin + searchInputStyle.marginTop;
45
39
  const defaultLabels = {
46
40
  clearSearch: "Clear search",
47
41
  filter: "Filter",
@@ -487,56 +481,10 @@ class DropdownVirtualizedItem extends React.Component {
487
481
 
488
482
  }
489
483
 
490
- class SearchTextInput extends React.Component {
491
- static isClassOf(instance) {
492
- return instance && instance.type && instance.type.__IS_SEARCH_TEXT_INPUT__;
493
- }
494
-
495
- componentDidMount() {
496
- if (this.props.autofocus) {
497
- var _this$props$itemRef;
498
-
499
- (_this$props$itemRef = this.props.itemRef) == null ? void 0 : _this$props$itemRef.current.focus();
500
- }
501
- }
502
-
503
- render() {
504
- const {
505
- labels,
506
- onChange,
507
- onClick,
508
- itemRef,
509
- searchText,
510
- style,
511
- testId
512
- } = this.props;
513
- return React.createElement(SearchField, {
514
- clearAriaLabel: labels.clearSearch,
515
- onChange: onChange,
516
- onClick: onClick,
517
- placeholder: labels.filter,
518
- ref: itemRef,
519
- style: style,
520
- testId: testId,
521
- value: searchText
522
- });
523
- }
524
-
525
- }
526
- SearchTextInput.defaultProps = {
527
- labels: {
528
- clearSearch: defaultLabels.clearSearch,
529
- filter: defaultLabels.filter
530
- }
531
- };
532
- SearchTextInput.__IS_SEARCH_TEXT_INPUT__ = true;
533
-
534
484
  function getDropdownMenuHeight(items, initialHeight = 0) {
535
485
  return items.slice(0, MAX_VISIBLE_ITEMS).reduce((sum, item) => {
536
486
  if (SeparatorItem.isClassOf(item.component)) {
537
487
  return sum + SEPARATOR_ITEM_HEIGHT;
538
- } else if (SearchTextInput.isClassOf(item.component)) {
539
- return sum + SEARCH_ITEM_HEIGHT;
540
488
  } else {
541
489
  return sum + DROPDOWN_ITEM_HEIGHT;
542
490
  }
@@ -561,8 +509,6 @@ class DropdownCoreVirtualized extends React.Component {
561
509
 
562
510
  if (SeparatorItem.isClassOf(item.component)) {
563
511
  return SEPARATOR_ITEM_HEIGHT;
564
- } else if (SearchTextInput.isClassOf(item.component)) {
565
- return SEARCH_ITEM_HEIGHT;
566
512
  } else {
567
513
  return DROPDOWN_ITEM_HEIGHT;
568
514
  }
@@ -765,6 +711,7 @@ class DropdownCore extends React.Component {
765
711
 
766
712
  constructor(props) {
767
713
  super(props);
714
+ this.searchFieldRef = React.createRef();
768
715
 
769
716
  this.handleInteract = event => {
770
717
  const {
@@ -799,7 +746,7 @@ class DropdownCore extends React.Component {
799
746
 
800
747
  switch (keyCode) {
801
748
  case keyCodes.tab:
802
- if (this.hasSearchBox() && this.focusedIndex === 0 && searchText) {
749
+ if (this.isSearchFieldFocused() && searchText) {
803
750
  return;
804
751
  }
805
752
 
@@ -808,7 +755,7 @@ class DropdownCore extends React.Component {
808
755
  return;
809
756
 
810
757
  case keyCodes.space:
811
- if (this.hasSearchBox() && this.focusedIndex === 0) {
758
+ if (this.isSearchFieldFocused()) {
812
759
  return;
813
760
  }
814
761
 
@@ -836,7 +783,7 @@ class DropdownCore extends React.Component {
836
783
 
837
784
  switch (keyCode) {
838
785
  case keyCodes.space:
839
- if (this.hasSearchBox() && this.focusedIndex === 0) {
786
+ if (this.isSearchFieldFocused()) {
840
787
  return;
841
788
  }
842
789
 
@@ -880,6 +827,16 @@ class DropdownCore extends React.Component {
880
827
  }
881
828
  };
882
829
 
830
+ this.handleSearchTextChanged = searchText => {
831
+ const {
832
+ onSearchTextChanged
833
+ } = this.props;
834
+
835
+ if (onSearchTextChanged) {
836
+ onSearchTextChanged(searchText);
837
+ }
838
+ };
839
+
883
840
  this.resetFocusedIndex();
884
841
  this.state = {
885
842
  prevItems: this.props.items,
@@ -937,22 +894,18 @@ class DropdownCore extends React.Component {
937
894
  this.removeEventListeners();
938
895
  }
939
896
 
940
- hasSearchBox() {
941
- return !!this.props.onSearchTextChanged && typeof this.props.searchText === "string";
942
- }
943
-
944
897
  resetFocusedIndex() {
945
898
  const {
946
899
  initialFocusedIndex
947
900
  } = this.props;
948
901
 
949
- if (initialFocusedIndex) {
950
- if (this.hasSearchBox()) {
951
- this.focusedIndex = initialFocusedIndex + 1;
952
- } else {
953
- this.focusedIndex = initialFocusedIndex;
954
- }
902
+ if (typeof initialFocusedIndex !== "undefined") {
903
+ this.focusedIndex = initialFocusedIndex;
955
904
  } else {
905
+ if (this.hasSearchField() && !this.isSearchFieldFocused()) {
906
+ return this.focusSearchField();
907
+ }
908
+
956
909
  this.focusedIndex = 0;
957
910
  }
958
911
  }
@@ -997,24 +950,42 @@ class DropdownCore extends React.Component {
997
950
  }
998
951
 
999
952
  focusCurrentItem() {
1000
- const fousedItemRef = this.state.itemRefs[this.focusedIndex];
953
+ const focusedItemRef = this.state.itemRefs[this.focusedIndex];
1001
954
 
1002
- if (fousedItemRef) {
955
+ if (focusedItemRef) {
1003
956
  if (this.virtualizedListRef.current) {
1004
- this.virtualizedListRef.current.scrollToItem(fousedItemRef.originalIndex);
957
+ this.virtualizedListRef.current.scrollToItem(focusedItemRef.originalIndex);
1005
958
  }
1006
959
 
1007
- const node = ReactDOM.findDOMNode(fousedItemRef.ref.current);
960
+ const node = ReactDOM.findDOMNode(focusedItemRef.ref.current);
1008
961
 
1009
962
  if (node) {
1010
963
  node.focus();
1011
- this.focusedOriginalIndex = fousedItemRef.originalIndex;
964
+ this.focusedOriginalIndex = focusedItemRef.originalIndex;
1012
965
  }
1013
966
  }
1014
967
  }
1015
968
 
969
+ focusSearchField() {
970
+ if (this.searchFieldRef.current) {
971
+ this.searchFieldRef.current.focus();
972
+ }
973
+ }
974
+
975
+ hasSearchField() {
976
+ return !!this.props.isFilterable;
977
+ }
978
+
979
+ isSearchFieldFocused() {
980
+ return this.hasSearchField() && document.activeElement === this.searchFieldRef.current;
981
+ }
982
+
1016
983
  focusPreviousItem() {
1017
984
  if (this.focusedIndex === 0) {
985
+ if (this.hasSearchField() && !this.isSearchFieldFocused()) {
986
+ return this.focusSearchField();
987
+ }
988
+
1018
989
  this.focusedIndex = this.state.itemRefs.length - 1;
1019
990
  } else {
1020
991
  this.focusedIndex -= 1;
@@ -1025,6 +996,10 @@ class DropdownCore extends React.Component {
1025
996
 
1026
997
  focusNextItem() {
1027
998
  if (this.focusedIndex === this.state.itemRefs.length - 1) {
999
+ if (this.hasSearchField() && !this.isSearchFieldFocused()) {
1000
+ return this.focusSearchField();
1001
+ }
1002
+
1028
1003
  this.focusedIndex = 0;
1029
1004
  } else {
1030
1005
  this.focusedIndex += 1;
@@ -1059,15 +1034,11 @@ class DropdownCore extends React.Component {
1059
1034
  maybeRenderNoResults() {
1060
1035
  const {
1061
1036
  items,
1062
- onSearchTextChanged,
1063
- searchText,
1064
1037
  labels: {
1065
1038
  noResults
1066
1039
  }
1067
1040
  } = this.props;
1068
- const showSearchTextInput = !!onSearchTextChanged && typeof searchText === "string";
1069
- const includeSearchCount = showSearchTextInput ? 1 : 0;
1070
- const numResults = items.length - includeSearchCount;
1041
+ const numResults = items.length;
1071
1042
 
1072
1043
  if (numResults === 0) {
1073
1044
  return React.createElement(LabelMedium, {
@@ -1103,20 +1074,6 @@ class DropdownCore extends React.Component {
1103
1074
 
1104
1075
  const focusIndex = focusCounter - 1;
1105
1076
  const currentRef = this.state.itemRefs[focusIndex] ? this.state.itemRefs[focusIndex].ref : null;
1106
-
1107
- if (SearchTextInput.isClassOf(component)) {
1108
- return React.cloneElement(component, _extends({}, populatedProps, {
1109
- key: "search-text-input",
1110
- itemRef: currentRef,
1111
- onClick: () => {
1112
- this.handleClickFocus(0);
1113
- this.focusCurrentItem();
1114
- },
1115
- style: searchInputStyle,
1116
- autofocus: this.focusedIndex === 0
1117
- }));
1118
- }
1119
-
1120
1077
  return React.cloneElement(component, _extends({}, populatedProps, {
1121
1078
  key: index,
1122
1079
  onClick: () => {
@@ -1137,21 +1094,6 @@ class DropdownCore extends React.Component {
1137
1094
  }
1138
1095
 
1139
1096
  const focusIndex = focusCounter - 1;
1140
-
1141
- if (SearchTextInput.isClassOf(item.component)) {
1142
- return _extends({}, item, {
1143
- onClick: () => {
1144
- this.handleClickFocus(0);
1145
- this.focusCurrentItem();
1146
- },
1147
- populatedProps: {
1148
- style: searchInputStyle,
1149
- itemRef: this.state.itemRefs[focusIndex] ? this.state.itemRefs[focusIndex].ref : null,
1150
- autofocus: this.focusedIndex === 0
1151
- }
1152
- });
1153
- }
1154
-
1155
1097
  return _extends({}, item, {
1156
1098
  role: itemRole,
1157
1099
  ref: item.focusable ? this.state.itemRefs[focusIndex] ? this.state.itemRefs[focusIndex].ref : null : null,
@@ -1170,6 +1112,23 @@ class DropdownCore extends React.Component {
1170
1112
  });
1171
1113
  }
1172
1114
 
1115
+ renderSearchField() {
1116
+ const {
1117
+ searchText
1118
+ } = this.props;
1119
+ const {
1120
+ labels
1121
+ } = this.state;
1122
+ return React.createElement(SearchField, {
1123
+ clearAriaLabel: labels.clearSearch,
1124
+ onChange: this.handleSearchTextChanged,
1125
+ placeholder: labels.filter,
1126
+ ref: this.searchFieldRef,
1127
+ style: styles$3.searchInputStyle,
1128
+ value: searchText || ""
1129
+ });
1130
+ }
1131
+
1173
1132
  renderDropdownMenu(listRenderer, isReferenceHidden) {
1174
1133
  const {
1175
1134
  dropdownStyle,
@@ -1178,13 +1137,15 @@ class DropdownCore extends React.Component {
1178
1137
  } = this.props;
1179
1138
  const openerStyle = openerElement && window.getComputedStyle(openerElement);
1180
1139
  const minDropdownWidth = openerStyle ? openerStyle.getPropertyValue("width") : 0;
1181
- const initialHeight = 12;
1182
- const maxDropdownHeight = getDropdownMenuHeight(this.props.items, initialHeight);
1140
+ const maxDropdownHeight = getDropdownMenuHeight(this.props.items);
1183
1141
  return React.createElement(View, {
1184
1142
  onMouseUp: this.handleDropdownMouseUp,
1143
+ style: [styles$3.dropdown, light && styles$3.light, isReferenceHidden && styles$3.hidden, dropdownStyle],
1144
+ testId: "dropdown-core-container"
1145
+ }, this.props.isFilterable && this.renderSearchField(), React.createElement(View, {
1185
1146
  role: this.props.role,
1186
- style: [styles$3.dropdown, light && styles$3.light, isReferenceHidden && styles$3.hidden, generateDropdownMenuStyles(minDropdownWidth, maxDropdownHeight), dropdownStyle]
1187
- }, listRenderer, this.maybeRenderNoResults());
1147
+ style: [styles$3.listboxOrMenu, generateDropdownMenuStyles(minDropdownWidth, maxDropdownHeight)]
1148
+ }, listRenderer), this.maybeRenderNoResults());
1188
1149
  }
1189
1150
 
1190
1151
  renderDropdown() {
@@ -1210,7 +1171,7 @@ class DropdownCore extends React.Component {
1210
1171
  const {
1211
1172
  labels
1212
1173
  } = this.state;
1213
- const totalItems = this.hasSearchBox() ? items.length - 1 : items.length;
1174
+ const totalItems = items.length;
1214
1175
  return React.createElement(StyledSpan, {
1215
1176
  "aria-live": "polite",
1216
1177
  "aria-atomic": "true",
@@ -1240,6 +1201,8 @@ class DropdownCore extends React.Component {
1240
1201
  DropdownCore.defaultProps = {
1241
1202
  alignment: "left",
1242
1203
  labels: {
1204
+ clearSearch: defaultLabels.clearSearch,
1205
+ filter: defaultLabels.filter,
1243
1206
  noResults: defaultLabels.noResults,
1244
1207
  someSelected: defaultLabels.someSelected
1245
1208
  },
@@ -1255,12 +1218,14 @@ const styles$3 = StyleSheet.create({
1255
1218
  paddingTop: Spacing.xxxSmall_4,
1256
1219
  paddingBottom: Spacing.xxxSmall_4,
1257
1220
  border: `solid 1px ${Color.offBlack16}`,
1258
- boxShadow: `0px 8px 8px 0px ${fade(Color.offBlack, 0.1)}`,
1259
- overflowY: "auto"
1221
+ boxShadow: `0px 8px 8px 0px ${fade(Color.offBlack, 0.1)}`
1260
1222
  },
1261
1223
  light: {
1262
1224
  border: "none"
1263
1225
  },
1226
+ listboxOrMenu: {
1227
+ overflowY: "auto"
1228
+ },
1264
1229
  hidden: {
1265
1230
  pointerEvents: "none",
1266
1231
  visibility: "hidden"
@@ -1270,6 +1235,11 @@ const styles$3 = StyleSheet.create({
1270
1235
  alignSelf: "center",
1271
1236
  marginTop: Spacing.xxSmall_6
1272
1237
  },
1238
+ searchInputStyle: {
1239
+ margin: Spacing.xSmall_8,
1240
+ marginTop: Spacing.xxxSmall_4,
1241
+ minHeight: "auto"
1242
+ },
1273
1243
  srOnly: {
1274
1244
  border: 0,
1275
1245
  clip: "rect(0,0,0,0)",
@@ -1887,26 +1857,6 @@ class SingleSelect extends React.Component {
1887
1857
  return this.mapOptionItemsToDropdownItems(isFilterable ? this.filterChildren(children) : children);
1888
1858
  }
1889
1859
 
1890
- getSearchField() {
1891
- if (!this.props.isFilterable) {
1892
- return null;
1893
- }
1894
-
1895
- return {
1896
- component: React.createElement(SearchTextInput, {
1897
- key: "search-text-input",
1898
- onChange: this.handleSearchTextChanged,
1899
- searchText: this.state.searchText,
1900
- labels: {
1901
- clearSearch: defaultLabels.clearSearch,
1902
- filter: defaultLabels.filter
1903
- }
1904
- }),
1905
- focusable: true,
1906
- populatedProps: {}
1907
- };
1908
- }
1909
-
1910
1860
  renderOpener(numItems) {
1911
1861
  const _this$props = this.props,
1912
1862
  {
@@ -1955,10 +1905,8 @@ class SingleSelect extends React.Component {
1955
1905
  searchText
1956
1906
  } = this.state;
1957
1907
  const allChildren = React.Children.toArray(children).filter(Boolean);
1958
- const filteredItems = this.getMenuItems(allChildren);
1908
+ const items = this.getMenuItems(allChildren);
1959
1909
  const opener = this.renderOpener(allChildren.length);
1960
- const searchField = this.getSearchField();
1961
- const items = searchField ? [searchField].concat(filteredItems) : filteredItems;
1962
1910
  return React.createElement(DropdownCore$1, {
1963
1911
  role: "listbox",
1964
1912
  alignment: alignment,
@@ -1972,6 +1920,7 @@ class SingleSelect extends React.Component {
1972
1920
  openerElement: this.state.openerElement,
1973
1921
  style: style,
1974
1922
  className: className,
1923
+ isFilterable: isFilterable,
1975
1924
  onSearchTextChanged: isFilterable ? this.handleSearchTextChanged : null,
1976
1925
  searchText: isFilterable ? searchText : ""
1977
1926
  });
@@ -2120,30 +2069,6 @@ class MultiSelect extends React.Component {
2120
2069
  }
2121
2070
  }
2122
2071
 
2123
- getSearchField() {
2124
- if (!this.props.isFilterable) {
2125
- return [];
2126
- }
2127
-
2128
- const {
2129
- clearSearch,
2130
- filter
2131
- } = this.state.labels;
2132
- return [{
2133
- component: React.createElement(SearchTextInput, {
2134
- key: "search-text-input",
2135
- onChange: this.handleSearchTextChanged,
2136
- searchText: this.state.searchText,
2137
- labels: {
2138
- clearSearch,
2139
- filter
2140
- }
2141
- }),
2142
- focusable: true,
2143
- populatedProps: {}
2144
- }];
2145
- }
2146
-
2147
2072
  getShortcuts(numOptions) {
2148
2073
  const {
2149
2074
  selectedValues,
@@ -2282,6 +2207,8 @@ class MultiSelect extends React.Component {
2282
2207
  searchText
2283
2208
  } = this.state;
2284
2209
  const {
2210
+ clearSearch,
2211
+ filter,
2285
2212
  noResults,
2286
2213
  someSelected
2287
2214
  } = this.state.labels;
@@ -2293,7 +2220,8 @@ class MultiSelect extends React.Component {
2293
2220
  role: "listbox",
2294
2221
  alignment: alignment,
2295
2222
  dropdownStyle: [isFilterable && filterableDropdownStyle, selectDropdownStyle, dropdownStyle],
2296
- items: [].concat(this.getSearchField(), this.getShortcuts(numOptions), filteredItems),
2223
+ isFilterable: isFilterable,
2224
+ items: [].concat(this.getShortcuts(numOptions), filteredItems),
2297
2225
  light: light,
2298
2226
  onOpenChanged: this.handleOpenChanged,
2299
2227
  open: open,
@@ -2304,6 +2232,8 @@ class MultiSelect extends React.Component {
2304
2232
  onSearchTextChanged: isFilterable ? this.handleSearchTextChanged : null,
2305
2233
  searchText: isFilterable ? searchText : "",
2306
2234
  labels: {
2235
+ clearSearch,
2236
+ filter,
2307
2237
  noResults,
2308
2238
  someSelected
2309
2239
  }