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
相关产品推荐
相关产品推荐

