You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Tooltip显示时如何调整页面焦点顺序,使其与Trigger同级?

解决React-Popper Tooltip焦点序列混乱问题

问题背景

基于React-Popper实现的Tooltip挂载在DOM根层级,而非Trigger元素的子节点,导致Tooltip的可聚焦内容默认处于页面焦点队列的末尾。即便显示时强制聚焦Tooltip内第一个按钮,也会跳过Trigger之后的其他页面可聚焦元素,破坏正常的焦点流转逻辑。

解决方案

1. 绑定无障碍关联属性

给Trigger按钮添加aria-controls(指向Tooltip的ID)和aria-expanded(绑定Tooltip的显示状态),让浏览器识别Trigger与Tooltip的关联关系,同时符合无障碍规范:

<button
  ref={triggerRef}
  aria-controls="custom-tooltip"
  aria-expanded={isTooltipOpen}
  onFocus={() => setIsTooltipOpen(true)}
  onBlur={() => setIsTooltipOpen(false)}
>
  触发Tooltip
</button>

2. 手动管控焦点流转逻辑

步骤1:记录Trigger的下一个可聚焦元素

在Tooltip展开时,遍历页面所有可聚焦元素,找到Trigger的位置并记录其下一个元素:

const triggerRef = useRef(null);
const nextFocusableElRef = useRef(null);
const tooltipFirstBtnRef = useRef(null);

const getNextFocusable = (currentEl) => {
  const allFocusable = Array.from(
    document.querySelectorAll(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    )
  );
  const currentIdx = allFocusable.indexOf(currentEl);
  return allFocusable[currentIdx + 1] || null;
};

// Tooltip打开时执行
useEffect(() => {
  if (isTooltipOpen && triggerRef.current) {
    nextFocusableElRef.current = getNextFocusable(triggerRef.current);
    tooltipFirstBtnRef.current?.focus();
  }
}, [isTooltipOpen]);
步骤2:拦截键盘事件调整焦点

在Tooltip容器上监听keydown事件,处理Tab/Shift+Tab的焦点跳转:

const handleTooltipKeyDown = (e) => {
  const tooltipFocusable = Array.from(
    e.currentTarget.querySelectorAll(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    )
  );
  const firstEl = tooltipFocusable[0];
  const lastEl = tooltipFocusable[tooltipFocusable.length - 1];

  // 最后一个元素按Tab:跳转到Trigger的下一个可聚焦元素
  if (e.key === 'Tab' && !e.shiftKey && e.target === lastEl) {
    e.preventDefault();
    nextFocusableElRef.current?.focus();
  }

  // 第一个元素按Shift+Tab:回到Trigger
  if (e.key === 'Tab' && e.shiftKey && e.target === firstEl) {
    e.preventDefault();
    triggerRef.current?.focus();
  }
};

// Tooltip容器组件
<div
  id="custom-tooltip"
  ref={tooltipRef}
  onKeyDown={handleTooltipKeyDown}
>
  <button ref={tooltipFirstBtnRef}>操作1</button>
  <button>操作2</button>
  <button onClick={() => setIsTooltipOpen(false)}>关闭</button>
</div>

3. 关闭Tooltip时重置焦点

当Tooltip关闭(点击关闭按钮、Trigger失焦),将焦点还原到Trigger,保证交互连贯性:

useEffect(() => {
  if (!isTooltipOpen && triggerRef.current) {
    triggerRef.current.focus();
  }
}, [isTooltipOpen]);

核心逻辑

通过手动拦截Tab键盘事件,强制覆盖浏览器默认的焦点序列,模拟出Tooltip与Trigger处于同一DOM层级的焦点流转效果:Trigger → Tooltip内元素 → Trigger的下一个页面元素,反向Shift+Tab则按原路径回溯。

内容的提问来源于stack exchange,提问作者Helena Sánchez

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 01:37:37