listbox というUIパターンを実装しました。ちょっと凝ったラジオボタンリストみたいなものです↓
仕様
クリック時の挙動はだいたいラジオボタンと同じです。
キーボード操作時がラジオボタンと違っています。ラジオボタンではフォーカス移動と同時に選択が行われますが、このリストボックスではフォーカスを移動させただけでは選択されず、スペースで決定することで初めて選択されます。
実装
実装はこんな感じです。azukiazusaさんの実装を大いに参考にしてます→ 【React】アクセシビリティに考慮したリストボックスを実装する (コンボボックスがない、ステートではなくブラウザのフォーカス機能でフォーカス管理をしているあたりが異なります)
const TinyListbox = ({ options, selectedIndex, onChangeIndex, }: { options: string[]; selectedIndex: number; onChangeIndex: (index: number) => void; }) => { const optionsRef = useRef<(HTMLDivElement | null)[]>([]); return ( <div role="listbox" tabIndex={-1} onKeyDown={(e) => { const focusedIndex = optionsRef.current.findIndex( (ref) => document.activeElement === ref ); if (focusedIndex === -1) return; // 上下キーでフォーカスを移動させる switch (e.key) { case "ArrowDown": e.preventDefault(); optionsRef.current[(focusedIndex + 1) % options.length]?.focus(); break; case "ArrowUp": e.preventDefault(); optionsRef.current[ (focusedIndex - 1 + options.length) % options.length ]?.focus(); break; default: break; } }} style={{ display: "flex", gap: "8px", outline: "none" }} > {options.map((option, index) => ( <div key={option} role="option" aria-selected={selectedIndex === index} tabIndex={selectedIndex === index ? 0 : -1} onKeyDown={(e) => { // フォーカスされてるときにスペースあるいはエンターされたら選択する switch (e.key) { case "Enter": case " ": e.preventDefault(); onChangeIndex(index); break; } }} onClick={() => onChangeIndex(index)} style={{ padding: "4px 8px", background: selectedIndex === index ? "#ddd" : "#fff", cursor: "pointer", }} ref={(el) => { optionsRef.current[index] = el; return () => (optionsRef.current[index] = null); }} > {option} </div> ))} </div> ); };