iter: d21 无障碍收尾(aria-label / role / aria-hidden)
This commit is contained in:
@@ -17,7 +17,7 @@ export function modal(props: ModalProps, ...children: unknown[]): HTMLElement {
|
||||
const box = h(
|
||||
'div',
|
||||
{ class: `ua-modal ua-modal-${props.size ?? 'md'}` },
|
||||
h('div', { class: 'ua-modal-head' }, h('h3', { class: 'ua-modal-title' }, props.title), h('button', { class: 'ua-modal-close', onClick: close }, '×')),
|
||||
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 }, '×')),
|
||||
h('div', { class: 'ua-modal-body' }, ...children),
|
||||
props.footer ? h('div', { class: 'ua-modal-foot' }, props.footer) : null, // 底部操作区:右对齐,仅传了才渲染
|
||||
);
|
||||
|
||||
@@ -4,9 +4,9 @@ import type { SkeletonProps } from './types.js';
|
||||
/*** 骨架屏:variant 决定占位形状(text 多行 / rect 块 / circle 圆),纯展示无状态 ***/
|
||||
export function skeleton(props: SkeletonProps): HTMLElement {
|
||||
const variant = props.variant ?? 'rect'; // 缺省整块矩形
|
||||
if (variant !== 'text') return h('div', { class: `ua-skeleton ua-skeleton-${variant}` }); // 单形状直接返回
|
||||
if (variant !== 'text') return h('div', { class: `ua-skeleton ua-skeleton-${variant}`, 'aria-hidden': 'true' }); // 纯装饰,读屏跳过
|
||||
const rows = props.rows ?? 3; // text 行数,末行缩短模拟段落收尾
|
||||
const lines: HTMLElement[] = [];
|
||||
for (let i = 0; i < rows; i++) lines.push(h('div', { class: `ua-skeleton ua-skeleton-text${i === rows - 1 ? ' ua-last' : ''}` }));
|
||||
return h('div', { class: 'ua-skeleton-group' }, ...lines);
|
||||
for (let i = 0; i < rows; i++) lines.push(h('div', { class: `ua-skeleton ua-skeleton-text${i === rows - 1 ? ' ua-last' : ''}`, 'aria-hidden': 'true' }));
|
||||
return h('div', { class: 'ua-skeleton-group', 'aria-hidden': 'true' }, ...lines); // 整组装饰性占位
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { SpinnerProps } from './types.js';
|
||||
/*** 加载指示器:纯 CSS 旋转动画,size 控制直径 ***/
|
||||
export function spinner(props: SpinnerProps = {}): HTMLElement {
|
||||
const size = `${props.size ?? 18}px`;
|
||||
const el = h('span', { class: 'ua-spinner' });
|
||||
const el = h('span', { class: 'ua-spinner', role: 'status', 'aria-label': '加载中' }); // 读屏播报加载状态
|
||||
el.style.width = size;
|
||||
el.style.height = size;
|
||||
return el;
|
||||
|
||||
@@ -6,7 +6,7 @@ export function tabs(props: TabsProps): HTMLElement {
|
||||
// —— 状态集中声明 ——
|
||||
const buttons: HTMLButtonElement[] = []; // 页签按钮,顺序与 tabs 一致
|
||||
const indicator = h('span', { class: 'ua-tabs-indicator' });
|
||||
const head = h('div', { class: 'ua-tabs-head', role: 'tablist' }, indicator);
|
||||
const head = h('div', { class: 'ua-tabs-head', role: 'tablist', 'aria-label': '标签页' }, indicator);
|
||||
|
||||
/*** 同步激活态与指示条几何位置 ***/
|
||||
const sync = () => {
|
||||
@@ -27,6 +27,7 @@ export function tabs(props: TabsProps): HTMLElement {
|
||||
const btn = h('button', { class: 'ua-tab', role: 'tab', type: 'button' }, tab.label) as HTMLButtonElement;
|
||||
btn.dataset.value = tab.value;
|
||||
btn.disabled = tab.disabled ?? false;
|
||||
if (tab.disabled) btn.setAttribute('aria-disabled', 'true'); // 读屏提示不可选
|
||||
btn.addEventListener('click', () => {
|
||||
if (btn.disabled) return; // 卫语句:禁用页签不响应
|
||||
if (props.active) props.active(tab.value); // 用户切换写回信号
|
||||
|
||||
@@ -83,7 +83,8 @@
|
||||
"conventions": [
|
||||
"皮肤不持有业务状态,一律委托 headless 内核或调用方信号",
|
||||
"表单类组件统一约定:*Props.value/checked/active 为 Signal 双向绑定,onChange/onInput 为回调",
|
||||
"语义色 variant 四档:default|info / success / warning / danger,一律来自令牌,禁止硬编码颜色"
|
||||
"语义色 variant 四档:default|info / success / warning / danger,一律来自令牌,禁止硬编码颜色",
|
||||
"无障碍基线:装饰性占位(skeleton)aria-hidden、加载指示(spinner)role=status、关闭按钮必须 aria-label、弹层弹窗 aria-haspopup/expanded、读屏可见的状态一律同步 aria-*"
|
||||
],
|
||||
"docs": {
|
||||
"intro": "27 个 DOM 组件,统一用法:组件是普通函数,传入 props 返回 DOM 节点;需要状态时先创建 signal 再双向绑定。引入 CSS:import '@ui-arch/ui/ui.css'(内部已消费 tokens)。",
|
||||
|
||||
Reference in New Issue
Block a user