@trackunit/react-form-components 1.8.64 → 1.8.65

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.
package/index.esm.js CHANGED
@@ -1,20 +1,20 @@
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, Text, Heading, Tag, Spinner, MenuItem, useResize, useDebounce, useIsFirstRender } from '@trackunit/react-components';
4
+ import { IconButton, Icon, Tooltip, useIsTextTruncated, MenuItem, Tag, useResize, useDebounce, Spinner, Text, Heading, useIsFirstRender } from '@trackunit/react-components';
5
5
  import { themeSpacing } from '@trackunit/ui-design-tokens';
6
- import { forwardRef, useRef, useEffect, useImperativeHandle, useCallback, useState, useMemo, cloneElement, createContext, useContext, isValidElement, useLayoutEffect } from 'react';
6
+ import { forwardRef, useRef, useEffect, useImperativeHandle, useCallback, useState, isValidElement, cloneElement, useLayoutEffect, 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';
10
10
  import parsePhoneNumberFromString, { isSupportedCountry, getCountries, getCountryCallingCode, AsYouType, parseIncompletePhoneNumber, isValidPhoneNumber } from 'libphonenumber-js';
11
- import { uuidv4, nonNullable } from '@trackunit/shared-utils';
12
- import { Controller } from 'react-hook-form';
13
11
  import ReactSelect, { components } from 'react-select';
14
12
  export { default as ValueType } from 'react-select';
13
+ import ReactAsyncSelect from 'react-select/async';
15
14
  import ReactAsyncCreatableSelect from 'react-select/async-creatable';
16
15
  import ReactCreatableSelect from 'react-select/creatable';
17
- import ReactAsyncSelect from 'react-select/async';
16
+ import { uuidv4, nonNullable } from '@trackunit/shared-utils';
17
+ import { Controller } from 'react-hook-form';
18
18
  import { twMerge } from 'tailwind-merge';
19
19
  import { z } from 'zod';
20
20
 
@@ -697,6 +697,103 @@ const TextAreaBaseInput = ({ id, name, value, rows, disabled, placeholder, readO
697
697
  */
698
698
  const TextBaseInput = ({ ref, ...rest }) => jsx(BaseInput, { ref: ref, type: "text", ...rest });
699
699
 
700
+ const cvaSelect = cvaMerge([
701
+ "relative",
702
+ "flex",
703
+ "shadow-sm",
704
+ "rounded-lg",
705
+ "border-neutral-300",
706
+ "hover:border-neutral-400",
707
+ "hover:bg-neutral-50",
708
+ "bg-white",
709
+ "transition",
710
+ "text-sm",
711
+ "min-h-0",
712
+ ], {
713
+ variants: {
714
+ fieldSize: {
715
+ small: ["h-7", "text-xs"],
716
+ medium: ["h-8"],
717
+ large: ["h-10"],
718
+ },
719
+ invalid: {
720
+ true: "border border-red-600 text-red-600 hover:border-red-600",
721
+ false: "",
722
+ },
723
+ disabled: {
724
+ true: "!bg-neutral-100 hover:border-neutral-300",
725
+ false: "",
726
+ },
727
+ },
728
+ defaultVariants: {
729
+ invalid: false,
730
+ disabled: false,
731
+ },
732
+ });
733
+ const cvaSelectControl = cvaMerge([], {
734
+ variants: {
735
+ isDisabled: {
736
+ true: "!bg-neutral-100",
737
+ false: "",
738
+ },
739
+ prefix: {
740
+ true: ["ps-7"],
741
+ false: "",
742
+ },
743
+ invalid: {
744
+ true: "!border-0",
745
+ false: "",
746
+ },
747
+ },
748
+ defaultVariants: {
749
+ isDisabled: false,
750
+ prefix: false,
751
+ invalid: false,
752
+ },
753
+ });
754
+ const cvaSelectIcon = cvaMerge([
755
+ "mr-2",
756
+ "flex",
757
+ "cursor-pointer",
758
+ "items-center",
759
+ "justify-center",
760
+ "text-neutral-400",
761
+ "hover:text-neutral-500",
762
+ ]);
763
+ const cvaSelectPrefixSuffix = cvaMerge(["flex", "justify-center", "items-center", "text-neutral-400", "absolute", "inset-y-0"], {
764
+ variants: {
765
+ kind: {
766
+ prefix: ["pl-3", "left-0"],
767
+ suffix: ["pr-3", "right-0"],
768
+ },
769
+ },
770
+ });
771
+ const cvaSelectXIcon = cvaMerge([
772
+ "mr-2",
773
+ "flex",
774
+ "cursor-pointer",
775
+ "items-center",
776
+ "justify-center",
777
+ "text-neutral-400",
778
+ "hover:text-neutral-500",
779
+ "ml-1",
780
+ ]);
781
+ const cvaSelectMenuList = cvaMerge([], {
782
+ variants: {
783
+ menuIsOpen: {
784
+ true: "animate-fade-in-fast",
785
+ false: "animate-fade-out-fast",
786
+ },
787
+ },
788
+ });
789
+ const cvaSelectDynamicTagContainer = cvaMerge(["h-full", "flex", "gap-1", "items-center"], {
790
+ variants: {
791
+ visible: { true: "visible", false: "invisible" },
792
+ },
793
+ });
794
+ const cvaSelectCounter = cvaMerge(["overflow-hidden", "whitespace-nowrap"]);
795
+ const cvaSelectMenu = cvaMerge(["relative", "p-1", "grid", "gap-1"]);
796
+
700
797
  /**
701
798
  * Shared CVA for binary control items: Checkbox, RadioItem, ToggleSwitchOption
702
799
  */
@@ -923,137 +1020,709 @@ const Checkbox = ({ className, dataTestId = "checkbox", onChange, checked = fals
923
1020
  Checkbox.displayName = "Checkbox";
924
1021
 
925
1022
  /**
926
- * The Label component is used for labels for input fields.
927
- * This component is **not used directly**, but is part of the FormGroup and Field components.
928
- *
929
- * @param {LabelProps} props - The props for the Label component
930
- * @returns {ReactElement} Label component
931
- */
932
- const Label = ({ id, htmlFor, children, className, dataTestId, disabled, isInvalid, }) => {
933
- return (jsx("label", { className: cvaLabel({ invalid: isInvalid, disabled, className }), "data-testid": dataTestId, htmlFor: htmlFor || "", id: id || "", children: children }));
934
- };
935
-
936
- const cvaFormGroup = cvaMerge(["component-formGroup-gap", "group", "form-group"]);
937
- const cvaFormGroupContainerBefore = cvaMerge(["flex", "mb-1", "items-center"]);
938
- const cvaFormGroupContainerAfter = cvaMerge(["flex", "justify-between", "mt-1", "text-xs", "text-neutral-500"], {
939
- variants: {
940
- invalid: {
941
- true: "text-danger-500",
942
- false: "",
943
- },
944
- isWarning: {
945
- true: "text-default-500 ",
946
- false: "",
947
- },
948
- },
949
- compoundVariants: [
950
- {
951
- invalid: true,
952
- isWarning: true,
953
- className: "text-danger-500 ", // Ensures that 'invalid' takes precedence
954
- },
955
- ],
956
- });
957
- const cvaHelpAddon = cvaMerge(["ml-auto"]);
958
-
959
- /**
960
- * The FormGroup component should be used to wrap any Input element that needs a label.
961
- * Besides a label the component supplies an optional Tooltip, HelpText and HelpAddon support.
1023
+ * A single select menu item is a basic wrapper around Menu item designed to be used as a single value render in Select list
962
1024
  *
963
- * @param {FormGroupProps} props - The props for the FormGroup component
964
- * @returns {ReactElement} FormGroup component
1025
+ * @param {SelectMenuItemProps} props - The props for the SingleSelectMenuItem
1026
+ * @returns {ReactElement} SingleSelectMenuItem
965
1027
  */
966
- const FormGroup = ({ isInvalid, isWarning, helpText, helpAddon, tip, className, dataTestId, label, htmlFor, children, required = false, }) => {
967
- const [t] = useTranslation();
968
- const validationStateIcon = useMemo(() => {
969
- const color = isInvalid ? "danger" : isWarning ? "warning" : null;
970
- return color ? jsx(Icon, { color: color, name: "ExclamationTriangle", size: "small" }) : null;
971
- }, [isInvalid, isWarning]);
972
- return (jsxs("div", { className: cvaFormGroup({ className }), "data-testid": dataTestId, children: [label ? (jsxs("div", { className: cvaFormGroupContainerBefore(), children: [jsxs(Fragment, { children: [jsx(Label, { className: "component-formGroup-font", dataTestId: dataTestId ? `${dataTestId}-label` : undefined, htmlFor: htmlFor, id: htmlFor + "-label", children: label }), required ? (jsx(Tooltip, { "data-testid": "required-asterisk", label: t("field.required.asterisk.tooltip"), children: "*" })) : null] }), tip ? (jsx(Tooltip, { className: "ml-1", dataTestId: dataTestId ? `${dataTestId}-tooltip` : undefined, label: tip, placement: "bottom" })) : null] })) : null, children, helpText || helpAddon ? (jsxs("div", { className: cvaFormGroupContainerAfter({ invalid: isInvalid, isWarning: isWarning }), children: [helpText ? (jsxs("div", { className: "flex gap-1", children: [validationStateIcon, jsx("span", { "data-testid": dataTestId ? `${dataTestId}-helpText` : undefined, children: helpText })] })) : undefined, helpAddon ? (jsx("span", { className: cvaHelpAddon(), "data-testid": dataTestId ? `${dataTestId}-helpAddon` : null, children: helpAddon })) : null] })) : null] }));
1028
+ const SingleSelectMenuItem = ({ label, icon, onClick, selected, focused, dataTestId, disabled, optionLabelDescription, optionPrefix, fieldSize, }) => {
1029
+ return (jsx(MenuItem, { dataTestId: dataTestId, disabled: disabled, fieldSize: fieldSize, focused: focused, label: label, onClick: onClick, optionLabelDescription: optionLabelDescription, optionPrefix: isValidElement(optionPrefix)
1030
+ ? cloneElement(optionPrefix, {
1031
+ className: "mr-1 flex items-center",
1032
+ size: "medium",
1033
+ })
1034
+ : optionPrefix, prefix: icon, selected: selected, suffix: selected ? jsx(Icon, { className: "block text-blue-600", name: "Check", size: "medium" }) : undefined }));
973
1035
  };
974
-
975
1036
  /**
976
- * The checkbox field component is used for entering boolean values.
1037
+ * A multi select menu item is a basic wrapper around Menu item designed to be used as a multi value render in Select list
977
1038
  *
978
- * _**Do use**_ the CheckboxField for boolean input.
1039
+ * @param {SelectMenuItemProps} props - The props for the MultiSelectMenuItem
1040
+ * @returns {ReactElement} multi select menu item
979
1041
  */
980
- const CheckboxField = ({ label, id, tip, helpText, helpAddon, isInvalid, className, checked, dataTestId, checkboxLabel, onChange, ref, ...rest }) => {
981
- const htmlForId = id ? id : "checkboxField-" + uuidv4();
982
- return (jsx(FormGroup, { className: "flex flex-col gap-1", dataTestId: dataTestId ? `${dataTestId}-FormGroup` : undefined, helpAddon: helpAddon, helpText: helpText, htmlFor: htmlForId, label: label, required: rest.required ? !(rest.disabled || rest.readOnly) : false, tip: tip, children: jsx(Checkbox, { checked: checked, className: className, dataTestId: dataTestId, id: htmlForId, label: checkboxLabel, onChange: onChange, ref: ref, ...rest }) }));
1042
+ const MultiSelectMenuItem = ({ label, onClick, selected, focused, dataTestId, disabled, optionLabelDescription, optionPrefix, fieldSize, }) => {
1043
+ return (jsx(MenuItem, { dataTestId: dataTestId, disabled: disabled, fieldSize: fieldSize, focused: focused, label: label, onClick: e => {
1044
+ e.stopPropagation();
1045
+ onClick && onClick(e);
1046
+ }, optionLabelDescription: optionLabelDescription, optionPrefix: isValidElement(optionPrefix)
1047
+ ? cloneElement(optionPrefix, {
1048
+ className: "mr-1 flex items-center",
1049
+ size: "medium",
1050
+ })
1051
+ : optionPrefix, prefix: jsx(Checkbox, { checked: selected, className: "gap-x-0", disabled: disabled, onChange: () => null, onClick: e => {
1052
+ e.stopPropagation();
1053
+ }, readOnly: false }), selected: selected }));
983
1054
  };
984
- CheckboxField.displayName = "CheckboxField";
985
1055
 
986
1056
  /**
1057
+ * Extended Tag component with information about its own width.
1058
+ * Used in the select component.
987
1059
  *
988
- * @param inputValue - value to check if it is a string
989
- * @returns {boolean} - true if value is a string
990
- */
991
- const isString = (inputValue) => {
992
- return typeof inputValue === "string";
993
- };
994
- /**
995
- *
996
- * @param inputValue - value to check if it is a number
997
- * @returns {boolean} - true if value is a number
998
- */
999
- const isNumber = (inputValue) => {
1000
- return typeof inputValue === "number";
1001
- };
1002
-
1003
- /**
1004
- * Validates a url
1060
+ * @param {TagProps} props - The props for the tag component
1061
+ * @returns {ReactElement} TagWithWidth component
1005
1062
  */
1006
- const validateColorCode = (colorCode, required) => {
1007
- if (!colorCode && !required) {
1008
- return undefined;
1009
- }
1010
- if (!colorCode && required) {
1011
- return "REQUIRED";
1012
- }
1013
- if (colorCode && isString(colorCode) && isValidHEXColor(colorCode)) {
1014
- return undefined;
1015
- }
1016
- return "INVALID_HEX_CODE";
1063
+ const TagWithWidth = ({ onWidthKnown, children, ...rest }) => {
1064
+ const ref = useRef(null);
1065
+ useLayoutEffect(() => {
1066
+ onWidthKnown && onWidthKnown({ width: ref.current?.offsetWidth || 0 });
1067
+ }, [ref, onWidthKnown]);
1068
+ return (jsx(Tag, { ref: ref, ...rest, icon: isValidElement(rest.icon) ? cloneElement(rest.icon, { size: "small" }) : rest.icon, children: children }));
1017
1069
  };
1018
1070
 
1019
- const cvaInputColorField = cvaMerge([
1020
- "ml-3",
1021
- "h-4",
1022
- "w-4",
1023
- "self-center",
1024
- "bg-inherit",
1025
- "disabled:opacity-50",
1026
- "disabled:pointer-events-none",
1027
- "rounded-[4px]",
1028
- ], {
1029
- variants: {
1030
- readOnly: {
1031
- true: "pointer-events-none",
1032
- false: "",
1033
- },
1034
- },
1035
- compoundVariants: [
1036
- {
1037
- readOnly: true,
1038
- },
1039
- ],
1040
- defaultVariants: {
1041
- readOnly: false,
1042
- },
1043
- });
1044
-
1045
1071
  /**
1046
- * Validates if the given value is a valid hex color.
1072
+ * TagsContainer component to display tags in limited space when children can't fit space it displays counter
1047
1073
  *
1048
- * @param value - The string value to be validated.
1049
- * @returns {boolean} True if the value is a valid hex color, otherwise false.
1074
+ * @param {TagsContainerProps} props - The props for the TagContainer
1075
+ * @returns {ReactElement} TagsContainer
1050
1076
  */
1051
- const isValidHEXColor = (value) => {
1052
- const hexRegex = /^#([0-9A-F]{6})$/i;
1053
- return hexRegex.test(value);
1054
- };
1055
- /**
1056
- * The ColorField component is used to enter color.
1077
+ const TagsContainer = ({ items, width = "100%", itemsGap = 6, postFix, preFix, disabled, }) => {
1078
+ const containerRef = useRef(null);
1079
+ const [isReady, setIsReady] = useState(false);
1080
+ const [counterWidth, setCounterWidth] = useState(0);
1081
+ const [availableSpaceWidth, setAvailableSpaceWidth] = useState(0);
1082
+ const [childrenWidths, setChildrenWidths] = useState([]);
1083
+ const itemsCount = items.length;
1084
+ const dimensions = useResize();
1085
+ const { width: windowWidth } = useDebounce(dimensions, 100);
1086
+ useEffect(() => {
1087
+ const containerWidth = containerRef.current?.offsetWidth || 0;
1088
+ setAvailableSpaceWidth(containerWidth);
1089
+ }, [windowWidth]);
1090
+ const onWidthKnownHandler = useCallback(({ width: reportedWidth }) => {
1091
+ setChildrenWidths(prev => {
1092
+ const next = [...prev, { width: reportedWidth + itemsGap }];
1093
+ if (next.length === itemsCount) {
1094
+ setIsReady(true);
1095
+ }
1096
+ return next;
1097
+ });
1098
+ }, [itemsCount, itemsGap]);
1099
+ const renderedElements = useMemo(() => {
1100
+ const requiredSpace = childrenWidths.reduce((previous, current) => {
1101
+ return previous + current.width;
1102
+ }, 0);
1103
+ const availableSpace = availableSpaceWidth - counterWidth;
1104
+ const { elements } = items
1105
+ .concat({ text: "", onClick: () => null, disabled: false })
1106
+ .reduce((acc, item, index) => {
1107
+ const spaceNeeded = childrenWidths.slice(0, index + 1).reduce((previous, current) => {
1108
+ return previous + current.width;
1109
+ }, 0);
1110
+ const isLast = index === items.length;
1111
+ const counterRequired = requiredSpace > availableSpace && acc.counter !== 0;
1112
+ if (isLast && counterRequired) {
1113
+ return {
1114
+ ...acc,
1115
+ elements: [
1116
+ ...acc.elements,
1117
+ 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),
1118
+ ],
1119
+ };
1120
+ }
1121
+ if (isLast) {
1122
+ return acc;
1123
+ }
1124
+ const itemCanFit = spaceNeeded <= availableSpace;
1125
+ if (itemCanFit) {
1126
+ return {
1127
+ ...acc,
1128
+ elements: [
1129
+ ...acc.elements,
1130
+ jsx(TagWithWidth, { className: "inline-flex shrink-0", color: item.disabled ? "neutral" : "white", dataTestId: `${item.text}-tag`, disabled: disabled, icon: item.Icon, onClose: e => {
1131
+ e.stopPropagation();
1132
+ item.onClick();
1133
+ }, onWidthKnown: onWidthKnownHandler, children: item.text }, item.text + index),
1134
+ ],
1135
+ };
1136
+ }
1137
+ return {
1138
+ elements: acc.elements,
1139
+ counter: item.text !== "" ? acc.counter + 1 : acc.counter,
1140
+ };
1141
+ }, { elements: [], counter: 0 });
1142
+ return elements;
1143
+ }, [items, availableSpaceWidth, counterWidth, disabled, onWidthKnownHandler, childrenWidths]);
1144
+ return (jsxs("div", { className: cvaSelectDynamicTagContainer({ visible: isReady || !!preFix }), ref: containerRef, style: {
1145
+ width: `${width}`,
1146
+ }, children: [preFix, renderedElements, postFix] }));
1147
+ };
1148
+
1149
+ /**
1150
+ * A hook to retrieve components override object.
1151
+ * 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.
1152
+ *
1153
+ * @template IsMulti
1154
+ * @template Group
1155
+ * @param {Partial<SelectComponents<Option, IsMulti, Group>> | undefined} componentsProps a custom component prop that you can to override defaults
1156
+ * @param {boolean} disabled decide to override disabled variant
1157
+ * @param {boolean} menuIsOpen menu is open state
1158
+ * @param {string} dataTestId a test id
1159
+ * @param {number} maxSelectedDisplayCount a number of max display count
1160
+ * @param {boolean} hasError decide to override hasError variant
1161
+ * @param {ReactNode} prefix a prefix element
1162
+ * @returns {Partial<SelectComponents<Option, boolean, GroupBase<Option>>> | undefined} components object to override react-select default components
1163
+ */
1164
+ const useCustomComponents = ({ componentsProps, disabled, readOnly, setMenuIsEnabled, dataTestId, maxSelectedDisplayCount, prefix, hasError, fieldSize, getOptionLabelDescription, getOptionPrefix, }) => {
1165
+ const [t] = useTranslation();
1166
+ // perhaps it should not be wrap in memo (causing some issues with opening and closing on mobiles)
1167
+ const customComponents = useMemo(() => {
1168
+ return {
1169
+ ValueContainer: props => {
1170
+ if (props.isMulti && Array.isArray(props.children) && props.children.length > 0) {
1171
+ const PLACEHOLDER_KEY = "placeholder";
1172
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1173
+ const key = props && props.children && props.children[0] ? props.children[0]?.key : "";
1174
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1175
+ const values = props && props.children ? props.children[0] : [];
1176
+ const tags = key === PLACEHOLDER_KEY ? [] : values;
1177
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1178
+ const searchInput = props && props.children && props.children[1];
1179
+ const placeholderElement = Array.isArray(props.children)
1180
+ ? props.children.find(child => child && child.key === PLACEHOLDER_KEY)
1181
+ : null;
1182
+ return (jsx(components.ValueContainer, { ...props, isDisabled: props.selectProps.isDisabled, children: maxSelectedDisplayCount === undefined ? (jsx(TagsContainer, { disabled: disabled, items: tags
1183
+ ? tags.map(({ props: tagProps }) => {
1184
+ const optionPrefix = tagProps.data && getOptionPrefix ? getOptionPrefix(tagProps.data) : null;
1185
+ return {
1186
+ text: tagProps.children,
1187
+ onClick: disabled
1188
+ ? undefined
1189
+ : (e) => {
1190
+ setMenuIsEnabled(false);
1191
+ tagProps.removeProps.onClick && tagProps.removeProps.onClick(e);
1192
+ },
1193
+ disabled: disabled,
1194
+ Icon: optionPrefix,
1195
+ };
1196
+ })
1197
+ : [], postFix: searchInput, preFix: placeholderElement ? jsx("span", { className: "absolute", children: placeholderElement }) : null, width: "100%" })) : (jsxs(Fragment, { children: [tags
1198
+ ? tags.slice(0, maxSelectedDisplayCount).map(({ props: tagProps }) => {
1199
+ return (jsx(Tag, { className: "inline-flex shrink-0", color: disabled ? "unknown" : "primary", dataTestId: tagProps.children ? `${tagProps.children.toString()}-tag` : undefined, onClose: e => {
1200
+ e.stopPropagation();
1201
+ setMenuIsEnabled(false);
1202
+ tagProps.removeProps.onClick && tagProps.removeProps.onClick(e);
1203
+ }, children: tagProps.children }, tagProps.children?.toString()));
1204
+ })
1205
+ : null, tags && tags.length > maxSelectedDisplayCount ? (jsxs(Tag, { color: "neutral", dataTestId: "counter-tag", children: ["+", tags.length - maxSelectedDisplayCount] })) : null, searchInput, placeholderElement] })) }));
1206
+ }
1207
+ return (jsx(Fragment, { children: jsx(components.ValueContainer, { ...props, isDisabled: props.selectProps.isDisabled, children: props.children }) }));
1208
+ },
1209
+ LoadingIndicator: () => {
1210
+ return jsx(Spinner, { centering: "vertically", className: "mr-2", size: "small" });
1211
+ },
1212
+ DropdownIndicator: props => {
1213
+ const icon = props.selectProps.menuIsOpen ? (jsx(Icon, { name: "ChevronUp", size: "medium" })) : (jsx(Icon, { name: "ChevronDown", size: "medium" }));
1214
+ return props.selectProps.isLoading || props.selectProps.isDisabled || readOnly ? null : (jsx(components.DropdownIndicator, { ...props, children: jsx("div", { className: cvaSelectIcon(), children: icon }) }));
1215
+ },
1216
+ IndicatorSeparator: () => null,
1217
+ ClearIndicator: props => {
1218
+ if (disabled) {
1219
+ return null;
1220
+ }
1221
+ return (jsx(components.ClearIndicator, { ...props, innerProps: {
1222
+ ...props.innerProps,
1223
+ onMouseDown: e => {
1224
+ e.preventDefault();
1225
+ },
1226
+ }, 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" }) }) }));
1227
+ },
1228
+ Control: props => {
1229
+ return (jsx(components.Control, { ...props, className: cvaSelectControl({
1230
+ isDisabled: props.isDisabled,
1231
+ prefix: prefix ? true : false,
1232
+ invalid: hasError,
1233
+ }) }));
1234
+ },
1235
+ SingleValue: props => {
1236
+ const optionPrefix = getOptionPrefix ? getOptionPrefix(props.data) : null;
1237
+ 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] }) }));
1238
+ },
1239
+ Menu: props => {
1240
+ return (jsx(components.Menu, { ...props, className: cvaSelectMenuList({ menuIsOpen: props.selectProps.menuIsOpen }) }));
1241
+ },
1242
+ Placeholder: props => {
1243
+ return (jsx(components.Placeholder, { ...props, className: "!text-neutral-400", children: props.children }));
1244
+ },
1245
+ MenuList: props => {
1246
+ return (jsx(components.MenuList, { ...props, innerProps: {
1247
+ ...props.innerProps,
1248
+ onScroll: e => {
1249
+ const listEl = e.currentTarget;
1250
+ if (listEl.scrollTop + listEl.clientHeight >= listEl.scrollHeight &&
1251
+ props.selectProps.onMenuScrollToBottom) {
1252
+ /Firefox/.test(navigator.userAgent)
1253
+ ? props.selectProps.onMenuScrollToBottom(new WheelEvent("scroll"))
1254
+ : props.selectProps.onMenuScrollToBottom(new TouchEvent(""));
1255
+ }
1256
+ },
1257
+ }, children: props.children }));
1258
+ },
1259
+ Option: props => {
1260
+ const componentProps = {
1261
+ label: props.label,
1262
+ focused: props.isFocused,
1263
+ selected: props.isSelected,
1264
+ onClick: props.innerProps.onClick,
1265
+ };
1266
+ return (jsx(components.Option, { ...props, innerProps: {
1267
+ ...props.innerProps,
1268
+ role: "option",
1269
+ onClick: () => { },
1270
+ }, children: props.isMulti ? (jsx(MultiSelectMenuItem, { ...componentProps, dataTestId: typeof props.label === "string" ? props.label : undefined, disabled: disabled, fieldSize: fieldSize, optionLabelDescription: getOptionLabelDescription?.(props.data), optionPrefix: getOptionPrefix?.(props.data) })) : (jsx(SingleSelectMenuItem, { ...componentProps, dataTestId: typeof props.label === "string" ? props.label : undefined, disabled: disabled || props.isDisabled, fieldSize: fieldSize, optionLabelDescription: getOptionLabelDescription?.(props.data), optionPrefix: getOptionPrefix?.(props.data) })) }));
1271
+ },
1272
+ ...componentsProps,
1273
+ };
1274
+ }, [
1275
+ componentsProps,
1276
+ maxSelectedDisplayCount,
1277
+ disabled,
1278
+ setMenuIsEnabled,
1279
+ readOnly,
1280
+ dataTestId,
1281
+ t,
1282
+ prefix,
1283
+ hasError,
1284
+ getOptionLabelDescription,
1285
+ fieldSize,
1286
+ getOptionPrefix,
1287
+ ]);
1288
+ return customComponents;
1289
+ };
1290
+
1291
+ /**
1292
+ * @template IsMulti
1293
+ * @template Group
1294
+ * @param {RefObject<HTMLDivElement | null>} refContainer react ref to container element
1295
+ * @param {number | undefined} maxSelectedDisplayCount a number of max display count
1296
+ * @param {StylesConfig<Option, IsMulti, Group> | undefined} styles a optional object to override styles of react-select
1297
+ * @returns {StylesConfig<Option, boolean>} styles to override in select
1298
+ */
1299
+ const useCustomStyles = ({ refContainer, maxSelectedDisplayCount, styles, disabled, fieldSize, }) => {
1300
+ const customStyles = useMemo(() => {
1301
+ return {
1302
+ control: base => {
1303
+ return {
1304
+ ...base,
1305
+ minHeight: fieldSize === "small" ? "28px" : fieldSize === "large" ? "40px" : "32px",
1306
+ borderRadius: "var(--border-radius-lg)",
1307
+ backgroundColor: "inherit",
1308
+ };
1309
+ },
1310
+ singleValue: base => ({
1311
+ ...base,
1312
+ }),
1313
+ multiValue: base => ({
1314
+ ...base,
1315
+ }),
1316
+ multiValueLabel: base => ({
1317
+ ...base,
1318
+ }),
1319
+ indicatorsContainer: base => ({
1320
+ ...base,
1321
+ ...(disabled && { display: "none" }),
1322
+ }),
1323
+ indicatorSeparator: () => ({
1324
+ width: "0px",
1325
+ }),
1326
+ menu: base => {
1327
+ return {
1328
+ ...base,
1329
+ width: "100%",
1330
+ marginTop: "4px",
1331
+ marginBottom: "18px",
1332
+ transition: "all 1s ease-in-out",
1333
+ };
1334
+ },
1335
+ input: base => ({
1336
+ ...base,
1337
+ marginLeft: "0px",
1338
+ }),
1339
+ placeholder: base => ({
1340
+ ...base,
1341
+ }),
1342
+ option: () => ({}),
1343
+ menuPortal: base => ({
1344
+ ...base,
1345
+ width: refContainer.current ? `${refContainer.current.clientWidth}px` : base.width,
1346
+ backgroundColor: "#ffffff",
1347
+ borderRadius: "var(--border-radius-lg)",
1348
+ zIndex: "var(--z-overlay)",
1349
+ borderColor: "rgb(var(--color-neutral-300))",
1350
+ boxShadow: "var(--tw-ring-inset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)",
1351
+ }),
1352
+ menuList: base => {
1353
+ return {
1354
+ ...base,
1355
+ position: "relative",
1356
+ padding: "var(--spacing-1)",
1357
+ display: "grid",
1358
+ gap: "var(--spacing-1)",
1359
+ width: "100%",
1360
+ borderRadius: "0px",
1361
+ boxShadow: "none",
1362
+ paddingTop: "0px",
1363
+ };
1364
+ },
1365
+ valueContainer: base => {
1366
+ return {
1367
+ ...base,
1368
+ paddingBlock: 0,
1369
+ flexWrap: maxSelectedDisplayCount !== undefined ? "wrap" : "nowrap",
1370
+ gap: "0.25rem",
1371
+ };
1372
+ },
1373
+ container: base => ({
1374
+ ...base,
1375
+ width: "100%",
1376
+ }),
1377
+ dropdownIndicator: base => ({
1378
+ ...base,
1379
+ padding: "0px",
1380
+ }),
1381
+ clearIndicator: base => {
1382
+ return {
1383
+ ...base,
1384
+ padding: "0px",
1385
+ };
1386
+ },
1387
+ ...styles,
1388
+ };
1389
+ }, [refContainer, disabled, fieldSize, maxSelectedDisplayCount, styles]);
1390
+ return { customStyles };
1391
+ };
1392
+
1393
+ /**
1394
+ * A hook used by selects to share the common code
1395
+ *
1396
+ * @param {SelectProps} props - The props for the Select component
1397
+ * @returns {UseSelectProps} Select component
1398
+ */
1399
+ const useSelect = ({ id, className, 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 }) => {
1400
+ const refContainer = useRef(document.createElement("div"));
1401
+ const { customStyles } = useCustomStyles({
1402
+ refContainer,
1403
+ maxSelectedDisplayCount,
1404
+ styles,
1405
+ disabled: Boolean(disabled),
1406
+ fieldSize,
1407
+ });
1408
+ const [menuIsOpen, setMenuIsOpen] = useState(props.menuIsOpen ?? false);
1409
+ const [menuIsEnabled, setMenuIsEnabled] = useState(true);
1410
+ const customComponents = useCustomComponents({
1411
+ componentsProps: components,
1412
+ disabled: Boolean(disabled),
1413
+ readOnly: Boolean(props.readOnly),
1414
+ setMenuIsEnabled,
1415
+ dataTestId,
1416
+ maxSelectedDisplayCount,
1417
+ prefix,
1418
+ hasError,
1419
+ fieldSize,
1420
+ getOptionLabelDescription,
1421
+ getOptionPrefix,
1422
+ });
1423
+ const menuPlacement = "auto";
1424
+ const openMenuHandler = async () => {
1425
+ onMenuOpen?.();
1426
+ if (menuIsEnabled) {
1427
+ setMenuIsOpen(true);
1428
+ }
1429
+ else {
1430
+ setMenuIsEnabled(true);
1431
+ }
1432
+ };
1433
+ const closeMenuHandler = () => {
1434
+ setMenuIsOpen(false);
1435
+ onMenuClose && onMenuClose();
1436
+ };
1437
+ return {
1438
+ refContainer,
1439
+ customStyles,
1440
+ menuIsOpen,
1441
+ customComponents,
1442
+ menuPlacement,
1443
+ openMenuHandler,
1444
+ closeMenuHandler,
1445
+ };
1446
+ };
1447
+
1448
+ // This is here to ensure the bundled react-components can expose the react-select for jest in external iris apps.
1449
+ const ReactSyncSelect = ReactSelect.default || ReactSelect;
1450
+ /**
1451
+ * Selects are input components used to choose a value from a set.
1452
+ *
1453
+ * @param {SelectProps} props - The props for the Select component
1454
+ * @returns {ReactElement} Select component
1455
+ */
1456
+ const BaseSelect = (props) => {
1457
+ const { id, 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;
1458
+ const { refContainer, customStyles, menuIsOpen, customComponents, menuPlacement, openMenuHandler, closeMenuHandler } = useSelect(props);
1459
+ const reactSelectProps = useMemo(() => ({
1460
+ value,
1461
+ menuPlacement,
1462
+ maxMenuHeight,
1463
+ onChange,
1464
+ "aria-label": label,
1465
+ "data-testid": dataTestId,
1466
+ components: customComponents,
1467
+ styles: customStyles,
1468
+ tabSelectsValue: false,
1469
+ blurInputOnSelect: false,
1470
+ // This configuration allows for more flexible positioning control of the dropdown.
1471
+ // Setting menuPortalTarget to 'null' specifies that the dropdown should be rendered within
1472
+ // the parent element instead of 'document.body'.
1473
+ menuPortalTarget: props.menuPortalTarget !== undefined ? props.menuPortalTarget : document.body,
1474
+ isSearchable: disabled || readOnly ? false : isSearchable,
1475
+ menuShouldBlockScroll: true,
1476
+ menuShouldScrollIntoView: true,
1477
+ openMenuOnFocus,
1478
+ menuIsOpen: !readOnly ? menuIsOpen : false,
1479
+ openMenuOnClick,
1480
+ closeMenuOnSelect: !isMulti,
1481
+ isMulti,
1482
+ classNamePrefix,
1483
+ isLoading,
1484
+ isClearable,
1485
+ id,
1486
+ onMenuScrollToBottom,
1487
+ onInputChange,
1488
+ hideSelectedOptions,
1489
+ isDisabled: Boolean(disabled),
1490
+ }), [
1491
+ classNamePrefix,
1492
+ customComponents,
1493
+ customStyles,
1494
+ dataTestId,
1495
+ disabled,
1496
+ hideSelectedOptions,
1497
+ id,
1498
+ isClearable,
1499
+ isLoading,
1500
+ isMulti,
1501
+ isSearchable,
1502
+ label,
1503
+ maxMenuHeight,
1504
+ menuIsOpen,
1505
+ menuPlacement,
1506
+ onChange,
1507
+ onInputChange,
1508
+ onMenuScrollToBottom,
1509
+ openMenuOnClick,
1510
+ openMenuOnFocus,
1511
+ props.menuPortalTarget,
1512
+ readOnly,
1513
+ value,
1514
+ ]);
1515
+ const renderAsDisabled = Boolean(props.disabled) || props.readOnly;
1516
+ return (jsxs("div", { className: cvaSelect({
1517
+ invalid: hasError,
1518
+ fieldSize: fieldSize,
1519
+ disabled: renderAsDisabled,
1520
+ className: props.className,
1521
+ }), "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] }));
1522
+ };
1523
+ BaseSelect.displayName = "BaseSelect";
1524
+
1525
+ /**
1526
+ * CreatableSelects are input components used to choose a value from a set.
1527
+ *
1528
+ * @param {CreatableSelectProps} props - The props for the CreatableSelect component
1529
+ * @returns {ReactElement} CreatableSelect component
1530
+ */
1531
+ const CreatableSelect = (props) => {
1532
+ const { id, 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;
1533
+ const { refContainer, customStyles, menuIsOpen, customComponents, menuPlacement, openMenuHandler, closeMenuHandler } = useSelect(props);
1534
+ const reactCreatableSelectProps = useMemo(() => ({
1535
+ value,
1536
+ menuPlacement,
1537
+ maxMenuHeight,
1538
+ onChange,
1539
+ "aria-label": label,
1540
+ "data-testid": dataTestId,
1541
+ components: customComponents,
1542
+ styles: customStyles,
1543
+ tabSelectsValue: false,
1544
+ blurInputOnSelect: !isMulti,
1545
+ menuPortalTarget: props.menuPortalTarget || document.body,
1546
+ isSearchable: disabled || readOnly ? false : isSearchable,
1547
+ menuShouldBlockScroll: true,
1548
+ menuShouldScrollIntoView: true,
1549
+ openMenuOnFocus,
1550
+ menuIsOpen: !readOnly ? menuIsOpen : false,
1551
+ openMenuOnClick,
1552
+ closeMenuOnSelect: false,
1553
+ isMulti,
1554
+ classNamePrefix,
1555
+ isLoading,
1556
+ isClearable,
1557
+ id,
1558
+ onMenuScrollToBottom,
1559
+ onInputChange,
1560
+ allowCreateWhileLoading,
1561
+ onCreateOption,
1562
+ isDisabled: Boolean(disabled),
1563
+ }), [
1564
+ allowCreateWhileLoading,
1565
+ classNamePrefix,
1566
+ customComponents,
1567
+ customStyles,
1568
+ dataTestId,
1569
+ disabled,
1570
+ id,
1571
+ isClearable,
1572
+ isLoading,
1573
+ isMulti,
1574
+ isSearchable,
1575
+ label,
1576
+ maxMenuHeight,
1577
+ menuIsOpen,
1578
+ menuPlacement,
1579
+ onChange,
1580
+ onCreateOption,
1581
+ onInputChange,
1582
+ onMenuScrollToBottom,
1583
+ openMenuOnClick,
1584
+ openMenuOnFocus,
1585
+ props.menuPortalTarget,
1586
+ readOnly,
1587
+ value,
1588
+ ]);
1589
+ const renderAsDisabled = Boolean(props.disabled) || props.readOnly;
1590
+ 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] }));
1591
+ };
1592
+ CreatableSelect.displayName = "CreatableSelect";
1593
+
1594
+ /**
1595
+ * The Label component is used for labels for input fields.
1596
+ * This component is **not used directly**, but is part of the FormGroup and Field components.
1597
+ *
1598
+ * @param {LabelProps} props - The props for the Label component
1599
+ * @returns {ReactElement} Label component
1600
+ */
1601
+ const Label = ({ id, htmlFor, children, className, dataTestId, disabled, isInvalid, }) => {
1602
+ return (jsx("label", { className: cvaLabel({ invalid: isInvalid, disabled, className }), "data-testid": dataTestId, htmlFor: htmlFor || "", id: id || "", children: children }));
1603
+ };
1604
+
1605
+ const cvaFormGroup = cvaMerge(["component-formGroup-gap", "group", "form-group"]);
1606
+ const cvaFormGroupContainerBefore = cvaMerge(["flex", "mb-1", "items-center"]);
1607
+ const cvaFormGroupContainerAfter = cvaMerge(["flex", "justify-between", "mt-1", "text-xs", "text-neutral-500"], {
1608
+ variants: {
1609
+ invalid: {
1610
+ true: "text-danger-500",
1611
+ false: "",
1612
+ },
1613
+ isWarning: {
1614
+ true: "text-default-500 ",
1615
+ false: "",
1616
+ },
1617
+ },
1618
+ compoundVariants: [
1619
+ {
1620
+ invalid: true,
1621
+ isWarning: true,
1622
+ className: "text-danger-500 ", // Ensures that 'invalid' takes precedence
1623
+ },
1624
+ ],
1625
+ });
1626
+ const cvaHelpAddon = cvaMerge(["ml-auto"]);
1627
+
1628
+ /**
1629
+ * The FormGroup component should be used to wrap any Input element that needs a label.
1630
+ * Besides a label the component supplies an optional Tooltip, HelpText and HelpAddon support.
1631
+ *
1632
+ * @param {FormGroupProps} props - The props for the FormGroup component
1633
+ * @returns {ReactElement} FormGroup component
1634
+ */
1635
+ const FormGroup = ({ isInvalid, isWarning, helpText, helpAddon, tip, className, dataTestId, label, htmlFor, children, required = false, }) => {
1636
+ const [t] = useTranslation();
1637
+ const validationStateIcon = useMemo(() => {
1638
+ const color = isInvalid ? "danger" : isWarning ? "warning" : null;
1639
+ return color ? jsx(Icon, { color: color, name: "ExclamationTriangle", size: "small" }) : null;
1640
+ }, [isInvalid, isWarning]);
1641
+ return (jsxs("div", { className: cvaFormGroup({ className }), "data-testid": dataTestId, children: [label ? (jsxs("div", { className: cvaFormGroupContainerBefore(), children: [jsxs(Fragment, { children: [jsx(Label, { className: "component-formGroup-font", dataTestId: dataTestId ? `${dataTestId}-label` : undefined, htmlFor: htmlFor, id: htmlFor + "-label", children: label }), required ? (jsx(Tooltip, { "data-testid": "required-asterisk", label: t("field.required.asterisk.tooltip"), children: "*" })) : null] }), tip ? (jsx(Tooltip, { className: "ml-1", dataTestId: dataTestId ? `${dataTestId}-tooltip` : undefined, label: tip, placement: "bottom" })) : null] })) : null, children, helpText || helpAddon ? (jsxs("div", { className: cvaFormGroupContainerAfter({ invalid: isInvalid, isWarning: isWarning }), children: [helpText ? (jsxs("div", { className: "flex gap-1", children: [validationStateIcon, jsx("span", { "data-testid": dataTestId ? `${dataTestId}-helpText` : undefined, children: helpText })] })) : undefined, helpAddon ? (jsx("span", { className: cvaHelpAddon(), "data-testid": dataTestId ? `${dataTestId}-helpAddon` : null, children: helpAddon })) : null] })) : null] }));
1642
+ };
1643
+
1644
+ /**
1645
+ * The checkbox field component is used for entering boolean values.
1646
+ *
1647
+ * _**Do use**_ the CheckboxField for boolean input.
1648
+ */
1649
+ const CheckboxField = ({ label, id, tip, helpText, helpAddon, isInvalid, className, checked, dataTestId, checkboxLabel, onChange, ref, ...rest }) => {
1650
+ const htmlForId = id ? id : "checkboxField-" + uuidv4();
1651
+ return (jsx(FormGroup, { className: "flex flex-col gap-1", dataTestId: dataTestId ? `${dataTestId}-FormGroup` : undefined, helpAddon: helpAddon, helpText: helpText, htmlFor: htmlForId, label: label, required: rest.required ? !(rest.disabled || rest.readOnly) : false, tip: tip, children: jsx(Checkbox, { checked: checked, className: className, dataTestId: dataTestId, id: htmlForId, label: checkboxLabel, onChange: onChange, ref: ref, ...rest }) }));
1652
+ };
1653
+ CheckboxField.displayName = "CheckboxField";
1654
+
1655
+ /**
1656
+ *
1657
+ * @param inputValue - value to check if it is a string
1658
+ * @returns {boolean} - true if value is a string
1659
+ */
1660
+ const isString = (inputValue) => {
1661
+ return typeof inputValue === "string";
1662
+ };
1663
+ /**
1664
+ *
1665
+ * @param inputValue - value to check if it is a number
1666
+ * @returns {boolean} - true if value is a number
1667
+ */
1668
+ const isNumber = (inputValue) => {
1669
+ return typeof inputValue === "number";
1670
+ };
1671
+
1672
+ /**
1673
+ * Validates a url
1674
+ */
1675
+ const validateColorCode = (colorCode, required) => {
1676
+ if (!colorCode && !required) {
1677
+ return undefined;
1678
+ }
1679
+ if (!colorCode && required) {
1680
+ return "REQUIRED";
1681
+ }
1682
+ if (colorCode && isString(colorCode) && isValidHEXColor(colorCode)) {
1683
+ return undefined;
1684
+ }
1685
+ return "INVALID_HEX_CODE";
1686
+ };
1687
+
1688
+ const cvaInputColorField = cvaMerge([
1689
+ "ml-3",
1690
+ "h-4",
1691
+ "w-4",
1692
+ "self-center",
1693
+ "bg-inherit",
1694
+ "disabled:opacity-50",
1695
+ "disabled:pointer-events-none",
1696
+ "rounded-[4px]",
1697
+ ], {
1698
+ variants: {
1699
+ readOnly: {
1700
+ true: "pointer-events-none",
1701
+ false: "",
1702
+ },
1703
+ },
1704
+ compoundVariants: [
1705
+ {
1706
+ readOnly: true,
1707
+ },
1708
+ ],
1709
+ defaultVariants: {
1710
+ readOnly: false,
1711
+ },
1712
+ });
1713
+
1714
+ /**
1715
+ * Validates if the given value is a valid hex color.
1716
+ *
1717
+ * @param value - The string value to be validated.
1718
+ * @returns {boolean} True if the value is a valid hex color, otherwise false.
1719
+ */
1720
+ const isValidHEXColor = (value) => {
1721
+ const hexRegex = /^#([0-9A-F]{6})$/i;
1722
+ return hexRegex.test(value);
1723
+ };
1724
+ /**
1725
+ * The ColorField component is used to enter color.
1057
1726
  * ColorField validates that user enters a valid color address.
1058
1727
  *
1059
1728
  */
@@ -1310,6 +1979,73 @@ const EmailField = ({ label, id, tip, helpText, errorMessage, helpAddon, classNa
1310
1979
  };
1311
1980
  EmailField.displayName = "EmailField";
1312
1981
 
1982
+ /** Type guard for function refs vs. object refs without deprecated types or assertions */
1983
+ function isWritableRef(r) {
1984
+ return typeof r === "object" && r !== null && "current" in r;
1985
+ }
1986
+ /**
1987
+ * Multi adapter:
1988
+ * - keeps Option[] semantics (via `MultiValue<Option>`)
1989
+ * - renders FormGroup chrome (label, help, error)
1990
+ * - exposes a hidden <select> for a stable ref target
1991
+ * - optionally renders one hidden <input> per selected option IF `getOptionValue` is provided
1992
+ * - passes through all remaining BaseSelect props with isMulti=true
1993
+ */
1994
+ const FormFieldSelectAdapterMulti = (props) => {
1995
+ const { className, dataTestId, helpText, helpAddon, tip, label, isInvalid, errorMessage, name, onBlur, options, value, defaultValue, id, onChange, children, ref, ...selectProps } = props;
1996
+ // Hidden select for a stable DOM ref target (API parity with single adapter)
1997
+ const innerRef = useRef(null);
1998
+ // Bridge external ref (supports both callback and object refs)
1999
+ useEffect(() => {
2000
+ if (typeof ref === "function") {
2001
+ ref(innerRef.current);
2002
+ }
2003
+ else if (isWritableRef(ref)) {
2004
+ ref.current = innerRef.current;
2005
+ }
2006
+ }, [ref]);
2007
+ // Determine invalid state
2008
+ const renderAsInvalid = useMemo(() => (isInvalid === undefined ? Boolean(errorMessage) : isInvalid), [errorMessage, isInvalid]);
2009
+ // id to connect label and control
2010
+ const controlId = useMemo(() => (id ? id : "multiSelectField-" + uuidv4()), [id]);
2011
+ // If consumers provided getOptionValue (from BaseSelect props),
2012
+ // we can render hidden inputs for native form submit / RHF.
2013
+ const selectPropsWithAccessors = selectProps;
2014
+ const getOptionValue = typeof selectPropsWithAccessors.getOptionValue === "function" ? selectPropsWithAccessors.getOptionValue : undefined;
2015
+ // Compute selected options snapshot for hidden inputs (prefer controlled `value`)
2016
+ const selectedOptions = useMemo(() => value ?? defaultValue ?? [], [value, defaultValue]);
2017
+ // Build the exact prop bag for BaseSelect (multi=true).
2018
+ const childProps = {
2019
+ ...selectProps,
2020
+ id: controlId,
2021
+ onBlur,
2022
+ options,
2023
+ isMulti: true,
2024
+ value: value ?? null,
2025
+ defaultValue,
2026
+ onChange: next => onChange?.(next),
2027
+ };
2028
+ return (jsxs(FormGroup, { className: className, dataTestId: dataTestId, helpAddon: helpAddon, helpText: (renderAsInvalid && errorMessage) || helpText, htmlFor: controlId, isInvalid: renderAsInvalid, label: label, required: "required" in selectProps && selectProps.required
2029
+ ? !(("disabled" in selectProps && Boolean(selectProps.disabled)) ||
2030
+ ("readOnly" in selectProps && Boolean(selectProps.readOnly)))
2031
+ : false, tip: tip, children: [jsx("select", { "aria-hidden": "true", defaultValue: "", hidden: true, name: name, ref: innerRef }), typeof getOptionValue === "function" &&
2032
+ selectedOptions.map((opt, idx) => {
2033
+ const primitiveValue = getOptionValue(opt);
2034
+ return typeof primitiveValue === "string" ? (jsx("input", { name: name, type: "hidden", value: primitiveValue }, `${primitiveValue}-${idx}`)) : null;
2035
+ }), children(childProps)] }));
2036
+ };
2037
+ FormFieldSelectAdapterMulti.displayName = "FormFieldSelectAdapterMulti";
2038
+
2039
+ /**
2040
+ * MultiSelectField — validated multi-select field.
2041
+ * Types mirror BaseSelect: options: Option[], value/defaultValue: Option[], onChange: (Option[] | null) => void
2042
+ * Implemented as a generic const component (no forwardRef, no assertions).
2043
+ */
2044
+ const MultiSelectField = ({ ref, ...props }) => {
2045
+ return (jsx(FormFieldSelectAdapterMulti, { ...props, ref: ref, children: convertedProps => jsx(BaseSelect, { ...convertedProps }) }));
2046
+ };
2047
+ MultiSelectField.displayName = "MultiSelectField";
2048
+
1313
2049
  const isNumberValid = (number) => {
1314
2050
  if (!isNaN(+number) === false) {
1315
2051
  return false;
@@ -1351,1333 +2087,664 @@ const validateNumber = (number, required = false, min, max) => {
1351
2087
  }
1352
2088
  // if the value is greater than max
1353
2089
  if (isNumberValid(parsedNumber) && maxValue !== undefined && parsedNumber > maxValue) {
1354
- return "LESS_THAN";
1355
- }
1356
- // if the value is a number and is valid
1357
- if (isNumber(parsedNumber) && isNumberValid(parsedNumber)) {
1358
- return undefined;
1359
- }
1360
- return "INVALID_NUMBER";
1361
- };
1362
-
1363
- /**
1364
- * The number field component is used for entering numeric values and includes controls for incrementally increasing or decreasing the value.
1365
- *
1366
- * _**Do use**_ the NumberField when the controls to incrementally increase or decrease makes the task easier for the user.
1367
- *
1368
- * _**Do not use**_ this fields for non-serialized numbers. Use TextField instead.
1369
- */
1370
- const NumberField = ({ label, id, tip, helpText, errorMessage, helpAddon, isInvalid, maxLength, className, value, dataTestId, defaultValue, onBlur, onChange, ref, ...rest }) => {
1371
- const htmlForId = id ? id : "numberField-" + uuidv4();
1372
- const [t] = useTranslation();
1373
- const [innerValue, setInnerValue] = useState(() => {
1374
- return Number(value?.toString()) || Number(defaultValue?.toString());
1375
- });
1376
- const [renderAsInvalid, setRenderAsInvalid] = useState((isInvalid === undefined ? Boolean(errorMessage) : isInvalid) ||
1377
- !!validateNumber(value?.toString(), rest.required, rest.min, rest.max));
1378
- const errorType = useMemo(() => validateNumber(innerValue, rest.required, rest.min, rest.max), [innerValue, rest.max, rest.min, rest.required]);
1379
- const error = useMemo(() => {
1380
- // for the case when a custom error message is provided
1381
- if (errorMessage) {
1382
- return errorMessage;
1383
- }
1384
- else if (errorType) {
1385
- return t(`numberField.error.${errorType}`, { min: rest.min, max: rest.max });
1386
- }
1387
- return errorMessage;
1388
- }, [errorMessage, errorType, rest.max, rest.min, t]);
1389
- useEffect(() => {
1390
- if (errorMessage) {
1391
- setRenderAsInvalid(Boolean(errorMessage));
1392
- }
1393
- }, [errorMessage]);
1394
- const handleBlur = useCallback(event => {
1395
- const newValue = event.target.value;
1396
- setInnerValue(newValue.toString());
1397
- // for the case when a custom error message is provided
1398
- if (errorMessage && !validateNumber(newValue, rest.required, rest.min, rest.max)) {
1399
- setRenderAsInvalid(Boolean(errorMessage));
1400
- }
1401
- else {
1402
- setRenderAsInvalid(!!validateNumber(newValue, rest.required, rest.min, rest.max));
1403
- }
1404
- onBlur?.(event);
1405
- }, [errorMessage, onBlur, rest.max, rest.min, rest.required]);
1406
- const handleChange = useCallback((event) => {
1407
- setInnerValue(event.target.value);
1408
- if (onChange) {
1409
- onChange(event);
1410
- }
1411
- }, [onChange]);
1412
- return (jsx(FormGroup, { dataTestId: dataTestId ? `${dataTestId}-FormGroup` : undefined, helpAddon: helpAddon, helpText: (renderAsInvalid && error) || helpText, htmlFor: htmlForId, isInvalid: renderAsInvalid, label: label, required: rest.required ? !(rest.disabled || rest.readOnly) : false, tip: tip, children: jsx(NumberBaseInput, { "aria-labelledby": htmlForId + "-label", id: htmlForId, isInvalid: renderAsInvalid, maxLength: maxLength, onBlur: handleBlur, onChange: handleChange, ref: ref, value: value, ...rest, className: className, dataTestId: dataTestId }) }));
1413
- };
1414
- NumberField.displayName = "NumberField";
1415
-
1416
- const cvaOptionCardLabel = cvaMerge([
1417
- "group",
1418
- "transition",
1419
- "bg-white",
1420
- "outline",
1421
- "outline-1",
1422
- "outline-neutral-300",
1423
- "hover:bg-neutral-100",
1424
- "focus:bg-neutral-200",
1425
- "active:bg-neutral-200",
1426
- "peer-checked:bg-primary-50",
1427
- "peer-checked:outline-primary-600",
1428
- "peer-checked:outline-2",
1429
- "flex",
1430
- "gap-2",
1431
- "justify-center",
1432
- "items-center",
1433
- "text-center",
1434
- "rounded-md",
1435
- "relative",
1436
- ], {
1437
- variants: {
1438
- disabled: {
1439
- true: ["cursor-not-allowed", "bg-neutral-100"],
1440
- false: ["cursor-pointer"],
1441
- },
1442
- layout: {
1443
- default: ["flex-col", "p-responsive-space", "w-full", "aspect-square"],
1444
- compact: ["px-3", "py-1.5", "h-8", "min-h-[calc(var(--line-height-sm)+var(--spacing-3))]", "flex-row", "w-fit"],
1445
- },
1446
- },
1447
- });
1448
- const cvaOptionCardContent = cvaMerge(["flex", "flex-col", "items-center"]);
1449
- const cvaOptionCardContainer = cvaMerge(["contents"]);
1450
- const cvaOptionCardTitle = cvaMerge(["text-neutral-600"], {
1451
- variants: {
1452
- layout: {
1453
- default: ["text-lg", "line-clamp-2"],
1454
- compact: ["text-sm", "line-clamp-1"],
1455
- },
1456
- disabled: {
1457
- true: ["text-neutral-400"],
1458
- false: ["focus:text-neutral-800", "active:text-neutral-800"],
1459
- },
1460
- },
1461
- });
1462
- const cvaOptionCardText = cvaMerge(["text-neutral-600", "text-sm"], {
1463
- variants: {
1464
- type: {
1465
- subheading: ["font-medium"],
1466
- description: ["font-normal"],
1467
- },
1468
- disabled: {
1469
- true: ["text-neutral-400"],
1470
- false: ["focus:text-neutral-800", "active:text-neutral-800"],
1471
- },
1472
- },
1473
- });
1474
- const cvaInput = cvaMerge(["peer", "absolute", "h-0", "w-0", "opacity-0"]);
1475
- const cvaCustomImage = cvaMerge(["text-neutral-400"], {
1476
- variants: {
1477
- disabled: {
1478
- true: ["!text-neutral-400"],
1479
- false: [""],
1480
- },
1481
- },
1482
- });
1483
- const cvaTag = cvaMerge([], {
1484
- variants: {
1485
- layout: {
1486
- default: ["absolute", "top-2", "right-2"],
1487
- compact: [],
1488
- },
1489
- },
1490
- });
1491
-
1492
- /**
1493
- * A card version of a radio button that includes an icon, headings and a description.
1494
- */
1495
- const OptionCard = ({ icon, heading, subheading, description, disabled, id, value, className, contentClassName, dataTestId, customImage, layout = "default", ref, tagProps, ...rest }) => {
1496
- const htmlForId = id ?? "option-card-" + uuidv4();
1497
- const subContent = useMemo(() => (jsxs("div", { className: cvaOptionCardContent({ className: contentClassName }), children: [subheading ? (jsx(Text, { align: "center", className: cvaOptionCardText({ type: "subheading", disabled }), type: "span", children: subheading })) : null, description ? (jsx(Text, { align: "center", className: cvaOptionCardText({ type: "description", disabled }), type: "span", children: description })) : null] })), [subheading, description, contentClassName, disabled]);
1498
- return (jsx(Tooltip, { className: "w-fit", disabled: layout !== "compact" || (!subheading && !description), label: subContent, mode: "light", placement: "top", children: jsxs("div", { className: cvaOptionCardContainer(), "data-testid": dataTestId, children: [jsx("input", { className: cvaInput(), "data-testid": `${dataTestId}-option-card`, disabled: disabled, id: htmlForId, ref: ref, type: "radio", value: value, ...rest }), jsxs("label", { className: cvaOptionCardLabel({ className, disabled, layout }), "data-testid": `${dataTestId}-option-card-label`, htmlFor: htmlForId, children: [disabled && icon && !customImage
1499
- ? cloneElement(icon, { className: cvaCustomImage({ disabled, className: icon.props.className }) })
1500
- : null, disabled && customImage ? jsx("img", { alt: "logo", className: customImage.className, src: customImage.src }) : null, !disabled && !customImage && icon, !disabled && customImage ? jsx("img", { alt: "logo", className: customImage.className, src: customImage.src }) : null, heading ? (layout === "default" ? (jsx(Heading, { className: cvaOptionCardTitle({ disabled, layout }), subtle: disabled, variant: "secondary", children: heading })) : (jsx(Text, { align: "center", className: cvaOptionCardTitle({ disabled, layout }), subtle: disabled, type: "span", weight: "thick", children: heading }))) : null, layout === "default" && (subheading || description) ? subContent : null, tagProps ? jsx(Tag, { className: cvaTag({ className: tagProps.className, layout }), ...tagProps }) : null] })] }) }));
1501
- };
1502
- OptionCard.displayName = "OptionCard";
1503
-
1504
- /**
1505
- * A thin wrapper around the `BaseInput` component for password input fields.
1506
- *
1507
- * NOTE: If shown with a label, please use the `PasswordField` component instead.
1508
- */
1509
- const PasswordBaseInput = ({ ref, fieldSize, ...rest }) => {
1510
- const [showPassword, setShowPassword] = useState(false);
1511
- return (jsx(BaseInput, { ref: ref, ...rest, actions: jsx("div", { className: cvaActionContainer({ size: fieldSize }), children: jsx(IconButton, { className: cvaActionButton({ size: fieldSize }), icon: jsx(Icon, { name: showPassword ? "EyeSlash" : "Eye", size: "small" }), onClick: () => setShowPassword(prevState => !prevState), size: "small", variant: "secondary" }) }), type: showPassword ? "text" : "password" }));
1512
- };
1513
-
1514
- /**
1515
- * Password fields enter a password or other confidential information. Characters are masked as they are typed.
1516
- *
1517
- * _**Do use** when the user has to input a password or something that needs to be obfuscated_
1518
- *
1519
- * _**Do not use** to confirm user actions, such as deleting. Use a checkbox for such flows._
1520
- */
1521
- const PasswordField = ({ id, label, tip, helpText, helpAddon, errorMessage, isInvalid, maxLength, onChange, className, value, dataTestId, ref, ...rest }) => {
1522
- const renderAsInvalid = isInvalid === undefined ? Boolean(errorMessage) : isInvalid;
1523
- const htmlFor = id ? id : "passwordField-" + uuidv4();
1524
- const handleChange = useCallback((event) => {
1525
- onChange?.(event);
1526
- }, [onChange]);
1527
- return (jsx(FormGroup, { dataTestId: dataTestId ? `${dataTestId}-FormGroup` : undefined, helpAddon: helpAddon, helpText: (renderAsInvalid && errorMessage) || helpText, htmlFor: htmlFor, isInvalid: renderAsInvalid, label: label, required: rest.required ? !(rest.disabled || rest.readOnly) : false, tip: tip, children: jsx(PasswordBaseInput, { ...rest, "aria-labelledby": htmlFor + "-label", className: className, dataTestId: dataTestId, disabled: rest.readOnly, id: htmlFor, isInvalid: renderAsInvalid, maxLength: maxLength, onChange: handleChange, ref: ref, value: value }) }));
1528
- };
1529
- PasswordField.displayName = "PasswordField";
1530
-
1531
- /**
1532
- * Validates a phone number
1533
- */
1534
- const validatePhoneNumber = (phoneNumber) => {
1535
- if (!phoneNumber) {
1536
- return "REQUIRED";
1537
- }
1538
- const asYouType = new AsYouType();
1539
- asYouType.input(phoneNumber);
1540
- const countryCode = asYouType.getCallingCode();
1541
- const national = asYouType.getNationalNumber();
1542
- const safePhoneNumber = getPhoneNumberWithPlus(phoneNumber.trim());
1543
- const number = parsePhoneNumberFromString(safePhoneNumber);
1544
- if (phoneNumber && isValidPhoneNumber(phoneNumber)) {
1545
- return undefined;
1546
- }
1547
- if (!countryCode && national) {
1548
- return "REQUIRED_COUNTRY";
1549
- }
1550
- if (phoneNumber &&
1551
- (checkIfPhoneNumberHasPlus(phoneNumber)
1552
- ? isNaN(+phoneNumber.slice(1, phoneNumber.length))
1553
- : isNaN(+phoneNumber) || !number)) {
1554
- return "NOT_A_NUMBER";
1555
- }
1556
- if (safePhoneNumber.length <= 5) {
1557
- //needs to be handled manually, parsePhoneNumberFromString can't parse it
1558
- return "TOO_SHORT";
2090
+ return "LESS_THAN";
1559
2091
  }
1560
- return "INVALID_NUMBER";
1561
- };
1562
- /**
1563
- * Checks if the country code is valid and required
1564
- */
1565
- const isInvalidCountryCode = (error, required) => (!!required && error === "REQUIRED") || error === "REQUIRED_COUNTRY";
1566
- /**
1567
- * Checks if the phone number is valid and required
1568
- */
1569
- const isInvalidPhoneNumber = (error, required) => error !== "REQUIRED_COUNTRY" && ((!!error && error !== "REQUIRED") || (!!required && error === "REQUIRED"));
1570
- /**
1571
- * Checks if the phone number is valid and returns corresponding error message
1572
- */
1573
- const phoneErrorMessage = (phoneNumber, required) => {
1574
- if ((validatePhoneNumber(phoneNumber) === "REQUIRED" && !required) ||
1575
- (validatePhoneNumber(phoneNumber) === "REQUIRED" && required && phoneNumber === undefined)) {
2092
+ // if the value is a number and is valid
2093
+ if (isNumber(parsedNumber) && isNumberValid(parsedNumber)) {
1576
2094
  return undefined;
1577
2095
  }
1578
- return validatePhoneNumber(phoneNumber);
2096
+ return "INVALID_NUMBER";
1579
2097
  };
1580
2098
 
1581
2099
  /**
1582
- * The PhoneField component is used to enter phone number.
1583
- * It is a wrapper around the PhoneInput component and the FormGroup component.
1584
- * It is used to render a phone number field with a label, a tip, a help text, a help addon and an error message.
2100
+ * The number field component is used for entering numeric values and includes controls for incrementally increasing or decreasing the value.
1585
2101
  *
1586
- * @param {string} [label] - The label for the component.
1587
- * @param {string} [tip] - The tip for the component.
1588
- * @param {string} [helpText] - The help text for the component.
1589
- * @param {string} [helpAddon] - The help addon for the component.
1590
- * @param {string} [errorMessage] - The error message for the component.
1591
- * @param {string} [defaultValue] - The default value for the component.
1592
- * @param {boolean} [disabled=false] - Whether the component is disabled or not.
1593
- * @param {string} [fieldSize="medium"] - The size of the input field.
1594
- * @param {boolean} [disableAction=false] - Whether the action button is disabled or not.
2102
+ * _**Do use**_ the NumberField when the controls to incrementally increase or decrease makes the task easier for the user.
2103
+ *
2104
+ * _**Do not use**_ this fields for non-serialized numbers. Use TextField instead.
1595
2105
  */
1596
- const PhoneField = ({ label, id, tip, helpText, isInvalid, errorMessage, value, helpAddon, className, defaultValue, dataTestId, name, onBlur, ref, ...rest }) => {
1597
- const htmlForId = id ? id : "phoneField-" + uuidv4();
2106
+ const NumberField = ({ label, id, tip, helpText, errorMessage, helpAddon, isInvalid, maxLength, className, value, dataTestId, defaultValue, onBlur, onChange, ref, ...rest }) => {
2107
+ const htmlForId = id ? id : "numberField-" + uuidv4();
1598
2108
  const [t] = useTranslation();
1599
2109
  const [innerValue, setInnerValue] = useState(() => {
1600
- return (value?.toString() || defaultValue?.toString()) ?? undefined;
2110
+ return Number(value?.toString()) || Number(defaultValue?.toString());
1601
2111
  });
1602
2112
  const [renderAsInvalid, setRenderAsInvalid] = useState((isInvalid === undefined ? Boolean(errorMessage) : isInvalid) ||
1603
- !!phoneErrorMessage(value?.toString(), rest.required));
1604
- const errorType = useMemo(() => phoneErrorMessage(innerValue, rest.required), [innerValue, rest.required]);
2113
+ !!validateNumber(value?.toString(), rest.required, rest.min, rest.max));
2114
+ const errorType = useMemo(() => validateNumber(innerValue, rest.required, rest.min, rest.max), [innerValue, rest.max, rest.min, rest.required]);
1605
2115
  const error = useMemo(() => {
1606
2116
  // for the case when a custom error message is provided
1607
2117
  if (errorMessage) {
1608
2118
  return errorMessage;
1609
2119
  }
1610
2120
  else if (errorType) {
1611
- return t(`phoneField.error.${errorType}`);
2121
+ return t(`numberField.error.${errorType}`, { min: rest.min, max: rest.max });
1612
2122
  }
1613
2123
  return errorMessage;
1614
- }, [errorMessage, errorType, t]);
2124
+ }, [errorMessage, errorType, rest.max, rest.min, t]);
1615
2125
  useEffect(() => {
1616
- setRenderAsInvalid(Boolean(errorMessage));
2126
+ if (errorMessage) {
2127
+ setRenderAsInvalid(Boolean(errorMessage));
2128
+ }
1617
2129
  }, [errorMessage]);
1618
2130
  const handleBlur = useCallback(event => {
1619
2131
  const newValue = event.target.value;
1620
- setInnerValue(newValue);
2132
+ setInnerValue(newValue.toString());
1621
2133
  // for the case when a custom error message is provided
1622
- if (errorMessage && !phoneErrorMessage(newValue.toString(), rest.required)) {
2134
+ if (errorMessage && !validateNumber(newValue, rest.required, rest.min, rest.max)) {
1623
2135
  setRenderAsInvalid(Boolean(errorMessage));
1624
2136
  }
1625
2137
  else {
1626
- setRenderAsInvalid(!!phoneErrorMessage(newValue.toString(), rest.required));
2138
+ setRenderAsInvalid(!!validateNumber(newValue, rest.required, rest.min, rest.max));
1627
2139
  }
1628
2140
  onBlur?.(event);
1629
- }, [errorMessage, onBlur, rest.required]);
1630
- return (jsx(FormGroup, { className: className, dataTestId: dataTestId ? `${dataTestId}-FormGroup` : undefined, helpAddon: helpAddon, helpText: (renderAsInvalid && error) || helpText, htmlFor: htmlForId, isInvalid: renderAsInvalid, label: label, required: rest.required ? !(rest.disabled || rest.readOnly) : false, tip: tip, children: jsx(PhoneBaseInput, { "aria-labelledby": htmlForId + "-label", dataTestId: dataTestId, defaultValue: defaultValue, id: htmlForId, isInvalid: renderAsInvalid, name: name, onBlur: handleBlur, ref: ref, value: value, ...rest }) }));
1631
- };
1632
- PhoneField.displayName = "PhoneField";
1633
-
1634
- /**
1635
- * The PhoneFieldWithController component is a wrapper for the PhoneField component to connect it to react-hook-form.
1636
- *
1637
- */
1638
- const PhoneFieldWithController = ({ control, controllerProps, name, value, ref, ...rest }) => {
1639
- return (jsx(Controller, { control: control, defaultValue: value, name: name, ...controllerProps, render: ({ field }) => jsx(PhoneField, { ...rest, ...field, ref: ref }) }));
2141
+ }, [errorMessage, onBlur, rest.max, rest.min, rest.required]);
2142
+ const handleChange = useCallback((event) => {
2143
+ setInnerValue(event.target.value);
2144
+ if (onChange) {
2145
+ onChange(event);
2146
+ }
2147
+ }, [onChange]);
2148
+ return (jsx(FormGroup, { dataTestId: dataTestId ? `${dataTestId}-FormGroup` : undefined, helpAddon: helpAddon, helpText: (renderAsInvalid && error) || helpText, htmlFor: htmlForId, isInvalid: renderAsInvalid, label: label, required: rest.required ? !(rest.disabled || rest.readOnly) : false, tip: tip, children: jsx(NumberBaseInput, { "aria-labelledby": htmlForId + "-label", id: htmlForId, isInvalid: renderAsInvalid, maxLength: maxLength, onBlur: handleBlur, onChange: handleChange, ref: ref, value: value, ...rest, className: className, dataTestId: dataTestId }) }));
1640
2149
  };
1641
- PhoneFieldWithController.displayName = "PhoneFieldWithController";
2150
+ NumberField.displayName = "NumberField";
1642
2151
 
1643
- const cvaRadioGroup = cvaMerge(["flex", "gap-2", "flex-col", "items-start"], {
2152
+ const cvaOptionCardLabel = cvaMerge([
2153
+ "group",
2154
+ "transition",
2155
+ "bg-white",
2156
+ "outline",
2157
+ "outline-1",
2158
+ "outline-neutral-300",
2159
+ "hover:bg-neutral-100",
2160
+ "focus:bg-neutral-200",
2161
+ "active:bg-neutral-200",
2162
+ "peer-checked:bg-primary-50",
2163
+ "peer-checked:outline-primary-600",
2164
+ "peer-checked:outline-2",
2165
+ "flex",
2166
+ "gap-2",
2167
+ "justify-center",
2168
+ "items-center",
2169
+ "text-center",
2170
+ "rounded-md",
2171
+ "relative",
2172
+ ], {
1644
2173
  variants: {
2174
+ disabled: {
2175
+ true: ["cursor-not-allowed", "bg-neutral-100"],
2176
+ false: ["cursor-pointer"],
2177
+ },
1645
2178
  layout: {
1646
- inline: ["flex", "gap-3", "flex-row", "items-center"],
2179
+ default: ["flex-col", "p-responsive-space", "w-full", "aspect-square"],
2180
+ compact: ["px-3", "py-1.5", "h-8", "min-h-[calc(var(--line-height-sm)+var(--spacing-3))]", "flex-row", "w-fit"],
1647
2181
  },
1648
2182
  },
1649
2183
  });
1650
- const cvaRadioItem = cvaMerge([
1651
- "self-center",
1652
- "w-4",
1653
- "h-4",
1654
- "appearance-none",
1655
- "rounded-3xl",
1656
- "bg-white",
1657
- "border-solid",
1658
- "border",
1659
- "border-neutral-300",
1660
- "shadow-sm",
1661
- "shrink-0",
1662
- "transition",
1663
- "box-border",
1664
- "hover:cursor-pointer",
1665
- "hover:bg-neutral-100",
1666
- "focus-visible:outline-primary-700",
1667
- ], {
2184
+ const cvaOptionCardContent = cvaMerge(["flex", "flex-col", "items-center"]);
2185
+ const cvaOptionCardContainer = cvaMerge(["contents"]);
2186
+ const cvaOptionCardTitle = cvaMerge(["text-neutral-600"], {
1668
2187
  variants: {
1669
- checked: {
1670
- true: [
1671
- "border-solid",
1672
- "border-4",
1673
- "border-primary-600",
1674
- "bg-white",
1675
- "hover:bg-neutral-100",
1676
- "hover:cursor-pointer",
1677
- "outline-0",
1678
- "active:bg-neutral-200",
1679
- "active:ring-2",
1680
- "active:ring-inset",
1681
- "active:ring-primary-700",
1682
- "group-active:ring-2",
1683
- "group-active:ring-inset",
1684
- "group-active:ring-primary-700",
1685
- ],
1686
- false: "",
2188
+ layout: {
2189
+ default: ["text-lg", "line-clamp-2"],
2190
+ compact: ["text-sm", "line-clamp-1"],
1687
2191
  },
1688
- invalid: {
1689
- true: ["border-red-600", "active:ring-red-700"],
1690
- false: "",
2192
+ disabled: {
2193
+ true: ["text-neutral-400"],
2194
+ false: ["focus:text-neutral-800", "active:text-neutral-800"],
2195
+ },
2196
+ },
2197
+ });
2198
+ const cvaOptionCardText = cvaMerge(["text-neutral-600", "text-sm"], {
2199
+ variants: {
2200
+ type: {
2201
+ subheading: ["font-medium"],
2202
+ description: ["font-normal"],
1691
2203
  },
1692
2204
  disabled: {
1693
- true: [
1694
- "bg-neutral-400",
1695
- "border-neutral-300",
1696
- "cursor-not-allowed",
1697
- "hover:bg-neutral-400",
1698
- "active:bg-neutral-400",
1699
- "group-active:ring-0",
1700
- "group-active:ring-inset",
1701
- ],
1702
- false: "",
2205
+ true: ["text-neutral-400"],
2206
+ false: ["focus:text-neutral-800", "active:text-neutral-800"],
1703
2207
  },
1704
2208
  },
1705
- compoundVariants: [
1706
- {
1707
- checked: true,
1708
- disabled: true,
1709
- className: ["bg-white"],
2209
+ });
2210
+ const cvaInput = cvaMerge(["peer", "absolute", "h-0", "w-0", "opacity-0"]);
2211
+ const cvaCustomImage = cvaMerge(["text-neutral-400"], {
2212
+ variants: {
2213
+ disabled: {
2214
+ true: ["!text-neutral-400"],
2215
+ false: [""],
1710
2216
  },
1711
- ],
2217
+ },
2218
+ });
2219
+ const cvaTag = cvaMerge([], {
2220
+ variants: {
2221
+ layout: {
2222
+ default: ["absolute", "top-2", "right-2"],
2223
+ compact: [],
2224
+ },
2225
+ },
1712
2226
  });
1713
-
1714
- const RadioGroupContext = createContext(null);
1715
-
1716
- /**
1717
- * Use radio buttons when you have a group of mutually exclusive choices and only one selection from the group is allowed.
1718
- *
1719
- * Radio buttons are used for mutually exclusive choices, not for multiple choices. Only one radio button can be selected at a time. When a user chooses a new item, the previous choice is automatically deselected.
1720
- *
1721
- * _**Do use** Radio buttons in forms, settings, or selections in a list._
1722
- *
1723
- * _**Do not use** Radio buttons if a user can select many option from a list, use checkboxes instead of radio buttons._
1724
- *
1725
- * @param {RadioGroupProps} props - The props for the RadioGroup component
1726
- * @returns {ReactElement} RadioGroup component
1727
- */
1728
- const RadioGroup = ({ children, id, name, value, disabled, onChange, label, inline, className, dataTestId, isInvalid, }) => {
1729
- return (jsx(FormGroup, { dataTestId: dataTestId ? `${dataTestId}-FormGroup` : undefined, label: label, children: jsx("div", { className: cvaRadioGroup({ layout: inline ? "inline" : null, className }), "data-testid": dataTestId, children: jsx(RadioGroupContext.Provider, { value: {
1730
- id,
1731
- value,
1732
- name: name || id,
1733
- onChange,
1734
- disabled,
1735
- isInvalid,
1736
- }, children: children }) }) }));
1737
- };
1738
- RadioGroup.displayName = "RadioGroup";
1739
2227
 
1740
2228
  /**
1741
- * The RadioItem component.
1742
- *
1743
- * @param {RadioItemProps} props - The props for the RadioItem component
1744
- * @returns {ReactElement} RadioItem component
2229
+ * A card version of a radio button that includes an icon, headings and a description.
1745
2230
  */
1746
- const RadioItem = ({ label, value, dataTestId, className, description, suffix, ...rest }) => {
1747
- const groupCtx = useContext(RadioGroupContext);
1748
- const isChecked = groupCtx?.value === value;
1749
- const { ref: labelRef, isTextTruncated: isLabelTruncated } = useIsTextTruncated();
1750
- const { ref: descriptionRef, isTextTruncated: isDescriptionTruncated } = useIsTextTruncated();
1751
- const descriptionId = description ? `${groupCtx?.id}-${value}-description` : undefined;
1752
- const inputId = `${groupCtx?.id}-${value}`;
1753
- const hasLabel = label !== undefined && label !== null && label !== "";
1754
- return (jsxs("label", { className: hasLabel
1755
- ? cvaBinaryControlWrapper({ className })
1756
- : `inline-flex w-fit items-center gap-2 ${className || ""}`.trim(), "data-testid": dataTestId ? `${dataTestId}-Wrapper` : undefined, htmlFor: inputId, children: [jsx("input", { "aria-describedby": descriptionId, checked: isChecked, className: cvaRadioItem({
1757
- checked: isChecked,
1758
- disabled: groupCtx?.disabled,
1759
- invalid: groupCtx?.isInvalid,
1760
- }), "data-testid": dataTestId, id: inputId, onChange: groupCtx?.onChange, type: "radio", value: value, ...rest }), hasLabel ? (jsx(Tooltip, { className: cvaBinaryControlLabelTooltip(), dataTestId: dataTestId ? `${dataTestId}-Label-Tooltip` : undefined, disabled: !isLabelTruncated, label: label, placement: "top", children: jsx("span", { className: cvaLabel({
1761
- invalid: groupCtx?.isInvalid,
1762
- disabled: groupCtx?.disabled,
1763
- }), "data-testid": dataTestId ? `${dataTestId}-Label` : undefined, ref: labelRef, children: label }) }, "tooltip-" + rest.name)) : null, suffix ? (jsx("div", { className: cvaBinaryControlSuffixContainer(), "data-testid": dataTestId ? `${dataTestId}-suffix-container` : undefined, children: suffix })) : null, description ? (jsx(Tooltip, { className: cvaBinaryControlDescriptionTooltip(), dataTestId: dataTestId ? `${dataTestId}-Description-Tooltip` : undefined, disabled: !isDescriptionTruncated, label: description, placement: "top", children: jsx("span", { className: cvaBinaryControlDescription({ disabled: groupCtx?.disabled }), "data-testid": dataTestId ? `${dataTestId}-Description` : undefined, id: descriptionId, ref: descriptionRef, children: description }) }, "description-tooltip-" + rest.name)) : null] }));
2231
+ const OptionCard = ({ icon, heading, subheading, description, disabled, id, value, className, contentClassName, dataTestId, customImage, layout = "default", ref, tagProps, ...rest }) => {
2232
+ const htmlForId = id ?? "option-card-" + uuidv4();
2233
+ const subContent = useMemo(() => (jsxs("div", { className: cvaOptionCardContent({ className: contentClassName }), children: [subheading ? (jsx(Text, { align: "center", className: cvaOptionCardText({ type: "subheading", disabled }), type: "span", children: subheading })) : null, description ? (jsx(Text, { align: "center", className: cvaOptionCardText({ type: "description", disabled }), type: "span", children: description })) : null] })), [subheading, description, contentClassName, disabled]);
2234
+ return (jsx(Tooltip, { className: "w-fit", disabled: layout !== "compact" || (!subheading && !description), label: subContent, mode: "light", placement: "top", children: jsxs("div", { className: cvaOptionCardContainer(), "data-testid": dataTestId, children: [jsx("input", { className: cvaInput(), "data-testid": `${dataTestId}-option-card`, disabled: disabled, id: htmlForId, ref: ref, type: "radio", value: value, ...rest }), jsxs("label", { className: cvaOptionCardLabel({ className, disabled, layout }), "data-testid": `${dataTestId}-option-card-label`, htmlFor: htmlForId, children: [disabled && icon && !customImage
2235
+ ? cloneElement(icon, { className: cvaCustomImage({ disabled, className: icon.props.className }) })
2236
+ : null, disabled && customImage ? jsx("img", { alt: "logo", className: customImage.className, src: customImage.src }) : null, !disabled && !customImage && icon, !disabled && customImage ? jsx("img", { alt: "logo", className: customImage.className, src: customImage.src }) : null, heading ? (layout === "default" ? (jsx(Heading, { className: cvaOptionCardTitle({ disabled, layout }), subtle: disabled, variant: "secondary", children: heading })) : (jsx(Text, { align: "center", className: cvaOptionCardTitle({ disabled, layout }), subtle: disabled, type: "span", weight: "thick", children: heading }))) : null, layout === "default" && (subheading || description) ? subContent : null, tagProps ? jsx(Tag, { className: cvaTag({ className: tagProps.className, layout }), ...tagProps }) : null] })] }) }));
1764
2237
  };
1765
-
1766
- const cvaTimeRange = cvaMerge([
1767
- "flex",
1768
- "flex-1",
1769
- "items-center",
1770
- "gap-4",
1771
- "max-sm:gap-2",
1772
- "border-transparent",
1773
- "rounded-md",
1774
- ]);
2238
+ OptionCard.displayName = "OptionCard";
1775
2239
 
1776
2240
  /**
1777
- * TimeRange is used to create a time range entry.
2241
+ * A thin wrapper around the `BaseInput` component for password input fields.
1778
2242
  *
1779
- * @param {TimeRangeProps} props - The props for the TimeRange component
1780
- * @returns {ReactElement} TimeRange component
2243
+ * NOTE: If shown with a label, please use the `PasswordField` component instead.
1781
2244
  */
1782
- const TimeRange = ({ id, className, dataTestId, children, range, onChange, disabled, isInvalid, }) => {
1783
- const [timeRange, setTimeRange] = useState(range ?? {
1784
- timeFrom: DEFAULT_TIME,
1785
- timeTo: DEFAULT_TIME,
1786
- });
1787
- const onChangeFrom = (timeFrom) => {
1788
- setTimeRange(prev => ({ ...prev, timeFrom }));
1789
- };
1790
- const onChangeTo = (timeTo) => {
1791
- setTimeRange(prev => ({ ...prev, timeTo }));
1792
- };
1793
- const onRangeChange = () => onChange(timeRange);
1794
- return (jsxs("div", { className: cvaTimeRange({ className }), "data-testid": dataTestId, id: id, children: [jsx(BaseInput, { dataTestId: `${dataTestId}-from`, disabled: disabled, isInvalid: isInvalid, onBlur: onRangeChange, onChange: (time) => onChangeFrom(time.currentTarget.value), type: "time", value: timeRange.timeFrom === "" ? DEFAULT_TIME : timeRange.timeFrom }), children ?? jsx("div", { "data-testid": `${dataTestId}-separator`, children: "-" }), jsx(BaseInput, { dataTestId: `${dataTestId}-to`, disabled: disabled, isInvalid: isInvalid, onBlur: onRangeChange, onChange: (time) => onChangeTo(time.currentTarget.value), type: "time", value: timeRange.timeTo === "" ? DEFAULT_TIME : timeRange.timeTo })] }));
2245
+ const PasswordBaseInput = ({ ref, fieldSize, ...rest }) => {
2246
+ const [showPassword, setShowPassword] = useState(false);
2247
+ return (jsx(BaseInput, { ref: ref, ...rest, actions: jsx("div", { className: cvaActionContainer({ size: fieldSize }), children: jsx(IconButton, { className: cvaActionButton({ size: fieldSize }), icon: jsx(Icon, { name: showPassword ? "EyeSlash" : "Eye", size: "small" }), onClick: () => setShowPassword(prevState => !prevState), size: "small", variant: "secondary" }) }), type: showPassword ? "text" : "password" }));
1795
2248
  };
1796
- const DEFAULT_TIME = "12:00";
1797
-
1798
- const cvaScheduleItem = cvaMerge([
1799
- "grid",
1800
- "pb-4",
1801
- "gap-2",
1802
- "grid-cols-[60px,200px,60px,2fr]",
1803
- "max-sm:grid-cols-1",
1804
- ]);
1805
- const cvaScheduleItemText = cvaMerge(["flex", "font-bold", "self-center"]);
1806
2249
 
1807
2250
  /**
1808
- * Schedule is used to create a time range entries.
2251
+ * Password fields enter a password or other confidential information. Characters are masked as they are typed.
1809
2252
  *
1810
- * @param {ScheduleProps} props - The props for the Schedule component
1811
- * @returns {ReactElement} Schedule component
2253
+ * _**Do use** when the user has to input a password or something that needs to be obfuscated_
2254
+ *
2255
+ * _**Do not use** to confirm user actions, such as deleting. Use a checkbox for such flows._
1812
2256
  */
1813
- const Schedule = ({ className, dataTestId, schedule, onChange, invalidKeys = [] }) => {
1814
- const [t] = useTranslation();
1815
- const onRangeChange = (range, index) => {
1816
- const newSchedule = schedule.map((day, dayIndex) => (index === dayIndex ? { ...day, range: { ...range } } : day));
1817
- onChange(newSchedule);
1818
- };
1819
- const onActiveChange = (isActive, index) => {
1820
- const newSchedule = schedule.map((day, dayIndex) => index === dayIndex
1821
- ? {
1822
- ...day,
1823
- range: {
1824
- timeFrom: day.range.timeFrom ? day.range.timeFrom : DEFAULT_TIME,
1825
- timeTo: day.range.timeTo ? day.range.timeTo : DEFAULT_TIME,
1826
- },
1827
- isActive,
1828
- }
1829
- : day);
1830
- onChange(newSchedule);
1831
- };
1832
- const onAllDayChange = (isAllDayChecked, index) => {
1833
- const newSchedule = schedule.map((day, dayIndex) => index === dayIndex
1834
- ? {
1835
- ...day,
1836
- range: {
1837
- timeFrom: day.range.timeFrom ? day.range.timeFrom : DEFAULT_TIME,
1838
- timeTo: day.range.timeTo ? day.range.timeTo : DEFAULT_TIME,
1839
- },
1840
- isAllDay: isAllDayChecked,
1841
- }
1842
- : day);
1843
- onChange(newSchedule);
1844
- };
1845
- return (jsx("div", { className: className, "data-testid": dataTestId, children: schedule.map(({ label, range, isActive, key, checkboxLabel, isAllDay }, index) => {
1846
- return (jsxs("div", { className: cvaScheduleItem(), children: [jsxs("div", { className: "grid grid-cols-2 gap-4 sm:hidden", children: [jsx(Text, { className: "font-medium text-neutral-500", children: t("schedule.label.day") }), jsx(Text, { className: cvaScheduleItemText(), size: "medium", subtle: !isActive, children: label }), jsx(Text, { className: "font-medium text-neutral-500", children: t("schedule.label.active") }), jsx(Checkbox, { checked: isActive, label: checkboxLabel, onChange: (event) => onActiveChange(Boolean(event.currentTarget.checked), index) }), jsx(Text, { className: "font-medium text-neutral-500", children: t("schedule.label.allDay") }), jsx(Checkbox, { checked: isAllDay ? isActive : undefined, disabled: !isActive, onChange: (event) => onAllDayChange(Boolean(event.currentTarget.checked), index) }), jsx(TimeRange, { disabled: !isActive || isAllDay, isInvalid: !!invalidKeys.find((invalidKey) => invalidKey === key), onChange: (newRange) => onRangeChange(newRange, index), range: range })] }), jsxs("div", { className: "max-sm:hidden sm:grid sm:grid-cols-[100px,200px,60px,250px,250px] sm:gap-2", children: [jsx(Checkbox, { checked: isActive, dataTestId: `${dataTestId}-${key}-checkbox`, label: checkboxLabel, onChange: (event) => onActiveChange(Boolean(event.currentTarget.checked), index) }), jsx(Text, { className: cvaScheduleItemText(), size: "medium", subtle: !isActive, children: label }), jsx(Checkbox, { checked: isAllDay ? isActive : undefined, dataTestId: `${dataTestId}-${key}-allday-checkbox`, disabled: !isActive, onChange: (event) => onAllDayChange(Boolean(event.currentTarget.checked), index) }), jsx(TimeRange, { dataTestId: `${dataTestId}-${key}-range`, disabled: !isActive || isAllDay, isInvalid: !!invalidKeys.find((invalidKey) => invalidKey === key), onChange: (newRange) => onRangeChange(newRange, index), range: isAllDay ? undefined : range })] })] }, key + label));
1847
- }) }));
2257
+ const PasswordField = ({ id, label, tip, helpText, helpAddon, errorMessage, isInvalid, maxLength, onChange, className, value, dataTestId, ref, ...rest }) => {
2258
+ const renderAsInvalid = isInvalid === undefined ? Boolean(errorMessage) : isInvalid;
2259
+ const htmlFor = id ? id : "passwordField-" + uuidv4();
2260
+ const handleChange = useCallback((event) => {
2261
+ onChange?.(event);
2262
+ }, [onChange]);
2263
+ return (jsx(FormGroup, { dataTestId: dataTestId ? `${dataTestId}-FormGroup` : undefined, helpAddon: helpAddon, helpText: (renderAsInvalid && errorMessage) || helpText, htmlFor: htmlFor, isInvalid: renderAsInvalid, label: label, required: rest.required ? !(rest.disabled || rest.readOnly) : false, tip: tip, children: jsx(PasswordBaseInput, { ...rest, "aria-labelledby": htmlFor + "-label", className: className, dataTestId: dataTestId, disabled: rest.readOnly, id: htmlFor, isInvalid: renderAsInvalid, maxLength: maxLength, onChange: handleChange, ref: ref, value: value }) }));
1848
2264
  };
2265
+ PasswordField.displayName = "PasswordField";
1849
2266
 
1850
- const weekDay = {
1851
- Monday: "monday",
1852
- Tuesday: "tuesday",
1853
- Wednesday: "wednesday",
1854
- Thursday: "thursday",
1855
- Friday: "friday",
1856
- Saturday: "saturday",
1857
- Sunday: "sunday",
1858
- };
1859
- var ScheduleVariant;
1860
- (function (ScheduleVariant) {
1861
- ScheduleVariant["ALL_DAYS"] = "all";
1862
- ScheduleVariant["WEEKDAYS"] = "week";
1863
- ScheduleVariant["CUSTOM"] = "custom";
1864
- })(ScheduleVariant || (ScheduleVariant = {}));
1865
2267
  /**
1866
- * Parse a string of week range schedule string to human readable schedule range.
1867
- *
1868
- * @param {string} scheduleString String of week schedule
1869
- * @returns {WeekSchedule} Week schedule range
2268
+ * Validates a phone number
1870
2269
  */
1871
- const parseSchedule = (scheduleString) => {
1872
- if (!scheduleString) {
1873
- return {
1874
- variant: ScheduleVariant.CUSTOM,
1875
- schedule: [1, 2, 3, 4, 5, 6, 7].map(day => ({
1876
- day,
1877
- range: { timeFrom: "", timeTo: "" },
1878
- isAllDay: false,
1879
- isActive: false,
1880
- })),
1881
- };
2270
+ const validatePhoneNumber = (phoneNumber) => {
2271
+ if (!phoneNumber) {
2272
+ return "REQUIRED";
1882
2273
  }
1883
- const schedule = scheduleString.split(",").map(daySchedule => {
1884
- const [day, timeRange] = daySchedule.split("#");
1885
- const [timeFrom, timeTo] = (timeRange ?? "").split("-");
1886
- const isAllDay = timeFrom === "00:00" && timeTo === "24:00";
1887
- return {
1888
- day: Number(day),
1889
- range: timeFrom === undefined || timeTo === undefined
1890
- ? undefined
1891
- : {
1892
- timeFrom: timeFrom,
1893
- timeTo: timeTo,
1894
- },
1895
- isAllDay,
1896
- isActive: Boolean(timeFrom) && Boolean(timeTo),
1897
- };
1898
- });
1899
- const filteredSchedule = schedule
1900
- .filter(daySchedule => daySchedule.range)
1901
- .map(daySchedule => ({
1902
- day: daySchedule.day,
1903
- range: daySchedule.range,
1904
- isAllDay: daySchedule.isAllDay,
1905
- isActive: daySchedule.isActive,
1906
- }));
1907
- let variant;
1908
- switch (schedule.length) {
1909
- case 7:
1910
- variant = isUniform(schedule) ? ScheduleVariant.ALL_DAYS : ScheduleVariant.CUSTOM;
1911
- break;
1912
- case 5:
1913
- variant = hasConsecutiveDays(schedule) ? ScheduleVariant.WEEKDAYS : ScheduleVariant.CUSTOM;
1914
- break;
1915
- default:
1916
- return {
1917
- variant: ScheduleVariant.CUSTOM,
1918
- schedule: filteredSchedule,
1919
- };
2274
+ const asYouType = new AsYouType();
2275
+ asYouType.input(phoneNumber);
2276
+ const countryCode = asYouType.getCallingCode();
2277
+ const national = asYouType.getNationalNumber();
2278
+ const safePhoneNumber = getPhoneNumberWithPlus(phoneNumber.trim());
2279
+ const number = parsePhoneNumberFromString(safePhoneNumber);
2280
+ if (phoneNumber && isValidPhoneNumber(phoneNumber)) {
2281
+ return undefined;
1920
2282
  }
1921
- return {
1922
- variant,
1923
- schedule: filteredSchedule,
1924
- };
2283
+ if (!countryCode && national) {
2284
+ return "REQUIRED_COUNTRY";
2285
+ }
2286
+ if (phoneNumber &&
2287
+ (checkIfPhoneNumberHasPlus(phoneNumber)
2288
+ ? isNaN(+phoneNumber.slice(1, phoneNumber.length))
2289
+ : isNaN(+phoneNumber) || !number)) {
2290
+ return "NOT_A_NUMBER";
2291
+ }
2292
+ if (safePhoneNumber.length <= 5) {
2293
+ //needs to be handled manually, parsePhoneNumberFromString can't parse it
2294
+ return "TOO_SHORT";
2295
+ }
2296
+ return "INVALID_NUMBER";
1925
2297
  };
1926
2298
  /**
1927
- * Serialize week schedule to string schedule
2299
+ * Checks if the country code is valid and required
2300
+ */
2301
+ const isInvalidCountryCode = (error, required) => (!!required && error === "REQUIRED") || error === "REQUIRED_COUNTRY";
2302
+ /**
2303
+ * Checks if the phone number is valid and required
2304
+ */
2305
+ const isInvalidPhoneNumber = (error, required) => error !== "REQUIRED_COUNTRY" && ((!!error && error !== "REQUIRED") || (!!required && error === "REQUIRED"));
2306
+ /**
2307
+ * Checks if the phone number is valid and returns corresponding error message
2308
+ */
2309
+ const phoneErrorMessage = (phoneNumber, required) => {
2310
+ if ((validatePhoneNumber(phoneNumber) === "REQUIRED" && !required) ||
2311
+ (validatePhoneNumber(phoneNumber) === "REQUIRED" && required && phoneNumber === undefined)) {
2312
+ return undefined;
2313
+ }
2314
+ return validatePhoneNumber(phoneNumber);
2315
+ };
2316
+
2317
+ /**
2318
+ * The PhoneField component is used to enter phone number.
2319
+ * It is a wrapper around the PhoneInput component and the FormGroup component.
2320
+ * It is used to render a phone number field with a label, a tip, a help text, a help addon and an error message.
1928
2321
  *
1929
- * @param {WeekSchedule} weekSchedule Week schedule range
1930
- * @returns {string} Schedule string
2322
+ * @param {string} [label] - The label for the component.
2323
+ * @param {string} [tip] - The tip for the component.
2324
+ * @param {string} [helpText] - The help text for the component.
2325
+ * @param {string} [helpAddon] - The help addon for the component.
2326
+ * @param {string} [errorMessage] - The error message for the component.
2327
+ * @param {string} [defaultValue] - The default value for the component.
2328
+ * @param {boolean} [disabled=false] - Whether the component is disabled or not.
2329
+ * @param {string} [fieldSize="medium"] - The size of the input field.
2330
+ * @param {boolean} [disableAction=false] - Whether the action button is disabled or not.
1931
2331
  */
1932
- const serializeSchedule = (weekSchedule) => {
1933
- return weekSchedule.schedule
1934
- .filter(({ range, day, isAllDay }) => {
1935
- const hasRange = range.timeFrom && range.timeTo;
1936
- switch (weekSchedule.variant) {
1937
- case ScheduleVariant.WEEKDAYS:
1938
- return day <= 5 && hasRange;
1939
- case ScheduleVariant.ALL_DAYS:
1940
- return day <= 7 && hasRange;
1941
- case ScheduleVariant.CUSTOM:
1942
- default:
1943
- return hasRange || isAllDay;
2332
+ const PhoneField = ({ label, id, tip, helpText, isInvalid, errorMessage, value, helpAddon, className, defaultValue, dataTestId, name, onBlur, ref, ...rest }) => {
2333
+ const htmlForId = id ? id : "phoneField-" + uuidv4();
2334
+ const [t] = useTranslation();
2335
+ const [innerValue, setInnerValue] = useState(() => {
2336
+ return (value?.toString() || defaultValue?.toString()) ?? undefined;
2337
+ });
2338
+ const [renderAsInvalid, setRenderAsInvalid] = useState((isInvalid === undefined ? Boolean(errorMessage) : isInvalid) ||
2339
+ !!phoneErrorMessage(value?.toString(), rest.required));
2340
+ const errorType = useMemo(() => phoneErrorMessage(innerValue, rest.required), [innerValue, rest.required]);
2341
+ const error = useMemo(() => {
2342
+ // for the case when a custom error message is provided
2343
+ if (errorMessage) {
2344
+ return errorMessage;
2345
+ }
2346
+ else if (errorType) {
2347
+ return t(`phoneField.error.${errorType}`);
2348
+ }
2349
+ return errorMessage;
2350
+ }, [errorMessage, errorType, t]);
2351
+ useEffect(() => {
2352
+ setRenderAsInvalid(Boolean(errorMessage));
2353
+ }, [errorMessage]);
2354
+ const handleBlur = useCallback(event => {
2355
+ const newValue = event.target.value;
2356
+ setInnerValue(newValue);
2357
+ // for the case when a custom error message is provided
2358
+ if (errorMessage && !phoneErrorMessage(newValue.toString(), rest.required)) {
2359
+ setRenderAsInvalid(Boolean(errorMessage));
1944
2360
  }
1945
- })
1946
- .map(({ day, range, isAllDay }) => {
1947
- if (isAllDay) {
1948
- return `${day}#00:00-24:00`;
2361
+ else {
2362
+ setRenderAsInvalid(!!phoneErrorMessage(newValue.toString(), rest.required));
1949
2363
  }
1950
- return `${day}#${range.timeFrom}-${range.timeTo}`;
1951
- })
1952
- .join(",");
1953
- };
1954
- /**
1955
- * Checks if a list of schedule objects have the same ranges
1956
- *
1957
- * @param {RawSchedule[]} schedule List of schedule objects
1958
- * @returns {boolean} Whether the schedule is uniform
1959
- */
1960
- const isUniform = (schedule) => {
1961
- return schedule.every((day, _, collection) => collection[0]?.range?.timeFrom === day.range?.timeFrom && collection[0]?.range?.timeTo === day.range?.timeTo);
2364
+ onBlur?.(event);
2365
+ }, [errorMessage, onBlur, rest.required]);
2366
+ return (jsx(FormGroup, { className: className, dataTestId: dataTestId ? `${dataTestId}-FormGroup` : undefined, helpAddon: helpAddon, helpText: (renderAsInvalid && error) || helpText, htmlFor: htmlForId, isInvalid: renderAsInvalid, label: label, required: rest.required ? !(rest.disabled || rest.readOnly) : false, tip: tip, children: jsx(PhoneBaseInput, { "aria-labelledby": htmlForId + "-label", dataTestId: dataTestId, defaultValue: defaultValue, id: htmlForId, isInvalid: renderAsInvalid, name: name, onBlur: handleBlur, ref: ref, value: value, ...rest }) }));
1962
2367
  };
2368
+ PhoneField.displayName = "PhoneField";
2369
+
1963
2370
  /**
1964
- * Checks if a list of schedule objects are consecutive days
2371
+ * The PhoneFieldWithController component is a wrapper for the PhoneField component to connect it to react-hook-form.
1965
2372
  *
1966
- * @param {RawSchedule[]} schedule List of schedule objects
1967
- * @returns {boolean} Whether the schedule has consecutive days
1968
2373
  */
1969
- const hasConsecutiveDays = (schedule) => {
1970
- const days = [1, 2, 3, 4, 5];
1971
- return schedule.every(({ day }, index) => day === days[index]);
2374
+ const PhoneFieldWithController = ({ control, controllerProps, name, value, ref, ...rest }) => {
2375
+ return (jsx(Controller, { control: control, defaultValue: value, name: name, ...controllerProps, render: ({ field }) => jsx(PhoneField, { ...rest, ...field, ref: ref }) }));
1972
2376
  };
2377
+ PhoneFieldWithController.displayName = "PhoneFieldWithController";
1973
2378
 
1974
- const cvaSearch = cvaMerge([
1975
- "shadow-none",
1976
- "component-search-border",
1977
- "component-search-background",
1978
- "hover:component-search-background",
1979
- "hover:component-search-focus-hover",
1980
- "transition-all",
1981
- "duration-300",
1982
- ], {
2379
+ const cvaRadioGroup = cvaMerge(["flex", "gap-2", "flex-col", "items-start"], {
1983
2380
  variants: {
1984
- border: { true: ["!component-search-borderless"], false: "" },
1985
- widenOnFocus: {
1986
- true: [
1987
- "component-search-width",
1988
- "component-search-widen",
1989
- "hover:component-search-widen",
1990
- "focus-within:w-full",
1991
- "max-w-sm",
1992
- ],
1993
- false: "w-full",
2381
+ layout: {
2382
+ inline: ["flex", "gap-3", "flex-row", "items-center"],
1994
2383
  },
1995
2384
  },
1996
2385
  });
1997
-
1998
- /**
1999
- * The Search component is used to render a search input field.
2000
- *
2001
- * @param {SearchProps} props - The props for the Search component
2002
- */
2003
- const Search = ({ className, placeholder, value, widenInputOnFocus, hideBorderWhenNotInFocus = false, disabled = false, onKeyUp, onChange, onFocus, onBlur, name, onClear, dataTestId, autoComplete = "on", loading = false, inputClassName, iconName = "MagnifyingGlass", style, xMarkRef, ref, ...rest }) => {
2004
- const { t } = useTranslation();
2005
- return (jsx(TextBaseInput, { ...rest, autoComplete: autoComplete, className: cvaSearch({ className, border: hideBorderWhenNotInFocus, widenOnFocus: widenInputOnFocus }), dataTestId: dataTestId, disabled: disabled, inputClassName: inputClassName, name: name, onBlur: onBlur, onChange: onChange, onFocus: onFocus, onKeyUp: onKeyUp, placeholder: placeholder ?? t("search.placeholder"), prefix: loading ? (jsx(Spinner, { centering: "centered", size: rest.fieldSize ?? undefined })) : (jsx(Icon, { name: iconName, size: rest.fieldSize ?? undefined })), ref: ref, suffix:
2006
- //only show the clear button if there is a value and the onClear function is provided
2007
- onClear && value ? (jsx("button", { className: "flex", "data-testid": dataTestId ? `${dataTestId}_suffix_component` : null, onClick: () => {
2008
- onClear();
2009
- }, ref: xMarkRef, type: "button", children: jsx(Icon, { name: "XMark", size: "small" }) })) : undefined, value: value }));
2010
- };
2011
- Search.displayName = "Search";
2012
-
2013
- const cvaSelect = cvaMerge([
2014
- "relative",
2015
- "flex",
2016
- "shadow-sm",
2017
- "rounded-lg",
2018
- "border-neutral-300",
2019
- "hover:border-neutral-400",
2020
- "hover:bg-neutral-50",
2386
+ const cvaRadioItem = cvaMerge([
2387
+ "self-center",
2388
+ "w-4",
2389
+ "h-4",
2390
+ "appearance-none",
2391
+ "rounded-3xl",
2021
2392
  "bg-white",
2393
+ "border-solid",
2394
+ "border",
2395
+ "border-neutral-300",
2396
+ "shadow-sm",
2397
+ "shrink-0",
2022
2398
  "transition",
2023
- "text-sm",
2024
- "min-h-0",
2399
+ "box-border",
2400
+ "hover:cursor-pointer",
2401
+ "hover:bg-neutral-100",
2402
+ "focus-visible:outline-primary-700",
2025
2403
  ], {
2026
2404
  variants: {
2027
- fieldSize: {
2028
- small: ["h-7", "text-xs"],
2029
- medium: ["h-8"],
2030
- large: ["h-10"],
2405
+ checked: {
2406
+ true: [
2407
+ "border-solid",
2408
+ "border-4",
2409
+ "border-primary-600",
2410
+ "bg-white",
2411
+ "hover:bg-neutral-100",
2412
+ "hover:cursor-pointer",
2413
+ "outline-0",
2414
+ "active:bg-neutral-200",
2415
+ "active:ring-2",
2416
+ "active:ring-inset",
2417
+ "active:ring-primary-700",
2418
+ "group-active:ring-2",
2419
+ "group-active:ring-inset",
2420
+ "group-active:ring-primary-700",
2421
+ ],
2422
+ false: "",
2031
2423
  },
2032
2424
  invalid: {
2033
- true: "border border-red-600 text-red-600 hover:border-red-600",
2425
+ true: ["border-red-600", "active:ring-red-700"],
2034
2426
  false: "",
2035
2427
  },
2036
2428
  disabled: {
2037
- true: "!bg-neutral-100 hover:border-neutral-300",
2038
- false: "",
2039
- },
2040
- },
2041
- defaultVariants: {
2042
- invalid: false,
2043
- disabled: false,
2044
- },
2045
- });
2046
- const cvaSelectControl = cvaMerge([], {
2047
- variants: {
2048
- isDisabled: {
2049
- true: "!bg-neutral-100",
2050
- false: "",
2051
- },
2052
- prefix: {
2053
- true: ["ps-7"],
2054
- false: "",
2055
- },
2056
- invalid: {
2057
- true: "!border-0",
2429
+ true: [
2430
+ "bg-neutral-400",
2431
+ "border-neutral-300",
2432
+ "cursor-not-allowed",
2433
+ "hover:bg-neutral-400",
2434
+ "active:bg-neutral-400",
2435
+ "group-active:ring-0",
2436
+ "group-active:ring-inset",
2437
+ ],
2058
2438
  false: "",
2059
- },
2060
- },
2061
- defaultVariants: {
2062
- isDisabled: false,
2063
- prefix: false,
2064
- invalid: false,
2065
- },
2066
- });
2067
- const cvaSelectIcon = cvaMerge([
2068
- "mr-2",
2069
- "flex",
2070
- "cursor-pointer",
2071
- "items-center",
2072
- "justify-center",
2073
- "text-neutral-400",
2074
- "hover:text-neutral-500",
2075
- ]);
2076
- const cvaSelectPrefixSuffix = cvaMerge(["flex", "justify-center", "items-center", "text-neutral-400", "absolute", "inset-y-0"], {
2077
- variants: {
2078
- kind: {
2079
- prefix: ["pl-3", "left-0"],
2080
- suffix: ["pr-3", "right-0"],
2081
- },
2082
- },
2083
- });
2084
- const cvaSelectXIcon = cvaMerge([
2085
- "mr-2",
2086
- "flex",
2087
- "cursor-pointer",
2088
- "items-center",
2089
- "justify-center",
2090
- "text-neutral-400",
2091
- "hover:text-neutral-500",
2092
- "ml-1",
2093
- ]);
2094
- const cvaSelectMenuList = cvaMerge([], {
2095
- variants: {
2096
- menuIsOpen: {
2097
- true: "animate-fade-in-fast",
2098
- false: "animate-fade-out-fast",
2099
- },
2100
- },
2101
- });
2102
- const cvaSelectDynamicTagContainer = cvaMerge(["h-full", "flex", "gap-1", "items-center"], {
2103
- variants: {
2104
- visible: { true: "visible", false: "invisible" },
2105
- },
2106
- });
2107
- const cvaSelectCounter = cvaMerge(["overflow-hidden", "whitespace-nowrap"]);
2108
- const cvaSelectMenu = cvaMerge(["relative", "p-1", "grid", "gap-1"]);
2109
-
2110
- /**
2111
- * A single select menu item is a basic wrapper around Menu item designed to be used as a single value render in Select list
2112
- *
2113
- * @param {SelectMenuItemProps} props - The props for the SingleSelectMenuItem
2114
- * @returns {ReactElement} SingleSelectMenuItem
2115
- */
2116
- const SingleSelectMenuItem = ({ label, icon, onClick, selected, focused, dataTestId, disabled, optionLabelDescription, optionPrefix, fieldSize, }) => {
2117
- return (jsx(MenuItem, { dataTestId: dataTestId, disabled: disabled, fieldSize: fieldSize, focused: focused, label: label, onClick: onClick, optionLabelDescription: optionLabelDescription, optionPrefix: isValidElement(optionPrefix)
2118
- ? cloneElement(optionPrefix, {
2119
- className: "mr-1 flex items-center",
2120
- size: "medium",
2121
- })
2122
- : optionPrefix, prefix: icon, selected: selected, suffix: selected ? jsx(Icon, { className: "block text-blue-600", name: "Check", size: "medium" }) : undefined }));
2123
- };
2124
- /**
2125
- * A multi select menu item is a basic wrapper around Menu item designed to be used as a multi value render in Select list
2126
- *
2127
- * @param {SelectMenuItemProps} props - The props for the MultiSelectMenuItem
2128
- * @returns {ReactElement} multi select menu item
2129
- */
2130
- const MultiSelectMenuItem = ({ label, onClick, selected, focused, dataTestId, disabled, optionLabelDescription, optionPrefix, fieldSize, }) => {
2131
- return (jsx(MenuItem, { dataTestId: dataTestId, disabled: disabled, fieldSize: fieldSize, focused: focused, label: label, onClick: e => {
2132
- e.stopPropagation();
2133
- onClick && onClick(e);
2134
- }, optionLabelDescription: optionLabelDescription, optionPrefix: isValidElement(optionPrefix)
2135
- ? cloneElement(optionPrefix, {
2136
- className: "mr-1 flex items-center",
2137
- size: "medium",
2138
- })
2139
- : optionPrefix, prefix: jsx(Checkbox, { checked: selected, className: "gap-x-0", disabled: disabled, onChange: () => null, onClick: e => {
2140
- e.stopPropagation();
2141
- }, readOnly: false }), selected: selected }));
2142
- };
2143
-
2144
- /**
2145
- * Extended Tag component with information about its own width.
2146
- * Used in the select component.
2147
- *
2148
- * @param {TagProps} props - The props for the tag component
2149
- * @returns {ReactElement} TagWithWidth component
2150
- */
2151
- const TagWithWidth = ({ onWidthKnown, children, ...rest }) => {
2152
- const ref = useRef(null);
2153
- useLayoutEffect(() => {
2154
- onWidthKnown && onWidthKnown({ width: ref.current?.offsetWidth || 0 });
2155
- }, [ref, onWidthKnown]);
2156
- return (jsx(Tag, { ref: ref, ...rest, icon: isValidElement(rest.icon) ? cloneElement(rest.icon, { size: "small" }) : rest.icon, children: children }));
2157
- };
2439
+ },
2440
+ },
2441
+ compoundVariants: [
2442
+ {
2443
+ checked: true,
2444
+ disabled: true,
2445
+ className: ["bg-white"],
2446
+ },
2447
+ ],
2448
+ });
2449
+
2450
+ const RadioGroupContext = createContext(null);
2158
2451
 
2159
2452
  /**
2160
- * TagsContainer component to display tags in limited space when children can't fit space it displays counter
2453
+ * Use radio buttons when you have a group of mutually exclusive choices and only one selection from the group is allowed.
2161
2454
  *
2162
- * @param {TagsContainerProps} props - The props for the TagContainer
2163
- * @returns {ReactElement} TagsContainer
2455
+ * Radio buttons are used for mutually exclusive choices, not for multiple choices. Only one radio button can be selected at a time. When a user chooses a new item, the previous choice is automatically deselected.
2456
+ *
2457
+ * _**Do use** Radio buttons in forms, settings, or selections in a list._
2458
+ *
2459
+ * _**Do not use** Radio buttons if a user can select many option from a list, use checkboxes instead of radio buttons._
2460
+ *
2461
+ * @param {RadioGroupProps} props - The props for the RadioGroup component
2462
+ * @returns {ReactElement} RadioGroup component
2164
2463
  */
2165
- const TagsContainer = ({ items, width = "100%", itemsGap = 6, postFix, preFix, disabled, }) => {
2166
- const containerRef = useRef(null);
2167
- const [isReady, setIsReady] = useState(false);
2168
- const [counterWidth, setCounterWidth] = useState(0);
2169
- const [availableSpaceWidth, setAvailableSpaceWidth] = useState(0);
2170
- const [childrenWidths, setChildrenWidths] = useState([]);
2171
- const itemsCount = items.length;
2172
- const dimensions = useResize();
2173
- const { width: windowWidth } = useDebounce(dimensions, 100);
2174
- useEffect(() => {
2175
- const containerWidth = containerRef.current?.offsetWidth || 0;
2176
- setAvailableSpaceWidth(containerWidth);
2177
- }, [windowWidth]);
2178
- const onWidthKnownHandler = useCallback(({ width: reportedWidth }) => {
2179
- setChildrenWidths(prev => {
2180
- const next = [...prev, { width: reportedWidth + itemsGap }];
2181
- if (next.length === itemsCount) {
2182
- setIsReady(true);
2183
- }
2184
- return next;
2185
- });
2186
- }, [itemsCount, itemsGap]);
2187
- const renderedElements = useMemo(() => {
2188
- const requiredSpace = childrenWidths.reduce((previous, current) => {
2189
- return previous + current.width;
2190
- }, 0);
2191
- const availableSpace = availableSpaceWidth - counterWidth;
2192
- const { elements } = items
2193
- .concat({ text: "", onClick: () => null, disabled: false })
2194
- .reduce((acc, item, index) => {
2195
- const spaceNeeded = childrenWidths.slice(0, index + 1).reduce((previous, current) => {
2196
- return previous + current.width;
2197
- }, 0);
2198
- const isLast = index === items.length;
2199
- const counterRequired = requiredSpace > availableSpace && acc.counter !== 0;
2200
- if (isLast && counterRequired) {
2201
- return {
2202
- ...acc,
2203
- elements: [
2204
- ...acc.elements,
2205
- 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),
2206
- ],
2207
- };
2208
- }
2209
- if (isLast) {
2210
- return acc;
2211
- }
2212
- const itemCanFit = spaceNeeded <= availableSpace;
2213
- if (itemCanFit) {
2214
- return {
2215
- ...acc,
2216
- elements: [
2217
- ...acc.elements,
2218
- jsx(TagWithWidth, { className: "inline-flex shrink-0", color: item.disabled ? "neutral" : "white", dataTestId: `${item.text}-tag`, disabled: disabled, icon: item.Icon, onClose: e => {
2219
- e.stopPropagation();
2220
- item.onClick();
2221
- }, onWidthKnown: onWidthKnownHandler, children: item.text }, item.text + index),
2222
- ],
2223
- };
2224
- }
2225
- return {
2226
- elements: acc.elements,
2227
- counter: item.text !== "" ? acc.counter + 1 : acc.counter,
2228
- };
2229
- }, { elements: [], counter: 0 });
2230
- return elements;
2231
- }, [items, availableSpaceWidth, counterWidth, disabled, onWidthKnownHandler, childrenWidths]);
2232
- return (jsxs("div", { className: cvaSelectDynamicTagContainer({ visible: isReady || !!preFix }), ref: containerRef, style: {
2233
- width: `${width}`,
2234
- }, children: [preFix, renderedElements, postFix] }));
2464
+ const RadioGroup = ({ children, id, name, value, disabled, onChange, label, inline, className, dataTestId, isInvalid, }) => {
2465
+ return (jsx(FormGroup, { dataTestId: dataTestId ? `${dataTestId}-FormGroup` : undefined, label: label, children: jsx("div", { className: cvaRadioGroup({ layout: inline ? "inline" : null, className }), "data-testid": dataTestId, children: jsx(RadioGroupContext.Provider, { value: {
2466
+ id,
2467
+ value,
2468
+ name: name || id,
2469
+ onChange,
2470
+ disabled,
2471
+ isInvalid,
2472
+ }, children: children }) }) }));
2235
2473
  };
2474
+ RadioGroup.displayName = "RadioGroup";
2236
2475
 
2237
2476
  /**
2238
- * A hook to retrieve components override object.
2239
- * 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.
2477
+ * The RadioItem component.
2240
2478
  *
2241
- * @template IsMulti
2242
- * @template Group
2243
- * @param {Partial<SelectComponents<Option, IsMulti, Group>> | undefined} componentsProps a custom component prop that you can to override defaults
2244
- * @param {boolean} disabled decide to override disabled variant
2245
- * @param {boolean} menuIsOpen menu is open state
2246
- * @param {string} dataTestId a test id
2247
- * @param {number} maxSelectedDisplayCount a number of max display count
2248
- * @param {boolean} hasError decide to override hasError variant
2249
- * @param {ReactNode} prefix a prefix element
2250
- * @returns {Partial<SelectComponents<Option, boolean, GroupBase<Option>>> | undefined} components object to override react-select default components
2479
+ * @param {RadioItemProps} props - The props for the RadioItem component
2480
+ * @returns {ReactElement} RadioItem component
2251
2481
  */
2252
- const useCustomComponents = ({ componentsProps, disabled, readOnly, setMenuIsEnabled, dataTestId, maxSelectedDisplayCount, prefix, hasError, fieldSize, getOptionLabelDescription, getOptionPrefix, }) => {
2253
- const [t] = useTranslation();
2254
- // perhaps it should not be wrap in memo (causing some issues with opening and closing on mobiles)
2255
- const customComponents = useMemo(() => {
2256
- return {
2257
- ValueContainer: props => {
2258
- if (props.isMulti && Array.isArray(props.children) && props.children.length > 0) {
2259
- const PLACEHOLDER_KEY = "placeholder";
2260
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
2261
- const key = props && props.children && props.children[0] ? props.children[0]?.key : "";
2262
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
2263
- const values = props && props.children ? props.children[0] : [];
2264
- const tags = key === PLACEHOLDER_KEY ? [] : values;
2265
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
2266
- const searchInput = props && props.children && props.children[1];
2267
- const placeholderElement = Array.isArray(props.children)
2268
- ? props.children.find(child => child && child.key === PLACEHOLDER_KEY)
2269
- : null;
2270
- return (jsx(components.ValueContainer, { ...props, isDisabled: props.selectProps.isDisabled, children: maxSelectedDisplayCount === undefined ? (jsx(TagsContainer, { disabled: disabled, items: tags
2271
- ? tags.map(({ props: tagProps }) => {
2272
- const optionPrefix = tagProps.data && getOptionPrefix ? getOptionPrefix(tagProps.data) : null;
2273
- return {
2274
- text: tagProps.children,
2275
- onClick: disabled
2276
- ? undefined
2277
- : (e) => {
2278
- setMenuIsEnabled(false);
2279
- tagProps.removeProps.onClick && tagProps.removeProps.onClick(e);
2280
- },
2281
- disabled: disabled,
2282
- Icon: optionPrefix,
2283
- };
2284
- })
2285
- : [], postFix: searchInput, preFix: placeholderElement ? jsx("span", { className: "absolute", children: placeholderElement }) : null, width: "100%" })) : (jsxs(Fragment, { children: [tags
2286
- ? tags.slice(0, maxSelectedDisplayCount).map(({ props: tagProps }) => {
2287
- return (jsx(Tag, { className: "inline-flex shrink-0", color: disabled ? "unknown" : "primary", dataTestId: tagProps.children ? `${tagProps.children.toString()}-tag` : undefined, onClose: e => {
2288
- e.stopPropagation();
2289
- setMenuIsEnabled(false);
2290
- tagProps.removeProps.onClick && tagProps.removeProps.onClick(e);
2291
- }, children: tagProps.children }, tagProps.children?.toString()));
2292
- })
2293
- : null, tags && tags.length > maxSelectedDisplayCount ? (jsxs(Tag, { color: "neutral", dataTestId: "counter-tag", children: ["+", tags.length - maxSelectedDisplayCount] })) : null, searchInput, placeholderElement] })) }));
2294
- }
2295
- return (jsx(Fragment, { children: jsx(components.ValueContainer, { ...props, isDisabled: props.selectProps.isDisabled, children: props.children }) }));
2296
- },
2297
- LoadingIndicator: () => {
2298
- return jsx(Spinner, { centering: "vertically", className: "mr-2", size: "small" });
2299
- },
2300
- DropdownIndicator: props => {
2301
- const icon = props.selectProps.menuIsOpen ? (jsx(Icon, { name: "ChevronUp", size: "medium" })) : (jsx(Icon, { name: "ChevronDown", size: "medium" }));
2302
- return props.selectProps.isLoading || props.selectProps.isDisabled || readOnly ? null : (jsx(components.DropdownIndicator, { ...props, children: jsx("div", { className: cvaSelectIcon(), children: icon }) }));
2303
- },
2304
- IndicatorSeparator: () => null,
2305
- ClearIndicator: props => {
2306
- if (disabled) {
2307
- return null;
2308
- }
2309
- return (jsx(components.ClearIndicator, { ...props, innerProps: {
2310
- ...props.innerProps,
2311
- onMouseDown: e => {
2312
- e.preventDefault();
2313
- },
2314
- }, 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" }) }) }));
2315
- },
2316
- Control: props => {
2317
- return (jsx(components.Control, { ...props, className: cvaSelectControl({
2318
- isDisabled: props.isDisabled,
2319
- prefix: prefix ? true : false,
2320
- invalid: hasError,
2321
- }) }));
2322
- },
2323
- SingleValue: props => {
2324
- const optionPrefix = getOptionPrefix ? getOptionPrefix(props.data) : null;
2325
- 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] }) }));
2326
- },
2327
- Menu: props => {
2328
- return (jsx(components.Menu, { ...props, className: cvaSelectMenuList({ menuIsOpen: props.selectProps.menuIsOpen }) }));
2329
- },
2330
- Placeholder: props => {
2331
- return (jsx(components.Placeholder, { ...props, className: "!text-neutral-400", children: props.children }));
2332
- },
2333
- MenuList: props => {
2334
- return (jsx(components.MenuList, { ...props, innerProps: {
2335
- ...props.innerProps,
2336
- onScroll: e => {
2337
- const listEl = e.currentTarget;
2338
- if (listEl.scrollTop + listEl.clientHeight >= listEl.scrollHeight &&
2339
- props.selectProps.onMenuScrollToBottom) {
2340
- /Firefox/.test(navigator.userAgent)
2341
- ? props.selectProps.onMenuScrollToBottom(new WheelEvent("scroll"))
2342
- : props.selectProps.onMenuScrollToBottom(new TouchEvent(""));
2343
- }
2344
- },
2345
- }, children: props.children }));
2346
- },
2347
- Option: props => {
2348
- const componentProps = {
2349
- label: props.label,
2350
- focused: props.isFocused,
2351
- selected: props.isSelected,
2352
- onClick: props.innerProps.onClick,
2353
- };
2354
- return (jsx(components.Option, { ...props, innerProps: {
2355
- ...props.innerProps,
2356
- role: "option",
2357
- onClick: () => { },
2358
- }, children: props.isMulti ? (jsx(MultiSelectMenuItem, { ...componentProps, dataTestId: typeof props.label === "string" ? props.label : undefined, disabled: disabled, fieldSize: fieldSize, optionLabelDescription: getOptionLabelDescription?.(props.data), optionPrefix: getOptionPrefix?.(props.data) })) : (jsx(SingleSelectMenuItem, { ...componentProps, dataTestId: typeof props.label === "string" ? props.label : undefined, disabled: disabled || props.isDisabled, fieldSize: fieldSize, optionLabelDescription: getOptionLabelDescription?.(props.data), optionPrefix: getOptionPrefix?.(props.data) })) }));
2359
- },
2360
- ...componentsProps,
2361
- };
2362
- }, [
2363
- componentsProps,
2364
- maxSelectedDisplayCount,
2365
- disabled,
2366
- setMenuIsEnabled,
2367
- readOnly,
2368
- dataTestId,
2369
- t,
2370
- prefix,
2371
- hasError,
2372
- getOptionLabelDescription,
2373
- fieldSize,
2374
- getOptionPrefix,
2375
- ]);
2376
- return customComponents;
2482
+ const RadioItem = ({ label, value, dataTestId, className, description, suffix, ...rest }) => {
2483
+ const groupCtx = useContext(RadioGroupContext);
2484
+ const isChecked = groupCtx?.value === value;
2485
+ const { ref: labelRef, isTextTruncated: isLabelTruncated } = useIsTextTruncated();
2486
+ const { ref: descriptionRef, isTextTruncated: isDescriptionTruncated } = useIsTextTruncated();
2487
+ const descriptionId = description ? `${groupCtx?.id}-${value}-description` : undefined;
2488
+ const inputId = `${groupCtx?.id}-${value}`;
2489
+ const hasLabel = label !== undefined && label !== null && label !== "";
2490
+ return (jsxs("label", { className: hasLabel
2491
+ ? cvaBinaryControlWrapper({ className })
2492
+ : `inline-flex w-fit items-center gap-2 ${className || ""}`.trim(), "data-testid": dataTestId ? `${dataTestId}-Wrapper` : undefined, htmlFor: inputId, children: [jsx("input", { "aria-describedby": descriptionId, checked: isChecked, className: cvaRadioItem({
2493
+ checked: isChecked,
2494
+ disabled: groupCtx?.disabled,
2495
+ invalid: groupCtx?.isInvalid,
2496
+ }), "data-testid": dataTestId, id: inputId, onChange: groupCtx?.onChange, type: "radio", value: value, ...rest }), hasLabel ? (jsx(Tooltip, { className: cvaBinaryControlLabelTooltip(), dataTestId: dataTestId ? `${dataTestId}-Label-Tooltip` : undefined, disabled: !isLabelTruncated, label: label, placement: "top", children: jsx("span", { className: cvaLabel({
2497
+ invalid: groupCtx?.isInvalid,
2498
+ disabled: groupCtx?.disabled,
2499
+ }), "data-testid": dataTestId ? `${dataTestId}-Label` : undefined, ref: labelRef, children: label }) }, "tooltip-" + rest.name)) : null, suffix ? (jsx("div", { className: cvaBinaryControlSuffixContainer(), "data-testid": dataTestId ? `${dataTestId}-suffix-container` : undefined, children: suffix })) : null, description ? (jsx(Tooltip, { className: cvaBinaryControlDescriptionTooltip(), dataTestId: dataTestId ? `${dataTestId}-Description-Tooltip` : undefined, disabled: !isDescriptionTruncated, label: description, placement: "top", children: jsx("span", { className: cvaBinaryControlDescription({ disabled: groupCtx?.disabled }), "data-testid": dataTestId ? `${dataTestId}-Description` : undefined, id: descriptionId, ref: descriptionRef, children: description }) }, "description-tooltip-" + rest.name)) : null] }));
2377
2500
  };
2378
2501
 
2502
+ const cvaTimeRange = cvaMerge([
2503
+ "flex",
2504
+ "flex-1",
2505
+ "items-center",
2506
+ "gap-4",
2507
+ "max-sm:gap-2",
2508
+ "border-transparent",
2509
+ "rounded-md",
2510
+ ]);
2511
+
2379
2512
  /**
2380
- * @template IsMulti
2381
- * @template Group
2382
- * @param {RefObject<HTMLDivElement | null>} refContainer react ref to container element
2383
- * @param {number | undefined} maxSelectedDisplayCount a number of max display count
2384
- * @param {StylesConfig<Option, IsMulti, Group> | undefined} styles a optional object to override styles of react-select
2385
- * @returns {StylesConfig<Option, boolean>} styles to override in select
2513
+ * TimeRange is used to create a time range entry.
2514
+ *
2515
+ * @param {TimeRangeProps} props - The props for the TimeRange component
2516
+ * @returns {ReactElement} TimeRange component
2386
2517
  */
2387
- const useCustomStyles = ({ refContainer, maxSelectedDisplayCount, styles, disabled, fieldSize, }) => {
2388
- const customStyles = useMemo(() => {
2518
+ const TimeRange = ({ id, className, dataTestId, children, range, onChange, disabled, isInvalid, }) => {
2519
+ const [timeRange, setTimeRange] = useState(range ?? {
2520
+ timeFrom: DEFAULT_TIME,
2521
+ timeTo: DEFAULT_TIME,
2522
+ });
2523
+ const onChangeFrom = (timeFrom) => {
2524
+ setTimeRange(prev => ({ ...prev, timeFrom }));
2525
+ };
2526
+ const onChangeTo = (timeTo) => {
2527
+ setTimeRange(prev => ({ ...prev, timeTo }));
2528
+ };
2529
+ const onRangeChange = () => onChange(timeRange);
2530
+ return (jsxs("div", { className: cvaTimeRange({ className }), "data-testid": dataTestId, id: id, children: [jsx(BaseInput, { dataTestId: `${dataTestId}-from`, disabled: disabled, isInvalid: isInvalid, onBlur: onRangeChange, onChange: (time) => onChangeFrom(time.currentTarget.value), type: "time", value: timeRange.timeFrom === "" ? DEFAULT_TIME : timeRange.timeFrom }), children ?? jsx("div", { "data-testid": `${dataTestId}-separator`, children: "-" }), jsx(BaseInput, { dataTestId: `${dataTestId}-to`, disabled: disabled, isInvalid: isInvalid, onBlur: onRangeChange, onChange: (time) => onChangeTo(time.currentTarget.value), type: "time", value: timeRange.timeTo === "" ? DEFAULT_TIME : timeRange.timeTo })] }));
2531
+ };
2532
+ const DEFAULT_TIME = "12:00";
2533
+
2534
+ const cvaScheduleItem = cvaMerge([
2535
+ "grid",
2536
+ "pb-4",
2537
+ "gap-2",
2538
+ "grid-cols-[60px,200px,60px,2fr]",
2539
+ "max-sm:grid-cols-1",
2540
+ ]);
2541
+ const cvaScheduleItemText = cvaMerge(["flex", "font-bold", "self-center"]);
2542
+
2543
+ /**
2544
+ * Schedule is used to create a time range entries.
2545
+ *
2546
+ * @param {ScheduleProps} props - The props for the Schedule component
2547
+ * @returns {ReactElement} Schedule component
2548
+ */
2549
+ const Schedule = ({ className, dataTestId, schedule, onChange, invalidKeys = [] }) => {
2550
+ const [t] = useTranslation();
2551
+ const onRangeChange = (range, index) => {
2552
+ const newSchedule = schedule.map((day, dayIndex) => (index === dayIndex ? { ...day, range: { ...range } } : day));
2553
+ onChange(newSchedule);
2554
+ };
2555
+ const onActiveChange = (isActive, index) => {
2556
+ const newSchedule = schedule.map((day, dayIndex) => index === dayIndex
2557
+ ? {
2558
+ ...day,
2559
+ range: {
2560
+ timeFrom: day.range.timeFrom ? day.range.timeFrom : DEFAULT_TIME,
2561
+ timeTo: day.range.timeTo ? day.range.timeTo : DEFAULT_TIME,
2562
+ },
2563
+ isActive,
2564
+ }
2565
+ : day);
2566
+ onChange(newSchedule);
2567
+ };
2568
+ const onAllDayChange = (isAllDayChecked, index) => {
2569
+ const newSchedule = schedule.map((day, dayIndex) => index === dayIndex
2570
+ ? {
2571
+ ...day,
2572
+ range: {
2573
+ timeFrom: day.range.timeFrom ? day.range.timeFrom : DEFAULT_TIME,
2574
+ timeTo: day.range.timeTo ? day.range.timeTo : DEFAULT_TIME,
2575
+ },
2576
+ isAllDay: isAllDayChecked,
2577
+ }
2578
+ : day);
2579
+ onChange(newSchedule);
2580
+ };
2581
+ return (jsx("div", { className: className, "data-testid": dataTestId, children: schedule.map(({ label, range, isActive, key, checkboxLabel, isAllDay }, index) => {
2582
+ return (jsxs("div", { className: cvaScheduleItem(), children: [jsxs("div", { className: "grid grid-cols-2 gap-4 sm:hidden", children: [jsx(Text, { className: "font-medium text-neutral-500", children: t("schedule.label.day") }), jsx(Text, { className: cvaScheduleItemText(), size: "medium", subtle: !isActive, children: label }), jsx(Text, { className: "font-medium text-neutral-500", children: t("schedule.label.active") }), jsx(Checkbox, { checked: isActive, label: checkboxLabel, onChange: (event) => onActiveChange(Boolean(event.currentTarget.checked), index) }), jsx(Text, { className: "font-medium text-neutral-500", children: t("schedule.label.allDay") }), jsx(Checkbox, { checked: isAllDay ? isActive : undefined, disabled: !isActive, onChange: (event) => onAllDayChange(Boolean(event.currentTarget.checked), index) }), jsx(TimeRange, { disabled: !isActive || isAllDay, isInvalid: !!invalidKeys.find((invalidKey) => invalidKey === key), onChange: (newRange) => onRangeChange(newRange, index), range: range })] }), jsxs("div", { className: "max-sm:hidden sm:grid sm:grid-cols-[100px,200px,60px,250px,250px] sm:gap-2", children: [jsx(Checkbox, { checked: isActive, dataTestId: `${dataTestId}-${key}-checkbox`, label: checkboxLabel, onChange: (event) => onActiveChange(Boolean(event.currentTarget.checked), index) }), jsx(Text, { className: cvaScheduleItemText(), size: "medium", subtle: !isActive, children: label }), jsx(Checkbox, { checked: isAllDay ? isActive : undefined, dataTestId: `${dataTestId}-${key}-allday-checkbox`, disabled: !isActive, onChange: (event) => onAllDayChange(Boolean(event.currentTarget.checked), index) }), jsx(TimeRange, { dataTestId: `${dataTestId}-${key}-range`, disabled: !isActive || isAllDay, isInvalid: !!invalidKeys.find((invalidKey) => invalidKey === key), onChange: (newRange) => onRangeChange(newRange, index), range: isAllDay ? undefined : range })] })] }, key + label));
2583
+ }) }));
2584
+ };
2585
+
2586
+ const weekDay = {
2587
+ Monday: "monday",
2588
+ Tuesday: "tuesday",
2589
+ Wednesday: "wednesday",
2590
+ Thursday: "thursday",
2591
+ Friday: "friday",
2592
+ Saturday: "saturday",
2593
+ Sunday: "sunday",
2594
+ };
2595
+ var ScheduleVariant;
2596
+ (function (ScheduleVariant) {
2597
+ ScheduleVariant["ALL_DAYS"] = "all";
2598
+ ScheduleVariant["WEEKDAYS"] = "week";
2599
+ ScheduleVariant["CUSTOM"] = "custom";
2600
+ })(ScheduleVariant || (ScheduleVariant = {}));
2601
+ /**
2602
+ * Parse a string of week range schedule string to human readable schedule range.
2603
+ *
2604
+ * @param {string} scheduleString String of week schedule
2605
+ * @returns {WeekSchedule} Week schedule range
2606
+ */
2607
+ const parseSchedule = (scheduleString) => {
2608
+ if (!scheduleString) {
2389
2609
  return {
2390
- control: base => {
2391
- return {
2392
- ...base,
2393
- minHeight: fieldSize === "small" ? "28px" : fieldSize === "large" ? "40px" : "32px",
2394
- borderRadius: "var(--border-radius-lg)",
2395
- backgroundColor: "inherit",
2396
- };
2397
- },
2398
- singleValue: base => ({
2399
- ...base,
2400
- }),
2401
- multiValue: base => ({
2402
- ...base,
2403
- }),
2404
- multiValueLabel: base => ({
2405
- ...base,
2406
- }),
2407
- indicatorsContainer: base => ({
2408
- ...base,
2409
- ...(disabled && { display: "none" }),
2410
- }),
2411
- indicatorSeparator: () => ({
2412
- width: "0px",
2413
- }),
2414
- menu: base => {
2415
- return {
2416
- ...base,
2417
- width: "100%",
2418
- marginTop: "4px",
2419
- marginBottom: "18px",
2420
- transition: "all 1s ease-in-out",
2421
- };
2422
- },
2423
- input: base => ({
2424
- ...base,
2425
- marginLeft: "0px",
2426
- }),
2427
- placeholder: base => ({
2428
- ...base,
2429
- }),
2430
- option: () => ({}),
2431
- menuPortal: base => ({
2432
- ...base,
2433
- width: refContainer.current ? `${refContainer.current.clientWidth}px` : base.width,
2434
- backgroundColor: "#ffffff",
2435
- borderRadius: "var(--border-radius-lg)",
2436
- zIndex: "var(--z-overlay)",
2437
- borderColor: "rgb(var(--color-neutral-300))",
2438
- boxShadow: "var(--tw-ring-inset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)",
2439
- }),
2440
- menuList: base => {
2441
- return {
2442
- ...base,
2443
- position: "relative",
2444
- padding: "var(--spacing-1)",
2445
- display: "grid",
2446
- gap: "var(--spacing-1)",
2447
- width: "100%",
2448
- borderRadius: "0px",
2449
- boxShadow: "none",
2450
- paddingTop: "0px",
2451
- };
2452
- },
2453
- valueContainer: base => {
2454
- return {
2455
- ...base,
2456
- paddingBlock: 0,
2457
- flexWrap: maxSelectedDisplayCount !== undefined ? "wrap" : "nowrap",
2458
- gap: "0.25rem",
2459
- };
2460
- },
2461
- container: base => ({
2462
- ...base,
2463
- width: "100%",
2464
- }),
2465
- dropdownIndicator: base => ({
2466
- ...base,
2467
- padding: "0px",
2468
- }),
2469
- clearIndicator: base => {
2470
- return {
2471
- ...base,
2472
- padding: "0px",
2473
- };
2474
- },
2475
- ...styles,
2610
+ variant: ScheduleVariant.CUSTOM,
2611
+ schedule: [1, 2, 3, 4, 5, 6, 7].map(day => ({
2612
+ day,
2613
+ range: { timeFrom: "", timeTo: "" },
2614
+ isAllDay: false,
2615
+ isActive: false,
2616
+ })),
2476
2617
  };
2477
- }, [refContainer, disabled, fieldSize, maxSelectedDisplayCount, styles]);
2478
- return { customStyles };
2618
+ }
2619
+ const schedule = scheduleString.split(",").map(daySchedule => {
2620
+ const [day, timeRange] = daySchedule.split("#");
2621
+ const [timeFrom, timeTo] = (timeRange ?? "").split("-");
2622
+ const isAllDay = timeFrom === "00:00" && timeTo === "24:00";
2623
+ return {
2624
+ day: Number(day),
2625
+ range: timeFrom === undefined || timeTo === undefined
2626
+ ? undefined
2627
+ : {
2628
+ timeFrom: timeFrom,
2629
+ timeTo: timeTo,
2630
+ },
2631
+ isAllDay,
2632
+ isActive: Boolean(timeFrom) && Boolean(timeTo),
2633
+ };
2634
+ });
2635
+ const filteredSchedule = schedule
2636
+ .filter(daySchedule => daySchedule.range)
2637
+ .map(daySchedule => ({
2638
+ day: daySchedule.day,
2639
+ range: daySchedule.range,
2640
+ isAllDay: daySchedule.isAllDay,
2641
+ isActive: daySchedule.isActive,
2642
+ }));
2643
+ let variant;
2644
+ switch (schedule.length) {
2645
+ case 7:
2646
+ variant = isUniform(schedule) ? ScheduleVariant.ALL_DAYS : ScheduleVariant.CUSTOM;
2647
+ break;
2648
+ case 5:
2649
+ variant = hasConsecutiveDays(schedule) ? ScheduleVariant.WEEKDAYS : ScheduleVariant.CUSTOM;
2650
+ break;
2651
+ default:
2652
+ return {
2653
+ variant: ScheduleVariant.CUSTOM,
2654
+ schedule: filteredSchedule,
2655
+ };
2656
+ }
2657
+ return {
2658
+ variant,
2659
+ schedule: filteredSchedule,
2660
+ };
2479
2661
  };
2480
-
2481
2662
  /**
2482
- * A hook used by selects to share the common code
2663
+ * Serialize week schedule to string schedule
2483
2664
  *
2484
- * @param {SelectProps} props - The props for the Select component
2485
- * @returns {UseSelectProps} Select component
2665
+ * @param {WeekSchedule} weekSchedule Week schedule range
2666
+ * @returns {string} Schedule string
2486
2667
  */
2487
- const useSelect = ({ id, className, 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 }) => {
2488
- const refContainer = useRef(document.createElement("div"));
2489
- const { customStyles } = useCustomStyles({
2490
- refContainer,
2491
- maxSelectedDisplayCount,
2492
- styles,
2493
- disabled: Boolean(disabled),
2494
- fieldSize,
2495
- });
2496
- const [menuIsOpen, setMenuIsOpen] = useState(props.menuIsOpen ?? false);
2497
- const [menuIsEnabled, setMenuIsEnabled] = useState(true);
2498
- const customComponents = useCustomComponents({
2499
- componentsProps: components,
2500
- disabled: Boolean(disabled),
2501
- readOnly: Boolean(props.readOnly),
2502
- setMenuIsEnabled,
2503
- dataTestId,
2504
- maxSelectedDisplayCount,
2505
- prefix,
2506
- hasError,
2507
- fieldSize,
2508
- getOptionLabelDescription,
2509
- getOptionPrefix,
2510
- });
2511
- const menuPlacement = "auto";
2512
- const openMenuHandler = async () => {
2513
- onMenuOpen?.();
2514
- if (menuIsEnabled) {
2515
- setMenuIsOpen(true);
2668
+ const serializeSchedule = (weekSchedule) => {
2669
+ return weekSchedule.schedule
2670
+ .filter(({ range, day, isAllDay }) => {
2671
+ const hasRange = range.timeFrom && range.timeTo;
2672
+ switch (weekSchedule.variant) {
2673
+ case ScheduleVariant.WEEKDAYS:
2674
+ return day <= 5 && hasRange;
2675
+ case ScheduleVariant.ALL_DAYS:
2676
+ return day <= 7 && hasRange;
2677
+ case ScheduleVariant.CUSTOM:
2678
+ default:
2679
+ return hasRange || isAllDay;
2516
2680
  }
2517
- else {
2518
- setMenuIsEnabled(true);
2681
+ })
2682
+ .map(({ day, range, isAllDay }) => {
2683
+ if (isAllDay) {
2684
+ return `${day}#00:00-24:00`;
2519
2685
  }
2520
- };
2521
- const closeMenuHandler = () => {
2522
- setMenuIsOpen(false);
2523
- onMenuClose && onMenuClose();
2524
- };
2525
- return {
2526
- refContainer,
2527
- customStyles,
2528
- menuIsOpen,
2529
- customComponents,
2530
- menuPlacement,
2531
- openMenuHandler,
2532
- closeMenuHandler,
2533
- };
2686
+ return `${day}#${range.timeFrom}-${range.timeTo}`;
2687
+ })
2688
+ .join(",");
2534
2689
  };
2535
-
2536
2690
  /**
2537
- * CreatableSelects are input components used to choose a value from a set.
2691
+ * Checks if a list of schedule objects have the same ranges
2538
2692
  *
2539
- * @param {CreatableSelectProps} props - The props for the CreatableSelect component
2540
- * @returns {ReactElement} CreatableSelect component
2693
+ * @param {RawSchedule[]} schedule List of schedule objects
2694
+ * @returns {boolean} Whether the schedule is uniform
2541
2695
  */
2542
- const CreatableSelect = (props) => {
2543
- const { id, 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;
2544
- const { refContainer, customStyles, menuIsOpen, customComponents, menuPlacement, openMenuHandler, closeMenuHandler } = useSelect(props);
2545
- const reactCreatableSelectProps = useMemo(() => ({
2546
- value,
2547
- menuPlacement,
2548
- maxMenuHeight,
2549
- onChange,
2550
- "aria-label": label,
2551
- "data-testid": dataTestId,
2552
- components: customComponents,
2553
- styles: customStyles,
2554
- tabSelectsValue: false,
2555
- blurInputOnSelect: !isMulti,
2556
- menuPortalTarget: props.menuPortalTarget || document.body,
2557
- isSearchable: disabled || readOnly ? false : isSearchable,
2558
- menuShouldBlockScroll: true,
2559
- menuShouldScrollIntoView: true,
2560
- openMenuOnFocus,
2561
- menuIsOpen: !readOnly ? menuIsOpen : false,
2562
- openMenuOnClick,
2563
- closeMenuOnSelect: false,
2564
- isMulti,
2565
- classNamePrefix,
2566
- isLoading,
2567
- isClearable,
2568
- id,
2569
- onMenuScrollToBottom,
2570
- onInputChange,
2571
- allowCreateWhileLoading,
2572
- onCreateOption,
2573
- isDisabled: Boolean(disabled),
2574
- }), [
2575
- allowCreateWhileLoading,
2576
- classNamePrefix,
2577
- customComponents,
2578
- customStyles,
2579
- dataTestId,
2580
- disabled,
2581
- id,
2582
- isClearable,
2583
- isLoading,
2584
- isMulti,
2585
- isSearchable,
2586
- label,
2587
- maxMenuHeight,
2588
- menuIsOpen,
2589
- menuPlacement,
2590
- onChange,
2591
- onCreateOption,
2592
- onInputChange,
2593
- onMenuScrollToBottom,
2594
- openMenuOnClick,
2595
- openMenuOnFocus,
2596
- props.menuPortalTarget,
2597
- readOnly,
2598
- value,
2599
- ]);
2600
- const renderAsDisabled = Boolean(props.disabled) || props.readOnly;
2601
- 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] }));
2696
+ const isUniform = (schedule) => {
2697
+ return schedule.every((day, _, collection) => collection[0]?.range?.timeFrom === day.range?.timeFrom && collection[0]?.range?.timeTo === day.range?.timeTo);
2698
+ };
2699
+ /**
2700
+ * Checks if a list of schedule objects are consecutive days
2701
+ *
2702
+ * @param {RawSchedule[]} schedule List of schedule objects
2703
+ * @returns {boolean} Whether the schedule has consecutive days
2704
+ */
2705
+ const hasConsecutiveDays = (schedule) => {
2706
+ const days = [1, 2, 3, 4, 5];
2707
+ return schedule.every(({ day }, index) => day === days[index]);
2602
2708
  };
2603
- CreatableSelect.displayName = "CreatableSelect";
2604
2709
 
2605
- // This is here to ensure the bundled react-components can expose the react-select for jest in external iris apps.
2606
- const ReactSyncSelect = ReactSelect.default || ReactSelect;
2710
+ const cvaSearch = cvaMerge([
2711
+ "shadow-none",
2712
+ "component-search-border",
2713
+ "component-search-background",
2714
+ "hover:component-search-background",
2715
+ "hover:component-search-focus-hover",
2716
+ "transition-all",
2717
+ "duration-300",
2718
+ ], {
2719
+ variants: {
2720
+ border: { true: ["!component-search-borderless"], false: "" },
2721
+ widenOnFocus: {
2722
+ true: [
2723
+ "component-search-width",
2724
+ "component-search-widen",
2725
+ "hover:component-search-widen",
2726
+ "focus-within:w-full",
2727
+ "max-w-sm",
2728
+ ],
2729
+ false: "w-full",
2730
+ },
2731
+ },
2732
+ });
2733
+
2607
2734
  /**
2608
- * Selects are input components used to choose a value from a set.
2735
+ * The Search component is used to render a search input field.
2609
2736
  *
2610
- * @param {SelectProps} props - The props for the Select component
2611
- * @returns {ReactElement} Select component
2737
+ * @param {SearchProps} props - The props for the Search component
2612
2738
  */
2613
- const Select = (props) => {
2614
- const { id, 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;
2615
- const { refContainer, customStyles, menuIsOpen, customComponents, menuPlacement, openMenuHandler, closeMenuHandler } = useSelect(props);
2616
- const reactSelectProps = useMemo(() => ({
2617
- value,
2618
- menuPlacement,
2619
- maxMenuHeight,
2620
- onChange,
2621
- "aria-label": label,
2622
- "data-testid": dataTestId,
2623
- components: customComponents,
2624
- styles: customStyles,
2625
- tabSelectsValue: false,
2626
- blurInputOnSelect: false,
2627
- // This configuration allows for more flexible positioning control of the dropdown.
2628
- // Setting menuPortalTarget to 'null' specifies that the dropdown should be rendered within
2629
- // the parent element instead of 'document.body'.
2630
- menuPortalTarget: props.menuPortalTarget !== undefined ? props.menuPortalTarget : document.body,
2631
- isSearchable: disabled || readOnly ? false : isSearchable,
2632
- menuShouldBlockScroll: true,
2633
- menuShouldScrollIntoView: true,
2634
- openMenuOnFocus,
2635
- menuIsOpen: !readOnly ? menuIsOpen : false,
2636
- openMenuOnClick,
2637
- closeMenuOnSelect: !isMulti,
2638
- isMulti,
2639
- classNamePrefix,
2640
- isLoading,
2641
- isClearable,
2642
- id,
2643
- onMenuScrollToBottom,
2644
- onInputChange,
2645
- hideSelectedOptions,
2646
- isDisabled: Boolean(disabled),
2647
- }), [
2648
- classNamePrefix,
2649
- customComponents,
2650
- customStyles,
2651
- dataTestId,
2652
- disabled,
2653
- hideSelectedOptions,
2654
- id,
2655
- isClearable,
2656
- isLoading,
2657
- isMulti,
2658
- isSearchable,
2659
- label,
2660
- maxMenuHeight,
2661
- menuIsOpen,
2662
- menuPlacement,
2663
- onChange,
2664
- onInputChange,
2665
- onMenuScrollToBottom,
2666
- openMenuOnClick,
2667
- openMenuOnFocus,
2668
- props.menuPortalTarget,
2669
- readOnly,
2670
- value,
2671
- ]);
2672
- const renderAsDisabled = Boolean(props.disabled) || props.readOnly;
2673
- return (jsxs("div", { className: cvaSelect({
2674
- invalid: hasError,
2675
- fieldSize: fieldSize,
2676
- disabled: renderAsDisabled,
2677
- className: props.className,
2678
- }), "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] }));
2739
+ const Search = ({ className, placeholder, value, widenInputOnFocus, hideBorderWhenNotInFocus = false, disabled = false, onKeyUp, onChange, onFocus, onBlur, name, onClear, dataTestId, autoComplete = "on", loading = false, inputClassName, iconName = "MagnifyingGlass", style, xMarkRef, ref, ...rest }) => {
2740
+ const { t } = useTranslation();
2741
+ return (jsx(TextBaseInput, { ...rest, autoComplete: autoComplete, className: cvaSearch({ className, border: hideBorderWhenNotInFocus, widenOnFocus: widenInputOnFocus }), dataTestId: dataTestId, disabled: disabled, inputClassName: inputClassName, name: name, onBlur: onBlur, onChange: onChange, onFocus: onFocus, onKeyUp: onKeyUp, placeholder: placeholder ?? t("search.placeholder"), prefix: loading ? (jsx(Spinner, { centering: "centered", size: rest.fieldSize ?? undefined })) : (jsx(Icon, { name: iconName, size: rest.fieldSize ?? undefined })), ref: ref, suffix:
2742
+ //only show the clear button if there is a value and the onClear function is provided
2743
+ onClear && value ? (jsx("button", { className: "flex", "data-testid": dataTestId ? `${dataTestId}_suffix_component` : null, onClick: () => {
2744
+ onClear();
2745
+ }, ref: xMarkRef, type: "button", children: jsx(Icon, { name: "XMark", size: "small" }) })) : undefined, value: value }));
2679
2746
  };
2680
- Select.displayName = "Select";
2747
+ Search.displayName = "Search";
2681
2748
 
2682
2749
  /**
2683
2750
  *
@@ -2764,7 +2831,7 @@ CreatableSelectField.displayName = "CreatableSelectField";
2764
2831
  * @param {SelectFieldProps} props - The props for the SelectField component
2765
2832
  */
2766
2833
  const SelectField = ({ ref, ...props }) => {
2767
- return (jsx(FormFieldSelectAdapter, { ...props, ref: ref, children: convertedProps => jsx(Select, { ...convertedProps }) }));
2834
+ return (jsx(FormFieldSelectAdapter, { ...props, ref: ref, children: convertedProps => jsx(BaseSelect, { ...convertedProps }) }));
2768
2835
  };
2769
2836
  SelectField.displayName = "SelectField";
2770
2837
 
@@ -3177,4 +3244,4 @@ const useZodValidators = () => {
3177
3244
  */
3178
3245
  setupLibraryTranslations();
3179
3246
 
3180
- export { ActionButton, BaseInput, Checkbox, CheckboxField, ColorField, CreatableSelect, CreatableSelectField, DEFAULT_TIME, DateBaseInput, DateField, DropZone, DropZoneDefaultLabel, EMAIL_REGEX, EmailField, FormFieldSelectAdapter, FormGroup, Label, MultiSelectMenuItem, NumberBaseInput, NumberField, OptionCard, PasswordBaseInput, PasswordField, PhoneBaseInput, PhoneField, PhoneFieldWithController, RadioGroup, RadioItem, Schedule, ScheduleVariant, Search, Select, 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, cvaInputField, 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 };
3247
+ 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, cvaInputField, 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 };