import { type ReactNode, useId } from "react"; import { type FieldValues, type Path, type RegisterOptions, useFormContext, useWatch, } from "react-hook-form"; import { charCountClass } from "../../utils/charCount.ts"; interface TextFieldProps { name: Path; label?: string; /** Optional control rendered to the right of the label (e.g. a reset button). */ labelAction?: ReactNode; type?: "text" | "email" | "password" | "url" | "search"; placeholder?: string; autoComplete?: string; autoFocus?: boolean; disabled?: boolean; rules?: RegisterOptions>; /** Cap input length at `max` (native `maxLength`). */ maxLength?: number; /** Show a live `n / maxLength` counter. Requires `maxLength`. */ showCount?: boolean; } /** Live `n / max` counter; isolated so only counted fields subscribe to value. */ function CharCounter( { name, max }: { name: Path; max: number }, ) { const value = useWatch({ name }) as string | undefined; const len = value?.length ?? 0; return ( {len} / {max} ); } /** * Native text input wired to react-hook-form via the surrounding `FormProvider`. * Renders a label, the input (with an optional character counter), and a * validation hint sourced from `formState.errors[name]`. */ export function TextField({ name, label, labelAction, type = "text", placeholder, autoComplete, autoFocus, disabled, rules, maxLength, showCount = false, }: TextFieldProps) { const id = useId(); const { register, formState: { errors } } = useFormContext(); const error = errors[name]; const counted = showCount && maxLength != null; return (
{(label || labelAction) && (
{label ? ( ) : } {labelAction}
)}
{counted && name={name} max={maxLength} />}
{error?.message && ( {String(error.message)} )}
); }