React开发中如何限制tab导航仅在弹出的drawer内部跳转
React 无障碍Drawer Tab焦点仅内部流转实现方案
这个需求本质是实现焦点陷阱(Focus Trap),是WCAG无障碍规范对模态类悬浮组件的标准要求,以下是两种可落地的实现方式:
方案1:使用成熟第三方库实现(推荐)
React生态下常用focus-trap-react库,已经处理了所有边界情况(比如动态生成的可聚焦元素、特殊按键处理等),无需自己兼容各种场景:
- 安装依赖:
npm install focus-trap-react - 组件内集成:
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展开状态截图如下:
内容的提问来源于stack exchange,提问作者Vindy Perera
相关产品推荐
相关产品推荐

