diff --git a/src/components/nodes/EaseCurveNode.tsx b/src/components/nodes/EaseCurveNode.tsx index 95bd4b46..bb7958bc 100644 --- a/src/components/nodes/EaseCurveNode.tsx +++ b/src/components/nodes/EaseCurveNode.tsx @@ -1,6 +1,7 @@ "use client"; import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { createPortal } from "react-dom"; import { Handle, Position, NodeProps, Node, useReactFlow } from "@xyflow/react"; import { BaseNode } from "./BaseNode"; import { useCommentNavigation } from "@/hooks/useCommentNavigation"; @@ -48,7 +49,9 @@ export function EaseCurveNode({ id, data, selected }: NodeProps("editor"); const [showPresets, setShowPresets] = useState(false); - const presetsRef = useRef(null); + const presetsButtonRef = useRef(null); + const presetsPopoverRef = useRef(null); + const [presetsPosition, setPresetsPosition] = useState<{ top: number; left: number } | null>(null); // Auto-resize node height when switching tabs const EDITOR_HEIGHT = 480; @@ -81,12 +84,45 @@ export function EaseCurveNode({ id, data, selected }: NodeProps { + if (!showPresets || !presetsButtonRef.current) { + setPresetsPosition(null); + return; + } + + const updatePosition = () => { + if (presetsButtonRef.current) { + const rect = presetsButtonRef.current.getBoundingClientRect(); + setPresetsPosition({ + top: rect.top, + left: rect.right, + }); + } + }; + + updatePosition(); + + let animationId: number; + const trackPosition = () => { + updatePosition(); + animationId = requestAnimationFrame(trackPosition); + }; + animationId = requestAnimationFrame(trackPosition); + + return () => cancelAnimationFrame(animationId); + }, [showPresets]); + // Close presets popover on click outside or Escape useEffect(() => { if (!showPresets) return; const handleClickOutside = (e: MouseEvent) => { - if (presetsRef.current && !presetsRef.current.contains(e.target as HTMLElement)) { + const target = e.target as HTMLElement; + if ( + presetsButtonRef.current && !presetsButtonRef.current.contains(target) && + presetsPopoverRef.current && !presetsPopoverRef.current.contains(target) + ) { setShowPresets(false); } }; @@ -407,8 +443,9 @@ export function EaseCurveNode({ id, data, selected }: NodePropssec {/* Presets button */} -
+
- - {/* Presets popover */} - {showPresets && ( -
- {/* Preset Bezier thumbnails (top section) */} -
-
- Bezier Presets -
-
- {EASING_PRESETS.map((name) => { - const thumb = presetThumbnails.find((t) => t.name === name); - const isActive = nodeData.easingPreset === name; - return ( - - ); - })} -
-
- - {/* All easing functions (scrollable grid) */} -
-
- All Easing Functions -
-
- {presetThumbnails.map(({ name, polyline }) => { - const isActive = nodeData.easingPreset === name; - return ( - - ); - })} -
-
-
- )}
@@ -587,6 +536,103 @@ export function EaseCurveNode({ id, data, selected }: NodeProps )} + + {/* Presets popover - rendered via portal to avoid clipping */} + {showPresets && presetsPosition && createPortal( +
+ {/* Preset Bezier thumbnails (top section) */} +
+
+ Bezier Presets +
+
+ {EASING_PRESETS.map((name) => { + const thumb = presetThumbnails.find((t) => t.name === name); + const isActive = nodeData.easingPreset === name; + return ( + + ); + })} +
+
+ + {/* All easing functions (scrollable grid) */} +
+
+ All Easing Functions +
+
+ {presetThumbnails.map(({ name, polyline }) => { + const isActive = nodeData.easingPreset === name; + return ( + + ); + })} +
+
+
, + document.body + )} ); }