"use client"; import { useRef, type KeyboardEvent, type ReactNode } from "react"; export interface RadioOption { value: T; label: ReactNode; activeClass?: string; } interface RadioGroupProps { name: string; options: readonly RadioOption[]; value: T; onChange: (value: T) => void; size?: "xs" | "sm"; fullWidth?: boolean; } /** * Accessible segmented control implementing WAI-ARIA `radiogroup` roving tabIndex * and arrow/Home/End keyboard navigation (1 Tab stop per group instead of N). */ export function RadioGroup({ name, options, value, onChange, size = "sm", fullWidth = false, }: RadioGroupProps) { const buttonsRef = useRef<(HTMLButtonElement | null)[]>([]); function handleKeyDown(e: KeyboardEvent) { const currentIndex = Math.max(0, options.findIndex((o) => o.value === value)); let nextIndex = -1; if (e.key === "ArrowRight" || e.key === "ArrowDown") { nextIndex = (currentIndex + 1) % options.length; } else if (e.key === "ArrowLeft" || e.key === "ArrowUp") { nextIndex = (currentIndex - 1 + options.length) % options.length; } else if (e.key === "Home") { nextIndex = 0; } else if (e.key === "End") { nextIndex = options.length - 1; } if (nextIndex !== -1) { e.preventDefault(); onChange(options[nextIndex].value); buttonsRef.current[nextIndex]?.focus(); } } const pad = size === "xs" ? fullWidth ? "min-h-8 flex-1 min-w-0 px-1.5 py-1 text-xs" : "min-h-8 px-2.5 py-1 text-xs" : fullWidth ? "min-h-9 flex-1 min-w-0 px-2 py-1.5 text-sm" : "min-h-9 min-w-9 px-3 py-1.5 text-sm"; return (
{options.map((option, i) => { const checked = option.value === value; const activeStyle = option.activeClass ?? "bg-surface text-ink border border-selected-line/60 shadow-2xs"; return ( ); })}
); }