import * as React from "react" /** * 生成唯一 ID(用于 aria-describedby、aria-labelledby 等)。 * 基于 React.useId,SSR 安全,服务端与客户端一致。 */ export function useA11yId(prefix: string): string { const id = React.useId() return `${prefix}-${id}` } /** * 合并多组 aria/data 属性。 * - 普通属性:后者覆盖前者 * - aria-* / data-* 字符串属性:以空格拼接,便于聚合 describedby 等 */ export function mergeA11yProps>( ...props: (T | undefined | null | false)[] ): T { const result = {} as Record for (const prop of props) { if (!prop) continue for (const key of Object.keys(prop)) { const value = prop[key] if (value === undefined || value === null) continue const isAriaOrData = key.startsWith("aria-") || key.startsWith("data-") const existing = result[key] if ( isAriaOrData && typeof existing === "string" && typeof value === "string" ) { result[key] = `${existing} ${value}`.trim() } else { result[key] = value } } } return result as T } /** * 计算输入框的 aria 属性。 * @param describedBy 额外描述元素的 ID * @param error 错误信息元素的 ID(存在则标记 invalid) * @param hint 提示信息元素的 ID */ export function describeInput( describedBy?: string, error?: string, hint?: string ): { ariaDescribedBy?: string; ariaInvalid?: boolean } { const ids = [describedBy, error, hint].filter( (v): v is string => v != null && v.length > 0 ) return { ariaDescribedBy: ids.length > 0 ? ids.join(" ") : undefined, ariaInvalid: Boolean(error), } } /** * 提供加载状态的 aria 属性。 * aria-busy 标记区域正在加载,aria-live=polite 让屏幕阅读器在空闲时播报。 */ export function loadingAria(isLoading: boolean): { ariaBusy: boolean ariaLive: "polite" | "assertive" } { return { ariaBusy: isLoading, ariaLive: "polite", } }