Quasar 组件无障碍(a11y)实践指南:WAI-ARIA、键盘导航与焦点管理
Quasar 组件无障碍a11y实践指南WAI-ARIA、键盘导航与焦点管理【免费下载链接】quasarQuasar Framework - Build high-performance VueJS user interfaces in record time项目地址: https://gitcode.com/gh_mirrors/qu/quasar导读Quasar Framework 的组件库内置了一层相当完整的无障碍Accessibility简称 a11y支持语义化 HTML 标记、WAI-ARIA 属性、键盘交互模式和焦点管理都已内建到组件渲染逻辑中。本文以 docs/src/pages/options/accessibility.md组件无障碍行为从 v2.25 起系统化为主线逐项拆解框架替你做了什么、哪些责任必须由你的应用承担并给出 QBtn、QCheckbox、QSelect、QDialog 等核心组件的源码级验证帮你写出能通过 WCAG 审计、对屏幕阅读器与纯键盘用户真正可用的 Quasar 应用。无障碍是框架与应用之间的一份「契约」Quasar 可以渲染出正确的rolecheckbox、aria-checked与 Space/Enter 处理但它无法知道一个纯图标按钮的含义、你的品牌色对比度是否达标或者某个抽屉在语义上代表什么地标landmark。本文末尾的组件一览表会精确告诉你每一块职责属于契约的哪一侧。Quasar 提供了什么语义化标记Semantic markupQuasar 组件在「有原生元素可用」的地方一律渲染原生 HTML 元素——原生元素自带的语义、状态与键盘行为永远比用 ARIA 重新造轮子更健壮真正的buttonQBtn、a href所有路由链接、formQForm、tableQTable、hrQSeparator来自 QLayout 家族的完整地标集header、footer、aside和main。当视觉设计必须使用自定义元素时组件会声明对应的 ARIA role 并管理其必需状态rolecheckbox/radio/switch配合aria-checked含mixed三态roleslider/spinbutton配合aria-valuemin/aria-valuemax/aria-valuenowroleprogressbar、roletablist/tab/tabpanelroletree/treeitem配合aria-level/aria-setsize/aria-posinset补偿虚拟滚动rolecomboboxlistbox/optionQSelectroleseparator配合完整键盘缩放QSplitter。以三态复选框为例ui/src/components/checkbox/use-checkbox.js 中的attributes计算属性逐项生成const attrs { tabindex: tabindex.value, role: type toggle ? switch : checkbox, aria-label: props.label, // switch role 不允许 mixed // 辅助技术也会把它映射为 false aria-checked: isIndeterminate.value ? type toggle ? false : mixed : isTrue.value ? true : false }同一文件还实现了 Enter/Space 键切换onKeydown中拦截keyCode 13/32stopAndPreventonKeyup中触发onClick完成状态切换use-checkbox.js#L56-L60、use-checkbox.js#L195-L199。有些容器刻意不声明任何 role因为其中可以承载任意内容错误声明会产生无效标记。QMenu 是典型例子ARIA 的menurole 只允许菜单项作为子元素所以 QMenu 保持中性当其中的内容确实是命令列表时由你在内部的 QList 上声明rolemenu。QItem 的角色则由上下文推导声明了 menu 时是menuitem在列表中时是listitem可点击时是button跳转链接则保持链接。装饰性元素对辅助技术隐藏每个 QIcon 默认渲染aria-hiddentrue交互式图标如输入框的清除按钮会以 role 本地化标签重新参与进来遮罩层、虚拟滚动填充、自定义滚动条等内部管道一律aria-hidden。键盘支持Keyboard support激活一切可点击元素均可通过键盘激活。QBtn 甚至在链接形态a的按钮上合成了 Space 激活——原生链接只响应 Enter。在 ui/src/components/btn/QBtn.js 中onKeydown拦截isKeyCode(e, [13, 32])Enter/Space随后把焦点移回按钮根元素并添加q-btn--active激活态通过keyup监听在按键释放时结束可点击的 QItem、chips、展开头、可排序表头和 stepper 头也都处理 Enter/Space。复合组件使用 roving tabindex——整个组件只占一个 Tab 停靠点方向键在内部移动遵循 WAI-ARIA APGQTabs、QOptionGroup单选模式、QRating、QDate 的日历、QCarousel 的导航、QEditor 的工具栏和 QTree。取值组件响应方向键QSlider、QRange、QKnob、QSplitter、QTime 的 spinbuttons多数带 Home/End 与 PageUp/PageDown 变体QSelect 实现了完整的 combobox 键盘契约包括 typeahead 首字母定位。Escape 关闭呈链式只有最顶层的弹出层dialog、menu、tooltip、覆盖模式 drawer响应逐层关闭persistent 弹层只会抖动或忽略。横向方向键全链路 RTL 感知见 RTL 支持——tabs、单选组、slider、日期导航、编辑器工具栏和 splitter 在 RTL 语言包下全部镜像方向。Escape 处理与焦点回收在所有平台上都处于激活状态因此混合设备带键盘的 iPad、带鼠标的 Android 设备也能获得完整键盘行为没有硬件键盘时它们自然不会触发。在 Capacitor/Cordova 构建下关闭行为还通过 Quasar 的 History 插件接入了平台返回键。焦点管理Focus management对话框记录打开它的元素渲染时把焦点移入对话框尊重autofocus/data-autofocusmodal 状态下焦点游离出对话框会被拉回关闭时把焦点还给打开者。实现位于 ui/src/components/dialog/QDialog.jsfocus()会依次查找[autofocus]、[data-autofocus]等选择器找不到时聚焦容器shake()用于 persistent 对话框的抖动反馈。渲染时以role: dialogaria-modal有 backdrop 时为true挂载根元素QDialog.js#L513-L524。菜单行为相同多一处精妙处理——从菜单最后一个可聚焦元素按 Tab或从第一个按 ShiftTab会关闭菜单并从锚点继续 Tab 遍历键盘焦点永远不会泄漏到 portal 背后的空洞中。弹出层过渡动画播放期间焦点移动会进入队列避免多个焦点意图相互竞争在 modal 对话框内打开的菜单会渲染在对话框元素内部这样aria-modal不会把它从辅助技术的可访问树中隐藏——QDialog.js#L505-L508 的注释明确说明了这一点并通过__getAriaModalEl暴露给 usePortal。QForm在验证失败后聚焦第一个无效字段。焦点环仅由键盘触发鼠标或触摸交互后组件把焦点停在一个不可见的辅助元素上不会闪现焦点环键盘焦点则保持可见指示。这是内建的无需:focus-visiblepolyfill。内建指示样式针对桌面模式混合触屏键盘的应用可能需要自己的焦点样式。这个「停靠不可见辅助元素」的机制实现于 ui/src/composables/private.use-refocus-target/use-refocus-target.js它渲染一个tabindex-1、classno-outline的span鼠标/触摸点击后把焦点移过去同时刻意跳过aria-hiddentrue的控件浏览器会拒绝把焦点移入被 AT 隐藏的元素并给出警告。屏幕阅读器标签与语言包内建控件标签——输入框清除按钮、chip 的删除图标、展开/折叠箭头、分页的 first/prev/next/last、QDate 的月份/年份导航、轮播箭头、编辑器工具栏——全部来自当前激活的 Quasar 语言包并自动以你的应用语言播报。语言包中这些键多为函数形态例如 ui/lang/en-US.js 中的expand: label (label ?Expand ${label}: Expand)。如果你自研语言包需要覆盖这些键如label.expand是函数屏幕阅读器用户获得的就是语言包提供的内容。ARIA 属性透传fall-through你在 Quasar 组件上放置的任何aria-*属性或role都会落到语义正确的元素上表单字段上落到原生input/ 焦点目标QDialog 上落到roledialog元素依此类推。你的属性优先级高于自动生成的属性字段上的aria-describedby是合并而非覆盖。这正是下面「应用的责任」清单中一切事项的标准机制——无需任何特殊 props。你的应用需要负责什么可访问名称Accessible names。没有任何东西能替你生成有意义的名称。请为以下元素提供aria-label或可见文本纯图标 QBtn 和 FABQImg 的altQVideo 的titleQDialog、QDrawer、QToolbar、QOptionGroup 以及任何希望可区分的rolenavigation/rolegroup容器的aria-label/aria-labelledby。多个未命名的地标或工具栏是 WCAG 违规项。注意QTooltip不为其锚点命名——它只描述锚点——所以纯图标控件即使带了 tooltip 也需要自己的aria-label。自定义触发器上的弹出语义。QBtnDropdown 和 QSelect 完整接线了触发器的 ARIAaria-expanded、条件性aria-controls从 v2.25 起 QMenu 也会在其锚点上维护aria-expanded当 QMenu 自身声明了 role 时还会加aria-haspopup。但只有锚点是 ARIA 允许承载这些状态的控件时它才这样做——你的锚点需要是button、链接或带 widget role 的元素普通div什么也得不到这是有意为之。即使采用推荐形态在弹出内容的 QList 上声明 rolearia-haspopup仍要由你提供——QMenu 锚点上作为普通属性QBtnDropdown 则通过其toggle-aria-haspopupprop。相关实现与测试可见 QMenu.test.js其中验证了aria-expanded在展开/收起时的翻转、已声明 popup role 时镜像aria-haspopup、无 role 弹出层不声明aria-haspopup三种行为。地标结构。QLayout 家族免费给你每种地标各一个——不要再自己加mainQPage 已经是了多个抽屉或导航区要加标签以便区分。颜色对比度。Quasar 在亮色或暗色模式下都不做对比度强制。请用 WebAIM 对比度检查器 检查 WCAG 2.2 对比度下限正文文本 4.5:1——如果你同时支持两种主题两种都要查。减少动效Reduced motion。只有动画 CSS 工具类尊重prefers-reduced-motion。组件过渡、ripple 和滚动驱动效果QParallax都不响应——对动效敏感的用户需要你自己调低这些例如$q.config.ripple false、过渡 props、关闭 autoplay。QCarousel 的autoplay在悬停或聚焦时从不暂停使用它时请提供暂停控件WCAG 2.2.2。播报异步状态。加载指示器——QSpinner、QInnerLoading、QSkeleton、QAjaxBar、QUploader 的逐文件状态——只是视觉表现。当状态变化对用户重要时请用 live region 包裹或伴随rolestatus加一句简短文本如 Loading…纯装饰性占位符用aria-hiddentrue隐藏。手势的键盘替代方案。滑动手势激活QSlideItem、下拉刷新和触摸平移没有键盘路径。请提供平行操作——可见按钮、上下文菜单或把 QPullToRefresh 的trigger()方法接到一个按钮上。视口缩放。WCAG 1.4.4 要求文本可放大至 200%。Quasar CLI 脚手架在 Web 模式下允许捏合缩放widthdevice-width, initial-scale1更早版本脚手架生成的应用带有user-scalableno, maximum-scale1的 viewport meta这会令每一项自动化审计失败——请把index.html中的meta nameviewport更新为相同配置。Cordova/Capacitor 构建有意保持固定视口以获得原生 App 手感无障碍由系统级缩放iOS Zoom、Android 放大覆盖。解锁视口有一个副作用iOS Safari 会在可编辑元素字号低于16px且获得焦点时自动放大页面。有三个界面低于此阈值且各自取字号来源不同——QField 系控件QInput、QSelect、QFile、QPagination取$input-font-size14pxQColorPicker 的 Tune 页输入框取$color-picker-tune-tab-input-font-size11pxQEditor 内容不声明自己的字号、继承$body-font-size14px。如果缩放破坏你的设计请提升你实际用到的界面对应的 Sass 变量——单独设$input-font-size: 16px仍会留下 QEditor 和 QColorPicker 继续缩放——或把 16px 字号限定到这些元素上而不是重新禁用缩放。测试。没有任何框架能替代测试。用纯键盘走一遍关键流程一切可达吗焦点可见吗能退出来吗做一轮屏幕阅读器测试macOS/iOS 自带 VoiceOverWindows 可用免费的 NVDA并把 axe 或 Lighthouse 加入 CI 做自动化检查——它们能抓出机械性失败名称、对比度、ARIA 合法性让你的人工测试专注于流程。组件一览Components at a glance每个组件名链接到其文档页的 Accessibility 小节那里描述了精确行为——包括它的局限——以及你应该额外补充什么。按钮组件内建行为键盘QBtn原生button或a需要时推导rolebuttonaria-disabled带percentage加载时显示 progressbar ARIAEnter/Space含链接按钮上的 SpaceQBtnDropdownDisclosure 模式aria-expanded、aria-controls弹出存在期间、本地化展开/折叠标签、可选toggle-aria-haspopup继承 QBtn QMenuQBtnGroup仅视觉分组——自行加rolegrouparia-label每个按钮独立 Tab 停靠QBtnToggle每个选项aria-pressed每选项attrs支持标签每按钮 Enter/SpaceQFab触发器上aria-expanded/aria-controls收起时动作对 AT 隐藏焦点回到触发器Enter/Space 打开并激活导航组件内建行为键盘QTabsroletablist/tabaria-selected、aria-orientationRoving tabindex方向键RTL 感知、Home/End显式激活QTabPanelsroletabpanel、tabindex0tab↔panel 的 id 接线是手动的文档有述面板可经 Tab 到达QBreadcrumbs原生链接无地标/aria-current——自己包nav并标记当前页原生链接行为QPagination命名的rolenavigation本地化 first/prev/next/last 标签活动页aria-current按钮上 Enter/SpaceEnter 提交输入模式QStepperaria-currentstep每步为带标签的group可导航头暴露为按钮禁用项为aria-disabled按钮可导航头上 Enter/SpaceQToolbar / QBarroletoolbar——页面有多个时用aria-label命名子元素为独立 Tab 停靠表单字段组件内建行为键盘QField / QInputlabel for接线错误经rolealert播报配合aria-invalid/aria-errormessage/aria-describedby仅在消息渲染期间可访问的清除按钮原生编辑清除按钮 Enter/SpaceQSelect完整 combobox 模式rolecomboboxaria-expanded/aria-controls/aria-activedescendantrolelistbox/optionaria-selected与虚拟滚动感知的aria-setsize/aria-posinset方向键打开/导航、typeahead、Home/End、PageUp/PageDown、Enter 选中、Esc 关闭QForm原生form验证失败时聚焦第一个无效字段原生提交QEditorroletextboxaria-multiline工具栏遵循 APG toolbar 模式标签本地化工具栏 roving tabindex、方向键/Home/EndCtrl 格式化快捷键QFile字段框架 验证 ARIA可键盘打开的选择器Enter/Space 打开选择器QUploader真实按钮 本地化名称、progressbar ARIA——但你仍需播报状态变化原生按钮激活QSelect 的 combobox/listbox 属性生成逻辑在 ui/src/components/select/QSelect.jscomboboxAttrs维护rolecombobox与aria-expanded选项展开时补充aria-activedescendantlistboxAttrs声明rolelistbox且只包裹选项本身——虚拟滚动的填充元素不是 listbox 的合法子元素QSelect.js#L1144-L1150。表单控件组件内建行为键盘QCheckboxrolecheckbox三态aria-checked含mixedlabel生成aria-labelEnter/Space 切换QRadioroleradioaria-checked组语义来自 QOptionGroupEnter/Space 选中QToggleroleswitcharia-checkedEnter/Space 切换QOptionGrouproleradiogroup/groupAPG 单选组模式Roving tabindex方向键选中RTL 感知跳过禁用项QSlider / QRangeroleslideraria-valuemin/max/now、aria-orientation、aria-readonlyQRange每个滑块一个命名的 slider方向键步进RTL/垂直感知、PageUp/PageDown ×10QRating每星一个 radio-group 模式 逐星标签icon-aria-labelRoving tabindex方向键移动、Enter/Space 选中QKnob焦点元素上roleslider 值 ARIA方向键步进、PageUp/PageDown ×10QColor三个视图全部本地化命名光谱面板roleslider饱和度亮度aria-valuetext、Tune 页原生输入 slider、色板色块命名按钮、aria-pressed选中光谱方向键按 1 移动饱和度/亮度Shift 按 10、Home/End 调饱和度、PageUp/PageDown 调亮度Tune 页原生输入 slider 键色板 roving tabindex方向键移动上/下按行、Home/End、Enter/Space 取色QDate日期按钮带完整日期标签aria-pressed选中今天aria-currentdate导航本地化Roving tabindex方向键跨月、Home/End、PageUp/PageDownShift 跨年QTime头部每单位rolespinbutton 本地化标签与值 ARIA钟面是指针专用的可视化方向键步进、Home/End、直接输入数字弹出层组件内建行为键盘QDialogroledialogaria-modal打开时焦点移入、modal 期间焦点受控、关闭时恢复——用aria-label/aria-labelledby命名Esc 关闭persistent 对话框抖动QMenu有意无 role 的弹出层在内部 QList 上声明rolemenu控件锚点上aria-expanded关闭时恢复焦点Esc 关闭Tab 越过边界时关闭并从锚点继续QTooltip显示期间经aria-describedby描述其锚点——从不命名roletooltip键盘聚焦时显示Esc 关闭且不移动焦点WCAG 1.4.13QPopupProxy按屏幕尺寸在 QMenu 与 QDialog 间切换——语义跟随实际渲染的组件委托QPopupEdit基于 QMenu 的编辑面本地化 Set/Cancel 按钮关闭时从不静默提交Esc 取消Enter 保存需自行接线列表与数据组件内建行为键盘QList / QItem上下文推导 rolelist/listitem、menu→menuitem、可点击→button、链接保持链接禁用可操作项aria-disabled可点击项 Enter/SpaceQExpansionItem头部rolebuttonaria-expanded/aria-controls与本地化展开/折叠标签收起内容真正隐藏Enter/Space 切换QTable原生table可排序头aria-sort 键盘排序本地化选中、分页与加载名称Enter/Space 排序其余标准控件QMarkupTable原生table包装滚动区是 Tab 停靠——header/scope/caption 语义由你编写原生QTreeroletree/treeitem/grouparia-expanded/selected/checked虚拟模式补充aria-level/setsize/posinsetRoving tabindex 覆盖每个可见节点含禁用项惰性方向键导航/展开/折叠、Home/End、Enter 选中、Space 展开——或勾选可勾选节点QVirtualScroll填充对 AT 隐藏切片变化时焦点不落body屏幕外项目对 AT 不存在拥有滚动的容器是 Tab 停靠QTimeline原生ul/li结构静态内容QChatMessage文本可读sent/received 仅为视觉——用name传达作者身份静态内容QCarousel导航为tablist roving tabindex 与逐幻灯片标签幻灯片为tabpanel箭头本地化方向键方向与 RTL 感知、Home/End选中跟随焦点QTable 的键盘排序值得单独说明在 ui/src/components/table/QTh.js 中可排序表头获得tabindex0、aria-sort值来自col.__ariaSort测试覆盖于 QTh.test.jsSpace 键被preventDefault后与 Enter 一起触发排序。反馈与媒体组件内建行为键盘QBadgerolestatuspolite live regionlabel生成aria-label—QBannerrolealert——动态插入时播报—QChip可点击 chip 为rolebutton选中型 chip 加aria-pressed删除图标可键盘操作且带本地化标签Enter/Space 激活/删除QLinearProgress / QCircularProgressroleprogressbar 值 ARIAindeterminate 时省略——请加aria-label—QAjaxBar激活时 progressbar ARIA空闲时aria-hidden—QSpinner无——配合 live region 或作为装饰隐藏—QInnerLoading仅视觉覆盖层——自行播报状态并管理被覆盖内容—QSkeleton装饰性占位符——自行标记加载区域—QIcon始终aria-hiddentrue语义化图标经 attrs 覆盖—QImgroleimg由alt命名alt标记为装饰—QAvatar / QCard表现型容器用tagprop 构造语义结构—QSeparator原生hraria-orientation—QVideoiframe由titleprop 命名——务必提供内嵌播放器自带QParallax滚动驱动动效用media插槽承载有意义图像alt并考虑减少动效用户—布局与滚动组件内建行为键盘QLayout经子组件的地标骨架header、footer、aside、main—QHeader / QFooter真实header/footer地标隐藏的 marginals 离开 AT 树reveal-hidden 的在聚焦时重新揭示聚焦重新揭示QDraweraside地标承载你的role/aria-*遮罩与开启条对 AT 隐藏无覆盖焦点陷阱Esc 关闭 modal 态抽屉QPage渲染页面的main——不要再加一个—QPageScroller定位包装——插槽里放真实按钮以获得键盘访问经插槽按钮QScrollArea自定义滚动条对 AT 隐藏内容溢出时容器是 Tab 停靠聚焦后原生滚动键QSplitter完整 WAI-ARIA 窗口分割器roleseparator、aria-controls、值 ARIA、本地化名称方向键缩放RTL 感知、Home/End、Enter 折叠/恢复QSlideItem滑动操作仅指针——请提供键盘替代无内建QInfiniteScroll原生滚动时加载加载状态不播报随键盘滚动工作QPullToRefresh指针手势通过按钮暴露trigger()给键盘用户经由你自己的按钮纯视觉或无渲染、没有无障碍表面的工具组件QSpace、QPageSticky、QIntersection、QResizeObserver、QScrollObserver、QNoSsr、QSlideTransition 和 QResponsive。外部资源WCAG 2.2 —— Web 内容无障碍指南快速参考WAI-ARIA Authoring Practices Guide (APG) —— Quasar 组件遵循的交互模式MDN Accessibility —— 实用的 HTML/ARIA 参考WebAIM —— 文章、对比度检查器与屏幕阅读器调查数据axe DevTools 与 Lighthouse —— 浏览器与 CI 中的自动化审计NVDAWindows免费与 VoiceOvermacOS/iOS 内建—— 测试用屏幕阅读器【免费下载链接】quasarQuasar Framework - Build high-performance VueJS user interfaces in record time项目地址: https://gitcode.com/gh_mirrors/qu/quasar创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考