React中Floating UI固定弹窗后如何使其随容器滚动而非固定于视口
嘿,我来帮你搞定这个Floating UI的滚动问题!你的核心需求是:弹窗固定后要跟着容器滚动,而不是钉在视口不动。问题出在两个地方——默认的autoUpdate监听了不必要的滚动事件,还有你用的是视口坐标来定位固定弹窗。下面是具体的解决方案:
问题根源拆解
autoUpdate的过度监听:默认的autoUpdate会监听窗口滚动、容器滚动、元素大小变化等所有可能影响位置的事件,导致固定弹窗被强制更新到视口固定位置。- 坐标体系错误:你用
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
相关产品推荐
相关产品推荐

