import { useMemo, useState } from "react";
/**
* useA11yId - 唯一 ARIA ID 生成器
*
* 用途:为表单元素生成 aria-labelledby / aria-describedby 关联的唯一 ID。
* 迁移自 CICD 项目 A11y 工具集。
*
* @example
* const id = useA11yId("email-input");
*
* {errorMessage}
*/
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 | undefined>
): Record {
const result: Record = {};
for (const prop of props) {
if (prop) {
Object.assign(result, prop);
}
}
return result;
}
/**
* 描述输入框的 ARIA 属性
*
* @example
* const a11y = describeInput({ label: "邮箱", required: true, error: "邮箱格式错误", description: "请输入工作邮箱" });
*
*/
export function describeInput(options: {
label: string;
required?: boolean;
error?: string;
description?: string;
invalid?: boolean;
}): Record {
const { label, required, error, description, invalid } = options;
const props: Record = {
"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;
}