@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.cjs.js +1511 -1438
- package/index.esm.js +1509 -1439
- package/package.json +2 -2
- package/src/components/BaseInput/BaseInput.d.ts +2 -2
- package/src/components/BaseInput/BaseInput.variants.d.ts +10 -3
- package/src/components/{Select/Select.d.ts → BaseSelect/BaseSelect.d.ts} +3 -2
- package/src/components/{Select → BaseSelect}/index.d.ts +2 -2
- package/src/components/MultiSelectField/FormFieldSelectAdapterMulti.d.ts +59 -0
- package/src/components/MultiSelectField/MultiSelectField.d.ts +10 -0
- package/src/components/MultiSelectField/index.d.ts +1 -0
- package/src/components/SelectField/CreatableSelectField.d.ts +1 -1
- package/src/components/SelectField/FormFieldSelectAdapter.d.ts +1 -1
- package/src/index.d.ts +2 -1
- /package/src/components/{Select/Select.variants.d.ts → BaseSelect/BaseSelect.variants.d.ts} +0 -0
- /package/src/components/{Select → BaseSelect}/CreatableSelect.d.ts +0 -0
- /package/src/components/{Select → BaseSelect}/SelectMenuItem/SelectMenuItem.d.ts +0 -0
- /package/src/components/{Select → BaseSelect}/TagWithWidth.d.ts +0 -0
- /package/src/components/{Select → BaseSelect}/TagsContainer.d.ts +0 -0
- /package/src/components/{Select → BaseSelect}/useCustomComponents.d.ts +0 -0
- /package/src/components/{Select → BaseSelect}/useCustomStyles.d.ts +0 -0
- /package/src/components/{Select → BaseSelect}/useSelect.d.ts +0 -0
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,
|
|
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,
|
|
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
|
|
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([
|
|
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-
|
|
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:
|
|
126
|
-
medium:
|
|
127
|
-
large:
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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"
|
|
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 (
|
|
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":
|
|
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
|
-
*
|
|
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 {
|
|
930
|
-
* @returns {ReactElement}
|
|
1029
|
+
* @param {SelectMenuItemProps} props - The props for the SingleSelectMenuItem
|
|
1030
|
+
* @returns {ReactElement} SingleSelectMenuItem
|
|
931
1031
|
*/
|
|
932
|
-
const
|
|
933
|
-
return (jsx(
|
|
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
|
-
*
|
|
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 {
|
|
964
|
-
* @returns {ReactElement}
|
|
1043
|
+
* @param {SelectMenuItemProps} props - The props for the MultiSelectMenuItem
|
|
1044
|
+
* @returns {ReactElement} multi select menu item
|
|
965
1045
|
*/
|
|
966
|
-
const
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
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
|
-
*
|
|
1061
|
+
* Extended Tag component with information about its own width.
|
|
1062
|
+
* Used in the select component.
|
|
977
1063
|
*
|
|
978
|
-
*
|
|
1064
|
+
* @param {TagProps} props - The props for the tag component
|
|
1065
|
+
* @returns {ReactElement} TagWithWidth component
|
|
979
1066
|
*/
|
|
980
|
-
const
|
|
981
|
-
const
|
|
982
|
-
|
|
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
|
|
989
|
-
* @returns {
|
|
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
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
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
|
-
*
|
|
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
|
-
* @
|
|
1049
|
-
* @
|
|
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
|
|
1052
|
-
const
|
|
1053
|
-
|
|
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
|
-
*
|
|
1057
|
-
*
|
|
1058
|
-
*
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
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
|
|
1106
|
-
const
|
|
1107
|
-
|
|
1108
|
-
|
|
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
|
-
*
|
|
1400
|
+
* @param {SelectProps} props - The props for the Select component
|
|
1401
|
+
* @returns {UseSelectProps} Select component
|
|
1165
1402
|
*/
|
|
1166
|
-
const
|
|
1167
|
-
|
|
1168
|
-
|
|
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
|
-
*
|
|
1455
|
+
* Selects are input components used to choose a value from a set.
|
|
1172
1456
|
*
|
|
1173
|
-
* @param {
|
|
1174
|
-
* @returns {ReactElement}
|
|
1457
|
+
* @param {SelectProps} props - The props for the Select component
|
|
1458
|
+
* @returns {ReactElement} Select component
|
|
1175
1459
|
*/
|
|
1176
|
-
const
|
|
1177
|
-
const
|
|
1178
|
-
const
|
|
1179
|
-
const
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
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
|
-
*
|
|
1232
|
-
*
|
|
1233
|
-
* @
|
|
1234
|
-
* @
|
|
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
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
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
|
-
*
|
|
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
|
|
1247
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
1270
|
-
const
|
|
1271
|
-
|
|
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
|
|
1289
|
-
|
|
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
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
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
|
|
1677
|
+
* Validates a url
|
|
1324
1678
|
*/
|
|
1325
|
-
const
|
|
1326
|
-
|
|
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
|
-
|
|
1341
|
-
if (required && !!number === false) {
|
|
1683
|
+
if (!colorCode && required) {
|
|
1342
1684
|
return "REQUIRED";
|
|
1343
1685
|
}
|
|
1344
|
-
|
|
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 "
|
|
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
|
-
*
|
|
1719
|
+
* Validates if the given value is a valid hex color.
|
|
1365
1720
|
*
|
|
1366
|
-
*
|
|
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
|
|
1371
|
-
const
|
|
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
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
const
|
|
1377
|
-
|
|
1378
|
-
const
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1782
|
+
DateField.displayName = "DateField";
|
|
1415
1783
|
|
|
1416
|
-
const
|
|
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
|
-
"
|
|
1786
|
+
"component-baseInput-background",
|
|
1431
1787
|
"justify-center",
|
|
1432
|
-
"
|
|
1433
|
-
"
|
|
1434
|
-
"
|
|
1435
|
-
"
|
|
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
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
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
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
1835
|
+
*
|
|
1836
|
+
* Default UX-intuitive label for the DropZone - can be overwritten by the label prop
|
|
1494
1837
|
*/
|
|
1495
|
-
const
|
|
1496
|
-
|
|
1497
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
1845
|
+
* @param {DropZoneProps} props - The props for the DropZone component
|
|
1846
|
+
* @returns {ReactElement} DropZone component
|
|
1508
1847
|
*/
|
|
1509
|
-
const
|
|
1510
|
-
const [
|
|
1511
|
-
|
|
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
|
-
*
|
|
1516
|
-
*
|
|
1517
|
-
*
|
|
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
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
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
|
|
1916
|
+
* Validates a email id
|
|
1533
1917
|
*/
|
|
1534
|
-
const
|
|
1535
|
-
if (!
|
|
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 (!
|
|
1548
|
-
return "
|
|
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 (
|
|
1557
|
-
|
|
1558
|
-
return "TOO_SHORT";
|
|
1925
|
+
if (emailId && isString(emailId) && validateEmailAddress(emailId)) {
|
|
1926
|
+
return undefined;
|
|
1559
1927
|
}
|
|
1560
|
-
return "
|
|
1928
|
+
return "INVALID_EMAIL";
|
|
1561
1929
|
};
|
|
1930
|
+
|
|
1562
1931
|
/**
|
|
1563
|
-
*
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
*
|
|
1568
|
-
|
|
1569
|
-
|
|
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
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
return
|
|
1577
|
-
}
|
|
1578
|
-
|
|
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
|
|
1583
|
-
*
|
|
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
|
|
1597
|
-
const htmlForId = id ? id : "
|
|
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()) ??
|
|
1964
|
+
return (value?.toString() || defaultValue?.toString()) ?? "";
|
|
1601
1965
|
});
|
|
1602
|
-
const [renderAsInvalid, setRenderAsInvalid] = useState((
|
|
1603
|
-
|
|
1604
|
-
const
|
|
1605
|
-
const
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
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
|
-
|
|
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 && !
|
|
2137
|
+
if (errorMessage && !validateNumber(newValue, rest.required, rest.min, rest.max)) {
|
|
1623
2138
|
setRenderAsInvalid(Boolean(errorMessage));
|
|
1624
2139
|
}
|
|
1625
2140
|
else {
|
|
1626
|
-
setRenderAsInvalid(!!
|
|
2141
|
+
setRenderAsInvalid(!!validateNumber(newValue, rest.required, rest.min, rest.max));
|
|
1627
2142
|
}
|
|
1628
2143
|
onBlur?.(event);
|
|
1629
|
-
}, [errorMessage, onBlur, rest.required]);
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
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
|
-
|
|
2153
|
+
NumberField.displayName = "NumberField";
|
|
1642
2154
|
|
|
1643
|
-
const
|
|
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
|
-
|
|
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
|
|
1651
|
-
|
|
1652
|
-
|
|
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
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
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
|
-
|
|
1689
|
-
true: ["
|
|
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
|
-
|
|
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
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
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
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
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
|
-
*
|
|
2666
|
+
* Serialize week schedule to string schedule
|
|
2483
2667
|
*
|
|
2484
|
-
* @param {
|
|
2485
|
-
* @returns {
|
|
2668
|
+
* @param {WeekSchedule} weekSchedule Week schedule range
|
|
2669
|
+
* @returns {string} Schedule string
|
|
2486
2670
|
*/
|
|
2487
|
-
const
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
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
|
-
|
|
2518
|
-
|
|
2684
|
+
})
|
|
2685
|
+
.map(({ day, range, isAllDay }) => {
|
|
2686
|
+
if (isAllDay) {
|
|
2687
|
+
return `${day}#00:00-24:00`;
|
|
2519
2688
|
}
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
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
|
-
*
|
|
2694
|
+
* Checks if a list of schedule objects have the same ranges
|
|
2538
2695
|
*
|
|
2539
|
-
* @param {
|
|
2540
|
-
* @returns {
|
|
2696
|
+
* @param {RawSchedule[]} schedule List of schedule objects
|
|
2697
|
+
* @returns {boolean} Whether the schedule is uniform
|
|
2541
2698
|
*/
|
|
2542
|
-
const
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
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
|
-
|
|
2606
|
-
|
|
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
|
-
*
|
|
2738
|
+
* The Search component is used to render a search input field.
|
|
2609
2739
|
*
|
|
2610
|
-
* @param {
|
|
2611
|
-
* @returns {ReactElement} Select component
|
|
2740
|
+
* @param {SearchProps} props - The props for the Search component
|
|
2612
2741
|
*/
|
|
2613
|
-
const
|
|
2614
|
-
const {
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
value,
|
|
2618
|
-
|
|
2619
|
-
|
|
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
|
-
|
|
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(
|
|
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,
|
|
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 };
|