"use client"; import { type HTMLAttributes, type ReactNode, type Ref, type RefCallback, type RefObject, useCallback, useRef, useState, } from "react"; import { getIcon } from "../icon/config.js"; import { type UseStateInitializer, type UseStateSetter } from "../types.js"; import { useEnsuredId } from "../useEnsuredId.js"; import { useEnsuredRef } from "../useEnsuredRef.js"; import { type TextFieldProps } from "./TextField.js"; import { type ConfigurableFormMessageProps, type FormMessageInputLengthCounterProps, } from "./types.js"; import { useFormReset } from "./useFormReset.js"; import { type ErrorMessageOptions, type GetErrorIcon, type GetErrorMessage, type IsErrored, type TextFieldValidationOptions, type TextFieldValidationType, defaultGetErrorIcon, defaultGetErrorMessage, defaultIsErrored, } from "./validation.js"; const noop = (): void => { // do nothing }; /** * @since 2.5.0 * @since 6.0.0 Added the `onInvalid` handler */ export type TextFieldChangeHandlers< E extends HTMLInputElement | HTMLTextAreaElement = HTMLInputElement, > = Pick, "onBlur" | "onChange" | "onInvalid">; /** @since 6.0.0 */ export interface ErrorChangeHandlerOptions< E extends HTMLInputElement | HTMLTextAreaElement = HTMLInputElement, > { /** * A ref containing the `TextField` or `TextArea` if you need access to that * DOM node for error reporting. */ ref: RefObject; /** * The current name for the `TextField` or `TextArea`. */ name: string; /** * This will be `true` when the `TextField`/`TextArea` has an error. */ error: boolean; /** * The error message returned by {@link GetErrorMessage}/the browser's * validation message. This is normally an empty string when the {@link error} * state is `false`. */ errorMessage: string; } /** * A function that reports the error state changing. A good use-case for this is * to keep track of all the errors within your form and keep a submit button * disabled until they have been resolved. * * Example: * * ```ts * const [errors, setErrors] = useState>({}); * const onErrorChange: ErrorChangeHandler = ({ name, error }) => * setErrors((prevErrors) => ({ ...prevErrors, [name]: error })); * * const invalid = Object.values(errors).some(Boolean); * * // form implementation is left as an exercise for the reader * * ``` * * @since 2.5.0 * @since 6.0.0 Changed to object argument. */ export type ErrorChangeHandler< E extends HTMLInputElement | HTMLTextAreaElement = HTMLInputElement, > = (options: ErrorChangeHandlerOptions) => void; /** @since 2.5.6 */ export interface TextFieldHookState { /** * The current value for the `TextField` or `TextArea`. */ value: string; /** * This will be `true` when the `TextField`/`TextArea` has an error. */ error: boolean; /** * The error message returned by {@link GetErrorMessage}/the browser's * validation message. This is normally an empty string when the {@link error} * state is `false`. */ errorMessage: string; } /** * All the props that will be generated and return from the `useTextField` hook * that should be passed to a `FormMessage` component. * * @since 2.5.0 */ export interface ProvidedFormMessageProps extends Pick, Required>, Partial> {} /** * All the props that will be generated and returned by the `useTextField` hook * that should be passed to a `TextField` component. * * @since 2.5.0 */ export interface ProvidedTextFieldProps< E extends HTMLInputElement | HTMLTextAreaElement = HTMLInputElement, > extends TextFieldValidationOptions, Required>, Required>, Pick { /** * A ref that must be passed to the `TextField`/`TextArea` so that the custom * validity behavior can work. * * @since 6.0.0 */ ref: RefCallback; } /** * @since 2.5.0 */ export interface ProvidedTextFieldMessageProps< E extends HTMLInputElement | HTMLTextAreaElement = HTMLInputElement, > extends ProvidedTextFieldProps { /** * These props will be defined as long as the `disableMessage` prop is not * `true` from the `useTextField` hook. */ messageProps: ProvidedFormMessageProps; } /** * @since 6.3.0 */ export interface TextFieldHookComponentOptions< E extends HTMLInputElement | HTMLTextAreaElement = HTMLInputElement, > { /** * An optional id to use for the `TextField`, `Password`, or `TextArea` that * is also used to create an id for the inline help/error messages. * * @defaultValue `"text-field-" + useId()` */ id?: string; /** * A unique name to attach to the `TextField`, `TextArea`, or `Password` * component. */ name: string; /** * @since 6.3.0 */ form?: string; /** * Boolean if the `FormMessage` should also display a counter for the * remaining letters allowed based on the `maxLength`. * * This will still be considered false if the `maxLength` value is not * provided. * * @defaultValue `false` */ counter?: boolean; /** * An optional help text to display in the `FormMessage` component when there * is not an error. */ helpText?: ReactNode; /** * A function used to determine if the `TextField` or `TextArea` is an in * errored state. * * @see {@link defaultIsErrored} * @defaultValue `defaultIsErrored` */ isErrored?: IsErrored; /** * An optional error icon used in the {@link getErrorIcon} option. * * @defaultValue `getIcon("error")` */ errorIcon?: ReactNode; /** * A function used to get the error icon to display at the right of the * `TextField` or `TextArea`. The default behavior will only show an icon when * the `error` state is `true` and an `errorIcon` option has been provided. * * @see {@link defaultGetErrorIcon} * @defaultValue `defaultGetErrorIcon` */ getErrorIcon?: GetErrorIcon; /** * A function to get and display an error message based on the `TextField` or * `TextArea` validity. * * @see {@link defaultGetErrorMessage} * @defaultValue `defaultGetErrorMessage` */ getErrorMessage?: GetErrorMessage; /** * An optional function that will be called whenever the `error` state is * changed. This can be used for more complex forms to `disable` the Submit * button or anything else if any field has an error. * * @defaultValue `() => {}` */ onErrorChange?: ErrorChangeHandler; /** * Set to `true` to prevent the state from automatically resetting with a * form's `reset` event. * * @defaultValue `false` * @since 6.3.0 */ disableReset?: boolean; /** * Set this to `true` to prevent the `errorMessage` from being * rendered inline below the `TextField`. * * @defaultValue `false` * @since 6.0.0 Only disables the `errorMessage` behavior so * that counters and help text can still be rendered easily. */ disableMessage?: boolean; /** * Boolean if the `maxLength` prop should not be passed to the `TextField` * component since it will prevent any additional characters from being * entered in the text field which might feel like weird behavior to some * users. This should really only be used when the `counter` option is also * enabled and rendering along with a `FormMessage` component. * * @defaultValue `false` */ disableMaxLength?: boolean; /** * @defaultValue `"recommended"` */ validationType?: TextFieldValidationType; } /** @since 2.5.6 */ export interface TextFieldHookOptions< E extends HTMLInputElement | HTMLTextAreaElement = HTMLInputElement, > extends TextFieldValidationOptions, TextFieldHookComponentOptions, TextFieldChangeHandlers { /** * An optional ref that should be merged with the ref returned by this hook. * This should really only be used if you are making a custom component using * this hook and forwarding refs. If you need a ref to access the `` or * `