diff --git a/.ua/backlog.json b/.ua/backlog.json index 3218547..e345fb0 100644 --- a/.ua/backlog.json +++ b/.ua/backlog.json @@ -1020,6 +1020,14 @@ "acceptance": "新增 maxSize(字节)/ maxCount(多选上限)两个可选参数;accept 支持 .ext / type/subtype / type/* 且对拖拽同样生效;被拒文件不进清单并在 role=status 提示行写明原因(类型不符 / 超上限 / 数量已满);不改onChange 语义;文档与演示同步;npm run check 全绿", "notes": "自补:可用性;校验入口收敛到 add(),点选与拖拽共用同一条路径", "status": "done" + }, + { + "id": "g83", + "title": "开关改用 switch 语义并补可访问名兜底", + "motivation": "开关底层是 input type=checkbox,读屏一律播报成「复选框」,与视觉上的开关语义不符;不留 label 时更是完全没有可访问名(只有一个匿名复选框)", + "acceptance": "input 补 role=switch(选中态沿用原生 checked 映射);新增可选 ariaLabel,缺省取 label,纯图标开关也有可访问名;文档 props 与演示(无文案开关实例)同步;npm run check 全绿", + "notes": "自补:无障碍;沿用原生 checkbox 的 Space 切换行为,不重造键盘交互", + "status": "open" } ] } diff --git a/.ua/index.json b/.ua/index.json index 8e5cfd9..29ecd33 100644 --- a/.ua/index.json +++ b/.ua/index.json @@ -1,6 +1,6 @@ { "$schema": "ui-arch-registry-index/0.1", - "generatedAt": "2026-10-26T05:00:02.358Z", + "generatedAt": "2026-10-26T05:00:45.858Z", "repoRoot": ".", "packages": [ { @@ -607,6 +607,11 @@ "t": "Signal", "d": "开关状态信号,双向绑定" }, + { + "n": "ariaLabel", + "t": "string", + "d": "无可见文案时的可访问名(如纯图标开关);缺省取 label" + }, { "n": "label", "t": "string", diff --git a/apps/demo/main.ts b/apps/demo/main.ts index 9acd6b9..aa92c7d 100644 --- a/apps/demo/main.ts +++ b/apps/demo/main.ts @@ -139,6 +139,7 @@ const name = signal(''); // —— 表单控件状态集中声明 —— const agreed = signal(false); const nightMode = signal(true); +const sound = signal(true); // 无可见文案的开关演示 const city = signal(''); const size = signal('m'); const bio = signal(''); @@ -254,6 +255,7 @@ const content = h( 'checkbox / switch / select / radioGroup / textarea:状态一律由调用方信号持有;select 弹层在视口下方空间不足时自动翻到上方,展开后键入字母可快跳选项;textarea 传 maxCount 显示实时字数。', h('div', { class: 'row' }, checkbox({ label: '同意条款', checked: agreed }), h('span', { class: 'out' }, () => (agreed() ? '已同意' : '未同意'))), h('div', { class: 'row' }, switchButton({ label: '夜间模式', checked: nightMode }), h('span', { class: 'out' }, () => (nightMode() ? '开' : '关'))), + h('div', { class: 'row' }, switchButton({ checked: sound, ariaLabel: '通知声音' }), h('span', { class: 'out' }, () => `无可见文案的开关(ariaLabel 兜底,读屏念「通知声音」):${sound() ? '开' : '关'}`)), h('div', { class: 'row' }, select({ options: cities.map((c) => ({ value: c, label: c })), value: city, placeholder: '选择城市…' }), h('span', { class: 'out' }, () => (city() ? `城市:${city()}` : '未选择'))), h('div', { class: 'row' }, radioGroup({ options: [{ value: 's', label: '小' }, { value: 'm', label: '中' }, { value: 'l', label: '大' }], value: size })), textarea({ placeholder: '一句话介绍…', value: bio, rows: 2, maxCount: 30 }), diff --git a/llms-full.txt b/llms-full.txt index 1c44ae9..1465a9b 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -1,6 +1,6 @@ # @ui-arch 组件库 Agent 文档(无图形版) -> 自动生成于 2026-10-26T05:00:02.358Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 +> 自动生成于 2026-10-26T05:00:45.858Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 > 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。 ## 快速上手 @@ -239,6 +239,7 @@ const agreed = signal(false); checkbox({ label: '同意条款', checked: agreed, 参数: - checked:Signal —— 开关状态信号,双向绑定 +- ariaLabel:string —— 无可见文案时的可访问名(如纯图标开关);缺省取 label - label:string —— 左侧文案 - disabled:boolean | Signal —— 禁用态:布尔直接置位,传信号则动态同步 - onChange(回调):(checked: boolean) => void —— 变更回调 diff --git a/packages/skin/ui/src/switch.ts b/packages/skin/ui/src/switch.ts index d8ede67..7c8c0f1 100644 --- a/packages/skin/ui/src/switch.ts +++ b/packages/skin/ui/src/switch.ts @@ -3,7 +3,13 @@ import type { SwitchProps } from './types.js'; /*** 开关:checked 信号双向绑定,disabled 支持布尔或信号禁用,轨道 + 滑块的纯 CSS 实现 ***/ export function switchButton(props: SwitchProps): HTMLElement { - const input = h('input', { type: 'checkbox', class: 'ua-switch-input', disabled: props.disabled }) as HTMLInputElement; // false/undefined 移除属性,Signal 走响应式 + const input = h('input', { + type: 'checkbox', + class: 'ua-switch-input', + role: 'switch', // 用开关语义而非复选框:读屏播报「开关」并跟随选中态 + 'aria-label': props.ariaLabel ?? props.label, // 无可见文案时也要有可访问名 + disabled: props.disabled, // false/undefined 移除属性,Signal 走响应式 + }) as HTMLInputElement; if (props.checked) { const checked = props.checked; input.addEventListener('change', () => checked(input.checked)); // 用户切换写回信号 diff --git a/packages/skin/ui/src/types.ts b/packages/skin/ui/src/types.ts index 45eefd0..8e8c989 100644 --- a/packages/skin/ui/src/types.ts +++ b/packages/skin/ui/src/types.ts @@ -91,7 +91,8 @@ export interface CheckboxProps { /*** 开关入参 ***/ export interface SwitchProps { - label?: string; // 左侧文案;缺省不渲染 + label?: string; // 左侧文案;缺省不渲染(此时读屏名靠 ariaLabel 兜底) + ariaLabel?: string; // 无可见文案时的可访问名,如纯图标开关;缺省取 label checked?: Signal; // 开关状态信号,双向绑定 disabled?: boolean | Signal; // 禁用态:布尔直接置位,传信号则动态同步 onChange?: (checked: boolean) => void; // 变更回调 diff --git a/packages/skin/ui/ua.manifest.json b/packages/skin/ui/ua.manifest.json index d75a69a..9cad106 100644 --- a/packages/skin/ui/ua.manifest.json +++ b/packages/skin/ui/ua.manifest.json @@ -478,6 +478,11 @@ "t": "Signal", "d": "开关状态信号,双向绑定" }, + { + "n": "ariaLabel", + "t": "string", + "d": "无可见文案时的可访问名(如纯图标开关);缺省取 label" + }, { "n": "label", "t": "string",