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

如何使用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.30 23:03:54