From 8e168293ecc49beb897e6892019e705406a25e2b Mon Sep 17 00:00:00 2001 From: HIsheR Date: Fri, 23 Oct 2026 17:11:31 +0800 Subject: [PATCH] =?UTF-8?q?iter:=20d28=20drawer=20=E7=84=A6=E7=82=B9?= =?UTF-8?q?=E9=99=B7=E9=98=B1=EF=BC=88Tab/Shift+Tab=20=E5=9B=9E=E7=BB=95?= =?UTF-8?q?=E3=80=81=E5=85=B3=E9=97=AD=E5=BD=92=E8=BF=98=E7=84=A6=E7=82=B9?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .ua/index.json | 2 +- packages/skin/ui/src/drawer.ts | 27 ++++++++++++++++++++++++--- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/.ua/index.json b/.ua/index.json index 6fd1601..26c419d 100644 --- a/.ua/index.json +++ b/.ua/index.json @@ -1,6 +1,6 @@ { "$schema": "ui-arch-registry-index/0.1", - "generatedAt": "2026-10-23T09:10:27.467Z", + "generatedAt": "2026-10-23T09:11:22.226Z", "repoRoot": ".", "packages": [ { diff --git a/packages/skin/ui/src/drawer.ts b/packages/skin/ui/src/drawer.ts index 8f1533b..3234e57 100644 --- a/packages/skin/ui/src/drawer.ts +++ b/packages/skin/ui/src/drawer.ts @@ -1,7 +1,7 @@ import { effect, h } from '@ui-arch/frame'; import type { DrawerProps } from './types.js'; -/*** 抽屉:open 信号驱动侧滑面板,点遮罩 / × / Esc 关闭;与 modal 的分工是不打断主视图、可从侧边保留上下文 ***/ +/*** 抽屉:open 信号驱动侧滑面板,点遮罩 / × / Esc 关闭;Tab 焦点陷阱 + 关闭归还焦点,与 modal 的分工是不打断主视图、可从侧边保留上下文 ***/ export function drawer(props: DrawerProps, ...children: unknown[]): HTMLElement { const close = (): void => { if (!props.open()) return; // 卫语句:已关闭不重复触发回调 @@ -28,9 +28,30 @@ export function drawer(props: DrawerProps, ...children: unknown[]): HTMLElement ); 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()) return; - (panel.querySelector('button, input, textarea') as HTMLElement | null)?.focus(); // 打开时聚焦首个可交互元素 + 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();