@trackunit/react-form-components 1.8.64 → 1.8.66

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
 
@@ -107,10 +107,28 @@ const cvaInputBase = cvaMerge([
107
107
  "hover:bg-neutral-50",
108
108
  "transition",
109
109
  ]);
110
- const cvaInputBaseDisabled = cvaMerge(["bg-neutral-100", "hover:bg-neutral-100", "hover:border-neutral-300"]);
110
+ const cvaInputBaseDisabled = cvaMerge([
111
+ "bg-neutral-100",
112
+ "hover:bg-neutral-100",
113
+ "hover:border-neutral-300",
114
+ "text-neutral-400",
115
+ ]);
111
116
  const cvaInputBaseInvalid = cvaMerge(["border-danger-600"]);
117
+ const cvaInputBaseReadOnly = cvaMerge(["text-neutral-500"]);
118
+ const cvaInputBaseSize = cvaMerge("", {
119
+ variants: {
120
+ size: {
121
+ small: ["h-7", "text-xs"],
122
+ medium: ["h-8", "text-sm"],
123
+ large: ["h-10", "text-sm"],
124
+ },
125
+ defaultVariants: {
126
+ size: "medium",
127
+ },
128
+ },
129
+ });
112
130
  const cvaInput$1 = cvaMerge([
113
- "overflow-hidden",
131
+ "overflow-clip",
114
132
  "grid",
115
133
  // ! The layout of the grid is critical to the functioning of the cvaInputItemPlacementManager 👇
116
134
  // The min restriction of the middle column (--spacing-20) is
@@ -119,17 +137,22 @@ const cvaInput$1 = cvaMerge([
119
137
  "grid-rows-1",
120
138
  cvaInputBase(),
121
139
  "focus-within:outline-native",
140
+ "text-neutral-900",
122
141
  ], {
123
142
  variants: {
124
143
  size: {
125
- small: ["h-7"],
126
- medium: ["h-8"],
127
- large: ["h-10"],
144
+ small: cvaInputBaseSize({ size: "small" }),
145
+ medium: cvaInputBaseSize({ size: "medium" }),
146
+ large: cvaInputBaseSize({ size: "large" }),
128
147
  },
129
148
  disabled: {
130
149
  true: cvaInputBaseDisabled(),
131
150
  false: [""],
132
151
  },
152
+ readOnly: {
153
+ true: cvaInputBaseReadOnly(),
154
+ false: [""],
155
+ },
133
156
  invalid: {
134
157
  true: cvaInputBaseInvalid(),
135
158
  false: [""],
@@ -145,6 +168,11 @@ const cvaInput$1 = cvaMerge([
145
168
  isWarning: true,
146
169
  className: ["border-danger-600"], // Ensures that 'invalid' takes precedence
147
170
  },
171
+ {
172
+ disabled: true,
173
+ readOnly: true,
174
+ className: "text-neutral-400",
175
+ },
148
176
  ],
149
177
  defaultVariants: {
150
178
  size: "medium",
@@ -171,42 +199,19 @@ const cvaInputItemPlacementManager = cvaMerge([], {
171
199
  },
172
200
  });
173
201
  const cvaAccessoriesContainer = cvaMerge(["grid", "h-full", "w-min", "auto-cols-min", "grid-flow-col"]);
174
- const cvaInputField = cvaMerge([
175
- "w-full",
176
- "px-3",
177
- "border-0",
178
- "bg-transparent",
179
- "text-neutral-900",
180
- "placeholder-neutral-400",
181
- "text-sm",
182
- "truncate",
183
- ], {
202
+ /**
203
+ * Text size and color is handled by cvaInput
204
+ */
205
+ const cvaInputElement = cvaMerge(["w-full", "px-3", "border-0", "bg-transparent", "placeholder-neutral-400", "truncate"], {
184
206
  variants: {
185
207
  size: {
186
- small: ["py-0.5", "text-xs"],
208
+ small: ["py-0.5"],
187
209
  medium: ["py-1"],
188
210
  large: ["py-2"],
189
211
  },
190
- disabled: {
191
- true: ["text-neutral-400"],
192
- false: [""],
193
- },
194
- readOnly: {
195
- true: ["text-neutral-500"],
196
- false: [""],
197
- },
198
212
  },
199
- compoundVariants: [
200
- {
201
- disabled: true,
202
- readOnly: true,
203
- className: "text-neutral-400",
204
- },
205
- ],
206
213
  defaultVariants: {
207
214
  size: "medium",
208
- disabled: false,
209
- readOnly: false,
210
215
  },
211
216
  });
212
217
  const cvaInputAddon = cvaMerge([
@@ -370,7 +375,7 @@ const GenericActionsRenderer = ({ genericAction, disabled, fieldSize, innerRef,
370
375
  */
371
376
  const InputLockReasonTooltip = ({ reasons, kind }) => {
372
377
  const [t] = useTranslation();
373
- if (!reasons || reasons.length === 0) {
378
+ if (reasons === undefined || reasons.length === 0) {
374
379
  return (jsx(Tooltip, { label: t("field.notEditable.tooltip"), children: jsx(Icon, { name: kind === "disabled" ? "QuestionMarkCircle" : "LockClosed", size: "small" }) }));
375
380
  }
376
381
  return (jsx(Tooltip, { label: jsx("ul", { className: typeof reasons === "string" ? "list-none !pl-0" : "list-disc", children: typeof reasons === "string" ? jsx("li", { children: reasons }) : reasons.map(reason => jsx("li", { children: reason }, reason)) }), placement: "top", children: jsx(Icon, { name: "LockClosed", size: "small" }) }));
@@ -428,7 +433,7 @@ const SuffixRenderer = ({ suffix, isInvalid, isWarning, dataTestId, disabled, })
428
433
  * For specific input types make sure to use the corresponding input component.
429
434
  * This is a base used by our other input components such as TextBaseInput, NumberBaseInput, PasswordBaseInput, etc.
430
435
  */
431
- const BaseInput = ({ className, isInvalid, dataTestId, prefix, suffix, addonBefore, addonAfter, actions, fieldSize = "medium", nonInteractive = false, inputClassName, placeholder, isWarning, type, genericAction, style, ref, ...rest }) => {
436
+ const BaseInput = ({ className, isInvalid = false, dataTestId, prefix, suffix, addonBefore, addonAfter, actions, fieldSize = "medium", nonInteractive = false, inputClassName, placeholder, isWarning = false, type, genericAction, style, ref, required = false, ...rest }) => {
432
437
  // Derive final flags
433
438
  const renderAsDisabled = Boolean(rest.disabled);
434
439
  const renderAsReadonly = Boolean(rest.readOnly);
@@ -462,19 +467,18 @@ const BaseInput = ({ className, isInvalid, dataTestId, prefix, suffix, addonBefo
462
467
  useImperativeHandle(ref, () => innerRef.current, []);
463
468
  return (jsxs("div", { className: cvaInput$1({
464
469
  disabled: renderAsDisabled,
470
+ readOnly: renderAsReadonly,
465
471
  invalid: isInvalid,
466
472
  isWarning,
467
473
  size: fieldSize,
468
474
  className,
469
- }), "data-testid": dataTestId ? `${dataTestId}-container` : undefined, style: style, children: [jsxs("div", { className: cvaAccessoriesContainer({ className: cvaInputItemPlacementManager({ position: "before" }) }), "data-testid": dataTestId ? `${dataTestId}-before-container` : undefined, ref: beforeContainerRef, children: [jsx(AddonRenderer, { addon: addonBefore, dataTestId: dataTestId, fieldSize: fieldSize, position: "before" }), jsx(PrefixRenderer, { dataTestId: dataTestId, disabled: renderAsDisabled, prefix: prefix, type: type })] }), jsx("input", { "aria-required": rest.required, className: cvaInputField({
470
- readOnly: renderAsReadonly,
475
+ }), "data-testid": dataTestId ? `${dataTestId}-container` : undefined, style: style, children: [jsxs("div", { className: cvaAccessoriesContainer({ className: cvaInputItemPlacementManager({ position: "before" }) }), "data-testid": dataTestId ? `${dataTestId}-before-container` : undefined, ref: beforeContainerRef, children: [jsx(AddonRenderer, { addon: addonBefore, dataTestId: dataTestId, fieldSize: fieldSize, position: "before" }), jsx(PrefixRenderer, { dataTestId: dataTestId, disabled: renderAsDisabled, prefix: prefix, type: type })] }), jsx("input", { "aria-required": required, className: cvaInputElement({
471
476
  size: fieldSize,
472
- disabled: renderAsDisabled,
473
477
  className: cvaInputItemPlacementManager({ position: "span", className: inputClassName }),
474
- }), "data-testid": dataTestId, placeholder: renderAsDisabled ? undefined : placeholder, ref: innerRef, style: {
478
+ }), "data-testid": dataTestId, placeholder: renderAsDisabled ? undefined : placeholder, ref: innerRef, required: required, style: {
475
479
  paddingLeft: `calc(var(--before-width, 0px) + ${themeSpacing[2]})`,
476
480
  paddingRight: `calc(var(--after-width, 0px) + ${themeSpacing[2]})`,
477
- }, type: type, ...rest, disabled: renderAsDisabled, readOnly: renderAsReadonly || nonInteractive }), jsxs("div", { className: cvaAccessoriesContainer({ className: cvaInputItemPlacementManager({ position: "after" }) }), "data-testid": dataTestId ? `${dataTestId}-after-container` : undefined, ref: afterContainerRef, children: [jsx(LockReasonRenderer, { dataTestId: dataTestId + "-disabled", lockReason: rest.disabled }), jsx(LockReasonRenderer, { dataTestId: dataTestId + "-readonly", lockReason: rest.readOnly && !rest.disabled ? rest.readOnly : undefined }), jsx(GenericActionsRenderer, { fieldSize: fieldSize, genericAction: genericAction, innerRef: innerRef }), jsx(SuffixRenderer, { dataTestId: dataTestId, disabled: renderAsDisabled, isInvalid: isInvalid, isWarning: isWarning, suffix: suffix }), actions, jsx(AddonRenderer, { addon: addonAfter, dataTestId: dataTestId, fieldSize: fieldSize, position: "after" })] })] }));
481
+ }, type: type, ...rest, disabled: renderAsDisabled, readOnly: renderAsReadonly || nonInteractive }), jsxs("div", { className: cvaAccessoriesContainer({ className: cvaInputItemPlacementManager({ position: "after" }) }), "data-testid": dataTestId ? `${dataTestId}-after-container` : undefined, ref: afterContainerRef, children: [jsx(LockReasonRenderer, { dataTestId: dataTestId + "-disabled", lockReason: rest.disabled }), jsx(LockReasonRenderer, { dataTestId: dataTestId + "-readonly", lockReason: Boolean(rest.readOnly) && !Boolean(rest.disabled) ? rest.readOnly : undefined }), jsx(GenericActionsRenderer, { fieldSize: fieldSize, genericAction: genericAction, innerRef: innerRef }), jsx(SuffixRenderer, { dataTestId: dataTestId, disabled: renderAsDisabled, isInvalid: isInvalid, isWarning: isWarning, suffix: suffix }), actions, jsx(AddonRenderer, { addon: addonAfter, dataTestId: dataTestId, fieldSize: fieldSize, position: "after" })] })] }));
478
482
  };
479
483
  BaseInput.displayName = "BaseInput";
480
484
 
@@ -697,6 +701,103 @@ const TextAreaBaseInput = ({ id, name, value, rows, disabled, placeholder, readO
697
701
  */
698
702
  const TextBaseInput = ({ ref, ...rest }) => jsx(BaseInput, { ref: ref, type: "text", ...rest });
699
703
 
704
+ const cvaSelect = cvaMerge([
705
+ "relative",
706
+ "flex",
707
+ "shadow-sm",
708
+ "rounded-lg",
709
+ "border-neutral-300",
710
+ "hover:border-neutral-400",
711
+ "hover:bg-neutral-50",
712
+ "bg-white",
713
+ "transition",
714
+ "text-sm",
715
+ "min-h-0",
716
+ ], {
717
+ variants: {
718
+ fieldSize: {
719
+ small: ["h-7", "text-xs"],
720
+ medium: ["h-8"],
721
+ large: ["h-10"],
722
+ },
723
+ invalid: {
724
+ true: "border border-red-600 text-red-600 hover:border-red-600",
725
+ false: "",
726
+ },
727
+ disabled: {
728
+ true: "!bg-neutral-100 hover:border-neutral-300",
729
+ false: "",
730
+ },
731
+ },
732
+ defaultVariants: {
733
+ invalid: false,
734
+ disabled: false,
735
+ },
736
+ });
737
+ const cvaSelectControl = cvaMerge([], {
738
+ variants: {
739
+ isDisabled: {
740
+ true: "!bg-neutral-100",
741
+ false: "",
742
+ },
743
+ prefix: {
744
+ true: ["ps-7"],
745
+ false: "",
746
+ },
747
+ invalid: {
748
+ true: "!border-0",
749
+ false: "",
750
+ },
751
+ },
752
+ defaultVariants: {
753
+ isDisabled: false,
754
+ prefix: false,
755
+ invalid: false,
756
+ },
757
+ });
758
+ const cvaSelectIcon = cvaMerge([
759
+ "mr-2",
760
+ "flex",
761
+ "cursor-pointer",
762
+ "items-center",
763
+ "justify-center",
764
+ "text-neutral-400",
765
+ "hover:text-neutral-500",
766
+ ]);
767
+ const cvaSelectPrefixSuffix = cvaMerge(["flex", "justify-center", "items-center", "text-neutral-400", "absolute", "inset-y-0"], {
768
+ variants: {
769
+ kind: {
770
+ prefix: ["pl-3", "left-0"],
771
+ suffix: ["pr-3", "right-0"],
772
+ },
773
+ },
774
+ });
775
+ const cvaSelectXIcon = cvaMerge([
776
+ "mr-2",
777
+ "flex",
778
+ "cursor-pointer",
779
+ "items-center",
780
+ "justify-center",
781
+ "text-neutral-400",
782
+ "hover:text-neutral-500",
783
+ "ml-1",
784
+ ]);
785
+ const cvaSelectMenuList = cvaMerge([], {
786
+ variants: {
787
+ menuIsOpen: {
788
+ true: "animate-fade-in-fast",
789
+ false: "animate-fade-out-fast",
790
+ },
791
+ },
792
+ });
793
+ const cvaSelectDynamicTagContainer = cvaMerge(["h-full", "flex", "gap-1", "items-center"], {
794
+ variants: {
795
+ visible: { true: "visible", false: "invisible" },
796
+ },
797
+ });
798
+ const cvaSelectCounter = cvaMerge(["overflow-hidden", "whitespace-nowrap"]);
799
+ const cvaSelectMenu = cvaMerge(["relative", "p-1", "grid", "gap-1"]);
800
+
700
801
  /**
701
802
  * Shared CVA for binary control items: Checkbox, RadioItem, ToggleSwitchOption
702
803
  */
@@ -923,800 +1024,1438 @@ const Checkbox = ({ className, dataTestId = "checkbox", onChange, checked = fals
923
1024
  Checkbox.displayName = "Checkbox";
924
1025
 
925
1026
  /**
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.
1027
+ * A single select menu item is a basic wrapper around Menu item designed to be used as a single value render in Select list
928
1028
  *
929
- * @param {LabelProps} props - The props for the Label component
930
- * @returns {ReactElement} Label component
1029
+ * @param {SelectMenuItemProps} props - The props for the SingleSelectMenuItem
1030
+ * @returns {ReactElement} SingleSelectMenuItem
931
1031
  */
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 }));
1032
+ const SingleSelectMenuItem = ({ label, icon, onClick, selected, focused, dataTestId, disabled, optionLabelDescription, optionPrefix, fieldSize, }) => {
1033
+ return (jsx(MenuItem, { dataTestId: dataTestId, disabled: disabled, fieldSize: fieldSize, focused: focused, label: label, onClick: onClick, optionLabelDescription: optionLabelDescription, optionPrefix: isValidElement(optionPrefix)
1034
+ ? cloneElement(optionPrefix, {
1035
+ className: "mr-1 flex items-center",
1036
+ size: "medium",
1037
+ })
1038
+ : optionPrefix, prefix: icon, selected: selected, suffix: selected ? jsx(Icon, { className: "block text-blue-600", name: "Check", size: "medium" }) : undefined }));
934
1039
  };
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
1040
  /**
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.
1041
+ * A multi select menu item is a basic wrapper around Menu item designed to be used as a multi value render in Select list
962
1042
  *
963
- * @param {FormGroupProps} props - The props for the FormGroup component
964
- * @returns {ReactElement} FormGroup component
1043
+ * @param {SelectMenuItemProps} props - The props for the MultiSelectMenuItem
1044
+ * @returns {ReactElement} multi select menu item
965
1045
  */
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] }));
1046
+ const MultiSelectMenuItem = ({ label, onClick, selected, focused, dataTestId, disabled, optionLabelDescription, optionPrefix, fieldSize, }) => {
1047
+ return (jsx(MenuItem, { dataTestId: dataTestId, disabled: disabled, fieldSize: fieldSize, focused: focused, label: label, onClick: e => {
1048
+ e.stopPropagation();
1049
+ onClick && onClick(e);
1050
+ }, optionLabelDescription: optionLabelDescription, optionPrefix: isValidElement(optionPrefix)
1051
+ ? cloneElement(optionPrefix, {
1052
+ className: "mr-1 flex items-center",
1053
+ size: "medium",
1054
+ })
1055
+ : optionPrefix, prefix: jsx(Checkbox, { checked: selected, className: "gap-x-0", disabled: disabled, onChange: () => null, onClick: e => {
1056
+ e.stopPropagation();
1057
+ }, readOnly: false }), selected: selected }));
973
1058
  };
974
1059
 
975
1060
  /**
976
- * The checkbox field component is used for entering boolean values.
1061
+ * Extended Tag component with information about its own width.
1062
+ * Used in the select component.
977
1063
  *
978
- * _**Do use**_ the CheckboxField for boolean input.
1064
+ * @param {TagProps} props - The props for the tag component
1065
+ * @returns {ReactElement} TagWithWidth component
979
1066
  */
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 }) }));
1067
+ const TagWithWidth = ({ onWidthKnown, children, ...rest }) => {
1068
+ const ref = useRef(null);
1069
+ useLayoutEffect(() => {
1070
+ onWidthKnown && onWidthKnown({ width: ref.current?.offsetWidth || 0 });
1071
+ }, [ref, onWidthKnown]);
1072
+ return (jsx(Tag, { ref: ref, ...rest, icon: isValidElement(rest.icon) ? cloneElement(rest.icon, { size: "small" }) : rest.icon, children: children }));
983
1073
  };
984
- CheckboxField.displayName = "CheckboxField";
985
1074
 
986
1075
  /**
1076
+ * TagsContainer component to display tags in limited space when children can't fit space it displays counter
987
1077
  *
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
1078
+ * @param {TagsContainerProps} props - The props for the TagContainer
1079
+ * @returns {ReactElement} TagsContainer
1005
1080
  */
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";
1081
+ const TagsContainer = ({ items, width = "100%", itemsGap = 6, postFix, preFix, disabled, }) => {
1082
+ const containerRef = useRef(null);
1083
+ const [isReady, setIsReady] = useState(false);
1084
+ const [counterWidth, setCounterWidth] = useState(0);
1085
+ const [availableSpaceWidth, setAvailableSpaceWidth] = useState(0);
1086
+ const [childrenWidths, setChildrenWidths] = useState([]);
1087
+ const itemsCount = items.length;
1088
+ const dimensions = useResize();
1089
+ const { width: windowWidth } = useDebounce(dimensions, 100);
1090
+ useEffect(() => {
1091
+ const containerWidth = containerRef.current?.offsetWidth || 0;
1092
+ setAvailableSpaceWidth(containerWidth);
1093
+ }, [windowWidth]);
1094
+ const onWidthKnownHandler = useCallback(({ width: reportedWidth }) => {
1095
+ setChildrenWidths(prev => {
1096
+ const next = [...prev, { width: reportedWidth + itemsGap }];
1097
+ if (next.length === itemsCount) {
1098
+ setIsReady(true);
1099
+ }
1100
+ return next;
1101
+ });
1102
+ }, [itemsCount, itemsGap]);
1103
+ const renderedElements = useMemo(() => {
1104
+ const requiredSpace = childrenWidths.reduce((previous, current) => {
1105
+ return previous + current.width;
1106
+ }, 0);
1107
+ const availableSpace = availableSpaceWidth - counterWidth;
1108
+ const { elements } = items
1109
+ .concat({ text: "", onClick: () => null, disabled: false })
1110
+ .reduce((acc, item, index) => {
1111
+ const spaceNeeded = childrenWidths.slice(0, index + 1).reduce((previous, current) => {
1112
+ return previous + current.width;
1113
+ }, 0);
1114
+ const isLast = index === items.length;
1115
+ const counterRequired = requiredSpace > availableSpace && acc.counter !== 0;
1116
+ if (isLast && counterRequired) {
1117
+ return {
1118
+ ...acc,
1119
+ elements: [
1120
+ ...acc.elements,
1121
+ jsx(TagWithWidth, { color: "white", disabled: disabled, icon: item.Icon, onWidthKnown: ({ width: reportedWidth }) => setCounterWidth(reportedWidth), children: jsxs("div", { className: cvaSelectCounter(), "data-testid": "select-counter", children: ["+", acc.counter] }) }, item.text + index),
1122
+ ],
1123
+ };
1124
+ }
1125
+ if (isLast) {
1126
+ return acc;
1127
+ }
1128
+ const itemCanFit = spaceNeeded <= availableSpace;
1129
+ if (itemCanFit) {
1130
+ return {
1131
+ ...acc,
1132
+ elements: [
1133
+ ...acc.elements,
1134
+ jsx(TagWithWidth, { className: "inline-flex shrink-0", color: item.disabled ? "neutral" : "white", dataTestId: `${item.text}-tag`, disabled: disabled, icon: item.Icon, onClose: e => {
1135
+ e.stopPropagation();
1136
+ item.onClick();
1137
+ }, onWidthKnown: onWidthKnownHandler, children: item.text }, item.text + index),
1138
+ ],
1139
+ };
1140
+ }
1141
+ return {
1142
+ elements: acc.elements,
1143
+ counter: item.text !== "" ? acc.counter + 1 : acc.counter,
1144
+ };
1145
+ }, { elements: [], counter: 0 });
1146
+ return elements;
1147
+ }, [items, availableSpaceWidth, counterWidth, disabled, onWidthKnownHandler, childrenWidths]);
1148
+ return (jsxs("div", { className: cvaSelectDynamicTagContainer({ visible: isReady || !!preFix }), ref: containerRef, style: {
1149
+ width: `${width}`,
1150
+ }, children: [preFix, renderedElements, postFix] }));
1017
1151
  };
1018
1152
 
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
1153
  /**
1046
- * Validates if the given value is a valid hex color.
1154
+ * A hook to retrieve components override object.
1155
+ * 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.
1047
1156
  *
1048
- * @param value - The string value to be validated.
1049
- * @returns {boolean} True if the value is a valid hex color, otherwise false.
1157
+ * @template IsMulti
1158
+ * @template Group
1159
+ * @param {Partial<SelectComponents<Option, IsMulti, Group>> | undefined} componentsProps a custom component prop that you can to override defaults
1160
+ * @param {boolean} disabled decide to override disabled variant
1161
+ * @param {boolean} menuIsOpen menu is open state
1162
+ * @param {string} dataTestId a test id
1163
+ * @param {number} maxSelectedDisplayCount a number of max display count
1164
+ * @param {boolean} hasError decide to override hasError variant
1165
+ * @param {ReactNode} prefix a prefix element
1166
+ * @returns {Partial<SelectComponents<Option, boolean, GroupBase<Option>>> | undefined} components object to override react-select default components
1050
1167
  */
1051
- const isValidHEXColor = (value) => {
1052
- const hexRegex = /^#([0-9A-F]{6})$/i;
1053
- return hexRegex.test(value);
1168
+ const useCustomComponents = ({ componentsProps, disabled, readOnly, setMenuIsEnabled, dataTestId, maxSelectedDisplayCount, prefix, hasError, fieldSize, getOptionLabelDescription, getOptionPrefix, }) => {
1169
+ const [t] = useTranslation();
1170
+ // perhaps it should not be wrap in memo (causing some issues with opening and closing on mobiles)
1171
+ const customComponents = useMemo(() => {
1172
+ return {
1173
+ ValueContainer: props => {
1174
+ if (props.isMulti && Array.isArray(props.children) && props.children.length > 0) {
1175
+ const PLACEHOLDER_KEY = "placeholder";
1176
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1177
+ const key = props && props.children && props.children[0] ? props.children[0]?.key : "";
1178
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1179
+ const values = props && props.children ? props.children[0] : [];
1180
+ const tags = key === PLACEHOLDER_KEY ? [] : values;
1181
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1182
+ const searchInput = props && props.children && props.children[1];
1183
+ const placeholderElement = Array.isArray(props.children)
1184
+ ? props.children.find(child => child && child.key === PLACEHOLDER_KEY)
1185
+ : null;
1186
+ return (jsx(components.ValueContainer, { ...props, isDisabled: props.selectProps.isDisabled, children: maxSelectedDisplayCount === undefined ? (jsx(TagsContainer, { disabled: disabled, items: tags
1187
+ ? tags.map(({ props: tagProps }) => {
1188
+ const optionPrefix = tagProps.data && getOptionPrefix ? getOptionPrefix(tagProps.data) : null;
1189
+ return {
1190
+ text: tagProps.children,
1191
+ onClick: disabled
1192
+ ? undefined
1193
+ : (e) => {
1194
+ setMenuIsEnabled(false);
1195
+ tagProps.removeProps.onClick && tagProps.removeProps.onClick(e);
1196
+ },
1197
+ disabled: disabled,
1198
+ Icon: optionPrefix,
1199
+ };
1200
+ })
1201
+ : [], postFix: searchInput, preFix: placeholderElement ? jsx("span", { className: "absolute", children: placeholderElement }) : null, width: "100%" })) : (jsxs(Fragment, { children: [tags
1202
+ ? tags.slice(0, maxSelectedDisplayCount).map(({ props: tagProps }) => {
1203
+ return (jsx(Tag, { className: "inline-flex shrink-0", color: disabled ? "unknown" : "primary", dataTestId: tagProps.children ? `${tagProps.children.toString()}-tag` : undefined, onClose: e => {
1204
+ e.stopPropagation();
1205
+ setMenuIsEnabled(false);
1206
+ tagProps.removeProps.onClick && tagProps.removeProps.onClick(e);
1207
+ }, children: tagProps.children }, tagProps.children?.toString()));
1208
+ })
1209
+ : null, tags && tags.length > maxSelectedDisplayCount ? (jsxs(Tag, { color: "neutral", dataTestId: "counter-tag", children: ["+", tags.length - maxSelectedDisplayCount] })) : null, searchInput, placeholderElement] })) }));
1210
+ }
1211
+ return (jsx(Fragment, { children: jsx(components.ValueContainer, { ...props, isDisabled: props.selectProps.isDisabled, children: props.children }) }));
1212
+ },
1213
+ LoadingIndicator: () => {
1214
+ return jsx(Spinner, { centering: "vertically", className: "mr-2", size: "small" });
1215
+ },
1216
+ DropdownIndicator: props => {
1217
+ const icon = props.selectProps.menuIsOpen ? (jsx(Icon, { name: "ChevronUp", size: "medium" })) : (jsx(Icon, { name: "ChevronDown", size: "medium" }));
1218
+ return props.selectProps.isLoading || props.selectProps.isDisabled || readOnly ? null : (jsx(components.DropdownIndicator, { ...props, children: jsx("div", { className: cvaSelectIcon(), children: icon }) }));
1219
+ },
1220
+ IndicatorSeparator: () => null,
1221
+ ClearIndicator: props => {
1222
+ if (disabled) {
1223
+ return null;
1224
+ }
1225
+ return (jsx(components.ClearIndicator, { ...props, innerProps: {
1226
+ ...props.innerProps,
1227
+ onMouseDown: e => {
1228
+ e.preventDefault();
1229
+ },
1230
+ }, children: jsx("div", { className: cvaSelectXIcon(), "data-testid": dataTestId ? `${dataTestId}-XMarkIcon` : null, onClick: props.clearValue, children: jsx(Icon, { ariaLabel: t("clearIndicator.icon.tooltip.clearAll"), name: "XCircle", size: "medium" }) }) }));
1231
+ },
1232
+ Control: props => {
1233
+ return (jsx(components.Control, { ...props, className: cvaSelectControl({
1234
+ isDisabled: props.isDisabled,
1235
+ prefix: prefix ? true : false,
1236
+ invalid: hasError,
1237
+ }) }));
1238
+ },
1239
+ SingleValue: props => {
1240
+ const optionPrefix = getOptionPrefix ? getOptionPrefix(props.data) : null;
1241
+ return (jsx(components.SingleValue, { ...props, className: props.isDisabled ? "text-neutral-700" : "", children: jsxs("div", { className: "flex items-center gap-1", "data-testid": dataTestId + "-singleValue", children: [optionPrefix !== null ? optionPrefix : null, props.children, getOptionLabelDescription && getOptionLabelDescription(props.data) ? (jsxs("span", { className: "ml-1 text-neutral-400", children: ["(", getOptionLabelDescription(props.data), ")"] })) : null] }) }));
1242
+ },
1243
+ Menu: props => {
1244
+ return (jsx(components.Menu, { ...props, className: cvaSelectMenuList({ menuIsOpen: props.selectProps.menuIsOpen }) }));
1245
+ },
1246
+ Placeholder: props => {
1247
+ return (jsx(components.Placeholder, { ...props, className: "!text-neutral-400", children: props.children }));
1248
+ },
1249
+ MenuList: props => {
1250
+ return (jsx(components.MenuList, { ...props, innerProps: {
1251
+ ...props.innerProps,
1252
+ onScroll: e => {
1253
+ const listEl = e.currentTarget;
1254
+ if (listEl.scrollTop + listEl.clientHeight >= listEl.scrollHeight &&
1255
+ props.selectProps.onMenuScrollToBottom) {
1256
+ /Firefox/.test(navigator.userAgent)
1257
+ ? props.selectProps.onMenuScrollToBottom(new WheelEvent("scroll"))
1258
+ : props.selectProps.onMenuScrollToBottom(new TouchEvent(""));
1259
+ }
1260
+ },
1261
+ }, children: props.children }));
1262
+ },
1263
+ Option: props => {
1264
+ const componentProps = {
1265
+ label: props.label,
1266
+ focused: props.isFocused,
1267
+ selected: props.isSelected,
1268
+ onClick: props.innerProps.onClick,
1269
+ };
1270
+ return (jsx(components.Option, { ...props, innerProps: {
1271
+ ...props.innerProps,
1272
+ role: "option",
1273
+ onClick: () => { },
1274
+ }, 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) })) }));
1275
+ },
1276
+ ...componentsProps,
1277
+ };
1278
+ }, [
1279
+ componentsProps,
1280
+ maxSelectedDisplayCount,
1281
+ disabled,
1282
+ setMenuIsEnabled,
1283
+ readOnly,
1284
+ dataTestId,
1285
+ t,
1286
+ prefix,
1287
+ hasError,
1288
+ getOptionLabelDescription,
1289
+ fieldSize,
1290
+ getOptionPrefix,
1291
+ ]);
1292
+ return customComponents;
1054
1293
  };
1294
+
1055
1295
  /**
1056
- * The ColorField component is used to enter color.
1057
- * ColorField validates that user enters a valid color address.
1058
- *
1059
- */
1060
- const ColorField = forwardRef(({ label, id, tip, helpText, errorMessage, helpAddon, className, defaultValue, dataTestId, value: propValue, onChange, isInvalid = false, onBlur, fieldSize = "medium", ...rest }, ref) => {
1061
- const renderAsDisabled = Boolean(rest.disabled);
1062
- const renderAsReadonly = Boolean(rest.readOnly);
1063
- const htmlForId = useMemo(() => (id ? id : "colorField-" + uuidv4()), [id]);
1064
- const innerRef = useRef(null);
1065
- useImperativeHandle(ref, () => innerRef.current, []);
1066
- const [t] = useTranslation();
1067
- // Internal state for color value
1068
- const [innerValue, setInnerValue] = useState(propValue || defaultValue || "");
1069
- const [renderAsInvalid, setRenderAsInvalid] = useState(!!errorMessage || (innerValue && typeof innerValue === "string" && !isValidHEXColor(innerValue)) || isInvalid);
1070
- const errorType = useMemo(() => validateColorCode(innerValue, rest.required), [rest.required, innerValue]);
1071
- const error = useMemo(() => (errorType ? t(`colorField.error.${errorType}`) : errorMessage), [errorType, errorMessage, t]);
1072
- const handleInputChange = useCallback((event) => {
1073
- const newValue = event.target.value;
1074
- setInnerValue(newValue);
1075
- if (onChange) {
1076
- onChange(event);
1077
- }
1078
- }, [onChange]);
1079
- const handleBlur = useCallback(event => {
1080
- const newValue = event.target.value;
1081
- setInnerValue(newValue);
1082
- setRenderAsInvalid(!!errorType);
1083
- onBlur?.(event);
1084
- }, [errorType, onBlur]);
1085
- return (jsx(FormGroup, { dataTestId: dataTestId ? `${dataTestId}-FormGroup` : undefined, helpAddon: helpAddon, helpText: (renderAsInvalid && error) || helpText, htmlFor: htmlForId, isInvalid: renderAsInvalid, label: label, required: rest.required ? !(renderAsDisabled || renderAsReadonly) : false, tip: tip, children: jsxs("div", { className: cvaInput$1({
1086
- size: fieldSize,
1087
- disabled: renderAsDisabled,
1088
- invalid: renderAsInvalid,
1089
- className,
1090
- }), "data-testid": dataTestId ? `${dataTestId}-container` : undefined, children: [jsx("input", { "aria-labelledby": htmlForId + "-label", className: cvaInputColorField({ readOnly: renderAsReadonly }), "data-testid": dataTestId, defaultValue: defaultValue, disabled: renderAsDisabled, id: htmlForId, onBlur: handleBlur, onChange: handleInputChange, readOnly: renderAsReadonly, ref: innerRef, type: "color", value: innerValue }), jsx("input", { "aria-labelledby": htmlForId + "-label-text", className: cvaInputField({
1091
- readOnly: renderAsReadonly,
1092
- disabled: renderAsDisabled,
1093
- className: "px-1 focus-visible:outline-none",
1094
- }), "data-testid": dataTestId ? `${dataTestId}-textField` : undefined, disabled: renderAsDisabled, onBlur: handleBlur, onChange: handleInputChange, readOnly: renderAsReadonly, type: "text", value: innerValue }), jsx(GenericActionsRenderer, { disabled: renderAsDisabled || renderAsReadonly, fieldSize: fieldSize, genericAction: "edit", innerRef: innerRef, tooltipLabel: t("colorField.tooltip") })] }) }));
1095
- });
1096
- ColorField.displayName = "ColorField";
1097
-
1098
- /**
1099
- * The date field component is used for entering date values.
1100
- *
1101
- * _**Do use**_ the DateField for date input.
1102
- *
1103
- * _**Do not use**_ this fields for non-serialized dates. Use TextField instead.
1296
+ * @template IsMulti
1297
+ * @template Group
1298
+ * @param {RefObject<HTMLDivElement | null>} refContainer react ref to container element
1299
+ * @param {number | undefined} maxSelectedDisplayCount a number of max display count
1300
+ * @param {StylesConfig<Option, IsMulti, Group> | undefined} styles a optional object to override styles of react-select
1301
+ * @returns {StylesConfig<Option, boolean>} styles to override in select
1104
1302
  */
1105
- const DateField = ({ label, id, tip, helpText, errorMessage, helpAddon, isInvalid, className, defaultValue, dataTestId, ref, ...rest }) => {
1106
- const renderAsInvalid = isInvalid === undefined ? Boolean(errorMessage) : isInvalid;
1107
- const htmlForId = id ? id : "dateField-" + uuidv4();
1108
- return (jsx(FormGroup, { dataTestId: dataTestId ? `${dataTestId}-FormGroup` : undefined, helpAddon: helpAddon, helpText: (renderAsInvalid && errorMessage) || helpText, htmlFor: htmlForId, isInvalid: renderAsInvalid, label: label, required: rest.required ? !(rest.disabled || rest.readOnly) : false, tip: tip, children: jsx(DateBaseInput, { "aria-labelledby": htmlForId + "-label", defaultValue: defaultValue, id: htmlForId, isInvalid: renderAsInvalid, ref: ref, ...rest, className: className, dataTestId: dataTestId }) }));
1303
+ const useCustomStyles = ({ refContainer, maxSelectedDisplayCount, styles, disabled, fieldSize, }) => {
1304
+ const customStyles = useMemo(() => {
1305
+ return {
1306
+ control: base => {
1307
+ return {
1308
+ ...base,
1309
+ minHeight: fieldSize === "small" ? "28px" : fieldSize === "large" ? "40px" : "32px",
1310
+ borderRadius: "var(--border-radius-lg)",
1311
+ backgroundColor: "inherit",
1312
+ };
1313
+ },
1314
+ singleValue: base => ({
1315
+ ...base,
1316
+ }),
1317
+ multiValue: base => ({
1318
+ ...base,
1319
+ }),
1320
+ multiValueLabel: base => ({
1321
+ ...base,
1322
+ }),
1323
+ indicatorsContainer: base => ({
1324
+ ...base,
1325
+ ...(disabled && { display: "none" }),
1326
+ }),
1327
+ indicatorSeparator: () => ({
1328
+ width: "0px",
1329
+ }),
1330
+ menu: base => {
1331
+ return {
1332
+ ...base,
1333
+ width: "100%",
1334
+ marginTop: "4px",
1335
+ marginBottom: "18px",
1336
+ transition: "all 1s ease-in-out",
1337
+ };
1338
+ },
1339
+ input: base => ({
1340
+ ...base,
1341
+ marginLeft: "0px",
1342
+ }),
1343
+ placeholder: base => ({
1344
+ ...base,
1345
+ }),
1346
+ option: () => ({}),
1347
+ menuPortal: base => ({
1348
+ ...base,
1349
+ width: refContainer.current ? `${refContainer.current.clientWidth}px` : base.width,
1350
+ backgroundColor: "#ffffff",
1351
+ borderRadius: "var(--border-radius-lg)",
1352
+ zIndex: "var(--z-overlay)",
1353
+ borderColor: "rgb(var(--color-neutral-300))",
1354
+ boxShadow: "var(--tw-ring-inset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)",
1355
+ }),
1356
+ menuList: base => {
1357
+ return {
1358
+ ...base,
1359
+ position: "relative",
1360
+ padding: "var(--spacing-1)",
1361
+ display: "grid",
1362
+ gap: "var(--spacing-1)",
1363
+ width: "100%",
1364
+ borderRadius: "0px",
1365
+ boxShadow: "none",
1366
+ paddingTop: "0px",
1367
+ };
1368
+ },
1369
+ valueContainer: base => {
1370
+ return {
1371
+ ...base,
1372
+ paddingBlock: 0,
1373
+ flexWrap: maxSelectedDisplayCount !== undefined ? "wrap" : "nowrap",
1374
+ gap: "0.25rem",
1375
+ };
1376
+ },
1377
+ container: base => ({
1378
+ ...base,
1379
+ width: "100%",
1380
+ }),
1381
+ dropdownIndicator: base => ({
1382
+ ...base,
1383
+ padding: "0px",
1384
+ }),
1385
+ clearIndicator: base => {
1386
+ return {
1387
+ ...base,
1388
+ padding: "0px",
1389
+ };
1390
+ },
1391
+ ...styles,
1392
+ };
1393
+ }, [refContainer, disabled, fieldSize, maxSelectedDisplayCount, styles]);
1394
+ return { customStyles };
1109
1395
  };
1110
- DateField.displayName = "DateField";
1111
-
1112
- const cvaDropZone = cvaMerge([
1113
- "flex",
1114
- "component-baseInput-background",
1115
- "justify-center",
1116
- "text-neutral-500",
1117
- "rounded-lg",
1118
- "border-2",
1119
- "border-neutral-200",
1120
- "border-dashed",
1121
- "hover:bg-neutral-100",
1122
- "hover:border-solid",
1123
- "hover:border-primary-500",
1124
- ], {
1125
- variants: {
1126
- size: {
1127
- small: ["p-2"],
1128
- medium: ["p-4"],
1129
- large: ["p-8"],
1130
- },
1131
- disabled: { true: ["bg-neutral-100", "hover:bg-neutral-100"], false: "" },
1132
- dragActive: { true: ["border-neutral-200", "bg-neutral-100"], false: "" },
1133
- dropComplete: {
1134
- true: [
1135
- "border-solid",
1136
- "border-primary-500",
1137
- "bg-neutral-100",
1138
- "hover:border-solid",
1139
- "hover:border-neutral-200",
1140
- ],
1141
- false: "",
1142
- },
1143
- invalid: { true: ["border-danger-600", "text-danger-500"], false: "" },
1144
- },
1145
- });
1146
- const cvaDropZoneLabel = cvaMerge([
1147
- "h-full",
1148
- "pt-1",
1149
- "pb-1",
1150
- "gap-2",
1151
- "items-center",
1152
- "flex-col",
1153
- "flex",
1154
- "justify-center",
1155
- ]);
1156
- const cvaDropZoneIconBackground = cvaMerge(["relative", "flex", "items-center", "justify-center", "rounded-full", "p-3"], {
1157
- variants: {
1158
- invalid: { true: ["bg-red-100"], false: ["bg-neutral-200"] },
1159
- },
1160
- });
1161
1396
 
1162
1397
  /**
1398
+ * A hook used by selects to share the common code
1163
1399
  *
1164
- * Default UX-intuitive label for the DropZone - can be overwritten by the label prop
1400
+ * @param {SelectProps} props - The props for the Select component
1401
+ * @returns {UseSelectProps} Select component
1165
1402
  */
1166
- const DropZoneDefaultLabel = () => (jsx(Trans, { components: {
1167
- clickable: jsx("span", { className: "text-primary-600 hover:text-primary-700 cursor-pointer underline" }),
1168
- }, i18nKey: "dropzone.label.default", values: {} }));
1403
+ 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 }) => {
1404
+ const refContainer = useRef(document.createElement("div"));
1405
+ const { customStyles } = useCustomStyles({
1406
+ refContainer,
1407
+ maxSelectedDisplayCount,
1408
+ styles,
1409
+ disabled: Boolean(disabled),
1410
+ fieldSize,
1411
+ });
1412
+ const [menuIsOpen, setMenuIsOpen] = useState(props.menuIsOpen ?? false);
1413
+ const [menuIsEnabled, setMenuIsEnabled] = useState(true);
1414
+ const customComponents = useCustomComponents({
1415
+ componentsProps: components,
1416
+ disabled: Boolean(disabled),
1417
+ readOnly: Boolean(props.readOnly),
1418
+ setMenuIsEnabled,
1419
+ dataTestId,
1420
+ maxSelectedDisplayCount,
1421
+ prefix,
1422
+ hasError,
1423
+ fieldSize,
1424
+ getOptionLabelDescription,
1425
+ getOptionPrefix,
1426
+ });
1427
+ const menuPlacement = "auto";
1428
+ const openMenuHandler = async () => {
1429
+ onMenuOpen?.();
1430
+ if (menuIsEnabled) {
1431
+ setMenuIsOpen(true);
1432
+ }
1433
+ else {
1434
+ setMenuIsEnabled(true);
1435
+ }
1436
+ };
1437
+ const closeMenuHandler = () => {
1438
+ setMenuIsOpen(false);
1439
+ onMenuClose && onMenuClose();
1440
+ };
1441
+ return {
1442
+ refContainer,
1443
+ customStyles,
1444
+ menuIsOpen,
1445
+ customComponents,
1446
+ menuPlacement,
1447
+ openMenuHandler,
1448
+ closeMenuHandler,
1449
+ };
1450
+ };
1169
1451
 
1452
+ // This is here to ensure the bundled react-components can expose the react-select for jest in external iris apps.
1453
+ const ReactSyncSelect = ReactSelect.default || ReactSelect;
1170
1454
  /**
1171
- * The Drop Zone can be used to drag and drop files or to browse and select files from the file system.
1455
+ * Selects are input components used to choose a value from a set.
1172
1456
  *
1173
- * @param {DropZoneProps} props - The props for the DropZone component
1174
- * @returns {ReactElement} DropZone component
1457
+ * @param {SelectProps} props - The props for the Select component
1458
+ * @returns {ReactElement} Select component
1175
1459
  */
1176
- const DropZone = ({ className, dataTestId, filesSelected, label = jsx(DropZoneDefaultLabel, {}), size = "large", isInvalid = false, disabled = false, accept, multiple = false, ...rest }) => {
1177
- const [dragActive, setDragActive] = useState(false);
1178
- const [fileDropped, setFileDropped] = useState(false);
1179
- const [t] = useTranslation();
1180
- const inputLabelRef = useRef(null);
1181
- // function that handles drag enter, drag leave and drag over
1182
- const handleDrag = (e) => {
1183
- e.preventDefault();
1184
- e.stopPropagation();
1185
- if ((e.type === "dragenter" || e.type === "dragover") && !disabled) {
1186
- setDragActive(true);
1187
- }
1188
- else if (e.type === "dragleave") {
1189
- setDragActive(false);
1190
- }
1191
- };
1192
- //function to handle when user clicks on dropzone to upload
1193
- const handleChange = (e) => {
1194
- e.preventDefault();
1195
- e.stopPropagation();
1196
- if (e.target.files && !disabled) {
1197
- filesSelected(e.target.files);
1198
- }
1199
- };
1200
- //function to handle drop
1201
- const handleDrop = (e) => {
1202
- e.preventDefault();
1203
- e.stopPropagation();
1204
- setDragActive(false);
1205
- if (e.dataTransfer.files[0] && !disabled) {
1206
- filesSelected(e.dataTransfer.files);
1207
- setFileDropped(true);
1208
- }
1209
- };
1210
- //function to handle focusable button click (for accessibility)
1211
- const handleButtonClick = (e) => {
1212
- e.preventDefault();
1213
- e.stopPropagation();
1214
- if (disabled) {
1215
- return;
1216
- }
1217
- inputLabelRef.current?.click();
1218
- };
1219
- return (jsx("div", { className: cvaDropZone({ size, dropComplete: fileDropped, dragActive, disabled, invalid: isInvalid, className }), "data-testid": dataTestId, onClick: e => {
1220
- if (disabled) {
1221
- e.preventDefault();
1222
- e.stopPropagation();
1223
- }
1224
- }, onDragEnter: handleDrag, onDragLeave: handleDrag, onDragOver: handleDrag, onDrop: handleDrop, ...rest, children: jsxs("label", { className: cvaDropZoneLabel(), "data-testid": dataTestId ? `${dataTestId}-label` : null, ref: inputLabelRef, children: [jsx("input", { accept: accept, className: "hidden", multiple: multiple, onChange: handleChange, title: t("dropzone.input.title"), type: "file" }), jsx("div", { className: cvaDropZoneIconBackground({ invalid: isInvalid }), children: jsx(Icon, { className: !isInvalid ? "text-neutral-400" : "", color: isInvalid ? "danger" : "neutral", name: "ArrowUpCircle", type: "solid" }) }), jsx("button", { disabled: disabled, onClick: handleButtonClick, children: label })] }) }));
1460
+ const BaseSelect = (props) => {
1461
+ 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;
1462
+ const { refContainer, customStyles, menuIsOpen, customComponents, menuPlacement, openMenuHandler, closeMenuHandler } = useSelect(props);
1463
+ const reactSelectProps = useMemo(() => ({
1464
+ value,
1465
+ menuPlacement,
1466
+ maxMenuHeight,
1467
+ onChange,
1468
+ "aria-label": label,
1469
+ "data-testid": dataTestId,
1470
+ components: customComponents,
1471
+ styles: customStyles,
1472
+ tabSelectsValue: false,
1473
+ blurInputOnSelect: false,
1474
+ // This configuration allows for more flexible positioning control of the dropdown.
1475
+ // Setting menuPortalTarget to 'null' specifies that the dropdown should be rendered within
1476
+ // the parent element instead of 'document.body'.
1477
+ menuPortalTarget: props.menuPortalTarget !== undefined ? props.menuPortalTarget : document.body,
1478
+ isSearchable: disabled || readOnly ? false : isSearchable,
1479
+ menuShouldBlockScroll: true,
1480
+ menuShouldScrollIntoView: true,
1481
+ openMenuOnFocus,
1482
+ menuIsOpen: !readOnly ? menuIsOpen : false,
1483
+ openMenuOnClick,
1484
+ closeMenuOnSelect: !isMulti,
1485
+ isMulti,
1486
+ classNamePrefix,
1487
+ isLoading,
1488
+ isClearable,
1489
+ id,
1490
+ onMenuScrollToBottom,
1491
+ onInputChange,
1492
+ hideSelectedOptions,
1493
+ isDisabled: Boolean(disabled),
1494
+ }), [
1495
+ classNamePrefix,
1496
+ customComponents,
1497
+ customStyles,
1498
+ dataTestId,
1499
+ disabled,
1500
+ hideSelectedOptions,
1501
+ id,
1502
+ isClearable,
1503
+ isLoading,
1504
+ isMulti,
1505
+ isSearchable,
1506
+ label,
1507
+ maxMenuHeight,
1508
+ menuIsOpen,
1509
+ menuPlacement,
1510
+ onChange,
1511
+ onInputChange,
1512
+ onMenuScrollToBottom,
1513
+ openMenuOnClick,
1514
+ openMenuOnFocus,
1515
+ props.menuPortalTarget,
1516
+ readOnly,
1517
+ value,
1518
+ ]);
1519
+ const renderAsDisabled = Boolean(props.disabled) || props.readOnly;
1520
+ return (jsxs("div", { className: cvaSelect({
1521
+ invalid: hasError,
1522
+ fieldSize: fieldSize,
1523
+ disabled: renderAsDisabled,
1524
+ className: props.className,
1525
+ }), "data-testid": dataTestId, ref: refContainer, children: [prefix !== undefined ? (jsx("div", { className: cvaSelectPrefixSuffix({ kind: "prefix" }), "data-testid": dataTestId ? `${dataTestId}-prefix` : null, children: prefix })) : null, async ? (jsx(ReactAsyncSelect, { ...props, ...reactSelectProps, ...async, menuPosition: menuPosition, onMenuClose: closeMenuHandler, onMenuOpen: openMenuHandler, placeholder: renderAsDisabled ? null : props.placeholder })) : (jsx(ReactSyncSelect, { ...props, ...reactSelectProps, isMulti: isMulti, menuPosition: menuPosition, onMenuClose: closeMenuHandler, onMenuOpen: openMenuHandler, options: options, placeholder: renderAsDisabled ? null : props.placeholder })), typeof props.disabled === "object" ? (jsx("div", { className: cvaSelectPrefixSuffix({ kind: "suffix" }), "data-testid": dataTestId ? `${dataTestId}-locked` : null, children: jsx(InputLockReasonTooltip, { ...props.disabled }) })) : null] }));
1225
1526
  };
1527
+ BaseSelect.displayName = "BaseSelect";
1226
1528
 
1227
- // Doing the same check as we do on the backend
1228
- // Using OWASP pattern from: https://owasp.org/www-community/OWASP_Validation_Regex_Repository
1229
- const EMAIL_REGEX = /^[a-zA-Z0-9_+&*-]+(?:\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,7}$/;
1230
1529
  /**
1231
- * @description Validate given email id.
1232
- * @param email The address to validate.
1233
- * @returns {boolean} Returns true if the email address is valid else false.
1234
- * @example validateEmailAddress(test@gmail.com) // true
1530
+ * CreatableSelects are input components used to choose a value from a set.
1531
+ *
1532
+ * @param {CreatableSelectProps} props - The props for the CreatableSelect component
1533
+ * @returns {ReactElement} CreatableSelect component
1235
1534
  */
1236
- const validateEmailAddress = (email) => {
1237
- if (!email) {
1238
- return false;
1239
- }
1240
- return EMAIL_REGEX.test(email);
1535
+ const CreatableSelect = (props) => {
1536
+ 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;
1537
+ const { refContainer, customStyles, menuIsOpen, customComponents, menuPlacement, openMenuHandler, closeMenuHandler } = useSelect(props);
1538
+ const reactCreatableSelectProps = useMemo(() => ({
1539
+ value,
1540
+ menuPlacement,
1541
+ maxMenuHeight,
1542
+ onChange,
1543
+ "aria-label": label,
1544
+ "data-testid": dataTestId,
1545
+ components: customComponents,
1546
+ styles: customStyles,
1547
+ tabSelectsValue: false,
1548
+ blurInputOnSelect: !isMulti,
1549
+ menuPortalTarget: props.menuPortalTarget || document.body,
1550
+ isSearchable: disabled || readOnly ? false : isSearchable,
1551
+ menuShouldBlockScroll: true,
1552
+ menuShouldScrollIntoView: true,
1553
+ openMenuOnFocus,
1554
+ menuIsOpen: !readOnly ? menuIsOpen : false,
1555
+ openMenuOnClick,
1556
+ closeMenuOnSelect: false,
1557
+ isMulti,
1558
+ classNamePrefix,
1559
+ isLoading,
1560
+ isClearable,
1561
+ id,
1562
+ onMenuScrollToBottom,
1563
+ onInputChange,
1564
+ allowCreateWhileLoading,
1565
+ onCreateOption,
1566
+ isDisabled: Boolean(disabled),
1567
+ }), [
1568
+ allowCreateWhileLoading,
1569
+ classNamePrefix,
1570
+ customComponents,
1571
+ customStyles,
1572
+ dataTestId,
1573
+ disabled,
1574
+ id,
1575
+ isClearable,
1576
+ isLoading,
1577
+ isMulti,
1578
+ isSearchable,
1579
+ label,
1580
+ maxMenuHeight,
1581
+ menuIsOpen,
1582
+ menuPlacement,
1583
+ onChange,
1584
+ onCreateOption,
1585
+ onInputChange,
1586
+ onMenuScrollToBottom,
1587
+ openMenuOnClick,
1588
+ openMenuOnFocus,
1589
+ props.menuPortalTarget,
1590
+ readOnly,
1591
+ value,
1592
+ ]);
1593
+ const renderAsDisabled = Boolean(props.disabled) || props.readOnly;
1594
+ return (jsxs("div", { className: cvaSelect({ invalid: hasError, disabled: renderAsDisabled, className: props.className }), "data-testid": dataTestId, ref: refContainer, children: [prefix !== undefined ? (jsx("div", { className: cvaSelectPrefixSuffix({ kind: "prefix" }), "data-testid": dataTestId ? `${dataTestId}-prefix` : null, children: prefix })) : null, async ? (jsx(ReactAsyncCreatableSelect, { ...props, ...reactCreatableSelectProps, ...async, onMenuClose: closeMenuHandler, onMenuOpen: openMenuHandler, placeholder: renderAsDisabled ? null : props.placeholder })) : (jsx(ReactCreatableSelect, { ...props, ...reactCreatableSelectProps, hideSelectedOptions: false, isMulti: isMulti, onMenuClose: closeMenuHandler, onMenuOpen: openMenuHandler, options: options, placeholder: renderAsDisabled ? null : props.placeholder })), typeof props.disabled === "object" ? (jsx("div", { className: cvaSelectPrefixSuffix({ kind: "suffix" }), "data-testid": dataTestId ? `${dataTestId}-locked` : null, children: jsx(InputLockReasonTooltip, { ...props.disabled }) })) : null] }));
1241
1595
  };
1596
+ CreatableSelect.displayName = "CreatableSelect";
1242
1597
 
1243
1598
  /**
1244
- * Validates a email id
1599
+ * The Label component is used for labels for input fields.
1600
+ * This component is **not used directly**, but is part of the FormGroup and Field components.
1601
+ *
1602
+ * @param {LabelProps} props - The props for the Label component
1603
+ * @returns {ReactElement} Label component
1245
1604
  */
1246
- const validateEmailId = (emailId, required) => {
1247
- if (!emailId && !required) {
1248
- return undefined;
1249
- }
1250
- if (!emailId && required) {
1251
- return "REQUIRED";
1252
- }
1253
- if (emailId && isString(emailId) && validateEmailAddress(emailId)) {
1254
- return undefined;
1255
- }
1256
- return "INVALID_EMAIL";
1605
+ const Label = ({ id, htmlFor, children, className, dataTestId, disabled, isInvalid, }) => {
1606
+ return (jsx("label", { className: cvaLabel({ invalid: isInvalid, disabled, className }), "data-testid": dataTestId, htmlFor: htmlFor || "", id: id || "", children: children }));
1257
1607
  };
1258
1608
 
1609
+ const cvaFormGroup = cvaMerge(["component-formGroup-gap", "group", "form-group"]);
1610
+ const cvaFormGroupContainerBefore = cvaMerge(["flex", "mb-1", "items-center"]);
1611
+ const cvaFormGroupContainerAfter = cvaMerge(["flex", "justify-between", "mt-1", "text-xs", "text-neutral-500"], {
1612
+ variants: {
1613
+ invalid: {
1614
+ true: "text-danger-500",
1615
+ false: "",
1616
+ },
1617
+ isWarning: {
1618
+ true: "text-default-500 ",
1619
+ false: "",
1620
+ },
1621
+ },
1622
+ compoundVariants: [
1623
+ {
1624
+ invalid: true,
1625
+ isWarning: true,
1626
+ className: "text-danger-500 ", // Ensures that 'invalid' takes precedence
1627
+ },
1628
+ ],
1629
+ });
1630
+ const cvaHelpAddon = cvaMerge(["ml-auto"]);
1631
+
1259
1632
  /**
1260
- * A Email Input component is used for input of the type Email.
1261
- *
1262
- * It has an ActionButton which sends an email to the specified address using "mailto:" link.
1633
+ * The FormGroup component should be used to wrap any Input element that needs a label.
1634
+ * Besides a label the component supplies an optional Tooltip, HelpText and HelpAddon support.
1263
1635
  *
1264
- * Extends props of BaseInput.
1636
+ * @param {FormGroupProps} props - The props for the FormGroup component
1637
+ * @returns {ReactElement} FormGroup component
1638
+ */
1639
+ const FormGroup = ({ isInvalid, isWarning, helpText, helpAddon, tip, className, dataTestId, label, htmlFor, children, required = false, }) => {
1640
+ const [t] = useTranslation();
1641
+ const validationStateIcon = useMemo(() => {
1642
+ const color = isInvalid ? "danger" : isWarning ? "warning" : null;
1643
+ return color ? jsx(Icon, { color: color, name: "ExclamationTriangle", size: "small" }) : null;
1644
+ }, [isInvalid, isWarning]);
1645
+ 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] }));
1646
+ };
1647
+
1648
+ /**
1649
+ * The checkbox field component is used for entering boolean values.
1265
1650
  *
1266
- * A reference to the input element is provided as the `ref` prop.
1267
- * For specific input types make sure to use the corresponding input component.
1651
+ * _**Do use**_ the CheckboxField for boolean input.
1268
1652
  */
1269
- const EmailBaseInput = ({ fieldSize = "medium", disabled = false, dataTestId, isInvalid = false, onChange, disableAction = false, ref, ...rest }) => {
1270
- const [email, setEmail] = useState(rest.value?.toString() || rest.defaultValue?.toString());
1271
- const sendEmail = () => {
1272
- return window.open(`mailto:${email}`);
1273
- };
1274
- const handleChange = useCallback(event => {
1275
- const newValue = event.target.value;
1276
- onChange?.(event);
1277
- setEmail(newValue);
1278
- }, [onChange]);
1279
- const renderAsInvalid = (email && !validateEmailAddress(email)) || isInvalid;
1280
- return (jsx(BaseInput, { actions: email && email.length > 0 ? (jsx(ActionButton, { dataTestId: dataTestId ? `${dataTestId}-emailIcon` : undefined, disabled: disableAction || isInvalid, onClick: sendEmail, size: fieldSize ?? undefined, type: "EMAIL", value: email })) : null, dataTestId: dataTestId, disabled: disabled, fieldSize: fieldSize, isInvalid: renderAsInvalid, onChange: handleChange, placeholder: rest.placeholder || "mail@example.com", ref: ref, type: "email", ...rest }));
1653
+ const CheckboxField = ({ label, id, tip, helpText, helpAddon, isInvalid, className, checked, dataTestId, checkboxLabel, onChange, ref, ...rest }) => {
1654
+ const htmlForId = id ? id : "checkboxField-" + uuidv4();
1655
+ 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 }) }));
1281
1656
  };
1657
+ CheckboxField.displayName = "CheckboxField";
1282
1658
 
1283
1659
  /**
1284
- * The EmailField component is used to enter email.
1285
- * EmailField validates that user enters a valid email address.
1286
1660
  *
1661
+ * @param inputValue - value to check if it is a string
1662
+ * @returns {boolean} - true if value is a string
1287
1663
  */
1288
- const EmailField = ({ label, id, tip, helpText, errorMessage, helpAddon, className, defaultValue, dataTestId, value, onChange, onBlur, isInvalid = false, ref, ...rest }) => {
1289
- const htmlForId = id ? id : "emailField-" + uuidv4();
1290
- const [t] = useTranslation();
1291
- const [innerValue, setInnerValue] = useState(() => {
1292
- return (value?.toString() || defaultValue?.toString()) ?? "";
1293
- });
1294
- const [renderAsInvalid, setRenderAsInvalid] = useState(!!errorMessage || (value && isString(value) && !validateEmailAddress(value)) || isInvalid);
1295
- const errorType = useMemo(() => validateEmailId(innerValue ?? "", rest.required), [rest.required, innerValue]);
1296
- const error = useMemo(() => (errorType ? t(`emailField.error.${errorType}`) : errorMessage), [errorType, errorMessage, t]);
1297
- const handleBlur = useCallback(event => {
1298
- const newValue = event.target.value;
1299
- setInnerValue(newValue);
1300
- setRenderAsInvalid(!!errorType);
1301
- onBlur?.(event);
1302
- }, [errorType, onBlur]);
1303
- const handleChange = useCallback((event) => {
1304
- setInnerValue(event.target.value);
1305
- if (onChange) {
1306
- onChange(event);
1307
- }
1308
- }, [onChange]);
1309
- 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(EmailBaseInput, { "aria-labelledby": htmlForId + "-label", defaultValue: defaultValue, id: htmlForId, isInvalid: renderAsInvalid, onBlur: handleBlur, onChange: handleChange, ref: ref, value: value, ...rest, className: className, dataTestId: dataTestId }) }));
1664
+ const isString = (inputValue) => {
1665
+ return typeof inputValue === "string";
1310
1666
  };
1311
- EmailField.displayName = "EmailField";
1312
-
1313
- const isNumberValid = (number) => {
1314
- if (!isNaN(+number) === false) {
1315
- return false;
1316
- }
1317
- if (typeof number === "number") {
1318
- return true;
1319
- }
1320
- return /^(?=.)([+-]?([0-9]*)(\.([0-9]+))?)$/.test(number);
1667
+ /**
1668
+ *
1669
+ * @param inputValue - value to check if it is a number
1670
+ * @returns {boolean} - true if value is a number
1671
+ */
1672
+ const isNumber = (inputValue) => {
1673
+ return typeof inputValue === "number";
1321
1674
  };
1675
+
1322
1676
  /**
1323
- * Validates a number
1677
+ * Validates a url
1324
1678
  */
1325
- const validateNumber = (number, required = false, min, max) => {
1326
- const parsedNumber = Number(number);
1327
- const minValue = typeof min === "string" ? parseFloat(min) : min;
1328
- const maxValue = typeof max === "string" ? parseFloat(max) : max;
1329
- if (number === undefined) {
1330
- return undefined;
1331
- }
1332
- // if the value is a string eg:'test'
1333
- if (number && !isNaN(+number) === false) {
1334
- return "INVALID_NUMBER";
1335
- }
1336
- // if the value is empty and not required
1337
- if (!parsedNumber && !required && !min && !max && !!number) {
1679
+ const validateColorCode = (colorCode, required) => {
1680
+ if (!colorCode && !required) {
1338
1681
  return undefined;
1339
1682
  }
1340
- // if the value is empty and required
1341
- if (required && !!number === false) {
1683
+ if (!colorCode && required) {
1342
1684
  return "REQUIRED";
1343
1685
  }
1344
- // if the value is not in between min and max
1345
- if (minValue && maxValue && isNumberValid(parsedNumber) && !(parsedNumber >= minValue && parsedNumber <= maxValue)) {
1346
- return "NOT_IN_BETWEEN";
1347
- }
1348
- // if the value is less than min
1349
- if (isNumberValid(parsedNumber) && minValue !== undefined && parsedNumber < minValue) {
1350
- return "GREATER_THAN";
1351
- }
1352
- // if the value is greater than max
1353
- 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)) {
1686
+ if (colorCode && isString(colorCode) && isValidHEXColor(colorCode)) {
1358
1687
  return undefined;
1359
1688
  }
1360
- return "INVALID_NUMBER";
1689
+ return "INVALID_HEX_CODE";
1361
1690
  };
1362
1691
 
1692
+ const cvaInputColorField = cvaMerge([
1693
+ "ml-3",
1694
+ "h-4",
1695
+ "w-4",
1696
+ "self-center",
1697
+ "bg-inherit",
1698
+ "disabled:opacity-50",
1699
+ "disabled:pointer-events-none",
1700
+ "rounded-[4px]",
1701
+ ], {
1702
+ variants: {
1703
+ readOnly: {
1704
+ true: "pointer-events-none",
1705
+ false: "",
1706
+ },
1707
+ },
1708
+ compoundVariants: [
1709
+ {
1710
+ readOnly: true,
1711
+ },
1712
+ ],
1713
+ defaultVariants: {
1714
+ readOnly: false,
1715
+ },
1716
+ });
1717
+
1363
1718
  /**
1364
- * The number field component is used for entering numeric values and includes controls for incrementally increasing or decreasing the value.
1719
+ * Validates if the given value is a valid hex color.
1365
1720
  *
1366
- * _**Do use**_ the NumberField when the controls to incrementally increase or decrease makes the task easier for the user.
1721
+ * @param value - The string value to be validated.
1722
+ * @returns {boolean} True if the value is a valid hex color, otherwise false.
1723
+ */
1724
+ const isValidHEXColor = (value) => {
1725
+ const hexRegex = /^#([0-9A-F]{6})$/i;
1726
+ return hexRegex.test(value);
1727
+ };
1728
+ /**
1729
+ * The ColorField component is used to enter color.
1730
+ * ColorField validates that user enters a valid color address.
1367
1731
  *
1368
- * _**Do not use**_ this fields for non-serialized numbers. Use TextField instead.
1369
1732
  */
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();
1733
+ const ColorField = forwardRef(({ label, id, tip, helpText, errorMessage, helpAddon, className, defaultValue, dataTestId, value: propValue, onChange, isInvalid = false, onBlur, fieldSize = "medium", ...rest }, ref) => {
1734
+ const renderAsDisabled = Boolean(rest.disabled);
1735
+ const renderAsReadonly = Boolean(rest.readOnly);
1736
+ const htmlForId = useMemo(() => (id ? id : "colorField-" + uuidv4()), [id]);
1737
+ const innerRef = useRef(null);
1738
+ useImperativeHandle(ref, () => innerRef.current, []);
1372
1739
  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 => {
1740
+ // Internal state for color value
1741
+ const [innerValue, setInnerValue] = useState(propValue || defaultValue || "");
1742
+ const [renderAsInvalid, setRenderAsInvalid] = useState(!!errorMessage || (innerValue && typeof innerValue === "string" && !isValidHEXColor(innerValue)) || isInvalid);
1743
+ const errorType = useMemo(() => validateColorCode(innerValue, rest.required), [rest.required, innerValue]);
1744
+ const error = useMemo(() => (errorType ? t(`colorField.error.${errorType}`) : errorMessage), [errorType, errorMessage, t]);
1745
+ const handleInputChange = useCallback((event) => {
1395
1746
  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);
1747
+ setInnerValue(newValue);
1408
1748
  if (onChange) {
1409
1749
  onChange(event);
1410
1750
  }
1411
1751
  }, [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 }) }));
1752
+ const handleBlur = useCallback(event => {
1753
+ const newValue = event.target.value;
1754
+ setInnerValue(newValue);
1755
+ setRenderAsInvalid(!!errorType);
1756
+ onBlur?.(event);
1757
+ }, [errorType, onBlur]);
1758
+ return (jsx(FormGroup, { dataTestId: dataTestId ? `${dataTestId}-FormGroup` : undefined, helpAddon: helpAddon, helpText: (renderAsInvalid && error) || helpText, htmlFor: htmlForId, isInvalid: renderAsInvalid, label: label, required: rest.required ? !(renderAsDisabled || renderAsReadonly) : false, tip: tip, children: jsxs("div", { className: cvaInput$1({
1759
+ size: fieldSize,
1760
+ disabled: renderAsDisabled,
1761
+ invalid: renderAsInvalid,
1762
+ readOnly: renderAsReadonly,
1763
+ className,
1764
+ }), "data-testid": dataTestId ? `${dataTestId}-container` : undefined, children: [jsx("input", { "aria-labelledby": htmlForId + "-label", className: cvaInputColorField({ readOnly: renderAsReadonly }), "data-testid": dataTestId, defaultValue: defaultValue, disabled: renderAsDisabled, id: htmlForId, onBlur: handleBlur, onChange: handleInputChange, readOnly: renderAsReadonly, ref: innerRef, type: "color", value: innerValue }), jsx("input", { "aria-labelledby": htmlForId + "-label-text", className: cvaInputElement({
1765
+ className: "px-1 focus-visible:outline-none",
1766
+ }), "data-testid": dataTestId ? `${dataTestId}-textField` : undefined, disabled: renderAsDisabled, onBlur: handleBlur, onChange: handleInputChange, readOnly: renderAsReadonly, type: "text", value: innerValue }), jsx(GenericActionsRenderer, { disabled: renderAsDisabled || renderAsReadonly, fieldSize: fieldSize, genericAction: "edit", innerRef: innerRef, tooltipLabel: t("colorField.tooltip") })] }) }));
1767
+ });
1768
+ ColorField.displayName = "ColorField";
1769
+
1770
+ /**
1771
+ * The date field component is used for entering date values.
1772
+ *
1773
+ * _**Do use**_ the DateField for date input.
1774
+ *
1775
+ * _**Do not use**_ this fields for non-serialized dates. Use TextField instead.
1776
+ */
1777
+ const DateField = ({ label, id, tip, helpText, errorMessage, helpAddon, isInvalid, className, defaultValue, dataTestId, ref, ...rest }) => {
1778
+ const renderAsInvalid = isInvalid === undefined ? Boolean(errorMessage) : isInvalid;
1779
+ const htmlForId = id ? id : "dateField-" + uuidv4();
1780
+ return (jsx(FormGroup, { dataTestId: dataTestId ? `${dataTestId}-FormGroup` : undefined, helpAddon: helpAddon, helpText: (renderAsInvalid && errorMessage) || helpText, htmlFor: htmlForId, isInvalid: renderAsInvalid, label: label, required: rest.required ? !(rest.disabled || rest.readOnly) : false, tip: tip, children: jsx(DateBaseInput, { "aria-labelledby": htmlForId + "-label", defaultValue: defaultValue, id: htmlForId, isInvalid: renderAsInvalid, ref: ref, ...rest, className: className, dataTestId: dataTestId }) }));
1413
1781
  };
1414
- NumberField.displayName = "NumberField";
1782
+ DateField.displayName = "DateField";
1415
1783
 
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",
1784
+ const cvaDropZone = cvaMerge([
1429
1785
  "flex",
1430
- "gap-2",
1786
+ "component-baseInput-background",
1431
1787
  "justify-center",
1432
- "items-center",
1433
- "text-center",
1434
- "rounded-md",
1435
- "relative",
1788
+ "text-neutral-500",
1789
+ "rounded-lg",
1790
+ "border-2",
1791
+ "border-neutral-200",
1792
+ "border-dashed",
1793
+ "hover:bg-neutral-100",
1794
+ "hover:border-solid",
1795
+ "hover:border-primary-500",
1436
1796
  ], {
1437
1797
  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"],
1798
+ size: {
1799
+ small: ["p-2"],
1800
+ medium: ["p-4"],
1801
+ large: ["p-8"],
1445
1802
  },
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: [""],
1803
+ disabled: { true: ["bg-neutral-100", "hover:bg-neutral-100"], false: "" },
1804
+ dragActive: { true: ["border-neutral-200", "bg-neutral-100"], false: "" },
1805
+ dropComplete: {
1806
+ true: [
1807
+ "border-solid",
1808
+ "border-primary-500",
1809
+ "bg-neutral-100",
1810
+ "hover:border-solid",
1811
+ "hover:border-neutral-200",
1812
+ ],
1813
+ false: "",
1480
1814
  },
1815
+ invalid: { true: ["border-danger-600", "text-danger-500"], false: "" },
1481
1816
  },
1482
1817
  });
1483
- const cvaTag = cvaMerge([], {
1818
+ const cvaDropZoneLabel = cvaMerge([
1819
+ "h-full",
1820
+ "pt-1",
1821
+ "pb-1",
1822
+ "gap-2",
1823
+ "items-center",
1824
+ "flex-col",
1825
+ "flex",
1826
+ "justify-center",
1827
+ ]);
1828
+ const cvaDropZoneIconBackground = cvaMerge(["relative", "flex", "items-center", "justify-center", "rounded-full", "p-3"], {
1484
1829
  variants: {
1485
- layout: {
1486
- default: ["absolute", "top-2", "right-2"],
1487
- compact: [],
1488
- },
1830
+ invalid: { true: ["bg-red-100"], false: ["bg-neutral-200"] },
1489
1831
  },
1490
1832
  });
1491
1833
 
1492
1834
  /**
1493
- * A card version of a radio button that includes an icon, headings and a description.
1835
+ *
1836
+ * Default UX-intuitive label for the DropZone - can be overwritten by the label prop
1494
1837
  */
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";
1838
+ const DropZoneDefaultLabel = () => (jsx(Trans, { components: {
1839
+ clickable: jsx("span", { className: "text-primary-600 hover:text-primary-700 cursor-pointer underline" }),
1840
+ }, i18nKey: "dropzone.label.default", values: {} }));
1503
1841
 
1504
1842
  /**
1505
- * A thin wrapper around the `BaseInput` component for password input fields.
1843
+ * The Drop Zone can be used to drag and drop files or to browse and select files from the file system.
1506
1844
  *
1507
- * NOTE: If shown with a label, please use the `PasswordField` component instead.
1845
+ * @param {DropZoneProps} props - The props for the DropZone component
1846
+ * @returns {ReactElement} DropZone component
1508
1847
  */
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" }));
1848
+ const DropZone = ({ className, dataTestId, filesSelected, label = jsx(DropZoneDefaultLabel, {}), size = "large", isInvalid = false, disabled = false, accept, multiple = false, ...rest }) => {
1849
+ const [dragActive, setDragActive] = useState(false);
1850
+ const [fileDropped, setFileDropped] = useState(false);
1851
+ const [t] = useTranslation();
1852
+ const inputLabelRef = useRef(null);
1853
+ // function that handles drag enter, drag leave and drag over
1854
+ const handleDrag = (e) => {
1855
+ e.preventDefault();
1856
+ e.stopPropagation();
1857
+ if ((e.type === "dragenter" || e.type === "dragover") && !disabled) {
1858
+ setDragActive(true);
1859
+ }
1860
+ else if (e.type === "dragleave") {
1861
+ setDragActive(false);
1862
+ }
1863
+ };
1864
+ //function to handle when user clicks on dropzone to upload
1865
+ const handleChange = (e) => {
1866
+ e.preventDefault();
1867
+ e.stopPropagation();
1868
+ if (e.target.files && !disabled) {
1869
+ filesSelected(e.target.files);
1870
+ }
1871
+ };
1872
+ //function to handle drop
1873
+ const handleDrop = (e) => {
1874
+ e.preventDefault();
1875
+ e.stopPropagation();
1876
+ setDragActive(false);
1877
+ if (e.dataTransfer.files[0] && !disabled) {
1878
+ filesSelected(e.dataTransfer.files);
1879
+ setFileDropped(true);
1880
+ }
1881
+ };
1882
+ //function to handle focusable button click (for accessibility)
1883
+ const handleButtonClick = (e) => {
1884
+ e.preventDefault();
1885
+ e.stopPropagation();
1886
+ if (disabled) {
1887
+ return;
1888
+ }
1889
+ inputLabelRef.current?.click();
1890
+ };
1891
+ return (jsx("div", { className: cvaDropZone({ size, dropComplete: fileDropped, dragActive, disabled, invalid: isInvalid, className }), "data-testid": dataTestId, onClick: e => {
1892
+ if (disabled) {
1893
+ e.preventDefault();
1894
+ e.stopPropagation();
1895
+ }
1896
+ }, onDragEnter: handleDrag, onDragLeave: handleDrag, onDragOver: handleDrag, onDrop: handleDrop, ...rest, children: jsxs("label", { className: cvaDropZoneLabel(), "data-testid": dataTestId ? `${dataTestId}-label` : null, ref: inputLabelRef, children: [jsx("input", { accept: accept, className: "hidden", multiple: multiple, onChange: handleChange, title: t("dropzone.input.title"), type: "file" }), jsx("div", { className: cvaDropZoneIconBackground({ invalid: isInvalid }), children: jsx(Icon, { className: !isInvalid ? "text-neutral-400" : "", color: isInvalid ? "danger" : "neutral", name: "ArrowUpCircle", type: "solid" }) }), jsx("button", { disabled: disabled, onClick: handleButtonClick, children: label })] }) }));
1512
1897
  };
1513
1898
 
1899
+ // Doing the same check as we do on the backend
1900
+ // Using OWASP pattern from: https://owasp.org/www-community/OWASP_Validation_Regex_Repository
1901
+ const EMAIL_REGEX = /^[a-zA-Z0-9_+&*-]+(?:\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,7}$/;
1514
1902
  /**
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._
1903
+ * @description Validate given email id.
1904
+ * @param email The address to validate.
1905
+ * @returns {boolean} Returns true if the email address is valid else false.
1906
+ * @example validateEmailAddress(test@gmail.com) // true
1520
1907
  */
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 }) }));
1908
+ const validateEmailAddress = (email) => {
1909
+ if (!email) {
1910
+ return false;
1911
+ }
1912
+ return EMAIL_REGEX.test(email);
1528
1913
  };
1529
- PasswordField.displayName = "PasswordField";
1530
1914
 
1531
1915
  /**
1532
- * Validates a phone number
1916
+ * Validates a email id
1533
1917
  */
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)) {
1918
+ const validateEmailId = (emailId, required) => {
1919
+ if (!emailId && !required) {
1545
1920
  return undefined;
1546
1921
  }
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";
1922
+ if (!emailId && required) {
1923
+ return "REQUIRED";
1555
1924
  }
1556
- if (safePhoneNumber.length <= 5) {
1557
- //needs to be handled manually, parsePhoneNumberFromString can't parse it
1558
- return "TOO_SHORT";
1925
+ if (emailId && isString(emailId) && validateEmailAddress(emailId)) {
1926
+ return undefined;
1559
1927
  }
1560
- return "INVALID_NUMBER";
1928
+ return "INVALID_EMAIL";
1561
1929
  };
1930
+
1562
1931
  /**
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
1932
+ * A Email Input component is used for input of the type Email.
1933
+ *
1934
+ * It has an ActionButton which sends an email to the specified address using "mailto:" link.
1935
+ *
1936
+ * Extends props of BaseInput.
1937
+ *
1938
+ * A reference to the input element is provided as the `ref` prop.
1939
+ * For specific input types make sure to use the corresponding input component.
1572
1940
  */
1573
- const phoneErrorMessage = (phoneNumber, required) => {
1574
- if ((validatePhoneNumber(phoneNumber) === "REQUIRED" && !required) ||
1575
- (validatePhoneNumber(phoneNumber) === "REQUIRED" && required && phoneNumber === undefined)) {
1576
- return undefined;
1577
- }
1578
- return validatePhoneNumber(phoneNumber);
1941
+ const EmailBaseInput = ({ fieldSize = "medium", disabled = false, dataTestId, isInvalid = false, onChange, disableAction = false, ref, ...rest }) => {
1942
+ const [email, setEmail] = useState(rest.value?.toString() || rest.defaultValue?.toString());
1943
+ const sendEmail = () => {
1944
+ return window.open(`mailto:${email}`);
1945
+ };
1946
+ const handleChange = useCallback(event => {
1947
+ const newValue = event.target.value;
1948
+ onChange?.(event);
1949
+ setEmail(newValue);
1950
+ }, [onChange]);
1951
+ const renderAsInvalid = (email && !validateEmailAddress(email)) || isInvalid;
1952
+ return (jsx(BaseInput, { actions: email && email.length > 0 ? (jsx(ActionButton, { dataTestId: dataTestId ? `${dataTestId}-emailIcon` : undefined, disabled: disableAction || isInvalid, onClick: sendEmail, size: fieldSize ?? undefined, type: "EMAIL", value: email })) : null, dataTestId: dataTestId, disabled: disabled, fieldSize: fieldSize, isInvalid: renderAsInvalid, onChange: handleChange, placeholder: rest.placeholder || "mail@example.com", ref: ref, type: "email", ...rest }));
1579
1953
  };
1580
1954
 
1581
1955
  /**
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.
1956
+ * The EmailField component is used to enter email.
1957
+ * EmailField validates that user enters a valid email address.
1585
1958
  *
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.
1595
1959
  */
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();
1960
+ const EmailField = ({ label, id, tip, helpText, errorMessage, helpAddon, className, defaultValue, dataTestId, value, onChange, onBlur, isInvalid = false, ref, ...rest }) => {
1961
+ const htmlForId = id ? id : "emailField-" + uuidv4();
1598
1962
  const [t] = useTranslation();
1599
1963
  const [innerValue, setInnerValue] = useState(() => {
1600
- return (value?.toString() || defaultValue?.toString()) ?? undefined;
1964
+ return (value?.toString() || defaultValue?.toString()) ?? "";
1601
1965
  });
1602
- 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]);
1605
- const error = useMemo(() => {
1606
- // for the case when a custom error message is provided
1607
- if (errorMessage) {
1608
- return errorMessage;
1609
- }
1610
- else if (errorType) {
1611
- return t(`phoneField.error.${errorType}`);
1612
- }
1966
+ const [renderAsInvalid, setRenderAsInvalid] = useState(!!errorMessage || (value && isString(value) && !validateEmailAddress(value)) || isInvalid);
1967
+ const errorType = useMemo(() => validateEmailId(innerValue ?? "", rest.required), [rest.required, innerValue]);
1968
+ const error = useMemo(() => (errorType ? t(`emailField.error.${errorType}`) : errorMessage), [errorType, errorMessage, t]);
1969
+ const handleBlur = useCallback(event => {
1970
+ const newValue = event.target.value;
1971
+ setInnerValue(newValue);
1972
+ setRenderAsInvalid(!!errorType);
1973
+ onBlur?.(event);
1974
+ }, [errorType, onBlur]);
1975
+ const handleChange = useCallback((event) => {
1976
+ setInnerValue(event.target.value);
1977
+ if (onChange) {
1978
+ onChange(event);
1979
+ }
1980
+ }, [onChange]);
1981
+ 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(EmailBaseInput, { "aria-labelledby": htmlForId + "-label", defaultValue: defaultValue, id: htmlForId, isInvalid: renderAsInvalid, onBlur: handleBlur, onChange: handleChange, ref: ref, value: value, ...rest, className: className, dataTestId: dataTestId }) }));
1982
+ };
1983
+ EmailField.displayName = "EmailField";
1984
+
1985
+ /** Type guard for function refs vs. object refs without deprecated types or assertions */
1986
+ function isWritableRef(r) {
1987
+ return typeof r === "object" && r !== null && "current" in r;
1988
+ }
1989
+ /**
1990
+ * Multi adapter:
1991
+ * - keeps Option[] semantics (via `MultiValue<Option>`)
1992
+ * - renders FormGroup chrome (label, help, error)
1993
+ * - exposes a hidden <select> for a stable ref target
1994
+ * - optionally renders one hidden <input> per selected option IF `getOptionValue` is provided
1995
+ * - passes through all remaining BaseSelect props with isMulti=true
1996
+ */
1997
+ const FormFieldSelectAdapterMulti = (props) => {
1998
+ const { className, dataTestId, helpText, helpAddon, tip, label, isInvalid, errorMessage, name, onBlur, options, value, defaultValue, id, onChange, children, ref, ...selectProps } = props;
1999
+ // Hidden select for a stable DOM ref target (API parity with single adapter)
2000
+ const innerRef = useRef(null);
2001
+ // Bridge external ref (supports both callback and object refs)
2002
+ useEffect(() => {
2003
+ if (typeof ref === "function") {
2004
+ ref(innerRef.current);
2005
+ }
2006
+ else if (isWritableRef(ref)) {
2007
+ ref.current = innerRef.current;
2008
+ }
2009
+ }, [ref]);
2010
+ // Determine invalid state
2011
+ const renderAsInvalid = useMemo(() => (isInvalid === undefined ? Boolean(errorMessage) : isInvalid), [errorMessage, isInvalid]);
2012
+ // id to connect label and control
2013
+ const controlId = useMemo(() => (id ? id : "multiSelectField-" + uuidv4()), [id]);
2014
+ // If consumers provided getOptionValue (from BaseSelect props),
2015
+ // we can render hidden inputs for native form submit / RHF.
2016
+ const selectPropsWithAccessors = selectProps;
2017
+ const getOptionValue = typeof selectPropsWithAccessors.getOptionValue === "function" ? selectPropsWithAccessors.getOptionValue : undefined;
2018
+ // Compute selected options snapshot for hidden inputs (prefer controlled `value`)
2019
+ const selectedOptions = useMemo(() => value ?? defaultValue ?? [], [value, defaultValue]);
2020
+ // Build the exact prop bag for BaseSelect (multi=true).
2021
+ const childProps = {
2022
+ ...selectProps,
2023
+ id: controlId,
2024
+ onBlur,
2025
+ options,
2026
+ isMulti: true,
2027
+ value: value ?? null,
2028
+ defaultValue,
2029
+ onChange: next => onChange?.(next),
2030
+ };
2031
+ 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
2032
+ ? !(("disabled" in selectProps && Boolean(selectProps.disabled)) ||
2033
+ ("readOnly" in selectProps && Boolean(selectProps.readOnly)))
2034
+ : false, tip: tip, children: [jsx("select", { "aria-hidden": "true", defaultValue: "", hidden: true, name: name, ref: innerRef }), typeof getOptionValue === "function" &&
2035
+ selectedOptions.map((opt, idx) => {
2036
+ const primitiveValue = getOptionValue(opt);
2037
+ return typeof primitiveValue === "string" ? (jsx("input", { name: name, type: "hidden", value: primitiveValue }, `${primitiveValue}-${idx}`)) : null;
2038
+ }), children(childProps)] }));
2039
+ };
2040
+ FormFieldSelectAdapterMulti.displayName = "FormFieldSelectAdapterMulti";
2041
+
2042
+ /**
2043
+ * MultiSelectField — validated multi-select field.
2044
+ * Types mirror BaseSelect: options: Option[], value/defaultValue: Option[], onChange: (Option[] | null) => void
2045
+ * Implemented as a generic const component (no forwardRef, no assertions).
2046
+ */
2047
+ const MultiSelectField = ({ ref, ...props }) => {
2048
+ return (jsx(FormFieldSelectAdapterMulti, { ...props, ref: ref, children: convertedProps => jsx(BaseSelect, { ...convertedProps }) }));
2049
+ };
2050
+ MultiSelectField.displayName = "MultiSelectField";
2051
+
2052
+ const isNumberValid = (number) => {
2053
+ if (!isNaN(+number) === false) {
2054
+ return false;
2055
+ }
2056
+ if (typeof number === "number") {
2057
+ return true;
2058
+ }
2059
+ return /^(?=.)([+-]?([0-9]*)(\.([0-9]+))?)$/.test(number);
2060
+ };
2061
+ /**
2062
+ * Validates a number
2063
+ */
2064
+ const validateNumber = (number, required = false, min, max) => {
2065
+ const parsedNumber = Number(number);
2066
+ const minValue = typeof min === "string" ? parseFloat(min) : min;
2067
+ const maxValue = typeof max === "string" ? parseFloat(max) : max;
2068
+ if (number === undefined) {
2069
+ return undefined;
2070
+ }
2071
+ // if the value is a string eg:'test'
2072
+ if (number && !isNaN(+number) === false) {
2073
+ return "INVALID_NUMBER";
2074
+ }
2075
+ // if the value is empty and not required
2076
+ if (!parsedNumber && !required && !min && !max && !!number) {
2077
+ return undefined;
2078
+ }
2079
+ // if the value is empty and required
2080
+ if (required && !!number === false) {
2081
+ return "REQUIRED";
2082
+ }
2083
+ // if the value is not in between min and max
2084
+ if (minValue && maxValue && isNumberValid(parsedNumber) && !(parsedNumber >= minValue && parsedNumber <= maxValue)) {
2085
+ return "NOT_IN_BETWEEN";
2086
+ }
2087
+ // if the value is less than min
2088
+ if (isNumberValid(parsedNumber) && minValue !== undefined && parsedNumber < minValue) {
2089
+ return "GREATER_THAN";
2090
+ }
2091
+ // if the value is greater than max
2092
+ if (isNumberValid(parsedNumber) && maxValue !== undefined && parsedNumber > maxValue) {
2093
+ return "LESS_THAN";
2094
+ }
2095
+ // if the value is a number and is valid
2096
+ if (isNumber(parsedNumber) && isNumberValid(parsedNumber)) {
2097
+ return undefined;
2098
+ }
2099
+ return "INVALID_NUMBER";
2100
+ };
2101
+
2102
+ /**
2103
+ * The number field component is used for entering numeric values and includes controls for incrementally increasing or decreasing the value.
2104
+ *
2105
+ * _**Do use**_ the NumberField when the controls to incrementally increase or decrease makes the task easier for the user.
2106
+ *
2107
+ * _**Do not use**_ this fields for non-serialized numbers. Use TextField instead.
2108
+ */
2109
+ const NumberField = ({ label, id, tip, helpText, errorMessage, helpAddon, isInvalid, maxLength, className, value, dataTestId, defaultValue, onBlur, onChange, ref, ...rest }) => {
2110
+ const htmlForId = id ? id : "numberField-" + uuidv4();
2111
+ const [t] = useTranslation();
2112
+ const [innerValue, setInnerValue] = useState(() => {
2113
+ return Number(value?.toString()) || Number(defaultValue?.toString());
2114
+ });
2115
+ const [renderAsInvalid, setRenderAsInvalid] = useState((isInvalid === undefined ? Boolean(errorMessage) : isInvalid) ||
2116
+ !!validateNumber(value?.toString(), rest.required, rest.min, rest.max));
2117
+ const errorType = useMemo(() => validateNumber(innerValue, rest.required, rest.min, rest.max), [innerValue, rest.max, rest.min, rest.required]);
2118
+ const error = useMemo(() => {
2119
+ // for the case when a custom error message is provided
2120
+ if (errorMessage) {
2121
+ return errorMessage;
2122
+ }
2123
+ else if (errorType) {
2124
+ return t(`numberField.error.${errorType}`, { min: rest.min, max: rest.max });
2125
+ }
1613
2126
  return errorMessage;
1614
- }, [errorMessage, errorType, t]);
2127
+ }, [errorMessage, errorType, rest.max, rest.min, t]);
1615
2128
  useEffect(() => {
1616
- setRenderAsInvalid(Boolean(errorMessage));
2129
+ if (errorMessage) {
2130
+ setRenderAsInvalid(Boolean(errorMessage));
2131
+ }
1617
2132
  }, [errorMessage]);
1618
2133
  const handleBlur = useCallback(event => {
1619
2134
  const newValue = event.target.value;
1620
- setInnerValue(newValue);
2135
+ setInnerValue(newValue.toString());
1621
2136
  // for the case when a custom error message is provided
1622
- if (errorMessage && !phoneErrorMessage(newValue.toString(), rest.required)) {
2137
+ if (errorMessage && !validateNumber(newValue, rest.required, rest.min, rest.max)) {
1623
2138
  setRenderAsInvalid(Boolean(errorMessage));
1624
2139
  }
1625
2140
  else {
1626
- setRenderAsInvalid(!!phoneErrorMessage(newValue.toString(), rest.required));
2141
+ setRenderAsInvalid(!!validateNumber(newValue, rest.required, rest.min, rest.max));
1627
2142
  }
1628
2143
  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 }) }));
2144
+ }, [errorMessage, onBlur, rest.max, rest.min, rest.required]);
2145
+ const handleChange = useCallback((event) => {
2146
+ setInnerValue(event.target.value);
2147
+ if (onChange) {
2148
+ onChange(event);
2149
+ }
2150
+ }, [onChange]);
2151
+ 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
2152
  };
1641
- PhoneFieldWithController.displayName = "PhoneFieldWithController";
2153
+ NumberField.displayName = "NumberField";
1642
2154
 
1643
- const cvaRadioGroup = cvaMerge(["flex", "gap-2", "flex-col", "items-start"], {
2155
+ const cvaOptionCardLabel = cvaMerge([
2156
+ "group",
2157
+ "transition",
2158
+ "bg-white",
2159
+ "outline",
2160
+ "outline-1",
2161
+ "outline-neutral-300",
2162
+ "hover:bg-neutral-100",
2163
+ "focus:bg-neutral-200",
2164
+ "active:bg-neutral-200",
2165
+ "peer-checked:bg-primary-50",
2166
+ "peer-checked:outline-primary-600",
2167
+ "peer-checked:outline-2",
2168
+ "flex",
2169
+ "gap-2",
2170
+ "justify-center",
2171
+ "items-center",
2172
+ "text-center",
2173
+ "rounded-md",
2174
+ "relative",
2175
+ ], {
1644
2176
  variants: {
2177
+ disabled: {
2178
+ true: ["cursor-not-allowed", "bg-neutral-100"],
2179
+ false: ["cursor-pointer"],
2180
+ },
1645
2181
  layout: {
1646
- inline: ["flex", "gap-3", "flex-row", "items-center"],
2182
+ default: ["flex-col", "p-responsive-space", "w-full", "aspect-square"],
2183
+ compact: ["px-3", "py-1.5", "h-8", "min-h-[calc(var(--line-height-sm)+var(--spacing-3))]", "flex-row", "w-fit"],
1647
2184
  },
1648
2185
  },
1649
2186
  });
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
- ], {
2187
+ const cvaOptionCardContent = cvaMerge(["flex", "flex-col", "items-center"]);
2188
+ const cvaOptionCardContainer = cvaMerge(["contents"]);
2189
+ const cvaOptionCardTitle = cvaMerge(["text-neutral-600"], {
1668
2190
  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: "",
2191
+ layout: {
2192
+ default: ["text-lg", "line-clamp-2"],
2193
+ compact: ["text-sm", "line-clamp-1"],
1687
2194
  },
1688
- invalid: {
1689
- true: ["border-red-600", "active:ring-red-700"],
1690
- false: "",
2195
+ disabled: {
2196
+ true: ["text-neutral-400"],
2197
+ false: ["focus:text-neutral-800", "active:text-neutral-800"],
2198
+ },
2199
+ },
2200
+ });
2201
+ const cvaOptionCardText = cvaMerge(["text-neutral-600", "text-sm"], {
2202
+ variants: {
2203
+ type: {
2204
+ subheading: ["font-medium"],
2205
+ description: ["font-normal"],
1691
2206
  },
1692
2207
  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: "",
2208
+ true: ["text-neutral-400"],
2209
+ false: ["focus:text-neutral-800", "active:text-neutral-800"],
1703
2210
  },
1704
2211
  },
1705
- compoundVariants: [
1706
- {
1707
- checked: true,
1708
- disabled: true,
1709
- className: ["bg-white"],
2212
+ });
2213
+ const cvaInput = cvaMerge(["peer", "absolute", "h-0", "w-0", "opacity-0"]);
2214
+ const cvaCustomImage = cvaMerge(["text-neutral-400"], {
2215
+ variants: {
2216
+ disabled: {
2217
+ true: ["!text-neutral-400"],
2218
+ false: [""],
1710
2219
  },
1711
- ],
2220
+ },
1712
2221
  });
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.
2222
+ const cvaTag = cvaMerge([], {
2223
+ variants: {
2224
+ layout: {
2225
+ default: ["absolute", "top-2", "right-2"],
2226
+ compact: [],
2227
+ },
2228
+ },
2229
+ });
2230
+
2231
+ /**
2232
+ * A card version of a radio button that includes an icon, headings and a description.
2233
+ */
2234
+ const OptionCard = ({ icon, heading, subheading, description, disabled, id, value, className, contentClassName, dataTestId, customImage, layout = "default", ref, tagProps, ...rest }) => {
2235
+ const htmlForId = id ?? "option-card-" + uuidv4();
2236
+ 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]);
2237
+ 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
2238
+ ? cloneElement(icon, { className: cvaCustomImage({ disabled, className: icon.props.className }) })
2239
+ : 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] })] }) }));
2240
+ };
2241
+ OptionCard.displayName = "OptionCard";
2242
+
2243
+ /**
2244
+ * A thin wrapper around the `BaseInput` component for password input fields.
2245
+ *
2246
+ * NOTE: If shown with a label, please use the `PasswordField` component instead.
2247
+ */
2248
+ const PasswordBaseInput = ({ ref, fieldSize, ...rest }) => {
2249
+ const [showPassword, setShowPassword] = useState(false);
2250
+ 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" }));
2251
+ };
2252
+
2253
+ /**
2254
+ * Password fields enter a password or other confidential information. Characters are masked as they are typed.
2255
+ *
2256
+ * _**Do use** when the user has to input a password or something that needs to be obfuscated_
2257
+ *
2258
+ * _**Do not use** to confirm user actions, such as deleting. Use a checkbox for such flows._
2259
+ */
2260
+ const PasswordField = ({ id, label, tip, helpText, helpAddon, errorMessage, isInvalid, maxLength, onChange, className, value, dataTestId, ref, ...rest }) => {
2261
+ const renderAsInvalid = isInvalid === undefined ? Boolean(errorMessage) : isInvalid;
2262
+ const htmlFor = id ? id : "passwordField-" + uuidv4();
2263
+ const handleChange = useCallback((event) => {
2264
+ onChange?.(event);
2265
+ }, [onChange]);
2266
+ 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 }) }));
2267
+ };
2268
+ PasswordField.displayName = "PasswordField";
2269
+
2270
+ /**
2271
+ * Validates a phone number
2272
+ */
2273
+ const validatePhoneNumber = (phoneNumber) => {
2274
+ if (!phoneNumber) {
2275
+ return "REQUIRED";
2276
+ }
2277
+ const asYouType = new AsYouType();
2278
+ asYouType.input(phoneNumber);
2279
+ const countryCode = asYouType.getCallingCode();
2280
+ const national = asYouType.getNationalNumber();
2281
+ const safePhoneNumber = getPhoneNumberWithPlus(phoneNumber.trim());
2282
+ const number = parsePhoneNumberFromString(safePhoneNumber);
2283
+ if (phoneNumber && isValidPhoneNumber(phoneNumber)) {
2284
+ return undefined;
2285
+ }
2286
+ if (!countryCode && national) {
2287
+ return "REQUIRED_COUNTRY";
2288
+ }
2289
+ if (phoneNumber &&
2290
+ (checkIfPhoneNumberHasPlus(phoneNumber)
2291
+ ? isNaN(+phoneNumber.slice(1, phoneNumber.length))
2292
+ : isNaN(+phoneNumber) || !number)) {
2293
+ return "NOT_A_NUMBER";
2294
+ }
2295
+ if (safePhoneNumber.length <= 5) {
2296
+ //needs to be handled manually, parsePhoneNumberFromString can't parse it
2297
+ return "TOO_SHORT";
2298
+ }
2299
+ return "INVALID_NUMBER";
2300
+ };
2301
+ /**
2302
+ * Checks if the country code is valid and required
2303
+ */
2304
+ const isInvalidCountryCode = (error, required) => (!!required && error === "REQUIRED") || error === "REQUIRED_COUNTRY";
2305
+ /**
2306
+ * Checks if the phone number is valid and required
2307
+ */
2308
+ const isInvalidPhoneNumber = (error, required) => error !== "REQUIRED_COUNTRY" && ((!!error && error !== "REQUIRED") || (!!required && error === "REQUIRED"));
2309
+ /**
2310
+ * Checks if the phone number is valid and returns corresponding error message
2311
+ */
2312
+ const phoneErrorMessage = (phoneNumber, required) => {
2313
+ if ((validatePhoneNumber(phoneNumber) === "REQUIRED" && !required) ||
2314
+ (validatePhoneNumber(phoneNumber) === "REQUIRED" && required && phoneNumber === undefined)) {
2315
+ return undefined;
2316
+ }
2317
+ return validatePhoneNumber(phoneNumber);
2318
+ };
2319
+
2320
+ /**
2321
+ * The PhoneField component is used to enter phone number.
2322
+ * It is a wrapper around the PhoneInput component and the FormGroup component.
2323
+ * It is used to render a phone number field with a label, a tip, a help text, a help addon and an error message.
2324
+ *
2325
+ * @param {string} [label] - The label for the component.
2326
+ * @param {string} [tip] - The tip for the component.
2327
+ * @param {string} [helpText] - The help text for the component.
2328
+ * @param {string} [helpAddon] - The help addon for the component.
2329
+ * @param {string} [errorMessage] - The error message for the component.
2330
+ * @param {string} [defaultValue] - The default value for the component.
2331
+ * @param {boolean} [disabled=false] - Whether the component is disabled or not.
2332
+ * @param {string} [fieldSize="medium"] - The size of the input field.
2333
+ * @param {boolean} [disableAction=false] - Whether the action button is disabled or not.
2334
+ */
2335
+ const PhoneField = ({ label, id, tip, helpText, isInvalid, errorMessage, value, helpAddon, className, defaultValue, dataTestId, name, onBlur, ref, ...rest }) => {
2336
+ const htmlForId = id ? id : "phoneField-" + uuidv4();
2337
+ const [t] = useTranslation();
2338
+ const [innerValue, setInnerValue] = useState(() => {
2339
+ return (value?.toString() || defaultValue?.toString()) ?? undefined;
2340
+ });
2341
+ const [renderAsInvalid, setRenderAsInvalid] = useState((isInvalid === undefined ? Boolean(errorMessage) : isInvalid) ||
2342
+ !!phoneErrorMessage(value?.toString(), rest.required));
2343
+ const errorType = useMemo(() => phoneErrorMessage(innerValue, rest.required), [innerValue, rest.required]);
2344
+ const error = useMemo(() => {
2345
+ // for the case when a custom error message is provided
2346
+ if (errorMessage) {
2347
+ return errorMessage;
2348
+ }
2349
+ else if (errorType) {
2350
+ return t(`phoneField.error.${errorType}`);
2351
+ }
2352
+ return errorMessage;
2353
+ }, [errorMessage, errorType, t]);
2354
+ useEffect(() => {
2355
+ setRenderAsInvalid(Boolean(errorMessage));
2356
+ }, [errorMessage]);
2357
+ const handleBlur = useCallback(event => {
2358
+ const newValue = event.target.value;
2359
+ setInnerValue(newValue);
2360
+ // for the case when a custom error message is provided
2361
+ if (errorMessage && !phoneErrorMessage(newValue.toString(), rest.required)) {
2362
+ setRenderAsInvalid(Boolean(errorMessage));
2363
+ }
2364
+ else {
2365
+ setRenderAsInvalid(!!phoneErrorMessage(newValue.toString(), rest.required));
2366
+ }
2367
+ onBlur?.(event);
2368
+ }, [errorMessage, onBlur, rest.required]);
2369
+ 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 }) }));
2370
+ };
2371
+ PhoneField.displayName = "PhoneField";
2372
+
2373
+ /**
2374
+ * The PhoneFieldWithController component is a wrapper for the PhoneField component to connect it to react-hook-form.
2375
+ *
2376
+ */
2377
+ const PhoneFieldWithController = ({ control, controllerProps, name, value, ref, ...rest }) => {
2378
+ return (jsx(Controller, { control: control, defaultValue: value, name: name, ...controllerProps, render: ({ field }) => jsx(PhoneField, { ...rest, ...field, ref: ref }) }));
2379
+ };
2380
+ PhoneFieldWithController.displayName = "PhoneFieldWithController";
2381
+
2382
+ const cvaRadioGroup = cvaMerge(["flex", "gap-2", "flex-col", "items-start"], {
2383
+ variants: {
2384
+ layout: {
2385
+ inline: ["flex", "gap-3", "flex-row", "items-center"],
2386
+ },
2387
+ },
2388
+ });
2389
+ const cvaRadioItem = cvaMerge([
2390
+ "self-center",
2391
+ "w-4",
2392
+ "h-4",
2393
+ "appearance-none",
2394
+ "rounded-3xl",
2395
+ "bg-white",
2396
+ "border-solid",
2397
+ "border",
2398
+ "border-neutral-300",
2399
+ "shadow-sm",
2400
+ "shrink-0",
2401
+ "transition",
2402
+ "box-border",
2403
+ "hover:cursor-pointer",
2404
+ "hover:bg-neutral-100",
2405
+ "focus-visible:outline-primary-700",
2406
+ ], {
2407
+ variants: {
2408
+ checked: {
2409
+ true: [
2410
+ "border-solid",
2411
+ "border-4",
2412
+ "border-primary-600",
2413
+ "bg-white",
2414
+ "hover:bg-neutral-100",
2415
+ "hover:cursor-pointer",
2416
+ "outline-0",
2417
+ "active:bg-neutral-200",
2418
+ "active:ring-2",
2419
+ "active:ring-inset",
2420
+ "active:ring-primary-700",
2421
+ "group-active:ring-2",
2422
+ "group-active:ring-inset",
2423
+ "group-active:ring-primary-700",
2424
+ ],
2425
+ false: "",
2426
+ },
2427
+ invalid: {
2428
+ true: ["border-red-600", "active:ring-red-700"],
2429
+ false: "",
2430
+ },
2431
+ disabled: {
2432
+ true: [
2433
+ "bg-neutral-400",
2434
+ "border-neutral-300",
2435
+ "cursor-not-allowed",
2436
+ "hover:bg-neutral-400",
2437
+ "active:bg-neutral-400",
2438
+ "group-active:ring-0",
2439
+ "group-active:ring-inset",
2440
+ ],
2441
+ false: "",
2442
+ },
2443
+ },
2444
+ compoundVariants: [
2445
+ {
2446
+ checked: true,
2447
+ disabled: true,
2448
+ className: ["bg-white"],
2449
+ },
2450
+ ],
2451
+ });
2452
+
2453
+ const RadioGroupContext = createContext(null);
2454
+
2455
+ /**
2456
+ * Use radio buttons when you have a group of mutually exclusive choices and only one selection from the group is allowed.
2457
+ *
2458
+ * 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
2459
  *
1721
2460
  * _**Do use** Radio buttons in forms, settings, or selections in a list._
1722
2461
  *
@@ -1921,763 +2660,94 @@ const parseSchedule = (scheduleString) => {
1921
2660
  return {
1922
2661
  variant,
1923
2662
  schedule: filteredSchedule,
1924
- };
1925
- };
1926
- /**
1927
- * Serialize week schedule to string schedule
1928
- *
1929
- * @param {WeekSchedule} weekSchedule Week schedule range
1930
- * @returns {string} Schedule string
1931
- */
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;
1944
- }
1945
- })
1946
- .map(({ day, range, isAllDay }) => {
1947
- if (isAllDay) {
1948
- return `${day}#00:00-24:00`;
1949
- }
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);
1962
- };
1963
- /**
1964
- * Checks if a list of schedule objects are consecutive days
1965
- *
1966
- * @param {RawSchedule[]} schedule List of schedule objects
1967
- * @returns {boolean} Whether the schedule has consecutive days
1968
- */
1969
- const hasConsecutiveDays = (schedule) => {
1970
- const days = [1, 2, 3, 4, 5];
1971
- return schedule.every(({ day }, index) => day === days[index]);
1972
- };
1973
-
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
- ], {
1983
- 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",
1994
- },
1995
- },
1996
- });
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",
2021
- "bg-white",
2022
- "transition",
2023
- "text-sm",
2024
- "min-h-0",
2025
- ], {
2026
- variants: {
2027
- fieldSize: {
2028
- small: ["h-7", "text-xs"],
2029
- medium: ["h-8"],
2030
- large: ["h-10"],
2031
- },
2032
- invalid: {
2033
- true: "border border-red-600 text-red-600 hover:border-red-600",
2034
- false: "",
2035
- },
2036
- 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",
2058
- 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
- };
2158
-
2159
- /**
2160
- * TagsContainer component to display tags in limited space when children can't fit space it displays counter
2161
- *
2162
- * @param {TagsContainerProps} props - The props for the TagContainer
2163
- * @returns {ReactElement} TagsContainer
2164
- */
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] }));
2235
- };
2236
-
2237
- /**
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.
2240
- *
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
2251
- */
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;
2377
- };
2378
-
2379
- /**
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
2386
- */
2387
- const useCustomStyles = ({ refContainer, maxSelectedDisplayCount, styles, disabled, fieldSize, }) => {
2388
- const customStyles = useMemo(() => {
2389
- 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,
2476
- };
2477
- }, [refContainer, disabled, fieldSize, maxSelectedDisplayCount, styles]);
2478
- return { customStyles };
2663
+ };
2479
2664
  };
2480
-
2481
2665
  /**
2482
- * A hook used by selects to share the common code
2666
+ * Serialize week schedule to string schedule
2483
2667
  *
2484
- * @param {SelectProps} props - The props for the Select component
2485
- * @returns {UseSelectProps} Select component
2668
+ * @param {WeekSchedule} weekSchedule Week schedule range
2669
+ * @returns {string} Schedule string
2486
2670
  */
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);
2671
+ const serializeSchedule = (weekSchedule) => {
2672
+ return weekSchedule.schedule
2673
+ .filter(({ range, day, isAllDay }) => {
2674
+ const hasRange = range.timeFrom && range.timeTo;
2675
+ switch (weekSchedule.variant) {
2676
+ case ScheduleVariant.WEEKDAYS:
2677
+ return day <= 5 && hasRange;
2678
+ case ScheduleVariant.ALL_DAYS:
2679
+ return day <= 7 && hasRange;
2680
+ case ScheduleVariant.CUSTOM:
2681
+ default:
2682
+ return hasRange || isAllDay;
2516
2683
  }
2517
- else {
2518
- setMenuIsEnabled(true);
2684
+ })
2685
+ .map(({ day, range, isAllDay }) => {
2686
+ if (isAllDay) {
2687
+ return `${day}#00:00-24:00`;
2519
2688
  }
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
- };
2689
+ return `${day}#${range.timeFrom}-${range.timeTo}`;
2690
+ })
2691
+ .join(",");
2534
2692
  };
2535
-
2536
2693
  /**
2537
- * CreatableSelects are input components used to choose a value from a set.
2694
+ * Checks if a list of schedule objects have the same ranges
2538
2695
  *
2539
- * @param {CreatableSelectProps} props - The props for the CreatableSelect component
2540
- * @returns {ReactElement} CreatableSelect component
2696
+ * @param {RawSchedule[]} schedule List of schedule objects
2697
+ * @returns {boolean} Whether the schedule is uniform
2541
2698
  */
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] }));
2699
+ const isUniform = (schedule) => {
2700
+ return schedule.every((day, _, collection) => collection[0]?.range?.timeFrom === day.range?.timeFrom && collection[0]?.range?.timeTo === day.range?.timeTo);
2701
+ };
2702
+ /**
2703
+ * Checks if a list of schedule objects are consecutive days
2704
+ *
2705
+ * @param {RawSchedule[]} schedule List of schedule objects
2706
+ * @returns {boolean} Whether the schedule has consecutive days
2707
+ */
2708
+ const hasConsecutiveDays = (schedule) => {
2709
+ const days = [1, 2, 3, 4, 5];
2710
+ return schedule.every(({ day }, index) => day === days[index]);
2602
2711
  };
2603
- CreatableSelect.displayName = "CreatableSelect";
2604
2712
 
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;
2713
+ const cvaSearch = cvaMerge([
2714
+ "shadow-none",
2715
+ "component-search-border",
2716
+ "component-search-background",
2717
+ "hover:component-search-background",
2718
+ "hover:component-search-focus-hover",
2719
+ "transition-all",
2720
+ "duration-300",
2721
+ ], {
2722
+ variants: {
2723
+ border: { true: ["!component-search-borderless"], false: "" },
2724
+ widenOnFocus: {
2725
+ true: [
2726
+ "component-search-width",
2727
+ "component-search-widen",
2728
+ "hover:component-search-widen",
2729
+ "focus-within:w-full",
2730
+ "max-w-sm",
2731
+ ],
2732
+ false: "w-full",
2733
+ },
2734
+ },
2735
+ });
2736
+
2607
2737
  /**
2608
- * Selects are input components used to choose a value from a set.
2738
+ * The Search component is used to render a search input field.
2609
2739
  *
2610
- * @param {SelectProps} props - The props for the Select component
2611
- * @returns {ReactElement} Select component
2740
+ * @param {SearchProps} props - The props for the Search component
2612
2741
  */
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] }));
2742
+ 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 }) => {
2743
+ const { t } = useTranslation();
2744
+ 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:
2745
+ //only show the clear button if there is a value and the onClear function is provided
2746
+ onClear && value ? (jsx("button", { className: "flex", "data-testid": dataTestId ? `${dataTestId}_suffix_component` : null, onClick: () => {
2747
+ onClear();
2748
+ }, ref: xMarkRef, type: "button", children: jsx(Icon, { name: "XMark", size: "small" }) })) : undefined, value: value }));
2679
2749
  };
2680
- Select.displayName = "Select";
2750
+ Search.displayName = "Search";
2681
2751
 
2682
2752
  /**
2683
2753
  *
@@ -2764,7 +2834,7 @@ CreatableSelectField.displayName = "CreatableSelectField";
2764
2834
  * @param {SelectFieldProps} props - The props for the SelectField component
2765
2835
  */
2766
2836
  const SelectField = ({ ref, ...props }) => {
2767
- return (jsx(FormFieldSelectAdapter, { ...props, ref: ref, children: convertedProps => jsx(Select, { ...convertedProps }) }));
2837
+ return (jsx(FormFieldSelectAdapter, { ...props, ref: ref, children: convertedProps => jsx(BaseSelect, { ...convertedProps }) }));
2768
2838
  };
2769
2839
  SelectField.displayName = "SelectField";
2770
2840
 
@@ -3177,4 +3247,4 @@ const useZodValidators = () => {
3177
3247
  */
3178
3248
  setupLibraryTranslations();
3179
3249
 
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 };
3250
+ export { ActionButton, BaseInput, BaseSelect, Checkbox, CheckboxField, ColorField, CreatableSelect, CreatableSelectField, DEFAULT_TIME, DateBaseInput, DateField, DropZone, DropZoneDefaultLabel, EMAIL_REGEX, EmailField, FormFieldSelectAdapter, FormGroup, Label, MultiSelectField, MultiSelectMenuItem, NumberBaseInput, NumberField, OptionCard, PasswordBaseInput, PasswordField, PhoneBaseInput, PhoneField, PhoneFieldWithController, RadioGroup, RadioItem, Schedule, ScheduleVariant, Search, SelectField, SingleSelectMenuItem, TextAreaBaseInput, TextAreaField, TextBaseInput, TextField, TimeRange, TimeRangeField, ToggleSwitch, ToggleSwitchOption, UploadField, UploadInput, UrlField, checkIfPhoneNumberHasPlus, countryCodeToFlagEmoji, cvaAccessoriesContainer, cvaActionButton, cvaActionContainer, cvaInput$1 as cvaInput, cvaInputAddon, cvaInputBase, cvaInputBaseDisabled, cvaInputBaseInvalid, cvaInputBaseReadOnly, cvaInputBaseSize, cvaInputElement, cvaInputItemPlacementManager, cvaInputPrefix, cvaInputSuffix, cvaLabel, cvaSelect, cvaSelectControl, cvaSelectCounter, cvaSelectDynamicTagContainer, cvaSelectIcon, cvaSelectMenu, cvaSelectMenuList, cvaSelectPrefixSuffix, cvaSelectXIcon, getCountryAbbreviation, getPhoneNumberWithPlus, isInvalidCountryCode, isInvalidPhoneNumber, isValidHEXColor, parseSchedule, phoneErrorMessage, serializeSchedule, useCustomComponents, useGetPhoneValidationRules, usePhoneInput, useZodValidators, validateEmailAddress, validatePhoneNumber, weekDay };