Files
Edu/packages/hooks/src/use-a11y-id.ts
SpecialX faaaf29f67 docs: ai 协作文档体系重构与多 ai 仲裁结果落地
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.设计规格文档
2026-07-10 12:58:22 +08:00

120 lines
3.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}