在西班牙公共部门数字化项目里,表单错误提示的合规处理经常被简化成“变红加文字”,但无障碍审计并不认可这种做法。根据西班牙皇家法令 Real Decreto 1112/2018 所引用的 EN 301 549 标准,公共部门网站与移动应用必须让残障用户能够感知、理解并修正输入错误。DOSH 作为团队内部使用的表单渲染库,如果错误提示类型设计得过于松散,各业务模块就会用字符串、布尔值或任意对象传递错误状态,最后导致读屏软件无法稳定播报错误摘要。本文的目标就是把这类提示统一成可推导、可测试的 TypeScript 类型,并接入无障碍属性映射。

西班牙无障碍法令对错误提示的约束不只在颜色上
很多前端团队对 WCAG 的理解停留在对比度层面,认为错误提示只要有红色边框和一段说明文字就足够了。但 EN 301 549 对表单错误的要求要具体得多。按照 WCAG 2.1 的 3.3.1 错误识别,如果输入错误能够被自动检测,就必须用文本方式指出出错的项目。3.3.3 错误建议进一步要求,在已知修正方式时向用户提供建议,除非这样做会危及安全或改变内容含义。4.1.3 状态消息则强调,动态出现的错误信息需要被辅助技术及时感知,不能只靠视觉变化。
也就是说,一个合规的错误提示至少需要回答三个问题:哪个字段出错了、错误原因是什么、用户应该怎么改。颜色、图标、边框都只能作为辅助线索,不能单独承担错误提示的职责。对于 DOSH 这类承载多个政府表单的库来说,同一套错误提示必须能映射到 aria-invalid、aria-describedby 以及具有 role="alert" 或 aria-live 的容器上。否则每次做无障碍审计,都会因为错误提示与控件没有关联而被扣分。
还有一个容易忽略的点:错误提示不应在页面加载时就静默存在,也不应在用户修正后仍然保留。理想状态是,当验证失败时,错误信息动态插入并触发播报;当用户填写正确时,错误状态被清除,同时辅助技术能感知到状态消失。因此类型设计中需要区分错误的严重级别和生命周期,而不是仅仅用一个布尔值表示有无错误。
用 TypeScript 定义可扩展的错误提示类型
为了让 DOSH 中的表单错误提示可复用,第一步是定义错误级别和错误提示实体。错误级别不能只用一个 boolean,因为“必填项为空”和“格式可能有误但允许保存草稿”的风险完全不同。一个实用的方案是使用字符串字面量联合类型,既能获得编译期检查,又能在运行时保持简单。下面是一个基础类型示例:
type ErrorSeverity = 'error' | 'warning' | 'info';
interface ErrorHint {
/** 错误关联的表单控件 id,必须与真实控件的 id 一致 */
fieldId: string;
/** 面向用户的可读错误消息,可由翻译模块替换 */
message: string;
/** 错误级别,决定 aria-invalid 与通知策略 */
severity: ErrorSeverity;
/** 可选的修正建议,例如格式示例或长度限制 */
suggestion?: string;
/** 机器可读的错误码,便于测试与 i18n 查表 */
code: string;
}
type ValidationResult =
| { valid: true; value: string }
| { valid: false; errors: ErrorHint[] };
ErrorHint 的核心是 fieldId 和 code,前者负责与具体控件绑定,后者负责翻译和测试。为什么 errors 要用数组而不是单个对象?因为一个字段可能同时命中多条规则,比如输入了过短的值并且包含非法字符,此时用户需要一次看到或听到所有需要修正的点,而不是处理完一条再看到另一条。允许数组也能让验证器按规则顺序返回错误,后续在错误摘要中按优先级排序。
仅定义实体还不够,DOSH 还需要统一的构造器,避免业务代码手动拼接错误对象。下面这个工厂函数把字段 id、错误码、默认文案和级别打包起来,让调用方不必关心完整结构:
function buildErrorHint(
fieldId: string,
code: string,
message: string,
severity: ErrorSeverity = 'error'
): ErrorHint {
return { fieldId, code, message, severity };
}
这样做的好处是,后续如果要在错误对象上增加 focusTarget 或 liveRegionId,只需要改工厂函数和接口定义,所有调用方不受影响。同时,ValidationResult 的联合类型让验证器返回值只有成功和失败两种形态,成功时携带规范化后的值,失败时携带错误数组。组件层在使用结果时可以依靠 TypeScript 的收窄能力,不必再做 if (result.errors) 这类脆弱的运行期判断。
在 DOSH 表单渲染层接入无障碍映射
类型定义之后,最关键的是把它映射到无障碍属性。DOSH 的文本输入组件可以接收 ErrorHint[],然后根据错误级别计算 aria-invalid,并给每个错误提示生成唯一 id,再把这些 id 通过 aria-describedby 关联到控件上。错误提示本身需要根据级别渲染为 role="alert" 或 role="status",这样动态插入时读屏软件能立即播报。
import { ErrorHint } from './error-types';
interface TextFieldProps {
id: string;
label: string;
errors: ErrorHint[];
}
export function TextField({ id, label, errors }: TextFieldProps) {
const hasError = errors.some(e => e.severity === 'error');
const errorIds = errors.map(e => `${id}-${e.code}`);
return (
<div className="field">
<label htmlFor={id}>{label}</label>
<input
id={id}
aria-invalid={hasError}
aria-describedby={errorIds.join(' ')}
/>
{errors.map((error, index) => (
<p
key={error.code}
id={errorIds[index]}
className="field-error"
role={error.severity === 'error' ? 'alert' : 'status'}
>
{error.message}
{error.suggestion ? `,${error.suggestion}` : ''}
</p>
))}
</div>
);
}
这段代码没有在高阶组件里隐藏错误渲染逻辑,而是把 errors 作为显式数据流传入。这样视觉错误文本和 aria-describedby 指向的内容天然一致,不会出现页面上写“NIF 格式错误”而读屏却读到“字段无效”的情况。需要注意,role="alert" 适合严重错误,会打断当前读屏;role="status" 适合警告或提示,不会打断。使用 severity 区分二者比在组件里写死更合理。
除了单字段错误,DOSH 还应在提交失败时生成错误摘要。错误摘要通常放在表单顶部,集中列出所有 severity === 'error' 的提示,并设置 role="alert" 或 aria-live="assertive"。摘要中的每一项可以点击或直接跳转到对应字段,但跳转逻辑不必写在类型层,组件根据 fieldId 调用 document.getElementById 即可。焦点管理是另一个无障碍重点:提交失败后把焦点移到第一个出错控件,能显著减少键盘和读屏用户的操作路径。
错误提示的国际化与测试策略
西班牙公共部门的表单不仅要提供西班牙语,还经常需要加泰罗尼亚语、巴斯克语或加利西亚语。把用户可见文案直接写死在验证器里会带来巨大的维护成本。更稳妥的做法是让 ErrorHint 只携带错误码,渲染层根据当前语言解析消息。错误码使用枚举或字面量联合,配合一个记录类型完成翻译映射。下面是一个支持西班牙语和加泰罗尼亚语的示例:
type ErrorCode = 'REQUIRED' | 'FORMAT_NIF' | 'MAX_LENGTH';
const errorMessages: Record<ErrorCode, { es: string; ca: string }> = {
REQUIRED: { es: 'Este campo es obligatorio.', ca: 'Aquest camp és obligatori.' },
FORMAT_NIF: { es: 'El formato del NIF no es válido.', ca: 'El format del NIF no és vàlid.' },
MAX_LENGTH: { es: 'Has superado la longitud máxima.', ca: 'Has superat la longitud màxima.' },
};
有了稳定的错误码,单元测试就不再需要依赖本地化文案。测试可以断言某个验证器在空值输入时返回 { code: 'REQUIRED', severity: 'error' },而不必担心西语重音符号或大小写变化。另一方面,类型守卫函数可以作为运行时保障,防止后端返回的异常数据被塞进错误列表。比如一个 isErrorHint 守卫会检查 fieldId、message、severity 和 code 是否都存在且类型正确。
对无障碍映射的测试同样不能忽略。可以围绕渲染函数做三组检查:第一,当 errors 包含 severity: 'error' 时,输入框必须具有 aria-invalid="true";第二,每个错误提示元素必须有 id,且这些 id 全部出现在输入框的 aria-describedby 中;第三,动态插入的错误容器必须具有 role="alert" 或等效的 aria-live 属性。把这些检查固化到 CI,可以避免后续功能迭代破坏无障碍支持。
常见实现误区与避坑清单
即便理解了规范,实际封装时仍会出现几类典型问题。最常见的是只用颜色变化提示错误,没有文本说明。第二类是错误信息与输入框缺少程序化关联,读屏用户聚焦到控件时听不到错误描述。第三类是把错误信息放在 placeholder 或 title 属性里,这些属性不能被稳定播报,也不适合承载需要用户记住的修正建议。第四类是动态插入错误提示时没有设置 role="alert",导致读屏软件完全忽略新出现的错误节点。
还有一类隐蔽问题来自错误状态清理。用户修正字段后,如果只把错误文字隐藏而不更新 aria-invalid 和 aria-describedby,读屏软件仍会认为该字段处于错误状态。DOSH 应在每次输入或失焦验证时同步更新错误数组,而不是依赖 CSS 显隐。使用受控组件时,可以结合 useState 或表单状态管理库把 ErrorHint[] 作为唯一错误来源,视觉渲染和无障碍渲染都从这一来源派生。
下面是一个可以作为收尾自检的清单:错误提示是否都通过 ErrorHint 结构生成;每个严重错误是否同时关联到控件和顶部摘要;错误容器是否具有正确的 role;提交失败后焦点是否移动到首个错误字段;错误码是否覆盖了需要的语言;运行时是否用类型守卫拦截非法数据。把这些条目纳入组件评审,西班牙无障碍法令就不再是上线前的突击修补,而是从一开始就融入 DOSH 的表单渲染流程。
TypeScript表单无障碍DOSH修改时间:2026-10-04 15:37:03