Files
Endoleap/src/components/canvas/frame-label-menu.tsx
T

35 lines
2.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 分区名称显示模式的受控单选控件。显示当前值并通过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>;
}