"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
* `