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

MUI Popover的transformOrigin属性失效问题求助

MUI Popover 关闭后重新打开位置偏移问题

问题描述

我有一个点击后打开Popover的按钮,Popover内包含异步加载数据的表格。遇到了transformOrigin属性失效的异常:

  • 首次加载页面后,Popover显示位置完全符合预期
  • Popover处于打开状态时调整窗口大小,它会自动修正到正确位置
  • 但关闭Popover后再次打开,它会偏移到非预期位置,甚至可能因按钮所处位置导致超出屏幕范围

相关代码

按钮组件(ChooseRacesBtn.js)

import Button from "@mui/material/Button";
import Popover from "@mui/material/Popover";
import { useState } from "react";
import ChooseRacesPanel from "./ChooseRacesPanel";

export default function ChooseRacesBtn({ date, setIndex }) {
    const [anchorEl, setAnchorEl] = useState(null);

    const handleClick = (event) => {
        setAnchorEl(event.currentTarget);
    };

    const handleClose = () => {
        setAnchorEl(null);
    };

    const open = Boolean(anchorEl);

    return (
        <div>
            <Button sx={{ m: 2 }} variant="contained" onClick={handleClick}>
                Choose Races
            </Button>
            <Popover
                open={open}
                anchorEl={anchorEl}
                onClose={handleClose}
                anchorOrigin={{
                    vertical: "bottom",
                    horizontal: "right",
                }}
                transformOrigin={{
                    vertical: "top",
                    horizontal: "right",
                }}
            >
                <ChooseRacesPanel date={date} setIndex={setIndex} />
            </Popover>
        </div>
    );
}

表格组件(ChooseRacesPanel.js)

import Table from "@mui/material/Table";
import TableCell from "@mui/material/TableCell";
import TableBody from "@mui/material/TableBody";
import TableContainer from "@mui/material/TableContainer";
import TableRow from "@mui/material/TableRow";
import Paper from "@mui/material/Paper";
import { useState, useEffect } from "react";
import { NavLink } from "react-router-dom";

export default function ChooseRacesPanel({ date, setIndex }) {
    const [meetingNames, setMeetingNames] = useState([]);
    const [meetingsRaces, setMeetingsRaces] = useState([]);
    useEffect(() => {
        const fetchData = async () => {
            const response = await fetch(
                `http://localhost:3000/meeting/${date}`,
                {
                    method: "GET",
                    headers: { "Content-Type": "application/json" },
                }
            );
            const json = await response.json();
            console.log(json);
            setMeetingNames(json.meetingNames);
            setMeetingsRaces(json.meetingRaces);
        };
        fetchData();
    }, [date]);
    return (
        <TableContainer component={Paper}>
            <Table>
                <TableBody>
                    {meetingNames.map((mName, index) => (
                        <TableRow
                            key={index}
                            sx={{
                                "&:last-child td, &:last-child th": {
                                    border: 0,
                                },
                            }}
                        >
                            <TableCell align="center">{mName}</TableCell>
                            {meetingsRaces[index].map((race, i) => (
                                <TableCell key={i} align="center" onClick={() => setIndex(parseInt(race.index))}>
                                    <NavLink
                                        to={race.link}
                                        style={({ isActive }) => ({
                                            color: isActive ? '#fff' : '#545e6f',
                                            background: isActive ? '#ee9154' : '#f0f0f0',
                                          })}
                                    >
                                        {race.time}
                                    </NavLink>
                                </TableCell>
                            ))}
                        </TableRow>
                    ))}
                </TableBody>
            </Table>
        </TableContainer>
    );
}

问题原因

这个问题的核心是Popover会缓存首次渲染时的内容尺寸,而表格内容是异步加载的:

  1. 首次打开时,Popover先以空内容渲染,获取初始尺寸并计算位置;随后数据加载完成,内容尺寸变化,但MUI会自动触发位置更新
  2. 关闭再打开时,Popover复用了之前缓存的旧尺寸信息,而此时按钮的位置可能因窗口变化或页面布局调整发生了改变,导致位置计算错误

解决方案

方案1:强制Popover每次打开时重新渲染

通过给Popover添加动态key,每次打开时更新key值,让Popover完全重新渲染,从而重新计算内容尺寸和位置:

export default function ChooseRacesBtn({ date, setIndex }) {
    const [anchorEl, setAnchorEl] = useState(null);
    const [popoverKey, setPopoverKey] = useState(0);

    const handleClick = (event) => {
        setAnchorEl(event.currentTarget);
        setPopoverKey(prev => prev + 1); // 每次打开更新key
    };

    const handleClose = () => {
        setAnchorEl(null);
    };

    const open = Boolean(anchorEl);

    return (
        <div>
            <Button sx={{ m: 2 }} variant="contained" onClick={handleClick}>
                Choose Races
            </Button>
            <Popover
                key={popoverKey} // 添加动态key
                open={open}
                anchorEl={anchorEl}
                onClose={handleClose}
                anchorOrigin={{
                    vertical: "bottom",
                    horizontal: "right",
                }}
                transformOrigin={{
                    vertical: "top",
                    horizontal: "right",
                }}
            >
                <ChooseRacesPanel date={date} setIndex={setIndex} />
            </Popover>
        </div>
    );
}

方案2:数据加载完成后手动更新位置

利用Popover的updatePosition方法,在表格数据加载完成后强制重新计算位置:

  1. 父组件中获取Popover引用并定义更新方法:
import { useRef } from "react";

export default function ChooseRacesBtn({ date, setIndex }) {
    const [anchorEl, setAnchorEl] = useState(null);
    const popoverRef = useRef(null);

    const handleClick = (event) => {
        setAnchorEl(event.currentTarget);
    };

    const handleClose = () => {
        setAnchorEl(null);
    };

    const updatePopoverPosition = () => {
        popoverRef.current?.updatePosition();
    };

    const open = Boolean(anchorEl);

    return (
        <div>
            <Button sx={{ m: 2 }} variant="contained" onClick={handleClick}>
                Choose Races
            </Button>
            <Popover
                ref={popoverRef}
                open={open}
                anchorEl={anchorEl}
                onClose={handleClose}
                anchorOrigin={{
                    vertical: "bottom",
                    horizontal: "right",
                }}
                transformOrigin={{
                    vertical: "top",
                    horizontal: "right",
                }}
            >
                <ChooseRacesPanel date={date} setIndex={setIndex} onDataLoaded={updatePopoverPosition} />
            </Popover>
        </div>
    );
}
  1. 子组件中数据加载完成后调用更新方法:
export default function ChooseRacesPanel({ date, setIndex, onDataLoaded }) {
    const [meetingNames, setMeetingNames] = useState([]);
    const [meetingsRaces, setMeetingsRaces] = useState([]);
    useEffect(() => {
        const fetchData = async () => {
            const response = await fetch(
                `http://localhost:3000/meeting/${date}`,
                {
                    method: "GET",
                    headers: { "Content-Type": "application/json" },
                }
            );
            const json = await response.json();
            setMeetingNames(json.meetingNames);
            setMeetingsRaces(json.meetingRaces);
            
            // 延迟调用确保DOM已更新
            setTimeout(() => onDataLoaded?.(), 0);
        };
        fetchData();
    }, [date, onDataLoaded]);
    
    // 其余代码不变
}

方案3:固定Popover内容尺寸(可选)

如果表格的尺寸可以提前确定,直接给内容容器设置固定宽高,避免动态尺寸变化导致的位置问题:

<TableContainer component={Paper} sx={{ width: 600, maxHeight: 400, overflowY: 'auto' }}>

内容的提问来源于stack exchange,提问作者Bilal Atique

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.18 17:05:40