"use client"; import { useEffect, useRef, type ReactNode, type RefObject } from "react"; import { X } from "lucide-react"; import { Button } from "@/components/ui/Button"; import { handleTabTrap } from "@/lib/a11y/focusTrap"; import { useRestoreFocus } from "@/lib/a11y/useRestoreFocus"; import { useBodyScrollLock } from "@/lib/ui/useBodyScrollLock"; import { useMountTransition } from "@/lib/ui/useMountTransition"; import { overlayScrim } from "@/lib/ui/variants"; // 抽屉进出场时长(与 --dur-base 一致)。 const DRAWER_MS = 180; interface DrawerProps { open: boolean; onClose: () => void; // 滑出方向:导航在左、本章助手在右。 side?: "left" | "right"; // 无障碍标签(role=dialog)。 label: string; // 抽屉面板 id:供触发按钮 aria-controls 指向(无障碍关联)。 id?: string; triggerRef?: RefObject; children: ReactNode; } // 移动端侧滑抽屉((null); const closeRef = useRef(null); const { shouldRender, isVisible } = useMountTransition(open, DRAWER_MS); useBodyScrollLock(open); useRestoreFocus(open, triggerRef); useEffect(() => { // 面板真正挂载(shouldRender)且处于打开态时才装监听/落焦—— // 因进出场延迟了卸载,若仅依赖 open 会错过挂载帧导致首焦点丢失。 if (!open || !shouldRender) return; const onKey = (e: KeyboardEvent): void => { if (e.key === "Escape") { e.preventDefault(); onClose(); } }; window.addEventListener("keydown", onKey); // 焦点陷阱首焦点落在具名关闭按钮上(而非整个面板)。 closeRef.current?.focus(); return () => window.removeEventListener("keydown", onKey); }, [open, shouldRender, onClose]); if (!shouldRender) return null; const sideClass = side === "left" ? "left-0 border-r" : "right-0 border-l"; const slideClass = side === "left" ? isVisible ? "translate-x-0" : "-translate-x-full" : isVisible ? "translate-x-0" : "translate-x-full"; return (
); }