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

React抽屉组件焦点限定:现有实现外的替代方案问询

React抽屉焦点限定的替代实现方案

你当前的实现已经很好地覆盖了抽屉焦点管理的核心需求:打开时聚焦抽屉内元素、Tab循环锁定在抽屉区域、ESC关闭并返回触发按钮焦点。下面提供几种可行的替代实现方案:

1. 利用原生HTML <dialog> 元素

原生<dialog>元素内置了焦点锁定和模态行为,无需手动编写Tab循环逻辑,React中可以直接使用:

import { useState, useRef } from "react";
import { AiOutlineClose } from "react-icons/ai";
import { data, buttonPositions } from "./data";

export default function Drawer() {
  const [position, setPosition] = useState("");
  const dialogRef = useRef(null);
  const btnRef = useRef(null);

  const handleOpen = (event, pos) => {
    btnRef.current = event.target;
    setPosition(pos);
    dialogRef.current.showModal();
  };

  const handleClose = () => {
    dialogRef.current.close();
    btnRef.current?.focus();
  };

  return (
    <>
      {buttonPositions.map((pos) => (
        <button
          key={pos}
          className={pos}
          onClick={(e) => handleOpen(e, pos)}
          aria-label={`Open ${pos} Drawer`}
        >
          Open {pos} Drawer
        </button>
      ))}
      <dialog
        ref={dialogRef}
        className={`drawer ${position}`}
        onClose={handleClose}
      >
        <div className="drawer-header">
          <h2>Drawer Content</h2>
          <button className="close-icon" onClick={handleClose} aria-label="Close Drawer">
            <AiOutlineClose />
          </button>
        </div>
        <div className="drawer-icons">
          {data.map((item, index) => (
            <a href="#" key={index}>
              {item.icon} <span>{item.linkName}</span>
            </a>
          ))}
        </div>
      </dialog>
    </>
  );
}

说明:

  • showModal()会自动将焦点锁定在dialog内,Tab循环默认生效
  • 按ESC键会自动触发close事件,无需手动监听
  • 需配合CSS调整dialog的位置(模拟左/右/上/下抽屉样式)

2. 使用无障碍UI组件库

成熟的UI库已经封装了完善的焦点管理和无障碍支持,无需手动实现:

示例:react-aria 的 Drawer 组件

import { useState } from "react";
import { Drawer, DrawerTrigger, DrawerContent, DrawerCloseButton } from "@react-aria/drawer";
import { data, buttonPositions } from "./data";

export default function App() {
  return (
    <>
      {buttonPositions.map((position) => (
        <Drawer key={position}>
          <DrawerTrigger>
            <button className={position}>Open {position} Drawer</button>
          </DrawerTrigger>
          <DrawerContent placement={position}>
            <DrawerCloseButton aria-label="Close Drawer" />
            <h2>Drawer Content</h2>
            <div className="drawer-icons">
              {data.map((item, index) => (
                <a href="#" key={index}>
                  {item.icon} <span>{item.linkName}</span>
                </a>
              ))}
            </div>
          </DrawerContent>
        </Drawer>
      ))}
    </>
  );
}

说明:

  • react-aria自动处理焦点锁定、Tab循环、ESC关闭等行为
  • 内置无障碍属性(如aria-modal、aria-labelledby)
  • 支持自定义抽屉位置(左/右/上/下)

3. 封装自定义焦点捕获Hook

将焦点管理逻辑抽离成可复用的Hook,简化组件代码:

// useFocusTrap.js
import { useEffect, useRef } from "react";

export function useFocusTrap(isActive) {
  const trapRef = useRef(null);

  useEffect(() => {
    if (!isActive || !trapRef.current) return;

    const focusableElements = trapRef.current.querySelectorAll(
      'a[href]:not([disabled]), button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"]):not([disabled])'
    );
    const firstElement = focusableElements[0];
    const lastElement = focusableElements[focusableElements.length - 1];

    const handleKeyDown = (e) => {
      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();
        }
      } else if (e.key === "Escape") {
        const closeEvent = new CustomEvent("escapepress");
        trapRef.current.dispatchEvent(closeEvent);
      }
    };

    trapRef.current.addEventListener("keydown", handleKeyDown);
    firstElement?.focus();

    return () => {
      trapRef.current.removeEventListener("keydown", handleKeyDown);
    };
  }, [isActive]);

  return trapRef;
}

在抽屉组件中使用:

import { useState, useRef } from "react";
import { AiOutlineClose } from "react-icons/ai";
import { data, buttonPositions } from "./data";
import { useFocusTrap } from "./useFocusTrap";

export default function Drawer() {
  const [isOpen, setIsOpen] = useState(false);
  const [position, setPosition] = useState("");
  const btnRef = useRef(null);
  const trapRef = useFocusTrap(isOpen);

  const handleOpen = (event, pos) => {
    btnRef.current = event.target;
    setIsOpen(true);
    setPosition(pos);
  };

  const handleClose = () => {
    setIsOpen(false);
    btnRef.current?.focus();
  };

  return (
    <>
      {buttonPositions.map((pos) => (
        <button
          key={pos}
          className={pos}
          onClick={(e) => handleOpen(e, pos)}
          aria-label={`Open ${pos} Drawer`}
        >
          Open {pos} Drawer
        </button>
      ))}
      {isOpen && (
        <div
          ref={trapRef}
          className={`${position === 'left' || position === 'right' ? 'drawerX' : 'drawerY'} ${position}`}
          aria-modal="true"
          aria-labelledby="drawer-header"
          onEscapepress={handleClose}
        >
          <div className="drawer-header" id="drawer-header">
            <h2>Drawer Content</h2>
            <button className="close-icon" onClick={handleClose} aria-label="Close Drawer">
              <AiOutlineClose />
            </button>
          </div>
          <div className="drawer-icons">
            {data.map((item, index) => (
              <a href="#" key={index}>
                {item.icon} <span>{item.linkName}</span>
              </a>
            ))}
          </div>
        </div>
      )}
    </>
  );
}

4. 优化现有实现的细节

你的当前实现可以补充以下细节,提升体验:

  • 处理Shift+Tab反向循环:当焦点在第一个元素时,按Shift+Tab跳转到最后一个元素
  • 优化焦点元素选择器:排除disabled状态的元素,避免无效焦点
  • 添加aria-modal="true"属性:明确告知屏幕阅读器这是模态组件

内容的提问来源于stack exchange,提问作者Sanat Gupta

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.23 06:15:01