UNPKG

@react-md/form

Version:

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

382 lines (354 loc) 10.4 kB
import type { Dispatch, SetStateAction } from "react"; import { useState } from "react"; /** * @internal * @remarks \@since 2.8.5 */ type Initializer<V extends string> = readonly V[] | (() => readonly V[]); /** * The change handler for indeterminate checkboxes. * * @param values - The current list of checked values. * @typeParam V - The values allowed for the list of checkboxes. * @internal * @remarks \@since 2.8.5 */ type OnChange<V extends string> = (values: readonly V[]) => void; /** * @remarks \@since 2.8.5 * @typeParam V - The values allowed for the list of checkboxes. */ export interface IndeterminateCheckedHookOptions<V extends string> { /** * Enabling this option will update the returned props to rename `onChange` to * `onCheckedChange` to work with the {@link MenuItemCheckbox} component. * * @defaultValue `false` */ menu?: boolean; /** * This is the `useState` initializer that can be used if some checkboxes should * be checked by default. */ onChange?: OnChange<V>; /** * The change handler for indeterminate checkboxes. * * @param values - The current list of checked values. */ defaultCheckedValues?: Initializer<V>; } /** @remarks \@since 2.8.5 */ export interface BaseProvidedIndeterminateCheckboxProps { /** * Note: This will only be provided when the {@link indeterminate} prop is * `true`. */ "aria-checked"?: "mixed"; /** * Boolean if the root checkbox is currently checked. */ checked: boolean; /** * This will be set to `true` when at least one checkbox has been checked but * not every checkbox to enable the {@link CheckboxProps.indeterminate} state. */ indeterminate: boolean; } /** * @remarks \@since 2.8.5 * @internal */ export interface ProvidedIndeterminateCheckboxProps extends BaseProvidedIndeterminateCheckboxProps { onChange(): void; } /** * @remarks \@since 2.8.5 * @internal */ export interface ProvidedIndeterminateMenuItemCheckboxProps extends BaseProvidedIndeterminateCheckboxProps { onCheckedChange(): void; } /** * @remarks \@since 2.8.5 * @internal */ interface ProvidedCombinedIndeterminateProps extends BaseProvidedIndeterminateCheckboxProps { onChange?(): void; onCheckedChange?(): void; } /** * @remarks \@since 2.8.5 * @typeParam V - The values allowed for the list of checkboxes. */ export interface BaseProvidedIndeterminateControlledCheckboxProps< V extends string > { /** * One of the values provided to the {@link useIndeterminateChecked} hook. */ value: V; /** * Boolean if the current checkbox is checked. */ checked: boolean; } /** * @remarks \@since 2.8.5 * @typeParam V - The values allowed for the list of checkboxes. * @internal */ export interface ProvidedIndeterminateControlledCheckboxProps<V extends string> extends BaseProvidedIndeterminateControlledCheckboxProps<V> { onChange(): void; } /** * @remarks \@since 2.8.5 * @typeParam V - The values allowed for the list of checkboxes. * @internal */ export interface ProvidedIndeterminateControlledMenuItemCheckboxProps< V extends string > extends BaseProvidedIndeterminateControlledCheckboxProps<V> { onCheckedChange(): void; } /** * @remarks \@since 2.8.5 * @typeParam V - The values allowed for the list of checkboxes. * @internal */ interface ProvidedCombinedIndeterminateControlledProps<V extends string> extends BaseProvidedIndeterminateControlledCheckboxProps<V> { onChange?(): void; onCheckedChange?(): void; } /** * @remarks \@since 2.8.5 * @typeParam V - The values allowed for the list of checkboxes. */ export interface BaseIndeterminateCheckedHookReturnValue<V extends string> { /** * A list of all the values that are currently checked. */ checkedValues: readonly V[]; /** * A function to manually override the {@link checkedValues} if the default * hook's implementation does not work for your use-case. */ setCheckedValues: Dispatch<SetStateAction<readonly V[]>>; } /** * @remarks \@since 2.8.5 * @typeParam V - The values allowed for the list of checkboxes. * @internal */ interface OnChangeReturnValue<V extends string> extends BaseIndeterminateCheckedHookReturnValue<V> { rootProps: ProvidedIndeterminateCheckboxProps; getProps(value: V): ProvidedIndeterminateControlledCheckboxProps<V>; } /** * @remarks \@since 2.8.5 * @typeParam V - The values allowed for the list of checkboxes. * @internal */ interface OnCheckedChangeReturnValue<V extends string> extends BaseIndeterminateCheckedHookReturnValue<V> { rootProps: ProvidedIndeterminateMenuItemCheckboxProps; getProps(value: V): ProvidedIndeterminateControlledMenuItemCheckboxProps<V>; } /** * @remarks \@since 2.8.5 * @typeParam V - The values allowed for the list of checkboxes. * @internal */ export interface CombinedIndeterminateCheckedHookReturnValue<V extends string> extends BaseIndeterminateCheckedHookReturnValue<V> { rootProps: ProvidedCombinedIndeterminateProps; getProps(value: V): ProvidedCombinedIndeterminateControlledProps<V>; } /** * This hook allows you to toggle the state of multiple checkboxes in a single * place along with an indeterminate checkbox that can check/uncheck all * checkboxes at once. * * @example * Simple value list with labels lookup: * ```tsx * const values = ["a", "b", "c", "d"] as const; * const LABELS = { * a: "Label 1", * b: "Label 2", * c: "Label 3", * d: "Label 4", * } as const; * const { getProps, rootProps } = useIndeterminateChecked(values); * * return ( * <> * <Checkbox id="root-checkbox" {...rootProps} label="Root Checkbox" /> * {values.map((value, i) => ( * <Checkbox * id={`child-checkbox-${i + 1}`} * label={LABELS[value]} * {...getProps(value)} * /> * ))} * </> * ); * ``` * * @example * Fetch Data From Server and check first result * ```tsx * interface ServerFetchedData { * id: Guid; * name: string; * } * * * const [data, setData] = useState<readonly ServerFetchedData[]>([]); * const { getProps, rootProps, setCheckedValues } = useIndeterminateChecked( * data.map(({ id }) => id), * ); * * useEffect(() => { * let cancelled = false; * (async function load() { * const response = await fetch("/my-api"); * const json = await response.json(); * if (!cancelled) { * // pretend validation and sanity checks * setData(json); * setCheckedValues(json[0].id); * } * })(); * return () => { * cancelled = true; * }; * }, []); * * return ( * <> * <Checkbox id="root-checkbox" {...rootProps} label="Root Checkbox" /> * {data.map(({ id, name }, i) => ( * <Checkbox * id={`child-checkbox-${i + 1}`} * label={name} * {...getProps(id)} * /> * ))} * </> * ); * ``` * * @example * With MenuItemCheckbox * ```tsx * const values = ["a", "b", "c", "d"] as const; * const LABELS = { * a: "Label 1", * b: "Label 2", * c: "Label 3", * d: "Label 4", * } as const; * const { getProps, rootProps } = useIndeterminateChecked(values, { * menu: true, * }); * * return ( * <DropdownMenu id="dropdown-menu-id" buttonChildren="Button"> * <MenuItemCheckbox * id="dropdown-menu-id-toggle-all" * {...rootProps} * > * Toggle All * </MenuItemCheckbox> * {values.map((value, i) => ( * <MenuItemCheckbox * id={`dropdown-menu-id-${i + 1}`} * key={value} * {...getProps(value)} * > * {LABELS[value]} * </MenuItemCheckbox> * ))} * </DropdownMenu> * ); * ``` * * @typeParam V - The allowed values for the checkboxes * @param values - The allowed values for the checkboxes which is used to * control the checked states. * @param defaultOrOptions - The {@link IndeterminateCheckedHookOptions} or a * `useState` initializer callback/default value for backwards compatibility * @param optionalOnChange - This is really just for backwards compatibility and * should not be used. Use {@link IndeterminateCheckedHookOptions.onChange} * instead. * @returns an object containing the `rootProps` to pass to the indeterminate * checkbox, a `getProps` function to provide the controlled behavior for the * additional `values` in the checkbox list, a list of `checkedValues`, and a * `setCheckedValues` function to manually override the state if needed. */ export function useIndeterminateChecked<V extends string>( values: readonly V[], options?: IndeterminateCheckedHookOptions<V> & { menu?: false } ): OnChangeReturnValue<V>; export function useIndeterminateChecked<V extends string>( values: readonly V[], options: IndeterminateCheckedHookOptions<V> & { menu: true } ): OnCheckedChangeReturnValue<V>; export function useIndeterminateChecked<V extends string>( values: readonly V[], { menu = false, onChange: propOnChange, defaultCheckedValues = [], }: IndeterminateCheckedHookOptions<V> = {} ): CombinedIndeterminateCheckedHookReturnValue<V> { const [checkedValues, setCheckedValues] = useState<readonly V[]>(defaultCheckedValues); const checked = checkedValues.length > 0; const indeterminate = checked && checkedValues.length < values.length; const updateCheckedValues = (values: readonly V[]): void => { propOnChange?.(values); setCheckedValues(values); }; const rootProps: ProvidedCombinedIndeterminateProps = { "aria-checked": indeterminate ? "mixed" : undefined, checked, indeterminate, [menu ? "onCheckedChange" : "onChange"]: () => { updateCheckedValues( checkedValues.length === 0 || indeterminate ? values : [] ); }, }; const getProps = ( value: V ): ProvidedCombinedIndeterminateControlledProps<V> => { return { value, checked: checkedValues.includes(value), [menu ? "onCheckedChange" : "onChange"]: () => { const i = checkedValues.indexOf(value); const nextChecked = checkedValues.slice(); if (i === -1) { nextChecked.push(value); } else { nextChecked.splice(i, 1); } updateCheckedValues(nextChecked); }, }; }; return { rootProps, getProps, checkedValues, setCheckedValues, }; }