可访问性(a11y)

让 Agent 在实现 UI 时同步检查焦点顺序、可见标签与动态内容的屏幕阅读器播报。

本页为 Agent 提供可访问性实施的完整参考:ARIA 角色与属性的正确用法、模态框焦点陷阱实现、语义 HTML 对比、jest-axe 自动测试配置,以及屏幕阅读器测试清单。目标级别为 WCAG 2.2 AA

SKILL 写明必测流程:仅键盘操作(Tab/Shift+Tab/Enter/Space)、缩放 200%、prefers-reduced-motion;自动化 axe 与 VoiceOver/NVDA 人工走查分工明确。

ARIA 角色与键盘焦点陷阱

ARIA 属性正确用法

<!-- ✅ 表单错误:aria-invalid + aria-describedby -->
<input
  id="email"
  type="email"
  aria-invalid="true"
  aria-describedby="email-error"
  aria-required="true"
/>
<span id="email-error" role="alert">请输入有效的电子邮件地址</span>

<!-- ✅ 动态内容:aria-live -->
<div aria-live="polite" aria-atomic="true" class="sr-only">
  <!-- 搜索结果数量更新时自动播报 -->
  找到 {{ resultCount }} 条结果
</div>

<!-- ✅ 图标按钮:aria-label -->
<button type="button" aria-label="关闭对话框">
  <svg aria-hidden="true" focusable="false">...</svg>
</button>

模态框焦点陷阱(Focus Trap)实现

// utils/focusTrap.ts
export function createFocusTrap(container: HTMLElement) {
  const focusable = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'

  function getFocusable() {
    return Array.from(container.querySelectorAll<HTMLElement>(focusable))
      .filter(el => !el.hasAttribute('disabled'))
  }

  function handleKeydown(e: KeyboardEvent) {
    if (e.key !== 'Tab') return
    const els = getFocusable()
    if (!els.length) return
    const first = els[0], last = els[els.length - 1]

    if (e.shiftKey) {
      if (document.activeElement === first) {
        e.preventDefault()
        last.focus()
      }
    } else {
      if (document.activeElement === last) {
        e.preventDefault()
        first.focus()
      }
    }
  }

  return {
    activate() {
      container.addEventListener('keydown', handleKeydown)
      getFocusable()[0]?.focus()         // 自动聚焦第一个可交互元素
    },
    deactivate() {
      container.removeEventListener('keydown', handleKeydown)
    },
  }
}
  • 错误信息用 aria-describedby / aria-invalid 与控件关联,而非仅靠颜色。
  • 对话框关闭时将焦点还原到触发元素(triggerEl.focus())。
  • 自定义控件(如下拉菜单)保持 roving tabindex 模式时,在 SKILL 中写清交互契约。

语义 HTML 对比与 jest-axe 自动测试

语义 HTML 对比(div+onclick vs 正确元素):

<!-- ❌ 反模式:div 模拟按钮 -->
<div class="btn" onclick="submit()">提交</div>
<!-- 问题:无键盘访问、无 role、屏幕阅读器不识别 -->

<!-- ✅ 正确:使用原生 button -->
<button type="submit">提交</button>
<!-- 自动获得:键盘 Enter/Space、role="button"、focus 管理 -->

<!-- ❌ 反模式:span 模拟链接 -->
<span class="link" onclick="navigate('/about')">关于我们</span>

<!-- ✅ 正确:使用 anchor -->
<a href="/about">关于我们</a>
<!-- 支持右键在新标签打开、屏幕阅读器链接列表等 -->

jest-axe 自动化测试配置

// jest.setup.ts
import { toHaveNoViolations } from 'jest-axe'
expect.extend(toHaveNoViolations)

// __tests__/Button.test.tsx
import { render } from '@testing-library/react'
import { axe } from 'jest-axe'
import { Button } from '@/components/Button'

test('Button 无可访问性违规', async () => {
  const { container } = render(
    <Button onClick={() => {}}>提交</Button>
  )
  const results = await axe(container)
  expect(results).toHaveNoViolations()
})

// package.json 配置
// "jest-axe": "^9.0.0"
// "@testing-library/jest-dom": "^6.0.0"

屏幕阅读器测试清单(必须用 VoiceOver/NVDA 验证的 5 个场景):

  • 场景 1:仅用 Tab 键完成表单提交全流程(填写→验证→提交)。
  • 场景 2:打开模态框→执行操作→关闭,焦点正确返回触发元素。
  • 场景 3:动态内容更新(搜索结果/通知)被 aria-live 正确播报。
  • 场景 4:图片/图表有替代文本,装饰性图片用 alt="" 静默。
  • 场景 5:页面标题层级连续(H1→H2→H3),导航地标 main/nav/footer 齐备。

Agent 走查流程

将「级别与范围 → 自动化 → 键盘与缩放 → 记录例外」写进 SKILL,便于按顺序执行。

  [ SKILL:WCAG 版本 + 级别(如 2.2 AA)+ 范围(页面/组件) ]
                    │
                    ▼
            [ 自动化:axe / 同类规则;阈值与基线策略 ]
                    │
                    ▼
            [ 人工:仅键盘 Tab/Shift+Tab、Enter/Space;缩放 200% ]
                    │
                    ▼
            [ 动态:对话框/路由 — 焦点 trap 与返回;live 区域 ]
                    │
                    ▼
            [ 输出:问题清单 + 第三方无法修时的工单/替代方案 ]

页内工具:WCAG 验收要点摘要

以下脚本仅在本页运行;勾选后生成可复制进 SKILL 或评审备注的条目列表。

SKILL 与走查对齐

勾选已纳入 SKILL 或本次迭代要验收的项,点击生成摘要。

SKILL 大纲

---
name: accessibility-a11y-cn
description: 按 WCAG 2.2 AA 检查前端可访问性与键盘/读屏体验
---
# 规则
- 使用语义 HTML:button/a/input 替代 div+onclick
- 所有交互元素有可访问名称(可见文本、aria-label 或 aria-labelledby)
- 颜色对比度文本 ≥ 4.5:1,大文本 ≥ 3:1(使用 color-contrast-checker 验证)
- 动态内容更新:使用 aria-live="polite" 或 role="alert"
- 模态框:打开时焦点移入,关闭时还原触发元素,Tab 循环不逃出

# 步骤
1. 安装 jest-axe:npm i -D jest-axe @types/jest-axe
2. jest.setup.ts:import { toHaveNoViolations } from 'jest-axe'; expect.extend(toHaveNoViolations)
3. 每个组件测试加 axe(container) 断言(CI 阻断违规)
4. 键盘走查:Tab/Shift+Tab 遍历所有控件,Enter/Space 激活,Escape 关闭模态
5. VoiceOver(macOS):Cmd+F5 开启,VO+Right 逐元素;NVDA(Win):NVDA+空格 浏览
6. 缩放 200%:内容不溢出、不遮挡;prefers-reduced-motion:动画降级
7. 输出:违规清单按 WCAG 成功标准编号分组,附修复建议与优先级

返回技能库 更多技能入口