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

在Next.js中如何通过ARIA让屏幕阅读器仅聚焦Overlay弹窗

屏幕阅读器仍读取主区域内容的解决方案

你已经给主区域设置了aria-hidden="true",弹窗也配置了role="dialog"和aria-modal="true",但屏幕阅读器仍读取主内容,以下是关键问题的排查和修复方案:

1. 修复useEffect闭包导致的aria-hidden重置失败

你的useEffect清理函数存在闭包问题——清理时无法获取最新的DOM元素引用,导致弹窗关闭后主区域的aria-hidden可能无法重置为false,或打开时设置不生效。

修改后的useEffect代码:

React.useEffect(() => {
  document.body.style.overflow = isRender ? 'hidden' : 'unset';
  
  const mainContent = document.getElementById('app');
  if (mainContent) {
    const ariaHiddenValue = isRender ? 'true' : 'false';
    mainContent.setAttribute('aria-hidden', ariaHiddenValue);
    console.log('Overlay isRender:', isRender, 'aria-hidden set to:', ariaHiddenValue);
  }

  return () => {
    document.body.style.overflow = 'unset';
    // 清理时重新获取主区域元素,避免闭包引用问题
    const mainContent = document.getElementById('app');
    if (mainContent) {
      mainContent.setAttribute('aria-hidden', 'false');
      console.log('Overlay cleanup: aria-hidden set to false');
    }
  };
}, [isRender]);

2. 补充弹窗的aria-labelledby关联元素

你的弹窗设置了aria-labelledby="preview-dialog-title",但DOM中不存在对应id的元素,这会导致屏幕阅读器无法正确识别弹窗语义结构,影响模态行为生效。

在弹窗内添加对应标题元素(可设为仅屏幕可见):

<div 
  className={previewOverlay} 
  role="dialog" 
  aria-modal="true"
  aria-labelledby="preview-dialog-title"
>
  {/* 添加屏幕阅读器可见的标题 */}
  <h2 id="preview-dialog-title" className="sr-only">媒体预览确认</h2>
  <AppHeader config={headerConfig} />

  {/* 原有内容 */}
</div>

3. 添加模态框焦点管理

模态框打开后必须将焦点移至弹窗内部,关闭后恢复原焦点,这是确保屏幕阅读器聚焦弹窗内容的核心步骤。

在Overlay组件中添加焦点管理逻辑:

const Overlay: React.FC<Props> = React.memo(
  ({
    render = true,
    className = '',
    portalId = 'overlay',
    children,
    ...restHtmlAttributes
  }: Props) => {
    const hasTransitionedIn = useMountTransition(render, 300);
    const isRender = render || hasTransitionedIn;
    const animationClassName = `${hasTransitionedIn ? 'transitioned-in' : ''} ${
      render ? 'visible' : ''
    }`;

    // 新增:保存模态框和之前的焦点元素引用
    const modalRef = React.useRef<HTMLDivElement>(null);
    const prevFocusedElement = React.useRef<HTMLElement | null>(null);

    // 新增:焦点管理逻辑
    React.useEffect(() => {
      if (isRender && modalRef.current) {
        // 记录打开前的焦点元素
        prevFocusedElement.current = document.activeElement as HTMLElement;
        // 聚焦弹窗内第一个可交互元素
        const firstFocusable = modalRef.current.querySelector(
          'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
        );
        if (firstFocusable) {
          (firstFocusable as HTMLElement).focus();
        }
      } else {
        // 关闭弹窗时恢复原焦点
        if (prevFocusedElement.current) {
          prevFocusedElement.current.focus();
          prevFocusedElement.current = null;
        }
      }
    }, [isRender]);

    // 原有useEffect...

    return ReactDOM.createPortal(
      <>
        {isRender && (
          <div
            ref={modalRef} // 绑定模态框ref
            className={`${overlayComponent} ${animationClassName} ${className} `}
            {...restHtmlAttributes}
          >
            {children}
          </div>
        )}
      </>,
      document.getElementById(portalId) as HTMLElement
    );
  }
);

4. 确认Portal容器的DOM层级

确保Portal的目标容器(id="overlay")不在主区域(id="app")内部,否则主区域设置aria-hidden="true"会将弹窗也隐藏,导致屏幕阅读器无法识别弹窗内容。正确的HTML结构应该是:

<body>
  <div id="app"><!-- 主内容 --></div>
  <div id="overlay"></div><!-- Portal容器,与app同级 -->
</body>

5. 验证aria-hidden的生效时机

通过控制台的console.log输出,确认弹窗打开时mainContent的aria-hidden确实被设置为true,如果设置时机滞后于弹窗渲染,可能导致屏幕阅读器提前读取主内容。

内容的提问来源于stack exchange,提问作者Shiba Y.

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.12 09:24:51