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

React开发中如何限制tab导航仅在弹出的drawer内部跳转

React 无障碍Drawer Tab焦点仅内部流转实现方案

这个需求本质是实现焦点陷阱(Focus Trap),是WCAG无障碍规范对模态类悬浮组件的标准要求,以下是两种可落地的实现方式:


方案1:使用成熟第三方库实现(推荐)

React生态下常用focus-trap-react库,已经处理了所有边界情况(比如动态生成的可聚焦元素、特殊按键处理等),无需自己兼容各种场景:

  1. 安装依赖:
    npm install focus-trap-react
  2. 组件内集成:
import FocusTrap from 'focus-trap-react'
import { useState, useRef } from 'react'

function YourPage() {
  const [isDrawerOpen, setIsDrawerOpen] = useState(false)
  // 记录触发打开Drawer的按钮,关闭后返回焦点
  const openTriggerRef = useRef(null)

  return (
    <div>
      <button 
        ref={openTriggerRef}
        onClick={() => setIsDrawerOpen(true)}
      >
        打开侧边抽屉
      </button>

      {/* Drawer 组件 */}
      {isDrawerOpen && (
        <FocusTrap 
          active={isDrawerOpen}
          focusTrapOptions={{
            // 关闭后自动把焦点返回给触发按钮
            returnFocusOnDeactivate: true,
            initialFocus: false // 如需自定义初始聚焦元素可修改该配置
          }}
        >
          <div 
            role="dialog"
            aria-modal="true"
            className="drawer-wrapper"
          >
            <button onClick={() => setIsDrawerOpen(false)}>关闭抽屉</button>
            {/* 抽屉内部其他内容:表单、按钮、链接等 */}
            <input type="text" placeholder="请输入内容" />
            <button>确认提交</button>
          </div>
        </FocusTrap>
      )}
    </div>
  )
}

方案2:手动实现(无额外依赖)

如果不想引入第三方包,可以自行实现核心逻辑,代码示例如下:

import { useState, useRef, useEffect } from 'react'

// 可聚焦元素选择器
const FOCUSABLE_SELECTOR = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'

function YourPage() {
  const [isDrawerOpen, setIsDrawerOpen] = useState(false)
  const drawerRef = useRef(null)
  const openTriggerRef = useRef(null)

  useEffect(() => {
    if (!isDrawerOpen || !drawerRef.current) return

    // 获取抽屉内所有可聚焦元素
    const focusableElements = drawerRef.current.querySelectorAll(FOCUSABLE_SELECTOR)
    const firstElement = focusableElements[0]
    const lastElement = focusableElements[focusableElements.length - 1]

    // 初始聚焦到第一个元素
    firstElement?.focus()

    const handleKeyDown = (e) => {
      // 处理ESC关闭抽屉
      if (e.key === 'Escape') {
        setIsDrawerOpen(false)
        return
      }
      // 处理Tab按键循环逻辑
      if (e.key === 'Tab') {
        if (e.shiftKey && document.activeElement === firstElement) {
          e.preventDefault()
          lastElement.focus()
        } else if (!e.shiftKey && document.activeElement === lastElement) {
          e.preventDefault()
          firstElement.focus()
        }
      }
    }

    document.addEventListener('keydown', handleKeyDown)
    return () => {
      document.removeEventListener('keydown', handleKeyDown)
      // 关闭后返回焦点给触发按钮
      openTriggerRef.current?.focus()
    }
  }, [isDrawerOpen])

  return (
    <div>
      <button 
        ref={openTriggerRef}
        onClick={() => setIsDrawerOpen(true)}
      >
        打开侧边抽屉
      </button>

      {isDrawerOpen && (
        <div 
          ref={drawerRef}
          role="dialog"
          aria-modal="true"
          className="drawer-wrapper"
        >
          <button onClick={() => setIsDrawerOpen(false)}>关闭抽屉</button>
          <input type="text" placeholder="请输入内容" />
          <button>确认提交</button>
        </div>
      )}
    </div>
  )
}

额外无障碍优化建议

  • Drawer展开时给页面主内容容器添加aria-hidden="true",避免屏幕阅读器读取到抽屉外的内容
  • 抽屉蒙层点击也要支持关闭抽屉
  • 如果你用的是组件库自带的Drawer(比如Ant Design、MUI),大部分已经内置了焦点陷阱功能,只需要开启对应的配置项即可

你附上的Drawer展开状态截图如下:
Drawer展开状态截图

内容的提问来源于stack exchange,提问作者Vindy Perera

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.02 01:06:03