@entur/dropdown 7.3.11 → 8.0.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";
@@ -14,21 +14,21 @@ import { LoadingDots } from "@entur/loader";
14
14
  import { Tooltip } from "@entur/tooltip";
15
15
  const DropdownList = ({
16
16
  ariaLabelChosenSingular = "valgt",
17
- ariaLabelSelectedItem = ", valgt element, trykk for å fjerne",
17
+ ariaLabelSelectedItem = ", valgt element",
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 = ({
@@ -192,6 +185,7 @@ const SelectedItemTag = ({
192
185
  removeSelectedItem,
193
186
  selectedItem
194
187
  }) => {
188
+ if (!selectedItem) return null;
195
189
  const { tabIndex: _, ...selectedItemProps } = getSelectedItemProps?.({
196
190
  selectedItem,
197
191
  index
@@ -329,25 +323,25 @@ const useNormalizedItems = (items) => React.useMemo(
329
323
  );
330
324
  const useResolvedItems = (itemsOrItemsResolver, debounceTimeout = 250) => {
331
325
  const itemsIsAFunction = typeof itemsOrItemsResolver === "function";
332
- const [items, setItems] = React.useState(
333
- itemsIsAFunction ? [] : itemsOrItemsResolver
334
- );
326
+ const [resolvedItems, setResolvedItems] = React.useState([]);
335
327
  const [loading, setLoading] = React.useState(false);
336
328
  const abortControllerRef = React.useRef(
337
329
  new AbortController()
338
330
  );
339
331
  const itemsResolver = React.useMemo(() => {
340
- if (itemsIsAFunction)
332
+ if (itemsIsAFunction) {
341
333
  return itemsOrItemsResolver;
342
- return () => Promise.resolve(itemsOrItemsResolver);
334
+ }
335
+ return null;
343
336
  }, [itemsOrItemsResolver, itemsIsAFunction]);
344
337
  const updateItems = async (inputValue) => {
338
+ if (!itemsResolver) return;
345
339
  if (abortControllerRef?.current) abortControllerRef?.current?.abort();
346
340
  const abortController = new AbortController();
347
341
  abortControllerRef.current = abortController;
348
342
  setLoading(true);
349
343
  try {
350
- const resolvedItems = await itemsResolver(
344
+ const fetchedItems = await itemsResolver(
351
345
  inputValue ?? "",
352
346
  abortControllerRef
353
347
  );
@@ -361,8 +355,7 @@ const useResolvedItems = (itemsOrItemsResolver, debounceTimeout = 250) => {
361
355
  );
362
356
  return;
363
357
  }
364
- setLoading(false);
365
- setItems(resolvedItems);
358
+ setResolvedItems(fetchedItems);
366
359
  } catch (error2) {
367
360
  if (error2 && typeof error2 === "object" && "name" in error2 && error2.name === "AbortError") {
368
361
  return;
@@ -371,9 +364,12 @@ const useResolvedItems = (itemsOrItemsResolver, debounceTimeout = 250) => {
371
364
  "The following error was received but not handled inside Entur Designsystems useResolvedItems hook:"
372
365
  );
373
366
  throw error2;
367
+ } finally {
368
+ setLoading(false);
374
369
  }
375
370
  };
376
371
  const debouncedFetchItems = useDebounce(updateItems, debounceTimeout);
372
+ const items = itemsIsAFunction ? resolvedItems : itemsOrItemsResolver;
377
373
  const normalizedItems = useNormalizedItems(items);
378
374
  React.useEffect(() => {
379
375
  return () => abortControllerRef?.current?.abort("Component unmounted");
@@ -408,6 +404,10 @@ const itemToString = (item) => item ? item.label : "";
408
404
  const itemToKey = (item) => item?.label + item?.value;
409
405
  const isFunctionWithQueryArgument = (object) => typeof object === "function" && object.length > 0;
410
406
  const clamp = (val, min = 1, max = 10) => Math.min(Math.max(val, min), max);
407
+ const resetInputState = (changes) => ({
408
+ ...changes,
409
+ inputValue: EMPTY_INPUT
410
+ });
411
411
  const useMultiselectUtils = ({
412
412
  listItems,
413
413
  selectedItems,
@@ -455,11 +455,11 @@ const useMultiselectUtils = ({
455
455
  (selectedItem) => selectedItem.value !== clickedItem.value
456
456
  )
457
457
  );
458
- const selectAllCheckboxState = () => {
458
+ const selectAllCheckboxState = useMemo(() => {
459
459
  if (allListItemsAreSelected) return true;
460
460
  if (someListItemsAreSelected) return "indeterminate";
461
461
  return false;
462
- };
462
+ }, [allListItemsAreSelected, someListItemsAreSelected]);
463
463
  const selectAllUnselectedItemsInListItems = (onChange) => {
464
464
  onChange([...selectedItems, ...unselectedItemsInListItems]);
465
465
  };
@@ -523,7 +523,6 @@ const SearchableDropdown = React.forwardRef(
523
523
  prepend,
524
524
  readOnly = false,
525
525
  selectedItem: value,
526
- selectOnBlur = false,
527
526
  selectOnTab = false,
528
527
  style,
529
528
  variant = "info",
@@ -550,77 +549,65 @@ const SearchableDropdown = React.forwardRef(
550
549
  if (shouldRefetchItems) fetchItems(inputValue2 ?? EMPTY_INPUT);
551
550
  filterListItems({ inputValue: inputValue2 ?? EMPTY_INPUT });
552
551
  };
553
- const resetInputState = ({
554
- changes
555
- }) => {
556
- updateListItems({ inputValue: EMPTY_INPUT });
557
- return {
558
- ...changes,
559
- inputValue: EMPTY_INPUT
560
- };
561
- };
562
552
  const inputHasFocus = typeof document !== "undefined" ? inputRef?.current === document?.activeElement : false;
563
553
  useEffect(() => {
564
554
  filterListItems({ inputValue });
565
555
  }, [normalizedItems]);
566
- useEffect(() => {
567
- if (selectedItem !== null && !inputHasFocus) {
568
- setShowSelectedItem(true);
569
- updateListItems({ inputValue: EMPTY_INPUT });
570
- setInputValue(EMPTY_INPUT);
571
- }
572
- }, []);
573
556
  const stateReducer = useCallback(
574
557
  (state, {
575
558
  type,
576
559
  changes
577
560
  }) => {
578
- if (changes.highlightedIndex !== void 0 && changes?.highlightedIndex >= 0) {
579
- setLastHighlightedIndex(changes?.highlightedIndex);
580
- }
581
561
  switch (type) {
582
562
  // empty input to show selected item and reset dropdown list on item selection
583
563
  case useCombobox.stateChangeTypes.ItemClick:
584
- case useCombobox.stateChangeTypes.InputKeyDownEnter:
585
- case useCombobox.stateChangeTypes.InputBlur:
586
- return resetInputState({ changes });
587
- case useCombobox.stateChangeTypes.ControlledPropUpdatedSelectedItem:
588
- if (changes.selectedItem !== null && !inputHasFocus)
589
- setShowSelectedItem(true);
590
- return resetInputState({ changes });
564
+ case useCombobox.stateChangeTypes.InputKeyDownEnter: {
565
+ return resetInputState(changes);
566
+ }
567
+ case useCombobox.stateChangeTypes.InputBlur: {
568
+ return resetInputState({
569
+ ...changes,
570
+ selectedItem: state.selectedItem
571
+ });
572
+ }
573
+ case useCombobox.stateChangeTypes.InputKeyDownEscape: {
574
+ return {
575
+ ...changes,
576
+ selectedItem: clearable && !state.isOpen ? null : state.selectedItem
577
+ };
578
+ }
579
+ case useCombobox.stateChangeTypes.ControlledPropUpdatedSelectedItem: {
580
+ return { ...changes, inputValue: state.inputValue };
581
+ }
591
582
  // remove leading whitespace, select element with spacebar on empty input
592
583
  case useCombobox.stateChangeTypes.InputChange: {
593
- const leadingWhitespaceTest = /^\s+/g;
594
584
  const isSpacePressedOnEmptyInput = changes.inputValue === " ";
595
- if (!isSpacePressedOnEmptyInput) setLastHighlightedIndex(0);
596
- if (changes.inputValue?.match(leadingWhitespaceTest)) {
597
- const sanitizedInputValue = changes.inputValue.replace(
598
- leadingWhitespaceTest,
599
- EMPTY_INPUT
600
- );
601
- if (isSpacePressedOnEmptyInput) {
602
- if (!state.isOpen)
603
- return {
604
- ...changes,
605
- inputValue: sanitizedInputValue,
606
- isOpen: true
607
- };
608
- if (changes.highlightedIndex !== void 0) {
609
- return {
610
- ...changes,
611
- inputValue: sanitizedInputValue,
612
- selectedItem: listItems[changes.highlightedIndex]
613
- };
614
- }
615
- }
616
- }
617
- return { ...changes, highlightedIndex: 0 };
585
+ if (!isSpacePressedOnEmptyInput)
586
+ return { ...changes, highlightedIndex: 0 };
587
+ const sanitizedInputValue = (changes.inputValue ?? "").replace(
588
+ /^\s+/,
589
+ EMPTY_INPUT
590
+ );
591
+ if (!state.isOpen)
592
+ return {
593
+ ...changes,
594
+ inputValue: sanitizedInputValue,
595
+ isOpen: true
596
+ };
597
+ const i = changes.highlightedIndex ?? -1;
598
+ if (i >= 0 && i < listItems.length)
599
+ return {
600
+ ...changes,
601
+ inputValue: sanitizedInputValue,
602
+ selectedItem: listItems[i]
603
+ };
604
+ return { ...changes, inputValue: sanitizedInputValue };
618
605
  }
619
606
  default:
620
607
  return changes;
621
608
  }
622
609
  },
623
- [fetchItems, filterListItems, inputHasFocus, resetInputState]
610
+ [listItems, EMPTY_INPUT, clearable]
624
611
  );
625
612
  const {
626
613
  isOpen,
@@ -647,9 +634,23 @@ const SearchableDropdown = React.forwardRef(
647
634
  onSelectedItemChange({ selectedItem: newSelectedItem }) {
648
635
  onChange(newSelectedItem);
649
636
  },
637
+ onHighlightedIndexChange: ({ highlightedIndex: highlightedIndex2 }) => {
638
+ if (highlightedIndex2 >= 0) setLastHighlightedIndex(highlightedIndex2);
639
+ },
650
640
  // Accessibility
651
641
  getA11yStatusMessage: (options) => getA11yStatusMessage({ ...options, resultCount: listItems.length })
652
642
  });
643
+ useEffect(() => {
644
+ if (value !== null && !inputHasFocus) {
645
+ setShowSelectedItem(true);
646
+ updateListItems({ inputValue: EMPTY_INPUT });
647
+ setInputValue(EMPTY_INPUT);
648
+ }
649
+ }, [value]);
650
+ const handleOnClear = () => {
651
+ inputRef.current?.focus();
652
+ reset();
653
+ };
653
654
  const { refs, floatingStyles, update } = useFloating({
654
655
  open: isOpen,
655
656
  placement: "bottom-start",
@@ -657,19 +658,17 @@ const SearchableDropdown = React.forwardRef(
657
658
  offset(space.extraSmall2),
658
659
  shift({ padding: space.extraSmall }),
659
660
  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
- });
661
+ apply({ elements, availableHeight }) {
662
+ elements.floating.style.setProperty(
663
+ "--list-max-height",
664
+ `${clamp(10 * 16, availableHeight, 20 * 16)}px`
665
+ );
667
666
  }
668
667
  }),
669
668
  flip({ fallbackStrategy: "initialPlacement" })
670
669
  ]
671
670
  });
672
- useEffect(() => {
671
+ useLayoutEffect(() => {
673
672
  if (isOpen && refs.reference.current && refs.floating.current) {
674
673
  return autoUpdate(
675
674
  refs.reference.current,
@@ -678,10 +677,39 @@ const SearchableDropdown = React.forwardRef(
678
677
  );
679
678
  }
680
679
  }, [isOpen, refs.reference, refs.floating, update]);
681
- const handleOnClear = () => {
682
- inputRef.current?.focus();
683
- reset();
684
- };
680
+ const labelProps = getLabelProps();
681
+ const toggleButtonProps = getToggleButtonProps({
682
+ "aria-busy": !(loading ?? resolvedItemsLoading) ? void 0 : "true"
683
+ });
684
+ const menuProps = getMenuProps({
685
+ refKey: "innerRef",
686
+ ref: refs.setFloating,
687
+ style: listStyle
688
+ });
689
+ const inputProps = getInputProps({
690
+ onKeyDown(e) {
691
+ if (isOpen && e.key === "Tab") {
692
+ const highlitedItem = listItems[highlightedIndex];
693
+ if (selectOnTab && highlitedItem) {
694
+ selectItem(highlitedItem);
695
+ setShowSelectedItem(true);
696
+ }
697
+ }
698
+ },
699
+ onBlur(e) {
700
+ if (selectedItem !== null) setShowSelectedItem(true);
701
+ onBlur?.(e);
702
+ },
703
+ onFocus(e) {
704
+ if (!readOnly) setShowSelectedItem(false);
705
+ onFocus?.(e);
706
+ },
707
+ disabled,
708
+ readOnly,
709
+ placeholder: selectedItem?.label ?? placeholder,
710
+ tabIndex: disabled || readOnly ? -1 : void 0,
711
+ ref: mergeRefs(inputRef, ref)
712
+ });
685
713
  return /* @__PURE__ */ jsxs(
686
714
  BaseFormControl,
687
715
  {
@@ -696,17 +724,14 @@ const SearchableDropdown = React.forwardRef(
696
724
  feedback,
697
725
  isFilled: selectedItem !== null || inputValue !== EMPTY_INPUT,
698
726
  label,
699
- labelId: getLabelProps().id,
700
- labelProps: getLabelProps(),
727
+ labelId: labelProps.id,
728
+ labelProps,
701
729
  labelTooltip,
702
730
  onClick: (e) => {
703
- if (e.target === e.currentTarget) {
704
- getInputProps()?.onClick?.(e);
705
- }
731
+ if (e.target === e.currentTarget) inputProps?.onClick?.(e);
706
732
  onClick?.(e);
707
733
  },
708
734
  onKeyDown,
709
- onFocus,
710
735
  prepend,
711
736
  readOnly,
712
737
  ref: refs.setReference,
@@ -720,17 +745,15 @@ const SearchableDropdown = React.forwardRef(
720
745
  ariaLabelSelectedItem,
721
746
  floatingStyles,
722
747
  getItemProps,
723
- getMenuProps,
724
748
  highlightedIndex,
725
749
  isOpen,
726
750
  listItems,
727
- style: listStyle,
728
- setListRef: refs.setFloating,
729
751
  loading: loading ?? resolvedItemsLoading,
730
752
  loadingText,
731
753
  noMatchesText,
732
754
  selectedItems: selectedItem !== null ? [selectedItem] : [],
733
- readOnly
755
+ readOnly,
756
+ ...menuProps
734
757
  }
735
758
  ),
736
759
  ...rest,
@@ -745,7 +768,7 @@ const SearchableDropdown = React.forwardRef(
745
768
  onClick: (event) => {
746
769
  if (!disabled && !readOnly) {
747
770
  inputRef.current?.focus();
748
- getInputProps()?.onClick?.(event);
771
+ inputProps?.onClick?.(event);
749
772
  }
750
773
  },
751
774
  tabIndex: readOnly ? 0 : -1,
@@ -758,38 +781,13 @@ const SearchableDropdown = React.forwardRef(
758
781
  className: classNames("eds-dropdown__input eds-form-control", {
759
782
  "eds-dropdown__input--hidden": showSelectedItem
760
783
  }),
761
- ...getInputProps({
762
- onKeyDown(e) {
763
- if (isOpen && e.key === "Tab") {
764
- const highlitedItem = listItems[highlightedIndex];
765
- if ((selectOnTab || selectOnBlur) && highlitedItem && highlitedItem !== selectedItem) {
766
- selectItem(highlitedItem);
767
- }
768
- }
769
- },
770
- onBlur(e) {
771
- if (selectedItem !== null) setShowSelectedItem(true);
772
- onBlur?.(e);
773
- },
774
- onFocus() {
775
- if (!readOnly) {
776
- setShowSelectedItem(false);
777
- }
778
- },
779
- disabled,
780
- readOnly,
781
- placeholder: selectedItem?.label ?? placeholder,
782
- tabIndex: disabled || readOnly ? -1 : void 0,
783
- ref: mergeRefs(inputRef, ref)
784
- })
784
+ ...inputProps
785
785
  }
786
786
  ),
787
787
  /* @__PURE__ */ jsx(
788
788
  DropdownFieldAppendix,
789
789
  {
790
- ...getToggleButtonProps({
791
- "aria-busy": !(loading ?? resolvedItemsLoading) ? void 0 : "true"
792
- }),
790
+ ...toggleButtonProps,
793
791
  ariaLabelCloseList,
794
792
  ariaLabelOpenList,
795
793
  clearable,
@@ -834,7 +832,6 @@ const MultiSelect = React.forwardRef(
834
832
  placeholder,
835
833
  readOnly = false,
836
834
  selectedItems = [],
837
- selectOnBlur = false,
838
835
  selectOnTab = false,
839
836
  style,
840
837
  variant = "information",
@@ -865,10 +862,14 @@ const MultiSelect = React.forwardRef(
865
862
  fetchItems
866
863
  } = useResolvedItems(initialItems, debounceTimeout);
867
864
  const isAllNonAsyncItemsSelected = typeof initialItems !== "function" && selectedItems.length === normalizedItems.length;
868
- const selectAll = {
869
- value: useRandomId("select-all"),
870
- label: labelSelectAll
871
- };
865
+ const selectAllUniqueId = useRandomId("select-all");
866
+ const selectAll = React.useMemo(
867
+ () => ({
868
+ value: selectAllUniqueId,
869
+ label: labelSelectAll
870
+ }),
871
+ [labelSelectAll]
872
+ );
872
873
  const summarySelectedItems = React.useMemo(
873
874
  () => ({
874
875
  value: EMPTY_INPUT,
@@ -885,15 +886,21 @@ const MultiSelect = React.forwardRef(
885
886
  ...!hideSelectAll ? [selectAll] : [],
886
887
  ...normalizedItems
887
888
  ]);
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
- };
889
+ const filterListItems = React.useCallback(
890
+ ({ inputValue: inputValue2 }) => setListItems([
891
+ ...!hideSelectAll ? [selectAll] : [],
892
+ ...normalizedItems.filter((item) => itemFilter(item, inputValue2))
893
+ ]),
894
+ [hideSelectAll, selectAll, normalizedItems, itemFilter]
895
+ );
896
+ const updateListItems = React.useCallback(
897
+ ({ inputValue: inputValue2 }) => {
898
+ const shouldRefetchItems = isFunctionWithQueryArgument(initialItems);
899
+ if (shouldRefetchItems) fetchItems(inputValue2 ?? EMPTY_INPUT);
900
+ filterListItems({ inputValue: inputValue2 ?? EMPTY_INPUT });
901
+ },
902
+ [filterListItems, initialItems, fetchItems]
903
+ );
897
904
  React.useEffect(() => {
898
905
  filterListItems({ inputValue });
899
906
  }, [normalizedItems]);
@@ -925,67 +932,57 @@ const MultiSelect = React.forwardRef(
925
932
  });
926
933
  const stateReducer = React.useCallback(
927
934
  (state, {
928
- changes,
929
- type
935
+ type,
936
+ changes
930
937
  }) => {
931
- if (changes.highlightedIndex !== void 0 && changes?.highlightedIndex >= 0) {
932
- setLastHighlightedIndex(changes?.highlightedIndex);
933
- }
934
938
  switch (type) {
935
- // reset input value when leaving input field
936
- case useCombobox.stateChangeTypes.InputBlur:
937
- return {
938
- ...changes,
939
- inputValue: EMPTY_INPUT
940
- };
941
939
  // keep menu open and edit input value on item selection
942
940
  case useCombobox.stateChangeTypes.InputKeyDownEnter:
943
941
  case useCombobox.stateChangeTypes.ItemClick: {
944
942
  return {
945
943
  ...changes,
946
944
  isOpen: true,
947
- inputValue: clearInputOnSelect ? EMPTY_INPUT : inputRef?.current?.value ?? EMPTY_INPUT
945
+ inputValue: clearInputOnSelect ? EMPTY_INPUT : state.inputValue
948
946
  };
949
947
  }
948
+ // reset input value when leaving input field
949
+ case useCombobox.stateChangeTypes.InputBlur: {
950
+ const { selectedItem: _, ...otherChanges } = changes;
951
+ return resetInputState(otherChanges);
952
+ }
950
953
  // edit input value when selected items is updated outside component
951
954
  case useCombobox.stateChangeTypes.ControlledPropUpdatedSelectedItem: {
952
- return {
953
- ...changes,
954
- inputValue: inputRef?.current?.value ?? EMPTY_INPUT
955
- };
955
+ return { ...changes, inputValue: state.inputValue };
956
956
  }
957
957
  // remove leading whitespace, select item with spacebar if input is empty and filter list items
958
958
  case useCombobox.stateChangeTypes.InputChange: {
959
- const leadingWhitespaceTest = /^\s+/g;
960
959
  const isSpacePressedOnEmptyInput = changes.inputValue === " ";
961
- if (changes.inputValue?.match(leadingWhitespaceTest)) {
962
- const sanitizedInputValue = changes.inputValue.replace(
963
- leadingWhitespaceTest,
964
- EMPTY_INPUT
965
- );
966
- if (isSpacePressedOnEmptyInput) {
967
- if (!state.isOpen)
968
- return {
969
- ...changes,
970
- inputValue: sanitizedInputValue,
971
- isOpen: true
972
- };
973
- if (changes.highlightedIndex !== void 0) {
974
- return {
975
- ...changes,
976
- inputValue: sanitizedInputValue,
977
- selectedItem: listItems[changes.highlightedIndex]
978
- };
979
- }
980
- }
981
- }
982
- return changes;
960
+ if (!isSpacePressedOnEmptyInput)
961
+ return { ...changes, highlightedIndex: hideSelectAll ? 0 : 1 };
962
+ const sanitizedInputValue = (changes.inputValue ?? "").replace(
963
+ /^\s+/,
964
+ EMPTY_INPUT
965
+ );
966
+ if (!state.isOpen)
967
+ return {
968
+ ...changes,
969
+ inputValue: sanitizedInputValue,
970
+ isOpen: true
971
+ };
972
+ const i = changes.highlightedIndex ?? -1;
973
+ if (i >= 0 && i < listItems.length)
974
+ return {
975
+ ...changes,
976
+ inputValue: sanitizedInputValue,
977
+ selectedItem: listItems[i]
978
+ };
979
+ return { ...changes, inputValue: sanitizedInputValue };
983
980
  }
984
981
  default:
985
982
  return changes;
986
983
  }
987
984
  },
988
- [hideSelectAll, normalizedItems, filterListItems, initialItems]
985
+ [hideSelectAll, listItems, clearInputOnSelect]
989
986
  );
990
987
  const {
991
988
  getInputProps,
@@ -994,10 +991,8 @@ const MultiSelect = React.forwardRef(
994
991
  getMenuProps,
995
992
  getToggleButtonProps,
996
993
  highlightedIndex,
997
- setHighlightedIndex,
998
994
  inputValue,
999
- isOpen,
1000
- setInputValue
995
+ isOpen
1001
996
  } = useCombobox({
1002
997
  defaultHighlightedIndex: lastHighlightedIndex,
1003
998
  // after selection, highlight previously selected item.
@@ -1007,8 +1002,6 @@ const MultiSelect = React.forwardRef(
1007
1002
  stateReducer,
1008
1003
  onInputValueChange(changes) {
1009
1004
  updateListItems({ inputValue: changes.inputValue });
1010
- setHighlightedIndex(hideSelectAll ? 0 : 1);
1011
- setLastHighlightedIndex(hideSelectAll ? 0 : 1);
1012
1005
  },
1013
1006
  onSelectedItemChange({ selectedItem: clickedItem }) {
1014
1007
  if (!clickedItem) return;
@@ -1017,6 +1010,9 @@ const MultiSelect = React.forwardRef(
1017
1010
  onChange: setSelectedItems
1018
1011
  });
1019
1012
  },
1013
+ onHighlightedIndexChange: ({ highlightedIndex: highlightedIndex2 }) => {
1014
+ if (highlightedIndex2 >= 0) setLastHighlightedIndex(highlightedIndex2);
1015
+ },
1020
1016
  // Accessibility
1021
1017
  getA11yStatusMessage: (options) => getA11yStatusMessage({
1022
1018
  ...options,
@@ -1032,19 +1028,17 @@ const MultiSelect = React.forwardRef(
1032
1028
  offset(space.extraSmall2),
1033
1029
  shift({ padding: space.extraSmall }),
1034
1030
  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
- });
1031
+ apply({ elements, availableHeight }) {
1032
+ elements.floating.style.setProperty(
1033
+ "--list-max-height",
1034
+ `${clamp(10 * 16, availableHeight, 20 * 16)}px`
1035
+ );
1042
1036
  }
1043
1037
  }),
1044
1038
  flip({ fallbackStrategy: "initialPlacement" })
1045
1039
  ]
1046
1040
  });
1047
- useEffect(() => {
1041
+ useLayoutEffect(() => {
1048
1042
  if (isOpen && refs.reference.current && refs.floating.current) {
1049
1043
  return autoUpdate(
1050
1044
  refs.reference.current,
@@ -1057,6 +1051,42 @@ const MultiSelect = React.forwardRef(
1057
1051
  inputRef.current?.focus();
1058
1052
  reset();
1059
1053
  };
1054
+ const dropdownProps = getDropdownProps({
1055
+ preventKeyAction: isOpen,
1056
+ value: inputValue ?? EMPTY_INPUT,
1057
+ ref: mergeRefs(inputRef, ref)
1058
+ });
1059
+ const inputProps = getInputProps({
1060
+ onKeyDown: (e) => {
1061
+ if (selectOnTab && isOpen && e.key === "Tab") {
1062
+ const highlitedItem = listItems[highlightedIndex];
1063
+ if (!highlitedItem) return;
1064
+ const shouldSkipTabSelection = clickedItemIsSelectAll(highlitedItem) || !clickedItemIsSelectAll(highlitedItem) && clickedItemIsInSelectedItems(highlitedItem);
1065
+ if (shouldSkipTabSelection) return;
1066
+ handleListItemClicked({
1067
+ clickedItem: highlitedItem,
1068
+ onChange: setSelectedItems
1069
+ });
1070
+ }
1071
+ },
1072
+ onBlur,
1073
+ onFocus,
1074
+ ...dropdownProps,
1075
+ className: "eds-dropdown__input eds-form-control",
1076
+ disabled: readOnly || disabled,
1077
+ placeholder,
1078
+ tabIndex: disabled || readOnly ? -1 : void 0
1079
+ });
1080
+ const labelProps = getLabelProps();
1081
+ const menuProps = getMenuProps({
1082
+ "aria-multiselectable": true,
1083
+ refKey: "innerRef",
1084
+ ref: refs.setFloating,
1085
+ style: listStyle
1086
+ });
1087
+ const toggleButtonProps = getToggleButtonProps({
1088
+ "aria-busy": !(loading ?? resolvedItemsLoading) ? void 0 : "true"
1089
+ });
1060
1090
  return /* @__PURE__ */ jsxs(
1061
1091
  BaseFormControl,
1062
1092
  {
@@ -1071,21 +1101,14 @@ const MultiSelect = React.forwardRef(
1071
1101
  feedback,
1072
1102
  isFilled: hasSelectedItems || inputValue !== EMPTY_INPUT,
1073
1103
  label,
1074
- labelId: getLabelProps().id,
1075
- labelProps: getLabelProps(),
1104
+ labelId: labelProps.id,
1105
+ labelProps,
1076
1106
  labelTooltip,
1077
- onBlur: (e) => {
1078
- setInputValue("");
1079
- onBlur?.(e);
1080
- },
1081
1107
  onClick: (e) => {
1082
- if (e.target === e.currentTarget) {
1083
- getInputProps()?.onClick?.(e);
1084
- }
1108
+ if (e.target === e.currentTarget) inputProps?.onClick?.(e);
1085
1109
  onClick?.(e);
1086
1110
  },
1087
1111
  onKeyDown,
1088
- onFocus,
1089
1112
  readOnly,
1090
1113
  ref: refs.setReference,
1091
1114
  style,
@@ -1097,19 +1120,17 @@ const MultiSelect = React.forwardRef(
1097
1120
  ariaLabelSelectedItem,
1098
1121
  floatingStyles,
1099
1122
  getItemProps,
1100
- getMenuProps,
1101
1123
  highlightedIndex,
1102
1124
  isOpen,
1103
1125
  listItems,
1104
- style: listStyle,
1105
- setListRef: refs.setFloating,
1106
1126
  loading: loading ?? resolvedItemsLoading,
1107
1127
  loadingText,
1108
1128
  noMatchesText,
1109
1129
  selectAllCheckboxState,
1110
1130
  selectAllItem: selectAll,
1111
1131
  selectedItems,
1112
- readOnly
1132
+ readOnly,
1133
+ ...menuProps
1113
1134
  }
1114
1135
  ),
1115
1136
  ...rest,
@@ -1124,7 +1145,7 @@ const MultiSelect = React.forwardRef(
1124
1145
  }
1125
1146
  ),
1126
1147
  children: [
1127
- selectedItems.length > 1 ? /* @__PURE__ */ jsx(VisuallyHidden, { onClick: inputRef.current?.focus, children: ariaLabelJumpToInput }) : null,
1148
+ selectedItems.length > 1 ? /* @__PURE__ */ jsx(VisuallyHidden, { onClick: () => inputRef.current?.focus(), children: ariaLabelJumpToInput }) : null,
1128
1149
  selectedItems.length <= maxChips ? selectedItems.map((selectedItem, index) => /* @__PURE__ */ jsx(
1129
1150
  SelectedItemTag,
1130
1151
  {
@@ -1152,43 +1173,14 @@ const MultiSelect = React.forwardRef(
1152
1173
  selectedItem: summarySelectedItems
1153
1174
  }
1154
1175
  ),
1155
- /* @__PURE__ */ jsx(
1156
- "input",
1157
- {
1158
- ...getInputProps({
1159
- onKeyDown: (e) => {
1160
- if (selectOnTab && isOpen && e.key === "Tab") {
1161
- const highlitedItem = listItems[highlightedIndex];
1162
- if (!highlitedItem) return;
1163
- const shouldSkipTabSelection = clickedItemIsSelectAll(highlitedItem) || !clickedItemIsSelectAll(highlitedItem) && clickedItemIsInSelectedItems(highlitedItem);
1164
- if (shouldSkipTabSelection) return;
1165
- handleListItemClicked({
1166
- clickedItem: highlitedItem,
1167
- onChange: setSelectedItems
1168
- });
1169
- }
1170
- },
1171
- ...getDropdownProps({
1172
- preventKeyAction: isOpen,
1173
- value: inputValue ?? EMPTY_INPUT,
1174
- ref: mergeRefs(inputRef, ref)
1175
- }),
1176
- className: "eds-dropdown__input eds-form-control",
1177
- disabled: readOnly || disabled,
1178
- placeholder,
1179
- tabIndex: disabled || readOnly ? -1 : void 0
1180
- })
1181
- }
1182
- )
1176
+ /* @__PURE__ */ jsx("input", { ...inputProps })
1183
1177
  ]
1184
1178
  }
1185
1179
  ),
1186
1180
  /* @__PURE__ */ jsx(
1187
1181
  DropdownFieldAppendix,
1188
1182
  {
1189
- ...getToggleButtonProps({
1190
- "aria-busy": !(loading ?? resolvedItemsLoading) ? void 0 : "true"
1191
- }),
1183
+ ...toggleButtonProps,
1192
1184
  ariaLabelCloseList,
1193
1185
  ariaLabelOpenList,
1194
1186
  clearable,
@@ -1214,7 +1206,7 @@ const Dropdown = React.forwardRef(
1214
1206
  ariaLabelOpenList = "Åpne liste med valg",
1215
1207
  ariaLabelSelectedItem,
1216
1208
  className,
1217
- clearable = false,
1209
+ clearable,
1218
1210
  disabled = false,
1219
1211
  disableLabelAnimation,
1220
1212
  feedback,
@@ -1231,7 +1223,6 @@ const Dropdown = React.forwardRef(
1231
1223
  prepend,
1232
1224
  readOnly = false,
1233
1225
  selectedItem,
1234
- selectOnBlur = false,
1235
1226
  selectOnTab = false,
1236
1227
  style,
1237
1228
  variant = "information",
@@ -1252,22 +1243,20 @@ const Dropdown = React.forwardRef(
1252
1243
  items: normalizedItems,
1253
1244
  defaultHighlightedIndex: selectedItem ? void 0 : 0,
1254
1245
  selectedItem,
1255
- stateReducer(_, { changes, type }) {
1246
+ stateReducer(state, { changes, type }) {
1256
1247
  const toggleButtonIsFocused = typeof document !== "undefined" && document.activeElement === refs.reference.current;
1257
1248
  switch (type) {
1258
1249
  case useSelect.stateChangeTypes.ToggleButtonKeyDownArrowDown:
1259
1250
  case useSelect.stateChangeTypes.ToggleButtonKeyDownArrowUp:
1260
1251
  if (!toggleButtonIsFocused) return { ...changes, isOpen: false };
1252
+ break;
1253
+ case useSelect.stateChangeTypes.ToggleButtonBlur:
1254
+ return { ...changes, selectedItem: state.selectedItem };
1261
1255
  }
1262
1256
  return changes;
1263
1257
  },
1264
- onStateChange({ type, selectedItem: newSelectedItem }) {
1265
- switch (type) {
1266
- case useSelect.stateChangeTypes.ToggleButtonBlur:
1267
- if (!selectOnBlur) return;
1268
- }
1269
- if (newSelectedItem === void 0) return;
1270
- onChange?.(newSelectedItem ?? null);
1258
+ onSelectedItemChange({ selectedItem: newSelectedItem }) {
1259
+ onChange?.(newSelectedItem);
1271
1260
  },
1272
1261
  itemToString
1273
1262
  });
@@ -1278,19 +1267,17 @@ const Dropdown = React.forwardRef(
1278
1267
  offset(space.extraSmall2),
1279
1268
  shift({ padding: space.extraSmall }),
1280
1269
  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
- });
1270
+ apply({ elements, availableHeight }) {
1271
+ elements.floating.style.setProperty(
1272
+ "--list-max-height",
1273
+ `${clamp(10 * 16, availableHeight, 20 * 16)}px`
1274
+ );
1288
1275
  }
1289
1276
  }),
1290
1277
  flip({ fallbackStrategy: "initialPlacement" })
1291
1278
  ]
1292
1279
  });
1293
- useEffect(() => {
1280
+ useLayoutEffect(() => {
1294
1281
  if (isOpen && refs.reference.current && refs.floating.current) {
1295
1282
  return autoUpdate(
1296
1283
  refs.reference.current,
@@ -1300,9 +1287,37 @@ const Dropdown = React.forwardRef(
1300
1287
  }
1301
1288
  }, [isOpen, refs.reference, refs.floating, update]);
1302
1289
  const handleOnClear = () => {
1303
- reset();
1304
1290
  refs.reference.current?.focus();
1291
+ reset();
1305
1292
  };
1293
+ const labelProps = getLabelProps({
1294
+ isFilled
1295
+ });
1296
+ const toggleButtonProps = getToggleButtonProps({
1297
+ ref: mergeRefs(ref, refs.setReference),
1298
+ "aria-disabled": disabled,
1299
+ "aria-label": disabled ? "Disabled dropdown" : "",
1300
+ disabled,
1301
+ readOnly,
1302
+ label,
1303
+ labelId: labelProps?.id,
1304
+ tabIndex: disabled || readOnly ? -1 : 0,
1305
+ onKeyDown(e) {
1306
+ if (isOpen && e.key === "Tab") {
1307
+ const highlitedItem = normalizedItems[highlightedIndex];
1308
+ if (selectOnTab && highlitedItem && highlitedItem !== selectedItem) {
1309
+ selectItem(highlitedItem);
1310
+ }
1311
+ } else if (!isOpen && e.key === "Escape" && clearable) {
1312
+ reset();
1313
+ }
1314
+ }
1315
+ });
1316
+ const menuProps = getMenuProps({
1317
+ refKey: "innerRef",
1318
+ ref: refs.setFloating,
1319
+ style: listStyle
1320
+ });
1306
1321
  return /* @__PURE__ */ jsxs(
1307
1322
  BaseFormControl,
1308
1323
  {
@@ -1311,31 +1326,12 @@ const Dropdown = React.forwardRef(
1311
1326
  }),
1312
1327
  disableLabelAnimation,
1313
1328
  feedback,
1314
- isFilled,
1315
- labelProps: getLabelProps(),
1329
+ labelProps,
1316
1330
  labelTooltip,
1317
1331
  prepend,
1318
1332
  style,
1319
1333
  variant,
1320
- ...getToggleButtonProps({
1321
- ref: mergeRefs(ref, refs.setReference),
1322
- "aria-disabled": disabled,
1323
- "aria-label": disabled ? "Disabled dropdown" : "",
1324
- disabled,
1325
- readOnly,
1326
- label,
1327
- labelId: getLabelProps()?.id,
1328
- children: void 0,
1329
- tabIndex: disabled || readOnly ? -1 : 0,
1330
- onKeyDown(e) {
1331
- if (isOpen && e.key === "Tab") {
1332
- const highlitedItem = normalizedItems[highlightedIndex];
1333
- if ((selectOnTab || selectOnBlur) && highlitedItem && highlitedItem !== selectedItem) {
1334
- selectItem(highlitedItem);
1335
- }
1336
- }
1337
- }
1338
- }),
1334
+ ...toggleButtonProps,
1339
1335
  after: /* @__PURE__ */ jsx(
1340
1336
  DropdownList,
1341
1337
  {
@@ -1343,17 +1339,15 @@ const Dropdown = React.forwardRef(
1343
1339
  ariaLabelSelectedItem,
1344
1340
  floatingStyles,
1345
1341
  getItemProps,
1346
- getMenuProps,
1347
1342
  highlightedIndex,
1348
1343
  isOpen,
1349
1344
  listItems: normalizedItems,
1350
- noMatchesText,
1351
- style: listStyle,
1352
- setListRef: refs.setFloating,
1353
1345
  loading: loading ?? resolvedItemsLoading,
1354
1346
  loadingText,
1347
+ noMatchesText,
1355
1348
  selectedItems: selectedItem !== null ? [selectedItem] : [],
1356
- readOnly
1349
+ readOnly,
1350
+ ...menuProps
1357
1351
  }
1358
1352
  ),
1359
1353
  ...rest,