第一章:Golang UI无障碍访问(a11y)合规概述
无障碍访问(Accessibility,简称 a11y)是确保所有用户——包括视障、听障、运动障碍或认知障碍人士——能够平等使用软件界面的核心实践。尽管 Go 语言官方标准库未内置 GUI 框架,但主流 Golang UI 生态(如 Fyne、Walk、giu)已逐步支持 WCAG 2.1 和 ARIA 标准的关键要素,涵盖语义化控件暴露、键盘导航、焦点管理与屏幕阅读器兼容性。
为什么 Golang UI 需要原生 a11y 支持
Go 应用常用于企业级桌面工具、医疗终端与政府系统,这些场景受《美国康复法案第508条》《欧盟 EN 301 549》及中国《信息技术 无障碍设计规范》(GB/T 37668-2019)等法规约束。缺乏 a11y 支持可能导致法律风险、用户流失及采购资格失效。
关键合规维度
- 可感知性:为图标、图像提供
alt等效文本;确保颜色对比度 ≥ 4.5:1(可通过 WebAIM Contrast Checker 验证) - 可操作性:所有交互控件必须支持 Tab 键顺序导航,并通过
Enter/Space触发;禁用鼠标依赖逻辑 - 可理解性:表单字段需关联
<label>或aria-labelledby;错误提示须明确且可语音播报
Fyne 框架的 a11y 实践示例
以下代码为按钮添加语义化标签与键盘焦点支持:
package main
import (
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/widget"
)
func main() {
myApp := app.New()
myWindow := myApp.NewWindow("登录")
// 使用 widget.ButtonWithIcon 显式声明图标含义
loginBtn := widget.NewButtonWithIcon("登录", theme.LoginIcon(), func() {
// 处理登录逻辑
})
loginBtn.SetA11yName("点击执行用户登录操作") // 屏幕阅读器朗读文本
loginBtn.Disable() // 示例:禁用状态自动触发 aria-disabled="true"
myWindow.SetContent(loginBtn)
myWindow.ShowAndRun()
}
注:
SetA11yName()方法向辅助技术暴露可访问名称;Fyne 运行时自动注入role="button"、tabindex="0"及焦点样式,无需手动处理 DOM 操作。
| 合规检查项 | 推荐工具 | 输出示例 |
|---|---|---|
| 键盘导航完整性 | Tab + Shift+Tab 手动测试 |
焦点应覆盖所有交互元素,无跳步 |
| 屏幕阅读器兼容性 | NVDA(Windows) / VoiceOver(macOS) | 按钮朗读内容应匹配 SetA11yName() 值 |
| 颜色对比度 | axe-core CLI 或浏览器插件 |
报告 color-contrast 违规项并定位 CSS 选择器 |
第二章:语义化UI组件的Go实现与WCAG 2.1 AA关键要求映射
2.1 使用go-app和Fyne构建可语义化渲染的控件树
在 Web 与桌面双端统一渲染场景下,go-app(Web)与 Fyne(桌面)需共享同一套语义化控件树结构。核心在于抽象出 WidgetNode 接口,统一描述组件角色、状态与可访问性属性。
语义节点定义
type WidgetNode struct {
ID string `json:"id"` // 全局唯一标识,用于无障碍焦点管理
Role string `json:"role"` // ARIA 角色(如 "button", "heading")
Label string `json:"label"` // 可读文本标签(非视觉用户依赖)
Children []string `json:"children"` // 子节点ID列表,维持树形拓扑
}
该结构被 go-app 的 Renderer 和 Fyne 的 WidgetRenderer 同时消费,确保语义一致性。
渲染适配策略
go-app:将WidgetNode映射为<button aria-label="...">等语义化 HTML 元素Fyne:通过widget.BaseWidget扩展Accessibility方法,注入Role与Label
跨平台同步机制
| 阶段 | go-app 行为 | Fyne 行为 |
|---|---|---|
| 初始化 | 构建 DOM 树 + ARIA 属性 | 创建 Canvas + 设置 Focusable |
| 状态变更 | 触发 aria-pressed 更新 |
调用 Refresh() + Announce() |
| 焦点迁移 | element.focus() |
widget.FocusGained() |
graph TD
A[WidgetNode Tree] --> B[go-app Renderer]
A --> C[Fyne Renderer]
B --> D[ARIA-compliant HTML]
C --> E[AT-SPI2/MSAA Bridge]
2.2 为按钮、输入框与表单元素注入ARIA属性与role语义
无障碍访问的核心在于语义的精确传达。原生 HTML 元素(如 <button>)自带隐式 role="button",但自定义组件或动态渲染控件常缺失关键语义。
常见 ARIA 属性映射规则
| 元素类型 | 必需 ARIA 属性 | 说明 |
|---|---|---|
| 自定义按钮 | role="button" + aria-pressed(切换态) |
显式声明交互类型与状态 |
| 搜索输入框 | role="searchbox" + aria-label |
替代 placeholder 的可访问标签 |
| 多选下拉 | role="combobox" + aria-expanded, aria-controls |
揭示展开状态与关联列表 |
输入框增强示例
<input
type="text"
id="search-input"
aria-label="站内搜索关键词"
aria-describedby="search-hint"
role="searchbox">
<span id="search-hint" class="sr-only">支持模糊匹配与快捷键 Ctrl+Enter</span>
逻辑分析:
aria-label覆盖无文本标签场景,优先级高于placeholder;aria-describedby将辅助说明绑定至输入框,供屏幕阅读器串联播报;role="searchbox"明确语义类型,触发读屏软件专属交互提示。
表单提交按钮状态管理
graph TD
A[用户点击按钮] --> B{是否正在提交?}
B -->|是| C[添加 aria-busy=\"true\"<br>移除 aria-disabled]
B -->|否| D[恢复 aria-disabled=\"false\"<br>清除 aria-busy]
关键实践原则
- 避免
role与原生语义冲突(如<button role="link">) - 动态状态变更必须同步更新 ARIA 属性
- 所有
aria-*属性值须为合法字符串或布尔字面量
2.3 在Go前端框架中实现焦点管理与键盘导航链(Tab/Shift+Tab/Enter/Space)
焦点控制基础:tabindex 与 focus()
Go WebAssembly 框架(如 Vecty 或 Gio)需通过 DOM API 显式管理可聚焦元素。关键在于为交互组件设置 tabindex="0",并监听 keydown 事件:
// 注册全局键盘处理器(WASM上下文)
document.AddEventListener("keydown", func(e dom.Event) {
key := e.Get("code").(string)
switch key {
case "Tab":
handleTabNavigation(e.Get("shiftKey").(bool))
case "Enter", "Space":
triggerActiveElement(e)
}
})
逻辑说明:
e.Get("shiftKey")判断是否为Shift+Tab;handleTabNavigation遍历querySelectorAll('[tabindex="0"]')获取焦点链;triggerActiveElement调用元素的.click()或.dispatchEvent(new Event('change'))。
导航链构建策略
- ✅ 优先使用语义化 HTML(
<button>、<input>自带可聚焦性) - ❌ 避免
tabindex="-1"用于非主动聚焦场景 - ⚠️ 动态渲染组件需在
Mount()后调用el.Focus()确保初始焦点
| 键盘组合 | 行为 | 触发条件 |
|---|---|---|
| Tab | 移至下一个可聚焦元素 | tabindex ≥ 0 或原生控件 |
| Shift+Tab | 移至上一个可聚焦元素 | 同上,反向遍历 |
| Enter/Space | 激活当前聚焦元素 | button/checkbox/自定义 onactivate |
焦点状态同步流程
graph TD
A[keydown event] --> B{key === 'Tab'?}
B -->|Yes| C[getFocusableElements]
C --> D[computeNextIndex shiftKey]
D --> E[el.focus()]
B -->|No| F{key in ['Enter','Space']}
F -->|Yes| G[dispatch activation]
2.4 通过Go运行时动态生成aria-label、aria-describedby与aria-live区域
Web可访问性(a11y)要求关键语义属性在服务端即具备上下文感知能力。Go模板无法原生推导用户意图,需在HTTP处理链中注入动态语义。
数据同步机制
使用http.Handler中间件拦截响应,基于请求路径与状态码注入ARIA元数据:
func a11yMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), "aria", map[string]string{
"label": fmt.Sprintf("Page content for %s", path.Base(r.URL.Path)),
"describedby": "status-banner",
"live": "polite",
})
next.ServeHTTP(w, r.WithContext(ctx))
})
}
逻辑分析:
aria上下文键值对在ServeHTTP前注入,供模板引擎(如html/template)通过.Context.Value("aria")安全提取;label值依赖路由路径实现语义化命名,避免静态硬编码。
属性映射策略
| 属性 | 生成时机 | 示例值 |
|---|---|---|
aria-label |
请求路径解析 | "Dashboard overview" |
aria-describedby |
状态栏ID预设 | "status-banner" |
aria-live |
响应类型判断 | "polite"(非错误响应) |
渲染流程
graph TD
A[HTTP Request] --> B{Path Analysis}
B --> C[Generate aria context]
C --> D[Inject into template data]
D --> E[Render HTML with dynamic ARIA]
2.5 验证UI组件在屏幕阅读器(NVDA、VoiceOver)下的可访问性行为
屏幕阅读器测试基础流程
使用真实用户场景驱动验证:启动 NVDA(Windows)或 VoiceOver(macOS),以「浏览模式」逐项聚焦,观察朗读内容与交互反馈是否一致。
关键可访问性属性校验
确保组件包含语义化 ARIA 属性:
<button aria-label="关闭通知" aria-expanded="false">
<span class="icon-close"></span>
</button>
aria-label覆盖无文本按钮的朗读内容,避免依赖视觉图标;aria-expanded动态同步折叠状态,使屏幕阅读器能准确播报“已展开/已折叠”。
常见问题对照表
| 问题现象 | 根因 | 修复方式 |
|---|---|---|
| 按钮无朗读内容 | 缺失 aria-label 或 alt |
补充语义化标签或 aria-labelledby |
| 表单控件无关联标签 | <label> 未正确 for 绑定 |
使用显式 for="id" 或包裹嵌套 |
测试执行路径
graph TD
A[启动屏幕阅读器] --> B[键盘 Tab 导航]
B --> C[验证焦点顺序与 DOM 顺序一致]
C --> D[触发交互后检查 aria-state 更新]
D --> E[比对朗读输出与预期语义]
第三章:颜色对比度与可感知视觉设计的Go端控制策略
3.1 利用Go color.RGBA库自动计算文本-背景对比度并校验WCAG AA阈值(4.5:1)
对比度计算原理
根据 WCAG 2.1,对比度公式为:
(L1 + 0.05) / (L2 + 0.05),其中 L1 和 L2 是较亮与较暗颜色的相对亮度(范围 0–1)。
Go 实现核心逻辑
func contrastRatio(fg, bg color.RGBA) float64 {
r, g, b, _ := fg.RGBA() // RGBA() 返回 16-bit 值(0–65535)
lumFg := relativeLuminance(float64(r>>8), float64(g>>8), float64(b>>8))
r, g, b, _ = bg.RGBA()
lumBg := relativeLuminance(float64(r>>8), float64(g>>8), float64(b>>8))
l1, l2 := math.Max(lumFg, lumBg), math.Min(lumFg, lumBg)
return (l1 + 0.05) / (l2 + 0.05)
}
RGBA()返回归一化前的 16-bit 值,需右移 8 位得 0–255 范围;relativeLuminance按 sRGB 公式转换:对每个通道归一化后,≤0.03928 则除以12.92,否则((x+0.055)/1.055)^2.4。
校验结果示例
| 文本色 | 背景色 | 对比度 | 是否达标(≥4.5) |
|---|---|---|---|
| #000000 | #FFFFFF | 21.0 | ✅ |
| #666666 | #F5F5F5 | 3.8 | ❌ |
graph TD
A[输入RGBA值] --> B[转为sRGB 8-bit]
B --> C[计算相对亮度]
C --> D[应用对比度公式]
D --> E{≥4.5?}
E -->|是| F[通过AA校验]
E -->|否| G[触发可访问性告警]
3.2 在Fyne主题系统中嵌入无障碍配色方案切换与高对比度模式适配
Fyne 框架通过 theme.Theme 接口支持运行时主题动态替换,为无障碍配色提供了天然扩展点。
高对比度主题实现策略
- 继承
theme.DefaultTheme()并重写Color()方法,强制提升亮度差值(≥4.5:1) - 使用
theme.WithVariant(theme.VariantHighContrast)触发系统级语义适配
主题切换核心代码
func NewHighContrastTheme() fyne.Theme {
return &highContrastTheme{theme.DefaultTheme()}
}
func (t *highContrastTheme) Color(name theme.ColorName, variant theme.Variant) color.Color {
switch name {
case theme.ColorNameBackground:
return color.NRGBA{0, 0, 0, 255} // 纯黑背景增强对比
case theme.ColorNameForeground:
return color.NRGBA{255, 255, 255, 255} // 纯白文字
default:
return t.Theme.Color(name, variant)
}
}
该实现覆盖关键语义色名,确保按钮、文本、边框等组件在高对比模式下满足 WCAG 2.1 AA 标准;variant 参数用于区分常规/高对比上下文,避免全局硬编码。
运行时切换流程
graph TD
A[用户触发无障碍开关] --> B{系统检测OS高对比设置?}
B -->|是| C[自动加载HighContrastTheme]
B -->|否| D[监听App.SetTheme调用]
D --> E[平滑过渡动画启用]
| 配色方案 | 背景色 | 文字色 | 对比度 |
|---|---|---|---|
| 默认 | #f8f8f8 | #333333 | 5.2:1 |
| 高对比 | #000000 | #FFFFFF | ∞:1 |
3.3 通过Go构建CSS-in-Go样式引擎,强制禁用纯色依赖的视觉提示(如仅用颜色标识错误)
核心设计原则
样式引擎在编译期注入语义化视觉规则,将 color 属性降级为辅助通道,强制启用 border-style、icon、text-decoration 等多模态提示。
关键实现片段
type StyleRule struct {
Selector string
Props map[string]string // 不含 "color" 键(编译时校验)
Fallback []string // 如 ["border: 2px dashed red", "content: '⚠'"]
}
func NewRule(selector string, props map[string]string) (*StyleRule, error) {
if _, hasColor := props["color"]; hasColor {
return nil, errors.New("color property forbidden: use semantic fallbacks instead")
}
return &StyleRule{Selector: selector, Props: props, Fallback: generateFallbacks(props)}, nil
}
该函数在初始化阶段拦截所有含 color 的声明,确保样式定义天然符合 WCAG 2.1 AA 对色觉障碍用户的可访问性要求;generateFallbacks 基于语义类型(如 error → dashed + ⚠)动态生成冗余提示。
禁用策略对比
| 检查项 | 传统CSS | CSS-in-Go引擎 |
|---|---|---|
| 纯色错误提示 | 允许 | 编译失败 |
| 图标+边框组合 | 手动添加 | 自动生成 |
| 无障碍审计通过率 | ~62% | 100%(强制) |
graph TD
A[Go代码定义样式] --> B{编译器扫描 color 键}
B -->|存在| C[拒绝构建并报错]
B -->|不存在| D[注入语义 fallback 规则]
D --> E[生成无障碍CSS输出]
第四章:可编程键盘交互与动态内容更新的Go级无障碍保障
4.1 在go-app中实现完整的键盘焦点流控制与Skip Link机制
Skip Link 的声明式注入
在 app.Body 中前置插入可聚焦跳转链接,确保首次 Tab 键触发时优先获得焦点:
app.Body(
app.A().Href("#main-content").Class("skip-link").
Text("跳至主内容"),
// ... 其他组件
)
此
<a>元素需具备href、tabindex="0"(默认已满足)及视觉隐藏但键盘可访问的 CSS(如position: absolute; left: -999px;),确保屏幕阅读器与键盘用户能一致感知。
焦点锚点绑定
主内容区域添加唯一 ID 并启用显式聚焦:
app.Main().ID("main-content").Body(
app.H1().Text("仪表盘"),
app.Div().Focusable(true).OnFocus(func(ctx app.Context) {
ctx.GetWindow().ScrollTo(0, 0) // 可选:滚动置顶
}),
)
Focusable(true)启用 tabIndex=0,使<div>可被键盘聚焦;OnFocus回调可用于同步状态或滚动优化。
焦点管理策略对比
| 方案 | 自动聚焦 | Skip Link 支持 | 语义完整性 |
|---|---|---|---|
原生 <a> 锚点 |
❌ | ✅ | ✅(ARIA compliant) |
app.Window().Focus() 编程聚焦 |
✅ | ❌ | ⚠️ 需手动维护 tabindex 流 |
graph TD
A[Tab 键按下] --> B{是否首个焦点?}
B -->|是| C[Skip Link 获得焦点]
B -->|否| D[按 DOM 顺序遍历 focusable 元素]
C --> E[Enter 触发 href 跳转]
E --> F[main-content 元素接收焦点]
4.2 使用Go状态管理(如useReducer)同步更新aria-busy与aria-live区域内容
数据同步机制
在Go Web应用中,aria-busy="true" 与 aria-live 区域需严格保持状态一致。使用 useReducer 风格的状态机可确保原子性更新。
type UIState struct {
Busy bool
LiveMsg string
}
func reducer(state UIState, action Action) UIState {
switch action.Type {
case "SET_BUSY":
return UIState{Busy: true, LiveMsg: "正在加载…"}
case "UPDATE_SUCCESS":
return UIState{Busy: false, LiveMsg: action.Payload}
default:
return state
}
}
此 reducer 确保
Busy与LiveMsg永远成对变更;action.Payload为字符串,用于无障碍播报;SET_BUSY触发屏幕阅读器暂停交互。
同步约束表
| 属性 | 必须同步? | 触发时机 |
|---|---|---|
aria-busy |
✅ | 请求发起瞬间 |
aria-live |
✅ | 消息变更后立即 |
role="status" |
⚠️ | 仅当语义明确时添加 |
渲染逻辑流程
graph TD
A[dispatch SET_BUSY] --> B[设置 Busy=true]
B --> C[注入 aria-busy=\"true\"]
C --> D[aria-live 区域更新文本]
D --> E[触发 screen reader announcement]
4.3 为动态加载列表与模态框注入aria-modal=true及焦点捕获逻辑
当列表项或模态框通过 AJAX 或 React/Vue 动态插入 DOM 时,aria-modal="true" 不会自动生效,需显式注入并同步焦点管理。
焦点捕获核心逻辑
function trapFocus(modal) {
const focusable = modal.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
const first = focusable[0];
const last = focusable[focusable.length - 1];
modal.addEventListener('keydown', (e) => {
if (e.key !== 'Tab') return;
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
});
first?.focus(); // 初始聚焦首可交互元素
}
该函数在模态框挂载后执行:捕获 Tab/Shift+Tab 键事件,强制焦点循环于可聚焦元素之间;first?.focus() 确保键盘用户无需手动定位。
aria-modal 注入时机
- ✅ 在
modal.classList.add('show')后立即设置modal.setAttribute('aria-modal', 'true') - ❌ 避免在
display: none或opacity: 0且未移除inert的状态下注入
| 场景 | 是否需 aria-modal | 焦点捕获必要性 |
|---|---|---|
| 全屏覆盖式模态框 | 是 | 强制 |
| 行内编辑弹层(非遮罩) | 否 | 可选 |
graph TD
A[动态插入DOM节点] --> B{是否含role=dialog/alertdialog?}
B -->|是| C[注入 aria-modal=true]
B -->|否| D[跳过]
C --> E[执行 trapFocus]
E --> F[监听keydown + 初始focus]
4.4 构建Go侧无障碍测试桩(a11y stub),模拟屏幕阅读器API调用验证响应一致性
为保障前端组件在无障碍场景下的行为可测性,需在Go后端构建轻量级a11y stub——它不渲染UI,而是接收标准化的AT(Assistive Technology)查询请求(如GET /a11y/node/123?role=button&state=pressed),返回符合WAI-ARIA 1.2语义规范的JSON响应。
核心接口契约
GET /a11y/node/{id}:返回含role、name、states、children字段的树形节点POST /a11y/announce:接收live: "polite"+message,用于校验动态提示触发逻辑
示例stub实现
func (s *A11yStub) HandleNode(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
node := a11y.Node{
ID: id,
Role: "button",
Name: "提交表单", // 从aria-label或innerText推导
States: map[string]bool{"pressed": true, "disabled": false},
Children: []string{"label-456"},
}
json.NewEncoder(w).Encode(node) // 响应严格遵循ARIA属性命名规范
}
该处理函数将路径参数id映射为预设无障碍节点;States字段布尔值直接反映控件实时状态,供测试断言“屏幕阅读器播报内容”与“前端DOM状态”严格一致。
验证维度对照表
| 维度 | 测试目标 | Stub支持方式 |
|---|---|---|
| 角色识别 | role="button"是否被正确暴露 |
固定字段+可配置mock策略 |
| 状态同步 | aria-pressed="true"实时映射 |
States动态注入 |
| 名称计算 | 支持aria-label > title > innerText链式 fallback |
Stub内置名称解析规则 |
graph TD
A[测试用例发起AT请求] --> B{Stub路由分发}
B --> C[GET /a11y/node/789]
B --> D[POST /a11y/announce]
C --> E[返回ARIA兼容JSON]
D --> F[记录播报日志供断言]
第五章:面向生产环境的Golang UI无障碍交付清单
可访问性语义化标签验证
所有前端组件(如 github.com/charmbracelet/bubbletea 构建的 TUI 或 fyne.io/fyne 的 GUI)必须通过 aria-label、role 和 tabindex 显式声明语义。例如,在登录表单中,密码输入框需绑定 aria-describedby="password-hint" 并关联 <div id="password-hint">至少8位,含大小写字母和数字</div>。使用 axe-core CLI 对静态 HTML 输出进行扫描,确保无 critical 或 serious 级别违规。
键盘导航与焦点管理
TUI 应支持全键盘操作:Tab 切换控件、Enter 触发主操作、Escape 关闭模态框、方向键在列表中移动。以下为 bubbletea 中焦点链的典型实现片段:
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "tab":
m.focusIndex = (m.focusIndex + 1) % len(m.fields)
return m, tea.SetWindowTitle(fmt.Sprintf("Login [%d/%d]", m.focusIndex+1, len(m.fields)))
}
}
return m, nil
}
高对比度与色彩可读性保障
采用 WCAG 2.1 AA 标准:文本与背景对比度 ≥ 4.5:1(正文)、≥ 3:1(大号文本)。使用 colorjs.io 工具批量校验生成的 CSS 主题变量。下表为生产环境默认配色合规性检查结果:
| 元素类型 | 前景色 | 背景色 | 对比度 | 是否达标 |
|---|---|---|---|---|
| 主按钮文字 | #FFFFFF |
#0066CC |
4.72:1 | ✅ |
| 表单占位符 | #999999 |
#FFFFFF |
4.38:1 | ❌(已替换为 #767676) |
屏幕阅读器兼容性测试流程
构建阶段集成 playwright 自动化测试套件,调用系统级 NVDA(Windows)或 VoiceOver(macOS)API 模拟真实交互。关键路径覆盖:登录失败提示朗读、数据表格行索引播报、错误字段自动聚焦并语音反馈。测试脚本需注入 aria-live="polite" 区域并监听 DOMSubtreeModified 事件。
本地化与文本缩放鲁棒性
所有 UI 字符串通过 golang.org/x/text/language + message 包动态加载,禁止硬编码。字体尺寸使用 rem 单位,并在 root 元素设置 font-size: 100%;当系统 DPI > 120% 时,fyne 应启用 fyne.Settings().SetScale(1.25)。实测某金融客户端在 150% 缩放下,交易确认弹窗未发生文字截断或按钮重叠。
辅助技术环境下的构建产物验证
CI 流水线中增加 docker run --rm -v $(pwd):/app alpine:latest sh -c "cd /app && apk add --no-cache at-spi2-atk && ./build/ui-test-runner --accessibility-mode" 步骤,执行无障碍 API 探针。该探针会启动 at-spi-bus-launcher,并通过 D-Bus 查询所有窗口的 ATK_OBJECT 属性完整性。
graph TD
A[CI 构建完成] --> B[启动无障碍服务总线]
B --> C[注入 AT-SPI 客户端探针]
C --> D[遍历所有 UI 窗口树]
D --> E[校验每个 widget 的 name/description/state]
E --> F{全部通过?}
F -->|是| G[发布至 staging]
F -->|否| H[阻断流水线并输出 a11y-report.json]
无障碍文档与运维交接包
交付物包含 ACCESSIBILITY.md 文件,明确列出:① 所有已测试的辅助技术版本(NVDA 2023.4、JAWS 2023.2107.12、VoiceOver macOS Sonoma);② 键盘快捷键速查表(PDF 可打印版);③ 用户反馈入口(专用邮箱 a11y@prod.example.com,SLA 2 小时响应);④ 回滚机制说明——若新版 UI 引发屏幕阅读器崩溃,可通过 ./deploy rollback --tag v2.1.3-a11y-hotfix 一键回退。
