import * as React from "react" import { Controller, type Control, type FieldPath, type FieldValues, type RegisterOptions } from "react-hook-form" import { DateRangeInput, type DateRangeInputProps, type DateRangeValue, } from "@/components/ui/input/date-range" import { DateInput, type DateInputProps } from "@/components/ui/input/date" import { Input, type InputClearableProps, type InputSearchProps, type InputTextProps } from "@/components/ui/input" import { MaskedInput, type MaskedInputProps } from "@/components/ui/input/masked" import { MoneyInput, type MoneyInputProps } from "@/components/ui/input/money" import { PhoneInput, formatPhoneDigits, type PhoneInputProps, } from "@/components/ui/input/phone" import { QuantityInput, type QuantityInputProps } from "@/components/ui/input/quantity" import { FormFieldShell, type FormFieldShellControlProps, resolveFormFieldIds, } from "@/components/form/form-field-shell" export type FormInputKind = | "text" | "search" | "password" | "phone" | "date" | "date-range" | "clearable" | "masked" | "money" | "quantity" export type FormInputPhoneInputValueMode = "raw" | "masked" type TextInputElementChangeEvent = React.ChangeEvent type FormTextInputControlProps = Omit< React.ComponentProps<"input">, "name" | "value" | "defaultValue" > & { onChange?: (event: TextInputElementChangeEvent) => void } type FormControlledFieldProps< TFieldValues extends FieldValues, TName extends FieldPath, > = FormFieldShellControlProps & { control: Control name: TName rules?: RegisterOptions fieldClassName?: string id?: string } export type FormTextInputProps< TFieldValues extends FieldValues = FieldValues, TName extends FieldPath = FieldPath, > = Omit & FormControlledFieldProps & { kind?: "text" transformIn?: (value: unknown) => string | number | readonly string[] | undefined transformOut?: (event: TextInputElementChangeEvent) => unknown } export type FormInputSearchVariantProps< TFieldValues extends FieldValues = FieldValues, TName extends FieldPath = FieldPath, > = Omit & FormControlledFieldProps & { kind: "search" onValueChange?: (value: string) => void } export type FormInputPasswordVariantProps< TFieldValues extends FieldValues = FieldValues, TName extends FieldPath = FieldPath, > = Omit, "name" | "value" | "defaultValue"> & { kind: "password" onValueChange?: (value: string) => void min?: never max?: never } & FormControlledFieldProps export type FormInputPhoneVariantProps< TFieldValues extends FieldValues = FieldValues, TName extends FieldPath = FieldPath, > = Omit & FormControlledFieldProps & { kind: "phone" valueMode?: FormInputPhoneInputValueMode onValueChange?: (value: string, rawValue: string, maskedValue: string) => void } export type FormInputDateVariantProps< TFieldValues extends FieldValues = FieldValues, TName extends FieldPath = FieldPath, > = Omit & FormControlledFieldProps & { kind: "date" emptyValue?: unknown onValueChange?: (value: string) => void } export type FormInputDateRangeVariantProps< TFieldValues extends FieldValues = FieldValues, TName extends FieldPath = FieldPath, > = Omit & FormControlledFieldProps & { kind: "date-range" onValueChange?: (value: DateRangeValue) => void } export type FormInputClearableVariantProps< TFieldValues extends FieldValues = FieldValues, TName extends FieldPath = FieldPath, > = Omit & FormControlledFieldProps & { kind: "clearable" emptyValue?: unknown onValueChange?: (value: string) => void } export type FormInputMaskedVariantProps< TFieldValues extends FieldValues = FieldValues, TName extends FieldPath = FieldPath, > = Omit & FormControlledFieldProps & { kind: "masked" valueMode?: "masked" | "raw" onValueChange?: (maskedValue: string, rawValue: string) => void } export type FormInputMoneyVariantProps< TFieldValues extends FieldValues = FieldValues, TName extends FieldPath = FieldPath, > = Omit & FormControlledFieldProps & { kind: "money" emptyValue?: unknown onValueChange?: (value: number | null, rawValue: string) => void } export type FormInputQuantityVariantProps< TFieldValues extends FieldValues = FieldValues, TName extends FieldPath = FieldPath, > = Omit & FormControlledFieldProps & { kind: "quantity" emptyValue?: unknown onValueChange?: (value: number | null) => void } export type FormInputProps< TFieldValues extends FieldValues = FieldValues, TName extends FieldPath = FieldPath, > = | FormTextInputProps | FormInputSearchVariantProps | FormInputPasswordVariantProps | FormInputPhoneVariantProps | FormInputDateVariantProps | FormInputDateRangeVariantProps | FormInputClearableVariantProps | FormInputMaskedVariantProps | FormInputMoneyVariantProps | FormInputQuantityVariantProps type PasswordInputChangeEvent = React.ChangeEvent type InputTextInputProps = Omit function buildShellProps< TFieldValues extends FieldValues, TName extends FieldPath, >(props: FormInputProps) { return { label: props.label, description: props.description, success: props.success, loading: props.loading, required: props.required, className: props.className, htmlFor: props.id ?? props.name, layout: props.layout, descriptionPosition: props.descriptionPosition, labelAction: props.labelAction, requiredIndicator: props.requiredIndicator, errorIcon: props.errorIcon, showErrorIcon: props.showErrorIcon, disabled: props.disabled, readOnly: props.readOnly, labelClassName: props.labelClassName, labelRowClassName: props.labelRowClassName, descriptionClassName: props.descriptionClassName, errorClassName: props.errorClassName, contentClassName: props.contentClassName, } satisfies FormFieldShellControlProps } function FormInput< TFieldValues extends FieldValues = FieldValues, TName extends FieldPath = FieldPath, >(props: FormInputProps) { const inputId = props.id ?? props.name const shellProps = buildShellProps(props) const kind = props.kind ?? "text" const resolvedFieldClassName = props.fieldClassName ?? "w-full" return ( { const error = fieldState.error?.message const resolvedIds = resolveFormFieldIds(inputId, { description: props.description, error, }) if (kind === "search") { const { control: _control, name: _name, label: _label, description: _description, required: _required, className: _className, layout: _layout, descriptionPosition: _descriptionPosition, labelAction: _labelAction, requiredIndicator: _requiredIndicator, errorIcon: _errorIcon, showErrorIcon: _showErrorIcon, disabled, readOnly: _readOnly, labelClassName: _labelClassName, labelRowClassName: _labelRowClassName, descriptionClassName: _descriptionClassName, errorClassName: _errorClassName, contentClassName: _contentClassName, fieldClassName, id, kind: _kind, onValueChange, onBlur, ...searchProps } = props as FormInputSearchVariantProps return ( )} type="search" id={id ?? inputId} name={field.name} ref={field.ref} value={field.value ?? ""} disabled={disabled} readOnly={props.readOnly} inputClassName={fieldClassName ?? resolvedFieldClassName} aria-invalid={fieldState.invalid || undefined} aria-describedby={resolvedIds.describedBy} aria-errormessage={error ? resolvedIds.errorId : undefined} onBlur={(event: React.FocusEvent) => { field.onBlur() onBlur?.(event) }} onValueChange={(value: string) => { field.onChange(value) onValueChange?.(value) }} /> ) } if (kind === "password") { const { control: _control, name: _name, label: _label, description: _description, required: _required, className: _className, layout: _layout, descriptionPosition: _descriptionPosition, labelAction: _labelAction, requiredIndicator: _requiredIndicator, errorIcon: _errorIcon, showErrorIcon: _showErrorIcon, disabled, readOnly: _readOnly, labelClassName: _labelClassName, labelRowClassName: _labelRowClassName, descriptionClassName: _descriptionClassName, errorClassName: _errorClassName, contentClassName: _contentClassName, fieldClassName, id, kind: _kind, onValueChange, onBlur, ...passwordProps } = props as FormInputPasswordVariantProps return ( )} type="password" id={id ?? inputId} ref={field.ref} name={field.name} value={field.value ?? ""} disabled={disabled} readOnly={props.readOnly} inputClassName={fieldClassName ?? resolvedFieldClassName} aria-describedby={resolvedIds.describedBy} aria-errormessage={error ? resolvedIds.errorId : undefined} onBlur={(event: React.FocusEvent) => { field.onBlur() onBlur?.(event) }} onChange={(event: PasswordInputChangeEvent) => { const nextValue = event.currentTarget.value field.onChange(nextValue) onValueChange?.(nextValue) }} aria-invalid={fieldState.invalid || undefined} /> ) } if (kind === "phone") { const { control: _control, name: _name, label: _label, description: _description, required: _required, className: _className, layout: _layout, descriptionPosition: _descriptionPosition, labelAction: _labelAction, requiredIndicator: _requiredIndicator, errorIcon: _errorIcon, showErrorIcon: _showErrorIcon, disabled, readOnly: _readOnly, labelClassName: _labelClassName, labelRowClassName: _labelRowClassName, descriptionClassName: _descriptionClassName, errorClassName: _errorClassName, contentClassName: _contentClassName, fieldClassName, id, kind: _kind, valueMode = "raw", countryCode = "+998", maxDigits = 12, onValueChange, onBlur, ...phoneProps } = props as FormInputPhoneVariantProps const fieldValue = field.value == null ? "" : String(field.value) const displayValue = valueMode === "raw" ? formatPhoneDigits(fieldValue, countryCode, maxDigits) : fieldValue return ( ) => { field.onBlur() onBlur?.(event) }} onValueChange={(maskedValue: string, rawValue: string) => { const nextValue = valueMode === "raw" ? rawValue : maskedValue field.onChange(nextValue) onValueChange?.(nextValue, rawValue, maskedValue) }} /> ) } if (kind === "date") { const { control: _control, name: _name, label: _label, description: _description, required: _required, className: _className, layout: _layout, descriptionPosition: _descriptionPosition, labelAction: _labelAction, requiredIndicator: _requiredIndicator, errorIcon: _errorIcon, showErrorIcon: _showErrorIcon, disabled, readOnly: _readOnly, labelClassName: _labelClassName, labelRowClassName: _labelRowClassName, descriptionClassName: _descriptionClassName, errorClassName: _errorClassName, contentClassName: _contentClassName, fieldClassName, id, kind: _kind, emptyValue = "", onValueChange, onBlur, ...dateProps } = props as FormInputDateVariantProps return ( ) => { field.onBlur() onBlur?.(event) }} onValueChange={(value: string) => { field.onChange(value || emptyValue) onValueChange?.(value) }} /> ) } if (kind === "clearable") { const { control: _control, name: _name, label: _label, description: _description, required: _required, className: _className, layout: _layout, descriptionPosition: _descriptionPosition, labelAction: _labelAction, requiredIndicator: _requiredIndicator, errorIcon: _errorIcon, showErrorIcon: _showErrorIcon, disabled, readOnly: _readOnly, labelClassName: _labelClassName, labelRowClassName: _labelRowClassName, descriptionClassName: _descriptionClassName, errorClassName: _errorClassName, contentClassName: _contentClassName, fieldClassName, id, kind: _kind, emptyValue = "", onValueChange, onBlur, ...clearableProps } = props as FormInputClearableVariantProps return ( )} clearable id={id ?? inputId} name={field.name} ref={field.ref} value={field.value ?? ""} disabled={disabled} readOnly={props.readOnly} className={fieldClassName ?? resolvedFieldClassName} aria-invalid={fieldState.invalid || undefined} aria-describedby={resolvedIds.describedBy} aria-errormessage={error ? resolvedIds.errorId : undefined} onBlur={(event: React.FocusEvent) => { field.onBlur() onBlur?.(event) }} onValueChange={(nextValue: string) => { field.onChange(nextValue || emptyValue) onValueChange?.(nextValue) }} /> ) } if (kind === "masked") { const { control: _control, name: _name, label: _label, description: _description, required: _required, className: _className, layout: _layout, descriptionPosition: _descriptionPosition, labelAction: _labelAction, requiredIndicator: _requiredIndicator, errorIcon: _errorIcon, showErrorIcon: _showErrorIcon, disabled, readOnly: _readOnly, labelClassName: _labelClassName, labelRowClassName: _labelRowClassName, descriptionClassName: _descriptionClassName, errorClassName: _errorClassName, contentClassName: _contentClassName, fieldClassName, id, kind: _kind, valueMode = "raw", onValueChange, onBlur, ...maskedProps } = props as FormInputMaskedVariantProps return ( ) => { field.onBlur() onBlur?.(event) }} onValueChange={(maskedValue: string, rawValue: string) => { const nextValue = valueMode === "raw" ? rawValue : maskedValue field.onChange(nextValue) onValueChange?.(maskedValue, rawValue) }} /> ) } if (kind === "money") { const { control: _control, name: _name, label: _label, description: _description, required: _required, className: _className, layout: _layout, descriptionPosition: _descriptionPosition, labelAction: _labelAction, requiredIndicator: _requiredIndicator, errorIcon: _errorIcon, showErrorIcon: _showErrorIcon, disabled, readOnly: _readOnly, labelClassName: _labelClassName, labelRowClassName: _labelRowClassName, descriptionClassName: _descriptionClassName, errorClassName: _errorClassName, contentClassName: _contentClassName, fieldClassName, id, kind: _kind, emptyValue = null, onValueChange, onBlur, ...moneyProps } = props as FormInputMoneyVariantProps return ( ) => { field.onBlur() onBlur?.(event) }} onValueChange={(nextValue: number | null, rawValue: string) => { field.onChange(nextValue ?? emptyValue) onValueChange?.(nextValue, rawValue) }} /> ) } if (kind === "quantity") { const { control: _control, name: _name, label: _label, description: _description, required: _required, className: _className, layout: _layout, descriptionPosition: _descriptionPosition, labelAction: _labelAction, requiredIndicator: _requiredIndicator, errorIcon: _errorIcon, showErrorIcon: _showErrorIcon, disabled, readOnly: _readOnly, labelClassName: _labelClassName, labelRowClassName: _labelRowClassName, descriptionClassName: _descriptionClassName, errorClassName: _errorClassName, contentClassName: _contentClassName, fieldClassName, id, kind: _kind, emptyValue = null, onValueChange, onBlur, ...quantityProps } = props as FormInputQuantityVariantProps return ( ) => { field.onBlur() onBlur?.(event) }} onValueChange={(nextValue: number | null) => { field.onChange(nextValue ?? emptyValue) onValueChange?.(nextValue) }} /> ) } if (kind === "date-range") { const { control: _control, name: _name, label: _label, description: _description, required: _required, className: _className, layout: _layout, descriptionPosition: _descriptionPosition, labelAction: _labelAction, requiredIndicator: _requiredIndicator, errorIcon: _errorIcon, showErrorIcon: _showErrorIcon, disabled, labelClassName: _labelClassName, labelRowClassName: _labelRowClassName, descriptionClassName: _descriptionClassName, errorClassName: _errorClassName, contentClassName: _contentClassName, fieldClassName, id, kind: _kind, onValueChange, onBlur, ...rangeProps } = props as FormInputDateRangeVariantProps const currentValue = field.value as DateRangeValue | undefined return ( { field.onChange(value) onValueChange?.(value) }} aria-invalid={fieldState.invalid || undefined} onBlur={(event: React.FocusEvent) => { field.onBlur() onBlur?.(event) }} /> ) } const { control: _control, name: _name, label: _label, description: _description, required: _required, className: _className, layout: _layout, descriptionPosition: _descriptionPosition, labelAction: _labelAction, requiredIndicator: _requiredIndicator, errorIcon: _errorIcon, showErrorIcon: _showErrorIcon, disabled, readOnly, labelClassName: _labelClassName, labelRowClassName: _labelRowClassName, descriptionClassName: _descriptionClassName, errorClassName: _errorClassName, contentClassName: _contentClassName, fieldClassName, transformIn, transformOut, id, kind: _kind, onChange, onBlur, ...inputProps } = props as FormTextInputProps return ( )} id={id ?? inputId} ref={field.ref} name={field.name} value={ (transformIn ? transformIn(field.value) : field.value ?? "") as unknown as InputTextInputProps["value"] } disabled={disabled} readOnly={readOnly} onBlur={(event: React.FocusEvent) => { field.onBlur() onBlur?.(event) }} onChange={(event: TextInputElementChangeEvent) => { field.onChange(transformOut ? transformOut(event) : event.target.value) onChange?.(event) }} aria-invalid={fieldState.invalid || undefined} aria-describedby={resolvedIds.describedBy} aria-errormessage={error ? resolvedIds.errorId : undefined} className={fieldClassName ?? resolvedFieldClassName} /> ) }} /> ) } export { FormInput }