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