This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
all-in-kingsoft/hhs/REACT/5. 工程实践篇/17-可访问性.md
T
2026-04-29 22:19:51 +08:00

239 lines
6.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [React, A11y, Accessibility, Frontend]
create time: 2026-04-29 22:16
---
# 可访问性
## 概述
可访问性(Accessibility,简称 a11y)确保残障用户也能正常使用应用。这不仅是道德责任,在许多国家和地区也是法律要求。本文档梳理 React 中实现无障碍的关键实践。
## WCAG 核心原则
```mermaid
graph TB
A["WCAG 2.1 四大原则"] --> B["Perceivable<br/>可感知"]
A --> C["Operable<br/>可操作"]
A --> D["Understandable<br/>可理解"]
A --> E["Robust<br/>鲁棒性"]
B --> B1["文本替代"]
B --> B2["颜色对比度 ≥ 4.5:1"]
C --> C1["键盘可达"]
C --> C2["足够的时间"]
D --> D1["可读的文本"]
D --> D2["一致导航"]
E --> E1["兼容辅助技术"]
style A fill:#F5A87D,color:#000
style B fill:#4FC08D,color:#fff
style C fill:#61DAFB,color:#000
style D fill:#A0AEC0,color:#000
style E fill:#ED8936,color:#000
```
## 语义化 HTML(最重要的一条)
```tsx
// ❌ 滥用 div + onClick
<div className="btn" onClick={() => navigate("/home")}>Home</div>
<div className="link" onClick={() => goTo("/about")}>About</div>
// ✅ 使用原生元素——天生支持键盘、屏幕阅读器、SEO
<button type="button">按钮</button>
<a href="/home">首页</a>
<a href="/about">关于</a>
```
### 常用语义标签对照表
| 功能 | 错误写法 | 正确写法 |
|------|---------|---------|
| 按钮行为 | `<div onClick>` | `<button>` |
| 链接跳转 | `<span onClick=navigate>` | `<a href>` |
| 表单输入 | `<div contentEditable>` | `<input>`/`<textarea>` |
| 弹窗 | `<div className="modal">` | `<dialog>` 或 role="dialog" |
| 导航区 | `<div class="nav">` | `<nav>` |
| 侧边栏 | `<aside class="sidebar">` | `<aside>` |
## aria 属性体系
```tsx
// 1. aria-label —— 给无文本内容的图标添加描述
<button aria-label="关闭对话框">
<CloseIcon />
</button>
// 2. aria-describedby —— 关联说明文字
<input
aria-label="邮箱地址"
aria-describedby="email-hint"
/>
<p id="email-hint">请使用注册时使用的邮箱</p>
// 3. aria-live —— 动态内容变化通知屏幕阅读器
<div aria-live="polite">
{formErrors.length > 0 && (
<p>{formErrors.join("、")}</p>
)}
</div>
{/* polite = 等待空闲再朗读;assertive = 立即打断 */}
// 4. aria-expanded —— 折叠面板展开状态
<button aria-expanded={isExpanded} onClick={() => setIsExpanded(!isExpanded)}>
{isExpanded ? "收起" : "展开"}
<ChevronIcon rotated={isExpanded ? 180 : 0} />
</button>
// 5. aria-hidden — 装饰性元素隐藏给屏幕阅读器
<img src="decorative-pattern.png" alt="" aria-hidden="true" />
```
## 键盘导航
### Tab Order 管理
```tsx
// 默认 Tab 顺序 = DOM 顺序,不要随意改变!
// 需要自定义时:
// ✅ 用 tabIndex 控制焦点位置(仅必要场景)
<NavButton tabIndex={0} /> {/* 在 Tab 流中 */}
<FloatingActionButton tabIndex={-1} /> {/* 仅程序化聚焦 */}
// ❌ 不要设置负 tabIndex 阻止所有键盘操作
// 自定义键盘快捷键
function useKeyboardShortcut(key: string, handler: () => void) {
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === key && !isInputFocused()) { // 避免在输入框中触发
handler();
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [key, handler]);
}
```
### Focus Trap(模态框焦点陷阱)
```tsx
import { useRef, useEffect } from "react";
function Modal({ isOpen, onClose, children }: Props) {
const modalRef = useRef<HTMLDialogElement>(null);
useEffect(() => {
if (!isOpen) return;
const focusableElements = modalRef.current?.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstEl = focusableElements?.[0] as HTMLElement;
const lastEl = focusableElements?.[focusableElements.length - 1] as HTMLElement;
const handleTab = (e: KeyboardEvent) => {
if (e.key !== "Tab") return;
if (e.shiftKey) {
if (document.activeElement === firstEl) {
e.preventDefault();
lastEl.focus();
}
} else {
if (document.activeElement === lastEl) {
e.preventDefault();
firstEl.focus();
}
}
};
document.addEventListener("keydown", handleTab);
firstEl?.focus();
return () => document.removeEventListener("keydown", handleTab);
}, [isOpen]);
if (!isOpen) return null;
return (
<dialog ref={modalRef} open onClose={onClose}>
{children}
</dialog>
);
}
```
## 颜色与视觉
> [!tip] 设计检查清单
> 1. **Contrast Ratio** — 正文文本对比度 ≥ 4.5:1,大文本 ≥ 3:1
> 2. **不依赖颜色传达信息** — 错误提示除了红色还要加图标和文字
> 3. **缩放 200% 下仍可用** — 不支持水平滚动查看内容
> 4. **深色模式 ≠ 反转全部颜色** — 需单独测试对比度
```css
/* 确保有焦点指示器 */
button:focus-visible {
outline: 2px solid #0066ff;
outline-offset: 2px;
}
/* 保留系统偏好: prefers-reduced-motion / prefers-contrast */
@media (prefers-reduced-motion: reduce) {
* { animation-duration: 0s !important; transition-duration: 0s !important; }
}
```
## React 特定点
```tsx
// 1. Suspense fallback 要提供有意义的 loading 文案
<Suspense fallback={<p>Loading profile...</p>}>
<Profile />
</Suspense>
// 2. Form 字段必须有 label
<label htmlFor="username">用户名:</label>
<input id="username" name="username" />
{/* 或隐式关联 */}
<label>
用户名:
<input name="username" />
</label>
// 3. 图标按钮必须有 aria-label
<Button aria-label="删除这条消息">
<TrashIcon />
</Button>
// 4. 路由切换后,屏幕阅读器应知道页面变了
// Next.js Router 自动处理;SPA 项目中可手动聚焦顶部
useEffect(() => {
window.scrollTo(0, 0);
document.getElementById("main-content")?.focus();
}, [location.pathname]);
```
## 测试辅助技术兼容性
| 工具 | 用途 |
|------|------|
| axe DevTools | Chrome/Firefox 扩展,自动检测 a11y 问题 |
| Lighthouse | 生成 a11y 评分报告 |
| NVDA / VoiceOver | 免费屏幕阅读器(Win / Mac) |
| wAI11y | Jest/Vitest 断言库 |
> [!question] 思考:为什么自动化工具只能覆盖约 30% 的可访问性问题?
> - 自动化能检测缺少 alt、颜色对比不足等技术问题
> - 但无法判断交互逻辑是否合理、ARIA 含义是否正确表达、Tab 顺序是否符合直觉——这些需要人工测试
## 关联笔记