在遵循IWAC(International Web Accessibility Consortium)发布的无障碍指南做多语言站点时,西撒哈拉网络无障碍指南对焦点指示器提出了一套相当具体的要求:焦点必须可见、指示区域不能小于最小尺寸、颜色对比度要达到规定阈值。如果只是用普通的JavaScript写几个工具函数,参数写错也不会报错,直到测试阶段才发现焦点环尺寸不达标。用TypeScript把这些规范翻译成类型系统,能让编译器替你把关,本文完整演示这一封装过程。

一、先把指南要求梳理成可建模的规则集
动手写类型之前,必须先把西撒哈拉网络无障碍指南中关于焦点指示器的条款归纳清楚。这一步看似多余,实际决定了类型定义的边界。指南的核心条款可以归纳为四类:第一是焦点可见性,即元素获得焦点时必须呈现非隐藏的视觉提示,禁止outline:none后不提供替代方案;第二是指示区域尺寸,焦点环的偏移量和线条宽度有最小值约束,通常要求外围指示区域不小于2px的边界加2px的间隔;第三是对比度要求,焦点指示器与相邻背景的对比度比值不低于3:1;第四是移除条件,只有在用户偏好减少动画或特定交互场景下才允许弱化指示效果。
这四类规则天然适合映射为TypeScript中的字面量联合类型、数值范围约束和判别联合。比如对比度用数字类型,但需要通过品牌类型(branded type)限定取值区间;焦点样式策略用几个字符串字面量区分。先在注释或独立常量文件中固化这些规则阈值,后续类型和运行时校验共用同一份常量,避免规则漂移。
// 规则阈值集中管理,类型与运行时校验共用
export const FOCUS_RULES = {
minOutlineWidth: 2, // 焦点环最小线宽(px)
minOutlineOffset: 2, // 焦点环与元素的最小间隔(px)
minContrastRatio: 3, // 与背景的最低对比度比值
} as const;
二、设计核心类型:字面量联合与判别联合的组合
焦点指示器的实现策略通常有三种:原生outline方案、box-shadow方案和自定义伪元素方案。三者适用的场景不同,参数结构也不同。如果用一个大而全的接口把所有字段塞进去,调用方很容易传错组合,比如选了outline策略却传了伪元素的颜色参数。判别联合正好解决这个问题:用一个strategy字段作为判别式,每个策略分支只暴露自己需要的属性。
对比度是另一个值得精细建模的点。普通的number类型允许传入0.5这种明显不合规的值,编译器无法拦截。通过品牌类型给数字打上标记,再配合工厂函数做运行时校验,就能保证一旦类型系统给出了ContrastRatio类型的值,它一定通过了阈值检查。这种模式在领域建模中非常实用,值得掌握。
// 策略字面量联合
export type FocusStrategy = 'outline' | 'box-shadow' | 'pseudo-element';
// 品牌类型:约束对比度取值
declare const ContrastBrand: unique symbol;
export type ContrastRatio = number & { readonly [ContrastBrand]: 'validated' };
export function makeContrastRatio(value: number): ContrastRatio {
if (value < FOCUS_RULES.minContrastRatio) {
throw new Error(
`对比度 ${value} 低于 IWAC 要求的 ${FOCUS_RULES.minContrastRatio}:1`
);
}
return value as ContrastRatio;
}
// 判别联合:每种策略只暴露自己的参数
export type FocusIndicatorConfig =
| {
strategy: 'outline';
width: number;
offset: number;
color: string;
contrast: ContrastRatio;
}
| {
strategy: 'box-shadow';
shadowLayers: string[];
contrast: ContrastRatio;
}
| {
strategy: 'pseudo-element';
selector: string;
borderWidth: number;
borderRadius: number;
contrast: ContrastRatio;
};
这样设计之后,传入非法组合的代码在编译期就会报错。例如配置了strategy: 'outline'却试图设置shadowLayers,TypeScript会直接提示该属性不存在于该分支。这种错误提示越早出现,修复成本越低,这正是类型驱动的价值所在。
三、用泛型封装配置生成器并提供运行时兜底校验
类型定义只是第一步,还需要一层封装把配置转换为实际的CSS样式对象。这里用泛型约束保证生成器函数的入参与配置类型严格对应,同时考虑到类型系统无法验证运行时的动态值(比如用户传入的实际像素数),在生成器内部再做一次防御性校验,形成双保险。这种编译期加运行时的双层校验结构,在封装任何合规相关的库时都推荐采用。
另外,西撒哈拉指南特别强调尊重用户的减少动画偏好,因此封装中还应支持传入respectsReducedMotion标记,让生成器在输出样式时自动附加@media (prefers-reduced-motion)相关的处理逻辑。类型层面可以用Partial与Readonly修饰可选行为开关,保证核心合规参数必填、增强行为可选。
export interface IndicatorOptions {
respectsReducedMotion?: boolean;
transitionDuration?: number;
}
export function buildFocusStyles<T extends FocusIndicatorConfig>(
config: T,
options: IndicatorOptions = {}
): Record<string, string> {
// 运行时兜底:类型擦除后仍可能混入非法数据
if (config.contrast < FOCUS_RULES.minContrastRatio) {
throw new Error('对比度不满足 IWAC 最低要求');
}
const base: Record<string, string> = {};
if (config.strategy === 'outline') {
if (config.width < FOCUS_RULES.minOutlineWidth) {
throw new Error('焦点环线宽低于最小值');
}
base['outline-width'] = `${config.width}px`;
base['outline-offset'] = `${Math.max(config.offset, FOCUS_RULES.minOutlineOffset)}px`;
base['outline-color'] = config.color;
base['outline-style'] = 'solid';
}
if (config.strategy === 'box-shadow') {
base['box-shadow'] = config.shadowLayers.join(', ');
}
if (options.respectsReducedMotion) {
base['transition'] = 'none';
}
return base;
}
// 使用示例:编译期即校验参数组合
const styles = buildFocusStyles(
{
strategy: 'outline',
width: 3,
offset: 2,
color: '#1a4d9e',
contrast: makeContrastRatio(4.6),
},
{ respectsReducedMotion: true }
);
值得注意的是,buildFocusStyles内部对outline线宽做了下限钳制而非直接抛错,这是两种可选策略:严格模式抛错适合开发阶段快速暴露问题,宽松模式钳制适合生产环境的容错。团队可以根据实际流程选择,甚至通过泛型参数控制行为模式。
四、集成到组件库与导出公共API
封装完成后,还需要考虑如何在组件库中落地。推荐的做法是把类型和生成器作为独立模块导出,组件内部消费而非暴露原始配置给业务方。比如一个按钮组件的焦点管理只需要内部调用buildFocusStyles,业务方通过组件props的受限类型间接控制焦点表现,这样合规规则始终收敛在封装层内部,不会被外部绕过。
同时建议导出一个类型守卫函数isFocusIndicatorConfig,供接收外部动态配置(例如从配置中心下发的场景)时收窄类型。类型守卫配合unknown入参,是处理不可信输入的标准姿势,能避免使用any导致类型链路断裂。
export function isFocusIndicatorConfig(
input: unknown
): input is FocusIndicatorConfig {
if (typeof input !== 'object' || input === null) return false;
const cfg = input as Record<string, unknown>;
const strategies: string[] = ['outline', 'box-shadow', 'pseudo-element'];
return (
typeof cfg.strategy === 'string' &&
strategies.includes(cfg.strategy) &&
typeof cfg.contrast === 'number' &&
cfg.contrast >= FOCUS_RULES.minContrastRatio
);
}
经过这一整套封装,西撒哈拉网络无障碍指南中关于焦点指示器的硬性要求全部沉淀进了类型系统:非法的策略组合在编译期报错,不达标的数值在构造阶段抛出明确提示,动态输入则由类型守卫把关。后续如果指南条款更新,只需修改集中的规则常量和对应类型分支,全项目的校验逻辑会同步收紧,这正是类型驱动开发在无障碍合规场景下的典型收益。
TypeScript焦点指示器无障碍修改时间:2026-09-05 05:04:39