Files
NextEdu/docs/accessibility/a11y-audit.md

12 KiB
Raw Blame History

无障碍审计报告 (A11y Audit)

审计日期2026-06-17 审计范围:src/shared/ 核心组件与新增无障碍工具 合规目标WCAG 2.1 AA


一、已审计组件与 ARIA 改进

1. 新增无障碍工具库

文件 导出 用途
src/shared/lib/a11y.ts useA11yId 基于 React.useId 生成 SSR 安全的唯一 ID用于 aria-describedbyaria-labelledby
src/shared/lib/a11y.ts mergeA11yProps 合并多组 aria/data 属性,aria-*/data-* 字符串属性以空格拼接
src/shared/lib/a11y.ts describeInput 计算输入框的 aria-describedbyaria-invalid
src/shared/lib/a11y.ts loadingAria 提供加载状态的 aria-busyaria-live 属性

2. 新增 Hook

文件 导出 用途
src/shared/hooks/use-aria-live.ts useAriaLive 管理 aria-live 区域,支持 polite/assertive 通知,自动清除过期通知(默认 5s返回 { announce, liveRegion }

3. 新增 a11y 组件

文件 组件 用途
src/shared/components/a11y/skip-link.tsx SkipLink 跳转链接,视觉隐藏,获得焦点时高对比度显示,默认跳转 #main-content
src/shared/components/a11y/visually-hidden.tsx VisuallyHidden 视觉隐藏但屏幕阅读器可读,用于图标按钮文字描述、表单辅助说明
src/shared/components/a11y/focus-trap.tsx FocusTrap 焦点陷阱,捕获 Tab/Shift+Tab 循环,支持初始焦点与焦点恢复
src/shared/components/a11y/aria-status.tsx AriaStatus ARIA 状态通知区域,渲染 aria-live 区域,支持 polite/assertive

4. 增强的核心 UI 组件

src/shared/components/ui/table.tsx

组件 ARIA 改进
Table 默认 role="table"(可覆盖),支持 aria-rowcountaria-colcount
TableHeader 默认 role="rowgroup"
TableBody 默认 role="rowgroup"
TableFooter 默认 role="rowgroup"
TableRow 默认 role="row"
TableHead 默认 role="columnheader",支持 scope 属性(col/row/colgroup/rowgroup
TableCell 默认 role="cell"
TableCaption 已有 <caption> 元素,为表格提供可访问标题

所有 role 均为默认值,可通过 props 覆盖,完全向后兼容

src/shared/components/ui/dialog.tsx

改进项 说明
aria-modal="true" 显式添加到 DialogContentRadix 已内置,此处显式标注便于审计)
关闭按钮 aria-label="关闭" 添加明确的中文无障碍标签
关闭按钮 sr-only 文本 由 "Close" 改为 "关闭",与项目语言一致
焦点管理 Radix Dialog 原语已内置:打开时焦点移入内容区,关闭时恢复到触发元素
Esc 键关闭 Radix Dialog 原语已内置
aria-labelledby Radix 自动关联 DialogTitle 的 id 到 aria-labelledby

二、待改进项

优先级 项目 说明
表单组件 aria-describedby 关联 InputTextareaSelect 等需配合 describeInput 工具函数,将错误提示和帮助文本的 id 关联到输入框
图标按钮 aria-label 全项目排查仅含图标无文字的按钮,补充 aria-label 或使用 VisuallyHidden
Sheet/AlertDialog 焦点管理 参照 Dialog 增强,显式添加 aria-modal 和中文关闭标签
数据表格 aria-rowcount/aria-colcount 在使用 @tanstack/react-table 的页面中,为 Table 传入总行数和列数
面包屑 aria-label="面包屑导航" Breadcrumb 容器添加 navaria-label
分页组件 aria-label 分页导航添加 aria-label="分页",当前页使用 aria-current="page"
动态内容变更播报 在表单提交、数据加载场景接入 useAriaLive 进行状态播报
颜色对比度审查 使用 axe DevTools 全量扫描颜色对比度是否达到 4.5:1正文/ 3:1大文字
跳转链接全局应用 app/(dashboard)/layout.tsx 中的内联 skip-link 替换为 SkipLink 组件

三、屏幕阅读器测试指南

NVDAWindows免费

  1. 安装:从 nvaccess.org 下载安装
  2. 启动/退出Ctrl + Alt + N 启动,Insert + Q 退出
  3. 核心快捷键
    • / :逐行阅读
    • Tab / Shift + Tab:在可聚焦元素间移动
    • H:按标题跳转
    • T:跳转到表格
    • F:跳转到表单控件
    • B:跳转到按钮
    • Insert + Tab:播报当前焦点元素
    • Insert + Space:切换浏览/焦点模式
  4. 测试要点
    • 打开页面后 Tab 到 SkipLink确认可跳转到主内容区
    • Tab 遍历所有交互元素,确认每个元素有可读的名称
    • 打开 Dialog确认焦点移入对话框、Esc 可关闭、关闭后焦点回到触发按钮
    • 在表格中按 T 跳转,确认表格标题和行列关系正确播报

VoiceOvermacOS内置

  1. 启动/退出Cmd + F5
  2. 核心快捷键
    • Ctrl + Option + → / :逐元素导航
    • Ctrl + Option + Cmd + H:按标题跳转
    • Ctrl + Option + Cmd + T:跳转到表格
    • Ctrl + Option + Space:激活当前元素
    • Ctrl + Option + U打开转子Rotor按元素类型浏览
  3. 测试要点
    • 确认 SkipLink 获得焦点时高对比度显示
    • 确认 aria-live 区域在表单提交后播报结果
    • 确认 VisuallyHidden 内容被播报但不可见
    • 确认 Dialog 打开时 VoiceOver 朗读对话框标题

通用测试清单

  • 所有交互元素可通过键盘访问Tab/Shift+Tab/Enter/Space/Esc
  • 焦点顺序符合视觉阅读顺序
  • 焦点可见focus 样式清晰)
  • 每个交互元素有可访问名称(aria-label 或可见文字)
  • 表单错误信息通过 aria-livearia-describedby 播报
  • 加载状态通过 aria-busyaria-live 播报
  • 模态框打开时焦点被困在框内,关闭后恢复

四、WCAG 2.1 AA 合规检查清单

原则一:可感知 (Perceivable)

准则 状态 说明
1.1.1 非文本内容 图标按钮通过 aria-labelVisuallyHidden 提供文字替代
1.2.1 纯音频/视频 ⚠️ 项目暂无音视频内容,后续如需添加需提供字幕/文字稿
1.3.1 信息与关系 表格通过 rolescope 表达行列关系;表单通过 aria-describedby 关联说明
1.3.2 有意义的顺序 DOM 顺序与视觉顺序一致
1.3.3 感官特征 不仅依赖颜色/位置传达信息,配合文字说明
1.3.4 方向 不限制屏幕方向
1.4.1 颜色的使用 错误状态除颜色外配合文字/图标
1.4.3 对比度(最低) ⚠️ 需全量审查,语义色 muted-foreground 需确认对比度 ≥ 4.5:1
1.4.4 文字缩放 使用 rem/em 单位,支持 200% 缩放
1.4.10 回流 响应式布局,支持 320px 宽度
1.4.11 非文字对比度 边框、焦点环使用语义色,对比度 ≥ 3:1

原则二:可操作 (Operable)

准则 状态 说明
2.1.1 键盘 所有交互可通过键盘操作
2.1.2 无键盘陷阱 FocusTrap 仅在模态框激活时使用Esc 可退出
2.1.4 字符快捷键 无单字符快捷键
2.2.1 计时可调 无超时限制(会话超时由 NextAuth 管理,可延长)
2.3.1 三次闪烁 无闪烁内容
2.4.1 跳过区块 SkipLink 组件提供跳转到主内容
2.4.2 页面标题 Next.js metadata 提供页面标题
2.4.3 焦点顺序 DOM 顺序符合逻辑
2.4.4 链接目的 链接文字描述目的,避免"点击这里"
2.4.6 标题与标签 表单字段使用 Label 组件关联
2.4.7 焦点可见 所有交互元素有 focus:ring 样式
2.5.3 标签包含名称 可见标签文字包含在可访问名称中

原则三:可理解 (Understandable)

准则 状态 说明
3.1.1 页面语言 <html lang="zh-CN">
3.1.2 部分语言 暂无混语言内容
3.2.1 聚焦 聚焦不触发意外上下文变更
3.2.2 输入 表单提交需明确按钮触发
3.2.3 一致导航 侧边栏导航在页面间一致
3.2.4 一致标识 功能相同的组件使用一致标识
3.3.1 错误识别 表单错误通过 aria-invalidaria-describedby 播报
3.3.2 标签或说明 表单字段使用 Label 关联,提供 placeholder 补充
3.3.3 错误建议 ⚠️ 部分表单错误仅提示"必填",需补充修正建议
3.3.4 错误预防 删除/提交关键操作使用 AlertDialog 确认

原则四:健壮 (Robust)

准则 状态 说明
4.1.1 解析 React 保证有效 HTML
4.1.2 名称、角色、值 ARIA 角色和属性正确设置,状态变化通过 aria-live 播报
4.1.3 状态消息 useAriaLiveAriaStatus 提供 aria-live 状态播报

五、自动化测试工具推荐

工具 用途 链接
axe DevTools 浏览器插件,扫描页面无障碍问题 https://www.deque.com/axe/devtools/
Lighthouse Chrome 内置,生成无障碍评分 Chrome DevTools → Lighthouse
@axe-core/playwright E2E 测试中集成 axe 检查 https://github.com/dequelabs/axe-core-npm
eslint-plugin-jsx-a11y ESLint 静态检查 JSX 无障碍问题 https://github.com/jsx-eslint/eslint-plugin-jsx-a11y

六、使用示例

useAriaLive — 表单提交结果播报

"use client"

import { useAriaLive } from "@/shared/hooks/use-aria-live"

function MyForm(): JSX.Element {
  const { announce, liveRegion } = useAriaLive()

  const handleSubmit = async (): Promise<void> => {
    const result = await submitAction()
    if (result.success) {
      announce("保存成功", { politeness: "polite" })
    } else {
      announce(`保存失败:${result.message}`, { politeness: "assertive" })
    }
  }

  return (
    <>
      <form onSubmit={handleSubmit}>{/* ... */}</form>
      {liveRegion}
    </>
  )
}

describeInput — 输入框错误关联

import { useA11yId, describeInput } from "@/shared/lib/a11y"

function EmailField({ error }: { error?: string }): JSX.Element {
  const hintId = useA11yId("email-hint")
  const errorId = useA11yId("email-error")
  const { ariaDescribedBy, ariaInvalid } = describeInput(
    hintId,
    error ? errorId : undefined
  )

  return (
    <>
      <Input
        aria-describedby={ariaDescribedBy}
        aria-invalid={ariaInvalid}
      />
      <span id={hintId} className="text-muted-foreground text-sm">
        请输入有效邮箱地址
      </span>
      {error && (
        <span id={errorId} className="text-destructive text-sm" role="alert">
          {error}
        </span>
      )}
    </>
  )
}

FocusTrap — 自定义模态框

import { FocusTrap } from "@/shared/components/a11y/focus-trap"

interface CustomModalProps {
  open: boolean
  onClose: () => void
  children: React.ReactNode
}

function CustomModal({ open, onClose, children }: CustomModalProps): JSX.Element {
  return (
    <FocusTrap active={open} restoreFocus>
      <div role="dialog" aria-modal="true">
        {children}
        <button onClick={onClose}>关闭</button>
      </div>
    </FocusTrap>
  )
}