模态框打开后如何让内部按钮优先获得Tab键聚焦
问题场景
存在一个优惠条目列表,每个条目配套删除按钮,点击删除按钮会弹出包含「取消」「删除」两个操作按钮的模态框。
需求与现存问题
- 预期效果:模态框打开后,用户按下Tab键时焦点需首先定位到模态框内的操作按钮,且焦点仅在模态框范围内循环
- 当前问题:按下Tab键后焦点会继续遍历当前行删除按钮之后的其他可Tab聚焦元素,需要完成整轮页面焦点遍历后才会定位到模态框内的按钮
现有实现代码
<section tabIndex="-1" role="dialog" onClick={onCancel}> <section role="document" onClick={(e) => e.stopPropagation()}> <Button tabIndex="0" secondary className={Styles.cancel} onClick={onCancel}> {t("abort")} </Button> <Button tabIndex="0" secondary className={Styles.delete} onClick={onDelete}> {t("deleteModal.CTA")} </Button> </section> </section>
实现方案
问题根源是模态框打开时没有主动接管焦点,也没有做焦点范围限制,焦点仍停留在触发弹窗的列表删除按钮上,Tab遍历自然会按页面DOM顺序继续向后走。按以下步骤修改即可实现效果:
- 模态框打开瞬间主动移入焦点
模态框挂载/显示的回调中,直接获取模态框内第一个可聚焦元素(即取消按钮)的DOM实例,调用focus()方法主动将焦点移入模态框,无需等Tab自然遍历。 - 实现焦点陷阱,限制焦点仅在模态框内循环
给模态框根节点绑定keydown事件监听,拦截Tab按键逻辑:- 正向按Tab时,如果当前焦点在模态框最后一个可聚焦元素(删除按钮)上,阻止默认跳转行为,手动把焦点移回模态框第一个可聚焦元素
- 反向按Shift+Tab时,如果当前焦点在模态框第一个可聚焦元素(取消按钮)上,阻止默认跳转行为,手动把焦点移到模态框最后一个可聚焦元素
- 补充无障碍语义属性
给模态框根节点添加aria-modal="true"属性,告知辅助技术当前模态框为最高优先级交互层,配合焦点控制逻辑兼容读屏设备。
修正后参考代码(React实现)
import { useEffect } from 'react'; // 组件内逻辑 useEffect(() => { if (!isModalOpen) return; // 打开弹窗时主动聚焦第一个按钮 const cancelBtn = document.querySelector(`.${Styles.cancel}`); cancelBtn?.focus(); const handleKeyDown = (e) => { if (e.key !== 'Tab') return; const focusableInModal = [ document.querySelector(`.${Styles.cancel}`), document.querySelector(`.${Styles.delete}`) ].filter(Boolean); const firstFocusEl = focusableInModal[0]; const lastFocusEl = focusableInModal.at(-1); // Shift+Tab 反向遍历 if (e.shiftKey) { if (document.activeElement === firstFocusEl) { e.preventDefault(); lastFocusEl.focus(); } return; } // 普通Tab 正向遍历 if (document.activeElement === lastFocusEl) { e.preventDefault(); firstFocusEl.focus(); } }; document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); }, [isModalOpen]); // 模态框DOM结构 <section tabIndex="-1" role="dialog" aria-modal="true" onClick={onCancel}> <section role="document" onClick={(e) => e.stopPropagation()}> <Button tabIndex="0" secondary className={Styles.cancel} onClick={onCancel}> {t("abort")} </Button> <Button tabIndex="0" secondary className={Styles.delete} onClick={onDelete}> {t("deleteModal.CTA")} </Button> </section> </section>
内容的提问来源于stack exchange,提问作者DBi
相关产品推荐
相关产品推荐

