@trackunit/react-form-components 1.8.121 → 1.8.123

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.
Files changed (31) hide show
  1. package/index.cjs.js +873 -576
  2. package/index.esm.js +863 -572
  3. package/package.json +9 -8
  4. package/src/components/BaseInput/BaseInput.d.ts +1 -5
  5. package/src/components/BaseSelect/BaseSelect.d.ts +2 -5
  6. package/src/components/BaseSelect/BaseSelect.variants.d.ts +26 -16
  7. package/src/components/BaseSelect/CreatableSelect.d.ts +15 -10
  8. package/src/components/BaseSelect/custom-components/CounterTag.d.ts +15 -0
  9. package/src/components/BaseSelect/custom-components/MultiValue.d.ts +18 -0
  10. package/src/components/BaseSelect/{SelectMenuItem → custom-components/SelectMenuItem}/SelectMenuItem.d.ts +3 -3
  11. package/src/components/BaseSelect/index.d.ts +2 -1
  12. package/src/components/BaseSelect/useCreatableSelect.d.ts +10 -0
  13. package/src/components/BaseSelect/useCustomComponents.d.ts +26 -23
  14. package/src/components/BaseSelect/useMultiMeasure.d.ts +27 -0
  15. package/src/components/BaseSelect/useMultiValueOverflow.d.ts +21 -0
  16. package/src/components/BaseSelect/useSelect.d.ts +22 -39
  17. package/src/components/DropZone/DropZone.d.ts +1 -1
  18. package/src/components/FormGroup/FormGroup.d.ts +2 -2
  19. package/src/components/MultiSelectField/FormFieldSelectAdapterMulti.d.ts +21 -18
  20. package/src/components/MultiSelectField/MultiSelectField.d.ts +9 -4
  21. package/src/components/PhoneField/PhoneBaseInput/PhoneBaseInput.d.ts +2 -2
  22. package/src/components/SelectField/CreatableSelectField.d.ts +5 -3
  23. package/src/components/SelectField/FormFieldSelectAdapter.d.ts +14 -10
  24. package/src/components/SelectField/SelectField.d.ts +6 -9
  25. package/src/components/UrlField/UrlBaseInput/UrlBaseInput.d.ts +2 -1
  26. package/src/components/storybook-utils/sharedArgTypes.d.ts +0 -54
  27. package/src/translation.d.ts +2 -2
  28. package/src/types.d.ts +1 -1
  29. package/src/components/BaseSelect/TagWithWidth.d.ts +0 -16
  30. package/src/components/BaseSelect/TagsContainer.d.ts +0 -51
  31. package/src/components/BaseSelect/useCustomStyles.d.ts +0 -20
package/index.cjs.js CHANGED
@@ -12,10 +12,11 @@ var usehooksTs = require('usehooks-ts');
12
12
  var parsePhoneNumberFromString = require('libphonenumber-js');
13
13
  var ReactSelect = require('react-select');
14
14
  var ReactAsyncSelect = require('react-select/async');
15
+ var esToolkit = require('es-toolkit');
16
+ var tailwindMerge = require('tailwind-merge');
15
17
  var ReactAsyncCreatableSelect = require('react-select/async-creatable');
16
18
  var ReactCreatableSelect = require('react-select/creatable');
17
19
  var sharedUtils = require('@trackunit/shared-utils');
18
- var tailwindMerge = require('tailwind-merge');
19
20
  var reactHookForm = require('react-hook-form');
20
21
  var zod = require('zod');
21
22
 
@@ -49,6 +50,8 @@ var defaultTranslations = {
49
50
  "schedule.label.allDay": "All Day",
50
51
  "schedule.label.day": "Day",
51
52
  "search.placeholder": "Search",
53
+ "select.loadingMessage": "Loading...",
54
+ "select.noOptionsMessage": "No options found",
52
55
  "urlField.error.INVALID_URL": "Please enter a valid URL",
53
56
  "urlField.error.REQUIRED": "The URL is required"
54
57
  };
@@ -434,10 +437,8 @@ const SuffixRenderer = ({ suffix, isInvalid, isWarning, "data-testid": dataTestI
434
437
  * For specific input types make sure to use the corresponding input component.
435
438
  * This is a base used by our other input components such as TextBaseInput, NumberBaseInput, PasswordBaseInput, etc.
436
439
  */
437
- const BaseInput = ({ className, isInvalid = false, "data-testid": dataTestId, prefix, suffix, addonBefore, addonAfter, actions, fieldSize = "medium", nonInteractive = false, inputClassName, placeholder, isWarning = false, type, genericAction, style, ref, required = false, ...rest }) => {
440
+ const BaseInput = ({ className, isInvalid = false, "data-testid": dataTestId, prefix, suffix, addonBefore, addonAfter, actions, fieldSize = "medium", inputClassName, placeholder, isWarning = false, type, genericAction, style, ref, required = false, readOnly = false, disabled = false, ...rest }) => {
438
441
  // Derive final flags
439
- const renderAsDisabled = Boolean(rest.disabled);
440
- const renderAsReadonly = Boolean(rest.readOnly);
441
442
  const beforeContainerRef = react.useRef(null);
442
443
  const afterContainerRef = react.useRef(null);
443
444
  react.useEffect(() => {
@@ -467,19 +468,18 @@ const BaseInput = ({ className, isInvalid = false, "data-testid": dataTestId, pr
467
468
  const innerRef = react.useRef(null);
468
469
  react.useImperativeHandle(ref, () => innerRef.current, []);
469
470
  return (jsxRuntime.jsxs("div", { className: cvaInput$1({
470
- disabled: renderAsDisabled,
471
- readOnly: renderAsReadonly,
471
+ disabled: Boolean(disabled),
472
472
  invalid: isInvalid,
473
473
  isWarning,
474
474
  size: fieldSize,
475
475
  className,
476
- }), "data-testid": dataTestId ? `${dataTestId}-container` : undefined, style: style, children: [jsxRuntime.jsxs("div", { className: cvaAccessoriesContainer({ className: cvaInputItemPlacementManager({ position: "before" }) }), "data-testid": dataTestId ? `${dataTestId}-before-container` : undefined, ref: beforeContainerRef, children: [jsxRuntime.jsx(AddonRenderer, { addon: addonBefore, "data-testid": dataTestId, fieldSize: fieldSize, position: "before" }), jsxRuntime.jsx(PrefixRenderer, { "data-testid": dataTestId, disabled: renderAsDisabled, prefix: prefix, type: type })] }), jsxRuntime.jsx("input", { "aria-required": required, className: cvaInputElement({
476
+ }), "data-testid": dataTestId ? `${dataTestId}-container` : undefined, style: style, children: [jsxRuntime.jsxs("div", { className: cvaAccessoriesContainer({ className: cvaInputItemPlacementManager({ position: "before" }) }), "data-testid": dataTestId ? `${dataTestId}-before-container` : undefined, ref: beforeContainerRef, children: [jsxRuntime.jsx(AddonRenderer, { addon: addonBefore, "data-testid": dataTestId, fieldSize: fieldSize, position: "before" }), jsxRuntime.jsx(PrefixRenderer, { "data-testid": dataTestId, disabled: Boolean(disabled), prefix: prefix, type: type })] }), jsxRuntime.jsx("input", { "aria-required": required, className: cvaInputElement({
477
477
  size: fieldSize,
478
478
  className: cvaInputItemPlacementManager({ position: "span", className: inputClassName }),
479
- }), "data-testid": dataTestId, placeholder: renderAsDisabled ? undefined : placeholder, ref: innerRef, required: required, style: {
479
+ }), "data-testid": dataTestId, placeholder: Boolean(disabled) ? undefined : placeholder, ref: innerRef, required: required, style: {
480
480
  paddingLeft: `calc(var(--before-width, 0px) + ${uiDesignTokens.themeSpacing[2]})`,
481
481
  paddingRight: `calc(var(--after-width, 0px) + ${uiDesignTokens.themeSpacing[2]})`,
482
- }, type: type, ...rest, disabled: renderAsDisabled, readOnly: renderAsReadonly || nonInteractive }), jsxRuntime.jsxs("div", { className: cvaAccessoriesContainer({ className: cvaInputItemPlacementManager({ position: "after" }) }), "data-testid": dataTestId ? `${dataTestId}-after-container` : undefined, ref: afterContainerRef, children: [jsxRuntime.jsx(LockReasonRenderer, { "data-testid": dataTestId + "-disabled", lockReason: rest.disabled }), jsxRuntime.jsx(LockReasonRenderer, { "data-testid": dataTestId + "-readonly", lockReason: Boolean(rest.readOnly) && !Boolean(rest.disabled) ? rest.readOnly : undefined }), jsxRuntime.jsx(GenericActionsRenderer, { fieldSize: fieldSize, genericAction: genericAction, innerRef: innerRef }), jsxRuntime.jsx(SuffixRenderer, { "data-testid": dataTestId, disabled: renderAsDisabled, isInvalid: isInvalid, isWarning: isWarning, suffix: suffix }), actions, jsxRuntime.jsx(AddonRenderer, { addon: addonAfter, "data-testid": dataTestId, fieldSize: fieldSize, position: "after" })] })] }));
482
+ }, type: type, ...rest, disabled: Boolean(disabled), readOnly: Boolean(readOnly) }), jsxRuntime.jsxs("div", { className: cvaAccessoriesContainer({ className: cvaInputItemPlacementManager({ position: "after" }) }), "data-testid": dataTestId ? `${dataTestId}-after-container` : undefined, ref: afterContainerRef, children: [jsxRuntime.jsx(LockReasonRenderer, { "data-testid": dataTestId + "-disabled", lockReason: disabled }), jsxRuntime.jsx(LockReasonRenderer, { "data-testid": dataTestId + "-readonly", lockReason: Boolean(readOnly) && !Boolean(disabled) ? readOnly : undefined }), jsxRuntime.jsx(GenericActionsRenderer, { fieldSize: fieldSize, genericAction: genericAction, innerRef: innerRef }), jsxRuntime.jsx(SuffixRenderer, { "data-testid": dataTestId, disabled: Boolean(disabled), isInvalid: isInvalid, isWarning: isWarning, suffix: suffix }), actions, jsxRuntime.jsx(AddonRenderer, { addon: addonAfter, "data-testid": dataTestId, fieldSize: fieldSize, position: "after" })] })] }));
483
483
  };
484
484
  BaseInput.displayName = "BaseInput";
485
485
 
@@ -652,7 +652,7 @@ const PhoneBaseInput = ({ "data-testid": dataTestId, isInvalid, disabled = false
652
652
  onFocus?.(event);
653
653
  fieldIsFocused.current = true;
654
654
  }, [onFocus]);
655
- return (jsxRuntime.jsx("div", { className: "grid grid-cols-1 gap-2", "data-testid": dataTestId ? `${dataTestId}-container` : null, children: jsxRuntime.jsx(BaseInput, { actions: !disableAction && innerValue && innerValue.length > 0 ? (jsxRuntime.jsx(ActionButton, { "data-testid": dataTestId ? `${dataTestId}-phoneIcon` : undefined, disabled: isInvalid, size: fieldSize ?? undefined, type: "PHONE_NUMBER", value: value?.toString() || "" })) : null, "data-testid": dataTestId ? `${dataTestId}-phoneNumberInput` : undefined, disabled: disabled, fieldSize: fieldSize, id: "phoneInput-number", isInvalid: isInvalid, name: name, onBlur: handleBlur, onChange: handleChange, onFocus: handleFocus, prefix: (countryCode && countryCodeToFlagEmoji(countryCode)) || undefined, readOnly: readOnly, ref: ref, type: "tel", value: innerValue, ...rest }) }));
655
+ return (jsxRuntime.jsx("div", { className: "grid grid-cols-1 gap-2", "data-testid": dataTestId ? `${dataTestId}-container` : null, children: jsxRuntime.jsx(BaseInput, { actions: !disableAction && innerValue && innerValue.length > 0 ? (jsxRuntime.jsx(ActionButton, { "data-testid": dataTestId ? `${dataTestId}-phoneIcon` : undefined, disabled: isInvalid, size: fieldSize, type: "PHONE_NUMBER", value: value?.toString() || "" })) : null, "data-testid": dataTestId ? `${dataTestId}-phoneNumberInput` : undefined, disabled: disabled, fieldSize: fieldSize, id: "phoneInput-number", isInvalid: isInvalid, name: name, onBlur: handleBlur, onChange: handleChange, onFocus: handleFocus, prefix: (countryCode && countryCodeToFlagEmoji(countryCode)) || undefined, readOnly: readOnly, ref: ref, type: "tel", value: innerValue, ...rest }) }));
656
656
  };
657
657
 
658
658
  const cvaTextAreaBaseInput = cssClassVarianceUtilities.cvaMerge([
@@ -702,62 +702,54 @@ const TextAreaBaseInput = ({ id, name, value, rows, disabled, placeholder, readO
702
702
  */
703
703
  const TextBaseInput = ({ ref, ...rest }) => jsxRuntime.jsx(BaseInput, { ref: ref, type: "text", ...rest });
704
704
 
705
- const cvaSelect = cssClassVarianceUtilities.cvaMerge([
706
- "relative",
707
- "flex",
708
- "shadow-sm",
709
- "rounded-lg",
710
- "border-neutral-300",
711
- "hover:border-neutral-400",
712
- "hover:bg-neutral-50",
713
- "bg-white",
714
- "transition",
715
- "text-sm",
716
- "min-h-0",
717
- ], {
705
+ /**
706
+ * The container for the select component — with state styling
707
+ * !This is _the_ place in select styles to manage the aperance of the text and background
708
+ */
709
+ const cvaSelectContainer = cssClassVarianceUtilities.cvaMerge([cvaInputBase(), "relative", "transition", "min-h-0"], {
718
710
  variants: {
719
711
  fieldSize: {
720
- small: ["h-7", "text-xs"],
721
- medium: ["h-8"],
722
- large: ["h-10"],
723
- },
724
- invalid: {
725
- true: "border border-red-600 text-red-600 hover:border-red-600",
726
- false: "",
712
+ small: cvaInputBaseSize({ size: "small" }),
713
+ medium: cvaInputBaseSize({ size: "medium" }),
714
+ large: cvaInputBaseSize({ size: "large" }),
727
715
  },
728
716
  disabled: {
729
- true: "!bg-neutral-100 hover:border-neutral-300",
717
+ true: cvaInputBaseDisabled(),
730
718
  false: "",
731
719
  },
732
- },
733
- defaultVariants: {
734
- invalid: false,
735
- disabled: false,
736
- },
737
- });
738
- const cvaSelectControl = cssClassVarianceUtilities.cvaMerge([], {
739
- variants: {
740
- isDisabled: {
741
- true: "!bg-neutral-100",
720
+ invalid: {
721
+ true: cvaInputBaseInvalid(),
742
722
  false: "",
743
723
  },
744
- prefix: {
745
- true: ["ps-7"],
724
+ focused: {
725
+ true: "outline-native",
746
726
  false: "",
747
727
  },
748
- invalid: {
749
- true: "!border-0",
728
+ readOnly: {
729
+ true: cvaInputBaseReadOnly(),
750
730
  false: "",
751
731
  },
752
- },
753
- defaultVariants: {
754
- isDisabled: false,
755
- prefix: false,
756
- invalid: false,
732
+ defaultVariants: {
733
+ invalid: false,
734
+ disabled: false,
735
+ fieldSize: "medium",
736
+ focused: false,
737
+ readOnly: false,
738
+ },
757
739
  },
758
740
  });
759
- const cvaSelectIcon = cssClassVarianceUtilities.cvaMerge([
760
- "mr-2",
741
+ const cvaSelectControl = cssClassVarianceUtilities.cvaMerge(["px-3", "gap-x-1", "flex", "h-full"]);
742
+ const cvaSelectLoadingMessage = cssClassVarianceUtilities.cvaMerge(["text-neutral-400", "text-center"]);
743
+ const cvaSelectNoOptionsMessage = cssClassVarianceUtilities.cvaMerge(["text-neutral-400", "text-center"]);
744
+ const cvaSelectDropdownIconContainer = cssClassVarianceUtilities.cvaMerge([
745
+ "flex",
746
+ "cursor-pointer",
747
+ "place-items-center",
748
+ "text-neutral-400",
749
+ "hover:text-neutral-500",
750
+ ]);
751
+ const cvaSelectPrefixSuffix = cssClassVarianceUtilities.cvaMerge(["flex", "items-center", "text-neutral-400"]);
752
+ const cvaSelectClearIndicator = cssClassVarianceUtilities.cvaMerge([
761
753
  "flex",
762
754
  "cursor-pointer",
763
755
  "items-center",
@@ -765,39 +757,88 @@ const cvaSelectIcon = cssClassVarianceUtilities.cvaMerge([
765
757
  "text-neutral-400",
766
758
  "hover:text-neutral-500",
767
759
  ]);
768
- const cvaSelectPrefixSuffix = cssClassVarianceUtilities.cvaMerge(["flex", "justify-center", "items-center", "text-neutral-400", "absolute", "inset-y-0"], {
760
+ const cvaSelectDropdownIndicator = cssClassVarianceUtilities.cvaMerge(["transition-transform", "duration-200"], {
769
761
  variants: {
770
- kind: {
771
- prefix: ["pl-3", "left-0"],
772
- suffix: ["pr-3", "right-0"],
762
+ menuIsOpen: {
763
+ true: "rotate-180",
764
+ false: "rotate-0",
773
765
  },
774
766
  },
775
767
  });
776
- const cvaSelectXIcon = cssClassVarianceUtilities.cvaMerge([
777
- "mr-2",
768
+ const cvaSelectValueContainer = cssClassVarianceUtilities.cvaMerge([
778
769
  "flex",
779
- "cursor-pointer",
770
+ "flex-nowrap",
780
771
  "items-center",
781
- "justify-center",
782
- "text-neutral-400",
783
- "hover:text-neutral-500",
784
- "ml-1",
772
+ "grow",
773
+ "overflow-hidden",
774
+ "relative",
775
+ "gap-x-1",
785
776
  ]);
786
- const cvaSelectMenuList = cssClassVarianceUtilities.cvaMerge([], {
777
+ // for the placement of items inside the value container
778
+ const insideValueContainerClasses = cssClassVarianceUtilities.cvaMerge(["col-start-1", "col-end-3", "row-start-1", "row-end-2"]);
779
+ const cvaSelectSingleValue = cssClassVarianceUtilities.cvaMerge([
780
+ insideValueContainerClasses(),
781
+ "max-w-full",
782
+ "overflow-hidden",
783
+ "text-ellipsis",
784
+ "whitespace-nowrap",
785
+ ]);
786
+ const cvaSelectMenu = cssClassVarianceUtilities.cvaMerge([reactComponents.cvaMenu({ limitWidth: false }), "absolute", "w-full"], {
787
787
  variants: {
788
- menuIsOpen: {
789
- true: "animate-fade-in-fast",
790
- false: "animate-fade-out-fast",
788
+ placement: {
789
+ bottom: "top-[calc(100%+var(--spacing-1))]",
790
+ top: "bottom-[calc(-100%+var(--spacing-1))]",
791
791
  },
792
792
  },
793
+ defaultVariants: {
794
+ placement: "bottom",
795
+ },
793
796
  });
794
- const cvaSelectDynamicTagContainer = cssClassVarianceUtilities.cvaMerge(["h-full", "flex", "gap-1", "items-center"], {
797
+ const cvaSelectMenuList = cssClassVarianceUtilities.cvaMerge([reactComponents.cvaMenuList(), "relative", "w-full"]);
798
+ const cvaSelectPlaceholder = cssClassVarianceUtilities.cvaMerge(["absolute", "text-neutral-400"]);
799
+ const cvaSelectIndicatorsContainer = cssClassVarianceUtilities.cvaMerge(["flex", "items-center", "gap-x-1"]);
800
+ const cvaSelectMultiValue = cssClassVarianceUtilities.cvaMerge([], {
795
801
  variants: {
796
- visible: { true: "visible", false: "invisible" },
802
+ hidden: {
803
+ true: "!hidden",
804
+ false: "",
805
+ },
806
+ invisible: {
807
+ true: "invisible",
808
+ false: "",
809
+ },
810
+ },
811
+ defaultVariants: {
812
+ hidden: false,
797
813
  },
798
814
  });
799
- const cvaSelectCounter = cssClassVarianceUtilities.cvaMerge(["overflow-hidden", "whitespace-nowrap"]);
800
- const cvaSelectMenu = cssClassVarianceUtilities.cvaMerge(["relative", "p-1", "grid", "gap-1"]);
815
+
816
+ /**
817
+ * Internal component for displaying a counter badge showing hidden multi-select values.
818
+ * Used for measurement and display when tags overflow the container.
819
+ */
820
+ const CounterTag = ({ fieldSize, hiddenCount, totalCount, ref, className, "data-testid": dataTestId, }) => {
821
+ if (hiddenCount === 0) {
822
+ return null;
823
+ }
824
+ return (jsxRuntime.jsx(reactComponents.Tag, { className: tailwindMerge.twMerge("inline-flex shrink-0", className), color: "neutral", "data-testid": dataTestId ?? "select-counter", ref: ref, size: fieldSize === "small" ? "small" : "medium", children: totalCount > hiddenCount ? `+${hiddenCount}` : hiddenCount }));
825
+ };
826
+
827
+ /**
828
+ * Internal component for rendering multi-select values with measurement support.
829
+ * Uses the measurement state to determine if the value should be displayed or hidden based on available width.
830
+ */
831
+ const MultiValue = ({ data, children, onClose, className, disabled, fieldSize, getOptionPrefix, ref, "data-testid": dataTestId, }) => {
832
+ const optionPrefix = getOptionPrefix ? getOptionPrefix(data) : null;
833
+ const handleOnClose = (e) => {
834
+ if (disabled) {
835
+ return;
836
+ }
837
+ e.stopPropagation();
838
+ onClose?.(e);
839
+ };
840
+ return (jsxRuntime.jsx(reactComponents.Tag, { className: tailwindMerge.twMerge(className, "shrink-0", "inline-flex"), color: disabled ? "neutral" : "white", "data-testid": dataTestId, icon: optionPrefix, onClose: disabled ? undefined : handleOnClose, ref: ref, size: fieldSize === "small" ? "small" : "medium", children: jsxRuntime.jsx("span", { className: "flex items-center gap-1", children: children }) }));
841
+ };
801
842
 
802
843
  /**
803
844
  * Shared CVA for binary control items: Checkbox, RadioItem, ToggleSwitchOption
@@ -1030,13 +1071,13 @@ Checkbox.displayName = "Checkbox";
1030
1071
  * @param {SelectMenuItemProps} props - The props for the SingleSelectMenuItem
1031
1072
  * @returns {ReactElement} SingleSelectMenuItem
1032
1073
  */
1033
- const SingleSelectMenuItem = ({ label, icon, onClick, selected, focused, "data-testid": dataTestId, disabled, optionLabelDescription, optionPrefix, fieldSize, }) => {
1074
+ const SingleSelectMenuItem = ({ label, icon, onClick, selected = false, focused = false, disabled = false, "data-testid": dataTestId, optionLabelDescription, optionPrefix, fieldSize = "medium", }) => {
1034
1075
  return (jsxRuntime.jsx(reactComponents.MenuItem, { "data-testid": dataTestId, disabled: disabled, fieldSize: fieldSize, focused: focused, label: label, onClick: onClick, optionLabelDescription: optionLabelDescription, optionPrefix: react.isValidElement(optionPrefix)
1035
1076
  ? react.cloneElement(optionPrefix, {
1036
1077
  className: "mr-1 flex items-center",
1037
1078
  size: "medium",
1038
1079
  })
1039
- : optionPrefix, prefix: icon, selected: selected, suffix: selected ? jsxRuntime.jsx(reactComponents.Icon, { className: "block text-blue-600", name: "Check", size: "medium" }) : undefined }));
1080
+ : optionPrefix, prefix: icon, selected: selected, suffix: selected ? jsxRuntime.jsx(reactComponents.Icon, { color: "primary", name: "Check", size: fieldSize === "large" ? "medium" : "small" }) : undefined }));
1040
1081
  };
1041
1082
  /**
1042
1083
  * A multi select menu item is a basic wrapper around Menu item designed to be used as a multi value render in Select list
@@ -1047,7 +1088,7 @@ const SingleSelectMenuItem = ({ label, icon, onClick, selected, focused, "data-t
1047
1088
  const MultiSelectMenuItem = ({ label, onClick, selected, focused, "data-testid": dataTestId, disabled, optionLabelDescription, optionPrefix, fieldSize, }) => {
1048
1089
  return (jsxRuntime.jsx(reactComponents.MenuItem, { "data-testid": dataTestId, disabled: disabled, fieldSize: fieldSize, focused: focused, label: label, onClick: e => {
1049
1090
  e.stopPropagation();
1050
- onClick && onClick(e);
1091
+ onClick?.(e);
1051
1092
  }, optionLabelDescription: optionLabelDescription, optionPrefix: react.isValidElement(optionPrefix)
1052
1093
  ? react.cloneElement(optionPrefix, {
1053
1094
  className: "mr-1 flex items-center",
@@ -1059,542 +1100,778 @@ const MultiSelectMenuItem = ({ label, onClick, selected, focused, "data-testid":
1059
1100
  };
1060
1101
 
1061
1102
  /**
1062
- * Extended Tag component with information about its own width.
1063
- * Used in the select component.
1103
+ * Custom hook to measure the geometry of multiple elements indexed by number.
1104
+ * Similar to useMeasure but handles multiple elements efficiently with a single ResizeObserver.
1064
1105
  *
1065
- * @param {TagProps} props - The props for the tag component
1066
- * @returns {ReactElement} TagWithWidth component
1067
- */
1068
- const TagWithWidth = ({ onWidthKnown, children, ...rest }) => {
1069
- const ref = react.useRef(null);
1070
- react.useLayoutEffect(() => {
1071
- onWidthKnown && onWidthKnown({ width: ref.current?.offsetWidth || 0 });
1072
- }, [ref, onWidthKnown]);
1073
- return (jsxRuntime.jsx(reactComponents.Tag, { ref: ref, ...rest, icon: react.isValidElement(rest.icon) ? react.cloneElement(rest.icon, { size: "small" }) : rest.icon, children: children }));
1074
- };
1075
-
1076
- /**
1077
- * TagsContainer component to display tags in limited space when children can't fit space it displays counter
1078
- *
1079
- * @param {TagsContainerProps} props - The props for the TagContainer
1080
- * @returns {ReactElement} TagsContainer
1081
- */
1082
- const TagsContainer = ({ items, width = "100%", itemsGap = 6, postFix, preFix, disabled, }) => {
1083
- const containerRef = react.useRef(null);
1084
- const [isReady, setIsReady] = react.useState(false);
1085
- const [counterWidth, setCounterWidth] = react.useState(0);
1086
- const [availableSpaceWidth, setAvailableSpaceWidth] = react.useState(0);
1087
- const [childrenWidths, setChildrenWidths] = react.useState([]);
1088
- const itemsCount = items.length;
1089
- const dimensions = reactComponents.useResize();
1090
- const { width: windowWidth } = reactComponents.useDebounce(dimensions, { delay: 100 });
1106
+ * @param {UseMultiMeasureOptions} options - Configuration options
1107
+ * @returns {UseMultiMeasureResult} An object containing `geometries` Map and `getRef` function to create refs
1108
+ * @example
1109
+ * ```tsx
1110
+ * const { geometries, getRef } = useMultiMeasure({
1111
+ * onChange: (geometries) => console.log('Geometries changed', geometries)
1112
+ * });
1113
+ * return items.map((item, index) => (
1114
+ * <div key={index} ref={getRef(index)}>Item {index}</div>
1115
+ * ));
1116
+ * ```
1117
+ */
1118
+ const useMultiMeasure = ({ skip = false, onChange } = {}) => {
1119
+ const [geometries, setGeometries] = react.useState(new Map());
1120
+ const [elementCount, setElementCount] = react.useState(0); // Track element count to trigger useLayoutEffect
1121
+ const elementsRef = react.useRef(new Map());
1122
+ const observerRef = react.useRef(null);
1123
+ const onChangeRef = react.useRef(onChange);
1124
+ const refCallbacksRef = react.useRef(new Map());
1125
+ const updateScheduledRef = react.useRef(false);
1091
1126
  react.useEffect(() => {
1092
- const containerWidth = containerRef.current?.offsetWidth || 0;
1093
- setAvailableSpaceWidth(containerWidth);
1094
- }, [windowWidth]);
1095
- const onWidthKnownHandler = react.useCallback(({ width: reportedWidth }) => {
1096
- setChildrenWidths(prev => {
1097
- const next = [...prev, { width: reportedWidth + itemsGap }];
1098
- if (next.length === itemsCount) {
1099
- setIsReady(true);
1127
+ onChangeRef.current = onChange;
1128
+ }, [onChange]);
1129
+ // Update geometries for all currently tracked elements
1130
+ const updateAllGeometries = react.useCallback(() => {
1131
+ // Batch multiple rapid calls (like during unmount/remount) into a single update
1132
+ if (updateScheduledRef.current) {
1133
+ return;
1134
+ }
1135
+ updateScheduledRef.current = true;
1136
+ queueMicrotask(() => {
1137
+ updateScheduledRef.current = false;
1138
+ const newGeometries = new Map();
1139
+ elementsRef.current.forEach((element, index) => {
1140
+ const rect = element.getBoundingClientRect();
1141
+ newGeometries.set(index, {
1142
+ width: rect.width,
1143
+ height: rect.height,
1144
+ top: rect.top,
1145
+ bottom: rect.bottom,
1146
+ left: rect.left,
1147
+ right: rect.right,
1148
+ x: rect.x,
1149
+ y: rect.y,
1150
+ });
1151
+ });
1152
+ setGeometries(prevGeometries => {
1153
+ if (esToolkit.isEqual(prevGeometries, newGeometries)) {
1154
+ return prevGeometries;
1155
+ }
1156
+ onChangeRef.current?.(newGeometries);
1157
+ return newGeometries;
1158
+ });
1159
+ });
1160
+ }, []);
1161
+ // Measure elements after they're mounted (useLayoutEffect runs synchronously after DOM updates)
1162
+ react.useLayoutEffect(() => {
1163
+ if (skip || elementsRef.current.size === 0) {
1164
+ return;
1165
+ }
1166
+ // Initial synchronous measurement - this runs in the same render cycle, so tests don't need waitFor
1167
+ const newGeometries = new Map();
1168
+ elementsRef.current.forEach((element, index) => {
1169
+ const rect = element.getBoundingClientRect();
1170
+ newGeometries.set(index, {
1171
+ width: rect.width,
1172
+ height: rect.height,
1173
+ top: rect.top,
1174
+ bottom: rect.bottom,
1175
+ left: rect.left,
1176
+ right: rect.right,
1177
+ x: rect.x,
1178
+ y: rect.y,
1179
+ });
1180
+ });
1181
+ setGeometries(prevGeometries => {
1182
+ if (esToolkit.isEqual(prevGeometries, newGeometries)) {
1183
+ return prevGeometries;
1100
1184
  }
1101
- return next;
1185
+ onChangeRef.current?.(newGeometries);
1186
+ return newGeometries;
1102
1187
  });
1103
- }, [itemsCount, itemsGap]);
1104
- const renderedElements = react.useMemo(() => {
1105
- const requiredSpace = childrenWidths.reduce((previous, current) => {
1106
- return previous + current.width;
1107
- }, 0);
1108
- const availableSpace = availableSpaceWidth - counterWidth;
1109
- const { elements } = items
1110
- .concat({ text: "", onClick: () => null, disabled: false })
1111
- .reduce((acc, item, index) => {
1112
- const spaceNeeded = childrenWidths.slice(0, index + 1).reduce((previous, current) => {
1113
- return previous + current.width;
1114
- }, 0);
1115
- const isLast = index === items.length;
1116
- const counterRequired = requiredSpace > availableSpace && acc.counter !== 0;
1117
- if (isLast && counterRequired) {
1118
- return {
1119
- ...acc,
1120
- elements: [
1121
- ...acc.elements,
1122
- jsxRuntime.jsx(TagWithWidth, { color: "white", disabled: disabled, icon: item.Icon, onWidthKnown: ({ width: reportedWidth }) => setCounterWidth(reportedWidth), children: jsxRuntime.jsxs("div", { className: cvaSelectCounter(), "data-testid": "select-counter", children: ["+", acc.counter] }) }, item.text + index),
1123
- ],
1124
- };
1188
+ }, [skip, elementCount]);
1189
+ // Set up ResizeObserver for subsequent changes
1190
+ react.useEffect(() => {
1191
+ if (skip) {
1192
+ return;
1193
+ }
1194
+ if (observerRef.current !== null) {
1195
+ observerRef.current.disconnect();
1196
+ observerRef.current = null;
1197
+ }
1198
+ const observer = new ResizeObserver(() => {
1199
+ updateAllGeometries();
1200
+ });
1201
+ // Observe all current elements
1202
+ elementsRef.current.forEach(element => {
1203
+ observer.observe(element);
1204
+ });
1205
+ observerRef.current = observer;
1206
+ return () => {
1207
+ if (observerRef.current !== null) {
1208
+ observerRef.current.disconnect();
1209
+ observerRef.current = null;
1125
1210
  }
1126
- if (isLast) {
1127
- return acc;
1211
+ };
1212
+ }, [skip, updateAllGeometries]);
1213
+ // Create stable ref callbacks per index
1214
+ const getRef = react.useCallback((index) => {
1215
+ const existing = refCallbacksRef.current.get(index);
1216
+ if (existing) {
1217
+ return existing;
1218
+ }
1219
+ const callback = (el) => {
1220
+ if (el) {
1221
+ elementsRef.current.set(index, el);
1222
+ // Observe this new element
1223
+ if (!skip && observerRef.current) {
1224
+ observerRef.current.observe(el);
1225
+ }
1226
+ // Trigger re-render to measure new element
1227
+ setElementCount(elementsRef.current.size);
1128
1228
  }
1129
- const itemCanFit = spaceNeeded <= availableSpace;
1130
- if (itemCanFit) {
1131
- return {
1132
- ...acc,
1133
- elements: [
1134
- ...acc.elements,
1135
- jsxRuntime.jsx(TagWithWidth, { className: "inline-flex shrink-0", color: item.disabled ? "neutral" : "white", "data-testid": `${item.text}-tag`, disabled: disabled, icon: item.Icon, onClose: e => {
1136
- e.stopPropagation();
1137
- item.onClick();
1138
- }, onWidthKnown: onWidthKnownHandler, children: item.text }, item.text + index),
1139
- ],
1140
- };
1229
+ else {
1230
+ elementsRef.current.delete(index);
1231
+ // Trigger re-render to update measurements
1232
+ setElementCount(elementsRef.current.size);
1233
+ }
1234
+ };
1235
+ refCallbacksRef.current.set(index, callback);
1236
+ return callback;
1237
+ }, [skip]);
1238
+ return { geometries, getRef };
1239
+ };
1240
+
1241
+ const DEFAULT_STATE = {
1242
+ phase: "uninitialized",
1243
+ visibleCount: 0,
1244
+ totalCount: 0,
1245
+ withCounter: false,
1246
+ multiValueGeometries: new Map(),
1247
+ availableWidthForTags: 0,
1248
+ };
1249
+ const RESERVED_TEXT_INPUT_WIDTH = 40;
1250
+ const GAP_BEFORE_COUNTER = 4;
1251
+ const GAP_BEFORE_TEXT_INPUT = 4;
1252
+ const measurementReducer = (state, action) => {
1253
+ switch (action.type) {
1254
+ case "UPDATE_GEOMETRIES": {
1255
+ // Only reset phase if the count meaningfully changed from the last completed measurement
1256
+ // Don't reset for hide/show cycles (0 size) - only when actual values added/removed
1257
+ const meaningfulCountChange = action.geometries.size > 0 && action.geometries.size !== state.totalCount;
1258
+ return {
1259
+ ...state,
1260
+ multiValueGeometries: action.geometries,
1261
+ phase: meaningfulCountChange ? "measuring" : state.phase,
1262
+ };
1263
+ }
1264
+ case "SET_AVAILABLE_WIDTH_FOR_TAGS": {
1265
+ // Only update if we have a valid width - prevents temporary 0-width states from breaking measurement
1266
+ const newWidth = action.availableWidthForTags > 0 ? action.availableWidthForTags : state.availableWidthForTags;
1267
+ // If width didn't actually change, don't update state
1268
+ if (newWidth === state.availableWidthForTags) {
1269
+ return state;
1141
1270
  }
1142
1271
  return {
1143
- elements: acc.elements,
1144
- counter: item.text !== "" ? acc.counter + 1 : acc.counter,
1272
+ ...state,
1273
+ availableWidthForTags: newWidth,
1274
+ phase: "measuring",
1145
1275
  };
1146
- }, { elements: [], counter: 0 });
1147
- return elements;
1148
- }, [items, availableSpaceWidth, counterWidth, disabled, onWidthKnownHandler, childrenWidths]);
1149
- return (jsxRuntime.jsxs("div", { className: cvaSelectDynamicTagContainer({ visible: isReady || !!preFix }), ref: containerRef, style: {
1150
- width: `${width}`,
1151
- }, children: [preFix, renderedElements, postFix] }));
1276
+ }
1277
+ case "SET_MEASUREMENTS": {
1278
+ return {
1279
+ ...state,
1280
+ phase: action.phase,
1281
+ visibleCount: action.visibleCount,
1282
+ totalCount: action.totalCount,
1283
+ withCounter: action.withCounter,
1284
+ };
1285
+ }
1286
+ case "RESET": {
1287
+ return DEFAULT_STATE;
1288
+ }
1289
+ case "CHANGE_PHASE": {
1290
+ return {
1291
+ ...state,
1292
+ phase: action.phase,
1293
+ };
1294
+ }
1295
+ default: {
1296
+ throw new Error(`${action} is not known`);
1297
+ }
1298
+ }
1299
+ };
1300
+ /**
1301
+ * Hook to manage multi-value overflow detection and rendering.
1302
+ * Measures which values fit in the container and determines when to show a counter.
1303
+ */
1304
+ const useMultiValueOverflow = ({ skip = false }) => {
1305
+ const [measurementState, dispatch] = react.useReducer(measurementReducer, DEFAULT_STATE);
1306
+ const { ref: setValueContainerRef, geometry: containerGeometry } = reactComponents.useMeasure({
1307
+ skip: skip || measurementState.multiValueGeometries.size === 0,
1308
+ });
1309
+ const { ref: setCounterRef } = reactComponents.useMeasure({
1310
+ skip: skip || measurementState.multiValueGeometries.size === 0,
1311
+ });
1312
+ const { ref: setFakeCounterRef, geometry: fakeCounterGeometry } = reactComponents.useMeasure({
1313
+ skip: skip || measurementState.multiValueGeometries.size === 0,
1314
+ });
1315
+ const [menuElement, setMenuElement] = react.useState(null);
1316
+ const setMenuRef = react.useCallback((element) => {
1317
+ setMenuElement(element);
1318
+ }, []);
1319
+ const menuIsOpen = menuElement !== null;
1320
+ const { getRef: setGeometryRef } = useMultiMeasure({
1321
+ skip,
1322
+ onChange: geometries => {
1323
+ dispatch({ type: "UPDATE_GEOMETRIES", geometries });
1324
+ },
1325
+ });
1326
+ const availableWidthForTags = react.useMemo(() => {
1327
+ if (!containerGeometry || skip) {
1328
+ return 0;
1329
+ }
1330
+ const reservedInputWidth = menuIsOpen ? RESERVED_TEXT_INPUT_WIDTH : 0;
1331
+ let availableWidth = containerGeometry.width - reservedInputWidth - GAP_BEFORE_TEXT_INPUT;
1332
+ if (measurementState.phase === "measuring-with-counter") {
1333
+ availableWidth -= (fakeCounterGeometry?.width ?? 0) + GAP_BEFORE_COUNTER;
1334
+ }
1335
+ return Math.max(0, availableWidth);
1336
+ }, [containerGeometry, fakeCounterGeometry?.width, measurementState.phase, menuIsOpen, skip]);
1337
+ reactComponents.useDebounce(availableWidthForTags, {
1338
+ onBounce: debouncedAvailableWidthForTags => {
1339
+ dispatch({ type: "SET_AVAILABLE_WIDTH_FOR_TAGS", availableWidthForTags: debouncedAvailableWidthForTags });
1340
+ },
1341
+ delay: 60,
1342
+ });
1343
+ // Measurement logic
1344
+ react.useLayoutEffect(() => {
1345
+ if (skip) {
1346
+ return;
1347
+ }
1348
+ const hasInvalidMultiItemGeometry = Array.from(measurementState.multiValueGeometries.values()).some(
1349
+ // Don't measure if any geometry has invalid dimensions (not yet laid out)
1350
+ geo => geo.width === 0 || geo.height === 0);
1351
+ if (!containerGeometry ||
1352
+ containerGeometry.width === 0 ||
1353
+ measurementState.multiValueGeometries.size === 0 ||
1354
+ measurementState.availableWidthForTags <= 0 ||
1355
+ hasInvalidMultiItemGeometry) {
1356
+ // Early return
1357
+ return;
1358
+ }
1359
+ const totalCount = measurementState.multiValueGeometries.size;
1360
+ switch (measurementState.phase) {
1361
+ case "uninitialized": {
1362
+ dispatch({ type: "CHANGE_PHASE", phase: "measuring" });
1363
+ break;
1364
+ }
1365
+ case "measuring": {
1366
+ const visibleCount = getVisibleCountFromGeometries({
1367
+ geometries: measurementState.multiValueGeometries,
1368
+ availableWidth: measurementState.availableWidthForTags,
1369
+ containerX: containerGeometry.x,
1370
+ totalCount,
1371
+ });
1372
+ if (visibleCount === totalCount) {
1373
+ dispatch({ type: "SET_MEASUREMENTS", totalCount, visibleCount, withCounter: false, phase: "complete" });
1374
+ }
1375
+ else {
1376
+ dispatch({
1377
+ type: "SET_MEASUREMENTS",
1378
+ totalCount,
1379
+ visibleCount,
1380
+ withCounter: true,
1381
+ phase: "measuring-with-counter",
1382
+ });
1383
+ }
1384
+ break;
1385
+ }
1386
+ case "measuring-with-counter": {
1387
+ if (!fakeCounterGeometry || fakeCounterGeometry.width === 0) {
1388
+ // Wait another render cycle for the fake counter to be measured
1389
+ return;
1390
+ }
1391
+ const visibleCount = getVisibleCountFromGeometries({
1392
+ geometries: measurementState.multiValueGeometries,
1393
+ availableWidth: measurementState.availableWidthForTags,
1394
+ containerX: containerGeometry.x,
1395
+ totalCount,
1396
+ });
1397
+ dispatch({ type: "SET_MEASUREMENTS", totalCount, visibleCount, withCounter: true, phase: "complete" });
1398
+ break;
1399
+ }
1400
+ case "complete": {
1401
+ break;
1402
+ }
1403
+ default: {
1404
+ throw new Error(`${measurementState.phase} is not known`);
1405
+ }
1406
+ }
1407
+ }, [
1408
+ containerGeometry,
1409
+ containerGeometry?.width,
1410
+ fakeCounterGeometry,
1411
+ fakeCounterGeometry?.width,
1412
+ measurementState,
1413
+ measurementState.phase,
1414
+ measurementState.totalCount,
1415
+ measurementState.visibleCount,
1416
+ skip,
1417
+ ]);
1418
+ // Store current values in a ref so getter functions can stay stable
1419
+ const valuesRef = react.useRef({
1420
+ visibleCount: measurementState.visibleCount,
1421
+ totalCount: measurementState.totalCount,
1422
+ withCounter: measurementState.withCounter,
1423
+ isComplete: measurementState.phase === "complete",
1424
+ });
1425
+ // Use state setter to trigger rerenders when values change
1426
+ // The setter itself is stable, but calling it triggers a rerender
1427
+ const [, setVersion] = react.useState(0);
1428
+ // Update ref and trigger rerender when state changes
1429
+ react.useEffect(() => {
1430
+ valuesRef.current = {
1431
+ visibleCount: measurementState.visibleCount,
1432
+ totalCount: measurementState.totalCount,
1433
+ withCounter: measurementState.withCounter,
1434
+ isComplete: measurementState.phase === "complete",
1435
+ };
1436
+ // Trigger rerender so components can read new values from getters
1437
+ // eslint-disable-next-line react-hooks/set-state-in-effect
1438
+ setVersion(prev => prev + 1);
1439
+ }, [
1440
+ measurementState.visibleCount,
1441
+ measurementState.totalCount,
1442
+ measurementState.withCounter,
1443
+ measurementState.phase,
1444
+ ]);
1445
+ // Create stable getter functions that read from the ref
1446
+ // These functions stay stable (same reference) but always return the latest values
1447
+ const getVisibleCount = react.useCallback(() => valuesRef.current.visibleCount, []);
1448
+ const getTotalCount = react.useCallback(() => valuesRef.current.totalCount, []);
1449
+ const getCounterWidth = react.useCallback(() => valuesRef.current.withCounter, []);
1450
+ const getIsComplete = react.useCallback(() => valuesRef.current.isComplete, []);
1451
+ return react.useMemo(() => ({
1452
+ setValueContainerRef,
1453
+ setCounterRef,
1454
+ setFakeCounterRef,
1455
+ setGeometryRef,
1456
+ setMenuRef,
1457
+ getVisibleCount,
1458
+ getTotalCount,
1459
+ getCounterWidth,
1460
+ getIsComplete,
1461
+ }), [
1462
+ setValueContainerRef,
1463
+ setCounterRef,
1464
+ setFakeCounterRef,
1465
+ setGeometryRef,
1466
+ setMenuRef,
1467
+ getVisibleCount,
1468
+ getTotalCount,
1469
+ getCounterWidth,
1470
+ getIsComplete,
1471
+ ]);
1472
+ };
1473
+ const getVisibleCountFromGeometries = ({ geometries, availableWidth, containerX, totalCount, }) => {
1474
+ const firstGeometry = geometries.get(0);
1475
+ if (firstGeometry === undefined) {
1476
+ throw new Error("First multiValue geometry is not available, this should never happen");
1477
+ }
1478
+ let visibleCount = 0;
1479
+ for (let i = 0; i < totalCount; i++) {
1480
+ const geometry = geometries.get(i);
1481
+ if (geometry === undefined) {
1482
+ throw new Error(`MultiValue geometry at index ${i} is not available, this should never happen`);
1483
+ }
1484
+ const distanceFromFirst = geometry.x + geometry.width - containerX;
1485
+ const fits = distanceFromFirst <= availableWidth;
1486
+ if (fits) {
1487
+ visibleCount = i + 1; //+1 because we want to count, not get the index
1488
+ }
1489
+ }
1490
+ return visibleCount;
1152
1491
  };
1153
1492
 
1154
1493
  /**
1155
1494
  * A hook to retrieve components override object.
1156
1495
  * This complex object includes all the compositional components that are used in react-select. If you wish to overwrite a component, pass in an object with the appropriate namespace.
1157
1496
  *
1158
- * @template IsMulti
1159
- * @template Group
1160
- * @param {Partial<SelectComponents<Option, IsMulti, Group>> | undefined} componentsProps a custom component prop that you can to override defaults
1161
- * @param {boolean} disabled decide to override disabled variant
1162
- * @param {boolean} menuIsOpen menu is open state
1163
- * @param {string} dataTestId a test id
1164
- * @param {number} maxSelectedDisplayCount a number of max display count
1165
- * @param {boolean} hasError decide to override hasError variant
1166
- * @param {ReactNode} prefix a prefix element
1167
- * @returns {Partial<SelectComponents<Option, boolean, GroupBase<Option>>> | undefined} components object to override react-select default components
1168
- */
1169
- const useCustomComponents = ({ componentsProps, disabled, readOnly, setMenuIsEnabled, "data-testid": dataTestId, maxSelectedDisplayCount, prefix, hasError, fieldSize, getOptionLabelDescription, getOptionPrefix, }) => {
1497
+ * @template TOption
1498
+ * @template TIsMulti
1499
+ * @template TGroup
1500
+ * @param {CustomComponentsProps<TOption>} props - The custom components props
1501
+ * @returns {Partial<SelectComponents<TOption, TIsMulti, TGroup>>} components object to override react-select default components
1502
+ */
1503
+ const useCustomComponents = ({ disabled, readOnly, "data-testid": dataTestId, prefix, hasError, fieldSize = "medium", getOptionLabelDescription, getOptionPrefix, className, isMulti, //prefer using the component prop (ala. selectValueContainer.isMulti) inside of customComponents instead of this one.
1504
+ autoComplete, // see https://github.com/JedWatson/react-select/issues/758
1505
+ }) => {
1170
1506
  const [t] = useTranslation();
1507
+ const { setValueContainerRef, setCounterRef, setFakeCounterRef, setGeometryRef, setMenuRef, getVisibleCount, getTotalCount, getCounterWidth, getIsComplete, } = useMultiValueOverflow({ skip: !isMulti });
1508
+ const interactable = react.useMemo(() => !Boolean(disabled) && !Boolean(readOnly), [disabled, readOnly]);
1171
1509
  // perhaps it should not be wrap in memo (causing some issues with opening and closing on mobiles)
1510
+ // Component functions stay stable (reading from refs), so react-select won't lose focus
1172
1511
  const customComponents = react.useMemo(() => {
1173
1512
  return {
1174
- ValueContainer: props => {
1175
- if (props.isMulti && Array.isArray(props.children) && props.children.length > 0) {
1176
- const PLACEHOLDER_KEY = "placeholder";
1177
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1178
- const key = props && props.children && props.children[0] ? props.children[0]?.key : "";
1179
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1180
- const values = props && props.children ? props.children[0] : [];
1181
- const tags = key === PLACEHOLDER_KEY ? [] : values;
1182
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1183
- const searchInput = props && props.children && props.children[1];
1184
- const placeholderElement = Array.isArray(props.children)
1185
- ? props.children.find(child => child && child.key === PLACEHOLDER_KEY)
1186
- : null;
1187
- return (jsxRuntime.jsx(ReactSelect.components.ValueContainer, { ...props, isDisabled: props.selectProps.isDisabled, children: maxSelectedDisplayCount === undefined ? (jsxRuntime.jsx(TagsContainer, { disabled: disabled, items: tags
1188
- ? tags.map(({ props: tagProps }) => {
1189
- const optionPrefix = tagProps.data && getOptionPrefix ? getOptionPrefix(tagProps.data) : null;
1190
- return {
1191
- text: tagProps.children,
1192
- onClick: disabled
1193
- ? undefined
1194
- : (e) => {
1195
- setMenuIsEnabled(false);
1196
- tagProps.removeProps.onClick && tagProps.removeProps.onClick(e);
1197
- },
1198
- disabled: disabled,
1199
- Icon: optionPrefix,
1200
- };
1201
- })
1202
- : [], postFix: searchInput, preFix: placeholderElement ? jsxRuntime.jsx("span", { className: "absolute", children: placeholderElement }) : null, width: "100%" })) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [tags
1203
- ? tags.slice(0, maxSelectedDisplayCount).map(({ props: tagProps }) => {
1204
- return (jsxRuntime.jsx(reactComponents.Tag, { className: "inline-flex shrink-0", color: disabled ? "unknown" : "primary", "data-testid": tagProps.children ? `${tagProps.children.toString()}-tag` : undefined, onClose: e => {
1205
- e.stopPropagation();
1206
- setMenuIsEnabled(false);
1207
- tagProps.removeProps.onClick && tagProps.removeProps.onClick(e);
1208
- }, children: tagProps.children }, tagProps.children?.toString()));
1209
- })
1210
- : null, tags && tags.length > maxSelectedDisplayCount ? (jsxRuntime.jsxs(reactComponents.Tag, { color: "neutral", "data-testid": "counter-tag", children: ["+", tags.length - maxSelectedDisplayCount] })) : null, searchInput, placeholderElement] })) }));
1211
- }
1212
- return (jsxRuntime.jsx(jsxRuntime.Fragment, { children: jsxRuntime.jsx(ReactSelect.components.ValueContainer, { ...props, isDisabled: props.selectProps.isDisabled, children: props.children }) }));
1513
+ SelectContainer: selectContainer => {
1514
+ return (jsxRuntime.jsx(ReactSelect.components.SelectContainer, { ...selectContainer, className: cvaSelectContainer({
1515
+ invalid: hasError,
1516
+ fieldSize,
1517
+ disabled: Boolean(disabled),
1518
+ className: [className, selectContainer.className],
1519
+ readOnly: Boolean(readOnly),
1520
+ focused: selectContainer.isFocused,
1521
+ }), getStyles: getNoStyles, innerProps: {
1522
+ ...selectContainer.innerProps,
1523
+ ...(dataTestId ? { "data-testid": dataTestId } : {}),
1524
+ }, children: selectContainer.children }));
1525
+ },
1526
+ Control: selectControl => {
1527
+ return (jsxRuntime.jsxs(ReactSelect.components.Control, { ...selectControl, className: cvaSelectControl({
1528
+ className: selectControl.className,
1529
+ }), getStyles: getNoStyles, innerProps: Boolean(readOnly)
1530
+ ? // We omit the onMouseDown and onTouchEnd events to allow text selection
1531
+ esToolkit.omit(selectControl.innerProps, ["onMouseDown", "onTouchEnd"])
1532
+ : selectControl.innerProps, children: [prefix !== undefined ? (jsxRuntime.jsx("div", { className: cvaSelectPrefixSuffix(), "data-testid": dataTestId ? `${dataTestId}-prefix` : null, children: prefix })) : null, selectControl.children, typeof disabled === "object" ? (jsxRuntime.jsx("div", { className: cvaSelectPrefixSuffix(), "data-testid": dataTestId ? `${dataTestId}-disabled-locked` : null, children: jsxRuntime.jsx(InputLockReasonTooltip, { ...disabled }) })) : null, typeof readOnly === "object" && !Boolean(disabled) ? (jsxRuntime.jsx("div", { className: cvaSelectPrefixSuffix(), "data-testid": dataTestId ? `${dataTestId}-readonly-locked` : null, children: jsxRuntime.jsx(InputLockReasonTooltip, { ...readOnly }) })) : null] }));
1533
+ },
1534
+ Placeholder: selectPlaceholder => {
1535
+ return (jsxRuntime.jsx(ReactSelect.components.Placeholder, { ...selectPlaceholder, className: cvaSelectPlaceholder({ className: selectPlaceholder.className }), getStyles: getNoStyles, children: selectPlaceholder.children }));
1213
1536
  },
1214
- LoadingIndicator: () => {
1215
- return jsxRuntime.jsx(reactComponents.Spinner, { centering: "vertically", className: "mr-2", size: "small" });
1537
+ LoadingIndicator: selectLoadingIndicator => {
1538
+ return jsxRuntime.jsx(reactComponents.Spinner, { ...selectLoadingIndicator.innerProps, centering: "vertically", size: "small" });
1216
1539
  },
1217
- DropdownIndicator: props => {
1218
- const icon = props.selectProps.menuIsOpen ? (jsxRuntime.jsx(reactComponents.Icon, { name: "ChevronUp", size: "medium" })) : (jsxRuntime.jsx(reactComponents.Icon, { name: "ChevronDown", size: "medium" }));
1219
- return props.selectProps.isLoading || props.selectProps.isDisabled || readOnly ? null : (jsxRuntime.jsx(ReactSelect.components.DropdownIndicator, { ...props, children: jsxRuntime.jsx("div", { className: cvaSelectIcon(), children: icon }) }));
1540
+ DropdownIndicator: selectDropdownIndicator => {
1541
+ if (!interactable || selectDropdownIndicator.selectProps.isLoading) {
1542
+ return null;
1543
+ }
1544
+ return (jsxRuntime.jsx(ReactSelect.components.DropdownIndicator, { ...selectDropdownIndicator, className: cvaSelectDropdownIconContainer({ className: selectDropdownIndicator.className }), getStyles: getNoStyles, children: jsxRuntime.jsx(reactComponents.Icon, { className: cvaSelectDropdownIndicator({ menuIsOpen: selectDropdownIndicator.selectProps.menuIsOpen }), name: "ChevronDown", size: "medium" }) }));
1545
+ },
1546
+ // --------------------------------
1547
+ // Inside Popover👇
1548
+ // --------------------------------
1549
+ ValueContainer: selectValueContainer => {
1550
+ // Read latest overflow values using getters (functions stay stable)
1551
+ const currentVisibleCount = getVisibleCount();
1552
+ const currentTotalCount = getTotalCount();
1553
+ const currentWithCounter = getCounterWidth();
1554
+ const currentIsComplete = getIsComplete();
1555
+ return (jsxRuntime.jsx(ReactSelect.components.ValueContainer, { ...selectValueContainer, className: cvaSelectValueContainer({ className: selectValueContainer.className }), getStyles: getNoStyles, innerProps: {
1556
+ ...selectValueContainer.innerProps,
1557
+ ref: reactComponents.useMergeRefs([setValueContainerRef, selectValueContainer.innerProps?.ref]),
1558
+ }, children: selectValueContainer.isMulti ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [getPlaceholderElement(selectValueContainer.children), currentWithCounter && !currentIsComplete ? (
1559
+ // Render the test-counter-tag in the beginning to make sure it's included in the calculation
1560
+ // will be removed when the calculation is complete
1561
+ jsxRuntime.jsx(CounterTag, { className: "invisible" // Is invisible because we're only putting it here to measure the width
1562
+ , "data-testid": "fake-multiselect-counter", fieldSize: fieldSize, hiddenCount: currentTotalCount - currentVisibleCount, ref: setFakeCounterRef, totalCount: currentTotalCount })) : null, getMultiValueComponents(selectValueContainer.children), currentWithCounter && currentIsComplete ? (
1563
+ // This is the actual tag that will be visible in the UI
1564
+ jsxRuntime.jsx(CounterTag, { "data-testid": dataTestId ? `${dataTestId}-multiselect-counter` : "multiselect-counter", fieldSize: fieldSize, hiddenCount: currentTotalCount - currentVisibleCount, ref: setCounterRef, totalCount: currentTotalCount })) : null, getInputComponent(selectValueContainer.children)] })) : (selectValueContainer.children) }));
1220
1565
  },
1221
1566
  IndicatorSeparator: () => null,
1222
- ClearIndicator: props => {
1223
- if (disabled) {
1567
+ Input: selectInput => {
1568
+ return jsxRuntime.jsx(ReactSelect.components.Input, { ...selectInput, autoComplete: autoComplete });
1569
+ },
1570
+ IndicatorsContainer: selectIndicatorsContainer => {
1571
+ return (jsxRuntime.jsx(ReactSelect.components.IndicatorsContainer, { ...selectIndicatorsContainer, className: cvaSelectIndicatorsContainer({ className: selectIndicatorsContainer.className }), getStyles: getNoStyles, children: selectIndicatorsContainer.children }));
1572
+ },
1573
+ ClearIndicator: selectClearIndicator => {
1574
+ if (Boolean(disabled)) {
1224
1575
  return null;
1225
1576
  }
1226
- return (jsxRuntime.jsx(ReactSelect.components.ClearIndicator, { ...props, innerProps: {
1227
- ...props.innerProps,
1228
- onMouseDown: e => {
1229
- e.preventDefault();
1230
- },
1231
- }, children: jsxRuntime.jsx("div", { className: cvaSelectXIcon(), "data-testid": dataTestId ? `${dataTestId}-XMarkIcon` : null, onClick: props.clearValue, children: jsxRuntime.jsx(reactComponents.Icon, { ariaLabel: t("clearIndicator.icon.tooltip.clearAll"), name: "XCircle", size: "medium" }) }) }));
1577
+ return (jsxRuntime.jsx(ReactSelect.components.ClearIndicator, { ...selectClearIndicator, className: cvaSelectClearIndicator({ className: selectClearIndicator.className }), getStyles: getNoStyles, children: jsxRuntime.jsx(reactComponents.Icon, { ariaLabel: t("clearIndicator.icon.tooltip.clearAll"), "data-testid": `${dataTestId}-XMarkIcon`, name: "XCircle", size: "medium" }) }));
1232
1578
  },
1233
- Control: props => {
1234
- return (jsxRuntime.jsx(ReactSelect.components.Control, { ...props, className: cvaSelectControl({
1235
- isDisabled: props.isDisabled,
1236
- prefix: prefix ? true : false,
1237
- invalid: hasError,
1238
- }) }));
1579
+ LoadingMessage: selectLoadingMessage => {
1580
+ return (jsxRuntime.jsx(ReactSelect.components.LoadingMessage, { ...selectLoadingMessage, className: cvaSelectLoadingMessage({ className: selectLoadingMessage.className }), getStyles: getNoStyles, children: t("select.loadingMessage") }));
1581
+ },
1582
+ NoOptionsMessage: selectNoOptionsMessage => {
1583
+ return (jsxRuntime.jsx(ReactSelect.components.NoOptionsMessage, { ...selectNoOptionsMessage, className: cvaSelectNoOptionsMessage({ className: selectNoOptionsMessage.className }), getStyles: getNoStyles, children: t("select.noOptionsMessage") }));
1584
+ },
1585
+ SingleValue: selectSingleValue => {
1586
+ const optionPrefix = getOptionPrefix ? getOptionPrefix(selectSingleValue.data) : null;
1587
+ return (jsxRuntime.jsx(ReactSelect.components.SingleValue, { ...selectSingleValue, className: cvaSelectSingleValue({
1588
+ className: selectSingleValue.className,
1589
+ }), getStyles: getNoStyles, children: jsxRuntime.jsxs("div", { className: "flex items-center gap-1", "data-testid": dataTestId + "-singleValue", children: [optionPrefix !== null ? optionPrefix : null, selectSingleValue.children, getOptionLabelDescription && getOptionLabelDescription(selectSingleValue.data) ? (jsxRuntime.jsxs("span", { className: "ml-1 text-neutral-400", children: ["(", getOptionLabelDescription(selectSingleValue.data), ")"] })) : null] }) }));
1239
1590
  },
1240
- SingleValue: props => {
1241
- const optionPrefix = getOptionPrefix ? getOptionPrefix(props.data) : null;
1242
- return (jsxRuntime.jsx(ReactSelect.components.SingleValue, { ...props, className: props.isDisabled ? "text-neutral-700" : "", children: jsxRuntime.jsxs("div", { className: "flex items-center gap-1", "data-testid": dataTestId + "-singleValue", children: [optionPrefix !== null ? optionPrefix : null, props.children, getOptionLabelDescription && getOptionLabelDescription(props.data) ? (jsxRuntime.jsxs("span", { className: "ml-1 text-neutral-400", children: ["(", getOptionLabelDescription(props.data), ")"] })) : null] }) }));
1591
+ MultiValueContainer: ({ children }) => children, // Just pass on the children
1592
+ MultiValueRemove: () => null, // is built-in to the MultiValue (tag) component
1593
+ MultiValueLabel: ({ children }) => children, // Just pass on the children
1594
+ MultiValue: selectMultiValue => {
1595
+ // Read latest overflow values using getters (functions stay stable)
1596
+ const currentVisibleCount = getVisibleCount();
1597
+ const currentWithCounter = getCounterWidth();
1598
+ const currentIsComplete = getIsComplete();
1599
+ const index = selectMultiValue.index;
1600
+ return (jsxRuntime.jsx(ReactSelect.components.MultiValue, { ...selectMultiValue, getStyles: getNoStyles, children: jsxRuntime.jsx(MultiValue, { className: cvaSelectMultiValue({
1601
+ hidden: currentIsComplete && index + 1 > currentVisibleCount, // Hide if doesn't fit
1602
+ invisible: currentWithCounter && !currentIsComplete, // Make invisible if measuring with counter. When there's a counter, it would otherwise briefly change layout to make room for the "fake" counter
1603
+ }), data: selectMultiValue.data, "data-testid": dataTestId ? `${dataTestId}-multiValue-${index}` : undefined, disabled: Boolean(disabled), fieldSize: fieldSize, getOptionPrefix: getOptionPrefix, onClose: selectMultiValue.removeProps.onClick, ref: setGeometryRef(index), children: selectMultiValue.children }) }));
1243
1604
  },
1244
- Menu: props => {
1245
- return (jsxRuntime.jsx(ReactSelect.components.Menu, { ...props, className: cvaSelectMenuList({ menuIsOpen: props.selectProps.menuIsOpen }) }));
1605
+ Menu: selectMenu => {
1606
+ return (jsxRuntime.jsx(ReactSelect.components.Menu, { ...selectMenu, className: cvaSelectMenu({ className: selectMenu.className, placement: selectMenu.placement }), getStyles: getNoStyles, innerProps: {
1607
+ ...selectMenu.innerProps,
1608
+ ref: reactComponents.useMergeRefs([setMenuRef, selectMenu.innerProps.ref]),
1609
+ } }));
1246
1610
  },
1247
- Placeholder: props => {
1248
- return (jsxRuntime.jsx(ReactSelect.components.Placeholder, { ...props, className: "!text-neutral-400", children: props.children }));
1611
+ MenuPortal: selectMenuPortal => {
1612
+ return (jsxRuntime.jsx(ReactSelect.components.MenuPortal, { ...selectMenuPortal, className: "!z-overlay", children: selectMenuPortal.children }));
1249
1613
  },
1250
- MenuList: props => {
1251
- return (jsxRuntime.jsx(ReactSelect.components.MenuList, { ...props, innerProps: {
1252
- ...props.innerProps,
1614
+ MenuList: selectMenuList => {
1615
+ return (jsxRuntime.jsx(ReactSelect.components.MenuList, { className: cvaSelectMenuList({
1616
+ className: selectMenuList.className,
1617
+ }), ...selectMenuList, getStyles: getNoStyles, innerProps: {
1618
+ ...selectMenuList.innerProps,
1253
1619
  onScroll: e => {
1254
1620
  const listEl = e.currentTarget;
1255
1621
  if (listEl.scrollTop + listEl.clientHeight >= listEl.scrollHeight &&
1256
- props.selectProps.onMenuScrollToBottom) {
1257
- /Firefox/.test(navigator.userAgent)
1258
- ? props.selectProps.onMenuScrollToBottom(new WheelEvent("scroll"))
1259
- : props.selectProps.onMenuScrollToBottom(new TouchEvent(""));
1622
+ selectMenuList.selectProps.onMenuScrollToBottom) {
1623
+ if (/Firefox/.test(navigator.userAgent)) {
1624
+ selectMenuList.selectProps.onMenuScrollToBottom(new WheelEvent("scroll"));
1625
+ }
1626
+ else {
1627
+ selectMenuList.selectProps.onMenuScrollToBottom(new TouchEvent(""));
1628
+ }
1260
1629
  }
1261
1630
  },
1262
- }, children: props.children }));
1631
+ }, children: selectMenuList.children }));
1263
1632
  },
1264
- Option: props => {
1633
+ Option: selectOption => {
1265
1634
  const componentProps = {
1266
- label: props.label,
1267
- focused: props.isFocused,
1268
- selected: props.isSelected,
1269
- onClick: props.innerProps.onClick,
1635
+ label: selectOption.label,
1636
+ focused: selectOption.isFocused,
1637
+ selected: selectOption.isSelected,
1638
+ onClick: selectOption.innerProps.onClick,
1270
1639
  };
1271
- return (jsxRuntime.jsx(ReactSelect.components.Option, { ...props, innerProps: {
1272
- ...props.innerProps,
1640
+ return (jsxRuntime.jsx(ReactSelect.components.Option, { ...selectOption, innerProps: {
1641
+ ...selectOption.innerProps,
1273
1642
  role: "option",
1274
- onClick: () => { },
1275
- }, children: props.isMulti ? (jsxRuntime.jsx(MultiSelectMenuItem, { ...componentProps, "data-testid": typeof props.label === "string" ? props.label : undefined, disabled: disabled, fieldSize: fieldSize, optionLabelDescription: getOptionLabelDescription?.(props.data), optionPrefix: getOptionPrefix?.(props.data) })) : (jsxRuntime.jsx(SingleSelectMenuItem, { ...componentProps, "data-testid": typeof props.label === "string" ? props.label : undefined, disabled: disabled || props.isDisabled, fieldSize: fieldSize, optionLabelDescription: getOptionLabelDescription?.(props.data), optionPrefix: getOptionPrefix?.(props.data) })) }));
1643
+ }, children: selectOption.isMulti ? (jsxRuntime.jsx(MultiSelectMenuItem, { ...componentProps, "data-testid": typeof selectOption.label === "string" ? selectOption.label : undefined, disabled: Boolean(disabled), fieldSize: fieldSize, optionLabelDescription: getOptionLabelDescription?.(selectOption.data), optionPrefix: getOptionPrefix?.(selectOption.data) })) : (jsxRuntime.jsx(SingleSelectMenuItem, { ...componentProps, "data-testid": typeof selectOption.label === "string" ? selectOption.label : undefined, disabled: Boolean(disabled) || selectOption.isDisabled, fieldSize: fieldSize, optionLabelDescription: getOptionLabelDescription?.(selectOption.data), optionPrefix: getOptionPrefix?.(selectOption.data) })) }));
1276
1644
  },
1277
- ...componentsProps,
1278
1645
  };
1279
1646
  }, [
1280
- componentsProps,
1281
- maxSelectedDisplayCount,
1647
+ hasError,
1648
+ fieldSize,
1282
1649
  disabled,
1283
- setMenuIsEnabled,
1650
+ className,
1284
1651
  readOnly,
1285
1652
  dataTestId,
1286
- t,
1287
1653
  prefix,
1288
- hasError,
1289
- getOptionLabelDescription,
1290
- fieldSize,
1654
+ interactable,
1655
+ getVisibleCount,
1656
+ getTotalCount,
1657
+ getCounterWidth,
1658
+ getIsComplete,
1659
+ setValueContainerRef,
1660
+ setFakeCounterRef,
1661
+ setCounterRef,
1662
+ autoComplete,
1663
+ t,
1291
1664
  getOptionPrefix,
1665
+ getOptionLabelDescription,
1666
+ setGeometryRef,
1667
+ setMenuRef,
1292
1668
  ]);
1293
1669
  return customComponents;
1294
1670
  };
1295
-
1296
- /**
1297
- * @template IsMulti
1298
- * @template Group
1299
- * @param {RefObject<HTMLDivElement | null>} refContainer react ref to container element
1300
- * @param {number | undefined} maxSelectedDisplayCount a number of max display count
1301
- * @param {StylesConfig<Option, IsMulti, Group> | undefined} styles a optional object to override styles of react-select
1302
- * @returns {StylesConfig<Option, boolean>} styles to override in select
1303
- */
1304
- const useCustomStyles = ({ refContainer, maxSelectedDisplayCount, styles, disabled, fieldSize, }) => {
1305
- const customStyles = react.useMemo(() => {
1306
- return {
1307
- control: base => {
1308
- return {
1309
- ...base,
1310
- minHeight: fieldSize === "small" ? "28px" : fieldSize === "large" ? "40px" : "32px",
1311
- borderRadius: "var(--border-radius-lg)",
1312
- backgroundColor: "inherit",
1313
- };
1314
- },
1315
- singleValue: base => ({
1316
- ...base,
1317
- }),
1318
- multiValue: base => ({
1319
- ...base,
1320
- }),
1321
- multiValueLabel: base => ({
1322
- ...base,
1323
- }),
1324
- indicatorsContainer: base => ({
1325
- ...base,
1326
- ...(disabled && { display: "none" }),
1327
- }),
1328
- indicatorSeparator: () => ({
1329
- width: "0px",
1330
- }),
1331
- menu: base => {
1332
- return {
1333
- ...base,
1334
- width: "100%",
1335
- marginTop: "4px",
1336
- marginBottom: "18px",
1337
- transition: "all 1s ease-in-out",
1338
- };
1339
- },
1340
- input: base => ({
1341
- ...base,
1342
- marginLeft: "0px",
1343
- }),
1344
- placeholder: base => ({
1345
- ...base,
1346
- }),
1347
- option: () => ({}),
1348
- menuPortal: base => ({
1349
- ...base,
1350
- width: refContainer.current ? `${refContainer.current.clientWidth}px` : base.width,
1351
- backgroundColor: "#ffffff",
1352
- borderRadius: "var(--border-radius-lg)",
1353
- zIndex: "var(--z-overlay)",
1354
- borderColor: "rgb(var(--color-neutral-300))",
1355
- boxShadow: "var(--tw-ring-inset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)",
1356
- }),
1357
- menuList: base => {
1358
- return {
1359
- ...base,
1360
- position: "relative",
1361
- padding: "var(--spacing-1)",
1362
- display: "grid",
1363
- gap: "var(--spacing-1)",
1364
- width: "100%",
1365
- borderRadius: "0px",
1366
- boxShadow: "none",
1367
- paddingTop: "0px",
1368
- };
1369
- },
1370
- valueContainer: base => {
1371
- return {
1372
- ...base,
1373
- paddingBlock: 0,
1374
- flexWrap: maxSelectedDisplayCount !== undefined ? "wrap" : "nowrap",
1375
- gap: "0.25rem",
1376
- };
1377
- },
1378
- container: base => ({
1379
- ...base,
1380
- width: "100%",
1381
- }),
1382
- dropdownIndicator: base => ({
1383
- ...base,
1384
- padding: "0px",
1385
- }),
1386
- clearIndicator: base => {
1387
- return {
1388
- ...base,
1389
- padding: "0px",
1390
- };
1391
- },
1392
- ...styles,
1393
- };
1394
- }, [refContainer, disabled, fieldSize, maxSelectedDisplayCount, styles]);
1395
- return { customStyles };
1671
+ const getNoStyles = () => {
1672
+ // To prevent the default styles from being applied from react-select.
1673
+ return {};
1674
+ };
1675
+ const getInputComponent = (children) => {
1676
+ return Array.isArray(children)
1677
+ ? children.find(child => child !== null && child !== undefined && /react-select-\d+-input/.test(child?.props?.id))
1678
+ : null;
1679
+ };
1680
+ const getMultiValueComponents = (children) => {
1681
+ if (!Array.isArray(children)) {
1682
+ return null;
1683
+ }
1684
+ // Only return if children[0] is an array (the multiValue array)
1685
+ return Array.isArray(children[0]) ? children[0] : null;
1686
+ };
1687
+ const getPlaceholderElement = (children) => {
1688
+ return Array.isArray(children)
1689
+ ? children.find(child => child !== null && child !== undefined && child.key === "placeholder")
1690
+ : null;
1396
1691
  };
1397
1692
 
1398
1693
  /**
1399
1694
  * A hook used by selects to share the common code
1400
1695
  *
1401
1696
  * @param {SelectProps} props - The props for the Select component
1402
- * @returns {UseSelectProps} Select component
1403
- */
1404
- const useSelect = ({ id, className, "data-testid": dataTestId = "select", prefix, async, maxMenuHeight = 200, label, hasError, disabled, isMulti, components, value, options, onChange, isLoading, classNamePrefix = "", onMenuOpen, onMenuClose, maxSelectedDisplayCount = undefined, isClearable = false, isSearchable = true, onMenuScrollToBottom, styles, filterOption, onInputChange, getOptionLabelDescription, getOptionPrefix, fieldSize = "medium", ...props }) => {
1405
- const refContainer = react.useRef(document.createElement("div"));
1406
- const { customStyles } = useCustomStyles({
1407
- refContainer,
1408
- maxSelectedDisplayCount,
1409
- styles,
1410
- disabled: Boolean(disabled),
1411
- fieldSize,
1412
- });
1413
- const [menuIsOpen, setMenuIsOpen] = react.useState(props.menuIsOpen ?? false);
1414
- const [menuIsEnabled, setMenuIsEnabled] = react.useState(true);
1697
+ * @returns {ReactSelectProps} Props for react-select component
1698
+ */
1699
+ const useSelect = ({ disabled = false, //renaming to isDisabled, so not directly passed to react-select
1700
+ "data-testid": dataTestId,
1701
+ // Extract critical props that must trigger recalculation when they change
1702
+ onMenuOpen, onMenuClose, options, value, onChange, defaultValue,
1703
+ // restProps are pur in a ref to keep dependencies stable while always having the latest values
1704
+ // See explanation on restPropsRef below for more details 👇
1705
+ ...restProps }) => {
1415
1706
  const customComponents = useCustomComponents({
1416
- componentsProps: components,
1417
- disabled: Boolean(disabled),
1418
- readOnly: Boolean(props.readOnly),
1419
- setMenuIsEnabled,
1420
- "data-testid": dataTestId,
1421
- maxSelectedDisplayCount,
1422
- prefix,
1423
- hasError,
1424
- fieldSize,
1425
- getOptionLabelDescription,
1426
- getOptionPrefix,
1707
+ disabled, // intentionally not evaluated as boolean, since it can be object too!
1708
+ readOnly: restProps.readOnly ?? false, // intentionally not evaluated as boolean, since it can be object too!
1709
+ "data-testid": dataTestId ?? "select",
1710
+ prefix: restProps.prefix,
1711
+ hasError: restProps.hasError,
1712
+ fieldSize: restProps.fieldSize,
1713
+ getOptionLabelDescription: restProps.getOptionLabelDescription,
1714
+ getOptionPrefix: restProps.getOptionPrefix,
1715
+ isMulti: restProps.isMulti ?? false,
1716
+ className: restProps.className,
1717
+ autoComplete: restProps.autoComplete,
1427
1718
  });
1428
- const menuPlacement = "auto";
1429
- const openMenuHandler = async () => {
1430
- onMenuOpen?.();
1431
- if (menuIsEnabled) {
1432
- setMenuIsOpen(true);
1433
- }
1434
- else {
1435
- setMenuIsEnabled(true);
1436
- }
1437
- };
1438
- const closeMenuHandler = () => {
1439
- setMenuIsOpen(false);
1440
- onMenuClose && onMenuClose();
1441
- };
1442
- return {
1443
- refContainer,
1444
- customStyles,
1445
- menuIsOpen,
1719
+ const interactable = react.useMemo(() => !Boolean(disabled) && !Boolean(restProps.readOnly), [disabled, restProps.readOnly]);
1720
+ // Determine the portal target for the menu
1721
+ const portalTarget = react.useMemo(() => (restProps.menuPortalTarget !== undefined ? restProps.menuPortalTarget : document.body), [restProps.menuPortalTarget]);
1722
+ // Use custom scroll blocking hook to prevent layout shifts
1723
+ // Pass the portal target so we only block scroll when menu is portaled to document.body
1724
+ const { blockScroll, restoreScroll } = reactComponents.useScrollBlock(portalTarget);
1725
+ // Store callbacks in refs to keep wrapper callbacks stable
1726
+ // This prevents unnecessary re-renders when parent components don't memoize these callbacks
1727
+ const onMenuOpenRef = react.useRef(onMenuOpen);
1728
+ const onMenuCloseRef = react.useRef(onMenuClose);
1729
+ react.useEffect(() => {
1730
+ onMenuOpenRef.current = onMenuOpen;
1731
+ }, [onMenuOpen]);
1732
+ react.useEffect(() => {
1733
+ onMenuCloseRef.current = onMenuClose;
1734
+ }, [onMenuClose]);
1735
+ // Wrap user's onMenuOpen callback to block scrolling
1736
+ // We apply scroll blocking directly instead of using react-select's menuShouldBlockScroll
1737
+ // because it doesn't properly account for existing body padding, causing layout shifts
1738
+ // See commeont next to menuShouldBlockScroll below for more
1739
+ const handleMenuOpen = react.useCallback(() => {
1740
+ blockScroll();
1741
+ onMenuOpenRef.current?.();
1742
+ }, [blockScroll]);
1743
+ // Wrap user's onMenuClose callback to restore scrolling
1744
+ const handleMenuClose = react.useCallback(() => {
1745
+ restoreScroll();
1746
+ onMenuCloseRef.current?.();
1747
+ }, [restoreScroll]);
1748
+ // Store restProps in a ref to keep dependencies stable while always having the latest values
1749
+ // This allows us to access restProps in useMemo without including them in the dependency array
1750
+ // Criteria for props in restProps:
1751
+ // - Props used in computations are already tracked via derived values (interactable, portalTarget, etc.)
1752
+ // - Other props are accessed via ref - they'll always be latest but won't trigger recalculation
1753
+ // This is a trade-off: we prioritize stability over perfect reactivity for less critical props
1754
+ const restPropsRef = react.useRef(restProps);
1755
+ react.useEffect(() => {
1756
+ restPropsRef.current = restProps;
1757
+ }, [restProps]);
1758
+ // eslint-disable-next-line react-hooks/refs
1759
+ return react.useMemo(() => {
1760
+ const currentRestProps = restPropsRef.current;
1761
+ return {
1762
+ ...currentRestProps,
1763
+ options,
1764
+ value,
1765
+ onChange,
1766
+ defaultValue,
1767
+ components: customComponents,
1768
+ unstyled: true,
1769
+ "aria-label": currentRestProps.label,
1770
+ "data-testid": dataTestId ?? "select",
1771
+ tabSelectsValue: false,
1772
+ blurInputOnSelect: false,
1773
+ // This configuration allows for more flexible positioning control of the dropdown.
1774
+ // Setting menuPortalTarget to 'null' specifies that the dropdown should be rendered within
1775
+ // the parent element instead of 'document.body'.
1776
+ menuPortalTarget: portalTarget,
1777
+ isSearchable: interactable ? (currentRestProps.isSearchable ?? true) : false,
1778
+ // Disable react-select's built-in scroll blocking as we handle it ourselves in onMenuOpen/onMenuClose
1779
+ // to prevent layout shifts caused by not accounting for existing body padding in the react-select implementation.
1780
+ // See: https://github.com/JedWatson/react-select/issues/5342 AND https://github.com/JedWatson/react-select/issues/5020
1781
+ menuShouldBlockScroll: false,
1782
+ menuShouldScrollIntoView: true,
1783
+ openMenuOnClick: interactable ? (currentRestProps.openMenuOnClick ?? true) : false,
1784
+ openMenuOnFocus: Boolean(currentRestProps.openMenuOnFocus),
1785
+ closeMenuOnSelect: !Boolean(currentRestProps.isMulti),
1786
+ isDisabled: Boolean(disabled),
1787
+ isClearable: Boolean(currentRestProps.isClearable),
1788
+ menuPlacement: currentRestProps.menuPlacement ?? "auto",
1789
+ placeholder: interactable ? currentRestProps.placeholder : undefined,
1790
+ hideSelectedOptions: Boolean(currentRestProps.hideSelectedOptions),
1791
+ menuIsOpen: interactable ? undefined : false, // close it if not interactable, otherwise leave state to react-select
1792
+ // Wire up our custom menu open/close handlers
1793
+ onMenuOpen: handleMenuOpen,
1794
+ onMenuClose: handleMenuClose,
1795
+ // 👇 putting these here to avoid them _accidentally_ being overwritten in the future👇
1796
+ maxMenuHeight: undefined, // controlled custom components styling
1797
+ minMenuHeight: undefined, // controlled custom components styling
1798
+ theme: undefined,
1799
+ classNames: undefined,
1800
+ styles: undefined,
1801
+ };
1802
+ }, [
1446
1803
  customComponents,
1447
- menuPlacement,
1448
- openMenuHandler,
1449
- closeMenuHandler,
1450
- };
1804
+ disabled,
1805
+ interactable,
1806
+ handleMenuOpen,
1807
+ handleMenuClose,
1808
+ dataTestId,
1809
+ portalTarget,
1810
+ // Critical props that must trigger recalculation when they change
1811
+ options,
1812
+ value,
1813
+ onChange,
1814
+ defaultValue,
1815
+ // restPropsRef is intentionally not included - we access restPropsRef.current inside useMemo
1816
+ // to always get the latest restProps without causing recalculation when the object reference changes
1817
+ ]);
1451
1818
  };
1452
1819
 
1453
1820
  // This is here to ensure the bundled react-components can expose the react-select for jest in external iris apps.
1454
1821
  const ReactSyncSelect = ReactSelect.default || ReactSelect;
1455
1822
  /**
1456
- * Selects are input components used to choose a value from a set.
1823
+ * BaseSelect are input components used to choose a value from a set.
1457
1824
  *
1458
1825
  * @param {SelectProps} props - The props for the Select component
1459
1826
  * @returns {ReactElement} Select component
1460
1827
  */
1461
1828
  const BaseSelect = (props) => {
1462
- const { id, "data-testid": dataTestId = "select", prefix, async, maxMenuHeight = 200, label, hasError, disabled, isMulti, menuPosition = "absolute", value, options, onChange, isLoading, classNamePrefix = "select", onMenuScrollToBottom, onInputChange, isSearchable, isClearable = false, readOnly, fieldSize = "medium", openMenuOnClick = !disabled, openMenuOnFocus = false, hideSelectedOptions = false, } = props;
1463
- const { refContainer, customStyles, menuIsOpen, customComponents, menuPlacement, openMenuHandler, closeMenuHandler } = useSelect(props);
1464
- const reactSelectProps = react.useMemo(() => ({
1465
- value,
1466
- menuPlacement,
1467
- maxMenuHeight,
1468
- onChange,
1469
- "aria-label": label,
1470
- "data-testid": dataTestId,
1471
- components: customComponents,
1472
- styles: customStyles,
1473
- tabSelectsValue: false,
1474
- blurInputOnSelect: false,
1475
- // This configuration allows for more flexible positioning control of the dropdown.
1476
- // Setting menuPortalTarget to 'null' specifies that the dropdown should be rendered within
1477
- // the parent element instead of 'document.body'.
1478
- menuPortalTarget: props.menuPortalTarget !== undefined ? props.menuPortalTarget : document.body,
1479
- isSearchable: disabled || readOnly ? false : isSearchable,
1480
- menuShouldBlockScroll: true,
1481
- menuShouldScrollIntoView: true,
1482
- openMenuOnFocus,
1483
- menuIsOpen: !readOnly ? menuIsOpen : false,
1484
- openMenuOnClick,
1485
- closeMenuOnSelect: !isMulti,
1486
- isMulti,
1487
- classNamePrefix,
1488
- isLoading,
1489
- isClearable,
1490
- id,
1491
- onMenuScrollToBottom,
1492
- onInputChange,
1493
- hideSelectedOptions,
1494
- isDisabled: Boolean(disabled),
1495
- }), [
1496
- classNamePrefix,
1497
- customComponents,
1498
- customStyles,
1499
- dataTestId,
1500
- disabled,
1501
- hideSelectedOptions,
1502
- id,
1503
- isClearable,
1504
- isLoading,
1505
- isMulti,
1506
- isSearchable,
1507
- label,
1508
- maxMenuHeight,
1509
- menuIsOpen,
1510
- menuPlacement,
1511
- onChange,
1512
- onInputChange,
1513
- onMenuScrollToBottom,
1514
- openMenuOnClick,
1515
- openMenuOnFocus,
1516
- props.menuPortalTarget,
1517
- readOnly,
1518
- value,
1519
- ]);
1520
- const renderAsDisabled = Boolean(props.disabled) || props.readOnly;
1521
- return (jsxRuntime.jsxs("div", { className: cvaSelect({
1522
- invalid: hasError,
1523
- fieldSize: fieldSize,
1524
- disabled: renderAsDisabled,
1525
- className: props.className,
1526
- }), "data-testid": dataTestId, ref: refContainer, children: [prefix !== undefined ? (jsxRuntime.jsx("div", { className: cvaSelectPrefixSuffix({ kind: "prefix" }), "data-testid": dataTestId ? `${dataTestId}-prefix` : null, children: prefix })) : null, async ? (jsxRuntime.jsx(ReactAsyncSelect, { ...props, ...reactSelectProps, ...async, menuPosition: menuPosition, onMenuClose: closeMenuHandler, onMenuOpen: openMenuHandler, placeholder: renderAsDisabled ? null : props.placeholder })) : (jsxRuntime.jsx(ReactSyncSelect, { ...props, ...reactSelectProps, isMulti: isMulti, menuPosition: menuPosition, onMenuClose: closeMenuHandler, onMenuOpen: openMenuHandler, options: options, placeholder: renderAsDisabled ? null : props.placeholder })), typeof props.disabled === "object" ? (jsxRuntime.jsx("div", { className: cvaSelectPrefixSuffix({ kind: "suffix" }), "data-testid": dataTestId ? `${dataTestId}-locked` : null, children: jsxRuntime.jsx(InputLockReasonTooltip, { ...props.disabled }) })) : null] }));
1829
+ const select = useSelect(props);
1830
+ return props.async ? jsxRuntime.jsx(ReactAsyncSelect, { ...select }) : jsxRuntime.jsx(ReactSyncSelect, { ...select });
1831
+ };
1832
+
1833
+ /**
1834
+ * A hook used by creatable selects that extends useSelect with creatable-specific functionality
1835
+ *
1836
+ * @param props - The props for the CreatableSelect component
1837
+ * @returns {ReactCreatableProps} Props for react-select creatable component
1838
+ */
1839
+ const useCreatableSelect = (props) => {
1840
+ const { onCreateOption, allowCreateWhileLoading, isMulti } = props;
1841
+ const baseSelectProps = useSelect(props);
1842
+ // Store onCreateOption in a ref to keep wrapper stable
1843
+ // This prevents unnecessary re-renders when parent components don't memoize this callback
1844
+ const onCreateOptionRef = react.useRef(onCreateOption);
1845
+ react.useEffect(() => {
1846
+ onCreateOptionRef.current = onCreateOption;
1847
+ }, [onCreateOption]);
1848
+ return react.useMemo(() => ({
1849
+ ...baseSelectProps,
1850
+ allowCreateWhileLoading,
1851
+ onCreateOption: onCreateOptionRef.current,
1852
+ // Override some defaults specific to creatable selects
1853
+ closeMenuOnSelect: false, // Keep menu open for multi-creation
1854
+ blurInputOnSelect: !Boolean(isMulti), // Only blur if not multi
1855
+ }), [baseSelectProps, allowCreateWhileLoading, isMulti]);
1527
1856
  };
1528
- BaseSelect.displayName = "BaseSelect";
1529
1857
 
1530
1858
  /**
1531
1859
  * CreatableSelects are input components used to choose a value from a set.
1532
1860
  *
1533
- * @param {CreatableSelectProps} props - The props for the CreatableSelect component
1534
- * @returns {ReactElement} CreatableSelect component
1861
+ /**
1862
+ * CreatableSelect is a component that allows users to select from existing options or create new ones.
1863
+ *
1864
+ * @template TOption - The option type.
1865
+ * @template TIsAsync - Indicates whether the component is asynchronous.
1866
+ * @template TIsMulti - Indicates whether multiple selections are allowed.
1867
+ * @template TGroup - The group base type for options.
1868
+ * @param {CreatableSelectProps} props - The props to configure the CreatableSelect component.
1869
+ * @returns {ReactElement} A ReactElement rendering the CreatableSelect.
1535
1870
  */
1536
1871
  const CreatableSelect = (props) => {
1537
- const { id, "data-testid": dataTestId = "creatableSelect", prefix, async, maxMenuHeight = 200, label, hasError, disabled, isMulti, value, options, onChange, isLoading, classNamePrefix = "creatableSelect", onMenuScrollToBottom, onInputChange, isSearchable, isClearable = false, readOnly, openMenuOnClick = !disabled, openMenuOnFocus = !disabled, allowCreateWhileLoading, onCreateOption, } = props;
1538
- const { refContainer, customStyles, menuIsOpen, customComponents, menuPlacement, openMenuHandler, closeMenuHandler } = useSelect(props);
1539
- const reactCreatableSelectProps = react.useMemo(() => ({
1540
- value,
1541
- menuPlacement,
1542
- maxMenuHeight,
1543
- onChange,
1544
- "aria-label": label,
1545
- "data-testid": dataTestId,
1546
- components: customComponents,
1547
- styles: customStyles,
1548
- tabSelectsValue: false,
1549
- blurInputOnSelect: !isMulti,
1550
- menuPortalTarget: props.menuPortalTarget || document.body,
1551
- isSearchable: disabled || readOnly ? false : isSearchable,
1552
- menuShouldBlockScroll: true,
1553
- menuShouldScrollIntoView: true,
1554
- openMenuOnFocus,
1555
- menuIsOpen: !readOnly ? menuIsOpen : false,
1556
- openMenuOnClick,
1557
- closeMenuOnSelect: false,
1558
- isMulti,
1559
- classNamePrefix,
1560
- isLoading,
1561
- isClearable,
1562
- id,
1563
- onMenuScrollToBottom,
1564
- onInputChange,
1565
- allowCreateWhileLoading,
1566
- onCreateOption,
1567
- isDisabled: Boolean(disabled),
1568
- }), [
1569
- allowCreateWhileLoading,
1570
- classNamePrefix,
1571
- customComponents,
1572
- customStyles,
1573
- dataTestId,
1574
- disabled,
1575
- id,
1576
- isClearable,
1577
- isLoading,
1578
- isMulti,
1579
- isSearchable,
1580
- label,
1581
- maxMenuHeight,
1582
- menuIsOpen,
1583
- menuPlacement,
1584
- onChange,
1585
- onCreateOption,
1586
- onInputChange,
1587
- onMenuScrollToBottom,
1588
- openMenuOnClick,
1589
- openMenuOnFocus,
1590
- props.menuPortalTarget,
1591
- readOnly,
1592
- value,
1593
- ]);
1594
- const renderAsDisabled = Boolean(props.disabled) || props.readOnly;
1595
- return (jsxRuntime.jsxs("div", { className: cvaSelect({ invalid: hasError, disabled: renderAsDisabled, className: props.className }), "data-testid": dataTestId, ref: refContainer, children: [prefix !== undefined ? (jsxRuntime.jsx("div", { className: cvaSelectPrefixSuffix({ kind: "prefix" }), "data-testid": dataTestId ? `${dataTestId}-prefix` : null, children: prefix })) : null, async ? (jsxRuntime.jsx(ReactAsyncCreatableSelect, { ...props, ...reactCreatableSelectProps, ...async, onMenuClose: closeMenuHandler, onMenuOpen: openMenuHandler, placeholder: renderAsDisabled ? null : props.placeholder })) : (jsxRuntime.jsx(ReactCreatableSelect, { ...props, ...reactCreatableSelectProps, hideSelectedOptions: false, isMulti: isMulti, onMenuClose: closeMenuHandler, onMenuOpen: openMenuHandler, options: options, placeholder: renderAsDisabled ? null : props.placeholder })), typeof props.disabled === "object" ? (jsxRuntime.jsx("div", { className: cvaSelectPrefixSuffix({ kind: "suffix" }), "data-testid": dataTestId ? `${dataTestId}-locked` : null, children: jsxRuntime.jsx(InputLockReasonTooltip, { ...props.disabled }) })) : null] }));
1872
+ const creatableSelect = useCreatableSelect(props);
1873
+ return props.async ? (jsxRuntime.jsx(ReactAsyncCreatableSelect, { ...creatableSelect })) : (jsxRuntime.jsx(ReactCreatableSelect, { ...creatableSelect }));
1596
1874
  };
1597
- CreatableSelect.displayName = "CreatableSelect";
1598
1875
 
1599
1876
  /**
1600
1877
  * The Label component is used for labels for input fields.
@@ -1637,7 +1914,7 @@ const cvaHelpAddon = cssClassVarianceUtilities.cvaMerge(["ml-auto"]);
1637
1914
  * @param {FormGroupProps} props - The props for the FormGroup component
1638
1915
  * @returns {ReactElement} FormGroup component
1639
1916
  */
1640
- const FormGroup = ({ isInvalid, isWarning, helpText, helpAddon, tip, className, "data-testid": dataTestId, label, htmlFor, children, required = false, }) => {
1917
+ const FormGroup = ({ isInvalid = false, isWarning = false, helpText, helpAddon, tip, className, "data-testid": dataTestId, label, htmlFor, children, required = false, }) => {
1641
1918
  const [t] = useTranslation();
1642
1919
  const validationStateIcon = react.useMemo(() => {
1643
1920
  const color = isInvalid ? "danger" : isWarning ? "warning" : null;
@@ -1731,7 +2008,7 @@ const isValidHEXColor = (value) => {
1731
2008
  * ColorField validates that user enters a valid color address.
1732
2009
  *
1733
2010
  */
1734
- const ColorField = react.forwardRef(({ label, id, tip, helpText, errorMessage, helpAddon, className, defaultValue, "data-testid": dataTestId, value: propValue, onChange, isInvalid, onBlur, fieldSize = "medium", style, disabled, readOnly, nonInteractive, isWarning, inputClassName, ...inputProps }, ref) => {
2011
+ const ColorField = react.forwardRef(({ label, id, tip, helpText, errorMessage, helpAddon, className, defaultValue, "data-testid": dataTestId, value: propValue, onChange, isInvalid, onBlur, fieldSize = "medium", style, disabled, readOnly, isWarning, inputClassName, ...inputProps }, ref) => {
1735
2012
  const renderAsDisabled = Boolean(disabled);
1736
2013
  const renderAsReadonly = Boolean(readOnly);
1737
2014
  const htmlForId = react.useMemo(() => (id ? id : "colorField-" + sharedUtils.uuidv4()), [id]);
@@ -1783,7 +2060,7 @@ const ColorField = react.forwardRef(({ label, id, tip, helpText, errorMessage, h
1783
2060
  className,
1784
2061
  }), "data-testid": dataTestId ? `${dataTestId}-container` : undefined, style: style, children: [jsxRuntime.jsx("input", { "aria-labelledby": htmlForId + "-label", className: cvaInputColorField({ readOnly: renderAsReadonly }), "data-testid": dataTestId, defaultValue: defaultValue, disabled: renderAsDisabled, id: htmlForId, onBlur: handleBlur, onChange: handleInputChange, readOnly: renderAsReadonly, ref: innerRef, type: "color", value: innerValue }), jsxRuntime.jsx("input", { "aria-labelledby": htmlForId + "-label-text", className: cvaInputElement({
1785
2062
  className: tailwindMerge.twMerge("px-1 focus-visible:outline-none", inputClassName),
1786
- }), "data-testid": dataTestId ? `${dataTestId}-textField` : undefined, disabled: renderAsDisabled, onBlur: handleBlur, onChange: handleInputChange, readOnly: renderAsReadonly || nonInteractive, type: "text", value: innerValue, ...inputProps }), jsxRuntime.jsx(GenericActionsRenderer, { disabled: renderAsDisabled || renderAsReadonly, fieldSize: fieldSize, genericAction: "edit", innerRef: innerRef, tooltipLabel: t("colorField.tooltip") })] }) }));
2063
+ }), "data-testid": dataTestId ? `${dataTestId}-textField` : undefined, disabled: renderAsDisabled, onBlur: handleBlur, onChange: handleInputChange, readOnly: renderAsReadonly, type: "text", value: innerValue, ...inputProps }), jsxRuntime.jsx(GenericActionsRenderer, { disabled: renderAsDisabled || renderAsReadonly, fieldSize: fieldSize, genericAction: "edit", innerRef: innerRef, tooltipLabel: t("colorField.tooltip") })] }) }));
1787
2064
  });
1788
2065
  ColorField.displayName = "ColorField";
1789
2066
 
@@ -1969,7 +2246,7 @@ const EmailBaseInput = ({ fieldSize = "medium", disabled = false, "data-testid":
1969
2246
  setEmail(newValue);
1970
2247
  }, [onChange]);
1971
2248
  const renderAsInvalid = (email && !validateEmailAddress(email)) || isInvalid;
1972
- return (jsxRuntime.jsx(BaseInput, { actions: email && email.length > 0 ? (jsxRuntime.jsx(ActionButton, { "data-testid": dataTestId ? `${dataTestId}-emailIcon` : undefined, disabled: disableAction || isInvalid, onClick: sendEmail, size: fieldSize ?? undefined, type: "EMAIL", value: email })) : null, "data-testid": dataTestId, disabled: disabled, fieldSize: fieldSize, isInvalid: renderAsInvalid, onChange: handleChange, placeholder: rest.placeholder || "mail@example.com", ref: ref, type: "email", ...rest }));
2249
+ return (jsxRuntime.jsx(BaseInput, { actions: email && email.length > 0 ? (jsxRuntime.jsx(ActionButton, { "data-testid": dataTestId ? `${dataTestId}-emailIcon` : undefined, disabled: disableAction || isInvalid, onClick: sendEmail, size: fieldSize, type: "EMAIL", value: email })) : null, "data-testid": dataTestId, disabled: disabled, fieldSize: fieldSize, isInvalid: renderAsInvalid, onChange: handleChange, placeholder: rest.placeholder || "mail@example.com", ref: ref, type: "email", ...rest }));
1973
2250
  };
1974
2251
 
1975
2252
  /**
@@ -2008,14 +2285,17 @@ function isWritableRef(r) {
2008
2285
  }
2009
2286
  /**
2010
2287
  * Multi adapter:
2011
- * - keeps Option[] semantics (via `MultiValue<Option>`)
2288
+ * - keeps TOption[] semantics (via `MultiValue<TOption>`)
2012
2289
  * - renders FormGroup chrome (label, help, error)
2013
2290
  * - exposes a hidden <select> for a stable ref target
2014
2291
  * - optionally renders one hidden <input> per selected option IF `getOptionValue` is provided
2015
2292
  * - passes through all remaining BaseSelect props with isMulti=true
2293
+ *
2294
+ * @param {FormFieldSelectAdapterMultiProps} props - The props for the FormFieldSelectAdapterMulti component
2295
+ * @returns {ReactElement} FormFieldSelectAdapterMulti component
2016
2296
  */
2017
2297
  const FormFieldSelectAdapterMulti = (props) => {
2018
- const { className, "data-testid": dataTestId, helpText, helpAddon, tip, label, isInvalid, errorMessage, name, onBlur, options, value, defaultValue, id, onChange, children, ref, ...selectProps } = props;
2298
+ const { className, "data-testid": dataTestId, helpText, helpAddon, tip, label, isInvalid, errorMessage, name, onBlur, options, value, defaultValue, id, htmlFor: htmlForProp, onChange, children, ref, ...selectProps } = props;
2019
2299
  // Hidden select for a stable DOM ref target (API parity with single adapter)
2020
2300
  const innerRef = react.useRef(null);
2021
2301
  // Bridge external ref (supports both callback and object refs)
@@ -2030,25 +2310,28 @@ const FormFieldSelectAdapterMulti = (props) => {
2030
2310
  // Determine invalid state
2031
2311
  const renderAsInvalid = react.useMemo(() => (isInvalid === undefined ? Boolean(errorMessage) : isInvalid), [errorMessage, isInvalid]);
2032
2312
  // id to connect label and control
2033
- const controlId = react.useMemo(() => (id ? id : "multiSelectField-" + sharedUtils.uuidv4()), [id]);
2313
+ const controlId = react.useMemo(() => htmlForProp ?? id ?? "multiSelectField-" + sharedUtils.uuidv4(), [htmlForProp, id]);
2034
2314
  // If consumers provided getOptionValue (from BaseSelect props),
2035
2315
  // we can render hidden inputs for native form submit / RHF.
2036
2316
  const selectPropsWithAccessors = selectProps;
2037
- const getOptionValue = typeof selectPropsWithAccessors.getOptionValue === "function" ? selectPropsWithAccessors.getOptionValue : undefined;
2317
+ const getOptionValue = react.useMemo(() => typeof selectPropsWithAccessors.getOptionValue === "function"
2318
+ ? selectPropsWithAccessors.getOptionValue
2319
+ : undefined, [selectPropsWithAccessors]);
2038
2320
  // Compute selected options snapshot for hidden inputs (prefer controlled `value`)
2039
2321
  const selectedOptions = react.useMemo(() => value ?? defaultValue ?? [], [value, defaultValue]);
2040
2322
  // Build the exact prop bag for BaseSelect (multi=true).
2041
- const childProps = {
2323
+ const childProps = react.useMemo(() => ({
2042
2324
  ...selectProps,
2325
+ "data-testid": dataTestId,
2043
2326
  id: controlId,
2044
2327
  onBlur,
2045
2328
  options,
2046
2329
  isMulti: true,
2047
2330
  value: value ?? null,
2048
2331
  defaultValue,
2049
- onChange: next => onChange?.(next),
2050
- };
2051
- return (jsxRuntime.jsxs(FormGroup, { className: className, "data-testid": dataTestId, helpAddon: helpAddon, helpText: (renderAsInvalid && errorMessage) || helpText, htmlFor: controlId, isInvalid: renderAsInvalid, label: label, required: "required" in selectProps && selectProps.required
2332
+ onChange,
2333
+ }), [selectProps, dataTestId, controlId, onBlur, options, value, defaultValue, onChange]);
2334
+ return (jsxRuntime.jsxs(FormGroup, { className: className, "data-testid": dataTestId ? `${dataTestId}-FormGroup` : undefined, helpAddon: helpAddon, helpText: (renderAsInvalid && errorMessage) || helpText, htmlFor: controlId, isInvalid: renderAsInvalid, label: label, required: "required" in selectProps && selectProps.required
2052
2335
  ? !(("disabled" in selectProps && Boolean(selectProps.disabled)) ||
2053
2336
  ("readOnly" in selectProps && Boolean(selectProps.readOnly)))
2054
2337
  : false, tip: tip, children: [jsxRuntime.jsx("select", { "aria-hidden": "true", defaultValue: "", hidden: true, name: name, ref: innerRef }), typeof getOptionValue === "function" &&
@@ -2057,12 +2340,13 @@ const FormFieldSelectAdapterMulti = (props) => {
2057
2340
  return typeof primitiveValue === "string" ? (jsxRuntime.jsx("input", { name: name, type: "hidden", value: primitiveValue }, `${primitiveValue}-${idx}`)) : null;
2058
2341
  }), children(childProps)] }));
2059
2342
  };
2060
- FormFieldSelectAdapterMulti.displayName = "FormFieldSelectAdapterMulti";
2061
2343
 
2062
2344
  /**
2063
- * MultiSelectField validated multi-select field.
2064
- * Types mirror BaseSelect: options: Option[], value/defaultValue: Option[], onChange: (Option[] | null) => void
2065
- * Implemented as a generic const component (no forwardRef, no assertions).
2345
+ * MultiSelectField is a custom Select component wrapped in the FormGroup component
2346
+ * that allows you to select multiple options from a list.
2347
+ *
2348
+ * @param {MultiSelectFieldProps} props - The props for the MultiSelectField component
2349
+ * @returns {ReactElement} MultiSelectField component
2066
2350
  */
2067
2351
  const MultiSelectField = ({ ref, ...props }) => {
2068
2352
  return (jsxRuntime.jsx(FormFieldSelectAdapterMulti, { ...props, ref: ref, children: convertedProps => jsxRuntime.jsx(BaseSelect, { ...convertedProps }) }));
@@ -2772,15 +3056,16 @@ Search.displayName = "Search";
2772
3056
  /**
2773
3057
  *
2774
3058
  */
2775
- const FormFieldSelectAdapter = ({ className, "data-testid": dataTestId, helpText, helpAddon, tip, label, isInvalid, errorMessage, name, onBlur, options, value, defaultValue, id, onChange, children, ref, ...rest }) => {
3059
+ const FormFieldSelectAdapter = ({ className, "data-testid": dataTestId, helpText, helpAddon, tip, label, isInvalid = false, errorMessage, name, onBlur, options, value, defaultValue, id, htmlFor: htmlForProp, onChange, children, ref, required = false, ...rest }) => {
2776
3060
  const isFirstRender = reactComponents.useIsFirstRender();
2777
- const [innerValue, setInnerValue] = react.useState(value || defaultValue);
3061
+ const [innerValue, setInnerValue] = react.useState(value ?? defaultValue);
2778
3062
  react.useEffect(() => {
2779
3063
  setInnerValue(defaultValue);
2780
3064
  }, [defaultValue]);
2781
- const renderAsInvalid = isInvalid === undefined ? Boolean(errorMessage) : isInvalid;
2782
- const htmlFor = react.useMemo(() => (id ? id : "selectField-" + sharedUtils.uuidv4()), [id]);
3065
+ const renderAsInvalid = isInvalid || Boolean(errorMessage);
3066
+ const htmlFor = react.useMemo(() => htmlForProp ?? id ?? "selectField-" + sharedUtils.uuidv4(), [htmlForProp, id]);
2783
3067
  const innerRef = react.useRef(null);
3068
+ // eslint-disable-next-line local-rules/no-typescript-assertion, @typescript-eslint/no-non-null-assertion
2784
3069
  react.useImperativeHandle(ref, () => innerRef.current, []);
2785
3070
  react.useEffect(() => {
2786
3071
  if (innerValue === undefined) {
@@ -2796,8 +3081,13 @@ const FormFieldSelectAdapter = ({ className, "data-testid": dataTestId, helpText
2796
3081
  const optionsWithCurrentSelectionBackupOption = [
2797
3082
  // Add the current selection in case there's no options loaded yet (in CreatableSelect)
2798
3083
  // Also _don't_ add it if it's a duplicate
2799
- innerValue && !options.find(option => option.value === innerValue)
2800
- ? { value: innerValue, label: String(innerValue) }
3084
+ // Only add backup option when innerValue is defined (not undefined) and not an empty string
3085
+ // Note: Empty string is used as a sentinel value for "no selection" (see onChange handler below)
3086
+ // and should not be added as a backup option. Zero (0) is a valid option value, so we allow it.
3087
+ innerValue !== undefined && innerValue !== "" && !options.find(option => option.value === innerValue)
3088
+ ? // It is safe enough to assert this because the only properties that are used are value and label, and those are present in the TOption type.
3089
+ // eslint-disable-next-line local-rules/no-typescript-assertion
3090
+ { value: innerValue, label: String(innerValue) }
2801
3091
  : null,
2802
3092
  ...options,
2803
3093
  ].filter(sharedUtils.nonNullable);
@@ -2805,15 +3095,16 @@ const FormFieldSelectAdapter = ({ className, "data-testid": dataTestId, helpText
2805
3095
  return (jsxRuntime.jsxs(FormGroup, { isInvalid: renderAsInvalid,
2806
3096
  htmlFor,
2807
3097
  className,
2808
- "data-testid": dataTestId,
3098
+ "data-testid": dataTestId ? `${dataTestId}-FormGroup` : undefined,
2809
3099
  helpText: (renderAsInvalid && errorMessage) || helpText,
2810
3100
  helpAddon,
2811
3101
  tip,
2812
3102
  label,
2813
- required: rest.required ? !(rest.disabled || rest.readOnly) : false, children: [jsxRuntime.jsx("select", { onChange, ref: innerRef, name, value: innerValue, hidden: true, children: optionsWithCurrentSelectionBackupOption.map(option => {
3103
+ required: required ? !Boolean(rest.disabled ?? rest.readOnly) : false, children: [jsxRuntime.jsx("select", { onChange, ref: innerRef, name, value: innerValue, hidden: true, children: optionsWithCurrentSelectionBackupOption.map(option => {
2814
3104
  return (jsxRuntime.jsx("option", { value: option.value, children: option.label }, option.value));
2815
3105
  }) }), children({
2816
3106
  ...rest,
3107
+ required,
2817
3108
  id,
2818
3109
  onBlur,
2819
3110
  options: optionsWithCurrentSelectionBackupOption,
@@ -2823,6 +3114,7 @@ const FormFieldSelectAdapter = ({ className, "data-testid": dataTestId, helpText
2823
3114
  // So even if react-select sends a null value, we need to convert it to an empty string
2824
3115
  setInnerValue(!e ? "" : e.value);
2825
3116
  },
3117
+ "data-testid": dataTestId,
2826
3118
  value: selectedOption,
2827
3119
  defaultValue: selectedOption,
2828
3120
  })] }));
@@ -2838,24 +3130,23 @@ FormFieldSelectAdapter.displayName = "FormFieldSelectAdapter";
2838
3130
  *
2839
3131
  * @param {SelectFieldProps & CreatableSelectProps} props - The props for the CreatableSelectField component
2840
3132
  */
2841
- const CreatableSelectField = ({ allowCreateWhileLoading, onCreateOption, ref, ...props }) => {
2842
- const creatableSelectOnlyProps = { allowCreateWhileLoading, onCreateOption };
3133
+ const CreatableSelectField = ({ allowCreateWhileLoading = false, onCreateOption, ref, ...props }) => {
3134
+ const creatableSelectOnlyProps = {
3135
+ allowCreateWhileLoading,
3136
+ onCreateOption,
3137
+ };
2843
3138
  return (jsxRuntime.jsx(FormFieldSelectAdapter, { ...props, ref: ref, children: convertedProps => jsxRuntime.jsx(CreatableSelect, { ...convertedProps, ...creatableSelectOnlyProps }) }));
2844
3139
  };
2845
3140
  CreatableSelectField.displayName = "CreatableSelectField";
2846
3141
 
2847
3142
  /**
2848
- * The SelectField component is a Select component wrapped in the FromGroup component.
2849
- *
2850
- * This means that it can easily be added to any form alongside other Field components.
2851
- *
2852
- * This is done to make the field compatible with the React-hook-form library.
3143
+ * MultiSelect is a custom Select component wrapped in the FormGroup component
2853
3144
  *
2854
3145
  * @param {SelectFieldProps} props - The props for the SelectField component
2855
3146
  */
2856
- const SelectField = ({ ref, ...props }) => {
3147
+ function SelectField({ ref, ...props }) {
2857
3148
  return (jsxRuntime.jsx(FormFieldSelectAdapter, { ...props, ref: ref, children: convertedProps => jsxRuntime.jsx(BaseSelect, { ...convertedProps }) }));
2858
- };
3149
+ }
2859
3150
  SelectField.displayName = "SelectField";
2860
3151
 
2861
3152
  /**
@@ -3078,7 +3369,7 @@ const cvaUploadInputField = cssClassVarianceUtilities.cvaMerge([
3078
3369
  *
3079
3370
  * NOTE: If shown with a label, please use the `UploadField` component instead.
3080
3371
  */
3081
- const UploadInput = ({ disabled, acceptedTypes, nonInteractive, uploadLabel, multipleFiles, ref, ...rest }) => (jsxRuntime.jsx("label", { className: "tu-upload-input", children: jsxRuntime.jsx(BaseInput, { accept: acceptedTypes, addonBefore: uploadLabel, disabled: disabled, inputClassName: cvaUploadInputField(), multiple: multipleFiles, nonInteractive: nonInteractive, onClick: event => {
3372
+ const UploadInput = ({ disabled, acceptedTypes, nonInteractive, uploadLabel, multipleFiles, ref, ...rest }) => (jsxRuntime.jsx("label", { className: "tu-upload-input", children: jsxRuntime.jsx(BaseInput, { accept: acceptedTypes, addonBefore: uploadLabel, disabled: disabled, inputClassName: cvaUploadInputField(), multiple: multipleFiles, onClick: event => {
3082
3373
  // onClick used to work with nonInteractive option
3083
3374
  if (nonInteractive) {
3084
3375
  event.preventDefault();
@@ -3133,10 +3424,10 @@ const validateUrl = (url, required) => {
3133
3424
  *
3134
3425
  * NOTE: If shown with a label, please use the `UrlField` component instead.
3135
3426
  */
3136
- const UrlBaseInput = ({ "data-testid": dataTestId, isInvalid, disabled = false, fieldSize = "medium", disableAction = false, value, defaultValue, ref, ...rest }) => {
3427
+ const UrlBaseInput = ({ isInvalid = false, "data-testid": dataTestId, disabled = false, fieldSize = "medium", disableAction = false, value, defaultValue, ref, ...rest }) => {
3137
3428
  const [url, setUrl] = react.useState(value?.toString() || defaultValue?.toString());
3138
3429
  const renderAsInvalid = (url && typeof url === "string" && !validateUrlAddress(url)) || isInvalid;
3139
- return (jsxRuntime.jsx(BaseInput, { "data-testid": dataTestId ? `${dataTestId}-url-input` : undefined, disabled: disabled, id: "url-input", isInvalid: renderAsInvalid, onChange: e => setUrl(e.target.value), placeholder: rest.placeholder || "https://www.example.com", ref: ref, type: "url", value: url, ...rest, actions: !disableAction && (jsxRuntime.jsx(ActionButton, { "data-testid": (dataTestId && `${dataTestId}-url-input-Icon`) || "url-input-action-icon", disabled: renderAsInvalid || Boolean(disabled) || disableAction, size: fieldSize ?? undefined, type: "WEB_ADDRESS", value: url })) }));
3430
+ return (jsxRuntime.jsx(BaseInput, { "data-testid": dataTestId ? `${dataTestId}-url-input` : undefined, disabled: disabled, id: "url-input", isInvalid: renderAsInvalid, onChange: e => setUrl(e.target.value), placeholder: rest.placeholder || "https://www.example.com", ref: ref, type: "url", value: url, ...rest, actions: !disableAction && (jsxRuntime.jsx(ActionButton, { "data-testid": (dataTestId && `${dataTestId}-url-input-Icon`) || "url-input-action-icon", disabled: renderAsInvalid || Boolean(disabled) || disableAction, size: fieldSize, type: "WEB_ADDRESS", value: url })) }));
3140
3431
  };
3141
3432
 
3142
3433
  /**
@@ -3295,7 +3586,6 @@ exports.FormFieldSelectAdapter = FormFieldSelectAdapter;
3295
3586
  exports.FormGroup = FormGroup;
3296
3587
  exports.Label = Label;
3297
3588
  exports.MultiSelectField = MultiSelectField;
3298
- exports.MultiSelectMenuItem = MultiSelectMenuItem;
3299
3589
  exports.NumberBaseInput = NumberBaseInput;
3300
3590
  exports.NumberField = NumberField;
3301
3591
  exports.OptionCard = OptionCard;
@@ -3309,7 +3599,6 @@ exports.RadioItem = RadioItem;
3309
3599
  exports.Schedule = Schedule;
3310
3600
  exports.Search = Search;
3311
3601
  exports.SelectField = SelectField;
3312
- exports.SingleSelectMenuItem = SingleSelectMenuItem;
3313
3602
  exports.TextAreaBaseInput = TextAreaBaseInput;
3314
3603
  exports.TextAreaField = TextAreaField;
3315
3604
  exports.TextBaseInput = TextBaseInput;
@@ -3338,15 +3627,21 @@ exports.cvaInputItemPlacementManager = cvaInputItemPlacementManager;
3338
3627
  exports.cvaInputPrefix = cvaInputPrefix;
3339
3628
  exports.cvaInputSuffix = cvaInputSuffix;
3340
3629
  exports.cvaLabel = cvaLabel;
3341
- exports.cvaSelect = cvaSelect;
3630
+ exports.cvaSelectClearIndicator = cvaSelectClearIndicator;
3631
+ exports.cvaSelectContainer = cvaSelectContainer;
3342
3632
  exports.cvaSelectControl = cvaSelectControl;
3343
- exports.cvaSelectCounter = cvaSelectCounter;
3344
- exports.cvaSelectDynamicTagContainer = cvaSelectDynamicTagContainer;
3345
- exports.cvaSelectIcon = cvaSelectIcon;
3633
+ exports.cvaSelectDropdownIconContainer = cvaSelectDropdownIconContainer;
3634
+ exports.cvaSelectDropdownIndicator = cvaSelectDropdownIndicator;
3635
+ exports.cvaSelectIndicatorsContainer = cvaSelectIndicatorsContainer;
3636
+ exports.cvaSelectLoadingMessage = cvaSelectLoadingMessage;
3346
3637
  exports.cvaSelectMenu = cvaSelectMenu;
3347
3638
  exports.cvaSelectMenuList = cvaSelectMenuList;
3639
+ exports.cvaSelectMultiValue = cvaSelectMultiValue;
3640
+ exports.cvaSelectNoOptionsMessage = cvaSelectNoOptionsMessage;
3641
+ exports.cvaSelectPlaceholder = cvaSelectPlaceholder;
3348
3642
  exports.cvaSelectPrefixSuffix = cvaSelectPrefixSuffix;
3349
- exports.cvaSelectXIcon = cvaSelectXIcon;
3643
+ exports.cvaSelectSingleValue = cvaSelectSingleValue;
3644
+ exports.cvaSelectValueContainer = cvaSelectValueContainer;
3350
3645
  exports.getCountryAbbreviation = getCountryAbbreviation;
3351
3646
  exports.getPhoneNumberWithPlus = getPhoneNumberWithPlus;
3352
3647
  exports.isInvalidCountryCode = isInvalidCountryCode;
@@ -3355,9 +3650,11 @@ exports.isValidHEXColor = isValidHEXColor;
3355
3650
  exports.parseSchedule = parseSchedule;
3356
3651
  exports.phoneErrorMessage = phoneErrorMessage;
3357
3652
  exports.serializeSchedule = serializeSchedule;
3653
+ exports.useCreatableSelect = useCreatableSelect;
3358
3654
  exports.useCustomComponents = useCustomComponents;
3359
3655
  exports.useGetPhoneValidationRules = useGetPhoneValidationRules;
3360
3656
  exports.usePhoneInput = usePhoneInput;
3657
+ exports.useSelect = useSelect;
3361
3658
  exports.useZodValidators = useZodValidators;
3362
3659
  exports.validateEmailAddress = validateEmailAddress;
3363
3660
  exports.validatePhoneNumber = validatePhoneNumber;