import type { CSSProperties, FocusEvent, HTMLAttributes, KeyboardEvent, MouseEvent, ReactNode, } from "react"; import { createRef, forwardRef, useCallback, useMemo, useState } from "react"; import { loop } from "../../loop"; import type { LabelRequiredForA11y } from "../../types"; import { tryToSubmitRelatedForm } from "../tryToSubmitRelatedForm"; import { RadioWidget } from "./RadioWidget"; import type { RadioWidgetAttributes, RadioItemStyleObject, RadioItem, } from "./types"; import { defaultGetRadioClassName, defaultGetRadioStyle, getRadioItemValue, } from "./utils"; /** * This is a controlled component to render a group of radio buttons when the * `` does not work. * * @remarks \@since 2.7.0 */ export interface BaseRadioGroupProps extends Omit, "onChange"> { /** */ id: string; /** * The current value for the radio group. This should be the empty string * (`""`) if no values are selected. Otherwise it should match one of the * `values`' value. */ value: string; /** * A list of values/radio props that should be used to render the radio items. */ items: readonly RadioItem[]; /** * A function that changes the current selection within the radio group. */ onChange(nextValue: string): void; /** * An optional function to get a `style` object for each rendered radio. */ getRadioStyle?(item: RadioItemStyleObject): CSSProperties | undefined; /** * An optional function to get a `className` for each rendered radio. */ getRadioClassName?(item: RadioItemStyleObject): string | undefined; } /** * @remarks \@since 2.7.0 */ export type RadioGroupProps = LabelRequiredForA11y; /** * The `RadioGroup` is a low-level component that does not provide any styles * and instead only provides the accessibility required for a * `role="radiogroup"` and rendering each `role="radio"` item. * * @remarks \@since 2.7.0 */ export const RadioGroup = forwardRef( function RadioGroup( { id, getRadioStyle = defaultGetRadioStyle, getRadioClassName = defaultGetRadioClassName, items, value: currentValue, onBlur, onFocus, onClick, onChange, onKeyDown, ...props }, ref ) { const refs = items.map(() => createRef()); const [focused, setFocused] = useState(false); const handleBlur = useCallback( (event: FocusEvent) => { onBlur?.(event); setFocused(false); }, [onBlur] ); const handleFocus = useCallback( (event: FocusEvent) => { onFocus?.(event); setFocused(true); }, [onFocus] ); const handleClick = useCallback( (event: MouseEvent) => { onClick?.(event); /* istanbul ignore next: can't really happen */ const radio = (event.target as HTMLElement)?.closest( '[role="radio"]' ); const index = radio ? refs.findIndex(({ current }) => radio === current) : -1; if (index !== -1) { onChange(getRadioItemValue(items[index])); /* istanbul ignore next: can't really happen */ refs[index].current?.focus(); } }, [onChange, onClick, refs, items] ); const handleKeyDown = useCallback( (event: KeyboardEvent) => { onKeyDown?.(event); if (tryToSubmitRelatedForm(event)) { return; } if ( ![" ", "ArrowLeft", "ArrowUp", "ArrowRight", "ArrowDown"].includes( event.key ) ) { return; } /* istanbul ignore next: can't really happen */ const radio = (event.target as HTMLElement)?.closest( '[role="radio"]' ); if (!radio) { return; } event.preventDefault(); event.stopPropagation(); if (event.key === " ") { radio.click(); return; } const increment = event.key === "ArrowRight" || event.key === "ArrowDown"; const index = refs.findIndex(({ current }) => current === radio); /* istanbul ignore next: can't really happen */ if (index !== -1) { const nextIndex = loop({ value: index, max: items.length - 1, increment, }); refs[nextIndex].current?.focus(); onChange(getRadioItemValue(items[nextIndex])); } }, [onChange, onKeyDown, refs, items] ); const focusable = useMemo( () => items.some((value) => getRadioItemValue(value) === currentValue), [currentValue, items] ); return ( {items.map((item, i) => { let props: RadioWidgetAttributes | undefined; let value: string; let checked = false; let children: ReactNode; let itemStyle: CSSProperties | undefined; let itemClassName: string | undefined; if (typeof item === "string") { value = item; checked = currentValue === value; children = value; itemStyle = getRadioStyle({ index: i, checked, value: item }); itemClassName = getRadioClassName({ index: i, checked, value: item, }); } else { ({ value, children, ...props } = item); checked = currentValue === value; itemStyle = getRadioStyle({ index: i, checked, ...item }); itemClassName = getRadioClassName({ index: i, checked, ...item, }) || undefined; if (typeof children === "undefined") { children = value; } } return ( {children} ); })} ); } );