科摩罗网络无障碍指南将焦点指示器定义为键盘操作可视化的第一道屏障,其中第4.6节到第4.9节明确规定了轮廓颜色、宽度、偏移量以及动画延迟的边界。在IWAC组件库中,如果这些规则只作为注释或文档存在,开发者在配置主题时就很难避免传入互相矛盾的样式。把指南条文转化为TypeScript类型,可以在编码阶段就把无效的焦点指示器组合拦下来。

一、把指南约束拆解成类型字段
科摩罗指南对焦点指示器的要求并不是笼统地让焦点可见,而是对不同实现方式给出了不同参数。第4.6节要求无论使用轮廓、阴影还是独立覆盖层,焦点提示与其相邻背景颜色的对比度都不能低于3比1。第4.7节进一步指出,当元素本身已经带有边框时,焦点轮廓必须与边框在颜色或位置上形成至少2像素的可辨识差异。第4.8节允许使用内阴影或外辉光,但禁止把透明度变化作为唯一提示。第4.9节则要求焦点出现动画的延迟不得超过100毫秒,以防用户按下Tab键后失去位置感。
这些条文直接映射成类型字段后,就是contrastAgainstAdjacentColor、outlineWidthPx、outlineOffsetPx和appearDelayMs。其中对比度字段保留为无单位比值,类型上可以标记为number,但注释中必须注明最小值为3。动画延迟则更适合用字面量联合类型来限制,因为指南给出的允许值只有0、20、40、60、80、100毫秒,如果放开成任意数字,就可能出现150毫秒这类不合规配置。
焦点指示器在实际实现上通常分成三种形态:outline轮廓、box-shadow阴影以及overlay覆盖层。指南没有强制要求使用哪一种,但对每种形态的必填字段都不相同。轮廓需要颜色、宽度、偏移和线条样式;阴影需要颜色、扩散半径以及是否为内阴影;覆盖层需要颜色和内缩距离。下面的代码先把这些差异抽离成基础接口与具体变体。
type FocusIndicatorKind = 'outline' | 'box-shadow' | 'overlay';
interface BaseFocusIndicator {
kind: FocusIndicatorKind;
contrastAgainstAdjacentColor: number; // 无单位比值,至少3
appearDelayMs: 0 | 20 | 40 | 60 | 80 | 100;
reduceMotionFallback: boolean;
}
interface OutlineFocusIndicator extends BaseFocusIndicator {
kind: 'outline';
outlineColor: string;
outlineWidthPx: number;
outlineOffsetPx: number;
outlineStyle: 'solid' | 'dashed';
}
基础接口里放入所有形态共有的字段,可以避免每个具体类型重复声明contrastAgainstAdjacentColor和appearDelayMs。但这里有一个关键点:kind字段不能是宽泛的字符串,必须使用字符串字面量联合。否则类型收窄就失去了标签,使用方也无法通过switch语句让TypeScript自动推断出具体形态。
二、用判别联合消除非法状态
如果把焦点指示器定义成一个接口,并把所有字段都设成可选属性,使用方就可能只传shadowColor却不传kind,或者传了outlineWidthPx却把kind写成box-shadow。这种配置在运行时才会暴露问题,而且排查起来很麻烦。判别联合的核心思路是让kind字段成为可辨识标签,每个具体变体都通过扩展基础接口并收紧kind的字面量值来声明自己的形状。
还需要解决主题系统中的默认值覆盖问题。当全局主题定义了部分焦点样式,而组件又需要覆盖其中某一个字段时,直接使用Partial<FocusIndicatorConfig>会把kind也变成可选的,这样标签就丢失了,TypeScript无法再根据kind收窄类型。更稳的方式是针对每个具体变体单独做省略,再把kind重新固定回来。代码如下:
type ShadowFocusIndicator = BaseFocusIndicator & {
kind: 'box-shadow';
shadowColor: string;
shadowSpreadPx: number;
shadowInset: boolean;
};
type OverlayFocusIndicator = BaseFocusIndicator & {
kind: 'overlay';
overlayColor: string;
overlayInsetPx: number;
};
type OutlineConfig = Omit<OutlineFocusIndicator, 'kind'> & { kind: 'outline' };
type ShadowConfig = Omit<ShadowFocusIndicator, 'kind'> & { kind: 'box-shadow' };
type OverlayConfig = Omit<OverlayFocusIndicator, 'kind'> & { kind: 'overlay' };
export type FocusIndicatorConfig = OutlineConfig | ShadowConfig | OverlayConfig;
export type ResolvedFocusIndicator<T extends BaseFocusIndicator> = Readonly<T>;
export function createFocusIndicator<T extends FocusIndicatorConfig>(config: T): ResolvedFocusIndicator<T> {
return Object.freeze(config) as ResolvedFocusIndicator<T>;
}
这个设计中,FocusIndicatorConfig是三个变体配置的联合,而ResolvedFocusIndicator返回一个只读版本,避免在主题生效后被意外修改。Omit加交集类型的方式既保留了kind标签,又允许传入时省略某个具体形态的某些字段,只要基础字段完整即可。这样主题系统可以先给出一个BaseFocusIndicator级别的默认值,组件再根据当前形态补全自己的字段。
覆盖层形态还有一个指向指南的隐藏约束:第4.8节补充说明,焦点覆盖层不得遮挡交互元素的文本内容。这个约束没有办法完全用类型表达,但可以把overlayInsetPx设计成必须由调用方显式声明的数字,并在运行时校验函数中比较该值与元素内边距的大小。类型负责保证字段存在,运行时负责保证数值合理。
三、IWAC封装与运行时样式解析
把类型定义好之后,需要把它接入IWAC的主题系统中。如果IWAC核心包没有直接导出焦点指示器的类型,可以通过模块增强来扩展其主题接口。这样就不用修改原始依赖,升级包版本时也不会产生直接冲突。下面的代码演示了如何声明合并,以及如何把配置转换成最终的CSS样式对象。
declare module '@iwac/core' {
interface IWACTheme {
focusIndicator?: FocusIndicatorConfig;
}
}
export function toFocusStyle(config: FocusIndicatorConfig, prefersReducedMotion: boolean) {
const delay = prefersReducedMotion ? 0 : config.appearDelayMs;
if (config.kind === 'outline') {
return {
outlineColor: config.outlineColor,
outlineWidth: `${config.outlineWidthPx}px`,
outlineOffset: `${config.outlineOffsetPx}px`,
outlineStyle: config.outlineStyle,
transitionDelay: `${delay}ms`,
};
}
if (config.kind === 'box-shadow') {
return {
boxShadow: `${config.shadowInset ? 'inset ' : ''}0 0 0 ${config.shadowSpreadPx}px ${config.shadowColor}`,
transitionDelay: `${delay}ms`,
};
}
return {
position: 'absolute',
inset: `${config.overlayInsetPx}px`,
border: `2px solid ${config.overlayColor}`,
transitionDelay: `${delay}ms`,
};
}
toFocusStyle函数利用config.kind进行分支,在每个分支内TypeScript已经将config收窄为对应的具体类型,因此访问outlineWidthPx或shadowSpreadPx不会报错。如果使用者传入了错误组合,比如把kind写成outline却只给了shadowColor,编译器会直接提示缺少outline相关字段,这就是判别联合带来的实际收益。
运行时还需要考虑用户系统是否开启了减少动效。科摩罗指南鼓励在prefers-reduced-motion生效时关闭焦点指示器的出现动画,把延迟归零而不是继续保留20毫秒或更长的过渡。上面的函数把延迟单独抽出,在减少动效时强制回到0毫秒,但轮廓本身仍然存在,不会让焦点丢失。
四、指南演进与类型可维护性
无障碍指南不会永远不变,科摩罗的规范也会随着本地化测试和国际标准修订而调整。类型定义要有可追溯性,否则协作者很难知道某个字段为什么必须是固定值。可以在每个字段的JSDoc注释中标注对应的指南条款编号,例如outlineWidthPx对应第4.7节,appearDelayMs对应第4.9节。这样做避免了类型文件变成一堆无解释的魔法数字,也方便后续对照条文做增量更新。
还可以补上编译期的断言测试,确保关键约束不会因为有人把联合类型放宽而被破坏。比如验证OutlineFocusIndicator不能赋值给ShadowFocusIndicator,appearDelayMs只接受定义好的六个字面量,以及FocusIndicatorConfig中缺少kind时无法通过创建函数。可使用tsd或expect-type这类工具编写断言,不需要额外地运行时执行逻辑。
最后需要避免过度设计。科摩罗指南目前基于主流浏览器能力定义了轮廓、阴影和覆盖层三种形态,已经足够覆盖大多数交互场景。如果一开始就为未来可能出现的每一种焦点指示器都抽象出插件架构,类型会变得复杂且难以阅读。保留FocusIndicatorKind这个联合标签,并在需要新形态时增加一个变体,才是当前阶段更稳健的做法。类型封装的目标不是穷举所有样式,而是把指南中的确定性规则变成开发者可以快速检索和使用的约束边界。
TypeScriptIWAC焦点指示器修改时间:2026-10-05 17:10:10