import { effect, h } from '@ui-arch/frame'; import type { DrawerProps } from './types.js'; import { lockScroll, pushOverlay, trapFocus } from './overlay.js'; let seq = 0; // 实例序号:给标题生成唯一 id,供 aria-labelledby 关联 /*** 抽屉:open 信号驱动侧滑面板,点遮罩 / × / Esc 关闭;Tab 焦点陷阱 + 关闭归还焦点,与 modal 的分工是不打断主视图、可从侧边保留上下文 ***/ export function drawer(props: DrawerProps, ...children: unknown[]): HTMLElement { const titleId = `ua-drawer-title-${++seq}`; // 标题 id:抽屉的可访问名 const modal = props.modal !== false; // 是否模态:false 时无遮罩、不锁滚动、不困焦点,页面仍可操作 const close = (): void => { if (!props.open()) return; // 卫语句:已关闭不重复触发回调 props.open(false); props.onClose?.(); }; const overlay = h('div', { class: () => `ua-drawer-overlay${props.open() ? ' ua-open' : ''}${modal ? '' : ' is-plain'}`, onClick: (e: Event) => { if (!modal) return; // 卫语句:非模态没有遮罩,点外面不算关闭 if (e.target !== overlay) return; // 卫语句:只处理点在遮罩本身的 if (props.closeOnOverlay === false) return; // 显式禁止时防误关,× 与 Esc 仍可用 close(); }, }); const panel = h( 'aside', { class: `ua-drawer ua-drawer-${props.placement ?? 'right'}`, style: `${props.placement === 'top' || props.placement === 'bottom' ? 'height' : 'width'}:${props.width ?? 320}px`, // 上下滑出改高度 role: 'dialog', 'aria-labelledby': titleId, 'aria-hidden': () => String(!props.open()), 'aria-modal': modal ? 'true' : 'false', }, h('div', { class: 'ua-drawer-head' }, h('h3', { class: 'ua-drawer-title', id: titleId }, 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; // 打开前焦点宿主,关闭时归还 let release: (() => void) | null = null; // Esc 栈注销函数,关闭时释放 let releaseLock: (() => void) | null = null; // 背景滚动锁解锁函数,全关才还原 effect(() => { if (props.open()) { lastFocus = document.activeElement as HTMLElement | null; // 记录打开前焦点 release ??= pushOverlay(close); // 入 Esc 栈:多弹层时只关最上层 if (modal) releaseLock ??= lockScroll(); // 仅模态期锁背景滚动 (panel.querySelector('button, input, textarea') as HTMLElement | null)?.focus(); // 打开时聚焦首个可交互元素 } else { release?.(); // 出 Esc 栈 release = null; releaseLock?.(); // 解锁背景滚动 releaseLock = null; lastFocus?.focus(); // 关闭时归还焦点 lastFocus = null; } }); if (modal) trapFocus(panel, () => props.open()); // 模态才困焦点:非模态要让用户能继续操作背后页面 return overlay; }