1.AI 协作文档体系重构(objections/worklines/contracts+matrix.md) 2.coord 仲裁文档(final-decisions/cross-review/final-rulings/orchestration) 3.各服务 01/02 文档补全 4.共享包初始化(shared-ts/shared-go/hooks/ui-components/ui-tokens) 5.Proto 契约补全 6.004 架构影响地图更新 7.端口分配表 8.设计规格文档
120 lines
3.0 KiB
TypeScript
120 lines
3.0 KiB
TypeScript
import { useMemo, useState } from "react";
|
||
|
||
/**
|
||
* useA11yId - 唯一 ARIA ID 生成器
|
||
*
|
||
* 用途:为表单元素生成 aria-labelledby / aria-describedby 关联的唯一 ID。
|
||
* 迁移自 CICD 项目 A11y 工具集。
|
||
*
|
||
* @example
|
||
* const id = useA11yId("email-input");
|
||
* <input id={id} aria-describedby={`${id}-error`} />
|
||
* <span id={`${id}-error`}>{errorMessage}</span>
|
||
*/
|
||
|
||
let idCounter = 0;
|
||
|
||
/**
|
||
* 生成唯一 ID(带可选前缀)。
|
||
* 使用计数器 + 随机数,避免 SSR/CSR hydration mismatch。
|
||
*/
|
||
export function useA11yId(prefix?: string): string {
|
||
const [id] = useState(() => {
|
||
idCounter += 1;
|
||
const random = Math.random().toString(36).slice(2, 8);
|
||
const base = prefix
|
||
? `${prefix}-${idCounter}-${random}`
|
||
: `a11y-${idCounter}-${random}`;
|
||
return base;
|
||
});
|
||
|
||
return id;
|
||
}
|
||
|
||
/**
|
||
* 批量生成关联 ID(用于表单 input + label + error + description 关联)
|
||
*
|
||
* @example
|
||
* const ids = useA11yIds("email");
|
||
* // ids = { input: "email-input-1-xxx", label: "email-label-1-xxx", error: "email-error-1-xxx", description: "email-description-1-xxx" }
|
||
*/
|
||
export function useA11yIds(prefix: string): {
|
||
input: string;
|
||
label: string;
|
||
error: string;
|
||
description: string;
|
||
} {
|
||
const inputId = useA11yId(`${prefix}-input`);
|
||
const labelId = useA11yId(`${prefix}-label`);
|
||
const errorId = useA11yId(`${prefix}-error`);
|
||
const descriptionId = useA11yId(`${prefix}-description`);
|
||
|
||
return useMemo(
|
||
() => ({
|
||
input: inputId,
|
||
label: labelId,
|
||
error: errorId,
|
||
description: descriptionId,
|
||
}),
|
||
[inputId, labelId, errorId, descriptionId],
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 合并 ARIA 属性(覆盖优先级:后者覆盖前者)
|
||
*
|
||
* @example
|
||
* mergeA11yProps({ "aria-label": "默认" }, { "aria-label": "自定义" })
|
||
* // => { "aria-label": "自定义" }
|
||
*/
|
||
export function mergeA11yProps(
|
||
...props: Array<Record<string, unknown> | undefined>
|
||
): Record<string, unknown> {
|
||
const result: Record<string, unknown> = {};
|
||
for (const prop of props) {
|
||
if (prop) {
|
||
Object.assign(result, prop);
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* 描述输入框的 ARIA 属性
|
||
*
|
||
* @example
|
||
* const a11y = describeInput({ label: "邮箱", required: true, error: "邮箱格式错误", description: "请输入工作邮箱" });
|
||
* <input {...a11y} />
|
||
*/
|
||
export function describeInput(options: {
|
||
label: string;
|
||
required?: boolean;
|
||
error?: string;
|
||
description?: string;
|
||
invalid?: boolean;
|
||
}): Record<string, unknown> {
|
||
const { label, required, error, description, invalid } = options;
|
||
const props: Record<string, unknown> = {
|
||
"aria-label": label,
|
||
};
|
||
|
||
if (required) {
|
||
props["aria-required"] = true;
|
||
}
|
||
|
||
if (invalid || error) {
|
||
props["aria-invalid"] = true;
|
||
}
|
||
|
||
// aria-describedby 由调用方拼接(需关联 error/description 的 ID)
|
||
const describedBy: string[] = [];
|
||
if (description) describedBy.push("description");
|
||
if (error) describedBy.push("error");
|
||
|
||
if (describedBy.length > 0) {
|
||
props["aria-describedby"] = describedBy.join(" ");
|
||
}
|
||
|
||
return props;
|
||
}
|