在跨国项目的无障碍改造中,焦点指示器往往是最容易被忽视的一环。视觉正常的鼠标用户几乎感知不到它的存在,但对只依赖键盘或读屏软件的用户来说,焦点指示器就是页面的眼睛。当项目需要同时满足多国规范时,把规范条款转化为类型定义,是防止配置随意、样式漂移的有效手段。本文以IWAC项目适配塔吉克斯坦网络无障碍指南为例,展示一套完整的TypeScript类型封装思路。

为什么焦点指示器需要类型化封装
塔吉克斯坦网络无障碍指南在焦点可见性上的要求与WCAG 2.1的2.4.7条款方向一致:任何可通过键盘操作的元素,获得焦点时必须有清晰可见的提示;同时指南强调指示器与背景之间要保持足够的对比度,且不允许在用户切换到高对比度模式时消失。如果这些约束只写在文档里,开发过程中的配置对象就完全靠自觉,很容易出现有人写 outline: 'none'、有人传入非法颜色值的情况。
类型化的价值在于把规范前置到编译阶段。当我们用联合类型把焦点指示器的实现方式限定为outline、box-shadow、background等几种受控形态,用字面量类型锁死对比度等级,任何一个偏离规范的配置都会在编译期报错。相比运行时校验,这种方式零成本,且能在IDE里直接给出自动补全提示,等于把指南文档变成了编辑器里的实时提示。
此外,塔吉克斯坦指南中部分条款对焦点顺序、跳过链接(skip link)有额外要求,这些行为层面的约束同样可以通过判别联合类型建模,让组件消费配置时必须同时提供行为与样式两部分信息,避免只改样式不改行为的半吊子适配。
核心类型设计:样式描述与行为枚举
先定义指示器的视觉形态。指南允许的实现方式有限,我们用字符串字面量联合加以限制,并为每种形态建立对应的参数结构:
// 指示器允许的视觉形态
export type FocusIndicatorKind =
| 'outline'
| 'box-shadow'
| 'background'
| 'underline';
// 颜色必须来自受控调色板,防止传入低对比度颜色
export type ContrastPalette =
| '#0B3D91' // 深蓝,对比度 8.2:1
| '#B3261E' // 深红,对比度 6.1:1
| '#1B5E20'; // 深绿,对比度 7.3:1
export interface BaseIndicator {
kind: FocusIndicatorKind;
color: ContrastPalette;
/** 最小宽度,指南要求不低于 2px */
minWidthPx: 2 | 3 | 4;
}
export interface OutlineIndicator extends BaseIndicator {
kind: 'outline';
style: 'solid' | 'double';
offsetPx: number;
}
export interface ShadowIndicator extends BaseIndicator {
kind: 'box-shadow';
spreadPx: number;
blurPx: 0; // 指南要求阴影边缘清晰,禁止模糊
}
export interface BackgroundIndicator extends BaseIndicator {
kind: 'background';
/** 背景方案必须同时提供文字反色,保证内容对比 */
textColor: ContrastPalette;
}
export type FocusIndicator =
| OutlineIndicator
| ShadowIndicator
| BackgroundIndicator
| (BaseIndicator & { kind: 'underline' });这里的判别联合是关键设计。kind字段充当判别属性,编译器可以根据它自动收窄类型,比如当 kind === 'box-shadow' 时才允许访问 spreadPx。特别注意 blurPx: 0 这种单值字面量类型,它看起来极端,但恰恰是规范的体现:模糊的发光效果在低视力用户眼中可能完全无法辨认,指南明确要求指示器边缘锐利,那么就把这条要求直接写进类型。
行为层面同样建模。指南要求焦点顺序遵循DOM顺序,且复杂组件需提供焦点陷阱或循环,这些用枚举表达:
export enum FocusTraversal {
/** 焦点跟随 DOM 顺序,默认且推荐 */
Sequential = 'sequential',
/** 模态场景使用,焦点在容器内循环 */
Trap = 'trap',
/** 工具栏场景,左右键循环,Tab 跳出 */
Roving = 'roving',
}
export interface FocusBehavior {
traversal: FocusTraversal;
/** 是否允许使用 Tab 键跳出当前容器 */
escapeWithTab: boolean;
/** 跳过链接配置,指南要求页面顶部提供 */
skipLink?: {
targetSelector: string;
label: string;
};
}组合配置与编译期守卫
样式与行为合并成最终的组件配置类型,并针对常见违规场景编写类型守卫。守卫的价值是提供运行时的最后防线,处理从JSON文件或后端下发的动态配置:
export interface IwacFocusConfig {
indicator: FocusIndicator;
behavior: FocusBehavior;
/** 高对比度模式下指示器是否保留,指南强制为 true */
persistInHighContrast: true;
/** 焦点移除动画时长,指南要求不超过 200ms */
transitionMs: 0 | 100 | 200;
}
const PALETTE = new Set(['#0B3D91', '#B3261E', '#1B5E20']);
export function isFocusIndicator(v: unknown): v is FocusIndicator {
if (typeof v !== 'object' || v === null) return false;
const ind = v as Record<string, unknown>;
if (typeof ind.color !== 'string' || !PALETTE.has(ind.color)) {
return false;
}
const width = ind.minWidthPx as number;
if (width < 2) return false;
switch (ind.kind) {
case 'box-shadow':
return ind.blurPx === 0;
case 'background':
return typeof ind.textColor === 'string' && PALETTE.has(ind.textColor);
default:
return ['outline', 'underline'].includes(ind.kind as string);
}
}persistInHighContrast: true 依然是字面量类型的用法,任何人在配置里写 false 都会直接编译失败,这与规范中强制条款的表述完全对应。守卫函数则覆盖了三类典型违规:私自传入调色板之外的颜色、指示器宽度低于2px、以及为背景方案遗漏反色文字。
在组件侧消费这些类型时,建议再封装一层工厂函数,把样式描述翻译成实际的CSS:
export function buildFocusCss(indicator: FocusIndicator): string {
switch (indicator.kind) {
case 'outline':
return `outline: ${indicator.minWidthPx}px ${indicator.style} ${indicator.color};` +
`outline-offset: ${indicator.offsetPx}px;`;
case 'box-shadow':
return `box-shadow: 0 0 0 ${indicator.spreadPx}px ${indicator.color};`;
case 'background':
return `background-color: ${indicator.color};color: ${indicator.textColor};`;
case 'underline':
return `text-decoration: underline ${indicator.minWidthPx}px solid;` +
`text-decoration-color: ${indicator.color};`;
}
}落地建议与常见坑
封装完成后,团队接入时常踩两个坑。一是有人试图用 as FocusIndicator 强转绕过检查,这会让守卫和字面量约束全部失效,建议在ESLint中禁用非空断言相关的宽松写法。二是把塔吉克斯坦指南的类型与其他地区规范混在同一个文件里,随着适配国家增多,类型冲突会越来越频繁。更好的做法是为每套规范建立独立的命名空间模块,再通过一个顶层联合类型聚合,需要哪个地区就引入哪个模块。
另一个实践是把调色板与对比度数值的映射关系固化成常量表,并在CI里跑一个对比度计算脚本,保证调色板中的每个颜色对白色和黑色背景的比值都达标。这样类型系统负责结构正确,脚本负责数值正确,两层防线配合,规范落地就不再依赖口头约定。
总体而言,这套封装的核心思路是:凡是规范里的强制条款,用字面量类型锁死;凡是有限的可选项,用联合类型约束;凡是动态来源的数据,用类型守卫兜底。这样IWAC在适配塔吉克斯坦指南时,合规性检查从人工评审变成了编译输出,维护成本和出错概率都显著降低。
TypeScript焦点指示器无障碍指南修改时间:2026-09-03 12:59:02