让Floating UI根据需要固定弹出框的位置
我还没能让Floating UI与 React一起按我想要的方式工作。我创建了一个跟随鼠标移动的弹出组件。我可以设置 pinnedAt 属性让弹出框固定在一个位置,但Floating UI仍会在其容器滚动时持续更新已固定弹出框的位置,使其相对于视口保持在同一位置。我希望一旦固定后,它能够随容器一起滚动。
我该如何实现?这是我的组件代码:
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 };
}
export default function TechNodePopup({ abilityType, abilityName, nodeData, getTechNameFromId, initialX, initialY, pinnedAt }: 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: autoUpdate,
});
// Virtual element representing the mouse cursor
useEffect(() => {
refs.setReference({
getBoundingClientRect() {
const pos = pinnedAt ?? 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]);
// Get floating element to position itself based on mouse cursor upon mouse movement
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]);
// Calc tech type class (needed here and in content component)
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>
);
}
解决方案
你所看到的行为是autoUpdate在按它的设计去做。它在让一个“浮动”元素与其“参考”元素保持同步,即使你滚动。当你的虚拟参考,即getBoundingClientRect函数,返回的是相对于视口的坐标(clientX、clientY)时,问题就出现了。当容器滚动时,autoUpdate会重新执行该函数。它看到相同的坐标并将弹出框移动到视口中恰好相同的位置。解决方法是切换autoUpdate,使弹出框在固定后“粘”在内容上并随其滚动;你需要在pinnedAt存在时关闭autoUpdate。Floating UI的 whileElementsMounted选项允许你使用一个函数。如果你基于条件返回undefined或一个清理函数,你就可以有效地“冻结”定位逻辑。
以上大部分内容都已经解决;还需要做一些其他的改动:
whileElementsMounted(reference, floating, updateFn) {
if (pinnedAt) {
// pinned mode: do nothing, return no-op cleanup
return () => { };
}
// mouse cursor floating mode: run autoUpdate as usual, return its cleanup
const cleanup = autoUpdate(reference, floating, updateFn);
return () => {
cleanup();
};
}
...作为附加,这还包括一个改动,当 pinnedAt 被设置为某个新值而弹出框已经固定时,会让弹出框只移动一次:
// One-off moving of popup if pinnedAt changed while already pinned
useEffect(() => {
if (pinnedAt) {
update();
}
}, [pinnedAt, update]);
站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。