UNPKG

@react-md/form

Version:

This package is for creating all the different form input types.

145 lines (128 loc) 3.5 kB
import type { ChangeEvent, ChangeEventHandler, FocusEvent, FocusEventHandler, } from "react"; import { useCallback, useEffect, useRef, useState } from "react"; /** * @internal * @remarks \@since 2.5.2 */ type FormElement = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement; /** * @internal * @remarks \@since 2.5.2 */ interface EventHandlers<E extends FormElement> { onBlur?: FocusEventHandler<E>; onFocus?: FocusEventHandler<E>; onChange?: ChangeEventHandler<E>; } /** * @internal * @remarks \@since 2.5.2 */ interface FieldStatesOptions<E extends FormElement> extends EventHandlers<E> { value?: string | readonly string[]; defaultValue?: string | readonly string[]; } /** * @internal * @remarks \@since 2.5.2 */ interface ReturnValue<E extends FormElement> extends Required<EventHandlers<E>> { /** * Boolean if the TextField or TextArea current has a value with a `length > 0` * so that any labels will correctly float above the text field. This will * also make sure that number inputs will still be considered valued when * there is a `badInput` validity error. */ valued: boolean; /** * Boolean if the TextField or TextArea currently has focus. */ focused: boolean; } /** * This hook is used to handle the different states for the text field based on * the current value and user interaction. * * @internal * @remarks \@since 2.5.2 */ export function useFieldStates<E extends FormElement>({ onBlur, onFocus, onChange, value, defaultValue, }: FieldStatesOptions<E>): ReturnValue<E> { const [focused, setFocused] = useState(false); const [valued, setValued] = useState(() => { if (typeof value === "undefined") { return typeof defaultValue !== "undefined" && defaultValue.length > 0; } return value.length > 0; }); const handleBlur = useCallback( (event: FocusEvent<E>) => { if (onBlur) { onBlur(event); } setFocused(false); const input = event.currentTarget; if (input.getAttribute("type") === "number") { input.checkValidity(); setValued(input.validity.badInput || (value ?? input.value).length > 0); } }, [onBlur, value] ); const handleFocus = useCallback( (event: FocusEvent<E>) => { if (onFocus) { onFocus(event); } setFocused(true); }, [onFocus] ); const handleChange = useCallback( (event: ChangeEvent<E>) => { if (onChange) { onChange(event); } const input = event.currentTarget; if (input.getAttribute("type") === "number") { input.checkValidity(); /* istanbul ignore next */ if (input.validity.badInput) { return; } } setValued(input.value.length > 0); }, [onChange] ); // another way to handle this could be to just make the `valued` state derived // based on the `value`, but it gets wonky for number fields. This technically // still fails right now for number fields if you don't use the // `useNumberField` hook since the `value` will be set back to the empty // string on invalid numbers. const prevValue = useRef(value); useEffect(() => { if (prevValue.current !== value && typeof value === "string") { prevValue.current = value; setValued(value.length > 0); } }, [value]); return { valued, focused, onBlur: handleBlur, onFocus: handleFocus, onChange: handleChange, }; }