@entur/dropdown 7.3.8 → 7.3.9-beta.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.
@@ -1,6 +1,6 @@
1
1
  import { useDebounce, mergeRefs, useRandomId, warnAboutMissingStyles } from "@entur/utils";
2
2
  import { jsx, jsxs, Fragment } from "react/jsx-runtime";
3
- import React, { forwardRef, createElement, useState, useRef, useEffect, useCallback } from "react";
3
+ import React, { forwardRef, createElement, useMemo, useState, useRef, useEffect, useCallback, useLayoutEffect } from "react";
4
4
  import { useCombobox, useMultipleSelection, useSelect } from "downshift";
5
5
  import classNames from "classnames";
6
6
  import { useFloating, offset, shift, size, flip, autoUpdate } from "@floating-ui/react-dom";
@@ -16,19 +16,19 @@ const DropdownList = ({
16
16
  ariaLabelChosenSingular = "valgt",
17
17
  ariaLabelSelectedItem = ", valgt element, trykk for å fjerne",
18
18
  getItemProps,
19
- getMenuProps,
20
19
  isOpen,
21
20
  highlightedIndex,
22
21
  listItems,
23
22
  floatingStyles,
24
- setListRef,
23
+ innerRef,
25
24
  loading = false,
26
25
  loadingText = "Laster inn …",
27
26
  noMatchesText = "Ingen treff for søket",
27
+ readOnly = false,
28
28
  selectAllCheckboxState,
29
29
  selectAllItem,
30
30
  selectedItems,
31
- readOnly = false,
31
+ style,
32
32
  ...rest
33
33
  }) => {
34
34
  const isMultiselect = selectAllItem !== void 0;
@@ -41,7 +41,7 @@ const DropdownList = ({
41
41
  return true;
42
42
  });
43
43
  const ariaValuesSelectAll = () => {
44
- switch (selectAllCheckboxState?.()) {
44
+ switch (selectAllCheckboxState) {
45
45
  case "indeterminate": {
46
46
  return {
47
47
  label: `${selectAllItem?.label}, delvis valgt`,
@@ -64,7 +64,7 @@ const DropdownList = ({
64
64
  Checkbox,
65
65
  {
66
66
  "aria-hidden": "true",
67
- checked: selectAllCheckboxState?.(),
67
+ checked: selectAllCheckboxState,
68
68
  className: "eds-dropdown__list__item__checkbox",
69
69
  tabIndex: -1,
70
70
  onChange: () => void 0
@@ -111,75 +111,68 @@ const DropdownList = ({
111
111
  }) : null
112
112
  ] });
113
113
  };
114
- return (
115
- // use popover from @entur/tooltip when that package upgrades to floating-ui
116
- /* @__PURE__ */ jsx(
117
- "ul",
118
- {
119
- ...getMenuProps({
120
- "aria-multiselectable": isMultiselect,
121
- ref: setListRef,
122
- className: "eds-dropdown__list",
123
- style: {
124
- ...floatingStyles,
125
- display: isOpen && !readOnly ? void 0 : "none",
126
- ...rest.style
127
- }
128
- }),
129
- children: (() => {
130
- if (!isOpen || readOnly) {
131
- return null;
132
- }
133
- if (loading) {
134
- return /* @__PURE__ */ jsx(
135
- "li",
136
- {
137
- className: "eds-dropdown__list__item",
138
- children: loadingText
139
- },
140
- "dropdown-list-loading"
141
- );
142
- }
143
- if (isNoMatches) {
144
- return /* @__PURE__ */ jsx(
145
- "li",
146
- {
147
- className: "eds-dropdown__list__item",
148
- children: noMatchesText
149
- },
150
- "dropdown-list-no-match"
151
- );
152
- }
153
- return listItems.map((item, index) => {
154
- const key = item.itemKey ?? `${item.label ?? ""}-${item.value ?? ""}-${(item.icons ?? []).map((icon) => icon?.displayName ?? icon?.name ?? "unknown").join("-")}`;
155
- const itemIsSelectAll = item.value === selectAllItem?.value;
156
- if (itemIsSelectAll && listItems.length <= 2) return null;
157
- return /* @__PURE__ */ jsx(
158
- "li",
159
- {
160
- className: classNames("eds-dropdown__list__item", {
161
- "eds-dropdown__list__item--select-all": itemIsSelectAll,
162
- "eds-dropdown__list__item--highlighted": highlightedIndex === index,
163
- "eds-dropdown__list__item--selected": !isMultiselect && isItemSelected(item)
164
- }),
165
- ...getItemProps({
166
- // @ts-expect-error Since getItemProps expects the same item type
167
- // here as items, it throws error when selectAllItem is a string.
168
- // This does, however, not cause any functional issues.
169
- item,
170
- index,
171
- "aria-selected": itemIsSelectAll ? ariaValuesSelectAll().selected : isItemSelected(item)
172
- }),
173
- children: itemIsSelectAll ? selectAllListItemContent() : listItemContent(
174
- item
175
- )
176
- },
177
- key
178
- );
179
- });
180
- })()
181
- }
182
- )
114
+ return /* @__PURE__ */ jsx(
115
+ "ul",
116
+ {
117
+ className: "eds-dropdown__list",
118
+ ref: innerRef,
119
+ style: {
120
+ display: isOpen && !readOnly ? void 0 : "none",
121
+ ...floatingStyles,
122
+ ...style
123
+ },
124
+ ...rest,
125
+ children: (() => {
126
+ if (!isOpen || readOnly) return null;
127
+ if (loading) {
128
+ return /* @__PURE__ */ jsx(
129
+ "li",
130
+ {
131
+ className: "eds-dropdown__list__item",
132
+ children: loadingText
133
+ },
134
+ "dropdown-list-loading"
135
+ );
136
+ }
137
+ if (isNoMatches) {
138
+ return /* @__PURE__ */ jsx(
139
+ "li",
140
+ {
141
+ className: "eds-dropdown__list__item",
142
+ children: noMatchesText
143
+ },
144
+ "dropdown-list-no-match"
145
+ );
146
+ }
147
+ return listItems.map((item, index) => {
148
+ const key = item.itemKey ?? `${item.label ?? ""}-${item.value ?? ""}-${(item.icons ?? []).map((icon) => icon?.displayName ?? icon?.name ?? "unknown").join("-")}`;
149
+ const itemIsSelectAll = item.value === selectAllItem?.value;
150
+ if (itemIsSelectAll && listItems.length <= 2) return null;
151
+ return /* @__PURE__ */ jsx(
152
+ "li",
153
+ {
154
+ className: classNames("eds-dropdown__list__item", {
155
+ "eds-dropdown__list__item--select-all": itemIsSelectAll,
156
+ "eds-dropdown__list__item--highlighted": highlightedIndex === index,
157
+ "eds-dropdown__list__item--selected": !isMultiselect && isItemSelected(item)
158
+ }),
159
+ ...getItemProps({
160
+ // @ts-expect-error Since getItemProps expects the same item type
161
+ // here as items, it throws error when selectAllItem is a string.
162
+ // This does, however, not cause any functional issues.
163
+ item,
164
+ index,
165
+ "aria-selected": itemIsSelectAll ? ariaValuesSelectAll().selected : isItemSelected(item)
166
+ }),
167
+ children: itemIsSelectAll ? selectAllListItemContent() : listItemContent(
168
+ item
169
+ )
170
+ },
171
+ key
172
+ );
173
+ });
174
+ })()
175
+ }
183
176
  );
184
177
  };
185
178
  const SelectedItemTag = ({
@@ -455,11 +448,11 @@ const useMultiselectUtils = ({
455
448
  (selectedItem) => selectedItem.value !== clickedItem.value
456
449
  )
457
450
  );
458
- const selectAllCheckboxState = () => {
451
+ const selectAllCheckboxState = useMemo(() => {
459
452
  if (allListItemsAreSelected) return true;
460
453
  if (someListItemsAreSelected) return "indeterminate";
461
454
  return false;
462
- };
455
+ }, [allListItemsAreSelected, someListItemsAreSelected]);
463
456
  const selectAllUnselectedItemsInListItems = (onChange) => {
464
457
  onChange([...selectedItems, ...unselectedItemsInListItems]);
465
458
  };
@@ -553,7 +546,6 @@ const SearchableDropdown = React.forwardRef(
553
546
  const resetInputState = ({
554
547
  changes
555
548
  }) => {
556
- updateListItems({ inputValue: EMPTY_INPUT });
557
549
  return {
558
550
  ...changes,
559
551
  inputValue: EMPTY_INPUT
@@ -582,11 +574,12 @@ const SearchableDropdown = React.forwardRef(
582
574
  // empty input to show selected item and reset dropdown list on item selection
583
575
  case useCombobox.stateChangeTypes.ItemClick:
584
576
  case useCombobox.stateChangeTypes.InputKeyDownEnter:
585
- case useCombobox.stateChangeTypes.InputBlur:
586
577
  return resetInputState({ changes });
578
+ case useCombobox.stateChangeTypes.InputBlur:
579
+ return resetInputState({
580
+ changes: { ...changes, selectedItem: value }
581
+ });
587
582
  case useCombobox.stateChangeTypes.ControlledPropUpdatedSelectedItem:
588
- if (changes.selectedItem !== null && !inputHasFocus)
589
- setShowSelectedItem(true);
590
583
  return resetInputState({ changes });
591
584
  // remove leading whitespace, select element with spacebar on empty input
592
585
  case useCombobox.stateChangeTypes.InputChange: {
@@ -657,19 +650,17 @@ const SearchableDropdown = React.forwardRef(
657
650
  offset(space.extraSmall2),
658
651
  shift({ padding: space.extraSmall }),
659
652
  size({
660
- apply({ rects, elements, availableHeight }) {
661
- Object.assign(elements.floating.style, {
662
- width: `${rects.reference.width}px`,
663
- // Floating will flip when smaller than 10*16 px
664
- // and never exceed 20*16 px.
665
- maxHeight: `${clamp(10 * 16, availableHeight, 20 * 16)}px`
666
- });
653
+ apply({ elements, availableHeight }) {
654
+ elements.floating.style.setProperty(
655
+ "--list-max-height",
656
+ `${clamp(10 * 16, availableHeight, 20 * 16)}px`
657
+ );
667
658
  }
668
659
  }),
669
660
  flip({ fallbackStrategy: "initialPlacement" })
670
661
  ]
671
662
  });
672
- useEffect(() => {
663
+ useLayoutEffect(() => {
673
664
  if (isOpen && refs.reference.current && refs.floating.current) {
674
665
  return autoUpdate(
675
666
  refs.reference.current,
@@ -700,9 +691,7 @@ const SearchableDropdown = React.forwardRef(
700
691
  labelProps: getLabelProps(),
701
692
  labelTooltip,
702
693
  onClick: (e) => {
703
- if (e.target === e.currentTarget) {
704
- getInputProps()?.onClick?.(e);
705
- }
694
+ if (e.target === e.currentTarget) getInputProps()?.onClick?.(e);
706
695
  onClick?.(e);
707
696
  },
708
697
  onKeyDown,
@@ -720,17 +709,19 @@ const SearchableDropdown = React.forwardRef(
720
709
  ariaLabelSelectedItem,
721
710
  floatingStyles,
722
711
  getItemProps,
723
- getMenuProps,
724
712
  highlightedIndex,
725
713
  isOpen,
726
714
  listItems,
727
- style: listStyle,
728
- setListRef: refs.setFloating,
729
715
  loading: loading ?? resolvedItemsLoading,
730
716
  loadingText,
731
717
  noMatchesText,
732
718
  selectedItems: selectedItem !== null ? [selectedItem] : [],
733
- readOnly
719
+ readOnly,
720
+ ...getMenuProps({
721
+ refKey: "innerRef",
722
+ ref: refs.setFloating,
723
+ style: listStyle
724
+ })
734
725
  }
735
726
  ),
736
727
  ...rest,
@@ -762,8 +753,9 @@ const SearchableDropdown = React.forwardRef(
762
753
  onKeyDown(e) {
763
754
  if (isOpen && e.key === "Tab") {
764
755
  const highlitedItem = listItems[highlightedIndex];
765
- if ((selectOnTab || selectOnBlur) && highlitedItem && highlitedItem !== selectedItem) {
756
+ if ((selectOnTab || selectOnBlur) && highlitedItem) {
766
757
  selectItem(highlitedItem);
758
+ setShowSelectedItem(true);
767
759
  }
768
760
  }
769
761
  },
@@ -885,15 +877,21 @@ const MultiSelect = React.forwardRef(
885
877
  ...!hideSelectAll ? [selectAll] : [],
886
878
  ...normalizedItems
887
879
  ]);
888
- const filterListItems = ({ inputValue: inputValue2 }) => setListItems([
889
- ...!hideSelectAll ? [selectAll] : [],
890
- ...normalizedItems.filter((item) => itemFilter(item, inputValue2))
891
- ]);
892
- const updateListItems = ({ inputValue: inputValue2 }) => {
893
- const shouldRefetchItems = isFunctionWithQueryArgument(initialItems);
894
- if (shouldRefetchItems) fetchItems(inputValue2 ?? EMPTY_INPUT);
895
- filterListItems({ inputValue: inputValue2 ?? EMPTY_INPUT });
896
- };
880
+ const filterListItems = React.useCallback(
881
+ ({ inputValue: inputValue2 }) => setListItems([
882
+ ...!hideSelectAll ? [selectAll] : [],
883
+ ...normalizedItems.filter((item) => itemFilter(item, inputValue2))
884
+ ]),
885
+ [hideSelectAll, selectAll, normalizedItems, itemFilter]
886
+ );
887
+ const updateListItems = React.useCallback(
888
+ ({ inputValue: inputValue2 }) => {
889
+ const shouldRefetchItems = isFunctionWithQueryArgument(initialItems);
890
+ if (shouldRefetchItems) fetchItems(inputValue2 ?? EMPTY_INPUT);
891
+ filterListItems({ inputValue: inputValue2 ?? EMPTY_INPUT });
892
+ },
893
+ [filterListItems, initialItems, fetchItems]
894
+ );
897
895
  React.useEffect(() => {
898
896
  filterListItems({ inputValue });
899
897
  }, [normalizedItems]);
@@ -958,6 +956,8 @@ const MultiSelect = React.forwardRef(
958
956
  case useCombobox.stateChangeTypes.InputChange: {
959
957
  const leadingWhitespaceTest = /^\s+/g;
960
958
  const isSpacePressedOnEmptyInput = changes.inputValue === " ";
959
+ if (!isSpacePressedOnEmptyInput)
960
+ setLastHighlightedIndex(hideSelectAll ? 0 : 1);
961
961
  if (changes.inputValue?.match(leadingWhitespaceTest)) {
962
962
  const sanitizedInputValue = changes.inputValue.replace(
963
963
  leadingWhitespaceTest,
@@ -979,13 +979,13 @@ const MultiSelect = React.forwardRef(
979
979
  }
980
980
  }
981
981
  }
982
- return changes;
982
+ return { ...changes, highlightedIndex: hideSelectAll ? 0 : 1 };
983
983
  }
984
984
  default:
985
985
  return changes;
986
986
  }
987
987
  },
988
- [hideSelectAll, normalizedItems, filterListItems, initialItems]
988
+ [hideSelectAll, normalizedItems, initialItems]
989
989
  );
990
990
  const {
991
991
  getInputProps,
@@ -994,7 +994,6 @@ const MultiSelect = React.forwardRef(
994
994
  getMenuProps,
995
995
  getToggleButtonProps,
996
996
  highlightedIndex,
997
- setHighlightedIndex,
998
997
  inputValue,
999
998
  isOpen,
1000
999
  setInputValue
@@ -1007,8 +1006,6 @@ const MultiSelect = React.forwardRef(
1007
1006
  stateReducer,
1008
1007
  onInputValueChange(changes) {
1009
1008
  updateListItems({ inputValue: changes.inputValue });
1010
- setHighlightedIndex(hideSelectAll ? 0 : 1);
1011
- setLastHighlightedIndex(hideSelectAll ? 0 : 1);
1012
1009
  },
1013
1010
  onSelectedItemChange({ selectedItem: clickedItem }) {
1014
1011
  if (!clickedItem) return;
@@ -1032,19 +1029,17 @@ const MultiSelect = React.forwardRef(
1032
1029
  offset(space.extraSmall2),
1033
1030
  shift({ padding: space.extraSmall }),
1034
1031
  size({
1035
- apply({ rects, elements, availableHeight }) {
1036
- Object.assign(elements.floating.style, {
1037
- width: `${rects.reference.width}px`,
1038
- // Floating will flip when smaller than 10*16 px
1039
- // and never exceed 20*16 px.
1040
- maxHeight: `${clamp(10 * 16, availableHeight, 20 * 16)}px`
1041
- });
1032
+ apply({ elements, availableHeight }) {
1033
+ elements.floating.style.setProperty(
1034
+ "--list-max-height",
1035
+ `${clamp(10 * 16, availableHeight, 20 * 16)}px`
1036
+ );
1042
1037
  }
1043
1038
  }),
1044
1039
  flip({ fallbackStrategy: "initialPlacement" })
1045
1040
  ]
1046
1041
  });
1047
- useEffect(() => {
1042
+ useLayoutEffect(() => {
1048
1043
  if (isOpen && refs.reference.current && refs.floating.current) {
1049
1044
  return autoUpdate(
1050
1045
  refs.reference.current,
@@ -1074,14 +1069,8 @@ const MultiSelect = React.forwardRef(
1074
1069
  labelId: getLabelProps().id,
1075
1070
  labelProps: getLabelProps(),
1076
1071
  labelTooltip,
1077
- onBlur: (e) => {
1078
- setInputValue("");
1079
- onBlur?.(e);
1080
- },
1081
1072
  onClick: (e) => {
1082
- if (e.target === e.currentTarget) {
1083
- getInputProps()?.onClick?.(e);
1084
- }
1073
+ if (e.target === e.currentTarget) getInputProps()?.onClick?.(e);
1085
1074
  onClick?.(e);
1086
1075
  },
1087
1076
  onKeyDown,
@@ -1097,19 +1086,22 @@ const MultiSelect = React.forwardRef(
1097
1086
  ariaLabelSelectedItem,
1098
1087
  floatingStyles,
1099
1088
  getItemProps,
1100
- getMenuProps,
1101
1089
  highlightedIndex,
1102
1090
  isOpen,
1103
1091
  listItems,
1104
- style: listStyle,
1105
- setListRef: refs.setFloating,
1106
1092
  loading: loading ?? resolvedItemsLoading,
1107
1093
  loadingText,
1108
1094
  noMatchesText,
1109
1095
  selectAllCheckboxState,
1110
1096
  selectAllItem: selectAll,
1111
1097
  selectedItems,
1112
- readOnly
1098
+ readOnly,
1099
+ ...getMenuProps({
1100
+ "aria-multiselectable": true,
1101
+ refKey: "innerRef",
1102
+ ref: refs.setFloating,
1103
+ style: listStyle
1104
+ })
1113
1105
  }
1114
1106
  ),
1115
1107
  ...rest,
@@ -1168,6 +1160,10 @@ const MultiSelect = React.forwardRef(
1168
1160
  });
1169
1161
  }
1170
1162
  },
1163
+ onBlur: (e) => {
1164
+ setInputValue("");
1165
+ onBlur?.(e);
1166
+ },
1171
1167
  ...getDropdownProps({
1172
1168
  preventKeyAction: isOpen,
1173
1169
  value: inputValue ?? EMPTY_INPUT,
@@ -1278,19 +1274,17 @@ const Dropdown = React.forwardRef(
1278
1274
  offset(space.extraSmall2),
1279
1275
  shift({ padding: space.extraSmall }),
1280
1276
  size({
1281
- apply({ rects, elements, availableHeight }) {
1282
- Object.assign(elements.floating.style, {
1283
- width: `${rects.reference.width}px`,
1284
- // Floating will flip when smaller than 10*16 px
1285
- // and never exceed 20*16 px.
1286
- maxHeight: `${clamp(10 * 16, availableHeight, 20 * 16)}px`
1287
- });
1277
+ apply({ elements, availableHeight }) {
1278
+ elements.floating.style.setProperty(
1279
+ "--list-max-height",
1280
+ `${clamp(10 * 16, availableHeight, 20 * 16)}px`
1281
+ );
1288
1282
  }
1289
1283
  }),
1290
1284
  flip({ fallbackStrategy: "initialPlacement" })
1291
1285
  ]
1292
1286
  });
1293
- useEffect(() => {
1287
+ useLayoutEffect(() => {
1294
1288
  if (isOpen && refs.reference.current && refs.floating.current) {
1295
1289
  return autoUpdate(
1296
1290
  refs.reference.current,
@@ -1343,17 +1337,19 @@ const Dropdown = React.forwardRef(
1343
1337
  ariaLabelSelectedItem,
1344
1338
  floatingStyles,
1345
1339
  getItemProps,
1346
- getMenuProps,
1347
1340
  highlightedIndex,
1348
1341
  isOpen,
1349
1342
  listItems: normalizedItems,
1350
- noMatchesText,
1351
- style: listStyle,
1352
- setListRef: refs.setFloating,
1353
1343
  loading: loading ?? resolvedItemsLoading,
1354
1344
  loadingText,
1345
+ noMatchesText,
1355
1346
  selectedItems: selectedItem !== null ? [selectedItem] : [],
1356
- readOnly
1347
+ readOnly,
1348
+ ...getMenuProps({
1349
+ refKey: "innerRef",
1350
+ ref: refs.setFloating,
1351
+ style: listStyle
1352
+ })
1357
1353
  }
1358
1354
  ),
1359
1355
  ...rest,