Files
writer-work-flow/apps/web/components/AiToolbarMoreMenu.tsx

96 lines
2.8 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.

"use client";
import Link from "next/link";
import { MoreHorizontal } from "lucide-react";
import { useEffect, useId, useRef, useState } from "react";
import type { AiToolItem } from "@/lib/nav/ai-tools";
import type { ActiveNav } from "@/lib/nav/items";
import { buttonClass, cn, focusRing } from "@/lib/ui/variants";
interface AiToolbarMoreMenuProps {
items: AiToolItem[];
activeNav?: ActiveNav;
}
export function AiToolbarMoreMenu({
items,
activeNav,
}: AiToolbarMoreMenuProps) {
const [open, setOpen] = useState(false);
const menuId = useId();
const rootRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (!open) return;
const onKeyDown = (event: KeyboardEvent): void => {
if (event.key === "Escape") {
setOpen(false);
buttonRef.current?.focus();
}
};
const onPointerDown = (event: PointerEvent): void => {
const target = event.target;
if (target instanceof Node && !rootRef.current?.contains(target)) {
setOpen(false);
}
};
window.addEventListener("keydown", onKeyDown);
window.addEventListener("pointerdown", onPointerDown);
return () => {
window.removeEventListener("keydown", onKeyDown);
window.removeEventListener("pointerdown", onPointerDown);
};
}, [open]);
return (
<div ref={rootRef} className="relative">
<button
ref={buttonRef}
type="button"
aria-haspopup="true"
aria-expanded={open}
aria-controls={menuId}
onClick={() => setOpen((value) => !value)}
className={buttonClass({ variant: "secondary", size: "sm" })}
>
<MoreHorizontal className="h-4 w-4" aria-hidden="true" />
</button>
{open ? (
// 诚实的 Tab-only 折叠面板:普通导航链接,不冒充 ARIA menu无 roving tabindex/方向键)。
<nav
id={menuId}
aria-label="更多工具"
className="absolute right-0 z-30 mt-2 min-w-36 rounded border border-line bg-panel p-1 shadow-paper"
>
{items.map((item) => {
const active = item.key === activeNav;
return (
<Link
key={item.href}
href={item.href}
aria-current={active ? "page" : undefined}
onClick={() => setOpen(false)}
className={cn(
"block rounded px-3 py-2 text-sm transition-colors",
focusRing,
active
? "bg-[var(--color-cinnabar-wash)] text-cinnabar"
: "text-ink hover:bg-bg hover:text-cinnabar",
)}
>
{item.label}
</Link>
);
})}
</nav>
) : null}
</div>
);
}