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

React中使用Mantine打开模态框时如何禁用主内容交互?

解决Mantine模态框打开后主内容可交互的问题

问题分析

核心问题是模态框打开后,后方元素仍能通过键盘交互(如Tab键切换到后方输入框),仅靠overflow: hidden和pointer-events: none只能阻止鼠标操作与页面滚动,无法处理键盘焦点的跳转。

解决方案

通过焦点陷阱(Focus Trap)、ARIA属性配置和DOM状态管理三者结合,彻底阻断主内容的交互能力:

1. 启用Mantine Modal内置焦点陷阱

Mantine的Modal组件自带焦点陷阱功能,显式配置可确保焦点被限制在模态框内部循环,无法跳转到外部元素:

<Modal
  // 其他原有属性
  focusTrap={true} // 强制启用焦点陷阱
  returnFocus={true} // 关闭模态框后自动将焦点放回触发按钮
>

2. 模态框状态切换时管理主内容

添加useEffect监听模态框的打开/关闭状态,对主内容容器(或页面body)做以下处理:

  • 设置aria-hidden="true":告知屏幕阅读器主内容不可访问
  • 设置tabindex="-1":阻止主内容元素被Tab键选中
  • 保存并恢复打开前的焦点元素,避免关闭后焦点丢失

3. 强化事件阻断

配合原有CSS样式,在模态框打开时阻止主内容的键盘事件冒泡,进一步杜绝交互可能。

修改后的完整代码

import { Button, Divider, Flex, Group, Modal, TextInput, Box, Text, Image } from "@mantine/core";
import { SaveButton } from "@refinedev/mantine";

import { useDisclosure } from "@mantine/hooks";
import { useState, useEffect, useRef } from "react";
import { useTask } from "hooks/game/useTasks";

import { IOptions, ITaskItem as ITask } from "context/GameContext";

// 带标题的分割线组件
export const Line = (props: any) => <Divider w="500px" my="xs" label={`${props.title}`} labelPosition="center" />;

// 统一样式的输入框组件
export const Input = ({ field, handler }: any) => {
  return <TextInput mb={10} w={500} ta="center" variant="filled" value={field} onChange={handler} />;
};

const EditTaskModal = ({ ...task }: ITask) => {
  const [opened, { open, close }] = useDisclosure(false);
  const [title, setTitle] = useState(task.title);
  const [subtitle, setSubtitle] = useState(task.subtitle);
  const [texts, setTexts]: any = useState(task.texts);
  const [options, setOptions]: any = useState(task.options);
  const { update } = useTask();
  // 保存打开模态框前的焦点元素
  const prevFocusRef = useRef<HTMLElement | null>(null);

  useEffect(() => {
    if (!opened) {
      // 关闭时重置表单值
      setTitle(task.title);
      setSubtitle(task.subtitle);
      setTexts(task.texts);
      setOptions(task.options);
      // 恢复页面滚动
      document.body.style.overflow = "";
      // 恢复主内容的可访问状态
      const mainContent = document.getElementById("main-content"); // 需确保主内容容器有此ID
      if (mainContent) {
        mainContent.setAttribute("aria-hidden", "false");
        mainContent.removeAttribute("tabindex");
      }
      // 恢复之前的焦点
      if (prevFocusRef.current) {
        prevFocusRef.current.focus();
        prevFocusRef.current = null;
      }
      return;
    }

    // 打开时保存当前焦点元素
    prevFocusRef.current = document.activeElement as HTMLElement;
    // 锁定页面滚动
    document.body.style.overflow = "hidden";
    // 处理主内容容器
    const mainContent = document.getElementById("main-content");
    if (mainContent) {
      mainContent.setAttribute("aria-hidden", "true");
      mainContent.setAttribute("tabindex", "-1");
      // 阻止键盘事件冒泡到主内容
      const blockKeyboard = (e: KeyboardEvent) => e.stopPropagation();
      mainContent.addEventListener("keydown", blockKeyboard);
      return () => mainContent.removeEventListener("keydown", blockKeyboard);
    }
  }, [opened]);

  const handleSubmit = async (event: any) => {
    event.preventDefault();
    const materials: any = new FormData();

    title && materials.append("title", title);
    subtitle && materials.append("subtitle", subtitle);

    texts &&
      texts.length !== 0 &&
      texts.forEach((text: string, index: number) => {
        materials.append(`texts[${index}]`, text);
      });

    options &&
      options.length !== 0 &&
      options.forEach((option: any, index: number) => {
        materials.append(`options[${index}]`, option);
      });

    for (const [key, value] of materials.entries()) {
      console.log(key + ": " + value);
    }

    console.log(materials);
    await update(materials);
    close(); // 提交后关闭模态框
  };

  const handleFieldChange = (index: any, state: any, setState: any) => (event: any) => {
    let fields = [...state];
    fields[index] = event.target.value;
    setState(fields);
  };

  return (
    <>
      <Modal
        size="xl"
        radius="md"
        opened={opened}
        onClose={close}
        centered
        shadow="sm"
        overlayProps={{
          backgroundOpacity: 0.4,
          blur: 2
        }}
        closeOnClickOutside={false}
        focusTrap={true}
        returnFocus={true}
      >
        <Flex justify="center" align="center" direction="column" wrap="nowrap">
          <form onSubmit={handleSubmit}>
            {task.title && (
              <>
                <Line title="Title" />
                <TextInput
                  w={500}
                  ta="center"
                  variant="filled"
                  value={title}
                  onChange={(event) => setTitle(event.currentTarget.value)}
                />
              </>
            )}

            {task.subtitle && (
              <>
                <Line title="Subtitle" />
                <TextInput
                  w={500}
                  ta="center"
                  variant="filled"
                  value={subtitle}
                  onChange={(event) => setSubtitle(event.currentTarget.value)}
                />
              </>
            )}

            {task.texts && task.texts.length !== 0 && (
              <>
                <Line title="Content" />
                {texts.map((text: string, index: number) => {
                  return <Input field={text} handler={handleFieldChange(index, texts, setTexts)} />;
                })}
              </>
            )}

            {task.options && (
              <>
                <Line title="Content" />
                {options.map((option: IOptions, index: number) => {
                  return (
                    <Box key={index}> {/* 添加key属性修复React警告 */}
                      {option.emojy_index ? (
                        <Box style={{ display: "flex", alignItems: "center" }}>
                          <Text p={0}>{option.emojy_index}</Text>
                          <Image p={0} w={"48px"} src={`../emoji-${option.emojy_index}.png`} />
                        </Box>
                      ) : (
                        <>
                          {option.icon && <Image w={"24px"} src={option.icon} />}
                          <Input field={option.text} handler={handleFieldChange(index, options, setOptions)} />
                        </>
                      )}
                    </Box>
                  );
                })}
              </>
            )}

            <Group justify="center" mt="md">
              <SaveButton type="submit" />
            </Group>
          </form>
        </Flex>
      </Modal>
      <Button mt={10} variant="gradient" gradient={{ from: "violet", to: "blue", deg: 177 }} onClick={open}>
        Open update modal
      </Button>
    </>
  );
};

export default EditTaskModal;

关键修改说明

  • 显式启用focusTrap:强制焦点在模态框内部循环,彻底阻断外部元素的键盘访问
  • 焦点状态管理:保存并恢复打开前的焦点元素,符合无障碍设计规范
  • 主内容状态切换:通过ARIA属性和事件监听,从视觉和交互层面完全隔离主内容
  • 完善交互流程:表单提交后自动关闭模态框,添加循环元素的key属性修复React警告

内容的提问来源于stack exchange,提问作者Tkach Yevhenii

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.28 05:16:05