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