
本文介绍在 dom 根节点动态渲染 tooltip 时,如何通过程序化控制焦点流,使 tooltip 内容与触发元素在 tab 键导航中逻辑相邻,避免跳过页面其他可聚焦元素,从而满足 wcag 2.1 可访问性标准。
本文介绍在 dom 根节点动态渲染 tooltip 时,如何通过程序化控制焦点流,使 tooltip 内容与触发元素在 tab 键导航中逻辑相邻,避免跳过页面其他可聚焦元素,从而满足 wcag 2.1 可访问性标准。
在构建自定义 Tooltip/Popover 组件时,一个常见但关键的可访问性挑战是:当 Tooltip 渲染在 <body> 或 React Root 节点下(而非嵌套在触发器附近),其内部可聚焦元素(如“关闭”按钮、操作链接)会脱离原始 DOM 顺序,导致键盘用户按 Tab 键时——焦点从触发器直接跳至页面末尾的 Tooltip,再跳至文档中下一个自然可聚焦元素,中间跳过大量中间控件,严重破坏导航连续性与屏幕阅读器体验。
根本解法不是强行将 Tooltip 插入触发器 DOM 子树(这常与 Popper 定位逻辑冲突),而是主动接管焦点流转逻辑:当用户离开 Tooltip 时,不任由浏览器按 DOM 顺序选择下一个焦点,而是精准定位到“视觉与语义上紧邻触发器之后”的下一个可聚焦元素,并手动聚焦它。
✅ 正确实现步骤
- 为触发器和 Tooltip 分别绑定 ref:确保能准确获取触发器 DOM 节点(triggerRef)及 Tooltip 容器(tooltipRef);
- 监听 Tooltip 的 blur 事件(推荐委托至 Tooltip 容器,配合 event.relatedTarget 判断是否真正失焦);
- 计算“下一个逻辑焦点”:调用 findNextFocusableElement(triggerRef.current),该函数需遍历 DOM,从触发器节点开始,按标准 Tab 顺序查找下一个可聚焦元素(考虑 tabindex >= 0、<a href>、<button>、<input> 等);
- 延迟聚焦 + 隐藏 Tooltip:使用 setTimeout(..., 0) 或 requestAnimationFrame 确保 DOM 更新完成后再聚焦,避免因 Tooltip 移除导致聚焦失败:
const handleTooltipBlur = () => {
if (!triggerRef.current) return;
const nextEl = findNextFocusableElement(triggerRef.current);
if (nextEl) {
setTooltipVisible(false); // 先更新状态,触发卸载
requestAnimationFrame(() => {
nextEl.focus({ preventScroll: true });
});
}
};
// Tooltip 容器需绑定 onBlur
<div
ref={tooltipRef}
onBlur={handleTooltipBlur}
tabIndex={-1} // 确保自身可接收 blur 事件
>
<button onClick={close}>Close</button>
<a href="#">Action</a>
</div>⚠️ 关键注意事项
- tabIndex="-1" 不可省略:Tooltip 容器本身需设 tabIndex="-1" 才能捕获 blur 事件,否则失焦行为不可靠;
- 避免 setTimeout(..., 200) 硬编码:示例中的 200ms 是临时规避 React 状态更新时机问题,生产环境应优先用 requestAnimationFrame 或 useEffect cleanup 保证同步;
-
findNextFocusableElement 实现要点:
- 使用 document.querySelectorAll('button, [href], input:not([type="hidden"]), select, textarea, [tabindex]:not([tabindex="-1"])') 获取所有候选;
- 按 Array.from(...).sort((a, b) => a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING) 排序,确保 DOM 顺序;
- 过滤掉 disabled、hidden、display: none 或 visibility: hidden 元素;
- 键盘导航闭环:还需处理 Escape 关闭 Tooltip 时的焦点返回(应聚焦回触发器),以及首次打开时自动聚焦 Tooltip 内首个可操作元素(如 Close 按钮);
- 无障碍属性补充:为 Tooltip 添加 role="dialog"、aria-modal="true"、aria-labelledby(指向触发器 ID)和 aria-describedby(指向内容 ID),并管理 aria-hidden 对页面其余部分的遮罩。
✅ 总结
管理浮动 Tooltip 的焦点顺序,本质是用 JavaScript 补全浏览器原生 Tab 顺序的语义断层。核心不在于“把 Tooltip 塞进触发器”,而在于“当焦点离开 Tooltip 时,智能地告诉浏览器:‘接下来该聚焦这里’”。这一模式不仅适用于 Tooltip,同样适用于 Modal、Combobox 下拉、Side Navigation 等所有脱离 DOM 原始流的浮层组件。遵循此方案,即可在保持 Popper 定位灵活性的同时,交付符合 WCAG AA 级标准的健壮可访问性体验。

















