Files
UI/packages/skin/ui/src/drawer.ts
T

61 lines
2.9 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.
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<HTMLElement>('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;
}