尧图精选

Lexical 无障碍辅助包 @lexical/a11y 完全指南:ARIA Live Region、焦点陷阱与 Roving Tab Index

🕒 发布时间:2026/9/12 18:02:00 📁 来源:尧图网络
Lexical 无障碍辅助包 lexical/a11y 完全指南ARIA Live Region、焦点陷阱与 Roving Tab Index【免费下载链接】lexicalLexical is an extensible text editor framework that provides excellent reliability, accessibility and performance.项目地址: https://gitcode.com/GitHub_Trending/le/lexical导读lexical/a11y是 Lexical 生态中专攻无障碍Accessibility的框架无关辅助包为编辑器提供四类核心能力ARIA live region 状态播报、焦点陷阱focus trap、Roving Tab Index 导航与编辑器—工具栏焦点管理。这些能力以 Extension 形式与 Lexical 的扩展系统深度集成既可以从 React 中通过lexical/react提供的 Hook 一行接入也可以在 Svelte、Vue、Solid 或原生 DOM 中直接使用底层原语。读完本文你将掌握每个辅助能力的配置项、底层实现原理、React 接入方式与 Shadow DOM 兼容性并能直接在自己的编辑器中落地键盘可访问性方案。包概览框架无关的无障碍原语层lexical/a11y的定位在包描述中写得非常明确Framework-agnostic accessibility helpers框架无关的无障碍辅助工具。它本身不依赖任何 UI 框架只依赖lexical核心与lexical/extension扩展基础设施见 package.jsonlexical编辑器核心提供命令注册、DOM 工具等lexical/extensionExtension 定义、信号signal与依赖注入机制lexical/utils通用工具。包内对外导出的全部内容集中在 src/index.ts包括类别导出符号职责播报类AriaLiveRegionExtension为编辑器持有单个aria-live区域暴露稳定的announce输出播报类HistoryAnnounceExtension监听撤销/重做命令并通过共享 sink 播报播报类EditorModeAnnounceExtension监听setEditable状态切换并播报焦点类FocusTrapExtension将 Tab / ShiftTab 循环限制在容器内卸载时恢复焦点焦点类RovingTabIndexExtension实现 WAI-ARIA roving-tabindex 工具栏模式焦点类FocusManagerExtension实现编辑器与工具栏之间的 AltF10 / Escape 焦点跳转配套的类型导出包括AriaPoliteness、FocusTrapInitialFocus、FocusTrapOptions、RovingOrientation、RovingTabIndexOptions、FocusManagerOptions、ContainerRegistry等。包的版本为0.50.0与仓库当前主分支对齐见 package.json。架构基础Extension 信号 依赖注入所有 a11y 辅助能力都以defineExtension定义理解这一机制是正确使用的前提。Extension 的生命周期与输出以AriaLiveRegionExtension为例src/index.ts一个 Extension 由四部分组成name唯一标识如lexical/a11y/AriaLiveRegionconfig默认配置如{politeness: polite, owner: null}build(editor, config, state)在编辑器可用时构造对外输出的对象register(editor, config, state)挂载副作用事件监听、effect返回清理函数。运行时调优依赖信号机制配置值通过namedSignals(config)暴露为可写信号调用方可以在运行期直接改写例如把politeness.value从polite改为assertive已挂载的 region 会立即更新aria-live属性。依赖注入Extension 之间的协作播报类扩展之间存在明确的依赖链AriaLiveRegionExtension ← HistoryAnnounceExtension ← EditorModeAnnounceExtension后两者在dependencies中声明[AriaLiveRegionExtension]并通过state.getDependency(...)拿到共享的announcesinksrc/index.ts。这种设计保证整个编辑器只有一个 live region避免多个播报器各自创建 DOM 节点造成屏幕阅读器重复朗读。引用计数的容器注册焦点类扩展FocusTrapExtension、RovingTabIndexExtension、FocusManagerExtension共用同一种输出形态ContainerRegistry——一个以HTMLElement为键的引用计数注册表createRefCountedRegistry。调用方通过output.register(container, options)注册容器并拿到幂等的 disposer同一个容器被注册多次时只有最后一次 dispose 才会真正拆除监听见 FocusTrapExtension.test.ts 的引用计数测试。ARIA Live Region为编辑器建立状态播报通道创建视觉隐藏的 status 区域createLiveRegionsrc/index.ts在宿主元素下创建一个div并设置三个关键属性rolestatusWAI-ARIA status message 模式对应 WCAG 4.1.3aria-atomictrue播报整个区域文本而非增量视觉隐藏样式clip: rect(0 0 0 0)、position: absolute、1px尺寸等保证肉眼不可见但对屏幕阅读器可见。配置项与运行时信号AriaLiveRegionExtensionConfigsrc/index.ts有两个配置配置默认值说明politenesspolitepolite或assertive。前者等待用户空闲时播报后者立即打断播报ownernull区域挂载的宿主元素null时回退到编辑器根元素所属文档的body。显式指定可将区域放到与编辑器同一无障碍子树如 Shadow Root 或 portaled 覆盖层两者都通过namedSignals暴露为信号运行时改动即时生效owner改动会触发区域重新挂载。announce 输出与去重策略Extension 的输出AriaLiveRegion包含稳定的announce(message)方法src/index.ts。它有一个精心设计的细节连续播报相同文本时自动追加一个零宽空格\u200B使textContent发生变化确保屏幕阅读器感知到同一句话被再次播报。此外消息通过私有信号缓冲register中的三个 effect 分别负责区域创建/销毁、politeness 同步与消息镜像三者解耦src/index.ts。两个行为契约值得注意均有单测佐证见 AriaLiveRegionExtension.test.ts区域未挂载时announce是 no-op不会缓冲编辑器尚无根元素时播报会被丢弃而不是在挂载后补播测试第 197-222 行重挂载不重放旧消息编辑器 root 卸载再挂载后新 region 从空文本开始避免屏幕阅读器在无用户操作时重复朗读测试第 156-195 行。两个派生播报器HistoryAnnounceExtension监听UNDO_COMMAND/REDO_COMMAND默认文案Undone/Redone可用undone、redone配置覆盖disabled信号为true时通过 effect 完全不注册命令监听零额外开销src/index.ts。EditorModeAnnounceExtension监听registerEditableListener默认文案Editor is editable/Editor is read-only。播报基于状态切换初始挂载是静默的即使把disabled从true切回false已处于只读态的编辑器也不会立刻重播当前状态src/index.ts。Focus Trap模态对话框的焦点闭环registerFocusTrapsrc/index.ts是焦点陷阱的底层实现它完成三件事激活时落点根据initialFocus选择聚焦第一个可聚焦后代或容器自身容器需tabindex -1通常设-1保持不在自然 Tab 序中但可编程聚焦全量 Tab 接管容器上的keydown处理器拦截每次 Tab / ShiftTab在当前可聚焦元素列表中循环移动。注释说明为什么不用边界循环方案——Safari 的默认 Tab 路由会让焦点闪到浏览器地址栏产生可见的焦点闪烁文档级 focusin 安全网任何逃逸到容器外的焦点都会被拉回第一个可聚焦元素用于恢复 Safari 等场景下被浏览器 chrome 截走的焦点。FocusTrapOptions选项类型说明initialFocusfirstFocusable \| container默认firstFocusablecontainer适合首个可聚焦控件是关闭按钮的对话框让用户先落在对话框主体上allowOutside(target) boolean豁免谓词。返回true时焦点可停留在容器外用于 portaled 自动补全面板、tooltip 等逻辑上属于对话框但 DOM 在外的元素使用限制源码注释明确提示同一时刻只能挂一个 trap两个活跃 trap 会安装互相竞争的 document 级focusin监听争夺焦点Escape 不拦截关闭键行为由宿主自行处理trap 只管焦点闭环卸载顺序有讲究dispose 时先移除 focusin 监听、再恢复焦点利用mergeRegister的 LIFO 逆序否则恢复焦点到容器外元素会再次触发陷阱src/index.ts。Roving Tab Index键盘可导航的工具栏registerRovingTabIndexsrc/index.ts实现 WAI-ARIA roving-tabindex 模式组内同一时刻只有一个元素携带tabindex0其余为-1Tab 将整个组当作一个焦点单元跳出方向键在组内移动焦点Home / End 跳到两端。RovingTabIndexOptions选项默认值说明orientationhorizontalhorizontal响应 ←/→vertical响应 ↑/↓both全部响应itemSelector:scope button:not([disabled])组内成员选择器默认匹配直接子级非禁用按钮可改为[data-roving-item]等自定义标记两个实现细节懒查询成员列表在每次按键交互时重新查询组内增删按钮无需额外接线测试 RovingTabIndexExtension.test.ts 验证 ArrowRight 移动焦点与 tabindex 交换Firefox 兜底当焦点落在容器自身如roletoolbar而非子项时任意方向键/Home/End 都会先进入组内第一个成员避免用户被困src/index.ts。方向键循环采用单次取模归一化从首项继续左移会回绕到末项。Focus Manager编辑器与工具栏的快捷键跳转registerFocusManagersrc/index.ts实现 WAI-ARIA APG editor menubar 模式的导航部分AltF10编辑器内按下时KEY_DOWN_COMMANDCOMMAND_PRIORITY_LOW聚焦工具栏中[tabindex0]的成员即当前活跃的 roving 项否则聚焦第一个可聚焦项Escape工具栏内按下时返回编辑器。实现先editor.focus()恢复此前选区保证后续工具栏命令作用于同一选区再rootElement.focus()作为测试环境的兜底同时stopPropagation防止事件冒泡到窗口级 Modal 关闭处理器。FocusManagerOptions仅一个可选配置toolbarItemSelector默认与 roving 模式共享同一作用域:scope button:not([disabled]), :scope [tabindex0]src/index.ts。React 接入lexical/react 的四个 Hook对于 React 用户lexical/react为上述原语提供了等价的 Hook 包装全部通过getExtensionDependencyFromEditor拿到 Extension 输出因此必须先把对应 Extension 加入编辑器的扩展树。Hook对应 Extension签名与用法useLexicalAriaLiveRegionAriaLiveRegionExtension返回稳定的announce(message)函数见 useLexicalAriaLiveRegion.tsuseLexicalFocusTrapRefFocusTrapExtensionuseLexicalFocusTrapRef(isActive, initialFocus?, allowOutside?)返回RefCallbackisActive为true且元素挂载时激活陷阱useLexicalRovingTabIndexRefRovingTabIndexExtensionuseLexicalRovingTabIndexRef({orientation, itemSelector})返回RefCallback挂到roletoolbar容器上useLexicalFocusManagerRefFocusManagerExtensionuseLexicalFocusManagerRef({toolbarItemSelector})返回RefCallback挂到工具栏上一个典型组合富文本编辑器 工具栏const trapRef useLexicalFocusTrapRef(isModalOpen, container); const rovingRef useLexicalRovingTabIndexRef(); const managerRef useLexicalFocusManagerRef(); return ( div ref{managerRef} roletoolbar div ref{rovingRef} button加粗/button button斜体/button /div /div {isModalOpen ( div ref{trapRef} roledialog tabIndex{-1} {/* 对话框内容 */} /div )} / );useLexicalFocusTrapRef的实现细节值得注意allowOutside谓词存放在 ref 中、在事件触发时才读取因此传入内联 lambda 不会改变RefCallback的身份、也不会每次渲染重建陷阱useLexicalFocusTrapRef.ts。所有 Hook 都支持多元素同时使用各自独立。Shadow DOM 与跨文档支持a11y 原语对 Shadow DOM 和跨 iframe 场景做了专门处理仓库为FocusTrapExtension、RovingTabIndexExtension、FocusManagerExtension各准备了*.shadow.test.ts测试见 packages/lexical-a11y/src/tests/unit 目录验证组合事件getComposedEventTarget与组合包含关系containsComposed沿host链向上追溯的正确性live region 的宿主文档解析遵循编辑器根元素所属文档原则若编辑器被 portaled 到 iframe 中region 会创建在 iframe 文档而非顶层文档内AriaLiveRegionExtension.test.ts若配置了owner则 region 固定挂在owner下不受编辑器 root 反复挂载影响测试第 224-251 行。如何安装与使用在 monorepo 中通过包管理器安装本仓库使用 pnpm workspacepnpm add lexical/a11y lexical/extension以扩展树方式装配到编辑器对应仓库内 lexical-extension 的扩展系统import { AriaLiveRegionExtension, EditorModeAnnounceExtension, FocusTrapExtension, HistoryAnnounceExtension, RovingTabIndexExtension, } from lexical/a11y; const editor buildEditorFromExtensions( // 播报链依赖 AriaLiveRegionExtension AriaLiveRegionExtension, HistoryAnnounceExtension, EditorModeAnnounceExtension, // 焦点类通过 output.register 按需挂载容器 FocusTrapExtension, RovingTabIndexExtension, // ...其他编辑器扩展 );小结lexical/a11y把编辑器无障碍中最容易做错的四件事——状态播报、模态焦点闭环、工具栏键盘导航、编辑器/工具栏焦点往返——封装成可组合的 Extension 原语底层是框架无关的纯 DOM 实现可在 React、Svelte、Vue、Solid 或原生 JS 中直接调用上层由lexical/react提供一行式 Hook。无论是追求 WCAG 合规的产品化编辑器还是需要自定义无障碍方案的嵌入式场景这套能力都给出了可直接复制、有测试背书单元测试见 packages/lexical-a11y/src/tests/unit、React 侧测试见 packages/lexical-react/src/tests/unit的实现路径。【免费下载链接】lexicalLexical is an extensible text editor framework that provides excellent reliability, accessibility and performance.项目地址: https://gitcode.com/GitHub_Trending/le/lexical创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
上一篇/下一篇内容由系统自动关联 返回资讯列表 →