Files
writer-work-flow/apps/web/components/ui/SegmentedControl.tsx
Yaojia Wang 45025c36bf feat(ui): P4 无障碍与打磨收尾
- P4-1 对比度:muted-soft 提到 WCAG AA 小字(≥4.5:1)于两主题各面
- P4-3 ConflictCard 原文/建议加 Minus/Plus 图标 + sr-only 标签(不靠颜色)
- P4-4 动效:3 处 chevron transition-transform 加 motion-safe 门控
- P4-5 命令面板 ⌘K 键帽提示 + 键盘操作读数 + 空态引导 + mono 快捷键
- P2-8 SegmentedControl 升级 radiogroup(role=radio/aria-checked + 方向键 roving)
2026-07-11 08:12:36 +02:00

93 lines
2.4 KiB
TypeScript

import type { KeyboardEvent, ReactNode } from "react";
import { useRef } from "react";
import { cn, focusRing, segmentedClass, transitionUi } from "@/lib/ui/variants";
export interface SegmentOption<T extends string> {
value: T;
label: ReactNode;
}
interface SegmentedControlProps<T extends string> {
options: Array<SegmentOption<T>>;
value: T;
onChange: (value: T) => void;
ariaLabel: string;
className?: string;
}
export function SegmentedControl<T extends string>({
options,
value,
onChange,
ariaLabel,
className,
}: SegmentedControlProps<T>) {
const buttonsRef = useRef<Array<HTMLButtonElement | null>>([]);
const focusOption = (index: number) => {
const clamped = (index + options.length) % options.length;
const target = options[clamped];
if (!target) return;
onChange(target.value);
buttonsRef.current[clamped]?.focus();
};
const handleKeyDown = (event: KeyboardEvent<HTMLButtonElement>, index: number) => {
switch (event.key) {
case "ArrowRight":
case "ArrowDown":
event.preventDefault();
focusOption(index + 1);
break;
case "ArrowLeft":
case "ArrowUp":
event.preventDefault();
focusOption(index - 1);
break;
case "Home":
event.preventDefault();
focusOption(0);
break;
case "End":
event.preventDefault();
focusOption(options.length - 1);
break;
default:
break;
}
};
return (
<div className={segmentedClass(className)} role="radiogroup" aria-label={ariaLabel}>
{options.map((option, index) => {
const selected = option.value === value;
return (
<button
key={option.value}
ref={(node) => {
buttonsRef.current[index] = node;
}}
type="button"
role="radio"
aria-checked={selected}
tabIndex={selected ? 0 : -1}
onClick={() => onChange(option.value)}
onKeyDown={(event) => handleKeyDown(event, index)}
className={cn(
"rounded-md px-3 py-1.5 text-sm",
transitionUi,
focusRing,
selected
? "bg-panel text-cinnabar shadow-paper"
: "text-ink-soft hover:text-cinnabar",
)}
>
{option.label}
</button>
);
})}
</div>
);
}