import { useRef, type CSSProperties, type KeyboardEvent } from 'react'; import clsx from 'clsx'; import { Link } from '../../provider.js'; import Icon from '../Icon/index.js'; import styles from './styles.module.css'; export interface SegmentedItem { value: T; label: string; icon?: string; /** * If provided, this item renders as a navigation Link instead of a button. * `onChange` is not invoked for href items — navigation drives the next value. */ href?: string; /** Inline style applied to the item's button/link element. */ style?: CSSProperties; } export interface SegmentedProps { value: T; items: SegmentedItem[]; /** Required only when at least one item is button-style (no href). */ onChange?: (value: T) => void; size?: 'sm' | 'md'; orientation?: 'vertical' | 'horizontal'; /** Viewport width at which horizontal items stack. Use 0 to keep them horizontal. */ stackAt?: 0 | 360 | 480; className?: string; /** * Accessible name for the group. Applied to the radiogroup in button mode; * href (navigation) mode ignores it since each link names itself. */ ariaLabel?: string; } export default function Segmented({ value, items, onChange, size = 'md', orientation = 'vertical', stackAt = 480, className, ariaLabel, }: SegmentedProps) { // href items are navigation (aria-current); button items are a single-select // radiogroup (aria-checked + roving tabindex + arrow-key movement). const isNav = items.some((item) => item.href != null); const buttonRefs = useRef<(HTMLButtonElement | null)[]>([]); const activeExists = items.some((item) => item.value === value); const moveFocus = (event: KeyboardEvent, index: number) => { const [next, prev] = orientation === 'horizontal' ? ['ArrowRight', 'ArrowLeft'] : ['ArrowDown', 'ArrowUp']; const dir = event.key === next ? 1 : event.key === prev ? -1 : 0; if (dir === 0) return; event.preventDefault(); const n = items.length; const target = (index + dir + n) % n; onChange?.(items[target].value); buttonRefs.current[target]?.focus(); }; return (
{items.map((item, index) => { const isActive = value === item.value; const itemClass = clsx(styles.item, isActive && styles.itemActive); const inner = ( <> {item.icon && } {item.label} ); const key = String(item.value ?? '__null__'); if (item.href) { return ( {inner} ); } return ( ); })}
); }