import { effect, h } from '@ui-arch/frame'; import type { DrawerProps } from './types.js'; /*** 抽屉:open 信号驱动侧滑面板,点遮罩 / × / Esc 关闭;Tab 焦点陷阱 + 关闭归还焦点,与 modal 的分工是不打断主视图、可从侧边保留上下文 ***/ export function drawer(props: DrawerProps, ...children: unknown[]): HTMLElement { const close = (): void => { if (!props.open()) return; // 卫语句:已关闭不重复触发回调 props.open(false); props.onClose?.(); }; const overlay = h('div', { class: () => `ua-drawer-overlay${props.open() ? ' ua-open' : ''}`, onClick: (e: Event) => { if (e.target === overlay) close(); // 卫语句:仅点遮罩本身关闭 }, }); const panel = h( 'aside', { class: `ua-drawer ua-drawer-${props.placement ?? 'right'}`, style: `width:${props.width ?? 320}px`, role: 'dialog', 'aria-hidden': () => String(!props.open()), }, h('div', { class: 'ua-drawer-head' }, h('h3', { class: 'ua-drawer-title' }, props.title), h('button', { class: 'ua-drawer-close', 'aria-label': '关闭抽屉', onClick: close }, '×')), h('div', { class: 'ua-drawer-body' }, ...children), props.footer ? h('div', { class: 'ua-drawer-foot' }, props.footer) : null, ); overlay.append(panel); let lastFocus: HTMLElement | null = null; // 打开前焦点宿主,关闭时归还 const focusables = (): HTMLElement[] => Array.from(panel.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])')) .filter((el) => !el.hasAttribute('disabled')); // 面板内可聚焦元素(排除禁用) effect(() => { if (props.open()) { lastFocus = document.activeElement as HTMLElement | null; // 记录打开前焦点 (panel.querySelector('button, input, textarea') as HTMLElement | null)?.focus(); // 打开时聚焦首个可交互元素 } else if (lastFocus) { lastFocus.focus(); // 关闭时归还焦点 lastFocus = null; } }); overlay.addEventListener('keydown', (e: KeyboardEvent) => { if (e.key !== 'Tab' || !props.open()) return; // 仅处理打开中的 Tab const els = focusables(); if (els.length === 0) { e.preventDefault(); return; } // 无可聚焦元素:拦截 Tab 防焦点逃逸 const first = els[0]; const last = els[els.length - 1]; if (e.shiftKey && (document.activeElement === first || !panel.contains(document.activeElement))) { e.preventDefault(); last.focus(); // Shift+Tab 到首位时回绕到末尾 } else if (!e.shiftKey && (document.activeElement === last || !panel.contains(document.activeElement))) { e.preventDefault(); first.focus(); // Tab 到末位时回绕到开头 } }); document.addEventListener('keydown', (e: KeyboardEvent) => { if (e.key === 'Escape' && props.open()) close(); }); return overlay; }