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
相关产品推荐
相关产品推荐

