"use client"; import { type ChangeEventHandler, type FormEventHandler, useCallback, useRef, useState, } from "react"; import { type UseStateInitializer, type UseStateSetter } from "../types.js"; const noop = (): void => { // do nothing }; /** @since 6.0.0 */ export interface RadioGroupOptions { /** * A `name` to apply to all the radios within the group. This is required if * unless {@link menu} is set to `true`. */ name?: string; /** * Set this to `true` if using the `MenuItemRadio` component instead of the * `Radio` so the correct props can be provided. * * @defaultValue `false` */ menu?: boolean; /** * The value of a radio that should be checked by default. If you want to * force the user to select one of the radios, keep this as the empty string * or set it to a string or number that does not represent a valid radio * value. * * @defaultValue `""` */ defaultValue?: UseStateInitializer; /** * Set this to `true` if one of the radios within the group must be checked before * a form can be submitted. * * This option is invalid and will be ignored if {@link menu} is `true`. * * @defaultValue `false` */ required?: boolean; /** * If you need to prevent the default behavior in a radio group for some * reason, you can provide a custom `onChange` event handler and call * `event.stopPropagation()`. This will be called whenever a new radio button * is checked. * * This option is invalid and will be ignored if {@link menu} is `true`. * * @defaultValue `() => {}` */ onChange?: ChangeEventHandler; /** * If the radio group has {@link required} set to `true`, the radios will gain * the `error` state if a form is submitted without a checked radio. If you * want to prevent that behavior for some reason, you can provide this * function and call `event.stopPropagation()`. * * This option is invalid and will be ignored if {@link menu} is `true`. * * @defaultValue `() => {}` */ onInvalid?: FormEventHandler; } /** @since 6.0.0 */ export interface ProvidedRadioGroupProps { name: string; value: V; checked: boolean; onChange: ChangeEventHandler; error: boolean; required: boolean; onInvalid: FormEventHandler; } /** @since 6.0.0 */ export type GetRadioGroupProps = ( value: V ) => Readonly>; /** @since 6.0.0 */ export interface RadioGroupImplementation { reset: () => void; value: V; setValue: UseStateSetter; getRadioProps: GetRadioGroupProps; } export type GetMenuItemRadioGroupProps = ( value: V ) => Readonly<{ checked: boolean; onCheckedChange: () => void }>; /** @since 6.0.0 */ export interface MenuItemRadioGroupImplementation { reset: () => void; value: V; setValue: UseStateSetter; getRadioProps: GetMenuItemRadioGroupProps; } /** @since 6.0.0 */ export interface CombinedRadioGroupReturnValue { reset: () => void; value: V; setValue: UseStateSetter; getRadioProps: (value: V) => { name?: string; value?: V; checked: boolean; error?: boolean; required?: boolean; onChange?: ChangeEventHandler; onCheckedChange?: () => void; onInvalid?: FormEventHandler; }; } // Note: These overrides are set up so that the value will default to any // string. /** * @example Generic Number Example * ```tsx * const { value, getRadioProps } = useRadioGroup({ * name: "group", * defaultValue: -1 * }); * * * return ( * <> * * * * * ); * ``` * * @see {@link https://react-md.dev/components/radio | Radio Demos} * @see {@link https://react-md.dev/hooks/use-radio-group | useRadioGroup Demos} * @since 6.0.0 */ export function useRadioGroup( options: RadioGroupOptions & { menu?: false; name: string; defaultValue: UseStateInitializer; } ): RadioGroupImplementation; export function useRadioGroup( options: RadioGroupOptions & { menu: true; name?: never; required?: never; onChange?: never; onInvalid?: never; defaultValue: UseStateInitializer; } ): MenuItemRadioGroupImplementation; /** * @example Generic String Example * ```tsx * const { value, getRadioProps } = useRadioGroup({ name: "group" }); * * return ( * <> * * * * * ); * ``` * * @example String Union Example * ```tsx * const values = [ * { label: "First", value: "a" }, * { label: "Second", value: "b" }, * { label: "Third", value: "c" }, * ] as const; * * type Values = typeof values[number]["value"]; * // ^ "a" | "b" | "c" * * const { value, getRadioProps } = useRadioGroup({ * name: "group", * defaultValue: "", * }); * * * return ( * <> * {values.map(({ label, value }) => ( * * ))} * * ); * ``` * * @see {@link https://react-md.dev/components/radio | Radio Demos} * @see {@link https://react-md.dev/hooks/use-radio-group | useRadioGroup Demos} * @since 6.0.0 */ export function useRadioGroup( options: RadioGroupOptions & { menu?: false; name: string; defaultValue?: UseStateInitializer; } ): RadioGroupImplementation; export function useRadioGroup( options: RadioGroupOptions & { menu: true; name?: never; required?: never; onChange?: never; onInvalid?: never; defaultValue?: UseStateInitializer; } ): MenuItemRadioGroupImplementation; /** * @example Strict Union Example * ```tsx * type ValidValues = 1 | 2 | 3 | 4 | "" | "a" | "b"; * * const { value, getRadioProps } = useRadioGroup({ * name: "group", * defaultValue: "" * }); * * * return ( * <> * * * * * * * * * ); * ``` * * @see {@link https://react-md.dev/components/radio | Radio Demos} * @see {@link https://react-md.dev/hooks/use-radio-group | useRadioGroup Demos} * @since 6.0.0 */ export function useRadioGroup( options: RadioGroupOptions ): CombinedRadioGroupReturnValue { const { name, defaultValue, menu = false, required, onChange = noop, onInvalid = noop, } = options; const [value, setValue] = useState(() => { if (typeof defaultValue === "function") { return defaultValue(); } return defaultValue ?? ("" as V); }); const initial = useRef(value); const [error, setError] = useState(false); return { reset: useCallback(() => { setError(false); setValue(initial.current); }, []), value, setValue, getRadioProps(radioValue) { const checked = value === radioValue; if (menu) { return { checked, onCheckedChange() { setValue(radioValue); }, }; } return { name, value: radioValue, error, checked, required, onChange(event) { onChange(event); setError(false); setValue(radioValue); }, onInvalid(event) { onInvalid(event); setError(true); }, }; }, }; }