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

React中Floating UI固定弹窗后如何使其随容器滚动而非固定于视口

嘿,我来帮你搞定这个Floating UI的滚动问题!你的核心需求是:弹窗固定后要跟着容器滚动,而不是钉在视口不动。问题出在两个地方——默认的autoUpdate监听了不必要的滚动事件,还有你用的是视口坐标来定位固定弹窗。下面是具体的解决方案:

问题根源拆解

  1. autoUpdate的过度监听:默认的autoUpdate会监听窗口滚动、容器滚动、元素大小变化等所有可能影响位置的事件,导致固定弹窗被强制更新到视口固定位置。
  2. 坐标体系错误:你用clientX/clientY(视口坐标)作为固定点的参考,容器滚动时这个坐标不会跟着变化,所以弹窗会停在视口原来的位置。

具体修改方案

1. 新增滚动容器的引用

首先给组件加一个滚动容器的props,这样我们能准确计算容器的滚动偏移和视口位置:

// 在TechNodePopupProps里新增
scrollContainerRef: React.RefObject<HTMLElement>;

2. 自定义位置更新逻辑

根据弹窗是否固定,分别处理监听事件:固定时只监听容器滚动和窗口resize,非固定时保留默认的autoUpdate行为。

3. 修正固定点的坐标计算

把固定点的视口坐标转换成相对于滚动容器的坐标,这样容器滚动时,固定点的视口位置会跟着容器偏移,弹窗就能同步滚动了。

修改后的完整代码

import scopedCss from '@/components/TechNodePopup.module.css';
import { useEffect, useRef } from "react";
import clsx from "clsx";
import { useFloating, offset, flip, shift, autoUpdate } from "@floating-ui/react";
import { getAlignment } from "@floating-ui/utils";
import type { AbilityType } from "@/types/AbilityType";
import type { TechNodeWithMeta } from "@/types/TechNodeWithMeta";
import TechNodePopupContent from './TechNodePopupContent';

export interface TechNodePopupProps {
 abilityType: AbilityType;
 abilityName?: string;
 nodeData: TechNodeWithMeta;
 getTechNameFromId: (id: string) => string;
 initialX: number;
 initialY: number;
 pinnedAt?: { x: number; y: number };
 // 新增:滚动容器的引用
 scrollContainerRef: React.RefObject<HTMLElement>;
}

export default function TechNodePopup({ 
 abilityType, 
 abilityName, 
 nodeData, 
 getTechNameFromId, 
 initialX, 
 initialY, 
 pinnedAt,
 scrollContainerRef
}: TechNodePopupProps) {
 const mouseRef = useRef({ x: initialX, y: initialY });
 const { refs, floatingStyles, update, } = useFloating({
 strategy: "absolute",
 placement: "top-end",
 middleware: [
 offset(({ placement }) => {
 return { mainAxis: 20, crossAxis: getAlignment(placement) === "start" ? 20 : -20 };
 }),
 flip({ mainAxis: true, crossAxis: true, }),
 shift({ mainAxis: true, crossAxis: true, }),
 ],
 // 自定义位置更新逻辑:根据是否固定切换监听事件
 whileElementsMounted: (referenceEl, floatingEl, updateFn) => {
   if (pinnedAt && scrollContainerRef.current) {
     // 固定状态:只监听容器滚动和窗口resize
     const handleScroll = () => updateFn();
     const handleResize = () => updateFn();
     
     scrollContainerRef.current.addEventListener('scroll', handleScroll);
     window.addEventListener('resize', handleResize);
     
     return () => {
       scrollContainerRef.current?.removeEventListener('scroll', handleScroll);
       window.removeEventListener('resize', handleResize);
     };
   } else {
     // 非固定状态:用默认的autoUpdate监听鼠标移动等事件
     return autoUpdate(referenceEl, floatingEl, updateFn);
   }
 },
 });

 // 生成参考元素:鼠标位置或固定点位置
 useEffect(() => {
 refs.setReference({
 getBoundingClientRect() {
   let pos;
   if (pinnedAt && scrollContainerRef.current) {
     const container = scrollContainerRef.current;
     const containerRect = container.getBoundingClientRect();
     // 计算固定点的视口坐标:容器视口位置 + 固定点相对容器坐标 - 容器滚动偏移
     pos = {
       x: containerRect.left + pinnedAt.x - container.scrollLeft,
       y: containerRect.top + pinnedAt.y - container.scrollTop,
     };
   } else {
     pos = mouseRef.current;
   }
   return { 
     x: pos.x, 
     y: pos.y, 
     top: pos.y, 
     left: pos.x, 
     right: pos.x, 
     bottom: pos.y, 
     width: 0, 
     height: 0, 
   };
 },
 });
 }, [refs, pinnedAt, scrollContainerRef]);

 // 鼠标移动时更新弹窗位置(非固定状态)
 useEffect(() => {
 function handleMouseMove(e: MouseEvent) {
 mouseRef.current = { x: e.clientX, y: e.clientY };
 if (pinnedAt) {
 return;
 }
 update();
 }
 window.addEventListener("mousemove", handleMouseMove);
 return () => {
 window.removeEventListener("mousemove", handleMouseMove);
 };
 }, [update, pinnedAt]);

 // 计算技术类型样式类
 let techTypeClass = "";
 if (abilityType === "tech") {
 techTypeClass = clsx(nodeData.techType && `tech-${nodeData.techType.toLowerCase()}`);
 }

 return (
 <div
 ref={refs.setFloating}
 className={clsx("techNode", scopedCss.techNode, "selected", "highlighted", techTypeClass, pinnedAt && scopedCss.pinned)}
 style={{ ...floatingStyles, width: "410px", zIndex: 999, }}
 >
 <TechNodePopupContent
 abilityType={abilityType}
 abilityName={abilityName}
 techTypeClass={techTypeClass}
 nodeData={nodeData}
 getTechNameFromId={getTechNameFromId}
 />
 </div>
 );
}

额外提示

如果你的pinnedAt原本是用视口坐标(clientX/clientY)设置的,记得先转换成容器相对坐标:

// 示例:把视口坐标转成容器相对坐标
const container = scrollContainerRef.current;
const containerRect = container.getBoundingClientRect();
const pinnedAt = {
  x: clientX - containerRect.left + container.scrollLeft,
  y: clientY - containerRect.top + container.scrollTop,
};

这样修改后,固定的弹窗就会跟着滚动容器一起移动,不会再钉死在视口啦!

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.28 06:42:37