@clubmed/trident-ui 2.0.0-beta.67 → 2.0.0-beta.68
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/package.json
CHANGED
package/ui/forms/PhoneField.js
CHANGED
|
@@ -6,8 +6,7 @@ import "@clubmed/trident-icons";
|
|
|
6
6
|
import { jsx as i } from "react/jsx-runtime";
|
|
7
7
|
//#region lib/ui/forms/PhoneField.tsx
|
|
8
8
|
var a = (a) => {
|
|
9
|
-
let { id: o, name: s, value: c, label: l, className: u, description: d, mode: f = "full", dataTestId: p = "PhoneField", disabled: m, required: h, hideRequiredStar: g, validationStatus: _, errorMessage: v, onChange: y, ...b } = a, x = y, S = r()
|
|
10
|
-
return /* @__PURE__ */ i(e, {
|
|
9
|
+
let { id: o, name: s, value: c, label: l, className: u, description: d, mode: f = "full", dataTestId: p = "PhoneField", disabled: m, required: h, hideRequiredStar: g, validationStatus: _, errorMessage: v, onChange: y, ...b } = a, x = y, S = r(), C = {
|
|
11
10
|
id: o || S,
|
|
12
11
|
label: l,
|
|
13
12
|
className: u,
|
|
@@ -18,9 +17,12 @@ var a = (a) => {
|
|
|
18
17
|
required: h,
|
|
19
18
|
hideRequiredStar: g,
|
|
20
19
|
validationStatus: _,
|
|
21
|
-
errorMessage: v
|
|
20
|
+
errorMessage: v
|
|
21
|
+
};
|
|
22
|
+
return /* @__PURE__ */ i(e, {
|
|
23
|
+
...C,
|
|
22
24
|
children: i(f === "full" ? t : n, {
|
|
23
|
-
id:
|
|
25
|
+
id: C.id,
|
|
24
26
|
name: s,
|
|
25
27
|
value: c,
|
|
26
28
|
disabled: m,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PhoneField.js","names":[],"sources":["../../../lib/ui/forms/PhoneField.tsx"],"sourcesContent":["import { type IconicTypes } from '@clubmed/trident-icons';\nimport { FormControl, type FormControlProps } from './FormControl';\nimport { PhoneFieldFullInput, type PhoneFieldFullProps } from './PhoneFieldFullInput';\nimport { PhoneFieldSplitInput, type PhoneFieldSplitProps } from './PhoneFieldSplitInput';\nimport { useId } from 'react';\n\nexport interface PhoneValue {\n full: string;\n prefix?: string;\n number?: string;\n raw: string;\n}\n\nexport type PhoneFieldProps<Value = PhoneValue> = FormControlProps<Value> &\n PhoneFieldFullProps &\n PhoneFieldSplitProps & {\n mode?: 'full' | 'split';\n pattern?: string;\n iconType?: IconicTypes;\n placeholder?: string;\n };\n\nexport const PhoneField = <Value = PhoneValue,>(props: PhoneFieldProps<Value>) => {\n const {\n id,\n name,\n value,\n label,\n className,\n description,\n mode = 'full',\n dataTestId = 'PhoneField',\n disabled,\n required,\n hideRequiredStar,\n validationStatus,\n errorMessage,\n onChange,\n ...rest\n } = props;\n const onPhoneChange = onChange as ((event: Event, value: PhoneValue) => void) | undefined;\n const internalId = useId();\n\n const formControlProps = {\n id: id || internalId,\n label,\n className,\n description,\n dataName: 'PhoneField',\n dataTestId,\n disabled,\n required,\n hideRequiredStar,\n validationStatus,\n errorMessage,\n };\n\n return (\n <FormControl {...formControlProps}>\n {mode === 'full' ? (\n <PhoneFieldFullInput\n id={id}\n name={name}\n value={value as PhoneValue | undefined}\n disabled={disabled}\n required={required}\n validationStatus={validationStatus}\n onChange={onPhoneChange}\n {...rest}\n />\n ) : (\n <PhoneFieldSplitInput\n id={id}\n name={name}\n value={value as PhoneValue | undefined}\n disabled={disabled}\n required={required}\n validationStatus={validationStatus}\n onChange={onPhoneChange}\n {...rest}\n />\n )}\n </FormControl>\n );\n};\n"],"mappings":";;;;;;;AAsBA,IAAa,KAAmC,MAAkC;CAChF,IAAM,EACJ,OACA,SACA,UACA,UACA,cACA,gBACA,UAAO,QACP,gBAAa,cACb,aACA,aACA,qBACA,qBACA,iBACA,aACA,GAAG,MACD,GACE,IAAgB,GAChB,IAAa,GAAO
|
|
1
|
+
{"version":3,"file":"PhoneField.js","names":[],"sources":["../../../lib/ui/forms/PhoneField.tsx"],"sourcesContent":["import { type IconicTypes } from '@clubmed/trident-icons';\nimport { FormControl, type FormControlProps } from './FormControl';\nimport { PhoneFieldFullInput, type PhoneFieldFullProps } from './PhoneFieldFullInput';\nimport { PhoneFieldSplitInput, type PhoneFieldSplitProps } from './PhoneFieldSplitInput';\nimport { useId } from 'react';\n\nexport interface PhoneValue {\n full: string;\n prefix?: string;\n number?: string;\n raw: string;\n}\n\nexport type PhoneFieldProps<Value = PhoneValue> = FormControlProps<Value> &\n PhoneFieldFullProps &\n PhoneFieldSplitProps & {\n mode?: 'full' | 'split';\n pattern?: string;\n iconType?: IconicTypes;\n placeholder?: string;\n };\n\nexport const PhoneField = <Value = PhoneValue,>(props: PhoneFieldProps<Value>) => {\n const {\n id,\n name,\n value,\n label,\n className,\n description,\n mode = 'full',\n dataTestId = 'PhoneField',\n disabled,\n required,\n hideRequiredStar,\n validationStatus,\n errorMessage,\n onChange,\n ...rest\n } = props;\n const onPhoneChange = onChange as ((event: Event, value: PhoneValue) => void) | undefined;\n const internalId = useId();\n\n const formControlProps = {\n id: id || internalId,\n label,\n className,\n description,\n dataName: 'PhoneField',\n dataTestId,\n disabled,\n required,\n hideRequiredStar,\n validationStatus,\n errorMessage,\n };\n\n return (\n <FormControl {...formControlProps}>\n {mode === 'full' ? (\n <PhoneFieldFullInput\n id={formControlProps.id}\n name={name}\n value={value as PhoneValue | undefined}\n disabled={disabled}\n required={required}\n validationStatus={validationStatus}\n onChange={onPhoneChange}\n {...rest}\n />\n ) : (\n <PhoneFieldSplitInput\n id={formControlProps.id}\n name={name}\n value={value as PhoneValue | undefined}\n disabled={disabled}\n required={required}\n validationStatus={validationStatus}\n onChange={onPhoneChange}\n {...rest}\n />\n )}\n </FormControl>\n );\n};\n"],"mappings":";;;;;;;AAsBA,IAAa,KAAmC,MAAkC;CAChF,IAAM,EACJ,OACA,SACA,UACA,UACA,cACA,gBACA,UAAO,QACP,gBAAa,cACb,aACA,aACA,qBACA,qBACA,iBACA,aACA,GAAG,MACD,GACE,IAAgB,GAChB,IAAa,GAAO,EAEpB,IAAmB;EACvB,IAAI,KAAM;EACV;EACA;EACA;EACA,UAAU;EACV;EACA;EACA;EACA;EACA;EACA;EACD;AAED,QACE,kBAAC,GAAD;EAAa,GAAI;YAEb,EADD,MAAS,SACP,IAWA,GAXD;GACE,IAAI,EAAiB;GACf;GACC;GACG;GACA;GACQ;GAClB,UAAU;GACV,GAAI;GACJ,CAWA;EAEQ,CAAA"}
|
|
@@ -128,7 +128,8 @@ var _ = (e) => {
|
|
|
128
128
|
"border-red": w === "error",
|
|
129
129
|
"border-green": w === "success"
|
|
130
130
|
}),
|
|
131
|
-
"aria-label": `${C}-number
|
|
131
|
+
"aria-label": `${C}-number`,
|
|
132
|
+
id: S
|
|
132
133
|
}), /* @__PURE__ */ h("div", {
|
|
133
134
|
className: e("pointer-events-none absolute inset-0 flex items-center justify-end px-20 py-12", {
|
|
134
135
|
"text-grey": w === "disabled",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PhoneFieldSplitInput.js","names":[],"sources":["../../../lib/ui/forms/PhoneFieldSplitInput.tsx"],"sourcesContent":["import clsx from 'clsx';\nimport { Icon, type IconicTypes } from '@clubmed/trident-icons';\nimport {\n type ChangeEvent,\n type InputHTMLAttributes,\n type KeyboardEvent,\n useCallback,\n useEffect,\n useId,\n useRef,\n useState,\n} from 'react';\nimport {\n calculateCursorPosition,\n DEFAULT_PHONE_PREFIXES,\n extractDigits,\n formatPhoneDigits,\n formatWithPattern,\n getLiteralPrefixInfo,\n type PhonePrefix,\n stripLiteralPrefix,\n} from '../helpers/phone';\nimport { useInternalStatus } from '../hooks/useInternalStatus';\nimport type { FormControlProps } from './FormControl';\nimport type { PhoneValue } from './PhoneField';\n\ntype PhoneFieldChangeHandler = (event: Event, value: PhoneValue) => void;\n\nexport interface PhoneFieldSplitProps {\n prefixes?: PhonePrefix[];\n defaultPrefix?: string;\n}\n\nconst usePhoneField = (args: {\n externalValue?: PhoneValue;\n pattern: string;\n defaultPrefix: string;\n onChange?: PhoneFieldChangeHandler;\n}) => {\n const { externalValue, pattern, defaultPrefix, onChange } = args;\n const numberInputRef = useRef<HTMLInputElement>(null);\n const cursorPositionRef = useRef<number | null>(null);\n const [prefix, setPrefix] = useState(defaultPrefix);\n const [number, setNumber] = useState('');\n\n const emitChange = useCallback(\n (event: Event, nextPrefix: string, nextNumber: string) => {\n if (nextPrefix === prefix && nextNumber === number) {\n return;\n }\n\n const full = `${nextPrefix} ${nextNumber}`.trim();\n\n onChange?.(event, {\n full,\n prefix: nextPrefix,\n number: nextNumber,\n raw: extractDigits(full),\n });\n },\n [number, onChange, prefix],\n );\n\n useEffect(() => {\n if (cursorPositionRef.current !== null && numberInputRef.current) {\n numberInputRef.current.setSelectionRange(\n cursorPositionRef.current,\n cursorPositionRef.current,\n );\n cursorPositionRef.current = null;\n }\n }, [number]);\n\n useEffect(() => {\n if (externalValue && typeof externalValue === 'object' && 'full' in externalValue) {\n setPrefix(externalValue.prefix || defaultPrefix);\n setNumber(externalValue.number || '');\n }\n }, [externalValue, defaultPrefix]);\n\n const onKeyDown = useCallback(\n (e: KeyboardEvent<HTMLInputElement>) => {\n if (e.key !== 'Backspace' && e.key !== 'Delete') {\n return;\n }\n\n const cursorPosition = e.currentTarget.selectionStart || 0;\n const selectionEnd = e.currentTarget.selectionEnd || 0;\n\n if (cursorPosition !== selectionEnd) {\n return;\n }\n\n const { literalPrefixDigits } = getLiteralPrefixInfo(pattern);\n\n const targetChar =\n e.key === 'Backspace' ? number[cursorPosition - 1] : number[cursorPosition];\n\n if (!targetChar || /\\d/.test(targetChar)) {\n return;\n }\n\n e.preventDefault();\n\n const allDigits = stripLiteralPrefix(extractDigits(number), literalPrefixDigits);\n const digitsBeforeCursor = stripLiteralPrefix(\n extractDigits(number.substring(0, cursorPosition)),\n literalPrefixDigits,\n ).length;\n\n let newDigits: string;\n let targetCursorDigits: number;\n\n if (e.key === 'Backspace') {\n if (digitsBeforeCursor === 0) {\n return;\n }\n\n newDigits =\n allDigits.slice(0, digitsBeforeCursor - 1) + allDigits.slice(digitsBeforeCursor);\n targetCursorDigits = digitsBeforeCursor - 1;\n } else {\n if (digitsBeforeCursor >= allDigits.length) {\n return;\n }\n\n newDigits =\n allDigits.slice(0, digitsBeforeCursor) + allDigits.slice(digitsBeforeCursor + 1);\n targetCursorDigits = digitsBeforeCursor;\n }\n\n const formatted = formatWithPattern(newDigits, pattern);\n cursorPositionRef.current = calculateCursorPosition(\n targetCursorDigits,\n formatted,\n literalPrefixDigits,\n );\n\n setNumber(formatted);\n\n emitChange(e.nativeEvent, prefix, formatted);\n },\n [emitChange, pattern, number, prefix],\n );\n\n const onInputChange = useCallback(\n (e: ChangeEvent<HTMLInputElement>) => {\n const formattedValue = formatPhoneDigits(\n e.target.value,\n pattern,\n e.target.selectionStart || 0,\n );\n if (!formattedValue) return;\n\n cursorPositionRef.current = formattedValue.nextCursor;\n setNumber(formattedValue.formatted);\n\n emitChange(e.nativeEvent, prefix, formattedValue.formatted);\n },\n [emitChange, pattern, prefix],\n );\n\n const onPrefixChange = useCallback(\n (event: Event, newPrefix: string) => {\n setPrefix(newPrefix);\n\n emitChange(event, newPrefix, number);\n },\n [emitChange, number],\n );\n\n return { numberInputRef, prefix, number, onKeyDown, onInputChange, onPrefixChange };\n};\n\nexport interface PhoneFieldSplitInputProps\n extends Omit<InputHTMLAttributes<HTMLInputElement>, 'value' | 'onChange' | 'pattern'> {\n value?: PhoneValue;\n pattern?: string;\n prefixes?: PhoneFieldSplitProps['prefixes'];\n defaultPrefix?: PhoneFieldSplitProps['defaultPrefix'];\n iconType?: IconicTypes;\n validationStatus?: FormControlProps<PhoneValue>['validationStatus'];\n onChange?: (event: Event, value: PhoneValue) => void;\n}\n\nexport const PhoneFieldSplitInput = ({\n id,\n name,\n value: externalValue,\n pattern = '## ## ## ## ##',\n prefixes = DEFAULT_PHONE_PREFIXES,\n defaultPrefix = prefixes[0]?.code || '+1',\n placeholder = '',\n iconType,\n disabled = false,\n required = false,\n validationStatus = 'default',\n onChange,\n ...rest\n}: PhoneFieldSplitInputProps) => {\n const internalId = useId();\n const resolvedId = id ?? internalId;\n const resolvedName = name ?? resolvedId;\n\n const internalStatus = useInternalStatus({ isDisabled: disabled, validationStatus });\n const { numberInputRef, prefix, number, onKeyDown, onInputChange, onPrefixChange } =\n usePhoneField({\n externalValue,\n pattern,\n defaultPrefix,\n onChange,\n });\n\n return (\n <div className=\"flex gap-8\">\n <div\n className={clsx('relative rounded-pill z-0 flex-shrink-0', {\n 'bg-white': internalStatus !== 'disabled',\n 'bg-pearl border-middleGrey': internalStatus === 'disabled',\n })}\n >\n {/* Real select — invisible but interactive, uses fullLabel for dropdown options */}\n <select\n disabled={disabled}\n value={prefix}\n onChange={(e) => onPrefixChange(e.nativeEvent, e.target.value)}\n className=\"peer opacity-0 absolute inset-0 w-full h-full cursor-pointer outline-none\"\n id={`${resolvedId}-prefix`}\n aria-label={`${resolvedName}-prefix`}\n >\n {(prefixes as PhonePrefix[]).map((p) => (\n <option key={p.code} value={p.code}>\n {p.fullLabel ?? p.label ?? p.code}\n </option>\n ))}\n </select>\n\n {/* Visible fake select showing the short label */}\n <div\n aria-hidden=\"true\"\n className={clsx(\n 'text-b3 rounded-pill w-full border px-20 py-12 font-semibold pointer-events-none flex items-center justify-between gap-8 peer-focus-visible:ring-4 peer-focus-visible:ring-black peer-focus-visible:ring-offset-2',\n {\n 'border-middleGrey': internalStatus === 'default',\n 'text-black bg-white': internalStatus !== 'disabled',\n 'bg-pearl border-middleGrey text-black': internalStatus === 'disabled',\n 'border-red': internalStatus === 'error',\n 'border-green': internalStatus === 'success',\n },\n )}\n >\n <span>{(prefixes as PhonePrefix[]).find((p) => p.code === prefix)?.label ?? prefix}</span>\n <Icon name=\"ArrowDefaultDown\" type=\"svg\" width=\"24px\" color=\"black\" />\n </div>\n </div>\n\n <div className=\"relative flex-1\">\n <input\n {...(rest as Record<string, unknown>)}\n ref={numberInputRef}\n type=\"tel\"\n disabled={disabled}\n required={required}\n value={number}\n onChange={onInputChange}\n onKeyDown={onKeyDown}\n placeholder={placeholder}\n className={clsx(\n 'text-b3 rounded-pill w-full border overflow-hidden px-20 py-12 font-normal outline-none focus-visible:ring-4 focus-visible:ring-black focus-visible:ring-offset-2',\n {\n 'border-middleGrey focus:border-black active:border-black':\n internalStatus === 'default',\n 'pe-[52px]': internalStatus === 'error' || internalStatus === 'success',\n 'bg-white text-black': internalStatus !== 'disabled',\n 'bg-pearl border-middleGrey': internalStatus === 'disabled',\n 'border-red': internalStatus === 'error',\n 'border-green': internalStatus === 'success',\n },\n )}\n aria-label={`${resolvedName}-number`}\n />\n\n <div\n className={clsx(\n 'pointer-events-none absolute inset-0 flex items-center justify-end px-20 py-12',\n {\n 'text-grey': internalStatus === 'disabled',\n 'text-red': internalStatus === 'error',\n 'text-green': internalStatus === 'success',\n },\n )}\n >\n <span className=\"flex gap-x-8\">\n {internalStatus === 'error' && (\n <Icon name=\"CrossDefault\" width=\"24px\" type={iconType} />\n )}\n {internalStatus === 'success' && (\n <Icon name=\"CheckDefault\" width=\"24px\" type={iconType} />\n )}\n </span>\n </div>\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"PhoneFieldSplitInput.js","names":[],"sources":["../../../lib/ui/forms/PhoneFieldSplitInput.tsx"],"sourcesContent":["import clsx from 'clsx';\nimport { Icon, type IconicTypes } from '@clubmed/trident-icons';\nimport {\n type ChangeEvent,\n type InputHTMLAttributes,\n type KeyboardEvent,\n useCallback,\n useEffect,\n useId,\n useRef,\n useState,\n} from 'react';\nimport {\n calculateCursorPosition,\n DEFAULT_PHONE_PREFIXES,\n extractDigits,\n formatPhoneDigits,\n formatWithPattern,\n getLiteralPrefixInfo,\n type PhonePrefix,\n stripLiteralPrefix,\n} from '../helpers/phone';\nimport { useInternalStatus } from '../hooks/useInternalStatus';\nimport type { FormControlProps } from './FormControl';\nimport type { PhoneValue } from './PhoneField';\n\ntype PhoneFieldChangeHandler = (event: Event, value: PhoneValue) => void;\n\nexport interface PhoneFieldSplitProps {\n prefixes?: PhonePrefix[];\n defaultPrefix?: string;\n}\n\nconst usePhoneField = (args: {\n externalValue?: PhoneValue;\n pattern: string;\n defaultPrefix: string;\n onChange?: PhoneFieldChangeHandler;\n}) => {\n const { externalValue, pattern, defaultPrefix, onChange } = args;\n const numberInputRef = useRef<HTMLInputElement>(null);\n const cursorPositionRef = useRef<number | null>(null);\n const [prefix, setPrefix] = useState(defaultPrefix);\n const [number, setNumber] = useState('');\n\n const emitChange = useCallback(\n (event: Event, nextPrefix: string, nextNumber: string) => {\n if (nextPrefix === prefix && nextNumber === number) {\n return;\n }\n\n const full = `${nextPrefix} ${nextNumber}`.trim();\n\n onChange?.(event, {\n full,\n prefix: nextPrefix,\n number: nextNumber,\n raw: extractDigits(full),\n });\n },\n [number, onChange, prefix],\n );\n\n useEffect(() => {\n if (cursorPositionRef.current !== null && numberInputRef.current) {\n numberInputRef.current.setSelectionRange(\n cursorPositionRef.current,\n cursorPositionRef.current,\n );\n cursorPositionRef.current = null;\n }\n }, [number]);\n\n useEffect(() => {\n if (externalValue && typeof externalValue === 'object' && 'full' in externalValue) {\n setPrefix(externalValue.prefix || defaultPrefix);\n setNumber(externalValue.number || '');\n }\n }, [externalValue, defaultPrefix]);\n\n const onKeyDown = useCallback(\n (e: KeyboardEvent<HTMLInputElement>) => {\n if (e.key !== 'Backspace' && e.key !== 'Delete') {\n return;\n }\n\n const cursorPosition = e.currentTarget.selectionStart || 0;\n const selectionEnd = e.currentTarget.selectionEnd || 0;\n\n if (cursorPosition !== selectionEnd) {\n return;\n }\n\n const { literalPrefixDigits } = getLiteralPrefixInfo(pattern);\n\n const targetChar =\n e.key === 'Backspace' ? number[cursorPosition - 1] : number[cursorPosition];\n\n if (!targetChar || /\\d/.test(targetChar)) {\n return;\n }\n\n e.preventDefault();\n\n const allDigits = stripLiteralPrefix(extractDigits(number), literalPrefixDigits);\n const digitsBeforeCursor = stripLiteralPrefix(\n extractDigits(number.substring(0, cursorPosition)),\n literalPrefixDigits,\n ).length;\n\n let newDigits: string;\n let targetCursorDigits: number;\n\n if (e.key === 'Backspace') {\n if (digitsBeforeCursor === 0) {\n return;\n }\n\n newDigits =\n allDigits.slice(0, digitsBeforeCursor - 1) + allDigits.slice(digitsBeforeCursor);\n targetCursorDigits = digitsBeforeCursor - 1;\n } else {\n if (digitsBeforeCursor >= allDigits.length) {\n return;\n }\n\n newDigits =\n allDigits.slice(0, digitsBeforeCursor) + allDigits.slice(digitsBeforeCursor + 1);\n targetCursorDigits = digitsBeforeCursor;\n }\n\n const formatted = formatWithPattern(newDigits, pattern);\n cursorPositionRef.current = calculateCursorPosition(\n targetCursorDigits,\n formatted,\n literalPrefixDigits,\n );\n\n setNumber(formatted);\n\n emitChange(e.nativeEvent, prefix, formatted);\n },\n [emitChange, pattern, number, prefix],\n );\n\n const onInputChange = useCallback(\n (e: ChangeEvent<HTMLInputElement>) => {\n const formattedValue = formatPhoneDigits(\n e.target.value,\n pattern,\n e.target.selectionStart || 0,\n );\n if (!formattedValue) return;\n\n cursorPositionRef.current = formattedValue.nextCursor;\n setNumber(formattedValue.formatted);\n\n emitChange(e.nativeEvent, prefix, formattedValue.formatted);\n },\n [emitChange, pattern, prefix],\n );\n\n const onPrefixChange = useCallback(\n (event: Event, newPrefix: string) => {\n setPrefix(newPrefix);\n\n emitChange(event, newPrefix, number);\n },\n [emitChange, number],\n );\n\n return { numberInputRef, prefix, number, onKeyDown, onInputChange, onPrefixChange };\n};\n\nexport interface PhoneFieldSplitInputProps\n extends Omit<InputHTMLAttributes<HTMLInputElement>, 'value' | 'onChange' | 'pattern'> {\n value?: PhoneValue;\n pattern?: string;\n prefixes?: PhoneFieldSplitProps['prefixes'];\n defaultPrefix?: PhoneFieldSplitProps['defaultPrefix'];\n iconType?: IconicTypes;\n validationStatus?: FormControlProps<PhoneValue>['validationStatus'];\n onChange?: (event: Event, value: PhoneValue) => void;\n}\n\nexport const PhoneFieldSplitInput = ({\n id,\n name,\n value: externalValue,\n pattern = '## ## ## ## ##',\n prefixes = DEFAULT_PHONE_PREFIXES,\n defaultPrefix = prefixes[0]?.code || '+1',\n placeholder = '',\n iconType,\n disabled = false,\n required = false,\n validationStatus = 'default',\n onChange,\n ...rest\n}: PhoneFieldSplitInputProps) => {\n const internalId = useId();\n const resolvedId = id ?? internalId;\n const resolvedName = name ?? resolvedId;\n\n const internalStatus = useInternalStatus({ isDisabled: disabled, validationStatus });\n const { numberInputRef, prefix, number, onKeyDown, onInputChange, onPrefixChange } =\n usePhoneField({\n externalValue,\n pattern,\n defaultPrefix,\n onChange,\n });\n\n return (\n <div className=\"flex gap-8\">\n <div\n className={clsx('relative rounded-pill z-0 flex-shrink-0', {\n 'bg-white': internalStatus !== 'disabled',\n 'bg-pearl border-middleGrey': internalStatus === 'disabled',\n })}\n >\n {/* Real select — invisible but interactive, uses fullLabel for dropdown options */}\n <select\n disabled={disabled}\n value={prefix}\n onChange={(e) => onPrefixChange(e.nativeEvent, e.target.value)}\n className=\"peer opacity-0 absolute inset-0 w-full h-full cursor-pointer outline-none\"\n id={`${resolvedId}-prefix`}\n aria-label={`${resolvedName}-prefix`}\n >\n {(prefixes as PhonePrefix[]).map((p) => (\n <option key={p.code} value={p.code}>\n {p.fullLabel ?? p.label ?? p.code}\n </option>\n ))}\n </select>\n\n {/* Visible fake select showing the short label */}\n <div\n aria-hidden=\"true\"\n className={clsx(\n 'text-b3 rounded-pill w-full border px-20 py-12 font-semibold pointer-events-none flex items-center justify-between gap-8 peer-focus-visible:ring-4 peer-focus-visible:ring-black peer-focus-visible:ring-offset-2',\n {\n 'border-middleGrey': internalStatus === 'default',\n 'text-black bg-white': internalStatus !== 'disabled',\n 'bg-pearl border-middleGrey text-black': internalStatus === 'disabled',\n 'border-red': internalStatus === 'error',\n 'border-green': internalStatus === 'success',\n },\n )}\n >\n <span>{(prefixes as PhonePrefix[]).find((p) => p.code === prefix)?.label ?? prefix}</span>\n <Icon name=\"ArrowDefaultDown\" type=\"svg\" width=\"24px\" color=\"black\" />\n </div>\n </div>\n\n <div className=\"relative flex-1\">\n <input\n {...(rest as Record<string, unknown>)}\n ref={numberInputRef}\n type=\"tel\"\n disabled={disabled}\n required={required}\n value={number}\n onChange={onInputChange}\n onKeyDown={onKeyDown}\n placeholder={placeholder}\n className={clsx(\n 'text-b3 rounded-pill w-full border overflow-hidden px-20 py-12 font-normal outline-none focus-visible:ring-4 focus-visible:ring-black focus-visible:ring-offset-2',\n {\n 'border-middleGrey focus:border-black active:border-black':\n internalStatus === 'default',\n 'pe-[52px]': internalStatus === 'error' || internalStatus === 'success',\n 'bg-white text-black': internalStatus !== 'disabled',\n 'bg-pearl border-middleGrey': internalStatus === 'disabled',\n 'border-red': internalStatus === 'error',\n 'border-green': internalStatus === 'success',\n },\n )}\n aria-label={`${resolvedName}-number`}\n id={resolvedId}\n />\n\n <div\n className={clsx(\n 'pointer-events-none absolute inset-0 flex items-center justify-end px-20 py-12',\n {\n 'text-grey': internalStatus === 'disabled',\n 'text-red': internalStatus === 'error',\n 'text-green': internalStatus === 'success',\n },\n )}\n >\n <span className=\"flex gap-x-8\">\n {internalStatus === 'error' && (\n <Icon name=\"CrossDefault\" width=\"24px\" type={iconType} />\n )}\n {internalStatus === 'success' && (\n <Icon name=\"CheckDefault\" width=\"24px\" type={iconType} />\n )}\n </span>\n </div>\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
|