35 lines
2.7 KiB
TypeScript
35 lines
2.7 KiB
TypeScript
/**
|
||
* 分区名称显示模式的受控单选控件。显示当前值并通过change请求修改,组件本身不拥有分区模型。
|
||
*/
|
||
"use client";
|
||
import { useRef } from "react";
|
||
import { EyeOff, Scaling, Type } from "lucide-react";
|
||
import type { FrameLabelMode } from "@/lib/canvas-elements";
|
||
import styles from "./canvas.module.css";
|
||
/** 固定三种模式,数组顺序同时决定视觉滑块位置与键盘导航顺序。 */
|
||
const options = [
|
||
{ value: "hidden", label: "隐藏名称", hint: "在画布上隐藏分区名称", Icon: EyeOff },
|
||
{ value: "fixed", label: "固定字号", hint: "缩放画布时保持名称大小", Icon: Type },
|
||
{ value: "scaled", label: "随画布缩放", hint: "名称随画布一起放大缩小", Icon: Scaling },
|
||
] as const;
|
||
/** 鼠标点击切换模式;方向键循环移动,Home/End跳到首尾,并把焦点移至目标按钮。
|
||
* radio角色、aria-checked和单一Tab入口表达单选语义;SVG仅提供视觉提示。 */
|
||
export function FrameLabelMenu({ value, change }: { value: FrameLabelMode; change: (mode: FrameLabelMode) => void }) {
|
||
// 以数组保存三个真实按钮引用,键盘选择后直接聚焦;滑块索引在未知value时回退0。
|
||
const items = useRef<(HTMLButtonElement | null)[]>([]);
|
||
// 未知值只让视觉索引回退0,下面 radio 的 aria-checked/tabIndex 仍按原 value 比较,不会自动修复模型。
|
||
const current = Math.max(0, options.findIndex((option) => option.value === value));
|
||
// 键盘计算基于受控 value 而非实际焦点;change 应让宿主及时发布新值,连续按键才沿新索引前进。
|
||
// 左/上为-1,右/下为+1,通过加3再取模实现循环;Home/End直接使用0/2。
|
||
// 阻止默认滚动并停止冒泡,避免选择模式的按键同时移动画布;滑块每项28px。
|
||
return <div className={styles.frameLabelSegments} role="radiogroup" aria-label="分区名称显示模式" onKeyDown={(event) => {
|
||
const delta = { ArrowRight: 1, ArrowDown: 1, ArrowLeft: -1, ArrowUp: -1 }[event.key];
|
||
if (delta !== undefined || event.key === "Home" || event.key === "End") {
|
||
event.preventDefault(); event.stopPropagation(); const index = event.key === "Home" ? 0 : event.key === "End" ? 2 : (current + delta! + 3) % 3;
|
||
change(options[index].value); items.current[index]?.focus();
|
||
}
|
||
}}><span className={styles.frameLabelSlider} style={{ transform: `translateX(${current * 28}px)` }} />
|
||
{options.map(({ value: mode, label, hint, Icon }, index) => <button key={mode} ref={(element) => { items.current[index] = element; }} role="radio" aria-checked={value === mode} aria-label={label} title={`${label}:${hint}`} tabIndex={value === mode ? 0 : -1} onClick={() => change(mode)}><Icon size={15} /></button>)}
|
||
</div>;
|
||
}
|