diff --git a/packages/skin/ui/src/drawer.ts b/packages/skin/ui/src/drawer.ts index e1b8c41..717e992 100644 --- a/packages/skin/ui/src/drawer.ts +++ b/packages/skin/ui/src/drawer.ts @@ -2,8 +2,11 @@ import { effect, h } from '@ui-arch/frame'; import type { DrawerProps } from './types.js'; import { lockScroll, pushOverlay } 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 close = (): void => { if (!props.open()) return; // 卫语句:已关闭不重复触发回调 props.open(false); @@ -21,9 +24,10 @@ export function drawer(props: DrawerProps, ...children: unknown[]): HTMLElement class: `ua-drawer ua-drawer-${props.placement ?? 'right'}`, style: `width:${props.width ?? 320}px`, role: 'dialog', + 'aria-labelledby': titleId, '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-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, ); diff --git a/packages/skin/ui/src/modal.ts b/packages/skin/ui/src/modal.ts index 123e687..a700e80 100644 --- a/packages/skin/ui/src/modal.ts +++ b/packages/skin/ui/src/modal.ts @@ -2,8 +2,11 @@ import { effect, h } from '@ui-arch/frame'; import type { ModalProps } from './types.js'; import { lockScroll, pushOverlay } from './overlay.js'; +let seq = 0; // 实例序号:给标题生成唯一 id,供 aria-labelledby 关联 + /*** 弹窗:open 信号驱动显隐,点遮罩、× 或 Esc 触发 onClose;Tab 焦点陷阱 + 关闭归还焦点 ***/ export function modal(props: ModalProps, ...children: unknown[]): HTMLElement { + const titleId = `ua-modal-title-${++seq}`; // 标题 id:对话框的可访问名 const close = () => { if (!props.open()) return; // 卫语句:已关闭不重复触发回调 props.open(false); // 写回信号触发隐藏 @@ -17,8 +20,8 @@ export function modal(props: ModalProps, ...children: unknown[]): HTMLElement { }); const box = h( 'div', - { class: `ua-modal ua-modal-${props.size ?? 'md'}`, role: 'dialog', 'aria-modal': 'true' }, // 对话框语义,供读屏识别 - h('div', { class: 'ua-modal-head' }, h('h3', { class: 'ua-modal-title' }, props.title), h('button', { type: 'button', class: 'ua-modal-close', 'aria-label': '关闭弹窗', onClick: close }, '×')), + { class: `ua-modal ua-modal-${props.size ?? 'md'}`, role: 'dialog', 'aria-modal': 'true', 'aria-labelledby': titleId }, // 对话框语义 + 标题关联 + h('div', { class: 'ua-modal-head' }, h('h3', { class: 'ua-modal-title', id: titleId }, props.title), h('button', { type: 'button', class: 'ua-modal-close', 'aria-label': '关闭弹窗', onClick: close }, '×')), h('div', { class: 'ua-modal-body' }, ...children), props.footer ? h('div', { class: 'ua-modal-foot' }, props.footer) : null, // 底部操作区:右对齐,仅传了才渲染 );