构建符合 WCAG 2.2 AA 标准的无障碍 React DatePicker
我们的开发团队从 Claude 生成的 DatePicker 代码入手,该代码基于 WAI-ARIA APG 的“日期选择对话框”模式。初始提示指定了 WCAG 2.1/2.2 AA 级要求:带有 aria-describedby 的输入框、触发按钮、role="dialog" 的模态对话框,以及基于网格的日历表格。然而,原始版本在焦点、导航和无障碍性方面暴露了严重问题。
AI 实现了基本的键盘导航(方向键、Home/End、PageUp/Down、Enter/Space、Escape)、网格单元上的 roving tabindex,以及月份变化时的 aria-live="polite"。但使用 VoiceOver 和 NVDA 测试时发现问题:打开时焦点未落在选中日期上,aria-live 公告排队堆积,对话框在重复点击时闪烁。
针对实际无障碍性的 APG 模式调整
APG 规范要求使用 <td role="gridcell"> 而非嵌套按钮,依赖手动 onClick/onKeyDown 和 tabindex。实际测试显示,在 <td> 内使用 <button> 有明显优势:
- 原生焦点处理和 Enter/Space 支持,无需额外代码。
- 屏幕阅读器自动语义识别。
- 内置禁用状态,无需 aria-disabled 或 preventDefault。
- 鼠标或键盘点击事件更可靠。
这是有意选择:APG 是指导,但设备测试优先。我们确保对比度 ≥4.5:1,使用 CSS Modules 和原生 Date/Intl API 进行格式化——除了 React/TypeScript 外无外部依赖。
修复功能和用户体验问题
原始代码无法编译:缺少 index.html 和 TypeScript 错误。修复后,又出现更多问题:
功能 Bug:
- 超出 minDate/maxDate 的日期会跳转到其他月份,而不是显示禁用单元格。
- 无法清空输入框。
视觉问题:
- 单元格缺少焦点样式。
- td 尺寸不均。
无障碍问题:
- 打开时无焦点落在选中日期——屏幕阅读器保持沉默。
- 月份头部 aria-live="polite" 在快速导航时导致公告排队。
- 触发器失焦 + 点击导致对话框闪烁。
修复措施:
- 打开时焦点落在值或今天;屏幕阅读器宣布上下文(星期、日期、月份、年份)。
- 在独立隐藏区域使用 aria-live="assertive" 实现即时月份公告。
- 选择后焦点返回触发器,并更新 aria-label。
- 即使有禁用日期,也显示值的月份。
- 移除 OK/取消:Enter 确认,Esc 关闭。
组件结构与键盘导航
Props: value (Date | null), onChange, minDate?, maxDate?, disabledDates?, locale?。
关键 ARIA:
- 输入框:aria-describedby 说明格式,readonly。
- 触发器:动态 aria-label(“已选:MM/DD/YYYY” 或 “选择日期”)。
- 对话框:role="dialog",aria-modal="true",焦点陷阱。
- 网格:table role="grid",td 内 button role="gridcell",值上 aria-selected,不可选日期上 aria-disabled。
- 月份头部:在 polite 区域使用 aria-live="assertive"。
导航:
- 方向键:相邻日期。
- Home/End:月份首/末日。
- PageUp/Down:±1 个月。
- Shift+PageUp/Down:±1 年。
- Enter/Space:选择。
- Escape:关闭。
关键经验
- AI 能快速搭建骨架(ARIA、导航),但屏幕阅读器测试必不可少。
- <td> 内 <button> 在真实浏览器 + 屏幕阅读器中比纯 gridcell 更可靠。
- aria-live="assertive" 避免月份导航时的公告排队。
- 焦点管理:打开时定位值/今天,返回触发器。
- 最小栈:React + TS,无库依赖。
该组件作为单日期选择器已生产就绪,开源代码便于扩展(年份、范围)。
— Editorial Team
暂无评论。