import { useRef, type KeyboardEvent } from 'react' import styles from './Segmented.module.css' export type SegmentedOption = { value: T label: string // Accessible name, when the visible label alone does not say what the // option is (the A / A / A of a text size). ariaLabel?: string } export type SegmentedProps = { // Accessible name of the radiogroup. label: string options: readonly SegmentedOption[] value: T onValueChange: (value: T) => void className?: string } const step: Record = { ArrowRight: 1, ArrowDown: 1, ArrowLeft: -1, ArrowUp: -1 } // Segmented control with radiogroup semantics: one tab stop (the selected // option); arrow keys move the selection and focus, wrapping at the ends; // Home and End jump to the first and last option. export function Segmented({ label, options, value, onValueChange, className }: SegmentedProps) { const refs = useRef<(HTMLButtonElement | null)[]>([]) const selected = options.findIndex((option) => option.value === value) if (selected === -1) { throw new Error(`Segmented "${label}": value "${value}" is not one of its options`) } function select(index: number) { const option = options[index] if (!option) throw new Error(`Segmented "${label}": no option at index ${index}`) onValueChange(option.value) refs.current[index]?.focus() } function onKeyDown(event: KeyboardEvent) { const delta = step[event.key] let next: number if (delta !== undefined) next = (selected + delta + options.length) % options.length else if (event.key === 'Home') next = 0 else if (event.key === 'End') next = options.length - 1 else return event.preventDefault() select(next) } return (
{options.map((option, index) => { const checked = index === selected return ( ) })}
) }