@spear-ai/spectral 1.21.0 → 1.21.1
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/dist/Checkbox.js +3 -3
- package/dist/Checkbox.js.map +1 -1
- package/dist/Combobox.d.ts.map +1 -1
- package/dist/Combobox.js +2 -2
- package/dist/Combobox.js.map +1 -1
- package/dist/DateTimePicker/Calendar.d.ts +13 -1
- package/dist/DateTimePicker/Calendar.d.ts.map +1 -1
- package/dist/DateTimePicker/Calendar.js +11 -4
- package/dist/DateTimePicker/Calendar.js.map +1 -1
- package/dist/DateTimePicker/DateTimeDisplayInput.d.ts +1 -1
- package/dist/DateTimePicker/DateTimeDisplayInput.d.ts.map +1 -1
- package/dist/DateTimePicker/DateTimeDisplayInput.js +3 -3
- package/dist/DateTimePicker/DateTimeDisplayInput.js.map +1 -1
- package/dist/DateTimePicker.d.ts +13 -2
- package/dist/DateTimePicker.d.ts.map +1 -1
- package/dist/DateTimePicker.js +9 -5
- package/dist/DateTimePicker.js.map +1 -1
- package/dist/Input.d.ts +1 -1
- package/dist/Input.d.ts.map +1 -1
- package/dist/Input.js +24 -17
- package/dist/Input.js.map +1 -1
- package/dist/InputOTP.d.ts +2 -0
- package/dist/InputOTP.d.ts.map +1 -1
- package/dist/InputOTP.js +52 -40
- package/dist/InputOTP.js.map +1 -1
- package/dist/InputSearch.d.ts.map +1 -1
- package/dist/InputSearch.js +3 -2
- package/dist/InputSearch.js.map +1 -1
- package/dist/MultiSelect/MultiSelectBase.js +5 -4
- package/dist/MultiSelect/MultiSelectBase.js.map +1 -1
- package/dist/RadialMenu.d.ts.map +1 -1
- package/dist/RadialMenu.js.map +1 -1
- package/dist/RadioButtonGroup/RadioButtonGroupBase.d.ts +1 -1
- package/dist/RadioButtonGroup/RadioButtonGroupBase.d.ts.map +1 -1
- package/dist/RadioButtonGroup/RadioButtonGroupBase.js +3 -3
- package/dist/RadioButtonGroup/RadioButtonGroupBase.js.map +1 -1
- package/dist/RadioButtonGroup.d.ts +1 -1
- package/dist/RadioGroup.d.ts +1 -1
- package/dist/RadioGroup.d.ts.map +1 -1
- package/dist/RadioGroup.js +3 -3
- package/dist/RadioGroup.js.map +1 -1
- package/dist/Select.d.ts.map +1 -1
- package/dist/Select.js +3 -3
- package/dist/Select.js.map +1 -1
- package/dist/Slider.js +1 -1
- package/dist/Slider.js.map +1 -1
- package/dist/Switch.d.ts +2 -2
- package/dist/Switch.d.ts.map +1 -1
- package/dist/Switch.js +4 -4
- package/dist/Switch.js.map +1 -1
- package/dist/Textarea.d.ts +3 -3
- package/dist/Textarea.d.ts.map +1 -1
- package/dist/Textarea.js +4 -3
- package/dist/Textarea.js.map +1 -1
- package/dist/primitives/input.d.ts.map +1 -1
- package/dist/primitives/input.js +2 -0
- package/dist/primitives/input.js.map +1 -1
- package/dist/primitives/textarea.d.ts.map +1 -1
- package/dist/primitives/textarea.js +2 -0
- package/dist/primitives/textarea.js.map +1 -1
- package/dist/styles/spectral.css +1 -1
- package/dist/utils/formFieldUtils.d.ts +9 -1
- package/dist/utils/formFieldUtils.d.ts.map +1 -1
- package/dist/utils/formFieldUtils.js +19 -2
- package/dist/utils/formFieldUtils.js.map +1 -1
- package/package.json +1 -1
package/dist/Input.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ type InputProps = Omit<InputHTMLAttributes<HTMLInputElement>, 'id' | 'onChange'>
|
|
|
8
8
|
className?: string;
|
|
9
9
|
clearOnFocus?: boolean;
|
|
10
10
|
endIcon?: ReactElement;
|
|
11
|
+
hideStateIcon?: boolean;
|
|
11
12
|
labelClassName?: string;
|
|
12
13
|
onBlur?: (e: FocusEvent<HTMLInputElement>) => void;
|
|
13
14
|
onChange?: (value: string) => void;
|
|
@@ -15,7 +16,6 @@ type InputProps = Omit<InputHTMLAttributes<HTMLInputElement>, 'id' | 'onChange'>
|
|
|
15
16
|
placeholder?: string;
|
|
16
17
|
prefix?: string;
|
|
17
18
|
showClearButton?: boolean;
|
|
18
|
-
showStateIcon?: boolean;
|
|
19
19
|
startIcon?: ReactElement;
|
|
20
20
|
suppressHydrationWarning?: boolean;
|
|
21
21
|
type?: InputType;
|
package/dist/Input.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Input.d.ts","names":[],"sources":["../src/components/Input/Input.tsx"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"Input.d.ts","names":[],"sources":["../src/components/Input/Input.tsx"],"mappings":";;;;;KAoBY,SAAA;AAAA,KAEA,UAAA,GAAa,IAAA,CAAK,mBAAA,CAAoB,gBAAA,wBAChD,kBAAA;EACE,SAAA;EACA,YAAA;EACA,OAAA,GAAU,YAAA;EACV,aAAA;EACA,cAAA;EACA,MAAA,IAAU,CAAA,EAAG,UAAA,CAAW,gBAAA;EACxB,QAAA,IAAY,KAAA;EACZ,OAAA,IAAW,CAAA,EAAG,UAAA,CAAW,gBAAA;EACzB,WAAA;EACA,MAAA;EACA,eAAA;EACA,SAAA,GAAY,YAAA;EACZ,wBAAA;EACA,IAAA,GAAO,SAAA;EACP,KAAA;EACA,cAAA,GAAiB,kBAAA;AAAA;AAAA,uBAoCnB,QAAA,EAAU,UAAA;EACR,GAAA,GAAM,GAAA,CAAI,gBAAA;AAAA,IAEX,YAAA;AAAA"}
|
package/dist/Input.js
CHANGED
|
@@ -8,7 +8,7 @@ import { LoaderIcon } from "./Icons/LoaderIcon.js";
|
|
|
8
8
|
import { WarningIcon } from "./Icons/WarningIcon.js";
|
|
9
9
|
import { cn } from "./utils/twUtils.js";
|
|
10
10
|
import { ErrorMessage, WarningMessage } from "./FormFieldMessage.js";
|
|
11
|
-
import { getAriaProps, getFormFieldCSSProperties, getInputClasses, useFormFieldId, useFormFieldState } from "./utils/formFieldUtils.js";
|
|
11
|
+
import { getAriaProps, getErrorMessageId, getFormFieldCSSProperties, getInputClasses, getPasswordManagerIgnoreProps, getWarningMessageId, useFormFieldId, useFormFieldState } from "./utils/formFieldUtils.js";
|
|
12
12
|
import { useUncontrolledState } from "./hooks/useUncontrolledState.js";
|
|
13
13
|
import { Label } from "./Label.js";
|
|
14
14
|
import { useClearOnFocus, usePasswordVisibility, usePrefixWidth } from "./Input/InputUtils.js";
|
|
@@ -25,6 +25,10 @@ const mergeRefs = (...refs) => {
|
|
|
25
25
|
});
|
|
26
26
|
};
|
|
27
27
|
};
|
|
28
|
+
const iconBaseClasses = "absolute top-1/2 -translate-y-1/2";
|
|
29
|
+
const endIconClasses = cn(iconBaseClasses, "right-3 text-input-icon");
|
|
30
|
+
const startIconClasses = cn(iconBaseClasses, "left-3 text-input-icon");
|
|
31
|
+
const iconButtonClasses = cn(endIconClasses, "aspect-square max-h-6 rounded-sm hover:text-input-icon--hover cursor-pointer focus:outline-none focus-visible:outline-2 focus-visible:outline-accent focus-visible:outline-offset-1");
|
|
28
32
|
const getAutoCompleteValue = (type) => {
|
|
29
33
|
return {
|
|
30
34
|
date: "off",
|
|
@@ -38,10 +42,10 @@ const getAutoCompleteValue = (type) => {
|
|
|
38
42
|
}[type] || "off";
|
|
39
43
|
};
|
|
40
44
|
const Input = (allProps) => {
|
|
41
|
-
const { className, clearOnFocus = false, defaultValue, disabled, endIcon, errorMessage, id, label, labelClassName, messageReserveLines = 1, messageReserveSpace = false, name, onBlur, onChange, onFocus, placeholder, prefix, ref, required, showClearButton = false,
|
|
45
|
+
const { className, clearOnFocus = false, defaultValue, disabled, endIcon, errorMessage, hideStateIcon = false, id, label, labelClassName, messageReserveLines = 1, messageReserveSpace = false, name, onBlur, onChange, onFocus, placeholder, prefix, ref, required, showClearButton = false, startIcon, state = "default", suppressHydrationWarning = true, type = "text", value: valueProp, warningMessage, autoComplete, "aria-label": ariaLabel, "aria-describedby": ariaDescribedBy, ...props } = allProps;
|
|
42
46
|
const inputId = useFormFieldId(id, name);
|
|
43
|
-
const errorMessageId =
|
|
44
|
-
const warningMessageId =
|
|
47
|
+
const errorMessageId = getErrorMessageId(inputId);
|
|
48
|
+
const warningMessageId = getWarningMessageId(inputId);
|
|
45
49
|
const { isDisabled, isLoading, isInvalid } = useFormFieldState(disabled, state);
|
|
46
50
|
const ariaProps = getAriaProps(state, ariaDescribedBy, required, state === "error" ? errorMessageId : state === "warning" && warningMessage ? warningMessageId : void 0);
|
|
47
51
|
const [value, setValue] = useUncontrolledState({
|
|
@@ -73,60 +77,62 @@ const Input = (allProps) => {
|
|
|
73
77
|
}, [setValue]);
|
|
74
78
|
const showClearButtonNow = showClearButton && value.length > 0;
|
|
75
79
|
const getEndIcon = () => {
|
|
76
|
-
const iconClasses = "absolute right-4 top-1/2 -translate-y-1/2 text-input-icon hover:text-input-icon--hover focus:outline-none cursor-pointer";
|
|
77
80
|
const iconComponents = {
|
|
78
81
|
password: () => /* @__PURE__ */ jsx("button", {
|
|
79
82
|
"aria-controls": inputId,
|
|
80
83
|
"aria-label": isVisible ? `Hide ${label ?? "password"}` : `Show ${label ?? "password"}`,
|
|
81
84
|
"aria-pressed": isVisible,
|
|
82
|
-
className:
|
|
85
|
+
className: iconButtonClasses,
|
|
83
86
|
onClick: toggleVisibility,
|
|
84
87
|
type: "button",
|
|
85
88
|
children: isVisible ? /* @__PURE__ */ jsx(EyeClosedIcon, { size: 22 }) : /* @__PURE__ */ jsx(EyeOpenIcon, { size: 22 })
|
|
86
89
|
}),
|
|
87
90
|
clear: () => /* @__PURE__ */ jsx("button", {
|
|
88
91
|
"aria-label": String(`Clear ${label ?? "input"}`),
|
|
89
|
-
className:
|
|
92
|
+
className: iconButtonClasses,
|
|
90
93
|
onClick: handleClear,
|
|
91
94
|
type: "button",
|
|
92
|
-
children: /* @__PURE__ */ jsx(CloseCircleIcon, { size:
|
|
95
|
+
children: /* @__PURE__ */ jsx(CloseCircleIcon, { size: 22 })
|
|
93
96
|
}),
|
|
94
97
|
loading: () => /* @__PURE__ */ jsx("div", {
|
|
95
|
-
className:
|
|
98
|
+
className: endIconClasses,
|
|
96
99
|
children: /* @__PURE__ */ jsx(LoaderIcon, { size: 24 })
|
|
97
100
|
}),
|
|
98
101
|
error: () => /* @__PURE__ */ jsx("div", {
|
|
99
|
-
className: "right-
|
|
102
|
+
className: cn(iconBaseClasses, "right-3 text-danger-400"),
|
|
100
103
|
children: /* @__PURE__ */ jsx(ErrorIcon, { size: 24 })
|
|
101
104
|
}),
|
|
102
105
|
success: () => /* @__PURE__ */ jsx("div", {
|
|
103
|
-
className: "right-
|
|
106
|
+
className: cn(iconBaseClasses, "right-3 text-success-400"),
|
|
104
107
|
children: /* @__PURE__ */ jsx(CheckCircleIcon, { size: 24 })
|
|
105
108
|
}),
|
|
106
109
|
warning: () => /* @__PURE__ */ jsx("div", {
|
|
107
|
-
className: "right-
|
|
110
|
+
className: cn(iconBaseClasses, "right-3 text-warning-400"),
|
|
108
111
|
children: /* @__PURE__ */ jsx(WarningIcon, { size: 24 })
|
|
109
112
|
})
|
|
110
113
|
};
|
|
111
114
|
if (endIcon) return /* @__PURE__ */ jsx("div", {
|
|
112
|
-
className:
|
|
115
|
+
className: endIconClasses,
|
|
113
116
|
children: endIcon
|
|
114
117
|
});
|
|
115
118
|
if (type === "password") return iconComponents.password();
|
|
116
119
|
if (showClearButtonNow) return iconComponents.clear();
|
|
117
120
|
if (isLoading) return iconComponents.loading();
|
|
118
|
-
if (
|
|
121
|
+
if (hideStateIcon && (state === "success" || state === "warning" || state === "error")) return null;
|
|
119
122
|
if (state === "success") return iconComponents.success();
|
|
120
123
|
if (state === "warning") return iconComponents.warning();
|
|
121
124
|
if (state === "error") return iconComponents.error();
|
|
122
125
|
return null;
|
|
123
126
|
};
|
|
124
127
|
const getStartIcon = () => {
|
|
125
|
-
if (startIcon) return
|
|
128
|
+
if (startIcon) return /* @__PURE__ */ jsx("div", {
|
|
129
|
+
className: startIconClasses,
|
|
130
|
+
children: startIcon
|
|
131
|
+
});
|
|
126
132
|
return null;
|
|
127
133
|
};
|
|
128
134
|
const usesTabularNumbers = type === "number" || type === "date" || type === "datetime-local";
|
|
129
|
-
const inputClasses = cn(getInputClasses(state, className), "[text-indent:var(--prefix-width)]", showClearButtonNow && "pr-10", usesTabularNumbers && "tabular-nums");
|
|
135
|
+
const inputClasses = cn(getInputClasses(state, className), "[text-indent:var(--prefix-width)]", startIcon && "pl-10", showClearButtonNow && "pr-10", usesTabularNumbers && "tabular-nums");
|
|
130
136
|
const prefixClasses = cn("inset-y-0 left-4 text-base pointer-events-none absolute flex items-center text-input-text-prefix opacity-100 peer-disabled:opacity-50");
|
|
131
137
|
return /* @__PURE__ */ jsxs("div", {
|
|
132
138
|
className: "flex w-full flex-col gap-1.5",
|
|
@@ -148,7 +154,7 @@ const Input = (allProps) => {
|
|
|
148
154
|
}),
|
|
149
155
|
/* @__PURE__ */ jsx("input", {
|
|
150
156
|
"aria-label": ariaLabel ?? label,
|
|
151
|
-
autoComplete: getAutoCompleteValue(type),
|
|
157
|
+
autoComplete: autoComplete ?? getAutoCompleteValue(type),
|
|
152
158
|
className: inputClasses,
|
|
153
159
|
"data-state": state,
|
|
154
160
|
disabled: isDisabled,
|
|
@@ -164,6 +170,7 @@ const Input = (allProps) => {
|
|
|
164
170
|
suppressHydrationWarning,
|
|
165
171
|
type: type === "password" ? inputType : type,
|
|
166
172
|
value,
|
|
173
|
+
...getPasswordManagerIgnoreProps(type),
|
|
167
174
|
...ariaProps,
|
|
168
175
|
...props
|
|
169
176
|
}),
|
package/dist/Input.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Input.js","names":[],"sources":["../src/components/Input/Input.tsx"],"sourcesContent":["import { CheckCircleIcon, CloseCircleIcon, ErrorIcon, EyeClosedIcon, EyeOpenIcon, LoaderIcon, WarningIcon } from '@components/Icons'\nimport { Label } from '@components/Label/Label'\nimport { useUncontrolledState } from '@hooks/useUncontrolledState'\nimport { ErrorMessage, WarningMessage, getAriaProps, getFormFieldCSSProperties, getInputClasses, useFormFieldId, useFormFieldState, type BaseFormFieldProps } from '@utils/formFieldUtils'\nimport { cn } from '@utils/twUtils'\nimport { useCallback, useRef, type ChangeEvent, type CSSProperties, type FocusEvent, type InputHTMLAttributes, type ReactElement, type Ref } from 'react'\nimport { useClearOnFocus, usePasswordVisibility, usePrefixWidth } from './InputUtils'\n\nexport type InputType = 'text' | 'email' | 'url' | 'tel' | 'password' | 'number' | 'date' | 'datetime-local'\n\nexport type InputProps = Omit<InputHTMLAttributes<HTMLInputElement>, 'id' | 'onChange'> &\n BaseFormFieldProps & {\n className?: string\n clearOnFocus?: boolean\n endIcon?: ReactElement\n labelClassName?: string\n onBlur?: (e: FocusEvent<HTMLInputElement>) => void\n onChange?: (value: string) => void\n onFocus?: (e: FocusEvent<HTMLInputElement>) => void\n placeholder?: string\n prefix?: string\n showClearButton?: boolean\n showStateIcon?: boolean\n startIcon?: ReactElement\n suppressHydrationWarning?: boolean\n type?: InputType\n value?: string\n warningMessage?: BaseFormFieldProps['errorMessage']\n }\n\nconst mergeRefs = <T,>(...refs: (Ref<T> | undefined)[]): Ref<T> => {\n return (value: T | null) => {\n refs.forEach((ref) => {\n if (!ref) return\n if (typeof ref === 'function') {\n ref(value)\n } else {\n ;(ref as { current: T | null }).current = value\n }\n })\n }\n}\n\nconst getAutoCompleteValue = (type: InputType): string => {\n const autoCompleteMap: Record<InputType, string> = {\n date: 'off',\n email: 'email',\n number: 'off',\n password: 'current-password',\n tel: 'tel',\n text: 'off',\n url: 'url',\n 'datetime-local': 'off',\n }\n return autoCompleteMap[type] || 'off'\n}\n\nexport const Input = (\n allProps: InputProps & {\n ref?: Ref<HTMLInputElement>\n },\n): ReactElement => {\n const {\n className,\n clearOnFocus = false,\n defaultValue,\n disabled,\n endIcon,\n errorMessage,\n id,\n label,\n labelClassName,\n messageReserveLines = 1,\n messageReserveSpace = false,\n name,\n onBlur,\n onChange,\n onFocus,\n placeholder,\n prefix,\n ref,\n required,\n showClearButton = false,\n showStateIcon = true,\n startIcon,\n state = 'default',\n suppressHydrationWarning = true,\n type = 'text',\n value: valueProp,\n warningMessage,\n 'aria-label': ariaLabel,\n 'aria-describedby': ariaDescribedBy,\n ...props\n } = allProps\n const inputId = useFormFieldId(id, name)\n const errorMessageId = `${inputId}-error`\n const warningMessageId = `${inputId}-warning`\n const { isDisabled, isLoading, isInvalid } = useFormFieldState(disabled, state)\n const messageId = state === 'error' ? errorMessageId : state === 'warning' && warningMessage ? warningMessageId : undefined\n const ariaProps = getAriaProps(state, ariaDescribedBy, required, messageId)\n const normalizedDefaultValue = typeof defaultValue === 'string' ? defaultValue : defaultValue !== undefined && defaultValue !== null ? String(defaultValue) : ''\n const [value, setValue] = useUncontrolledState<string>({\n value: valueProp,\n defaultValue: normalizedDefaultValue,\n onChange,\n })\n\n const internalRef = useRef<HTMLInputElement>(null)\n const inputRef = mergeRefs(ref, internalRef)\n\n const { isVisible, toggleVisibility, inputType } = usePasswordVisibility()\n const { prefixWidth, prefixRef } = usePrefixWidth(prefix)\n const { handleFocus: clearOnFocusHandler } = useClearOnFocus(clearOnFocus, (e: ChangeEvent<HTMLInputElement>) => setValue(e.target.value))\n\n const handleBlur = useCallback(\n (e: FocusEvent<HTMLInputElement>): void => {\n onBlur?.(e)\n },\n [onBlur],\n )\n\n const handleFocus = useCallback(\n (e: FocusEvent<HTMLInputElement>): void => {\n clearOnFocusHandler(e, onFocus)\n },\n [clearOnFocusHandler, onFocus],\n )\n\n const handleChange = useCallback(\n (e: ChangeEvent<HTMLInputElement>): void => {\n const newValue = e.target.value\n setValue(newValue)\n },\n [setValue],\n )\n\n const handleClear = useCallback((): void => {\n const element = internalRef.current\n if (element) {\n setValue('')\n element.focus()\n }\n }, [setValue])\n\n const showClearButtonNow = showClearButton && value.length > 0\n\n const getEndIcon = (): ReactElement | null => {\n const iconClasses = 'absolute right-4 top-1/2 -translate-y-1/2 text-input-icon hover:text-input-icon--hover focus:outline-none cursor-pointer'\n\n const iconComponents = {\n password: () => (\n <button aria-controls={inputId} aria-label={isVisible ? `Hide ${label ?? 'password'}` : `Show ${label ?? 'password'}`} aria-pressed={isVisible} className={iconClasses} data-testid='spectral-input-password-toggle' onClick={toggleVisibility} type='button'>\n {isVisible ? <EyeClosedIcon size={22} /> : <EyeOpenIcon size={22} />}\n </button>\n ),\n clear: () => (\n <button aria-label={String(`Clear ${label ?? 'input'}`)} className={iconClasses} data-testid='spectral-input-clear-button' onClick={handleClear} type='button'>\n <CloseCircleIcon size={24} />\n </button>\n ),\n loading: () => (\n <div className='right-4 text-input-icon absolute top-1/2 -translate-y-1/2' data-testid='spectral-input-loading-icon'>\n <LoaderIcon size={24} />\n </div>\n ),\n error: () => (\n <div className='right-4 absolute top-1/2 -translate-y-1/2 text-danger-400' data-testid='spectral-input-error-icon'>\n <ErrorIcon size={24} />\n </div>\n ),\n success: () => (\n <div className='right-4 absolute top-1/2 -translate-y-1/2 text-success-400' data-testid='spectral-input-success-icon'>\n <CheckCircleIcon size={24} />\n </div>\n ),\n warning: () => (\n <div className='right-4 absolute top-1/2 -translate-y-1/2 text-warning-400' data-testid='spectral-input-warning-icon'>\n <WarningIcon size={24} />\n </div>\n ),\n }\n\n if (endIcon) return <div className='right-4 text-input-icon absolute top-1/2 -translate-y-1/2'>{endIcon}</div>\n if (type === 'password') return iconComponents.password()\n if (showClearButtonNow) return iconComponents.clear()\n if (isLoading) return iconComponents.loading()\n if (!showStateIcon && (state === 'success' || state === 'warning' || state === 'error')) return null\n if (state === 'success') return iconComponents.success()\n if (state === 'warning') return iconComponents.warning()\n if (state === 'error') return iconComponents.error()\n\n return null\n }\n\n const getStartIcon = (): ReactElement | null => {\n if (startIcon) return startIcon\n return null\n }\n\n const usesTabularNumbers = type === 'number' || type === 'date' || type === 'datetime-local'\n const inputClasses = cn(getInputClasses(state, className), '[text-indent:var(--prefix-width)]', showClearButtonNow && 'pr-10', usesTabularNumbers && 'tabular-nums')\n\n const prefixClasses = cn('inset-y-0 left-4 text-base pointer-events-none absolute flex items-center text-input-text-prefix opacity-100 peer-disabled:opacity-50')\n\n return (\n <div className='flex w-full flex-col gap-1.5' data-testid='spectral-input-container'>\n {label && (\n <Label className={cn('mb-2 block', labelClassName, isDisabled && 'cursor-not-allowed text-input-text--disabled placeholder:text-input-text-placeholder')} data-testid='spectral-input-label' htmlFor={inputId}>\n {label}\n </Label>\n )}\n <div className='relative' data-testid='spectral-input-wrapper'>\n <div className='relative'>\n {getStartIcon()}\n {prefix && (\n <span ref={prefixRef} className={prefixClasses}>\n {prefix}\n </span>\n )}\n <input\n aria-label={ariaLabel ?? label}\n autoComplete={getAutoCompleteValue(type)}\n className={inputClasses}\n data-state={state}\n data-testid='spectral-input'\n disabled={isDisabled}\n id={inputId}\n name={name}\n onBlur={handleBlur}\n onChange={handleChange}\n onFocus={handleFocus}\n placeholder={placeholder ?? label}\n ref={inputRef}\n required={required}\n style={\n getFormFieldCSSProperties({\n '--prefix-width': prefix ? `${prefixWidth}px` : '0',\n }) as CSSProperties\n }\n suppressHydrationWarning={suppressHydrationWarning}\n type={type === 'password' ? inputType : type}\n value={value}\n {...ariaProps}\n {...props}\n />\n {getEndIcon()}\n </div>\n\n <ErrorMessage\n dataTestId='spectral-input-error-message'\n id={errorMessageId}\n message={isInvalid ? (errorMessage ?? null) : null}\n messageReserveLines={messageReserveLines}\n messageReserveSpace={messageReserveSpace && state === 'error'}\n />\n <WarningMessage\n dataTestId='spectral-input-warning-message'\n id={warningMessageId}\n message={state === 'warning' ? (warningMessage ?? null) : null}\n messageReserveLines={messageReserveLines}\n messageReserveSpace={messageReserveSpace && state === 'warning'}\n />\n </div>\n </div>\n )\n}\nInput.displayName = 'Input'\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Input.js","names":[],"sources":["../src/components/Input/Input.tsx"],"sourcesContent":["import { CheckCircleIcon, CloseCircleIcon, ErrorIcon, EyeClosedIcon, EyeOpenIcon, LoaderIcon, WarningIcon } from '@components/Icons'\nimport { Label } from '@components/Label/Label'\nimport { useUncontrolledState } from '@hooks/useUncontrolledState'\nimport {\n ErrorMessage,\n getAriaProps,\n getErrorMessageId,\n getFormFieldCSSProperties,\n getInputClasses,\n getPasswordManagerIgnoreProps,\n getWarningMessageId,\n useFormFieldId,\n useFormFieldState,\n WarningMessage,\n type BaseFormFieldProps,\n} from '@utils/formFieldUtils'\nimport { cn } from '@utils/twUtils'\nimport { useCallback, useRef, type ChangeEvent, type CSSProperties, type FocusEvent, type InputHTMLAttributes, type ReactElement, type Ref } from 'react'\nimport { useClearOnFocus, usePasswordVisibility, usePrefixWidth } from './InputUtils'\n\nexport type InputType = 'text' | 'email' | 'url' | 'tel' | 'password' | 'number' | 'date' | 'datetime-local'\n\nexport type InputProps = Omit<InputHTMLAttributes<HTMLInputElement>, 'id' | 'onChange'> &\n BaseFormFieldProps & {\n className?: string\n clearOnFocus?: boolean\n endIcon?: ReactElement\n hideStateIcon?: boolean\n labelClassName?: string\n onBlur?: (e: FocusEvent<HTMLInputElement>) => void\n onChange?: (value: string) => void\n onFocus?: (e: FocusEvent<HTMLInputElement>) => void\n placeholder?: string\n prefix?: string\n showClearButton?: boolean\n startIcon?: ReactElement\n suppressHydrationWarning?: boolean\n type?: InputType\n value?: string\n warningMessage?: BaseFormFieldProps['errorMessage']\n }\n\nconst mergeRefs = <T,>(...refs: (Ref<T> | undefined)[]): Ref<T> => {\n return (value: T | null) => {\n refs.forEach((ref) => {\n if (!ref) return\n if (typeof ref === 'function') {\n ref(value)\n } else {\n ;(ref as { current: T | null }).current = value\n }\n })\n }\n}\n\nconst iconBaseClasses = 'absolute top-1/2 -translate-y-1/2'\nconst endIconClasses = cn(iconBaseClasses, 'right-3 text-input-icon')\nconst startIconClasses = cn(iconBaseClasses, 'left-3 text-input-icon')\nconst iconButtonClasses = cn(endIconClasses, 'aspect-square max-h-6 rounded-sm hover:text-input-icon--hover cursor-pointer focus:outline-none focus-visible:outline-2 focus-visible:outline-accent focus-visible:outline-offset-1')\n\nconst getAutoCompleteValue = (type: InputType): string => {\n const autoCompleteMap: Record<InputType, string> = {\n date: 'off',\n email: 'email',\n number: 'off',\n password: 'current-password',\n tel: 'tel',\n text: 'off',\n url: 'url',\n 'datetime-local': 'off',\n }\n return autoCompleteMap[type] || 'off'\n}\n\nexport const Input = (\n allProps: InputProps & {\n ref?: Ref<HTMLInputElement>\n },\n): ReactElement => {\n const {\n className,\n clearOnFocus = false,\n defaultValue,\n disabled,\n endIcon,\n errorMessage,\n hideStateIcon = false,\n id,\n label,\n labelClassName,\n messageReserveLines = 1,\n messageReserveSpace = false,\n name,\n onBlur,\n onChange,\n onFocus,\n placeholder,\n prefix,\n ref,\n required,\n showClearButton = false,\n startIcon,\n state = 'default',\n suppressHydrationWarning = true,\n type = 'text',\n value: valueProp,\n warningMessage,\n autoComplete,\n 'aria-label': ariaLabel,\n 'aria-describedby': ariaDescribedBy,\n ...props\n } = allProps\n const inputId = useFormFieldId(id, name)\n const errorMessageId = getErrorMessageId(inputId)\n const warningMessageId = getWarningMessageId(inputId)\n const { isDisabled, isLoading, isInvalid } = useFormFieldState(disabled, state)\n const messageId = state === 'error' ? errorMessageId : state === 'warning' && warningMessage ? warningMessageId : undefined\n const ariaProps = getAriaProps(state, ariaDescribedBy, required, messageId)\n const normalizedDefaultValue = typeof defaultValue === 'string' ? defaultValue : defaultValue !== undefined && defaultValue !== null ? String(defaultValue) : ''\n const [value, setValue] = useUncontrolledState<string>({\n value: valueProp,\n defaultValue: normalizedDefaultValue,\n onChange,\n })\n\n const internalRef = useRef<HTMLInputElement>(null)\n const inputRef = mergeRefs(ref, internalRef)\n\n const { isVisible, toggleVisibility, inputType } = usePasswordVisibility()\n const { prefixWidth, prefixRef } = usePrefixWidth(prefix)\n const { handleFocus: clearOnFocusHandler } = useClearOnFocus(clearOnFocus, (e: ChangeEvent<HTMLInputElement>) => setValue(e.target.value))\n\n const handleBlur = useCallback(\n (e: FocusEvent<HTMLInputElement>): void => {\n onBlur?.(e)\n },\n [onBlur],\n )\n\n const handleFocus = useCallback(\n (e: FocusEvent<HTMLInputElement>): void => {\n clearOnFocusHandler(e, onFocus)\n },\n [clearOnFocusHandler, onFocus],\n )\n\n const handleChange = useCallback(\n (e: ChangeEvent<HTMLInputElement>): void => {\n const newValue = e.target.value\n setValue(newValue)\n },\n [setValue],\n )\n\n const handleClear = useCallback((): void => {\n const element = internalRef.current\n if (element) {\n setValue('')\n element.focus()\n }\n }, [setValue])\n\n const showClearButtonNow = showClearButton && value.length > 0\n\n const getEndIcon = (): ReactElement | null => {\n const iconComponents = {\n password: () => (\n <button aria-controls={inputId} aria-label={isVisible ? `Hide ${label ?? 'password'}` : `Show ${label ?? 'password'}`} aria-pressed={isVisible} className={iconButtonClasses} data-testid='spectral-input-password-toggle' onClick={toggleVisibility} type='button'>\n {isVisible ? <EyeClosedIcon size={22} /> : <EyeOpenIcon size={22} />}\n </button>\n ),\n clear: () => (\n <button aria-label={String(`Clear ${label ?? 'input'}`)} className={iconButtonClasses} data-testid='spectral-input-clear-button' onClick={handleClear} type='button'>\n <CloseCircleIcon size={22} />\n </button>\n ),\n loading: () => (\n <div className={endIconClasses} data-testid='spectral-input-loading-icon'>\n <LoaderIcon size={24} />\n </div>\n ),\n error: () => (\n <div className={cn(iconBaseClasses, 'right-3 text-danger-400')} data-testid='spectral-input-error-icon'>\n <ErrorIcon size={24} />\n </div>\n ),\n success: () => (\n <div className={cn(iconBaseClasses, 'right-3 text-success-400')} data-testid='spectral-input-success-icon'>\n <CheckCircleIcon size={24} />\n </div>\n ),\n warning: () => (\n <div className={cn(iconBaseClasses, 'right-3 text-warning-400')} data-testid='spectral-input-warning-icon'>\n <WarningIcon size={24} />\n </div>\n ),\n }\n\n if (endIcon) return <div className={endIconClasses}>{endIcon}</div>\n if (type === 'password') return iconComponents.password()\n if (showClearButtonNow) return iconComponents.clear()\n if (isLoading) return iconComponents.loading()\n if (hideStateIcon && (state === 'success' || state === 'warning' || state === 'error')) return null\n if (state === 'success') return iconComponents.success()\n if (state === 'warning') return iconComponents.warning()\n if (state === 'error') return iconComponents.error()\n\n return null\n }\n\n const getStartIcon = (): ReactElement | null => {\n if (startIcon) return <div className={startIconClasses}>{startIcon}</div>\n return null\n }\n\n const usesTabularNumbers = type === 'number' || type === 'date' || type === 'datetime-local'\n const inputClasses = cn(getInputClasses(state, className), '[text-indent:var(--prefix-width)]', startIcon && 'pl-10', showClearButtonNow && 'pr-10', usesTabularNumbers && 'tabular-nums')\n\n const prefixClasses = cn('inset-y-0 left-4 text-base pointer-events-none absolute flex items-center text-input-text-prefix opacity-100 peer-disabled:opacity-50')\n\n return (\n <div className='flex w-full flex-col gap-1.5' data-testid='spectral-input-container'>\n {label && (\n <Label className={cn('mb-2 block', labelClassName, isDisabled && 'cursor-not-allowed text-input-text--disabled placeholder:text-input-text-placeholder')} data-testid='spectral-input-label' htmlFor={inputId}>\n {label}\n </Label>\n )}\n <div className='relative' data-testid='spectral-input-wrapper'>\n <div className='relative'>\n {getStartIcon()}\n {prefix && (\n <span ref={prefixRef} className={prefixClasses}>\n {prefix}\n </span>\n )}\n <input\n aria-label={ariaLabel ?? label}\n autoComplete={autoComplete ?? getAutoCompleteValue(type)}\n className={inputClasses}\n data-state={state}\n data-testid='spectral-input'\n disabled={isDisabled}\n id={inputId}\n name={name}\n onBlur={handleBlur}\n onChange={handleChange}\n onFocus={handleFocus}\n placeholder={placeholder ?? label}\n ref={inputRef}\n required={required}\n style={\n getFormFieldCSSProperties({\n '--prefix-width': prefix ? `${prefixWidth}px` : '0',\n }) as CSSProperties\n }\n suppressHydrationWarning={suppressHydrationWarning}\n type={type === 'password' ? inputType : type}\n value={value}\n {...getPasswordManagerIgnoreProps(type)}\n {...ariaProps}\n {...props}\n />\n {getEndIcon()}\n </div>\n\n <ErrorMessage\n dataTestId='spectral-input-error-message'\n id={errorMessageId}\n message={isInvalid ? (errorMessage ?? null) : null}\n messageReserveLines={messageReserveLines}\n messageReserveSpace={messageReserveSpace && state === 'error'}\n />\n <WarningMessage\n dataTestId='spectral-input-warning-message'\n id={warningMessageId}\n message={state === 'warning' ? (warningMessage ?? null) : null}\n messageReserveLines={messageReserveLines}\n messageReserveSpace={messageReserveSpace && state === 'warning'}\n />\n </div>\n </div>\n )\n}\nInput.displayName = 'Input'\n"],"mappings":"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"}
|
package/dist/InputOTP.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ interface InputOTPBaseProps extends Omit<OTPInputProps, 'textAlign' | 'pushPassw
|
|
|
21
21
|
separator?: boolean;
|
|
22
22
|
state?: FormFieldState;
|
|
23
23
|
variant?: 'outlined' | 'filled';
|
|
24
|
+
warningMessage?: string | undefined;
|
|
24
25
|
}
|
|
25
26
|
type InputOTPProps = InputOTPBaseProps & ({
|
|
26
27
|
value: number | string;
|
|
@@ -53,6 +54,7 @@ declare const InputOTP: {
|
|
|
53
54
|
state,
|
|
54
55
|
value,
|
|
55
56
|
variant,
|
|
57
|
+
warningMessage,
|
|
56
58
|
...props
|
|
57
59
|
}: InputOTPProps & {
|
|
58
60
|
ref?: Ref<ComponentRef<typeof OTPInput>>;
|
package/dist/InputOTP.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InputOTP.d.ts","names":[],"sources":["../src/components/InputOTP/InputOTP.tsx"],"mappings":";;;;;;;UAMiB,iBAAA,SAA0B,IAAA,CAAK,aAAA;EAC9C,UAAA,OAAiB,IAAA;EACjB,SAAA;EACA,YAAA;EACA,SAAA;EACA,mBAAA;EACA,mBAAA;EAQQ;;;;;EAFR,OAAA;EACA,SAAA;EACA,KAAA,GAAQ,cAAA;EACR,OAAA;AAAA;AAAA,KAGU,aAAA,GAAgB,iBAAA;EAAuB,KAAA;EAAwB,QAAA,GAAW,QAAA;AAAA;EAAyC,KAAA;EAAe,QAAA;AAAA;AAAA,
|
|
1
|
+
{"version":3,"file":"InputOTP.d.ts","names":[],"sources":["../src/components/InputOTP/InputOTP.tsx"],"mappings":";;;;;;;UAMiB,iBAAA,SAA0B,IAAA,CAAK,aAAA;EAC9C,UAAA,OAAiB,IAAA;EACjB,SAAA;EACA,YAAA;EACA,SAAA;EACA,mBAAA;EACA,mBAAA;EAQQ;;;;;EAFR,OAAA;EACA,SAAA;EACA,KAAA,GAAQ,cAAA;EACR,OAAA;EACA,cAAA;AAAA;AAAA,KAGU,aAAA,GAAgB,iBAAA;EAAuB,KAAA;EAAwB,QAAA,GAAW,QAAA;AAAA;EAAyC,KAAA;EAAe,QAAA;AAAA;AAAA,UA0KpI,UAAA;EACR,SAAA;EACA,KAAA;EACA,KAAA;AAAA;AAAA,cA0DW,QAAA;EAAA;;;;;;;;;;;;;;;;;;;;KAjMV,aAAA;IACD,GAAA,GAAM,GAAA,CAAI,YAAA,QAAoB,QAAA;EAAA,IAC/B,oBAAA,CAAA,GAAA,CAAA,OAAA;;;;;;;OAoFE,wBAAA;MACD,GAAA,GAAM,GAAA,CAAI,YAAA;IAAA,IACX,oBAAA,CAAA,GAAA,CAAA,OAAA;;;;;;;;;OAQE,wBAAA;MACD,KAAA;MACA,GAAA,GAAM,GAAA,CAAI,YAAA;IAAA,IACX,oBAAA,CAAA,GAAA,CAAA,OAAA;;;;;;;;OA+D+C,UAAA,GAAU,oBAAA,CAAA,GAAA,CAAA,OAAA;;;;;;;OAkBvD,wBAAA;MACD,GAAA,GAAM,GAAA,CAAI,YAAA;IAAA,IACX,oBAAA,CAAA,GAAA,CAAA,OAAA"}
|
package/dist/InputOTP.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { MinusIcon } from "./Icons/MinusIcon.js";
|
|
3
3
|
import { cn } from "./utils/twUtils.js";
|
|
4
|
-
import { ErrorMessage } from "./FormFieldMessage.js";
|
|
5
|
-
import { getErrorMessageId, useFormFieldId, useFormFieldState } from "./utils/formFieldUtils.js";
|
|
4
|
+
import { ErrorMessage, WarningMessage } from "./FormFieldMessage.js";
|
|
5
|
+
import { getErrorMessageId, getWarningMessageId, useFormFieldId, useFormFieldState } from "./utils/formFieldUtils.js";
|
|
6
6
|
import { createContext, useContext } from "react";
|
|
7
7
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
8
8
|
import { OTPInput, REGEXP_ONLY_DIGITS } from "input-otp";
|
|
@@ -15,11 +15,13 @@ const useRoot = () => {
|
|
|
15
15
|
if (!context) throw new Error("useRoot must be used within an InputOTP");
|
|
16
16
|
return context;
|
|
17
17
|
};
|
|
18
|
-
const Root = ({ autoFocus = false, children, className, errorMessage, id, inputMode = "numeric", messageReserveLines = 1, messageReserveSpace = false, maxLength, name, onChange, onComplete, pattern, ref, state = "default", value, variant = "outlined", ...props }) => {
|
|
18
|
+
const Root = ({ autoFocus = false, children, className, errorMessage, id, inputMode = "numeric", messageReserveLines = 1, messageReserveSpace = false, maxLength, name, onChange, onComplete, pattern, ref, state = "default", value, variant = "outlined", warningMessage, ...props }) => {
|
|
19
19
|
const inputId = useFormFieldId(id, name);
|
|
20
20
|
const errorMessageId = getErrorMessageId(inputId);
|
|
21
|
+
const warningMessageId = getWarningMessageId(inputId);
|
|
21
22
|
const { isInvalid } = useFormFieldState(false, state);
|
|
22
23
|
const effectivePattern = pattern ?? (inputMode === "numeric" ? REGEXP_ONLY_DIGITS : void 0);
|
|
24
|
+
const describedBy = [isInvalid && errorMessage ? errorMessageId : void 0, state === "warning" && warningMessage ? warningMessageId : void 0].filter(Boolean).join(" ") || void 0;
|
|
23
25
|
const handlePaste = (e) => {
|
|
24
26
|
let pasteData = e.clipboardData.getData("text/plain").trim().replaceAll("-", "");
|
|
25
27
|
if (inputMode === "numeric") pasteData = pasteData.replace(/\D/g, "");
|
|
@@ -29,43 +31,53 @@ const Root = ({ autoFocus = false, children, className, errorMessage, id, inputM
|
|
|
29
31
|
value: { isInvalid },
|
|
30
32
|
children: /* @__PURE__ */ jsxs("div", {
|
|
31
33
|
className: "gap-y-1 flex w-max flex-col",
|
|
32
|
-
children: [
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
34
|
+
children: [
|
|
35
|
+
/* @__PURE__ */ jsx(OTPInput, {
|
|
36
|
+
"aria-describedby": describedBy,
|
|
37
|
+
"aria-invalid": isInvalid,
|
|
38
|
+
autoFocus,
|
|
39
|
+
containerClassName: cn("gap-2 flex items-center has-[:disabled]:cursor-not-allowed has-[disabled]:opacity-50", className),
|
|
40
|
+
"data-1p-ignore": "true",
|
|
41
|
+
"data-dashlane-disabled-on-field": "true",
|
|
42
|
+
"data-lpignore": "true",
|
|
43
|
+
"data-protonpass-ignore": "true",
|
|
44
|
+
id: inputId,
|
|
45
|
+
inputMode,
|
|
46
|
+
maxLength,
|
|
47
|
+
onChange,
|
|
48
|
+
onComplete,
|
|
49
|
+
onPaste: handlePaste,
|
|
50
|
+
pasteTransformer: (pasted) => pasted.replaceAll("-", ""),
|
|
51
|
+
pattern: effectivePattern,
|
|
52
|
+
pushPasswordManagerStrategy: "none",
|
|
53
|
+
ref,
|
|
54
|
+
textAlign: "center",
|
|
55
|
+
value,
|
|
56
|
+
...props,
|
|
57
|
+
render: ({ slots }) => /* @__PURE__ */ jsx(OTPInputContext.Provider, {
|
|
58
|
+
value: {
|
|
59
|
+
slots,
|
|
60
|
+
variant,
|
|
61
|
+
maxLength
|
|
62
|
+
},
|
|
63
|
+
children: children ?? /* @__PURE__ */ jsx(Group, { children: /* @__PURE__ */ jsx(Slots, {}) })
|
|
64
|
+
})
|
|
65
|
+
}),
|
|
66
|
+
/* @__PURE__ */ jsx(ErrorMessage, {
|
|
67
|
+
dataTestId: "spectral-input-otp-error-message",
|
|
68
|
+
id: errorMessageId,
|
|
69
|
+
message: isInvalid ? errorMessage : null,
|
|
70
|
+
messageReserveLines,
|
|
71
|
+
messageReserveSpace
|
|
72
|
+
}),
|
|
73
|
+
/* @__PURE__ */ jsx(WarningMessage, {
|
|
74
|
+
dataTestId: "spectral-input-otp-warning-message",
|
|
75
|
+
id: warningMessageId,
|
|
76
|
+
message: state === "warning" ? warningMessage : null,
|
|
77
|
+
messageReserveLines,
|
|
78
|
+
messageReserveSpace
|
|
61
79
|
})
|
|
62
|
-
|
|
63
|
-
dataTestId: "spectral-input-otp-error-message",
|
|
64
|
-
id: errorMessageId,
|
|
65
|
-
message: isInvalid ? errorMessage : null,
|
|
66
|
-
messageReserveLines,
|
|
67
|
-
messageReserveSpace
|
|
68
|
-
})]
|
|
80
|
+
]
|
|
69
81
|
})
|
|
70
82
|
});
|
|
71
83
|
};
|
|
@@ -85,7 +97,7 @@ const Slot = ({ className, index, ref, ...props }) => {
|
|
|
85
97
|
isActive: false
|
|
86
98
|
};
|
|
87
99
|
return /* @__PURE__ */ jsxs("div", {
|
|
88
|
-
className: cn("h-12 w-10 relative z-10 flex items-center justify-center rounded-[8px] border tabular-nums transition duration-200 focus:outline-none", variant === "filled" ? "border-level-one bg-level-one" : "border-input-otp-border bg-transparent", !isInvalid && "border", isInvalid && "border-2 border-danger-400", slot.isActive && !isInvalid && "z-10 border-input-otp-border--focus", slot.isActive && isInvalid && "z-10 border-danger-400 focus-visible:outline-1 focus-visible:outline-offset-1 focus-visible:outline-danger-400", className),
|
|
100
|
+
className: cn("h-12 w-10 relative z-10 flex items-center justify-center rounded-[8px] border tabular-nums transition duration-200 focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent", variant === "filled" ? "border-level-one bg-level-one" : "border-input-otp-border bg-transparent", !isInvalid && "border", isInvalid && "border-2 border-danger-400", slot.isActive && !isInvalid && "z-10 border-input-otp-border--focus border-2", slot.isActive && isInvalid && "z-10 border-danger-400 focus-visible:outline-1 focus-visible:outline-offset-1 focus-visible:outline-danger-400", className),
|
|
89
101
|
"data-index": index,
|
|
90
102
|
"data-variant": variant,
|
|
91
103
|
ref,
|
package/dist/InputOTP.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InputOTP.js","names":[],"sources":["../src/components/InputOTP/InputOTP.tsx"],"sourcesContent":["import { MinusIcon } from '@components/Icons'\nimport { ErrorMessage, getErrorMessageId, useFormFieldId, useFormFieldState, type FormFieldState } from '@utils/formFieldUtils'\nimport { cn } from '@utils/twUtils'\nimport { OTPInput, REGEXP_ONLY_DIGITS, type OTPInputProps } from 'input-otp'\nimport { createContext, useContext, type ClipboardEvent, type ComponentPropsWithoutRef, type ComponentRef, type Ref } from 'react'\n\nexport interface InputOTPBaseProps extends Omit<OTPInputProps, 'textAlign' | 'pushPasswordManagerStrategy' | 'pasteTransformer' | 'noScriptCSSFallback' | 'placeholder' | 'containerClassName' | 'render' | 'pattern'> {\n onComplete?: (...args: unknown[]) => void\n className?: string\n errorMessage?: string | undefined\n inputMode?: 'numeric' | 'text' | 'decimal' | 'tel' | 'search' | 'email' | 'url'\n messageReserveLines?: number\n messageReserveSpace?: boolean\n /**\n * Regex pattern string to restrict allowed characters.\n * When `inputMode=\"numeric\"`, defaults to digits-only pattern.\n * Set to `undefined` to allow any characters.\n */\n pattern?: string | undefined\n separator?: boolean\n state?: FormFieldState\n variant?: 'outlined' | 'filled'\n}\n\nexport type InputOTPProps = InputOTPBaseProps & ({ value: number | string; onChange: (newValue: number | string) => void } | { value?: never; onChange?: never })\n\nconst InputOTPStateContext = createContext<{ isInvalid?: boolean }>({})\n\nconst OTPInputContext = createContext<{\n maxLength?: number\n slots?: { char: string | null; hasFakeCaret: boolean; isActive: boolean }[]\n variant?: 'outlined' | 'filled'\n} | null>(null)\n\nconst useRoot = () => {\n const context = useContext(OTPInputContext)\n if (!context) {\n throw new Error('useRoot must be used within an InputOTP')\n }\n return context\n}\n\nconst Root = ({\n autoFocus = false,\n children,\n className,\n errorMessage,\n id,\n inputMode = 'numeric',\n messageReserveLines = 1,\n messageReserveSpace = false,\n maxLength,\n name,\n onChange,\n onComplete,\n pattern,\n ref,\n state = 'default',\n value,\n variant = 'outlined',\n ...props\n}: InputOTPProps & {\n ref?: Ref<ComponentRef<typeof OTPInput>>\n}) => {\n const inputId = useFormFieldId(id, name)\n const errorMessageId = getErrorMessageId(inputId)\n const { isInvalid } = useFormFieldState(false, state)\n\n // Apply digits-only pattern when inputMode is numeric (unless explicitly overridden)\n const effectivePattern = pattern ?? (inputMode === 'numeric' ? REGEXP_ONLY_DIGITS : undefined)\n\n const handlePaste = (e: ClipboardEvent<HTMLDivElement>): void => {\n let pasteData = e.clipboardData.getData('text/plain').trim().replaceAll('-', '')\n\n // Filter to digits only when in numeric mode\n if (inputMode === 'numeric') {\n pasteData = pasteData.replace(/\\D/g, '')\n }\n\n if (pasteData.length === maxLength && typeof onChange === 'function') {\n onChange(pasteData)\n }\n }\n\n return (\n <InputOTPStateContext.Provider value={{ isInvalid }}>\n <div className='gap-y-1 flex w-max flex-col'>\n <OTPInput\n /* eslint-disable-next-line jsx-a11y/no-autofocus -- intentional: consumers can opt in for OTP-first flows; defaults to false */\n autoFocus={autoFocus}\n containerClassName={cn('gap-2 flex items-center disabled:cursor-not-allowed has-[disabled]:opacity-50', className)}\n data-1p-ignore='true'\n data-dashlane-disabled-on-field='true'\n data-lpignore='true'\n data-protonpass-ignore='true'\n data-testid='spectral-input-otp'\n id={inputId}\n inputMode={inputMode}\n maxLength={maxLength}\n onChange={onChange}\n onComplete={onComplete}\n onPaste={handlePaste}\n pasteTransformer={(pasted) => pasted.replaceAll('-', '')}\n pattern={effectivePattern}\n pushPasswordManagerStrategy='none'\n ref={ref}\n aria-describedby={isInvalid && errorMessage ? errorMessageId : undefined}\n aria-invalid={isInvalid}\n textAlign='center'\n value={value}\n {...props}\n render={({ slots }) => (\n <OTPInputContext.Provider value={{ slots, variant, maxLength }}>\n {children ?? (\n <Group>\n <Slots />\n </Group>\n )}\n </OTPInputContext.Provider>\n )}\n />\n <ErrorMessage\n dataTestId='spectral-input-otp-error-message'\n id={errorMessageId}\n message={isInvalid ? errorMessage : null}\n messageReserveLines={messageReserveLines}\n messageReserveSpace={messageReserveSpace}\n />\n </div>\n </InputOTPStateContext.Provider>\n )\n}\nRoot.displayName = 'InputOTP'\n\nconst Group = ({\n ref,\n ...props\n}: ComponentPropsWithoutRef<'div'> & {\n ref?: Ref<ComponentRef<'div'>>\n}) => <div className='gap-x-2 flex items-center justify-center' data-testid='spectral-input-otp-group' ref={ref} {...props} />\nGroup.displayName = 'InputOTP.Group'\n\nconst Slot = ({\n className,\n index,\n ref,\n ...props\n}: ComponentPropsWithoutRef<'div'> & {\n index: number\n ref?: Ref<ComponentRef<'div'>>\n}) => {\n const { variant = 'outlined', slots = [] } = useRoot()\n const { isInvalid } = useContext(InputOTPStateContext)\n const slot = slots[index] || { char: '', hasFakeCaret: true, isActive: false }\n\n return (\n <div\n className={cn(\n 'h-12 w-10 relative z-10 flex items-center justify-center rounded-[8px] border tabular-nums transition duration-200 focus:outline-none',\n variant === 'filled' ? 'border-level-one bg-level-one' : 'border-input-otp-border bg-transparent',\n !isInvalid && 'border',\n isInvalid && 'border-2 border-danger-400',\n slot.isActive && !isInvalid && 'z-10 border-input-otp-border--focus',\n slot.isActive && isInvalid && 'z-10 border-danger-400 focus-visible:outline-1 focus-visible:outline-offset-1 focus-visible:outline-danger-400',\n className,\n )}\n data-index={index}\n data-testid='spectral-input-otp-slot'\n data-variant={variant}\n ref={ref}\n {...props}\n >\n {slot.char}\n {slot.hasFakeCaret && (\n <div className='inset-0 pointer-events-none absolute flex items-center justify-center motion-safe:animate-caret-blink'>\n <div className='h-8 w-px bg-input-otp-caret' />\n </div>\n )}\n </div>\n )\n}\nSlot.displayName = 'InputOTP.Slot'\n\ninterface SlotsProps {\n className?: string\n count?: number\n start?: number\n}\n\n/**\n * Helper component that automatically renders multiple InputOTP.Slot components.\n * Uses the maxLength from the parent InputOTP to determine how many slots to render.\n *\n * @example\n * // Render all 6 slots\n * <InputOTP maxLength={6}>\n * <InputOTP.Group>\n * <InputOTP.Slots />\n * </InputOTP.Group>\n * </InputOTP>\n *\n * @example\n * // Render slots in groups with a separator (3-3 split)\n * <InputOTP maxLength={6}>\n * <InputOTP.Group>\n * <InputOTP.Slots count={3} />\n * </InputOTP.Group>\n * <InputOTP.Separator />\n * <InputOTP.Group>\n * <InputOTP.Slots start={3} />\n * </InputOTP.Group>\n * </InputOTP>\n */\nconst Slots = ({ start = 0, count, className }: SlotsProps) => {\n const { maxLength = 0 } = useRoot()\n const end = count !== undefined ? start + count : maxLength\n const indices = Array.from({ length: end - start }, (_, i) => start + i)\n\n return (\n <>\n {indices.map((index) => (\n <Slot key={index} index={index} className={className} />\n ))}\n </>\n )\n}\nSlots.displayName = 'InputOTP.Slots'\n\nconst Separator = ({\n ref,\n ...props\n}: ComponentPropsWithoutRef<'div'> & {\n ref?: Ref<ComponentRef<'div'>>\n}) => {\n const { variant = 'outlined' } = useRoot()\n\n return (\n <div ref={ref} role='separator' {...props} data-testid='spectral-input-otp-separator' data-variant={variant}>\n <MinusIcon size={24} color={variant === 'filled' ? 'var(--color-input-otp-filled-separator)' : 'var(--color-input-otp-border)'} />\n </div>\n )\n}\nSeparator.displayName = 'InputOTP.Separator'\n\nexport const InputOTP = Object.assign(Root, {\n Group,\n Slot,\n Slots,\n Separator,\n})\n"],"mappings":";;;;;;;;;;AA0BA,MAAM,uBAAuB,cAAuC,EAAE,CAAA;AAEtE,MAAM,kBAAkB,cAId,KAAI;AAEd,MAAM,gBAAgB;CACpB,MAAM,UAAU,WAAW,gBAAe;AAC1C,KAAI,CAAC,QACH,OAAM,IAAI,MAAM,0CAAyC;AAE3D,QAAO;;AAGT,MAAM,QAAQ,EACZ,YAAY,OACZ,UACA,WACA,cACA,IACA,YAAY,WACZ,sBAAsB,GACtB,sBAAsB,OACtB,WACA,MACA,UACA,YACA,SACA,KACA,QAAQ,WACR,OACA,UAAU,YACV,GAAG,YAGC;CACJ,MAAM,UAAU,eAAe,IAAI,KAAI;CACvC,MAAM,iBAAiB,kBAAkB,QAAO;CAChD,MAAM,EAAE,cAAc,kBAAkB,OAAO,MAAK;CAGpD,MAAM,mBAAmB,YAAY,cAAc,YAAY,qBAAqB;CAEpF,MAAM,eAAe,MAA4C;EAC/D,IAAI,YAAY,EAAE,cAAc,QAAQ,aAAa,CAAC,MAAM,CAAC,WAAW,KAAK,GAAE;AAG/E,MAAI,cAAc,UAChB,aAAY,UAAU,QAAQ,OAAO,GAAE;AAGzC,MAAI,UAAU,WAAW,aAAa,OAAO,aAAa,WACxD,UAAS,UAAS;;AAItB,QACE,oBAAC,qBAAqB,UAAtB;EAA+B,OAAO,EAAE,WAAW;YACjD,qBAAC,OAAD;GAAK,WAAU;aAAf,CACE,oBAAC,UAAD;IAEa;IACX,oBAAoB,GAAG,iFAAiF,UAAU;IAClH,kBAAe;IACf,mCAAgC;IAChC,iBAAc;IACd,0BAAuB;IAEvB,IAAI;IACO;IACA;IACD;IACE;IACZ,SAAS;IACT,mBAAmB,WAAW,OAAO,WAAW,KAAK,GAAG;IACxD,SAAS;IACT,6BAA4B;IACvB;IACL,oBAAkB,aAAa,eAAe,iBAAiB;IAC/D,gBAAc;IACd,WAAU;IACH;IACP,GAAI;IACJ,SAAS,EAAE,YACT,oBAAC,gBAAgB,UAAjB;KAA0B,OAAO;MAAE;MAAO;MAAS;MAAW;eAC3D,YACC,oBAAC,OAAD,YACE,oBAAC,OAAD,EAAQ,GACH;KAEe;IAE7B,GACD,oBAAC,cAAD;IACE,YAAW;IACX,IAAI;IACJ,SAAS,YAAY,eAAe;IACf;IACA;IACtB,EACE;;EACwB;;AAGnC,KAAK,cAAc;AAEnB,MAAM,SAAS,EACb,KACA,GAAG,YAGC,oBAAC,OAAD;CAAK,WAAU;CAAuF;CAAK,GAAI;CAAQ;AAC7H,MAAM,cAAc;AAEpB,MAAM,QAAQ,EACZ,WACA,OACA,KACA,GAAG,YAIC;CACJ,MAAM,EAAE,UAAU,YAAY,QAAQ,EAAE,KAAK,SAAQ;CACrD,MAAM,EAAE,cAAc,WAAW,qBAAoB;CACrD,MAAM,OAAO,MAAM,UAAU;EAAE,MAAM;EAAI,cAAc;EAAM,UAAU;EAAM;AAE7E,QACE,qBAAC,OAAD;EACE,WAAW,GACT,yIACA,YAAY,WAAW,kCAAkC,0CACzD,CAAC,aAAa,UACd,aAAa,8BACb,KAAK,YAAY,CAAC,aAAa,uCAC/B,KAAK,YAAY,aAAa,kHAC9B,UACD;EACD,cAAY;EAEZ,gBAAc;EACT;EACL,GAAI;YAdN,CAgBG,KAAK,MACL,KAAK,gBACJ,oBAAC,OAAD;GAAK,WAAU;aACb,oBAAC,OAAD,EAAK,WAAU,+BAA+B;GAC3C,EAEJ;;;AAGT,KAAK,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;AAgCnB,MAAM,SAAS,EAAE,QAAQ,GAAG,OAAO,gBAA4B;CAC7D,MAAM,EAAE,YAAY,MAAM,SAAQ;CAClC,MAAM,MAAM,UAAU,SAAY,QAAQ,QAAQ;AAGlD,QACE,4CAHc,MAAM,KAAK,EAAE,QAAQ,MAAM,OAAO,GAAG,GAAG,MAAM,QAAQ,EAI1D,CAAC,KAAK,UACZ,oBAAC,MAAD;EAAyB;EAAkB;EAAY,EAA5C,MAA4C,CACvD,EACF;;AAGN,MAAM,cAAc;AAEpB,MAAM,aAAa,EACjB,KACA,GAAG,YAGC;CACJ,MAAM,EAAE,UAAU,eAAe,SAAQ;AAEzC,QACE,oBAAC,OAAD;EAAU;EAAK,MAAK;EAAY,GAAI;EAAkD,gBAAc;YAClG,oBAAC,WAAD;GAAW,MAAM;GAAI,OAAO,YAAY,WAAW,4CAA4C;GAAkC;EAC9H;;AAGT,UAAU,cAAc;AAExB,MAAa,WAAW,OAAO,OAAO,MAAM;CAC1C;CACA;CACA;CACA;CACD,CAAA"}
|
|
1
|
+
{"version":3,"file":"InputOTP.js","names":[],"sources":["../src/components/InputOTP/InputOTP.tsx"],"sourcesContent":["import { MinusIcon } from '@components/Icons'\nimport { ErrorMessage, getErrorMessageId, getWarningMessageId, useFormFieldId, useFormFieldState, WarningMessage, type FormFieldState } from '@utils/formFieldUtils'\nimport { cn } from '@utils/twUtils'\nimport { OTPInput, REGEXP_ONLY_DIGITS, type OTPInputProps } from 'input-otp'\nimport { createContext, useContext, type ClipboardEvent, type ComponentPropsWithoutRef, type ComponentRef, type Ref } from 'react'\n\nexport interface InputOTPBaseProps extends Omit<OTPInputProps, 'textAlign' | 'pushPasswordManagerStrategy' | 'pasteTransformer' | 'noScriptCSSFallback' | 'placeholder' | 'containerClassName' | 'render' | 'pattern'> {\n onComplete?: (...args: unknown[]) => void\n className?: string\n errorMessage?: string | undefined\n inputMode?: 'numeric' | 'text' | 'decimal' | 'tel' | 'search' | 'email' | 'url'\n messageReserveLines?: number\n messageReserveSpace?: boolean\n /**\n * Regex pattern string to restrict allowed characters.\n * When `inputMode=\"numeric\"`, defaults to digits-only pattern.\n * Set to `undefined` to allow any characters.\n */\n pattern?: string | undefined\n separator?: boolean\n state?: FormFieldState\n variant?: 'outlined' | 'filled'\n warningMessage?: string | undefined\n}\n\nexport type InputOTPProps = InputOTPBaseProps & ({ value: number | string; onChange: (newValue: number | string) => void } | { value?: never; onChange?: never })\n\nconst InputOTPStateContext = createContext<{ isInvalid?: boolean }>({})\n\nconst OTPInputContext = createContext<{\n maxLength?: number\n slots?: { char: string | null; hasFakeCaret: boolean; isActive: boolean }[]\n variant?: 'outlined' | 'filled'\n} | null>(null)\n\nconst useRoot = () => {\n const context = useContext(OTPInputContext)\n if (!context) {\n throw new Error('useRoot must be used within an InputOTP')\n }\n return context\n}\n\nconst Root = ({\n autoFocus = false,\n children,\n className,\n errorMessage,\n id,\n inputMode = 'numeric',\n messageReserveLines = 1,\n messageReserveSpace = false,\n maxLength,\n name,\n onChange,\n onComplete,\n pattern,\n ref,\n state = 'default',\n value,\n variant = 'outlined',\n warningMessage,\n ...props\n}: InputOTPProps & {\n ref?: Ref<ComponentRef<typeof OTPInput>>\n}) => {\n const inputId = useFormFieldId(id, name)\n const errorMessageId = getErrorMessageId(inputId)\n const warningMessageId = getWarningMessageId(inputId)\n const { isInvalid } = useFormFieldState(false, state)\n\n // Apply digits-only pattern when inputMode is numeric (unless explicitly overridden)\n const effectivePattern = pattern ?? (inputMode === 'numeric' ? REGEXP_ONLY_DIGITS : undefined)\n\n const describedBy = [isInvalid && errorMessage ? errorMessageId : undefined, state === 'warning' && warningMessage ? warningMessageId : undefined].filter(Boolean).join(' ') || undefined\n\n const handlePaste = (e: ClipboardEvent<HTMLDivElement>): void => {\n let pasteData = e.clipboardData.getData('text/plain').trim().replaceAll('-', '')\n\n // Filter to digits only when in numeric mode\n if (inputMode === 'numeric') {\n pasteData = pasteData.replace(/\\D/g, '')\n }\n\n if (pasteData.length === maxLength && typeof onChange === 'function') {\n onChange(pasteData)\n }\n }\n\n return (\n <InputOTPStateContext.Provider value={{ isInvalid }}>\n <div className='gap-y-1 flex w-max flex-col'>\n <OTPInput\n aria-describedby={describedBy}\n aria-invalid={isInvalid}\n /* eslint-disable-next-line jsx-a11y/no-autofocus -- intentional: consumers can opt in for OTP-first flows; defaults to false */\n autoFocus={autoFocus}\n containerClassName={cn('gap-2 flex items-center has-[:disabled]:cursor-not-allowed has-[disabled]:opacity-50', className)}\n data-1p-ignore='true'\n data-dashlane-disabled-on-field='true'\n data-lpignore='true'\n data-protonpass-ignore='true'\n data-testid='spectral-input-otp'\n id={inputId}\n inputMode={inputMode}\n maxLength={maxLength}\n onChange={onChange}\n onComplete={onComplete}\n onPaste={handlePaste}\n pasteTransformer={(pasted) => pasted.replaceAll('-', '')}\n pattern={effectivePattern}\n pushPasswordManagerStrategy='none'\n ref={ref}\n textAlign='center'\n value={value}\n {...props}\n render={({ slots }) => (\n <OTPInputContext.Provider value={{ slots, variant, maxLength }}>\n {children ?? (\n <Group>\n <Slots />\n </Group>\n )}\n </OTPInputContext.Provider>\n )}\n />\n <ErrorMessage\n dataTestId='spectral-input-otp-error-message'\n id={errorMessageId}\n message={isInvalid ? errorMessage : null}\n messageReserveLines={messageReserveLines}\n messageReserveSpace={messageReserveSpace}\n />\n <WarningMessage\n dataTestId='spectral-input-otp-warning-message'\n id={warningMessageId}\n message={state === 'warning' ? warningMessage : null}\n messageReserveLines={messageReserveLines}\n messageReserveSpace={messageReserveSpace}\n />\n </div>\n </InputOTPStateContext.Provider>\n )\n}\nRoot.displayName = 'InputOTP'\n\nconst Group = ({\n ref,\n ...props\n}: ComponentPropsWithoutRef<'div'> & {\n ref?: Ref<ComponentRef<'div'>>\n}) => <div className='gap-x-2 flex items-center justify-center' data-testid='spectral-input-otp-group' ref={ref} {...props} />\nGroup.displayName = 'InputOTP.Group'\n\nconst Slot = ({\n className,\n index,\n ref,\n ...props\n}: ComponentPropsWithoutRef<'div'> & {\n index: number\n ref?: Ref<ComponentRef<'div'>>\n}) => {\n const { variant = 'outlined', slots = [] } = useRoot()\n const { isInvalid } = useContext(InputOTPStateContext)\n const slot = slots[index] || { char: '', hasFakeCaret: true, isActive: false }\n\n return (\n <div\n className={cn(\n 'h-12 w-10 relative z-10 flex items-center justify-center rounded-[8px] border tabular-nums transition duration-200 focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent',\n variant === 'filled' ? 'border-level-one bg-level-one' : 'border-input-otp-border bg-transparent',\n !isInvalid && 'border',\n isInvalid && 'border-2 border-danger-400',\n slot.isActive && !isInvalid && 'z-10 border-input-otp-border--focus border-2',\n slot.isActive && isInvalid && 'z-10 border-danger-400 focus-visible:outline-1 focus-visible:outline-offset-1 focus-visible:outline-danger-400',\n className,\n )}\n data-index={index}\n data-testid='spectral-input-otp-slot'\n data-variant={variant}\n ref={ref}\n {...props}\n >\n {slot.char}\n {slot.hasFakeCaret && (\n <div className='inset-0 pointer-events-none absolute flex items-center justify-center motion-safe:animate-caret-blink'>\n <div className='h-8 w-px bg-input-otp-caret' />\n </div>\n )}\n </div>\n )\n}\nSlot.displayName = 'InputOTP.Slot'\n\ninterface SlotsProps {\n className?: string\n count?: number\n start?: number\n}\n\n/**\n * Helper component that automatically renders multiple InputOTP.Slot components.\n * Uses the maxLength from the parent InputOTP to determine how many slots to render.\n *\n * @example\n * // Render all 6 slots\n * <InputOTP maxLength={6}>\n * <InputOTP.Group>\n * <InputOTP.Slots />\n * </InputOTP.Group>\n * </InputOTP>\n *\n * @example\n * // Render slots in groups with a separator (3-3 split)\n * <InputOTP maxLength={6}>\n * <InputOTP.Group>\n * <InputOTP.Slots count={3} />\n * </InputOTP.Group>\n * <InputOTP.Separator />\n * <InputOTP.Group>\n * <InputOTP.Slots start={3} />\n * </InputOTP.Group>\n * </InputOTP>\n */\nconst Slots = ({ start = 0, count, className }: SlotsProps) => {\n const { maxLength = 0 } = useRoot()\n const end = count !== undefined ? start + count : maxLength\n const indices = Array.from({ length: end - start }, (_, i) => start + i)\n\n return (\n <>\n {indices.map((index) => (\n <Slot key={index} index={index} className={className} />\n ))}\n </>\n )\n}\nSlots.displayName = 'InputOTP.Slots'\n\nconst Separator = ({\n ref,\n ...props\n}: ComponentPropsWithoutRef<'div'> & {\n ref?: Ref<ComponentRef<'div'>>\n}) => {\n const { variant = 'outlined' } = useRoot()\n\n return (\n <div ref={ref} role='separator' {...props} data-testid='spectral-input-otp-separator' data-variant={variant}>\n <MinusIcon size={24} color={variant === 'filled' ? 'var(--color-input-otp-filled-separator)' : 'var(--color-input-otp-border)'} />\n </div>\n )\n}\nSeparator.displayName = 'InputOTP.Separator'\n\nexport const InputOTP = Object.assign(Root, {\n Group,\n Slot,\n Slots,\n Separator,\n})\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InputSearch.d.ts","names":[],"sources":["../src/components/InputSearch/InputSearch.tsx"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"InputSearch.d.ts","names":[],"sources":["../src/components/InputSearch/InputSearch.tsx"],"mappings":";;;;;;KA4BY,iBAAA,GAAoB,UAAA;AAAA,UAEf,gBAAA,SAAyB,IAAA,CAAK,kBAAA;EAC7C,SAAA;EACA,iBAAA,IAAqB,KAAA,aAAkB,SAAA;EACvC,aAAA;EACA,YAAA;EACA,aAAA,GAAgB,aAAA;EAChB,YAAA;EACA,UAAA;EACA,cAAA;EACA,QAAA,IAAY,KAAA;EACZ,QAAA,IAAY,KAAA;EACZ,aAAA,IAAiB,KAAA;EACjB,WAAA;EACA,OAAA,EAAS,iBAAA;EACT,WAAA;EACA,YAAA,IAAgB,MAAA,EAAQ,iBAAA,KAAsB,SAAA;EAC9C,cAAA;EACA,KAAA,GAAQ,OAAA,CAAQ,cAAA;EAChB,KAAA;EACA,cAAA,GAAiB,kBAAA;AAAA;AAAA;EAajB,SAAA;EACA,iBAAA;EACA,aAAA;EACA,YAAA;EACA,QAAA;EACA,aAAA;EACA,YAAA;EACA,YAAA;EACA,EAAA;EACA,UAAA;EACA,KAAA;EACA,cAAA;EACA,mBAAA;EACA,mBAAA;EACA,IAAA;EACA,QAAA;EACA,QAAA;EACA,aAAA;EACA,WAAA;EACA,OAAA;EACA,WAAA;EACA,GAAA;EACA,YAAA;EACA,QAAA;EACA,cAAA;EACA,KAAA;EACA,KAAA,EAAO,SAAA;EACP,cAAA;EAAA,oBACoB,eAAA;EAAA,cACN;AAAA,GACb,gBAAA;EAAqB,GAAA,GAAM,GAAA,CAAI,cAAA;AAAA,IAAiB,oBAAA,CAAA,GAAA,CAAA,OAAA;AAAA"}
|
package/dist/InputSearch.js
CHANGED
|
@@ -7,7 +7,7 @@ import { SearchIcon } from "./Icons/SearchIcon.js";
|
|
|
7
7
|
import { WarningIcon } from "./Icons/WarningIcon.js";
|
|
8
8
|
import { cn } from "./utils/twUtils.js";
|
|
9
9
|
import { ErrorMessage, WarningMessage } from "./FormFieldMessage.js";
|
|
10
|
-
import { EmptyState, getAriaProps, getDropdownSurfaceClasses, getDropdownWidthStyles, getErrorMessageId, getFormFieldCSSProperties, getInputClasses, getOptionClasses, useFormFieldId, useFormFieldState } from "./utils/formFieldUtils.js";
|
|
10
|
+
import { EmptyState, getAriaProps, getDropdownSurfaceClasses, getDropdownWidthStyles, getErrorMessageId, getFormFieldCSSProperties, getInputClasses, getOptionClasses, getPasswordManagerIgnoreProps, getWarningMessageId, useFormFieldId, useFormFieldState } from "./utils/formFieldUtils.js";
|
|
11
11
|
import { useUncontrolledState } from "./hooks/useUncontrolledState.js";
|
|
12
12
|
import { Label } from "./Label.js";
|
|
13
13
|
import { useAutoDropdownHorizontalShift } from "./utils/dropdownPositioning.js";
|
|
@@ -35,7 +35,7 @@ const InputSearch = ({ className, createOptionLabel = defaultCreateOptionLabel,
|
|
|
35
35
|
const fieldId = useFormFieldId(id, name);
|
|
36
36
|
const listboxId = `${fieldId}-listbox`;
|
|
37
37
|
const errorMessageId = getErrorMessageId(fieldId);
|
|
38
|
-
const warningMessageId =
|
|
38
|
+
const warningMessageId = getWarningMessageId(fieldId);
|
|
39
39
|
const messageId = state === "error" ? errorMessageId : state === "warning" && warningMessage ? warningMessageId : void 0;
|
|
40
40
|
const { isDisabled, isLoading, isInvalid } = useFormFieldState(disabled, state);
|
|
41
41
|
const ariaProps = getAriaProps(state, ariaDescribedBy, required, messageId);
|
|
@@ -245,6 +245,7 @@ const InputSearch = ({ className, createOptionLabel = defaultCreateOptionLabel,
|
|
|
245
245
|
style: getFormFieldCSSProperties(),
|
|
246
246
|
type: "text",
|
|
247
247
|
value: query,
|
|
248
|
+
...getPasswordManagerIgnoreProps("text"),
|
|
248
249
|
...ariaProps
|
|
249
250
|
}),
|
|
250
251
|
showClearButton ? /* @__PURE__ */ jsx("button", {
|
package/dist/InputSearch.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InputSearch.js","names":[],"sources":["../src/components/InputSearch/InputSearch.tsx"],"sourcesContent":["import { CloseCircleIcon, ErrorIcon, LoaderIcon, PlusIcon, SearchIcon, WarningIcon } from '@components/Icons'\nimport { Label } from '@components/Label/Label'\nimport { useUncontrolledState } from '@hooks/useUncontrolledState'\nimport { useAutoDropdownHorizontalShift } from '@utils/dropdownPositioning'\nimport {\n EmptyState,\n ErrorMessage,\n WarningMessage,\n getAriaProps,\n getDropdownSurfaceClasses,\n getDropdownWidthStyles,\n getErrorMessageId,\n getFormFieldCSSProperties,\n getInputClasses,\n getOptionClasses,\n useFormFieldId,\n useFormFieldState,\n type BaseFormFieldProps,\n type BaseOption,\n type DropdownWidth,\n type FormFieldState,\n} from '@utils/formFieldUtils'\nimport { cn } from '@utils/twUtils'\nimport { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type CSSProperties, type KeyboardEvent, type ReactNode, type Ref } from 'react'\nimport { createPortal } from 'react-dom'\n\nexport type InputSearchOption = BaseOption\n\nexport interface InputSearchProps extends Omit<BaseFormFieldProps, 'onChange' | 'state'> {\n className?: string\n createOptionLabel?: (query: string) => ReactNode\n creatingLabel?: string\n defaultValue?: string\n dropdownWidth?: DropdownWidth\n emptyMessage?: string\n isCreating?: boolean\n labelClassName?: string\n onChange?: (value: string) => void\n onCreate?: (query: string) => void\n onValueChange?: (value: string) => void\n openOnFocus?: boolean\n options: InputSearchOption[]\n placeholder?: string\n renderOption?: (option: InputSearchOption) => ReactNode\n showSearchIcon?: boolean\n state?: Exclude<FormFieldState, 'disabled'>\n value?: string\n warningMessage?: BaseFormFieldProps['errorMessage']\n}\n\nconst defaultCreateOptionLabel = (query: string): ReactNode => (\n <>\n <PlusIcon size={16} className='shrink-0' />\n <span className='truncate'>\n Create <span className='font-semibold'>“{query}”</span>\n </span>\n </>\n)\n\nexport const InputSearch = ({\n className,\n createOptionLabel = defaultCreateOptionLabel,\n creatingLabel = 'Creating…',\n defaultValue = '',\n disabled,\n dropdownWidth = 'trigger',\n emptyMessage = 'No options found',\n errorMessage,\n id,\n isCreating = false,\n label,\n labelClassName,\n messageReserveLines = 1,\n messageReserveSpace = false,\n name,\n onChange,\n onCreate,\n onValueChange,\n openOnFocus = false,\n options,\n placeholder = 'Search…',\n ref,\n renderOption,\n required,\n showSearchIcon = true,\n state = 'default',\n value: valueProp,\n warningMessage,\n 'aria-describedby': ariaDescribedBy,\n 'aria-label': ariaLabel,\n}: InputSearchProps & { ref?: Ref<HTMLDivElement> }) => {\n if (process.env.NODE_ENV !== 'production' && !label && !ariaLabel) {\n // eslint-disable-next-line no-console\n console.warn('InputSearch: provide either `label` or `aria-label` for an accessible name.')\n }\n\n const fieldId = useFormFieldId(id, name)\n const listboxId = `${fieldId}-listbox`\n const errorMessageId = getErrorMessageId(fieldId)\n const warningMessageId = `${fieldId}-warning`\n const messageId = state === 'error' ? errorMessageId : state === 'warning' && warningMessage ? warningMessageId : undefined\n const { isDisabled, isLoading, isInvalid } = useFormFieldState(disabled, state)\n const ariaProps = getAriaProps(state, ariaDescribedBy, required, messageId)\n\n const [value, setValue] = useUncontrolledState<string>({\n value: valueProp,\n defaultValue,\n onChange: (nextValue) => {\n if (onChange) {\n onChange(nextValue)\n } else {\n onValueChange?.(nextValue)\n }\n },\n })\n\n const [query, setQuery] = useState('')\n const [isFocused, setIsFocused] = useState(false)\n const [highlightedIndex, setHighlightedIndex] = useState(-1)\n const [dropdownPosition, setDropdownPosition] = useState<{ top: number; left: number; width: number } | null>(null)\n const inputRef = useRef<HTMLInputElement>(null)\n const listRef = useRef<HTMLDivElement>(null)\n const blurTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)\n const triggerRef = useRef<HTMLDivElement>(null)\n\n const selectedOption = useMemo(() => options.find((option) => option.value === value) ?? null, [options, value])\n\n useEffect(() => {\n if (!isFocused) {\n setQuery(selectedOption?.label ?? '')\n }\n }, [selectedOption, isFocused])\n\n const trimmedQuery = query.trim()\n\n const filteredOptions = useMemo(() => {\n if (trimmedQuery.length === 0) return options\n const lower = trimmedQuery.toLowerCase()\n return options.filter((option) => option.label.toLowerCase().includes(lower))\n }, [trimmedQuery, options])\n\n const exactMatch = useMemo(() => options.some((option) => option.label.toLowerCase() === trimmedQuery.toLowerCase()), [options, trimmedQuery])\n\n const showCreateOption = onCreate !== undefined && trimmedQuery.length > 0 && !exactMatch\n const createOptionIndex = showCreateOption ? filteredOptions.length : -1\n const totalItems = filteredOptions.length + (showCreateOption ? 1 : 0)\n const showDropdown = isFocused && !isDisabled && !isLoading && (openOnFocus || trimmedQuery.length > 0)\n const showEmptyState = showDropdown && totalItems === 0\n\n const { dropdownShiftStyle, recalculateDropdownPosition, setDropdownElement } = useAutoDropdownHorizontalShift(showDropdown)\n\n const updateDropdownPosition = useCallback(() => {\n const trigger = triggerRef.current\n if (!trigger) return\n const rect = trigger.getBoundingClientRect()\n setDropdownPosition({\n top: rect.bottom + 4,\n left: rect.left,\n width: rect.width,\n })\n recalculateDropdownPosition()\n }, [recalculateDropdownPosition])\n\n useLayoutEffect(() => {\n if (!showDropdown) {\n setDropdownPosition(null)\n return\n }\n updateDropdownPosition()\n const handle = () => updateDropdownPosition()\n window.addEventListener('scroll', handle, true)\n window.addEventListener('resize', handle)\n return () => {\n window.removeEventListener('scroll', handle, true)\n window.removeEventListener('resize', handle)\n }\n }, [showDropdown, updateDropdownPosition])\n\n useLayoutEffect(() => {\n if (!showDropdown) return\n updateDropdownPosition()\n }, [showDropdown, updateDropdownPosition, filteredOptions.length, showCreateOption])\n\n useEffect(() => {\n setHighlightedIndex(-1)\n }, [filteredOptions.length, showCreateOption])\n\n useEffect(() => {\n if (highlightedIndex < 0 || !listRef.current) return\n const element = listRef.current.querySelector<HTMLElement>(`[data-index='${highlightedIndex}']`)\n element?.scrollIntoView({ block: 'nearest' })\n }, [highlightedIndex])\n\n useEffect(\n () => () => {\n if (blurTimeoutRef.current !== null) {\n clearTimeout(blurTimeoutRef.current)\n }\n },\n [],\n )\n\n const commitSelection = useCallback(\n (option: InputSearchOption) => {\n if (option.disabled) return\n setValue(option.value)\n setQuery(option.label)\n setIsFocused(false)\n setHighlightedIndex(-1)\n inputRef.current?.blur()\n },\n [setValue],\n )\n\n const commitCreate = useCallback(() => {\n if (!onCreate || isCreating || trimmedQuery.length === 0) return\n onCreate(trimmedQuery)\n }, [onCreate, isCreating, trimmedQuery])\n\n const handleKeyDown = useCallback(\n (event: KeyboardEvent<HTMLInputElement>) => {\n switch (event.key) {\n case 'ArrowDown': {\n if (totalItems === 0) return\n event.preventDefault()\n setHighlightedIndex((prev) => (prev < totalItems - 1 ? prev + 1 : prev))\n break\n }\n case 'ArrowUp': {\n if (totalItems === 0) return\n event.preventDefault()\n setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : 0))\n break\n }\n case 'Enter': {\n if (totalItems === 0) return\n event.preventDefault()\n if (highlightedIndex >= 0 && highlightedIndex < filteredOptions.length) {\n const option = filteredOptions[highlightedIndex]\n if (option) commitSelection(option)\n } else if (highlightedIndex === createOptionIndex && !isCreating) {\n commitCreate()\n }\n break\n }\n case 'Escape': {\n event.preventDefault()\n setIsFocused(false)\n setHighlightedIndex(-1)\n if (selectedOption) setQuery(selectedOption.label)\n inputRef.current?.blur()\n break\n }\n default:\n break\n }\n },\n [totalItems, filteredOptions, highlightedIndex, createOptionIndex, commitSelection, commitCreate, isCreating, selectedOption],\n )\n\n const handleFocus = useCallback(() => {\n if (blurTimeoutRef.current !== null) {\n clearTimeout(blurTimeoutRef.current)\n blurTimeoutRef.current = null\n }\n setIsFocused(true)\n setQuery('')\n }, [])\n\n const handleBlur = useCallback(() => {\n if (blurTimeoutRef.current !== null) {\n clearTimeout(blurTimeoutRef.current)\n }\n blurTimeoutRef.current = setTimeout(() => {\n blurTimeoutRef.current = null\n // Don't write to `query` here — the useEffect tied to selectedOption + isFocused\n // is the single source of truth for the visible query when the field isn't focused.\n // Setting it from this stale closure causes off-by-one display after selection.\n setIsFocused(false)\n setHighlightedIndex(-1)\n }, 150)\n }, [])\n\n const handleClear = useCallback(() => {\n setValue('')\n setQuery('')\n inputRef.current?.focus()\n }, [setValue])\n\n const showClearButton = !isLoading && !isDisabled && (value.length > 0 || (isFocused && query.length > 0))\n\n const { dropdownWidthMode, dropdownWidthStyle } = getDropdownWidthStyles({\n dropdownWidth,\n triggerWidth: '100%',\n })\n\n const inputClasses = cn(getInputClasses(state, className), 'pe-4', showSearchIcon && 'ps-11', showClearButton && 'pe-10!', state === 'loading' && 'cursor-wait')\n\n return (\n <div className='flex w-full flex-col' data-testid='spectral-input-search-container' ref={ref}>\n {label && (\n <Label className={cn('mb-2 block', labelClassName, isDisabled && 'cursor-not-allowed text-input-text--disabled')} data-testid='spectral-input-search-label' htmlFor={fieldId}>\n {label}\n </Label>\n )}\n\n <div className='relative' data-testid='spectral-input-search-wrapper' ref={triggerRef}>\n <div className='relative'>\n {showSearchIcon && (\n <span aria-hidden='true' className={cn('left-4 text-input-icon absolute top-1/2 -translate-y-1/2', isDisabled && 'text-input-text--disabled')} data-testid='spectral-input-search-icon'>\n <SearchIcon size={20} />\n </span>\n )}\n\n <input\n aria-activedescendant={highlightedIndex >= 0 ? `${fieldId}-option-${highlightedIndex}` : undefined}\n aria-autocomplete='list'\n aria-controls={listboxId}\n aria-expanded={showDropdown}\n // oxlint-disable-next-line jsx-a11y/role-supports-aria-props -- Valid per WAI-ARIA 1.2 Combobox pattern\n aria-haspopup='listbox'\n aria-label={ariaLabel ?? label}\n autoComplete='off'\n className={inputClasses}\n data-state={state}\n data-testid='spectral-input-search'\n disabled={isDisabled || isLoading}\n id={fieldId}\n name={name}\n onBlur={handleBlur}\n onChange={(event) => {\n setQuery(event.target.value)\n if (!isFocused) setIsFocused(true)\n }}\n onFocus={handleFocus}\n onKeyDown={handleKeyDown}\n placeholder={placeholder}\n ref={inputRef}\n required={required}\n role='combobox'\n style={getFormFieldCSSProperties() as CSSProperties}\n type='text'\n value={query}\n {...ariaProps}\n />\n\n {showClearButton ? (\n <button\n aria-label={`Clear ${label ?? 'search'}`}\n className='right-4 text-input-icon hover:text-input-icon--hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-input-border--focus rounded-sm absolute top-1/2 -translate-y-1/2 cursor-pointer'\n data-testid='spectral-input-search-clear-button'\n onClick={handleClear}\n // Prevent input blur from firing before click resolves\n onMouseDown={(event) => event.preventDefault()}\n type='button'\n >\n <CloseCircleIcon size={20} />\n </button>\n ) : state === 'loading' ? (\n <div className='right-4 text-input-icon absolute top-1/2 -translate-y-1/2' data-testid='spectral-input-search-loading-icon'>\n <LoaderIcon className='motion-safe:animate-spin' size={20} />\n </div>\n ) : state === 'error' ? (\n <div className='right-4 text-danger-400 absolute top-1/2 -translate-y-1/2' data-testid='spectral-input-search-error-icon'>\n <ErrorIcon size={20} />\n </div>\n ) : state === 'warning' ? (\n <div className='right-4 text-warning-400 absolute top-1/2 -translate-y-1/2' data-testid='spectral-input-search-warning-icon'>\n <WarningIcon size={20} />\n </div>\n ) : null}\n </div>\n\n </div>\n\n {showDropdown && dropdownPosition && typeof document !== 'undefined'\n ? createPortal(\n <div\n className={cn('origin-top p-1 z-50 fixed', getDropdownSurfaceClasses(), 'max-h-72 overflow-hidden', 'motion-safe:animate-in motion-safe:fade-in-0 motion-safe:zoom-in-95 motion-safe:slide-in-from-top-2')}\n data-dropdown-width-mode={dropdownWidthMode}\n data-dropdown-width-value={dropdownWidthMode === 'custom' ? dropdownWidth : undefined}\n data-testid='spectral-input-search-content'\n ref={setDropdownElement}\n style={{\n top: `${dropdownPosition.top}px`,\n left: `${dropdownPosition.left}px`,\n ...(dropdownWidthMode === 'trigger' ? { width: `${dropdownPosition.width}px` } : (dropdownWidthStyle as CSSProperties)),\n ...dropdownShiftStyle,\n }}\n >\n <div\n className='max-h-[17.5rem] overflow-y-auto'\n id={listboxId}\n ref={listRef}\n // oxlint-disable-next-line jsx-a11y/prefer-tag-over-role -- WAI-ARIA combobox pattern requires role='listbox' on the popup\n role='listbox'\n >\n {showEmptyState ? (\n <EmptyState message={emptyMessage} />\n ) : (\n <>\n {filteredOptions.map((option, index) => {\n const isHighlighted = index === highlightedIndex\n const isSelected = option.value === value\n return (\n <button\n aria-selected={isSelected}\n className={cn(getOptionClasses(!!option.disabled, isHighlighted, isSelected), 'text-left')}\n data-highlighted={isHighlighted ? '' : undefined}\n data-index={index}\n data-testid='spectral-input-search-item'\n disabled={option.disabled}\n id={`${fieldId}-option-${index}`}\n key={option.value}\n onMouseDown={(event) => {\n event.preventDefault()\n commitSelection(option)\n }}\n onMouseEnter={() => setHighlightedIndex(index)}\n role='option'\n tabIndex={-1}\n type='button'\n >\n <span className='min-w-0 flex-1 truncate'>{renderOption ? renderOption(option) : option.label}</span>\n </button>\n )\n })}\n\n {showCreateOption && (\n <button\n aria-selected={createOptionIndex === highlightedIndex}\n className={cn(getOptionClasses(isCreating, createOptionIndex === highlightedIndex, false), 'gap-2 text-left text-accent font-medium')}\n data-highlighted={createOptionIndex === highlightedIndex ? '' : undefined}\n data-index={createOptionIndex}\n data-testid='spectral-input-search-create-option'\n disabled={isCreating}\n id={`${fieldId}-option-${createOptionIndex}`}\n onMouseDown={(event) => {\n event.preventDefault()\n commitCreate()\n }}\n onMouseEnter={() => setHighlightedIndex(createOptionIndex)}\n role='option'\n tabIndex={-1}\n type='button'\n >\n {isCreating ? (\n <span className='gap-2 flex items-center'>\n <LoaderIcon className='shrink-0 motion-safe:animate-spin' size={16} />\n <span>{creatingLabel}</span>\n </span>\n ) : (\n <span className='gap-2 flex items-center min-w-0'>{createOptionLabel(trimmedQuery)}</span>\n )}\n </button>\n )}\n </>\n )}\n </div>\n </div>,\n document.body,\n )\n : null}\n\n <ErrorMessage\n dataTestId='spectral-input-search-error-message'\n id={errorMessageId}\n message={isInvalid ? (errorMessage ?? null) : null}\n messageReserveLines={messageReserveLines}\n messageReserveSpace={messageReserveSpace && state === 'error'}\n />\n <WarningMessage\n dataTestId='spectral-input-search-warning-message'\n id={warningMessageId}\n message={state === 'warning' ? (warningMessage ?? null) : null}\n messageReserveLines={messageReserveLines}\n messageReserveSpace={messageReserveSpace && state === 'warning'}\n />\n </div>\n )\n}\nInputSearch.displayName = 'InputSearch'\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"InputSearch.js","names":[],"sources":["../src/components/InputSearch/InputSearch.tsx"],"sourcesContent":["import { CloseCircleIcon, ErrorIcon, LoaderIcon, PlusIcon, SearchIcon, WarningIcon } from '@components/Icons'\nimport { Label } from '@components/Label/Label'\nimport { useUncontrolledState } from '@hooks/useUncontrolledState'\nimport { useAutoDropdownHorizontalShift } from '@utils/dropdownPositioning'\nimport {\n EmptyState,\n ErrorMessage,\n WarningMessage,\n getAriaProps,\n getDropdownSurfaceClasses,\n getDropdownWidthStyles,\n getErrorMessageId,\n getFormFieldCSSProperties,\n getInputClasses,\n getOptionClasses,\n getPasswordManagerIgnoreProps,\n getWarningMessageId,\n useFormFieldId,\n useFormFieldState,\n type BaseFormFieldProps,\n type BaseOption,\n type DropdownWidth,\n type FormFieldState,\n} from '@utils/formFieldUtils'\nimport { cn } from '@utils/twUtils'\nimport { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type CSSProperties, type KeyboardEvent, type ReactNode, type Ref } from 'react'\nimport { createPortal } from 'react-dom'\n\nexport type InputSearchOption = BaseOption\n\nexport interface InputSearchProps extends Omit<BaseFormFieldProps, 'onChange' | 'state'> {\n className?: string\n createOptionLabel?: (query: string) => ReactNode\n creatingLabel?: string\n defaultValue?: string\n dropdownWidth?: DropdownWidth\n emptyMessage?: string\n isCreating?: boolean\n labelClassName?: string\n onChange?: (value: string) => void\n onCreate?: (query: string) => void\n onValueChange?: (value: string) => void\n openOnFocus?: boolean\n options: InputSearchOption[]\n placeholder?: string\n renderOption?: (option: InputSearchOption) => ReactNode\n showSearchIcon?: boolean\n state?: Exclude<FormFieldState, 'disabled'>\n value?: string\n warningMessage?: BaseFormFieldProps['errorMessage']\n}\n\nconst defaultCreateOptionLabel = (query: string): ReactNode => (\n <>\n <PlusIcon size={16} className='shrink-0' />\n <span className='truncate'>\n Create <span className='font-semibold'>“{query}”</span>\n </span>\n </>\n)\n\nexport const InputSearch = ({\n className,\n createOptionLabel = defaultCreateOptionLabel,\n creatingLabel = 'Creating…',\n defaultValue = '',\n disabled,\n dropdownWidth = 'trigger',\n emptyMessage = 'No options found',\n errorMessage,\n id,\n isCreating = false,\n label,\n labelClassName,\n messageReserveLines = 1,\n messageReserveSpace = false,\n name,\n onChange,\n onCreate,\n onValueChange,\n openOnFocus = false,\n options,\n placeholder = 'Search…',\n ref,\n renderOption,\n required,\n showSearchIcon = true,\n state = 'default',\n value: valueProp,\n warningMessage,\n 'aria-describedby': ariaDescribedBy,\n 'aria-label': ariaLabel,\n}: InputSearchProps & { ref?: Ref<HTMLDivElement> }) => {\n if (process.env.NODE_ENV !== 'production' && !label && !ariaLabel) {\n // eslint-disable-next-line no-console\n console.warn('InputSearch: provide either `label` or `aria-label` for an accessible name.')\n }\n\n const fieldId = useFormFieldId(id, name)\n const listboxId = `${fieldId}-listbox`\n const errorMessageId = getErrorMessageId(fieldId)\n const warningMessageId = getWarningMessageId(fieldId)\n const messageId = state === 'error' ? errorMessageId : state === 'warning' && warningMessage ? warningMessageId : undefined\n const { isDisabled, isLoading, isInvalid } = useFormFieldState(disabled, state)\n const ariaProps = getAriaProps(state, ariaDescribedBy, required, messageId)\n\n const [value, setValue] = useUncontrolledState<string>({\n value: valueProp,\n defaultValue,\n onChange: (nextValue) => {\n if (onChange) {\n onChange(nextValue)\n } else {\n onValueChange?.(nextValue)\n }\n },\n })\n\n const [query, setQuery] = useState('')\n const [isFocused, setIsFocused] = useState(false)\n const [highlightedIndex, setHighlightedIndex] = useState(-1)\n const [dropdownPosition, setDropdownPosition] = useState<{ top: number; left: number; width: number } | null>(null)\n const inputRef = useRef<HTMLInputElement>(null)\n const listRef = useRef<HTMLDivElement>(null)\n const blurTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)\n const triggerRef = useRef<HTMLDivElement>(null)\n\n const selectedOption = useMemo(() => options.find((option) => option.value === value) ?? null, [options, value])\n\n useEffect(() => {\n if (!isFocused) {\n setQuery(selectedOption?.label ?? '')\n }\n }, [selectedOption, isFocused])\n\n const trimmedQuery = query.trim()\n\n const filteredOptions = useMemo(() => {\n if (trimmedQuery.length === 0) return options\n const lower = trimmedQuery.toLowerCase()\n return options.filter((option) => option.label.toLowerCase().includes(lower))\n }, [trimmedQuery, options])\n\n const exactMatch = useMemo(() => options.some((option) => option.label.toLowerCase() === trimmedQuery.toLowerCase()), [options, trimmedQuery])\n\n const showCreateOption = onCreate !== undefined && trimmedQuery.length > 0 && !exactMatch\n const createOptionIndex = showCreateOption ? filteredOptions.length : -1\n const totalItems = filteredOptions.length + (showCreateOption ? 1 : 0)\n const showDropdown = isFocused && !isDisabled && !isLoading && (openOnFocus || trimmedQuery.length > 0)\n const showEmptyState = showDropdown && totalItems === 0\n\n const { dropdownShiftStyle, recalculateDropdownPosition, setDropdownElement } = useAutoDropdownHorizontalShift(showDropdown)\n\n const updateDropdownPosition = useCallback(() => {\n const trigger = triggerRef.current\n if (!trigger) return\n const rect = trigger.getBoundingClientRect()\n setDropdownPosition({\n top: rect.bottom + 4,\n left: rect.left,\n width: rect.width,\n })\n recalculateDropdownPosition()\n }, [recalculateDropdownPosition])\n\n useLayoutEffect(() => {\n if (!showDropdown) {\n setDropdownPosition(null)\n return\n }\n updateDropdownPosition()\n const handle = () => updateDropdownPosition()\n window.addEventListener('scroll', handle, true)\n window.addEventListener('resize', handle)\n return () => {\n window.removeEventListener('scroll', handle, true)\n window.removeEventListener('resize', handle)\n }\n }, [showDropdown, updateDropdownPosition])\n\n useLayoutEffect(() => {\n if (!showDropdown) return\n updateDropdownPosition()\n }, [showDropdown, updateDropdownPosition, filteredOptions.length, showCreateOption])\n\n useEffect(() => {\n setHighlightedIndex(-1)\n }, [filteredOptions.length, showCreateOption])\n\n useEffect(() => {\n if (highlightedIndex < 0 || !listRef.current) return\n const element = listRef.current.querySelector<HTMLElement>(`[data-index='${highlightedIndex}']`)\n element?.scrollIntoView({ block: 'nearest' })\n }, [highlightedIndex])\n\n useEffect(\n () => () => {\n if (blurTimeoutRef.current !== null) {\n clearTimeout(blurTimeoutRef.current)\n }\n },\n [],\n )\n\n const commitSelection = useCallback(\n (option: InputSearchOption) => {\n if (option.disabled) return\n setValue(option.value)\n setQuery(option.label)\n setIsFocused(false)\n setHighlightedIndex(-1)\n inputRef.current?.blur()\n },\n [setValue],\n )\n\n const commitCreate = useCallback(() => {\n if (!onCreate || isCreating || trimmedQuery.length === 0) return\n onCreate(trimmedQuery)\n }, [onCreate, isCreating, trimmedQuery])\n\n const handleKeyDown = useCallback(\n (event: KeyboardEvent<HTMLInputElement>) => {\n switch (event.key) {\n case 'ArrowDown': {\n if (totalItems === 0) return\n event.preventDefault()\n setHighlightedIndex((prev) => (prev < totalItems - 1 ? prev + 1 : prev))\n break\n }\n case 'ArrowUp': {\n if (totalItems === 0) return\n event.preventDefault()\n setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : 0))\n break\n }\n case 'Enter': {\n if (totalItems === 0) return\n event.preventDefault()\n if (highlightedIndex >= 0 && highlightedIndex < filteredOptions.length) {\n const option = filteredOptions[highlightedIndex]\n if (option) commitSelection(option)\n } else if (highlightedIndex === createOptionIndex && !isCreating) {\n commitCreate()\n }\n break\n }\n case 'Escape': {\n event.preventDefault()\n setIsFocused(false)\n setHighlightedIndex(-1)\n if (selectedOption) setQuery(selectedOption.label)\n inputRef.current?.blur()\n break\n }\n default:\n break\n }\n },\n [totalItems, filteredOptions, highlightedIndex, createOptionIndex, commitSelection, commitCreate, isCreating, selectedOption],\n )\n\n const handleFocus = useCallback(() => {\n if (blurTimeoutRef.current !== null) {\n clearTimeout(blurTimeoutRef.current)\n blurTimeoutRef.current = null\n }\n setIsFocused(true)\n setQuery('')\n }, [])\n\n const handleBlur = useCallback(() => {\n if (blurTimeoutRef.current !== null) {\n clearTimeout(blurTimeoutRef.current)\n }\n blurTimeoutRef.current = setTimeout(() => {\n blurTimeoutRef.current = null\n // Don't write to `query` here — the useEffect tied to selectedOption + isFocused\n // is the single source of truth for the visible query when the field isn't focused.\n // Setting it from this stale closure causes off-by-one display after selection.\n setIsFocused(false)\n setHighlightedIndex(-1)\n }, 150)\n }, [])\n\n const handleClear = useCallback(() => {\n setValue('')\n setQuery('')\n inputRef.current?.focus()\n }, [setValue])\n\n const showClearButton = !isLoading && !isDisabled && (value.length > 0 || (isFocused && query.length > 0))\n\n const { dropdownWidthMode, dropdownWidthStyle } = getDropdownWidthStyles({\n dropdownWidth,\n triggerWidth: '100%',\n })\n\n const inputClasses = cn(getInputClasses(state, className), 'pe-4', showSearchIcon && 'ps-11', showClearButton && 'pe-10!', state === 'loading' && 'cursor-wait')\n\n return (\n <div className='flex w-full flex-col' data-testid='spectral-input-search-container' ref={ref}>\n {label && (\n <Label className={cn('mb-2 block', labelClassName, isDisabled && 'cursor-not-allowed text-input-text--disabled')} data-testid='spectral-input-search-label' htmlFor={fieldId}>\n {label}\n </Label>\n )}\n\n <div className='relative' data-testid='spectral-input-search-wrapper' ref={triggerRef}>\n <div className='relative'>\n {showSearchIcon && (\n <span aria-hidden='true' className={cn('left-4 text-input-icon absolute top-1/2 -translate-y-1/2', isDisabled && 'text-input-text--disabled')} data-testid='spectral-input-search-icon'>\n <SearchIcon size={20} />\n </span>\n )}\n\n <input\n aria-activedescendant={highlightedIndex >= 0 ? `${fieldId}-option-${highlightedIndex}` : undefined}\n aria-autocomplete='list'\n aria-controls={listboxId}\n aria-expanded={showDropdown}\n // oxlint-disable-next-line jsx-a11y/role-supports-aria-props -- Valid per WAI-ARIA 1.2 Combobox pattern\n aria-haspopup='listbox'\n aria-label={ariaLabel ?? label}\n autoComplete='off'\n className={inputClasses}\n data-state={state}\n data-testid='spectral-input-search'\n disabled={isDisabled || isLoading}\n id={fieldId}\n name={name}\n onBlur={handleBlur}\n onChange={(event) => {\n setQuery(event.target.value)\n if (!isFocused) setIsFocused(true)\n }}\n onFocus={handleFocus}\n onKeyDown={handleKeyDown}\n placeholder={placeholder}\n ref={inputRef}\n required={required}\n role='combobox'\n style={getFormFieldCSSProperties() as CSSProperties}\n type='text'\n value={query}\n {...getPasswordManagerIgnoreProps('text')}\n {...ariaProps}\n />\n\n {showClearButton ? (\n <button\n aria-label={`Clear ${label ?? 'search'}`}\n className='right-4 text-input-icon hover:text-input-icon--hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-input-border--focus rounded-sm absolute top-1/2 -translate-y-1/2 cursor-pointer'\n data-testid='spectral-input-search-clear-button'\n onClick={handleClear}\n // Prevent input blur from firing before click resolves\n onMouseDown={(event) => event.preventDefault()}\n type='button'\n >\n <CloseCircleIcon size={20} />\n </button>\n ) : state === 'loading' ? (\n <div className='right-4 text-input-icon absolute top-1/2 -translate-y-1/2' data-testid='spectral-input-search-loading-icon'>\n <LoaderIcon className='motion-safe:animate-spin' size={20} />\n </div>\n ) : state === 'error' ? (\n <div className='right-4 text-danger-400 absolute top-1/2 -translate-y-1/2' data-testid='spectral-input-search-error-icon'>\n <ErrorIcon size={20} />\n </div>\n ) : state === 'warning' ? (\n <div className='right-4 text-warning-400 absolute top-1/2 -translate-y-1/2' data-testid='spectral-input-search-warning-icon'>\n <WarningIcon size={20} />\n </div>\n ) : null}\n </div>\n\n </div>\n\n {showDropdown && dropdownPosition && typeof document !== 'undefined'\n ? createPortal(\n <div\n className={cn('origin-top p-1 z-50 fixed', getDropdownSurfaceClasses(), 'max-h-72 overflow-hidden', 'motion-safe:animate-in motion-safe:fade-in-0 motion-safe:zoom-in-95 motion-safe:slide-in-from-top-2')}\n data-dropdown-width-mode={dropdownWidthMode}\n data-dropdown-width-value={dropdownWidthMode === 'custom' ? dropdownWidth : undefined}\n data-testid='spectral-input-search-content'\n ref={setDropdownElement}\n style={{\n top: `${dropdownPosition.top}px`,\n left: `${dropdownPosition.left}px`,\n ...(dropdownWidthMode === 'trigger' ? { width: `${dropdownPosition.width}px` } : (dropdownWidthStyle as CSSProperties)),\n ...dropdownShiftStyle,\n }}\n >\n <div\n className='max-h-[17.5rem] overflow-y-auto'\n id={listboxId}\n ref={listRef}\n // oxlint-disable-next-line jsx-a11y/prefer-tag-over-role -- WAI-ARIA combobox pattern requires role='listbox' on the popup\n role='listbox'\n >\n {showEmptyState ? (\n <EmptyState message={emptyMessage} />\n ) : (\n <>\n {filteredOptions.map((option, index) => {\n const isHighlighted = index === highlightedIndex\n const isSelected = option.value === value\n return (\n <button\n aria-selected={isSelected}\n className={cn(getOptionClasses(!!option.disabled, isHighlighted, isSelected), 'text-left')}\n data-highlighted={isHighlighted ? '' : undefined}\n data-index={index}\n data-testid='spectral-input-search-item'\n disabled={option.disabled}\n id={`${fieldId}-option-${index}`}\n key={option.value}\n onMouseDown={(event) => {\n event.preventDefault()\n commitSelection(option)\n }}\n onMouseEnter={() => setHighlightedIndex(index)}\n role='option'\n tabIndex={-1}\n type='button'\n >\n <span className='min-w-0 flex-1 truncate'>{renderOption ? renderOption(option) : option.label}</span>\n </button>\n )\n })}\n\n {showCreateOption && (\n <button\n aria-selected={createOptionIndex === highlightedIndex}\n className={cn(getOptionClasses(isCreating, createOptionIndex === highlightedIndex, false), 'gap-2 text-left text-accent font-medium')}\n data-highlighted={createOptionIndex === highlightedIndex ? '' : undefined}\n data-index={createOptionIndex}\n data-testid='spectral-input-search-create-option'\n disabled={isCreating}\n id={`${fieldId}-option-${createOptionIndex}`}\n onMouseDown={(event) => {\n event.preventDefault()\n commitCreate()\n }}\n onMouseEnter={() => setHighlightedIndex(createOptionIndex)}\n role='option'\n tabIndex={-1}\n type='button'\n >\n {isCreating ? (\n <span className='gap-2 flex items-center'>\n <LoaderIcon className='shrink-0 motion-safe:animate-spin' size={16} />\n <span>{creatingLabel}</span>\n </span>\n ) : (\n <span className='gap-2 flex items-center min-w-0'>{createOptionLabel(trimmedQuery)}</span>\n )}\n </button>\n )}\n </>\n )}\n </div>\n </div>,\n document.body,\n )\n : null}\n\n <ErrorMessage\n dataTestId='spectral-input-search-error-message'\n id={errorMessageId}\n message={isInvalid ? (errorMessage ?? null) : null}\n messageReserveLines={messageReserveLines}\n messageReserveSpace={messageReserveSpace && state === 'error'}\n />\n <WarningMessage\n dataTestId='spectral-input-search-warning-message'\n id={warningMessageId}\n message={state === 'warning' ? (warningMessage ?? null) : null}\n messageReserveLines={messageReserveLines}\n messageReserveSpace={messageReserveSpace && state === 'warning'}\n />\n </div>\n )\n}\nInputSearch.displayName = 'InputSearch'\n"],"mappings":"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"}
|