@entur/dropdown 7.3.8 → 8.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  import { useDebounce, mergeRefs, useRandomId, warnAboutMissingStyles } from "@entur/utils";
2
2
  import { jsx, jsxs, Fragment } from "react/jsx-runtime";
3
- import React, { forwardRef, createElement, useState, useRef, useEffect, useCallback } from "react";
3
+ import React, { forwardRef, createElement, useMemo, useState, useRef, useEffect, useCallback, useLayoutEffect } from "react";
4
4
  import { useCombobox, useMultipleSelection, useSelect } from "downshift";
5
5
  import classNames from "classnames";
6
6
  import { useFloating, offset, shift, size, flip, autoUpdate } from "@floating-ui/react-dom";
@@ -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
@@ -408,6 +402,10 @@ const itemToString = (item) => item ? item.label : "";
408
402
  const itemToKey = (item) => item?.label + item?.value;
409
403
  const isFunctionWithQueryArgument = (object) => typeof object === "function" && object.length > 0;
410
404
  const clamp = (val, min = 1, max = 10) => Math.min(Math.max(val, min), max);
405
+ const resetInputState = (changes) => ({
406
+ ...changes,
407
+ inputValue: EMPTY_INPUT
408
+ });
411
409
  const useMultiselectUtils = ({
412
410
  listItems,
413
411
  selectedItems,
@@ -455,11 +453,11 @@ const useMultiselectUtils = ({
455
453
  (selectedItem) => selectedItem.value !== clickedItem.value
456
454
  )
457
455
  );
458
- const selectAllCheckboxState = () => {
456
+ const selectAllCheckboxState = useMemo(() => {
459
457
  if (allListItemsAreSelected) return true;
460
458
  if (someListItemsAreSelected) return "indeterminate";
461
459
  return false;
462
- };
460
+ }, [allListItemsAreSelected, someListItemsAreSelected]);
463
461
  const selectAllUnselectedItemsInListItems = (onChange) => {
464
462
  onChange([...selectedItems, ...unselectedItemsInListItems]);
465
463
  };
@@ -523,7 +521,6 @@ const SearchableDropdown = React.forwardRef(
523
521
  prepend,
524
522
  readOnly = false,
525
523
  selectedItem: value,
526
- selectOnBlur = false,
527
524
  selectOnTab = false,
528
525
  style,
529
526
  variant = "info",
@@ -550,77 +547,56 @@ const SearchableDropdown = React.forwardRef(
550
547
  if (shouldRefetchItems) fetchItems(inputValue2 ?? EMPTY_INPUT);
551
548
  filterListItems({ inputValue: inputValue2 ?? EMPTY_INPUT });
552
549
  };
553
- const resetInputState = ({
554
- changes
555
- }) => {
556
- updateListItems({ inputValue: EMPTY_INPUT });
557
- return {
558
- ...changes,
559
- inputValue: EMPTY_INPUT
560
- };
561
- };
562
550
  const inputHasFocus = typeof document !== "undefined" ? inputRef?.current === document?.activeElement : false;
563
551
  useEffect(() => {
564
552
  filterListItems({ inputValue });
565
553
  }, [normalizedItems]);
566
- useEffect(() => {
567
- if (selectedItem !== null && !inputHasFocus) {
568
- setShowSelectedItem(true);
569
- updateListItems({ inputValue: EMPTY_INPUT });
570
- setInputValue(EMPTY_INPUT);
571
- }
572
- }, []);
573
554
  const stateReducer = useCallback(
574
555
  (state, {
575
556
  type,
576
557
  changes
577
558
  }) => {
578
- if (changes.highlightedIndex !== void 0 && changes?.highlightedIndex >= 0) {
579
- setLastHighlightedIndex(changes?.highlightedIndex);
580
- }
581
559
  switch (type) {
582
560
  // empty input to show selected item and reset dropdown list on item selection
583
561
  case useCombobox.stateChangeTypes.ItemClick:
584
562
  case useCombobox.stateChangeTypes.InputKeyDownEnter:
563
+ return resetInputState(changes);
585
564
  case useCombobox.stateChangeTypes.InputBlur:
586
- return resetInputState({ changes });
565
+ return resetInputState({
566
+ ...changes,
567
+ selectedItem: state.selectedItem
568
+ });
587
569
  case useCombobox.stateChangeTypes.ControlledPropUpdatedSelectedItem:
588
- if (changes.selectedItem !== null && !inputHasFocus)
589
- setShowSelectedItem(true);
590
- return resetInputState({ changes });
570
+ return { ...changes, inputValue: state.inputValue };
591
571
  // remove leading whitespace, select element with spacebar on empty input
592
572
  case useCombobox.stateChangeTypes.InputChange: {
593
- const leadingWhitespaceTest = /^\s+/g;
594
573
  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 };
574
+ if (!isSpacePressedOnEmptyInput)
575
+ return { ...changes, highlightedIndex: 0 };
576
+ const sanitizedInputValue = (changes.inputValue ?? "").replace(
577
+ /^\s+/,
578
+ EMPTY_INPUT
579
+ );
580
+ if (!state.isOpen)
581
+ return {
582
+ ...changes,
583
+ inputValue: sanitizedInputValue,
584
+ isOpen: true
585
+ };
586
+ const i = changes.highlightedIndex ?? -1;
587
+ if (i >= 0 && i < listItems.length)
588
+ return {
589
+ ...changes,
590
+ inputValue: sanitizedInputValue,
591
+ selectedItem: listItems[i]
592
+ };
593
+ return { ...changes, inputValue: sanitizedInputValue };
618
594
  }
619
595
  default:
620
596
  return changes;
621
597
  }
622
598
  },
623
- [fetchItems, filterListItems, inputHasFocus, resetInputState]
599
+ [listItems, EMPTY_INPUT]
624
600
  );
625
601
  const {
626
602
  isOpen,
@@ -647,9 +623,23 @@ const SearchableDropdown = React.forwardRef(
647
623
  onSelectedItemChange({ selectedItem: newSelectedItem }) {
648
624
  onChange(newSelectedItem);
649
625
  },
626
+ onHighlightedIndexChange: ({ highlightedIndex: highlightedIndex2 }) => {
627
+ if (highlightedIndex2 >= 0) setLastHighlightedIndex(highlightedIndex2);
628
+ },
650
629
  // Accessibility
651
630
  getA11yStatusMessage: (options) => getA11yStatusMessage({ ...options, resultCount: listItems.length })
652
631
  });
632
+ useEffect(() => {
633
+ if (value !== null && !inputHasFocus) {
634
+ setShowSelectedItem(true);
635
+ updateListItems({ inputValue: EMPTY_INPUT });
636
+ setInputValue(EMPTY_INPUT);
637
+ }
638
+ }, [value]);
639
+ const handleOnClear = () => {
640
+ inputRef.current?.focus();
641
+ reset();
642
+ };
653
643
  const { refs, floatingStyles, update } = useFloating({
654
644
  open: isOpen,
655
645
  placement: "bottom-start",
@@ -657,19 +647,17 @@ const SearchableDropdown = React.forwardRef(
657
647
  offset(space.extraSmall2),
658
648
  shift({ padding: space.extraSmall }),
659
649
  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
- });
650
+ apply({ elements, availableHeight }) {
651
+ elements.floating.style.setProperty(
652
+ "--list-max-height",
653
+ `${clamp(10 * 16, availableHeight, 20 * 16)}px`
654
+ );
667
655
  }
668
656
  }),
669
657
  flip({ fallbackStrategy: "initialPlacement" })
670
658
  ]
671
659
  });
672
- useEffect(() => {
660
+ useLayoutEffect(() => {
673
661
  if (isOpen && refs.reference.current && refs.floating.current) {
674
662
  return autoUpdate(
675
663
  refs.reference.current,
@@ -678,10 +666,39 @@ const SearchableDropdown = React.forwardRef(
678
666
  );
679
667
  }
680
668
  }, [isOpen, refs.reference, refs.floating, update]);
681
- const handleOnClear = () => {
682
- inputRef.current?.focus();
683
- reset();
684
- };
669
+ const labelProps = getLabelProps();
670
+ const toggleButtonProps = getToggleButtonProps({
671
+ "aria-busy": !(loading ?? resolvedItemsLoading) ? void 0 : "true"
672
+ });
673
+ const menuProps = getMenuProps({
674
+ refKey: "innerRef",
675
+ ref: refs.setFloating,
676
+ style: listStyle
677
+ });
678
+ const inputProps = getInputProps({
679
+ onKeyDown(e) {
680
+ if (isOpen && e.key === "Tab") {
681
+ const highlitedItem = listItems[highlightedIndex];
682
+ if (selectOnTab && highlitedItem) {
683
+ selectItem(highlitedItem);
684
+ setShowSelectedItem(true);
685
+ }
686
+ }
687
+ },
688
+ onBlur(e) {
689
+ if (selectedItem !== null) setShowSelectedItem(true);
690
+ onBlur?.(e);
691
+ },
692
+ onFocus(e) {
693
+ if (!readOnly) setShowSelectedItem(false);
694
+ onFocus?.(e);
695
+ },
696
+ disabled,
697
+ readOnly,
698
+ placeholder: selectedItem?.label ?? placeholder,
699
+ tabIndex: disabled || readOnly ? -1 : void 0,
700
+ ref: mergeRefs(inputRef, ref)
701
+ });
685
702
  return /* @__PURE__ */ jsxs(
686
703
  BaseFormControl,
687
704
  {
@@ -696,17 +713,14 @@ const SearchableDropdown = React.forwardRef(
696
713
  feedback,
697
714
  isFilled: selectedItem !== null || inputValue !== EMPTY_INPUT,
698
715
  label,
699
- labelId: getLabelProps().id,
700
- labelProps: getLabelProps(),
716
+ labelId: labelProps.id,
717
+ labelProps,
701
718
  labelTooltip,
702
719
  onClick: (e) => {
703
- if (e.target === e.currentTarget) {
704
- getInputProps()?.onClick?.(e);
705
- }
720
+ if (e.target === e.currentTarget) inputProps?.onClick?.(e);
706
721
  onClick?.(e);
707
722
  },
708
723
  onKeyDown,
709
- onFocus,
710
724
  prepend,
711
725
  readOnly,
712
726
  ref: refs.setReference,
@@ -720,17 +734,15 @@ const SearchableDropdown = React.forwardRef(
720
734
  ariaLabelSelectedItem,
721
735
  floatingStyles,
722
736
  getItemProps,
723
- getMenuProps,
724
737
  highlightedIndex,
725
738
  isOpen,
726
739
  listItems,
727
- style: listStyle,
728
- setListRef: refs.setFloating,
729
740
  loading: loading ?? resolvedItemsLoading,
730
741
  loadingText,
731
742
  noMatchesText,
732
743
  selectedItems: selectedItem !== null ? [selectedItem] : [],
733
- readOnly
744
+ readOnly,
745
+ ...menuProps
734
746
  }
735
747
  ),
736
748
  ...rest,
@@ -745,7 +757,7 @@ const SearchableDropdown = React.forwardRef(
745
757
  onClick: (event) => {
746
758
  if (!disabled && !readOnly) {
747
759
  inputRef.current?.focus();
748
- getInputProps()?.onClick?.(event);
760
+ inputProps?.onClick?.(event);
749
761
  }
750
762
  },
751
763
  tabIndex: readOnly ? 0 : -1,
@@ -758,38 +770,13 @@ const SearchableDropdown = React.forwardRef(
758
770
  className: classNames("eds-dropdown__input eds-form-control", {
759
771
  "eds-dropdown__input--hidden": showSelectedItem
760
772
  }),
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
- })
773
+ ...inputProps
785
774
  }
786
775
  ),
787
776
  /* @__PURE__ */ jsx(
788
777
  DropdownFieldAppendix,
789
778
  {
790
- ...getToggleButtonProps({
791
- "aria-busy": !(loading ?? resolvedItemsLoading) ? void 0 : "true"
792
- }),
779
+ ...toggleButtonProps,
793
780
  ariaLabelCloseList,
794
781
  ariaLabelOpenList,
795
782
  clearable,
@@ -834,7 +821,6 @@ const MultiSelect = React.forwardRef(
834
821
  placeholder,
835
822
  readOnly = false,
836
823
  selectedItems = [],
837
- selectOnBlur = false,
838
824
  selectOnTab = false,
839
825
  style,
840
826
  variant = "information",
@@ -865,10 +851,14 @@ const MultiSelect = React.forwardRef(
865
851
  fetchItems
866
852
  } = useResolvedItems(initialItems, debounceTimeout);
867
853
  const isAllNonAsyncItemsSelected = typeof initialItems !== "function" && selectedItems.length === normalizedItems.length;
868
- const selectAll = {
869
- value: useRandomId("select-all"),
870
- label: labelSelectAll
871
- };
854
+ const selectAllUniqueId = useRandomId("select-all");
855
+ const selectAll = React.useMemo(
856
+ () => ({
857
+ value: selectAllUniqueId,
858
+ label: labelSelectAll
859
+ }),
860
+ [labelSelectAll]
861
+ );
872
862
  const summarySelectedItems = React.useMemo(
873
863
  () => ({
874
864
  value: EMPTY_INPUT,
@@ -885,15 +875,21 @@ const MultiSelect = React.forwardRef(
885
875
  ...!hideSelectAll ? [selectAll] : [],
886
876
  ...normalizedItems
887
877
  ]);
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
- };
878
+ const filterListItems = React.useCallback(
879
+ ({ inputValue: inputValue2 }) => setListItems([
880
+ ...!hideSelectAll ? [selectAll] : [],
881
+ ...normalizedItems.filter((item) => itemFilter(item, inputValue2))
882
+ ]),
883
+ [hideSelectAll, selectAll, normalizedItems, itemFilter]
884
+ );
885
+ const updateListItems = React.useCallback(
886
+ ({ inputValue: inputValue2 }) => {
887
+ const shouldRefetchItems = isFunctionWithQueryArgument(initialItems);
888
+ if (shouldRefetchItems) fetchItems(inputValue2 ?? EMPTY_INPUT);
889
+ filterListItems({ inputValue: inputValue2 ?? EMPTY_INPUT });
890
+ },
891
+ [filterListItems, initialItems, fetchItems]
892
+ );
897
893
  React.useEffect(() => {
898
894
  filterListItems({ inputValue });
899
895
  }, [normalizedItems]);
@@ -925,67 +921,57 @@ const MultiSelect = React.forwardRef(
925
921
  });
926
922
  const stateReducer = React.useCallback(
927
923
  (state, {
928
- changes,
929
- type
924
+ type,
925
+ changes
930
926
  }) => {
931
- if (changes.highlightedIndex !== void 0 && changes?.highlightedIndex >= 0) {
932
- setLastHighlightedIndex(changes?.highlightedIndex);
933
- }
934
927
  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
928
  // keep menu open and edit input value on item selection
942
929
  case useCombobox.stateChangeTypes.InputKeyDownEnter:
943
930
  case useCombobox.stateChangeTypes.ItemClick: {
944
931
  return {
945
932
  ...changes,
946
933
  isOpen: true,
947
- inputValue: clearInputOnSelect ? EMPTY_INPUT : inputRef?.current?.value ?? EMPTY_INPUT
934
+ inputValue: clearInputOnSelect ? EMPTY_INPUT : state.inputValue
948
935
  };
949
936
  }
937
+ // reset input value when leaving input field
938
+ case useCombobox.stateChangeTypes.InputBlur: {
939
+ const { selectedItem: _, ...otherChanges } = changes;
940
+ return resetInputState(otherChanges);
941
+ }
950
942
  // edit input value when selected items is updated outside component
951
943
  case useCombobox.stateChangeTypes.ControlledPropUpdatedSelectedItem: {
952
- return {
953
- ...changes,
954
- inputValue: inputRef?.current?.value ?? EMPTY_INPUT
955
- };
944
+ return { ...changes, inputValue: state.inputValue };
956
945
  }
957
946
  // remove leading whitespace, select item with spacebar if input is empty and filter list items
958
947
  case useCombobox.stateChangeTypes.InputChange: {
959
- const leadingWhitespaceTest = /^\s+/g;
960
948
  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;
949
+ if (!isSpacePressedOnEmptyInput)
950
+ return { ...changes, highlightedIndex: hideSelectAll ? 0 : 1 };
951
+ const sanitizedInputValue = (changes.inputValue ?? "").replace(
952
+ /^\s+/,
953
+ EMPTY_INPUT
954
+ );
955
+ if (!state.isOpen)
956
+ return {
957
+ ...changes,
958
+ inputValue: sanitizedInputValue,
959
+ isOpen: true
960
+ };
961
+ const i = changes.highlightedIndex ?? -1;
962
+ if (i >= 0 && i < listItems.length)
963
+ return {
964
+ ...changes,
965
+ inputValue: sanitizedInputValue,
966
+ selectedItem: listItems[i]
967
+ };
968
+ return { ...changes, inputValue: sanitizedInputValue };
983
969
  }
984
970
  default:
985
971
  return changes;
986
972
  }
987
973
  },
988
- [hideSelectAll, normalizedItems, filterListItems, initialItems]
974
+ [hideSelectAll, listItems, clearInputOnSelect]
989
975
  );
990
976
  const {
991
977
  getInputProps,
@@ -994,10 +980,8 @@ const MultiSelect = React.forwardRef(
994
980
  getMenuProps,
995
981
  getToggleButtonProps,
996
982
  highlightedIndex,
997
- setHighlightedIndex,
998
983
  inputValue,
999
- isOpen,
1000
- setInputValue
984
+ isOpen
1001
985
  } = useCombobox({
1002
986
  defaultHighlightedIndex: lastHighlightedIndex,
1003
987
  // after selection, highlight previously selected item.
@@ -1007,8 +991,6 @@ const MultiSelect = React.forwardRef(
1007
991
  stateReducer,
1008
992
  onInputValueChange(changes) {
1009
993
  updateListItems({ inputValue: changes.inputValue });
1010
- setHighlightedIndex(hideSelectAll ? 0 : 1);
1011
- setLastHighlightedIndex(hideSelectAll ? 0 : 1);
1012
994
  },
1013
995
  onSelectedItemChange({ selectedItem: clickedItem }) {
1014
996
  if (!clickedItem) return;
@@ -1017,6 +999,9 @@ const MultiSelect = React.forwardRef(
1017
999
  onChange: setSelectedItems
1018
1000
  });
1019
1001
  },
1002
+ onHighlightedIndexChange: ({ highlightedIndex: highlightedIndex2 }) => {
1003
+ if (highlightedIndex2 >= 0) setLastHighlightedIndex(highlightedIndex2);
1004
+ },
1020
1005
  // Accessibility
1021
1006
  getA11yStatusMessage: (options) => getA11yStatusMessage({
1022
1007
  ...options,
@@ -1032,19 +1017,17 @@ const MultiSelect = React.forwardRef(
1032
1017
  offset(space.extraSmall2),
1033
1018
  shift({ padding: space.extraSmall }),
1034
1019
  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
- });
1020
+ apply({ elements, availableHeight }) {
1021
+ elements.floating.style.setProperty(
1022
+ "--list-max-height",
1023
+ `${clamp(10 * 16, availableHeight, 20 * 16)}px`
1024
+ );
1042
1025
  }
1043
1026
  }),
1044
1027
  flip({ fallbackStrategy: "initialPlacement" })
1045
1028
  ]
1046
1029
  });
1047
- useEffect(() => {
1030
+ useLayoutEffect(() => {
1048
1031
  if (isOpen && refs.reference.current && refs.floating.current) {
1049
1032
  return autoUpdate(
1050
1033
  refs.reference.current,
@@ -1057,6 +1040,42 @@ const MultiSelect = React.forwardRef(
1057
1040
  inputRef.current?.focus();
1058
1041
  reset();
1059
1042
  };
1043
+ const dropdownProps = getDropdownProps({
1044
+ preventKeyAction: isOpen,
1045
+ value: inputValue ?? EMPTY_INPUT,
1046
+ ref: mergeRefs(inputRef, ref)
1047
+ });
1048
+ const inputProps = getInputProps({
1049
+ onKeyDown: (e) => {
1050
+ if (selectOnTab && isOpen && e.key === "Tab") {
1051
+ const highlitedItem = listItems[highlightedIndex];
1052
+ if (!highlitedItem) return;
1053
+ const shouldSkipTabSelection = clickedItemIsSelectAll(highlitedItem) || !clickedItemIsSelectAll(highlitedItem) && clickedItemIsInSelectedItems(highlitedItem);
1054
+ if (shouldSkipTabSelection) return;
1055
+ handleListItemClicked({
1056
+ clickedItem: highlitedItem,
1057
+ onChange: setSelectedItems
1058
+ });
1059
+ }
1060
+ },
1061
+ onBlur,
1062
+ onFocus,
1063
+ ...dropdownProps,
1064
+ className: "eds-dropdown__input eds-form-control",
1065
+ disabled: readOnly || disabled,
1066
+ placeholder,
1067
+ tabIndex: disabled || readOnly ? -1 : void 0
1068
+ });
1069
+ const labelProps = getLabelProps();
1070
+ const menuProps = getMenuProps({
1071
+ "aria-multiselectable": true,
1072
+ refKey: "innerRef",
1073
+ ref: refs.setFloating,
1074
+ style: listStyle
1075
+ });
1076
+ const toggleButtonProps = getToggleButtonProps({
1077
+ "aria-busy": !(loading ?? resolvedItemsLoading) ? void 0 : "true"
1078
+ });
1060
1079
  return /* @__PURE__ */ jsxs(
1061
1080
  BaseFormControl,
1062
1081
  {
@@ -1071,21 +1090,14 @@ const MultiSelect = React.forwardRef(
1071
1090
  feedback,
1072
1091
  isFilled: hasSelectedItems || inputValue !== EMPTY_INPUT,
1073
1092
  label,
1074
- labelId: getLabelProps().id,
1075
- labelProps: getLabelProps(),
1093
+ labelId: labelProps.id,
1094
+ labelProps,
1076
1095
  labelTooltip,
1077
- onBlur: (e) => {
1078
- setInputValue("");
1079
- onBlur?.(e);
1080
- },
1081
1096
  onClick: (e) => {
1082
- if (e.target === e.currentTarget) {
1083
- getInputProps()?.onClick?.(e);
1084
- }
1097
+ if (e.target === e.currentTarget) inputProps?.onClick?.(e);
1085
1098
  onClick?.(e);
1086
1099
  },
1087
1100
  onKeyDown,
1088
- onFocus,
1089
1101
  readOnly,
1090
1102
  ref: refs.setReference,
1091
1103
  style,
@@ -1097,19 +1109,17 @@ const MultiSelect = React.forwardRef(
1097
1109
  ariaLabelSelectedItem,
1098
1110
  floatingStyles,
1099
1111
  getItemProps,
1100
- getMenuProps,
1101
1112
  highlightedIndex,
1102
1113
  isOpen,
1103
1114
  listItems,
1104
- style: listStyle,
1105
- setListRef: refs.setFloating,
1106
1115
  loading: loading ?? resolvedItemsLoading,
1107
1116
  loadingText,
1108
1117
  noMatchesText,
1109
1118
  selectAllCheckboxState,
1110
1119
  selectAllItem: selectAll,
1111
1120
  selectedItems,
1112
- readOnly
1121
+ readOnly,
1122
+ ...menuProps
1113
1123
  }
1114
1124
  ),
1115
1125
  ...rest,
@@ -1124,7 +1134,7 @@ const MultiSelect = React.forwardRef(
1124
1134
  }
1125
1135
  ),
1126
1136
  children: [
1127
- selectedItems.length > 1 ? /* @__PURE__ */ jsx(VisuallyHidden, { onClick: inputRef.current?.focus, children: ariaLabelJumpToInput }) : null,
1137
+ selectedItems.length > 1 ? /* @__PURE__ */ jsx(VisuallyHidden, { onClick: () => inputRef.current?.focus(), children: ariaLabelJumpToInput }) : null,
1128
1138
  selectedItems.length <= maxChips ? selectedItems.map((selectedItem, index) => /* @__PURE__ */ jsx(
1129
1139
  SelectedItemTag,
1130
1140
  {
@@ -1152,43 +1162,14 @@ const MultiSelect = React.forwardRef(
1152
1162
  selectedItem: summarySelectedItems
1153
1163
  }
1154
1164
  ),
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
- )
1165
+ /* @__PURE__ */ jsx("input", { ...inputProps })
1183
1166
  ]
1184
1167
  }
1185
1168
  ),
1186
1169
  /* @__PURE__ */ jsx(
1187
1170
  DropdownFieldAppendix,
1188
1171
  {
1189
- ...getToggleButtonProps({
1190
- "aria-busy": !(loading ?? resolvedItemsLoading) ? void 0 : "true"
1191
- }),
1172
+ ...toggleButtonProps,
1192
1173
  ariaLabelCloseList,
1193
1174
  ariaLabelOpenList,
1194
1175
  clearable,
@@ -1231,7 +1212,6 @@ const Dropdown = React.forwardRef(
1231
1212
  prepend,
1232
1213
  readOnly = false,
1233
1214
  selectedItem,
1234
- selectOnBlur = false,
1235
1215
  selectOnTab = false,
1236
1216
  style,
1237
1217
  variant = "information",
@@ -1252,22 +1232,20 @@ const Dropdown = React.forwardRef(
1252
1232
  items: normalizedItems,
1253
1233
  defaultHighlightedIndex: selectedItem ? void 0 : 0,
1254
1234
  selectedItem,
1255
- stateReducer(_, { changes, type }) {
1235
+ stateReducer(state, { changes, type }) {
1256
1236
  const toggleButtonIsFocused = typeof document !== "undefined" && document.activeElement === refs.reference.current;
1257
1237
  switch (type) {
1258
1238
  case useSelect.stateChangeTypes.ToggleButtonKeyDownArrowDown:
1259
1239
  case useSelect.stateChangeTypes.ToggleButtonKeyDownArrowUp:
1260
1240
  if (!toggleButtonIsFocused) return { ...changes, isOpen: false };
1241
+ break;
1242
+ case useSelect.stateChangeTypes.ToggleButtonBlur:
1243
+ return { ...changes, selectedItem: state.selectedItem };
1261
1244
  }
1262
1245
  return changes;
1263
1246
  },
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);
1247
+ onSelectedItemChange({ selectedItem: newSelectedItem }) {
1248
+ onChange?.(newSelectedItem);
1271
1249
  },
1272
1250
  itemToString
1273
1251
  });
@@ -1278,19 +1256,17 @@ const Dropdown = React.forwardRef(
1278
1256
  offset(space.extraSmall2),
1279
1257
  shift({ padding: space.extraSmall }),
1280
1258
  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
- });
1259
+ apply({ elements, availableHeight }) {
1260
+ elements.floating.style.setProperty(
1261
+ "--list-max-height",
1262
+ `${clamp(10 * 16, availableHeight, 20 * 16)}px`
1263
+ );
1288
1264
  }
1289
1265
  }),
1290
1266
  flip({ fallbackStrategy: "initialPlacement" })
1291
1267
  ]
1292
1268
  });
1293
- useEffect(() => {
1269
+ useLayoutEffect(() => {
1294
1270
  if (isOpen && refs.reference.current && refs.floating.current) {
1295
1271
  return autoUpdate(
1296
1272
  refs.reference.current,
@@ -1300,9 +1276,35 @@ const Dropdown = React.forwardRef(
1300
1276
  }
1301
1277
  }, [isOpen, refs.reference, refs.floating, update]);
1302
1278
  const handleOnClear = () => {
1303
- reset();
1304
1279
  refs.reference.current?.focus();
1280
+ reset();
1305
1281
  };
1282
+ const labelProps = getLabelProps({
1283
+ isFilled
1284
+ });
1285
+ const toggleButtonProps = getToggleButtonProps({
1286
+ ref: mergeRefs(ref, refs.setReference),
1287
+ "aria-disabled": disabled,
1288
+ "aria-label": disabled ? "Disabled dropdown" : "",
1289
+ disabled,
1290
+ readOnly,
1291
+ label,
1292
+ labelId: labelProps?.id,
1293
+ tabIndex: disabled || readOnly ? -1 : 0,
1294
+ onKeyDown(e) {
1295
+ if (isOpen && e.key === "Tab") {
1296
+ const highlitedItem = normalizedItems[highlightedIndex];
1297
+ if (selectOnTab && highlitedItem && highlitedItem !== selectedItem) {
1298
+ selectItem(highlitedItem);
1299
+ }
1300
+ }
1301
+ }
1302
+ });
1303
+ const menuProps = getMenuProps({
1304
+ refKey: "innerRef",
1305
+ ref: refs.setFloating,
1306
+ style: listStyle
1307
+ });
1306
1308
  return /* @__PURE__ */ jsxs(
1307
1309
  BaseFormControl,
1308
1310
  {
@@ -1311,31 +1313,12 @@ const Dropdown = React.forwardRef(
1311
1313
  }),
1312
1314
  disableLabelAnimation,
1313
1315
  feedback,
1314
- isFilled,
1315
- labelProps: getLabelProps(),
1316
+ labelProps,
1316
1317
  labelTooltip,
1317
1318
  prepend,
1318
1319
  style,
1319
1320
  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
- }),
1321
+ ...toggleButtonProps,
1339
1322
  after: /* @__PURE__ */ jsx(
1340
1323
  DropdownList,
1341
1324
  {
@@ -1343,17 +1326,15 @@ const Dropdown = React.forwardRef(
1343
1326
  ariaLabelSelectedItem,
1344
1327
  floatingStyles,
1345
1328
  getItemProps,
1346
- getMenuProps,
1347
1329
  highlightedIndex,
1348
1330
  isOpen,
1349
1331
  listItems: normalizedItems,
1350
- noMatchesText,
1351
- style: listStyle,
1352
- setListRef: refs.setFloating,
1353
1332
  loading: loading ?? resolvedItemsLoading,
1354
1333
  loadingText,
1334
+ noMatchesText,
1355
1335
  selectedItems: selectedItem !== null ? [selectedItem] : [],
1356
- readOnly
1336
+ readOnly,
1337
+ ...menuProps
1357
1338
  }
1358
1339
  ),
1359
1340
  ...rest,