键盘导航是无障碍适配中最容易被忽视却最影响实际体验的部分。蒙古国网络无障碍指南(以下简称指南)对页面元素的键盘可达性、焦点顺序、快捷键冲突规避等都有具体条款,如果只是靠口头约定或者注释来约束开发行为,规范很快就会在迭代中失守。本文将以IWAC合规检查框架为背景,演示如何用TypeScript的类型系统把这些指南条款固化成类型定义,让编译器成为无障碍规范的第一道守门员。

为什么键盘导航规范适合用类型系统来封装
键盘导航的规则本质上是一组有限的、可枚举的状态与操作组合:哪些按键允许触发操作、焦点如何移动、元素处于哪种焦点状态。这类“有限集合”恰好是TypeScript字面量联合类型的强项。指南中常见的条款例如“所有交互元素必须可以通过Tab键到达”“组合快捷键不得与屏幕阅读器保留键冲突”“Esc必须能退出模态上下文”,这些都可以映射为类型层面的约束,而不是运行时的散弹式检查。
用类型封装的另一个好处是文档化。当团队成员调用一个接受KeyboardNavigationConfig类型参数的API时,编辑器的智能提示会直接列出所有合法的按键行为与焦点策略,规范从一份没人看的PDF变成了每天都会接触的代码补全。同时,类型定义与IWAC检查器共享同一套枚举,可以让静态检查和自动化合规扫描保持口径一致,避免“编译通过了但扫描不合规”的尴尬。
需要注意的一点是,类型封装的目标不是替代运行时检查,而是尽早暴露错误。比如开发者误将焦点陷阱配置成既不循环也不回退的非法状态,这类问题在编译期就能被拦下,而真实的焦点行为仍需要测试工具验证。
基础类型:按键事件与导航语义的建模
第一步是建立指南中提到的按键语义模型。不要直接使用浏览器的KeyboardEvent.key字符串,而是定义一层语义化的别名,让代码与指南条款一一对应。指南通常把按键分为导航键、确认键、取消键和修饰键四类,可以先用字面量联合类型表达:
// 指南中定义的语义化按键类型
export type NavigationKey =
| 'ArrowUp'
| 'ArrowDown'
| 'ArrowLeft'
| 'ArrowRight'
| 'Home'
| 'End'
| 'Tab';
export type ActivationKey = 'Enter' | 'Space';
export type DismissKey = 'Escape';
// 所有受指南约束的按键
export type GuidelineKey =
| NavigationKey
| 'PageUp'
| 'PageDown';
// 修饰键需单独声明,避免与单键混用
export type ModifierKey = 'Alt' | 'Ctrl' | 'Shift' | 'Meta';
// 完整的语义化按键事件描述
export interface SemanticKeyEvent {
key: GuidelineKey;
modifiers: readonly ModifierKey[];
isRepeat: boolean;
}这样的建模带来两个直接收益。其一,类型系统会拒绝'F5'这类不在指南语义范围内的按键进入导航逻辑,防止开发者随意用功能键绑定导航行为而与辅助技术冲突。其二,modifiers被声明为只读数组,强调修饰键组合应当在配置阶段确定,而不是在事件回调里动态拼接,这符合指南对快捷键稳定性的要求。
接着可以为焦点移动方向建模。指南要求列表、表格、树形控件各有明确的移动语义,用判别联合可以把每种控件的移动规则区分开:
// 焦点移动意图的判别联合
export type FocusMoveIntent =
| { control: 'list'; direction: 'next' | 'previous' }
| { control: 'grid'; row: number; column: number }
| { control: 'tree'; action: 'expand' | 'collapse' | 'enter' | 'leave' };
// 焦点策略枚举,与指南条款对应
export type FocusTrapStrategy =
| 'cycle' // 循环移动
| 'stop' // 到边界停止
| 'return-first' // 到边界回到首个元素
| 'none'; // 不限制,仅用于非模态场景
// 模态场景禁止 'none',用交叉类型收窄
export type ModalFocusConfig = FocusTrapConfig & { trap: Exclude<FocusTrapStrategy, 'none'> };这里Exclude<FocusTrapStrategy, 'none'>是一个典型技巧:复用已有的联合类型并剔除非法分支,使模态组件的配置天然满足指南“模态对话框必须捕获焦点”的条款。当后续指南版本新增策略时,只需要修改基础类型,所有派生类型自动更新。
可聚焦元素接口与IWAC合规标记
指南要求区分“天然可聚焦”与“需要编程式管理焦点”的元素。可以为组件库定义一个统一的可导航元素接口,并通过品牌类型(branded type)给通过合规检查的组件打标:
// 品牌类型:标记已通过IWAC合规审查的组件
declare const IWAC_COMPLIANT: unique symbol;
export interface NavigableElementSpec {
/** 元素在Tab序列中的角色 */
role: 'tab-stop' | 'roving-tabindex' | 'aria-activedescendant';
/** 是否可通过Tab到达,仅 tab-stop 角色允许为 true */
tabbable: boolean;
/** 触发激活所需按键集合 */
activationKeys: readonly ActivationKey[];
/** 焦点可见性要求,指南要求不可关闭 */
focusVisible: true;
}
// 校验函数的返回类型携带合规标记
export interface ComplianceResult<T> {
spec: T;
readonly [IWAC_COMPLIANT]: true;
}
// 只有携带标记的spec才能传入运行时导航管理器
export function createNavigator<T extends NavigableElementSpec>(
spec: ComplianceResult<T>
) {
// 内部实现省略
return { attach: (el: HTMLElement) => void };
}这种设计的核心思想是“能力即证明”。开发者不能凭空构造ComplianceResult,因为品牌符号只在合规校验模块内部生成。换句话说,任何要接入键盘导航管理器的组件,必须先经过一次基于指南规则的校验,编译器保证了这条流程不可绕过。相比在代码评审时反复提醒“这个组件做过无障碍检查吗”,类型签名直接回答了这个问题。
品牌类型还有一个实际价值:当组件库升级、合规规则收紧时,旧的未打标组件会立刻在新版本createNavigator的调用处报类型错误,升级路径清晰且错误定位精准,不会出现运行时才暴露的静默降级。
泛型封装与配置层的类型安全
最后一个层面是把整套定义组装成可复用的配置生成器。利用泛型和条件类型,可以让不同控件自动获得对应的合法配置形状,例如网格控件必须提供行列处理器,而列表控件则不允许出现:
// 按控件类型分发配置形状
export type NavigationConfig<C extends FocusMoveIntent['control']> =
C extends 'grid'
? GridConfig
: C extends 'tree'
? TreeConfig
: ListConfig;
export interface GridConfig {
control: 'grid';
rowSize: number;
onMove: (intent: Extract<FocusMoveIntent, { control: 'grid' }>) => void;
}
export interface ListConfig {
control: 'list';
trap: FocusTrapStrategy;
onMove: (intent: Extract<FocusMoveIntent, { control: 'list' }>) => void;
}
export interface TreeConfig {
control: 'tree';
allowTypeahead: boolean;
onMove: (intent: Extract<FocusMoveIntent, { control: 'tree' }>) => void;
}
// 配置工厂:回调参数类型随控件类型自动收窄
export function defineNavigation<C extends FocusMoveIntent['control']>(
control: C,
config: NavigationConfig<C>
): NavigationConfig<C> {
return config;
}
// 使用示例:onMove的参数被推断为grid专属意图
const gridNav = defineNavigation('grid', {
control: 'grid',
rowSize: 4,
onMove: (intent) => {
// intent.row / intent.column 均有类型提示
console.log(intent.row, intent.column);
}
});Extract<FocusMoveIntent, { control: 'grid' }>让回调函数的参数类型自动收窄到当前控件的意图分支,开发者在网格组件的onMove里只能访问row和column,访问树形控件的action会直接报错。这种“配置形状随语义收窄”的体验,是纯运行时校验方案难以提供的。
在工程组织上,建议把这套类型定义独立成一个guideline-types模块,与IWAC检查器共用同一个包,版本号跟随指南版本。业务组件只依赖类型包而不依赖检查器,保持轻量;合规团队升级规则时发布新的类型包版本,破坏性变更会以类型错误的形式在业务仓库中显式呈现,比变更日志更难被忽略。
总结来看,这套封装的路径是:先用字面量联合类型枚举指南中的按键与策略,再用判别联合区分控件语义,然后通过品牌类型建立合规证明机制,最后用泛型工厂让配置获得精确的类型推断。四个层次叠加之后,指南条款不再是文档里的文字,而是嵌入日常开发的编译期约束,键盘导航的无障碍质量也因此从“事后补救”转变为“默认正确”。
TypeScript键盘导航无障碍修改时间:2026-08-31 11:43:23