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; }