如何使用JavaScript在React中实现模态框Tab键焦点陷阱
问题原因
现有实现无法生效是几个核心逻辑错误导致的:
- 事件绑定逻辑写在
window.onload中:React单页应用下window.onload仅在页面首次加载时触发一次,模态框是点击后动态挂载的,触发时DOM节点还未渲染,根本拿不到目标元素,事件绑定完全没执行。 - 焦点陷阱逻辑不完整:仅处理了末尾元素按Tab跳转到开头的场景,没有处理开头元素按
Shift+Tab反向跳转回末尾的逻辑,无法形成闭环。 - 首焦点元素设置错误:你把设置了
tabIndex="-1"的modal容器作为第一个聚焦节点,设置了该属性的元素本身无法被Tab遍历聚焦,根本不会进入焦点序列。 - 事件判断逻辑缺失:现有代码只要检测到Tab按键就强制跳转到第一个元素,没有判断当前焦点是否真的在最后一个元素上,哪怕焦点在模态框中间按Tab也会被强制跳走,完全不符合交互预期。
- 没有事件清理逻辑:直接原生绑定事件没有在组件卸载时移除,会造成内存泄漏和重复触发问题。
修复实现
直接替换Modal组件代码即可,完全兼容你现有传入的props:
import React, { useEffect, useRef } from 'react' import E from './Modal.style' // 标准可聚焦元素选择器 const FOCUSABLE_SELECTOR = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'; const Modal = (props) => { const { children, modalHeader, modalText, left, onClose } = props const modalRef = useRef(null); // 记录触发模态框打开的元素,关闭时返还焦点 const lastActiveElement = useRef(null); useEffect(() => { if (!modalRef.current) return; // 模态框挂载时保存当前聚焦元素,将焦点移入模态框 lastActiveElement.current = document.activeElement; const focusableElements = modalRef.current.querySelectorAll(FOCUSABLE_SELECTOR); const firstElement = focusableElements[0]; const lastElement = focusableElements[focusableElements.length - 1]; // 默认聚焦第一个可操作元素(关闭按钮) firstElement?.focus(); const handleKeyDown = (e) => { if (e.key !== 'Tab') return; const activeElement = document.activeElement; // 处理Shift+Tab反向遍历 if (e.shiftKey) { if (activeElement === firstElement) { e.preventDefault(); lastElement.focus(); } return; } // 处理正向Tab遍历 if (activeElement === lastElement) { e.preventDefault(); firstElement.focus(); } } document.addEventListener('keydown', handleKeyDown); return () => { // 组件卸载时移除事件,返还焦点 document.removeEventListener('keydown', handleKeyDown); lastActiveElement.current?.focus(); onClose?.(); } }, []); return ( <E.Root left={left}> <div className="modal" ref={modalRef}> <button type="button" className="xButton">X</button> {modalHeader && <h3>{modalHeader}</h3>} {modalText && <span className="modalText">{modalText}</span>} <E.ButtonWrapper> {children} </E.ButtonWrapper> </div> </E.Root> ) }; export default Modal;
逻辑说明
- 放弃原生
getElementById的DOM查找方式,用ReactuseRef获取模态框根节点,对齐React渲染生命周期,不会出现拿不到DOM的问题。 - 自动查询模态框内所有可聚焦元素,动态获取第一个和最后一个节点,不管children传入多少个按钮都能自动适配,不需要硬编码ID。
- 同时处理正向Tab和反向Shift+Tab的焦点跳转,在首尾节点形成闭环,焦点完全不会跑出模态框。
- 模态框打开时自动把焦点移入框内,关闭时自动把焦点还给触发模态框打开的按钮,符合WAI-ARIA无障碍规范要求。
- 事件绑定写在
useEffect中,组件卸载时自动移除事件监听,不会造成内存泄漏。 - 移除了原有代码中无效的空
onFocus绑定、多余的ID硬编码,适配动态传入的按钮内容。
内容的提问来源于stack exchange,提问作者user16106044
相关产品推荐
相关产品推荐

