iter: d28 drawer 焦点陷阱(Tab/Shift+Tab 回绕、关闭归还焦点)
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "ui-arch-registry-index/0.1",
|
"$schema": "ui-arch-registry-index/0.1",
|
||||||
"generatedAt": "2026-10-23T09:10:27.467Z",
|
"generatedAt": "2026-10-23T09:11:22.226Z",
|
||||||
"repoRoot": ".",
|
"repoRoot": ".",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { effect, h } from '@ui-arch/frame';
|
import { effect, h } from '@ui-arch/frame';
|
||||||
import type { DrawerProps } from './types.js';
|
import type { DrawerProps } from './types.js';
|
||||||
|
|
||||||
/*** 抽屉:open 信号驱动侧滑面板,点遮罩 / × / Esc 关闭;与 modal 的分工是不打断主视图、可从侧边保留上下文 ***/
|
/*** 抽屉:open 信号驱动侧滑面板,点遮罩 / × / Esc 关闭;Tab 焦点陷阱 + 关闭归还焦点,与 modal 的分工是不打断主视图、可从侧边保留上下文 ***/
|
||||||
export function drawer(props: DrawerProps, ...children: unknown[]): HTMLElement {
|
export function drawer(props: DrawerProps, ...children: unknown[]): HTMLElement {
|
||||||
const close = (): void => {
|
const close = (): void => {
|
||||||
if (!props.open()) return; // 卫语句:已关闭不重复触发回调
|
if (!props.open()) return; // 卫语句:已关闭不重复触发回调
|
||||||
@@ -28,9 +28,30 @@ export function drawer(props: DrawerProps, ...children: unknown[]): HTMLElement
|
|||||||
);
|
);
|
||||||
overlay.append(panel);
|
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(() => {
|
effect(() => {
|
||||||
if (!props.open()) return;
|
if (props.open()) {
|
||||||
(panel.querySelector('button, input, textarea') as HTMLElement | null)?.focus(); // 打开时聚焦首个可交互元素
|
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) => {
|
document.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||||
if (e.key === 'Escape' && props.open()) close();
|
if (e.key === 'Escape' && props.open()) close();
|
||||||
|
|||||||
Reference in New Issue
Block a user