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