From 640c2860b9aeedcf95b781ed8df5be78edda0519 Mon Sep 17 00:00:00 2001 From: HIsheR Date: Mon, 26 Oct 2026 12:43:32 +0800 Subject: [PATCH] =?UTF-8?q?iter(g74):=20=E5=BC=B9=E7=AA=97=E4=B8=8E?= =?UTF-8?q?=E6=8A=BD=E5=B1=89=E8=A1=A5=20aria-labelledby=20=E6=A0=87?= =?UTF-8?q?=E9=A2=98=E5=85=B3=E8=81=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/skin/ui/src/drawer.ts | 6 +++++- packages/skin/ui/src/modal.ts | 7 +++++-- 2 files changed, 10 insertions(+), 3 deletions(-) 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, // 底部操作区:右对齐,仅传了才渲染 );