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