@zag-js/aria-hidden 无障碍隐藏机制详解:API、实现原理与版本演进 前端UI组件【免费下载链接】zagBuild your design system in React, Solid, Vue, Svelte or Vanilla. Powered by finite state machines项目地址https://gitcode.com/gh_mirrors/za/zag点击查看免费下载本文以 packages/utilities/aria-hidden 包的 CHANGELOG 为主线结合同目录源码与测试系统讲解 Zag 中向屏幕阅读器隐藏背景内容这一无障碍核心能力的实现方式包括hideOthers/inertOthers/ariaHidden等 API 的用法、walkTreeOutside树遍历与锁栈的设计原理以及 CHANGELOG 记录中围绕嵌套模态、aria-controls、触屏设备等问题的历史演进。读完本文你将能理解 Zag 的模态组件Dialog、Drawer、Menu、Select 等如何在弹层打开时把背景从辅助技术中移走并在复杂嵌套场景下不破坏可访问性。包定位一个隐藏屏幕阅读器目标的轻量工具zag-js/aria-hidden是 Zag 仓库 packages/utilities 下的一个工具包README 中对其功能只有一句精炼描述Hide targets from screen readers。它本身不提供 UI 组件而是作为底层能力被 Zag 的 Dialog、Drawer、Popover、Menu、Select、Combobox 等模态或弹出类组件机器所使用。安装方式见 README.mdyarn add zag-js/aria-hidden # or npm i zag-js/aria-hidden从 package.json 可以看到它的工程特征零运行时副作用sideEffects: false可安全参与 Tree-shaking单一依赖仅依赖zag-js/dom-query工作区版本其中findControlledElements、getWindow、isElement、isHTMLElement等 DOM 查询工具被 aria-hidden 直接复用MIT 许可作者为 Segun Adebayo与 Zag 项目整体一致。核心 API四个入口函数src/aria-hidden.ts 中定义了三个面向底层的能力函数src/index.ts 则导出一个面向组件集成的组合入口。hideOthers把目标之外的一切设为 aria-hiddenexport const hideOthers ( originalTarget: Element | Element[], parentNode getParentNode(originalTarget), markerName data-aria-hidden, followControlledElements true, ) { if (!parentNode) return return walkTreeOutside(originalTarget, { parentNode, markerName, attribute: aria-hidden, followControlledElements, }) }要点originalTarget可以是单个元素或元素数组通常传入模态容器的根节点parentNode默认取目标所在文档的body即默认在整个页面范围内隐藏其他内容markerName默认data-aria-hidden用于标记由本工具写入的隐藏状态以便在释放锁时准确恢复返回一个撤销函数undo调用后恢复所有被隐藏节点的属性。inertOthers使用原生 inert 属性export const inertOthers (...same shape...) walkTreeOutside(originalTarget, { parentNode, markerName: data-inerted, attribute: inert, followControlledElements, })与hideOthers的唯一区别是把aria-hidden换成原生inert属性同时标记属性变为data-inerted。inert不仅从辅助技术中移除元素还会阻止其接收焦点与点击效果更强。suppressOthers按浏览器能力自动选择const supportsInert () typeof HTMLElement ! undefined HTMLElement.prototype.hasOwnProperty(inert) export const suppressOthers (originalTarget, parentNode?, markerName data-suppressed, followControlledElements true) { const fn supportsInert() ? inertOthers : hideOthers return fn(originalTarget, parentNode, markerName, followControlledElements) }suppressOthers是特性检测入口支持inert的现代浏览器走inertOthers否则回退到aria-hidden方案兼顾效果与兼容性。ariaHidden延迟执行 自动清理的组合入口export function ariaHidden(targetsOrFn: TargetsOrFn, options: Options {}) { const { defer true } options const func defer ? raf : (v: any) v() const cleanups: (VoidFunction | undefined)[] [] cleanups.push( func(() { const targets typeof targetsOrFn function ? targetsOrFn() : targetsOrFn const elements targets.filter(Boolean) as HTMLElement[] if (elements.length 0) return cleanups.push(hideOthers(elements)) }), ) return () { cleanups.forEach((fn) fn?.()) } }支持传入元素数组或返回元素数组的函数便于在 DOM 挂载完成后再解析目标defer选项默认true通过requestAnimationFrame把隐藏动作推迟到下一帧确保弹层先渲染、再隐藏背景CHANGELOG 0.8.0 中为此引入defer支持详见后文返回的清理函数会依次执行cancelAnimationFrame与锁的撤销逻辑。实现原理walkTreeOutside 的三层机制所有隐藏动作最终都收敛到 src/walk-tree-outside.ts 的walkTreeOutside。它由三套机制协同工作。1. 树遍历与保留/停止集合applyAttributeToOthersL42-L149是核心遍历函数保留链elementsToKeep目标元素及其所有祖先被加入保留集合遍历时遇到保留节点会深入其内部继续处理停止集合elementsToStop目标本身及被控弹层加入停止集合停止集合内的子树完全不被触碰深度遍历从parentNode出发对每个非保留、非停止的子节点写入aria-hiddentrue或inert。2. 引用计数与非本工具控制状态为了让嵌套锁与外部手动设置的属性互不干扰源码用三个全局数据结构记录状态counterMapWeakMap记录每个节点被多少个锁隐藏的计数uncontrolledNodesWeakMap记录在本次锁定前就已经被隐藏的节点markerMap按 markerName 分组的 WeakMap记录每个 marker 的写入计数。写入逻辑L88-L107非常讲究只有counterValue 1且节点原本已有aria-hidden时才把节点记入uncontrolledNodes——这意味着释放锁时只移除自己写入的属性绝不主动揭开用户原本手动隐藏的内容测试用例 never reveals a node that was hidden before any lock 专门验证了这一点。当全局lockCount归零时所有 WeakMap 被整体清空避免内存泄漏。3. 按文档隔离的锁栈注释与实现都明确了一个设计意图同一时刻只应用最顶层的锁Only the topmost lock is applied, like nested modal dialogs in the browser。createLockStackL158-L177维护一个锁数组新锁入栈时先pause()上一个锁断开 MutationObserver 并撤销属性再resume()自己出栈时若移除的是顶层锁则恢复栈中下一个锁每个 Document 一份锁栈lockStacks: WeakMapDocument, LockStack因此 iframe 等另一个文档里的模态不会暂停当前文档的锁。4. aria-expanded 同步受控弹层的自动揭示walkTreeOutside在resume阶段会注册一个 MutationObserverL230-L234监听parentNode子树中所有aria-expanded属性的变化。当目标内部的控制器如 Dialog 里的 Select 触发器展开时属性变为true观察器重新遍历把被控弹层从隐藏集合中释放出来关闭时再次隐藏。被控弹层的解析依赖zag-js/dom-query的 findControlledElements遍历根内所有[aria-controls]元素但只有aria-expanded true的控制器才被跟随通过getElementById定位被控元素且仅当被控元素带有交互容器角色如rolemenu、rolelistbox且非aria-modal时才纳入保留集合避免双重管理模态。5. 忽略节点与 Shadow DOM 处理ignoreableNodes集合包含script、output、status、next-route-announcerisIgnoredNodeL35-L40还会跳过rolestatus、带aria-live或data-live-announcer的节点——这些即时通知元素不应被隐藏对应 CHANGELOG 1.18.0 的变更unwrapHostL11-L12会把 Shadow DOM 内的目标向上回溯到其 host 元素使 Shadow DOM 中的弹层能正确参与页面级隐藏测试中专门覆盖了这一场景。从 CHANGELOG 看无障碍能力的演进脉络CHANGELOG 的主体是版本号与依赖同步记录但其中数条带实质说明的变更恰好勾勒出 aria-hidden 走向健壮的历史轨迹。0.66.0替换内部实现接入社区成熟方案Replace internalaria-hiddenimplementation witharia-hiddenpackage for improved accessibility这是最早的关键转向放弃自研实现改用社区久经考验的aria-hidden方案源码首行注释Based on https://github.com/theKashey/aria-hidden印证了这一点并以 MIT 协议引入。此后 Zag 的模态无障碍隐藏有了坚实的基线。0.78.2修复触屏设备上的 aria-hidden 警告Fixed accessibility warning related toaria-hiddenon touch devices. Blocked aria-hidden on an element because its descendant retained focus. The focus must not be hidden from assistive technology users...这条修复对应真实浏览器告警当aria-hidden元素的子树中仍有元素持有焦点时浏览器会阻止该属性生效。修复后工具在锁定前会确保目标获得焦点避免把聚焦元素藏起来从而消除告警并符合 WCAG 对焦点可见性的要求。0.82.2属性值从空字符串修正为 trueFix issue wherearia-hiddencould be setinstead of true此前在某些分支下属性会被写成aria-hidden而严格的辅助技术与选择器如[aria-hiddentrue]可能无法识别。修复后统一写入true。这一约定在现有源码中得到延续applyAttributeToOthers中attributeValue attribute aria-hidden ? true : 。1.18.0排除 status 角色与 output 元素Exclude elements withrolestatusandoutputelements when applying aria-hidden动态通知区域rolestatus、output元素、以及后来的aria-live、next-route-announcer一旦被aria-hidden覆盖屏幕阅读器的实时区域播报就会失效。此变更将这些节点加入忽略集合确保 Toast、加载状态、路由切换等实时播报在模态打开时仍能工作——对应源码中的ignoreableNodes与isIgnoredNode。1.25.0支持 aria-controls 被控元素Focus trap now includes elements referenced byaria-controlsas part of the trap Aria-hidden now preserves controlled elements when their controller is visible Makes select/popover components work seamlessly inside modal dialogs这是模态内嵌选择器/弹层能力的关键一跃。此前 Select、Popover 这类通过aria-controls引用外部弹层的组件在模态打开时会连弹层一起被隐藏。此次改动让焦点陷阱把aria-controls引用的元素纳入可聚焦范围aria-hidden 在控制器可见时保留被控元素最终实现 Select/Popover 在模态对话框内无缝工作。这正是前文所述findControlledElements MutationObserver 同步机制的由来。1.46.0修复嵌套模态与先挂载后打开的时序问题这是当前版本1.46.0最新的实质性修复包含两个紧密相关的场景嵌套弹层被错误隐藏当一个嵌套的 Dialog、Drawer 或模态 Popover 的 portal 在父级打开之前就已存在于 DOMSolid 的Portal或关闭状态下挂载内容的渲染模式打开后该弹层会从屏幕阅读器中被隐藏模态内弹层被错误隐藏从模态内部打开的 Menu、Select 等弹层若其 portal 在模态打开前已在 DOM 中同样会被隐藏。根因在于早期实现的锁栈只关注body级别的一次性遍历先挂载的 portal 在父级锁生效时已被打上aria-hidden后续打开时若没有重新遍历属性便残留下来。1.46.0 通过锁栈的 pause/resume 机制与 MutationObserver 重新应用遍历解决了该时序问题。测试文件 aria-hidden.test.ts 中 keeps a nested target visible when its portal was mounted earlier、reveals a popup when its controller inside the target expands 等用例正是对这一修复的回归验证。0.8.0defer 选项Add support fordeferoption to allow defering the execution of a function to the next tick即ariaHidden的defer选项默认通过requestAnimationFrame推迟隐藏执行确保弹层完成渲染后再隐藏背景避免弹层刚挂载就被藏起来的闪烁与误判。版本节奏与依赖同步CHANGELOG 中大量条目是Updated dependencies []: zag-js/dom-queryx.y.z说明 aria-hidden 与 dom-query 保持严格同步发版二者是稳定的组合dom-query 提供 DOM 查询与findControlledElementsaria-hidden 只负责隐藏这一件事。此外 0.71.0 起不再随包发布src文件构建链经历了 vite0.10.3、tsup0.11.0、ESM typemodule0.1.3、es2019 编译目标0.2.0与显式exports字段0.2.1的演进最终沉淀为 package.json 中main: src/index.ts、files: [dist]的形态。测试佐证行为契约一览aria-hidden.test.ts 用 jsdom 环境覆盖了完整的行为契约关键场景可对照理解场景验证点基础隐藏与恢复hideOthers后仅目标所在 portal 可见撤销后 DOM 无残留属性嵌套目标后打开的嵌套目标保持可见中间目标关闭后恢复上一层乱序释放先释放底层锁也不影响顶层可见性重复释放同一锁被忽略手动预置属性锁前已有的aria-hidden不会被撤销uncontrolled 语义aria-expanded 联动控制器展开时被控弹层自动揭示收起时再次隐藏隐藏内容中的控制器位于隐藏区域的aria-controls触发器不会错误揭示弹层仅顶层锁响应控制器中间锁被暂停时其 MutationObserver 已断开不会误触发目标脱离文档目标被移除后调用不产生任何副作用也不打断既有锁iframe 隔离不同 document 的锁相互独立inert 语义保留既有inertfalse/inert属性值嵌套锁下依然正确Shadow DOMShadow root 内的目标以其 host 参与隐藏判定在 Zag 中的使用位置从源码结构看zag-js/aria-hidden被 Zag 的机器层组件如 Dialog、Drawer、Menu、Select 等通过各自机器的open/close动作调用打开时以弹层容器为目标执行hideOthers或按能力选择suppressOthers关闭时调用返回的撤销函数。结合 1.25.0 与 1.46.0 的修复Zag 得以支持模态对话框内打开 Select/Popover嵌套 Dialog以及 Solid Portal 先挂载后打开等复杂场景这正是 Zag 以状态机驱动组件所强调的可组合性的底层保障。如果你需要进一步深挖推荐按以下顺序阅读仓库内相关文件packages/utilities/aria-hidden/src/aria-hidden.ts三个能力入口函数packages/utilities/aria-hidden/src/index.tsariaHidden组合入口与defer语义packages/utilities/aria-hidden/src/walk-tree-outside.ts树遍历、计数、锁栈与 MutationObserver 核心实现packages/utilities/aria-hidden/tests/aria-hidden.test.ts完整行为契约packages/utilities/dom-query/src/controller.tsfindControlledElements对aria-controls的解析规则。赞分享前端UI组件【免费下载链接】zagBuild your design system in React, Solid, Vue, Svelte or Vanilla. Powered by finite state machines项目地址https://gitcode.com/gh_mirrors/za/zag点击查看免费下载相关推荐zag-js/aria-hidden 无障碍隔离原理与实战让模态组件正确隐藏背景内容zag js/aria hidden 无障碍隔离原理与实战让模态组件正确隐藏背景内容 zag js/aria hidden 是 Zag 组件库中负责从屏前端UI组件Zag zag-js/aria-hidden 锁栈机制详解嵌套模态对话框为何曾被屏幕阅读器隐藏Zag zag js/aria hidden 锁栈机制详解嵌套模态对话框为何曾被屏幕阅读器隐藏 本文基于 zag 仓库中 zag js/aria hi前端UI组件Radix Primitives 中的 radix-ui/react-visually-hidden实现原理、无障碍实践与 1.2.x 版本演进解读Radix Primitives 中的 radix ui/react visually hidden实现原理、无障碍实践与 1.2.x 版本演进解读 本文以前端UI组件上一篇小红书内容批量下载 3 步上手用 XHS-Downloader 一次性搞定图文与视频采集下一篇MTK刷机工具终极指南一台变砖的联发科手机如何用一条命令满血复活创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考