@entur/dropdown 7.3.9-beta.0 → 7.3.9

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, useMemo, useState, useRef, useEffect, useCallback, useLayoutEffect } from "react";
3
+ import React, { forwardRef, createElement, useState, useRef, useEffect, useCallback } 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,
19
20
  isOpen,
20
21
  highlightedIndex,
21
22
  listItems,
22
23
  floatingStyles,
23
- innerRef,
24
+ setListRef,
24
25
  loading = false,
25
26
  loadingText = "Laster inn …",
26
27
  noMatchesText = "Ingen treff for søket",
27
- readOnly = false,
28
28
  selectAllCheckboxState,
29
29
  selectAllItem,
30
30
  selectedItems,
31
- style,
31
+ readOnly = false,
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,68 +111,75 @@ const DropdownList = ({
111
111
  }) : null
112
112
  ] });
113
113
  };
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
- }
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
+ )
176
183
  );
177
184
  };
178
185
  const SelectedItemTag = ({
@@ -448,11 +455,11 @@ const useMultiselectUtils = ({
448
455
  (selectedItem) => selectedItem.value !== clickedItem.value
449
456
  )
450
457
  );
451
- const selectAllCheckboxState = useMemo(() => {
458
+ const selectAllCheckboxState = () => {
452
459
  if (allListItemsAreSelected) return true;
453
460
  if (someListItemsAreSelected) return "indeterminate";
454
461
  return false;
455
- }, [allListItemsAreSelected, someListItemsAreSelected]);
462
+ };
456
463
  const selectAllUnselectedItemsInListItems = (onChange) => {
457
464
  onChange([...selectedItems, ...unselectedItemsInListItems]);
458
465
  };
@@ -546,6 +553,7 @@ const SearchableDropdown = React.forwardRef(
546
553
  const resetInputState = ({
547
554
  changes
548
555
  }) => {
556
+ updateListItems({ inputValue: EMPTY_INPUT });
549
557
  return {
550
558
  ...changes,
551
559
  inputValue: EMPTY_INPUT
@@ -574,12 +582,11 @@ const SearchableDropdown = React.forwardRef(
574
582
  // empty input to show selected item and reset dropdown list on item selection
575
583
  case useCombobox.stateChangeTypes.ItemClick:
576
584
  case useCombobox.stateChangeTypes.InputKeyDownEnter:
577
- return resetInputState({ changes });
578
585
  case useCombobox.stateChangeTypes.InputBlur:
579
- return resetInputState({
580
- changes: { ...changes, selectedItem: value }
581
- });
586
+ return resetInputState({ changes });
582
587
  case useCombobox.stateChangeTypes.ControlledPropUpdatedSelectedItem:
588
+ if (changes.selectedItem !== null && !inputHasFocus)
589
+ setShowSelectedItem(true);
583
590
  return resetInputState({ changes });
584
591
  // remove leading whitespace, select element with spacebar on empty input
585
592
  case useCombobox.stateChangeTypes.InputChange: {
@@ -650,17 +657,19 @@ const SearchableDropdown = React.forwardRef(
650
657
  offset(space.extraSmall2),
651
658
  shift({ padding: space.extraSmall }),
652
659
  size({
653
- apply({ elements, availableHeight }) {
654
- elements.floating.style.setProperty(
655
- "--list-max-height",
656
- `${clamp(10 * 16, availableHeight, 20 * 16)}px`
657
- );
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
+ });
658
667
  }
659
668
  }),
660
669
  flip({ fallbackStrategy: "initialPlacement" })
661
670
  ]
662
671
  });
663
- useLayoutEffect(() => {
672
+ useEffect(() => {
664
673
  if (isOpen && refs.reference.current && refs.floating.current) {
665
674
  return autoUpdate(
666
675
  refs.reference.current,
@@ -691,7 +700,9 @@ const SearchableDropdown = React.forwardRef(
691
700
  labelProps: getLabelProps(),
692
701
  labelTooltip,
693
702
  onClick: (e) => {
694
- if (e.target === e.currentTarget) getInputProps()?.onClick?.(e);
703
+ if (e.target === e.currentTarget) {
704
+ getInputProps()?.onClick?.(e);
705
+ }
695
706
  onClick?.(e);
696
707
  },
697
708
  onKeyDown,
@@ -709,19 +720,17 @@ const SearchableDropdown = React.forwardRef(
709
720
  ariaLabelSelectedItem,
710
721
  floatingStyles,
711
722
  getItemProps,
723
+ getMenuProps,
712
724
  highlightedIndex,
713
725
  isOpen,
714
726
  listItems,
727
+ style: listStyle,
728
+ setListRef: refs.setFloating,
715
729
  loading: loading ?? resolvedItemsLoading,
716
730
  loadingText,
717
731
  noMatchesText,
718
732
  selectedItems: selectedItem !== null ? [selectedItem] : [],
719
- readOnly,
720
- ...getMenuProps({
721
- refKey: "innerRef",
722
- ref: refs.setFloating,
723
- style: listStyle
724
- })
733
+ readOnly
725
734
  }
726
735
  ),
727
736
  ...rest,
@@ -753,9 +762,8 @@ const SearchableDropdown = React.forwardRef(
753
762
  onKeyDown(e) {
754
763
  if (isOpen && e.key === "Tab") {
755
764
  const highlitedItem = listItems[highlightedIndex];
756
- if ((selectOnTab || selectOnBlur) && highlitedItem) {
765
+ if ((selectOnTab || selectOnBlur) && highlitedItem && highlitedItem !== selectedItem) {
757
766
  selectItem(highlitedItem);
758
- setShowSelectedItem(true);
759
767
  }
760
768
  }
761
769
  },
@@ -877,21 +885,15 @@ const MultiSelect = React.forwardRef(
877
885
  ...!hideSelectAll ? [selectAll] : [],
878
886
  ...normalizedItems
879
887
  ]);
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
- );
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
+ };
895
897
  React.useEffect(() => {
896
898
  filterListItems({ inputValue });
897
899
  }, [normalizedItems]);
@@ -956,8 +958,6 @@ const MultiSelect = React.forwardRef(
956
958
  case useCombobox.stateChangeTypes.InputChange: {
957
959
  const leadingWhitespaceTest = /^\s+/g;
958
960
  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, highlightedIndex: hideSelectAll ? 0 : 1 };
982
+ return changes;
983
983
  }
984
984
  default:
985
985
  return changes;
986
986
  }
987
987
  },
988
- [hideSelectAll, normalizedItems, initialItems]
988
+ [hideSelectAll, normalizedItems, filterListItems, initialItems]
989
989
  );
990
990
  const {
991
991
  getInputProps,
@@ -994,6 +994,7 @@ const MultiSelect = React.forwardRef(
994
994
  getMenuProps,
995
995
  getToggleButtonProps,
996
996
  highlightedIndex,
997
+ setHighlightedIndex,
997
998
  inputValue,
998
999
  isOpen,
999
1000
  setInputValue
@@ -1006,6 +1007,8 @@ const MultiSelect = React.forwardRef(
1006
1007
  stateReducer,
1007
1008
  onInputValueChange(changes) {
1008
1009
  updateListItems({ inputValue: changes.inputValue });
1010
+ setHighlightedIndex(hideSelectAll ? 0 : 1);
1011
+ setLastHighlightedIndex(hideSelectAll ? 0 : 1);
1009
1012
  },
1010
1013
  onSelectedItemChange({ selectedItem: clickedItem }) {
1011
1014
  if (!clickedItem) return;
@@ -1029,17 +1032,19 @@ const MultiSelect = React.forwardRef(
1029
1032
  offset(space.extraSmall2),
1030
1033
  shift({ padding: space.extraSmall }),
1031
1034
  size({
1032
- apply({ elements, availableHeight }) {
1033
- elements.floating.style.setProperty(
1034
- "--list-max-height",
1035
- `${clamp(10 * 16, availableHeight, 20 * 16)}px`
1036
- );
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
+ });
1037
1042
  }
1038
1043
  }),
1039
1044
  flip({ fallbackStrategy: "initialPlacement" })
1040
1045
  ]
1041
1046
  });
1042
- useLayoutEffect(() => {
1047
+ useEffect(() => {
1043
1048
  if (isOpen && refs.reference.current && refs.floating.current) {
1044
1049
  return autoUpdate(
1045
1050
  refs.reference.current,
@@ -1069,8 +1074,14 @@ const MultiSelect = React.forwardRef(
1069
1074
  labelId: getLabelProps().id,
1070
1075
  labelProps: getLabelProps(),
1071
1076
  labelTooltip,
1077
+ onBlur: (e) => {
1078
+ setInputValue("");
1079
+ onBlur?.(e);
1080
+ },
1072
1081
  onClick: (e) => {
1073
- if (e.target === e.currentTarget) getInputProps()?.onClick?.(e);
1082
+ if (e.target === e.currentTarget) {
1083
+ getInputProps()?.onClick?.(e);
1084
+ }
1074
1085
  onClick?.(e);
1075
1086
  },
1076
1087
  onKeyDown,
@@ -1086,22 +1097,19 @@ const MultiSelect = React.forwardRef(
1086
1097
  ariaLabelSelectedItem,
1087
1098
  floatingStyles,
1088
1099
  getItemProps,
1100
+ getMenuProps,
1089
1101
  highlightedIndex,
1090
1102
  isOpen,
1091
1103
  listItems,
1104
+ style: listStyle,
1105
+ setListRef: refs.setFloating,
1092
1106
  loading: loading ?? resolvedItemsLoading,
1093
1107
  loadingText,
1094
1108
  noMatchesText,
1095
1109
  selectAllCheckboxState,
1096
1110
  selectAllItem: selectAll,
1097
1111
  selectedItems,
1098
- readOnly,
1099
- ...getMenuProps({
1100
- "aria-multiselectable": true,
1101
- refKey: "innerRef",
1102
- ref: refs.setFloating,
1103
- style: listStyle
1104
- })
1112
+ readOnly
1105
1113
  }
1106
1114
  ),
1107
1115
  ...rest,
@@ -1160,10 +1168,6 @@ const MultiSelect = React.forwardRef(
1160
1168
  });
1161
1169
  }
1162
1170
  },
1163
- onBlur: (e) => {
1164
- setInputValue("");
1165
- onBlur?.(e);
1166
- },
1167
1171
  ...getDropdownProps({
1168
1172
  preventKeyAction: isOpen,
1169
1173
  value: inputValue ?? EMPTY_INPUT,
@@ -1274,17 +1278,19 @@ const Dropdown = React.forwardRef(
1274
1278
  offset(space.extraSmall2),
1275
1279
  shift({ padding: space.extraSmall }),
1276
1280
  size({
1277
- apply({ elements, availableHeight }) {
1278
- elements.floating.style.setProperty(
1279
- "--list-max-height",
1280
- `${clamp(10 * 16, availableHeight, 20 * 16)}px`
1281
- );
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
+ });
1282
1288
  }
1283
1289
  }),
1284
1290
  flip({ fallbackStrategy: "initialPlacement" })
1285
1291
  ]
1286
1292
  });
1287
- useLayoutEffect(() => {
1293
+ useEffect(() => {
1288
1294
  if (isOpen && refs.reference.current && refs.floating.current) {
1289
1295
  return autoUpdate(
1290
1296
  refs.reference.current,
@@ -1337,19 +1343,17 @@ const Dropdown = React.forwardRef(
1337
1343
  ariaLabelSelectedItem,
1338
1344
  floatingStyles,
1339
1345
  getItemProps,
1346
+ getMenuProps,
1340
1347
  highlightedIndex,
1341
1348
  isOpen,
1342
1349
  listItems: normalizedItems,
1350
+ noMatchesText,
1351
+ style: listStyle,
1352
+ setListRef: refs.setFloating,
1343
1353
  loading: loading ?? resolvedItemsLoading,
1344
1354
  loadingText,
1345
- noMatchesText,
1346
1355
  selectedItems: selectedItem !== null ? [selectedItem] : [],
1347
- readOnly,
1348
- ...getMenuProps({
1349
- refKey: "innerRef",
1350
- ref: refs.setFloating,
1351
- style: listStyle
1352
- })
1356
+ readOnly
1353
1357
  }
1354
1358
  ),
1355
1359
  ...rest,