a11y —— aria 属性构建器
aria 属性的拼写与取值是体力活:aria-haspopup 还是 ariaHaspopup?'true' 还是 true?假值要不要渲染?每个组件自己拼一遍,要么拼错,要么忘记在值为空时省略(读屏里出现一堆 aria-controls="undefined")。shared/a11y 用一组纯函数把约定固化下来。
核心约定:假值省略
Section titled “核心约定:假值省略”所有布尔型 aria 构建器遵守同一规则:
| 属性 | 假值行为 | 理由 |
|---|---|---|
aria-required: false |
返回 {},不渲染 |
读屏对「非必填」的显式声明是噪音 |
aria-haspopup: false |
返回 {},不渲染 |
同上 |
aria-current: false |
不会出现 | 构建器类型上就不收 false(多数读屏会忽略它) |
aria-controls / aria-labelledby / aria-describedby |
id 为空时返回 {} |
渲染出 aria-controls="undefined" 是常见 bug |
aria-expanded: false |
原样输出 | 折叠态需要被显式播报 |
aria-invalid: false |
原样输出 | 「校验通过」需要被显式播报 |
两个「例外」是刻意的:aria-expanded / aria-invalid 的假值有语义信息,读屏会据此说「已折叠」「输入有效」。
import { generateAriaId, ariaExpanded, ariaControls, ariaLabelledby, ariaDescribedby, ariaInvalid, ariaRequired, ariaCurrent, ariaHaspopup, getPopupTriggerAria, getDecorativeAria, type LhAriaAttrs, type LhAriaHaspopup,} from '@lh-ui/share/core/shared/a11y'// aria 属性包的形状:键为 'aria-*' 或 'role',直接 v-bind / {...spread}export type LhAriaAttrs = Record<string, string | number | boolean | undefined>
// aria-haspopup 的合法取值(APG)export type LhAriaHaspopup = | 'false' | 'true' | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog' | 'combobox'| 函数 | 签名 | 作用 |
|---|---|---|
generateAriaId |
(prefix = 'lh-aria') => string |
生成 aria 关联用的唯一 id |
ariaExpanded |
(expanded: boolean) => LhAriaAttrs |
展开 / 收起态 |
ariaControls |
(id: string | undefined) => LhAriaAttrs |
引用所控制的面板 |
ariaLabelledby |
(id: string | undefined) => LhAriaAttrs |
引用可见文案作为可达名称 |
ariaDescribedby |
(id: string | undefined) => LhAriaAttrs |
引用补充描述 |
ariaInvalid |
(invalid: boolean) => LhAriaAttrs |
校验失败标记 |
ariaRequired |
(required: boolean) => LhAriaAttrs |
必填语义 |
ariaCurrent |
(current: 'page' | 'step' | 'location' | 'date' | 'time' | true | undefined) => LhAriaAttrs |
当前位置标记 |
ariaHaspopup |
(kind: LhAriaHaspopup | boolean) => LhAriaAttrs |
声明弹出何种浮层 |
getPopupTriggerAria |
(options: { expanded; popupId?; haspopup? }) => LhAriaAttrs |
弹出层触发器公共属性包 |
getDecorativeAria |
(decorative = true) => LhAriaAttrs |
装饰性元素对辅助技术隐藏 |
两个组合属性包
Section titled “两个组合属性包”getPopupTriggerAria
Section titled “getPopupTriggerAria”popover / popconfirm / select / cascader / date-picker / dropdown 的触发器全部用它,一次拿到三件事:
getPopupTriggerAria({ expanded: true, popupId: 'lh-popover-1' })// {// 'aria-expanded': true,// 'aria-controls': 'lh-popover-1',// 'aria-haspopup': 'true', // 默认值// }
getPopupTriggerAria({ expanded: false, popupId: 'lh-popover-1', haspopup: 'menu' })// { 'aria-expanded': false, 'aria-controls': 'lh-popover-1', 'aria-haspopup': 'menu' }
getPopupTriggerAria({ expanded: false }) // 没传 popupId// { 'aria-expanded': false, 'aria-haspopup': 'true' } —— 不输出 aria-controlshaspopup 默认 'true'。语义上更精确的值(下拉是 listbox、级联是 menu、日历是 dialog)会让读屏播报更准确,各组件按自己的语义传。
getDecorativeAria
Section titled “getDecorativeAria”getDecorativeAria() // { 'aria-hidden': true }getDecorativeAria(false) // {} —— 显式取消隐藏装饰性元素(纯分割线、loading 装饰、骨架屏的几何形状)对辅助技术是噪音。注意只对真正无信息的元素用——有文案的元素加 aria-hidden 是无障碍违规。传 false 返回空对象,用于「默认隐藏、某些场景要可见」的条件分支。
generateAriaId
Section titled “generateAriaId”const id = generateAriaId('lh-popover') // 'lh-popover-1'、'lh-popover-2' ……前缀固定为传入值,模块内自增计数器保证同进程多实例不撞 id。SSR 安全(无 DOM 访问)。
注意:计数器是模块级可变状态,这个函数不是纯函数——同样的输入在进程的不同时刻返回不同输出。服务端渲染时计数从 0 开始,客户端注水时也从 0 开始,只要渲染顺序一致 id 就一致;但若 SSR / CSR 渲染顺序不同(异步组件、条件渲染差异),可能出现 id 不匹配。各 UI 库的弹层组件在客户端才生成 id(onMounted 之后),规避了这个问题。
<template> <button v-bind="getPopupTriggerAria({ expanded: open, popupId: panelId, haspopup: 'dialog' })" @click="toggle" > 打开 </button> <div :id="panelId" role="dialog" v-show="open">……</div></template>
<script setup lang="ts">const panelId = generateAriaId('lh-popover')</script>v-bind 一个返回对象的函数是 Vue 的惯用写法——属性包里的 undefined 值会被 Vue 自动跳过,与「假值省略」约定天然吻合。
const panelId = generateAriaId('lh-popover')
return ( <> <button {...getPopupTriggerAria({ expanded: open, popupId: panelId })} onClick={toggle} /> {open && <div id={panelId} role="dialog">……</div>} </>)// input 组件const aria = { ...ariaRequired(props.required), // 非必填时整个属性都不渲染 ...ariaInvalid(!!errorMessage.value), // 校验通过也显式播报 ...ariaDescribedby(errorMessage.value ? errorId : undefined),}分页 / 面包屑的当前位置
Section titled “分页 / 面包屑的当前位置”ariaCurrent(isActive ? 'page' : undefined) // 非当前页不输出,避免噪音ariaCurrent(isActive ? 'page' : undefined) // 分页用 'page',步骤条用 'step',日期用 'date'aria-controls的 id 必须真实存在:本模块只负责拼属性,id 对不对是组件的责任。浮层用v-if延迟渲染时,关闭态下面板不在 DOM 里,aria-controls指向不存在的元素——这是可接受的(读屏会忽略),但比指向错误元素好。- 不要给有文案的元素加
aria-hidden:这是无障碍硬违规。 ariaCurrent的取值按语义选:分页'page'、步骤条'step'、面包屑'location'、日历'date'。类型上收这些字面量 +true。- 组合属性包不可与原子函数混用覆盖:
getPopupTriggerAria已经包含aria-haspopup,外面再 spreadariaHaspopup(...)会被后展开的覆盖——按需选用一种。
@lh-ui/share/core/a11y → getPopupTriggerAria( )(填好参数后点运行)Copyright © 2024-2026 风默文学 | 宁ICP备2024006574号 |
苏公网安备32040002010537