@trackunit/react-form-components 1.7.34 → 1.7.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/index.cjs.js +352 -362
  2. package/index.esm.js +348 -356
  3. package/package.json +8 -8
  4. package/src/components/BaseInput/BaseInput.d.ts +1 -1
  5. package/src/components/{DateInput/DateInput.d.ts → DateField/DateBaseInput/DateBaseInput.d.ts} +3 -6
  6. package/src/components/DateField/DateField.d.ts +2 -2
  7. package/src/components/{EmailInput/EmailInput.d.ts → EmailField/EmailBaseInput/EmailBaseInput.d.ts} +5 -8
  8. package/src/components/FormGroup/FormGroup.d.ts +1 -1
  9. package/src/components/NumberField/NumberBaseInput/NumberBaseInput.d.ts +11 -0
  10. package/src/components/NumberField/NumberField.d.ts +2 -2
  11. package/src/components/PasswordField/PasswordBaseInput/PasswordBaseInput.d.ts +12 -0
  12. package/src/components/PasswordField/PasswordField.d.ts +2 -2
  13. package/src/components/{PhoneInput/PhoneInput.d.ts → PhoneField/PhoneBaseInput/PhoneBaseInput.d.ts} +4 -7
  14. package/src/components/PhoneField/PhoneField.d.ts +2 -2
  15. package/src/components/Search/Search.d.ts +2 -2
  16. package/src/components/{TextArea/TextArea.d.ts → TextAreaField/TextArea/TextAreaBaseInput.d.ts} +2 -5
  17. package/src/components/{TextArea/TextArea.variants.d.ts → TextAreaField/TextArea/TextAreaBaseInput.variants.d.ts} +1 -1
  18. package/src/components/TextAreaField/TextAreaField.d.ts +2 -2
  19. package/src/components/TextField/TextBaseInput/TextBaseInput.d.ts +11 -0
  20. package/src/components/TextField/TextField.d.ts +2 -2
  21. package/src/components/{UrlInput/UrlInput.d.ts → UrlField/UrlBaseInput/UrlBaseInput.d.ts} +4 -7
  22. package/src/components/UrlField/UrlField.d.ts +2 -2
  23. package/src/index.d.ts +10 -12
  24. package/src/components/EmailInput/index.d.ts +0 -1
  25. package/src/components/NumberInput/NumberInput.d.ts +0 -14
  26. package/src/components/PasswordField/index.d.ts +0 -1
  27. package/src/components/PasswordInput/PasswordInput.d.ts +0 -15
  28. package/src/components/PhoneInput/index.d.ts +0 -1
  29. package/src/components/TextArea/index.d.ts +0 -1
  30. package/src/components/TextAreaField/index.d.ts +0 -1
  31. package/src/components/TextInput/TextInput.d.ts +0 -14
  32. /package/src/components/{PhoneInput → PhoneField/PhoneBaseInput}/CountryCodeSelect.d.ts +0 -0
  33. /package/src/components/{PhoneInput → PhoneField/PhoneBaseInput}/CountryCodes.d.ts +0 -0
  34. /package/src/components/{PhoneInput → PhoneField/PhoneBaseInput}/PhoneInputValidationUtils.d.ts +0 -0
  35. /package/src/components/{PhoneInput → PhoneField/PhoneBaseInput}/PhoneNumberUtilities.d.ts +0 -0
package/index.esm.js CHANGED
@@ -1,14 +1,14 @@
1
1
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
2
  import { useNamespaceTranslation, registerTranslations, NamespaceTrans } from '@trackunit/i18n-library-translation';
3
+ import { Temporal } from '@js-temporal/polyfill';
3
4
  import { IconButton, Icon, Tooltip, useGeometry, useIsTextTruncated, Text, Heading, Tag, Spinner, MenuItem, useResize, useDebounce, useIsFirstRender } from '@trackunit/react-components';
4
- import { useCopyToClipboard } from 'usehooks-ts';
5
- import { cvaMerge } from '@trackunit/css-class-variance-utilities';
6
5
  import { themeSpacing } from '@trackunit/ui-design-tokens';
7
- import { forwardRef, useRef, useImperativeHandle, useMemo, useState, useCallback, useEffect, cloneElement, createContext, useContext, isValidElement, useLayoutEffect } from 'react';
6
+ import { forwardRef, useRef, useImperativeHandle, useCallback, useEffect, useState, useMemo, cloneElement, createContext, useContext, isValidElement, useLayoutEffect } from 'react';
7
+ import { cvaMerge } from '@trackunit/css-class-variance-utilities';
8
8
  import { titleCase } from 'string-ts';
9
- import { uuidv4, nonNullable } from '@trackunit/shared-utils';
10
- import { Temporal } from '@js-temporal/polyfill';
9
+ import { useCopyToClipboard } from 'usehooks-ts';
11
10
  import parsePhoneNumberFromString, { isSupportedCountry, getCountries, getCountryCallingCode, AsYouType, parseIncompletePhoneNumber, isValidPhoneNumber } from 'libphonenumber-js';
11
+ import { uuidv4, nonNullable } from '@trackunit/shared-utils';
12
12
  import { Controller } from 'react-hook-form';
13
13
  import ReactSelect, { components } from 'react-select';
14
14
  export { default as ValueType } from 'react-select';
@@ -98,82 +98,6 @@ const setupLibraryTranslations = () => {
98
98
  registerTranslations(translations);
99
99
  };
100
100
 
101
- const cvaActionButton = cvaMerge(["drop-shadow-none", "rounded-md"], {
102
- variants: {
103
- size: {
104
- small: ["w-6", "h-6", "min-h-0"],
105
- medium: ["w-6", "h-6", "min-h-0"],
106
- large: ["w-8", "h-8"],
107
- },
108
- },
109
- defaultVariants: {
110
- size: "medium",
111
- },
112
- });
113
- const cvaActionContainer = cvaMerge(["flex", "items-center"], {
114
- variants: {
115
- size: {
116
- //I just measured manually the top/bottom spacing
117
- //when using the action button inside an input
118
- //might need tweaking in the future
119
- small: ["m-[1px]"],
120
- medium: ["m-[3px]"],
121
- large: ["m-[7px]"],
122
- },
123
- },
124
- defaultVariants: {
125
- size: "medium",
126
- },
127
- });
128
-
129
- /**
130
- * The ActionButton component is a wrapper over IconButton to perform an action when the onClick event is triggered.
131
- *
132
- * @param {ActionButtonProps} props - The props for the ActionButton component
133
- * @returns {ReactElement} ActionButton component
134
- */
135
- const ActionButton = ({ type, value, dataTestId, size, disabled, className, onClick }) => {
136
- const [, copyToClipboard] = useCopyToClipboard();
137
- const getIconName = () => {
138
- switch (type) {
139
- case "PHONE_NUMBER":
140
- return "PhoneArrowUpRight";
141
- case "WEB_ADDRESS":
142
- return "ArrowTopRightOnSquare";
143
- case "EMAIL":
144
- return "Envelope";
145
- case "EDIT":
146
- return "Pencil";
147
- case "COPY":
148
- default:
149
- return "ClipboardDocument";
150
- }
151
- };
152
- const buttonAction = () => {
153
- switch (type) {
154
- case "EMAIL":
155
- return window.open(`mailto:${value}`);
156
- case "WEB_ADDRESS":
157
- if (value) {
158
- return window.open(value, "_blank", "noopener,noreferrer");
159
- }
160
- return null;
161
- case "PHONE_NUMBER":
162
- return window.open(`tel:${value}`);
163
- case "EDIT":
164
- return value?.current?.click();
165
- case "COPY":
166
- // Typescript seems to be unable to detect RefObject
167
- // as one of the members of the union RefObject | string | null which gives access to the `current` property
168
- return copyToClipboard(value?.current?.value ?? "");
169
- default:
170
- return null;
171
- }
172
- };
173
- const adjustedIconSize = size === "large" ? "medium" : size;
174
- return (jsx("div", { className: cvaActionContainer({ className, size }), children: jsx(IconButton, { className: cvaActionButton({ size: adjustedIconSize }), dataTestId: dataTestId || "testIconButtonId", disabled: disabled, icon: jsx(Icon, { name: getIconName(), size: adjustedIconSize }), onClick: buttonAction, size: "small", variant: "secondary" }) }));
175
- };
176
-
177
101
  const cvaInputBase = cvaMerge([
178
102
  "component-baseInput-shadow",
179
103
  "component-baseInput-border",
@@ -343,6 +267,82 @@ const AddonRenderer = ({ addon, dataTestId, className, fieldSize, position }) =>
343
267
  return (jsx("div", { className: cvaInputAddon({ size: fieldSize, position, className }), "data-testid": dataTestId ? `${dataTestId}-addon${titleCase(position)}` : null, children: addon }));
344
268
  };
345
269
 
270
+ const cvaActionButton = cvaMerge(["drop-shadow-none", "rounded-md"], {
271
+ variants: {
272
+ size: {
273
+ small: ["w-6", "h-6", "min-h-0"],
274
+ medium: ["w-6", "h-6", "min-h-0"],
275
+ large: ["w-8", "h-8"],
276
+ },
277
+ },
278
+ defaultVariants: {
279
+ size: "medium",
280
+ },
281
+ });
282
+ const cvaActionContainer = cvaMerge(["flex", "items-center"], {
283
+ variants: {
284
+ size: {
285
+ //I just measured manually the top/bottom spacing
286
+ //when using the action button inside an input
287
+ //might need tweaking in the future
288
+ small: ["m-[1px]"],
289
+ medium: ["m-[3px]"],
290
+ large: ["m-[7px]"],
291
+ },
292
+ },
293
+ defaultVariants: {
294
+ size: "medium",
295
+ },
296
+ });
297
+
298
+ /**
299
+ * The ActionButton component is a wrapper over IconButton to perform an action when the onClick event is triggered.
300
+ *
301
+ * @param {ActionButtonProps} props - The props for the ActionButton component
302
+ * @returns {ReactElement} ActionButton component
303
+ */
304
+ const ActionButton = ({ type, value, dataTestId, size, disabled, className, onClick }) => {
305
+ const [, copyToClipboard] = useCopyToClipboard();
306
+ const getIconName = () => {
307
+ switch (type) {
308
+ case "PHONE_NUMBER":
309
+ return "PhoneArrowUpRight";
310
+ case "WEB_ADDRESS":
311
+ return "ArrowTopRightOnSquare";
312
+ case "EMAIL":
313
+ return "Envelope";
314
+ case "EDIT":
315
+ return "Pencil";
316
+ case "COPY":
317
+ default:
318
+ return "ClipboardDocument";
319
+ }
320
+ };
321
+ const buttonAction = () => {
322
+ switch (type) {
323
+ case "EMAIL":
324
+ return window.open(`mailto:${value}`);
325
+ case "WEB_ADDRESS":
326
+ if (value) {
327
+ return window.open(value, "_blank", "noopener,noreferrer");
328
+ }
329
+ return null;
330
+ case "PHONE_NUMBER":
331
+ return window.open(`tel:${value}`);
332
+ case "EDIT":
333
+ return value?.current?.click();
334
+ case "COPY":
335
+ // Typescript seems to be unable to detect RefObject
336
+ // as one of the members of the union RefObject | string | null which gives access to the `current` property
337
+ return copyToClipboard(value?.current?.value ?? "");
338
+ default:
339
+ return null;
340
+ }
341
+ };
342
+ const adjustedIconSize = size === "large" ? "medium" : size;
343
+ return (jsx("div", { className: cvaActionContainer({ className, size }), children: jsx(IconButton, { className: cvaActionButton({ size: adjustedIconSize }), dataTestId: dataTestId || "testIconButtonId", disabled: disabled, icon: jsx(Icon, { name: getIconName(), size: adjustedIconSize }), onClick: buttonAction, size: "small", variant: "secondary" }) }));
344
+ };
345
+
346
346
  const GenericActionsRenderer = ({ genericAction, disabled, fieldSize, innerRef, tooltipLabel, }) => {
347
347
  const [t] = useTranslation();
348
348
  if (!genericAction) {
@@ -412,41 +412,260 @@ const SuffixRenderer = ({ suffix, isInvalid, isWarning, dataTestId, disabled, })
412
412
  };
413
413
 
414
414
  /**
415
- * A base input component that can be used for text inputs, password inputs, etc.
416
- * A reference to the input element is provided as the `ref` prop.
417
- * Extends props from [React.InputHTMLAttributes](https://reactjs.org/docs/dom-elements.html#input)
415
+ * A base input component that can be used for text inputs, password inputs, etc.
416
+ * A reference to the input element is provided as the `ref` prop.
417
+ * Extends props from [React.InputHTMLAttributes](https://reactjs.org/docs/dom-elements.html#input)
418
+ *
419
+ * For specific input types make sure to use the corresponding input component.
420
+ * This is a base used by our other input components such as TextBaseInput, NumberBaseInput, PasswordBaseInput, etc.
421
+ */
422
+ const BaseInput = ({ className, isInvalid, dataTestId, prefix, suffix, addonBefore, addonAfter, actions, fieldSize = "medium", nonInteractive = false, inputClassName, placeholder, isWarning, type, genericAction, style, ref, ...rest }) => {
423
+ // Derive final flags
424
+ const renderAsDisabled = Boolean(rest.disabled);
425
+ const renderAsReadonly = Boolean(rest.readOnly);
426
+ const beforeContainerRef = useRef(null);
427
+ const { width: beforeContainerWidth } = useGeometry(beforeContainerRef);
428
+ const afterContainerRef = useRef(null);
429
+ const { width: afterContainerWidth } = useGeometry(afterContainerRef);
430
+ // Keep a reference to the input element
431
+ const innerRef = useRef(null);
432
+ useImperativeHandle(ref, () => innerRef.current, []);
433
+ return (jsxs("div", { className: cvaInput$1({
434
+ disabled: renderAsDisabled,
435
+ invalid: isInvalid,
436
+ isWarning,
437
+ size: fieldSize,
438
+ className,
439
+ }), "data-testid": dataTestId ? `${dataTestId}-container` : undefined, style: style, children: [jsxs("div", { className: cvaAccessoriesContainer({ className: cvaInputItemPlacementManager({ position: "before" }) }), "data-testid": dataTestId ? `${dataTestId}-before-container` : undefined, ref: beforeContainerRef, children: [jsx(AddonRenderer, { addon: addonBefore, dataTestId: dataTestId, fieldSize: fieldSize, position: "before" }), jsx(PrefixRenderer, { dataTestId: dataTestId, disabled: renderAsDisabled, prefix: prefix, type: type })] }), jsx("input", { "aria-required": rest.required, className: cvaInputField({
440
+ readOnly: renderAsReadonly,
441
+ size: fieldSize,
442
+ disabled: renderAsDisabled,
443
+ className: cvaInputItemPlacementManager({ position: "span", className: inputClassName }),
444
+ }), "data-testid": dataTestId, placeholder: renderAsDisabled ? undefined : placeholder, ref: innerRef, style: {
445
+ paddingLeft: beforeContainerWidth ? `calc(${beforeContainerWidth}px + ${themeSpacing[2]})` : undefined,
446
+ paddingRight: afterContainerWidth ? `calc(${afterContainerWidth}px + ${themeSpacing[2]})` : undefined,
447
+ }, 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" })] })] }));
448
+ };
449
+ BaseInput.displayName = "BaseInput";
450
+
451
+ /**
452
+ * A wrapper around BaseInput with a pop-up day picker.
453
+ *
454
+ * The value is formatted to an ISO date string (YYYY-MM-DD)
455
+ *
456
+ * NOTE: If shown with a label, please use the `DateField` component instead.
457
+ */
458
+ const DateBaseInput = ({ min, max, defaultValue, value, ref, ...rest }) => {
459
+ const formatDateToInputString = (date) => date instanceof Date
460
+ ? Temporal.PlainDateTime.from({
461
+ year: date.getFullYear(),
462
+ month: date.getMonth() + 1,
463
+ day: date.getDate(),
464
+ })
465
+ .toPlainDate()
466
+ .toString()
467
+ : date;
468
+ // Chrome and Firefox need their default icon to have datepicker functionality.
469
+ const showIcon = !/Chrome/.test(navigator.userAgent) && !/Firefox/.test(navigator.userAgent);
470
+ return (jsx(BaseInput, { defaultValue: formatDateToInputString(defaultValue), max: formatDateToInputString(max), min: formatDateToInputString(min), ref: ref, suffix: showIcon ? jsx(Icon, { dataTestId: "calendar", name: "Calendar", size: "medium", type: "solid" }) : null, type: "date", value: formatDateToInputString(value), ...rest }));
471
+ };
472
+
473
+ /**
474
+ * A thin wrapper around the `BaseInput` component for number input fields.
475
+ *
476
+ * NOTE: If shown with a label, please use the `NumberField` component instead.
477
+ */
478
+ const NumberBaseInput = ({ ref, ...rest }) => {
479
+ const inputElementRef = useRef(null);
480
+ const preventDefaultWheelEvent = useCallback((event) => {
481
+ const inputElement = inputElementRef.current;
482
+ const activeElement = document.activeElement;
483
+ if (inputElement && activeElement === inputElement) {
484
+ event.preventDefault();
485
+ }
486
+ }, []);
487
+ const forwardAndStoreInputRef = useCallback((node) => {
488
+ const previousNode = inputElementRef.current;
489
+ if (previousNode) {
490
+ previousNode.removeEventListener("wheel", preventDefaultWheelEvent);
491
+ }
492
+ inputElementRef.current = node;
493
+ if (node) {
494
+ // NOTE: Prevent the default browser behavior of changing the value via mouse wheel
495
+ node.addEventListener("wheel", preventDefaultWheelEvent, { passive: false });
496
+ }
497
+ if (typeof ref === "function") {
498
+ ref(node);
499
+ }
500
+ else if (ref && typeof ref === "object") {
501
+ ref.current = node;
502
+ }
503
+ }, [preventDefaultWheelEvent, ref]);
504
+ useEffect(() => {
505
+ return () => {
506
+ const element = inputElementRef.current;
507
+ if (element) {
508
+ element.removeEventListener("wheel", preventDefaultWheelEvent);
509
+ }
510
+ };
511
+ }, [preventDefaultWheelEvent]);
512
+ return jsx(BaseInput, { ref: forwardAndStoreInputRef, type: "number", ...rest, value: rest.value });
513
+ };
514
+
515
+ /**
516
+ * @param phoneNumber - a phone number as a string
517
+ * @returns {boolean} true if the phone number starts with a plus sign
518
+ * @example checkIfPhoneNumberHasPlus("123456789") // false
519
+ * checkIfPhoneNumberHasPlus("+123456789") // true
520
+ */
521
+ const checkIfPhoneNumberHasPlus = (phoneNumber) => {
522
+ return phoneNumber.startsWith("+");
523
+ };
524
+ /**
525
+ * @param phoneNumber - a phone number as a string
526
+ * @returns {string|number} the phone number with a plus sign in front of it
527
+ * @example getPhoneNumberWithPlus("123456789") // "+123456789"
528
+ */
529
+ const getPhoneNumberWithPlus = (phoneNumber) => {
530
+ const stringPhoneNumber = phoneNumber.toString();
531
+ return checkIfPhoneNumberHasPlus(stringPhoneNumber) ? stringPhoneNumber : `+${stringPhoneNumber}`;
532
+ };
533
+ /**
534
+ * Generates a flag emoji based on the given country code.
535
+ *
536
+ * @param {string} countryCode - The two-letter country code (ISO 3166-1 alpha-2).
537
+ * @returns {string} - The corresponding flag emoji for the given country code.
538
+ * @example getPhoneNumberWithPlus("DK") // "🇩🇰"
539
+ */
540
+ const countryCodeToFlagEmoji = (countryCode) => {
541
+ let code = countryCode;
542
+ if (countryCode.startsWith("+")) {
543
+ code = getCountryAbbreviation(countryCode.substring(1));
544
+ }
545
+ else if (!isNaN(Number(countryCode))) {
546
+ code = getCountryAbbreviation(countryCode);
547
+ }
548
+ return isSupportedCountry(code.toUpperCase())
549
+ ? code.toUpperCase().replace(/./g, char => String.fromCodePoint(127397 + char.charCodeAt(0)))
550
+ : "";
551
+ };
552
+ /**
553
+ * Retrieves the ISO 3166-1 alpha-2 country code associated with a given international calling code.
554
+ *
555
+ * @param {string} callCode - The international calling code for a country.
556
+ * @returns {string} The abbreviation for a country or an empty string if no matching country is found.
557
+ * @example getCountryAbbreviation("45") // "DK"
558
+ * @example getCountryAbbreviation("+45") // "DK"
559
+ */
560
+ const getCountryAbbreviation = (callCode) => {
561
+ let code = callCode;
562
+ if (callCode.startsWith("+")) {
563
+ code = callCode.substring(1);
564
+ }
565
+ return getCountries().find(c => getCountryCallingCode(c) === code) || "";
566
+ };
567
+
568
+ const DEFAULT_COUNTRY_CODE = undefined;
569
+ /**
570
+ * A component for inputting phone numbers with an optional action button for initiating a phone call.
571
+ *
572
+ * @param {string} [dataTestId] - The data test ID for the component.
573
+ * @param {string|number} [value] - The value of the input field. The value should include the country code as well.
574
+ * @param {boolean} [disabled=false] - Whether the component is disabled or not.
575
+ * @param {string} [fieldSize="medium"] - The size of the input field.
576
+ * @param {boolean} [disableAction=false] - Whether the action button is disabled or not.
577
+ */
578
+ const PhoneBaseInput = ({ dataTestId, isInvalid, disabled = false, value, defaultValue, fieldSize = "medium", disableAction = false, onChange, readOnly, onFocus, onBlur, name, ref, ...rest }) => {
579
+ const [innerValue, setInnerValue] = useState(() => {
580
+ return (value?.toString() || defaultValue?.toString()) ?? "";
581
+ });
582
+ const fieldIsFocused = useRef(false);
583
+ const [countryCode, setCountryCode] = useState(DEFAULT_COUNTRY_CODE);
584
+ const determineCountry = useCallback((newValue) => {
585
+ const asYouType = new AsYouType();
586
+ asYouType.input(newValue);
587
+ setCountryCode(asYouType.getCountry());
588
+ }, []);
589
+ const handleChange = useCallback(event => {
590
+ const newValue = event.target.value;
591
+ event.target.value = parseIncompletePhoneNumber(newValue);
592
+ onChange?.(event);
593
+ setInnerValue(newValue);
594
+ determineCountry(newValue);
595
+ }, [onChange, determineCountry]);
596
+ const makePretty = useCallback((newValue) => {
597
+ const asYouType = new AsYouType();
598
+ const pretty = asYouType.input(newValue);
599
+ setInnerValue(pretty);
600
+ setCountryCode(asYouType.getCountry());
601
+ }, []);
602
+ useEffect(() => {
603
+ if (!fieldIsFocused.current) {
604
+ makePretty(typeof value === "string" ? value : "");
605
+ }
606
+ }, [makePretty, value]);
607
+ const handleBlur = useCallback(event => {
608
+ const newValue = event.target.value;
609
+ makePretty(newValue);
610
+ onBlur?.(event);
611
+ fieldIsFocused.current = false;
612
+ }, [makePretty, onBlur]);
613
+ const handleFocus = useCallback(event => {
614
+ const newValue = event.target.value;
615
+ const noneFormattedValue = parseIncompletePhoneNumber(newValue);
616
+ setInnerValue(noneFormattedValue);
617
+ onFocus?.(event);
618
+ fieldIsFocused.current = true;
619
+ }, [onFocus]);
620
+ return (jsx("div", { className: "grid grid-cols-1 gap-2", "data-testid": dataTestId ? `${dataTestId}-container` : null, children: jsx(BaseInput, { actions: !disableAction && innerValue && innerValue.length > 0 ? (jsx(ActionButton, { dataTestId: dataTestId ? `${dataTestId}-phoneIcon` : undefined, disabled: isInvalid, size: fieldSize ?? undefined, type: "PHONE_NUMBER", value: value?.toString() || "" })) : null, dataTestId: dataTestId ? `${dataTestId}-phoneNumberInput` : undefined, disabled: disabled, fieldSize: fieldSize, id: "phoneInput-number", isInvalid: isInvalid, name: name, onBlur: handleBlur, onChange: handleChange, onFocus: handleFocus, prefix: (countryCode && countryCodeToFlagEmoji(countryCode)) || undefined, readOnly: readOnly, ref: ref, type: "tel", value: innerValue, ...rest }) }));
621
+ };
622
+
623
+ const cvaTextAreaBaseInput = cvaMerge([
624
+ cvaInputBase(),
625
+ "block",
626
+ "overflow-auto",
627
+ "appearance-none",
628
+ "px-3",
629
+ "py-2",
630
+ "text-base",
631
+ "text-slate-900",
632
+ "placeholder-slate-400",
633
+ "w-full",
634
+ "h-20",
635
+ "transition",
636
+ ], {
637
+ variants: {
638
+ disabled: {
639
+ true: cvaInputBaseDisabled(),
640
+ false: "",
641
+ },
642
+ invalid: {
643
+ true: cvaInputBaseInvalid(),
644
+ false: "",
645
+ },
646
+ resize: {
647
+ both: "resize",
648
+ vertical: "resize-y",
649
+ horizontal: "resize-x",
650
+ none: "resize-none",
651
+ },
652
+ },
653
+ });
654
+
655
+ /**
656
+ * The TextArea is a base component, and should not be used very often.
657
+ * For most cases the TextAreaField is the correct component.
658
+ */
659
+ const TextAreaBaseInput = ({ id, name, value, rows, disabled, placeholder, readOnly, tabIndex, onChange, onFocus, onBlur, maxLength, resize = "vertical", defaultValue, required, dataTestId, isInvalid, className, ref, ...rest }) => {
660
+ return (jsx("textarea", { className: cvaTextAreaBaseInput({ className, resize, invalid: isInvalid, disabled }), defaultValue: defaultValue, disabled: disabled, id: id, maxLength: maxLength, name: name, onBlur: onBlur, onFocus: onFocus, placeholder: placeholder, readOnly: readOnly, ref: ref, required: required, rows: rows, tabIndex: tabIndex, value: value, ...rest, "data-testid": dataTestId, onChange: onChange }));
661
+ };
662
+
663
+ /**
664
+ * A thin wrapper around the `BaseInput` component for text input fields.
418
665
  *
419
- * For specific input types make sure to use the corresponding input component.
420
- * This is a base used by our other input components such as TextInput, NumberInput, PasswordInput, etc.
666
+ * NOTE: If shown with a label, please use the `TextField` component instead.
421
667
  */
422
- const BaseInput = ({ className, isInvalid, dataTestId, prefix, suffix, addonBefore, addonAfter, actions, fieldSize = "medium", nonInteractive = false, inputClassName, placeholder, isWarning, type, genericAction, style, ref, ...rest }) => {
423
- // Derive final flags
424
- const renderAsDisabled = Boolean(rest.disabled);
425
- const renderAsReadonly = Boolean(rest.readOnly);
426
- const beforeContainerRef = useRef(null);
427
- const { width: beforeContainerWidth } = useGeometry(beforeContainerRef);
428
- const afterContainerRef = useRef(null);
429
- const { width: afterContainerWidth } = useGeometry(afterContainerRef);
430
- // Keep a reference to the input element
431
- const innerRef = useRef(null);
432
- useImperativeHandle(ref, () => innerRef.current, []);
433
- return (jsxs("div", { className: cvaInput$1({
434
- disabled: renderAsDisabled,
435
- invalid: isInvalid,
436
- isWarning,
437
- size: fieldSize,
438
- className,
439
- }), "data-testid": dataTestId ? `${dataTestId}-container` : undefined, style: style, children: [jsxs("div", { className: cvaAccessoriesContainer({ className: cvaInputItemPlacementManager({ position: "before" }) }), "data-testid": dataTestId ? `${dataTestId}-before-container` : undefined, ref: beforeContainerRef, children: [jsx(AddonRenderer, { addon: addonBefore, dataTestId: dataTestId, fieldSize: fieldSize, position: "before" }), jsx(PrefixRenderer, { dataTestId: dataTestId, disabled: renderAsDisabled, prefix: prefix, type: type })] }), jsx("input", { "aria-required": rest.required, className: cvaInputField({
440
- readOnly: renderAsReadonly,
441
- size: fieldSize,
442
- disabled: renderAsDisabled,
443
- className: cvaInputItemPlacementManager({ position: "span", className: inputClassName }),
444
- }), "data-testid": dataTestId, placeholder: renderAsDisabled ? undefined : placeholder, ref: innerRef, style: {
445
- paddingLeft: beforeContainerWidth ? `calc(${beforeContainerWidth}px + ${themeSpacing[2]})` : undefined,
446
- paddingRight: afterContainerWidth ? `calc(${afterContainerWidth}px + ${themeSpacing[2]})` : undefined,
447
- }, 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" })] })] }));
448
- };
449
- BaseInput.displayName = "BaseInput";
668
+ const TextBaseInput = ({ ref, ...rest }) => jsx(BaseInput, { ref: ref, type: "text", ...rest });
450
669
 
451
670
  /**
452
671
  * Shared CVA for binary control items: Checkbox, RadioItem, ToggleSwitchOption
@@ -713,7 +932,7 @@ const FormGroup = ({ isInvalid, isWarning, helpText, helpAddon, tip, className,
713
932
  const color = isInvalid ? "danger" : isWarning ? "warning" : null;
714
933
  return color ? jsx(Icon, { color: color, name: "ExclamationTriangle", size: "small" }) : null;
715
934
  }, [isInvalid, isWarning]);
716
- return (jsxs("div", { className: cvaFormGroup({ className }), "data-testid": dataTestId, children: [jsxs("div", { className: cvaFormGroupContainerBefore(), children: [label ? (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] })) : null, tip ? (jsx(Tooltip, { className: "ml-1", dataTestId: dataTestId ? `${dataTestId}-tooltip` : undefined, label: tip, placement: "bottom" })) : 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] }));
935
+ 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] }));
717
936
  };
718
937
 
719
938
  /**
@@ -839,29 +1058,6 @@ const ColorField = forwardRef(({ label, id, tip, helpText, errorMessage, helpAdd
839
1058
  });
840
1059
  ColorField.displayName = "ColorField";
841
1060
 
842
- /**
843
- * A wrapper around BaseInput with a pop-up day picker.
844
- *
845
- * The value is formatted to an ISO date string (YYYY-MM-DD)
846
- *
847
- * NOTE: If shown with a label, please use the `DateField` component instead.
848
- */
849
- const DateInput = ({ min, max, defaultValue, value, ref, ...rest }) => {
850
- const formatDateToInputString = (date) => date instanceof Date
851
- ? Temporal.PlainDateTime.from({
852
- year: date.getFullYear(),
853
- month: date.getMonth() + 1,
854
- day: date.getDate(),
855
- })
856
- .toPlainDate()
857
- .toString()
858
- : date;
859
- // Chrome and Firefox need their default icon to have datepicker functionality.
860
- const showIcon = !/Chrome/.test(navigator.userAgent) && !/Firefox/.test(navigator.userAgent);
861
- return (jsx(BaseInput, { defaultValue: formatDateToInputString(defaultValue), max: formatDateToInputString(max), min: formatDateToInputString(min), ref: ref, suffix: showIcon ? jsx(Icon, { dataTestId: "calendar", name: "Calendar", size: "medium", type: "solid" }) : null, type: "date", value: formatDateToInputString(value), ...rest }));
862
- };
863
- DateInput.displayName = "DateInput";
864
-
865
1061
  /**
866
1062
  * The date field component is used for entering date values.
867
1063
  *
@@ -872,7 +1068,7 @@ DateInput.displayName = "DateInput";
872
1068
  const DateField = ({ label, id, tip, helpText, errorMessage, helpAddon, isInvalid, className, defaultValue, dataTestId, ref, ...rest }) => {
873
1069
  const renderAsInvalid = isInvalid === undefined ? Boolean(errorMessage) : isInvalid;
874
1070
  const htmlForId = id ? id : "dateField-" + uuidv4();
875
- 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(DateInput, { "aria-labelledby": htmlForId + "-label", defaultValue: defaultValue, id: htmlForId, isInvalid: renderAsInvalid, ref: ref, ...rest, className: className, dataTestId: dataTestId }) }));
1071
+ 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 }) }));
876
1072
  };
877
1073
  DateField.displayName = "DateField";
878
1074
 
@@ -1027,8 +1223,8 @@ const validateEmailId = (emailId, required) => {
1027
1223
  * A reference to the input element is provided as the `ref` prop.
1028
1224
  * For specific input types make sure to use the corresponding input component.
1029
1225
  */
1030
- const EmailInput = ({ fieldSize = "medium", disabled = false, dataTestId, isInvalid = false, onChange, disableAction = false, ref, ...rest }) => {
1031
- const [email, setEmail] = useState("");
1226
+ const EmailBaseInput = ({ fieldSize = "medium", disabled = false, dataTestId, isInvalid = false, onChange, disableAction = false, ref, ...rest }) => {
1227
+ const [email, setEmail] = useState(rest.value?.toString() || rest.defaultValue?.toString());
1032
1228
  const sendEmail = () => {
1033
1229
  return window.open(`mailto:${email}`);
1034
1230
  };
@@ -1040,7 +1236,6 @@ const EmailInput = ({ fieldSize = "medium", disabled = false, dataTestId, isInva
1040
1236
  const renderAsInvalid = (email && !validateEmailAddress(email)) || isInvalid;
1041
1237
  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, isInvalid: renderAsInvalid, onChange: handleChange, placeholder: rest.placeholder || "mail@example.com", ref: ref, type: "email", ...rest }));
1042
1238
  };
1043
- EmailInput.displayName = "EmailInput";
1044
1239
 
1045
1240
  /**
1046
1241
  * The EmailField component is used to enter email.
@@ -1068,7 +1263,7 @@ const EmailField = ({ label, id, tip, helpText, errorMessage, helpAddon, classNa
1068
1263
  onChange(event);
1069
1264
  }
1070
1265
  }, [onChange]);
1071
- 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(EmailInput, { "aria-labelledby": htmlForId + "-label", defaultValue: defaultValue, id: htmlForId, isInvalid: renderAsInvalid, onBlur: handleBlur, onChange: handleChange, ref: ref, value: value, ...rest, className: className, dataTestId: dataTestId }) }));
1266
+ 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 }) }));
1072
1267
  };
1073
1268
  EmailField.displayName = "EmailField";
1074
1269
 
@@ -1122,49 +1317,6 @@ const validateNumber = (number, required = false, min, max) => {
1122
1317
  return "INVALID_NUMBER";
1123
1318
  };
1124
1319
 
1125
- /**
1126
- * A thin wrapper around the `BaseInput` component for number input fields.
1127
- *
1128
- * NOTE: If shown with a label, please use the `NumberField` component instead.
1129
- */
1130
- const NumberInput = ({ ref, ...rest }) => {
1131
- const inputElementRef = useRef(null);
1132
- const preventDefaultWheelEvent = useCallback((event) => {
1133
- const inputElement = inputElementRef.current;
1134
- const activeElement = document.activeElement;
1135
- if (inputElement && activeElement === inputElement) {
1136
- event.preventDefault();
1137
- }
1138
- }, []);
1139
- const forwardAndStoreInputRef = useCallback((node) => {
1140
- const previousNode = inputElementRef.current;
1141
- if (previousNode) {
1142
- previousNode.removeEventListener("wheel", preventDefaultWheelEvent);
1143
- }
1144
- inputElementRef.current = node;
1145
- if (node) {
1146
- // NOTE: Prevent the default browser behavior of changing the value via mouse wheel
1147
- node.addEventListener("wheel", preventDefaultWheelEvent, { passive: false });
1148
- }
1149
- if (typeof ref === "function") {
1150
- ref(node);
1151
- }
1152
- else if (ref && typeof ref === "object") {
1153
- ref.current = node;
1154
- }
1155
- }, [preventDefaultWheelEvent, ref]);
1156
- useEffect(() => {
1157
- return () => {
1158
- const element = inputElementRef.current;
1159
- if (element) {
1160
- element.removeEventListener("wheel", preventDefaultWheelEvent);
1161
- }
1162
- };
1163
- }, [preventDefaultWheelEvent]);
1164
- return jsx(BaseInput, { ref: forwardAndStoreInputRef, type: "number", ...rest, value: rest.value });
1165
- };
1166
- NumberInput.displayName = "NumberInput";
1167
-
1168
1320
  /**
1169
1321
  * The number field component is used for entering numeric values and includes controls for incrementally increasing or decreasing the value.
1170
1322
  *
@@ -1214,7 +1366,7 @@ const NumberField = ({ label, id, tip, helpText, errorMessage, helpAddon, isInva
1214
1366
  onChange(event);
1215
1367
  }
1216
1368
  }, [onChange]);
1217
- 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(NumberInput, { "aria-labelledby": htmlForId + "-label", id: htmlForId, isInvalid: renderAsInvalid, maxLength: maxLength, onBlur: handleBlur, onChange: handleChange, ref: ref, value: value, ...rest, className: className, dataTestId: dataTestId }) }));
1369
+ 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 }) }));
1218
1370
  };
1219
1371
  NumberField.displayName = "NumberField";
1220
1372
 
@@ -1328,11 +1480,10 @@ OptionCard.displayName = "OptionCard";
1328
1480
  *
1329
1481
  * NOTE: If shown with a label, please use the `PasswordField` component instead.
1330
1482
  */
1331
- const PasswordInput = ({ ref, fieldSize, ...rest }) => {
1483
+ const PasswordBaseInput = ({ ref, fieldSize, ...rest }) => {
1332
1484
  const [showPassword, setShowPassword] = useState(false);
1333
1485
  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" }));
1334
1486
  };
1335
- PasswordInput.displayName = "PasswordInput";
1336
1487
 
1337
1488
  /**
1338
1489
  * Password fields enter a password or other confidential information. Characters are masked as they are typed.
@@ -1347,119 +1498,10 @@ const PasswordField = ({ id, label, tip, helpText, helpAddon, errorMessage, isIn
1347
1498
  const handleChange = useCallback((event) => {
1348
1499
  onChange?.(event);
1349
1500
  }, [onChange]);
1350
- 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(PasswordInput, { ...rest, "aria-labelledby": htmlFor + "-label", className: className, dataTestId: dataTestId, disabled: rest.readOnly, id: htmlFor, isInvalid: renderAsInvalid, maxLength: maxLength, onChange: handleChange, ref: ref, value: value }) }));
1501
+ 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 }) }));
1351
1502
  };
1352
1503
  PasswordField.displayName = "PasswordField";
1353
1504
 
1354
- /**
1355
- * @param phoneNumber - a phone number as a string
1356
- * @returns {boolean} true if the phone number starts with a plus sign
1357
- * @example checkIfPhoneNumberHasPlus("123456789") // false
1358
- * checkIfPhoneNumberHasPlus("+123456789") // true
1359
- */
1360
- const checkIfPhoneNumberHasPlus = (phoneNumber) => {
1361
- return phoneNumber.startsWith("+");
1362
- };
1363
- /**
1364
- * @param phoneNumber - a phone number as a string
1365
- * @returns {string|number} the phone number with a plus sign in front of it
1366
- * @example getPhoneNumberWithPlus("123456789") // "+123456789"
1367
- */
1368
- const getPhoneNumberWithPlus = (phoneNumber) => {
1369
- const stringPhoneNumber = phoneNumber.toString();
1370
- return checkIfPhoneNumberHasPlus(stringPhoneNumber) ? stringPhoneNumber : `+${stringPhoneNumber}`;
1371
- };
1372
- /**
1373
- * Generates a flag emoji based on the given country code.
1374
- *
1375
- * @param {string} countryCode - The two-letter country code (ISO 3166-1 alpha-2).
1376
- * @returns {string} - The corresponding flag emoji for the given country code.
1377
- * @example getPhoneNumberWithPlus("DK") // "🇩🇰"
1378
- */
1379
- const countryCodeToFlagEmoji = (countryCode) => {
1380
- let code = countryCode;
1381
- if (countryCode.startsWith("+")) {
1382
- code = getCountryAbbreviation(countryCode.substring(1));
1383
- }
1384
- else if (!isNaN(Number(countryCode))) {
1385
- code = getCountryAbbreviation(countryCode);
1386
- }
1387
- return isSupportedCountry(code.toUpperCase())
1388
- ? code.toUpperCase().replace(/./g, char => String.fromCodePoint(127397 + char.charCodeAt(0)))
1389
- : "";
1390
- };
1391
- /**
1392
- * Retrieves the ISO 3166-1 alpha-2 country code associated with a given international calling code.
1393
- *
1394
- * @param {string} callCode - The international calling code for a country.
1395
- * @returns {string} The abbreviation for a country or an empty string if no matching country is found.
1396
- * @example getCountryAbbreviation("45") // "DK"
1397
- * @example getCountryAbbreviation("+45") // "DK"
1398
- */
1399
- const getCountryAbbreviation = (callCode) => {
1400
- let code = callCode;
1401
- if (callCode.startsWith("+")) {
1402
- code = callCode.substring(1);
1403
- }
1404
- return getCountries().find(c => getCountryCallingCode(c) === code) || "";
1405
- };
1406
-
1407
- const DEFAULT_COUNTRY_CODE = undefined;
1408
- /**
1409
- * A component for inputting phone numbers with an optional action button for initiating a phone call.
1410
- *
1411
- * @param {string} [dataTestId] - The data test ID for the component.
1412
- * @param {string|number} [value] - The value of the input field. The value should include the country code as well.
1413
- * @param {boolean} [disabled=false] - Whether the component is disabled or not.
1414
- * @param {string} [fieldSize="medium"] - The size of the input field.
1415
- * @param {boolean} [disableAction=false] - Whether the action button is disabled or not.
1416
- */
1417
- const PhoneInput = ({ dataTestId, isInvalid, disabled = false, value, defaultValue, fieldSize = "medium", disableAction = false, onChange, readOnly, onFocus, onBlur, name, ref, ...rest }) => {
1418
- const [innerValue, setInnerValue] = useState(() => {
1419
- return (value?.toString() || defaultValue?.toString()) ?? "";
1420
- });
1421
- const fieldIsFocused = useRef(false);
1422
- const [countryCode, setCountryCode] = useState(DEFAULT_COUNTRY_CODE);
1423
- const determineCountry = useCallback((newValue) => {
1424
- const asYouType = new AsYouType();
1425
- asYouType.input(newValue);
1426
- setCountryCode(asYouType.getCountry());
1427
- }, []);
1428
- const handleChange = useCallback(event => {
1429
- const newValue = event.target.value;
1430
- event.target.value = parseIncompletePhoneNumber(newValue);
1431
- onChange?.(event);
1432
- setInnerValue(newValue);
1433
- determineCountry(newValue);
1434
- }, [onChange, determineCountry]);
1435
- const makePretty = useCallback((newValue) => {
1436
- const asYouType = new AsYouType();
1437
- const pretty = asYouType.input(newValue);
1438
- setInnerValue(pretty);
1439
- setCountryCode(asYouType.getCountry());
1440
- }, []);
1441
- useEffect(() => {
1442
- if (!fieldIsFocused.current) {
1443
- makePretty(typeof value === "string" ? value : "");
1444
- }
1445
- }, [makePretty, value]);
1446
- const handleBlur = useCallback(event => {
1447
- const newValue = event.target.value;
1448
- makePretty(newValue);
1449
- onBlur?.(event);
1450
- fieldIsFocused.current = false;
1451
- }, [makePretty, onBlur]);
1452
- const handleFocus = useCallback(event => {
1453
- const newValue = event.target.value;
1454
- const noneFormattedValue = parseIncompletePhoneNumber(newValue);
1455
- setInnerValue(noneFormattedValue);
1456
- onFocus?.(event);
1457
- fieldIsFocused.current = true;
1458
- }, [onFocus]);
1459
- return (jsx("div", { className: "grid grid-cols-1 gap-2", "data-testid": dataTestId ? `${dataTestId}-container` : null, children: jsx(BaseInput, { actions: !disableAction && innerValue && innerValue.length > 0 ? (jsx(ActionButton, { dataTestId: dataTestId ? `${dataTestId}-phoneIcon` : undefined, disabled: isInvalid, size: fieldSize ?? undefined, type: "PHONE_NUMBER", value: value?.toString() || "" })) : null, dataTestId: dataTestId ? `${dataTestId}-phoneNumberInput` : undefined, disabled: disabled, fieldSize: fieldSize, id: "phoneInput-number", isInvalid: isInvalid, name: name, onBlur: handleBlur, onChange: handleChange, onFocus: handleFocus, prefix: (countryCode && countryCodeToFlagEmoji(countryCode)) || undefined, readOnly: readOnly, ref: ref, type: "tel", value: innerValue, ...rest }) }));
1460
- };
1461
- PhoneInput.displayName = "PhoneInput";
1462
-
1463
1505
  /**
1464
1506
  * Validates a phone number
1465
1507
  */
@@ -1559,7 +1601,7 @@ const PhoneField = ({ label, id, tip, helpText, isInvalid, errorMessage, value,
1559
1601
  }
1560
1602
  onBlur?.(event);
1561
1603
  }, [errorMessage, onBlur, rest.required]);
1562
- 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(PhoneInput, { "aria-labelledby": htmlForId + "-label", dataTestId: dataTestId, defaultValue: defaultValue, id: htmlForId, isInvalid: renderAsInvalid, name: name, onBlur: handleBlur, ref: ref, value: value, ...rest }) }));
1604
+ 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 }) }));
1563
1605
  };
1564
1606
  PhoneField.displayName = "PhoneField";
1565
1607
 
@@ -1900,14 +1942,6 @@ const hasConsecutiveDays = (schedule) => {
1900
1942
  return schedule.every(({ day }, index) => day === days[index]);
1901
1943
  };
1902
1944
 
1903
- /**
1904
- * A thin wrapper around the `BaseInput` component for text input fields.
1905
- *
1906
- * NOTE: If shown with a label, please use the `TextField` component instead.
1907
- */
1908
- const TextInput = ({ ref, ...rest }) => jsx(BaseInput, { ref: ref, type: "text", ...rest });
1909
- TextInput.displayName = "TextInput";
1910
-
1911
1945
  const cvaSearch = cvaMerge([
1912
1946
  "shadow-none",
1913
1947
  "component-search-border",
@@ -1939,7 +1973,7 @@ const cvaSearch = cvaMerge([
1939
1973
  */
1940
1974
  const Search = ({ className, placeholder, value, widenInputOnFocus, hideBorderWhenNotInFocus = false, disabled, onKeyUp, onChange, onFocus, onBlur, name, onClear, dataTestId, autoComplete = "on", loading, inputClassName, iconName = "MagnifyingGlass", style, xMarkRef, ref, ...rest }) => {
1941
1975
  const { t } = useTranslation();
1942
- return (jsx(TextInput, { ...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:
1976
+ 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:
1943
1977
  //only show the clear button if there is a value and the onClear function is provided
1944
1978
  onClear && value ? (jsx("button", { className: "flex", "data-testid": dataTestId ? `${dataTestId}_suffix_component` : null, onClick: () => {
1945
1979
  onClear();
@@ -2705,47 +2739,6 @@ const SelectField = ({ ref, ...props }) => {
2705
2739
  };
2706
2740
  SelectField.displayName = "SelectField";
2707
2741
 
2708
- const cvaTextArea = cvaMerge([
2709
- cvaInputBase(),
2710
- "block",
2711
- "overflow-auto",
2712
- "appearance-none",
2713
- "px-3",
2714
- "py-2",
2715
- "text-base",
2716
- "text-slate-900",
2717
- "placeholder-slate-400",
2718
- "w-full",
2719
- "h-20",
2720
- "transition",
2721
- ], {
2722
- variants: {
2723
- disabled: {
2724
- true: cvaInputBaseDisabled(),
2725
- false: "",
2726
- },
2727
- invalid: {
2728
- true: cvaInputBaseInvalid(),
2729
- false: "",
2730
- },
2731
- resize: {
2732
- both: "resize",
2733
- vertical: "resize-y",
2734
- horizontal: "resize-x",
2735
- none: "resize-none",
2736
- },
2737
- },
2738
- });
2739
-
2740
- /**
2741
- * The TextArea is a base component, and should not be used very often.
2742
- * For most cases the TextAreaField is the correct component.
2743
- */
2744
- const TextArea = ({ id, name, value, rows, disabled, placeholder, readOnly, tabIndex, onChange, onFocus, onBlur, maxLength, resize = "vertical", defaultValue, required, dataTestId, isInvalid, className, ref, ...rest }) => {
2745
- return (jsx("textarea", { className: cvaTextArea({ className, resize, invalid: isInvalid, disabled }), defaultValue: defaultValue, disabled: disabled, id: id, maxLength: maxLength, name: name, onBlur: onBlur, onFocus: onFocus, placeholder: placeholder, readOnly: readOnly, ref: ref, required: required, rows: rows, tabIndex: tabIndex, value: value, ...rest, "data-testid": dataTestId, onChange: onChange }));
2746
- };
2747
- TextArea.displayName = "TextArea";
2748
-
2749
2742
  /**
2750
2743
  * The TextLengthIndicator component shows a `{length}/{maxLength}` label.
2751
2744
  * Used for TextFields to communicate a maximum allowed input length.
@@ -2771,7 +2764,7 @@ const TextAreaField = ({ id, label, tip, helpText, helpAddon, errorMessage, isIn
2771
2764
  }
2772
2765
  }, [onChange]);
2773
2766
  return (jsx(FormGroup, { className: twMerge(className, "grid", "grid-rows-[auto_1fr_auto]"), dataTestId: dataTestId ? `${dataTestId}-FormGroup` : undefined, helpAddon: helpAddon ||
2774
- (typeof maxLength === "number" && jsx(TextLengthIndicator, { length: valueLength, maxLength: maxLength })), helpText: errorMessage || helpText, htmlFor: htmlForId, isInvalid: renderAsInvalid, label: label, required: rest.required ? !(rest.disabled || rest.readOnly) : false, tip: tip, children: jsx(TextArea, { "aria-labelledby": htmlForId + "-label", id: htmlForId, isInvalid: renderAsInvalid, maxLength: maxLength, ref: ref, value: value, ...rest, className: "h-auto", dataTestId: dataTestId, onChange: handleChange }) }));
2767
+ (typeof maxLength === "number" && jsx(TextLengthIndicator, { length: valueLength, maxLength: maxLength })), helpText: errorMessage || helpText, htmlFor: htmlForId, isInvalid: renderAsInvalid, label: label, required: rest.required ? !(rest.disabled || rest.readOnly) : false, tip: tip, children: jsx(TextAreaBaseInput, { "aria-labelledby": htmlForId + "-label", id: htmlForId, isInvalid: renderAsInvalid, maxLength: maxLength, ref: ref, value: value, ...rest, className: "h-auto", dataTestId: dataTestId, onChange: handleChange }) }));
2775
2768
  };
2776
2769
  TextAreaField.displayName = "TextAreaField";
2777
2770
 
@@ -2789,7 +2782,7 @@ const TextField = ({ id, label, tip, helpText, helpAddon, errorMessage, isInvali
2789
2782
  }
2790
2783
  }, [onChange]);
2791
2784
  return (jsx(FormGroup, { dataTestId: dataTestId ? `${dataTestId}-FormGroup` : undefined, helpAddon: helpAddon ||
2792
- (typeof maxLength === "number" && jsx(TextLengthIndicator, { length: valueLength, maxLength: maxLength })), helpText: (renderAsInvalid && errorMessage) || helpText, htmlFor: htmlFor, isInvalid: renderAsInvalid, isWarning: isWarning, label: label, required: rest.required ? !(rest.disabled || rest.readOnly) : false, tip: tip, children: jsx(TextInput, { "aria-labelledby": htmlFor + "-label", id: htmlFor, isInvalid: renderAsInvalid, isWarning: isWarning, maxLength: maxLength, ref: ref, value: value, ...rest, className: className, dataTestId: dataTestId, onChange: handleChange }) }));
2785
+ (typeof maxLength === "number" && jsx(TextLengthIndicator, { length: valueLength, maxLength: maxLength })), helpText: (renderAsInvalid && errorMessage) || helpText, htmlFor: htmlFor, isInvalid: renderAsInvalid, isWarning: isWarning, label: label, required: rest.required ? !(rest.disabled || rest.readOnly) : false, tip: tip, children: jsx(TextBaseInput, { "aria-labelledby": htmlFor + "-label", id: htmlFor, isInvalid: renderAsInvalid, isWarning: isWarning, maxLength: maxLength, ref: ref, value: value, ...rest, className: className, dataTestId: dataTestId, onChange: handleChange }) }));
2793
2786
  };
2794
2787
  TextField.displayName = "TextField";
2795
2788
 
@@ -3013,12 +3006,11 @@ const validateUrl = (url, required) => {
3013
3006
  *
3014
3007
  * NOTE: If shown with a label, please use the `UrlField` component instead.
3015
3008
  */
3016
- const UrlInput = ({ dataTestId, isInvalid, disabled = false, fieldSize = "medium", disableAction = false, value, defaultValue, ref, ...rest }) => {
3009
+ const UrlBaseInput = ({ dataTestId, isInvalid, disabled = false, fieldSize = "medium", disableAction = false, value, defaultValue, ref, ...rest }) => {
3017
3010
  const [url, setUrl] = useState(value?.toString() || defaultValue?.toString());
3018
3011
  const renderAsInvalid = (url && typeof url === "string" && !validateUrlAddress(url)) || isInvalid;
3019
3012
  return (jsx(BaseInput, { dataTestId: dataTestId ? `${dataTestId}-url-input` : undefined, disabled: disabled, id: "url-input", isInvalid: renderAsInvalid, onChange: e => setUrl(e.target.value), placeholder: rest.placeholder || "https://www.example.com", ref: ref, type: "url", value: url, ...rest, actions: !disableAction && (jsx(ActionButton, { dataTestId: (dataTestId && `${dataTestId}-url-input-Icon`) || "url-input-action-icon", disabled: renderAsInvalid || Boolean(disabled) || disableAction, size: fieldSize ?? undefined, type: "WEB_ADDRESS", value: url })) }));
3020
3013
  };
3021
- UrlInput.displayName = "UrlField";
3022
3014
 
3023
3015
  /**
3024
3016
  * The UrlField component is used to enter url.
@@ -3040,7 +3032,7 @@ const UrlField = ({ label, id, tip, helpText, errorMessage, helpAddon, className
3040
3032
  setRenderAsInvalid(!!validateUrl(newValue, rest.required));
3041
3033
  onBlur?.(event);
3042
3034
  }, [onBlur, rest.required]);
3043
- 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(UrlInput, { "aria-labelledby": htmlForId + "-label", id: htmlForId, isInvalid: renderAsInvalid, onBlur: handleBlur, ref: ref, value: value || defaultValue, ...rest, className: className, dataTestId: dataTestId }) }));
3035
+ 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(UrlBaseInput, { "aria-labelledby": htmlForId + "-label", id: htmlForId, isInvalid: renderAsInvalid, onBlur: handleBlur, ref: ref, value: value || defaultValue, ...rest, className: className, dataTestId: dataTestId }) }));
3044
3036
  };
3045
3037
  UrlField.displayName = "UrlField";
3046
3038
 
@@ -3156,4 +3148,4 @@ const useZodValidators = () => {
3156
3148
  */
3157
3149
  setupLibraryTranslations();
3158
3150
 
3159
- export { ActionButton, BaseInput, Checkbox, CheckboxField, ColorField, CreatableSelect, CreatableSelectField, DEFAULT_TIME, DateField, DateInput, DropZone, DropZoneDefaultLabel, EMAIL_REGEX, EmailField, EmailInput, FormFieldSelectAdapter, FormGroup, Label, MultiSelectMenuItem, NumberField, NumberInput, OptionCard, PasswordField, PasswordInput, PhoneField, PhoneFieldWithController, PhoneInput, RadioGroup, RadioItem, Schedule, ScheduleVariant, Search, Select, SelectField, SingleSelectMenuItem, TextArea, TextAreaField, TextField, TextInput, TimeRange, TimeRangeField, ToggleSwitch, ToggleSwitchOption, UploadField, UploadInput, UrlField, UrlInput, checkIfPhoneNumberHasPlus, countryCodeToFlagEmoji, cvaAccessoriesContainer, cvaActionButton, cvaActionContainer, cvaInput$1 as cvaInput, cvaInputAddon, cvaInputBase, cvaInputBaseDisabled, cvaInputBaseInvalid, cvaInputField, cvaInputItemPlacementManager, cvaInputPrefix, cvaInputSuffix, cvaLabel, cvaSelect, cvaSelectControl, cvaSelectCounter, cvaSelectDynamicTagContainer, cvaSelectIcon, cvaSelectMenu, cvaSelectMenuList, cvaSelectPrefixSuffix, cvaSelectXIcon, getCountryAbbreviation, getPhoneNumberWithPlus, isInvalidCountryCode, isInvalidPhoneNumber, isValidHEXColor, parseSchedule, phoneErrorMessage, serializeSchedule, useCustomComponents, useGetPhoneValidationRules, usePhoneInput, useZodValidators, validateEmailAddress, validatePhoneNumber, weekDay };
3151
+ export { ActionButton, BaseInput, Checkbox, CheckboxField, ColorField, CreatableSelect, CreatableSelectField, DEFAULT_TIME, DateBaseInput, DateField, DropZone, DropZoneDefaultLabel, EMAIL_REGEX, EmailField, FormFieldSelectAdapter, FormGroup, Label, MultiSelectMenuItem, NumberBaseInput, NumberField, OptionCard, PasswordBaseInput, PasswordField, PhoneBaseInput, PhoneField, PhoneFieldWithController, RadioGroup, RadioItem, Schedule, ScheduleVariant, Search, Select, SelectField, SingleSelectMenuItem, TextAreaBaseInput, TextAreaField, TextBaseInput, TextField, TimeRange, TimeRangeField, ToggleSwitch, ToggleSwitchOption, UploadField, UploadInput, UrlField, checkIfPhoneNumberHasPlus, countryCodeToFlagEmoji, cvaAccessoriesContainer, cvaActionButton, cvaActionContainer, cvaInput$1 as cvaInput, cvaInputAddon, cvaInputBase, cvaInputBaseDisabled, cvaInputBaseInvalid, cvaInputField, cvaInputItemPlacementManager, cvaInputPrefix, cvaInputSuffix, cvaLabel, cvaSelect, cvaSelectControl, cvaSelectCounter, cvaSelectDynamicTagContainer, cvaSelectIcon, cvaSelectMenu, cvaSelectMenuList, cvaSelectPrefixSuffix, cvaSelectXIcon, getCountryAbbreviation, getPhoneNumberWithPlus, isInvalidCountryCode, isInvalidPhoneNumber, isValidHEXColor, parseSchedule, phoneErrorMessage, serializeSchedule, useCustomComponents, useGetPhoneValidationRules, usePhoneInput, useZodValidators, validateEmailAddress, validatePhoneNumber, weekDay };