diff --git a/src/components/nodes/BaseNode.tsx b/src/components/nodes/BaseNode.tsx index 4675e340..70c10d3b 100644 --- a/src/components/nodes/BaseNode.tsx +++ b/src/components/nodes/BaseNode.tsx @@ -81,20 +81,26 @@ export function BaseNode({ const isAnimatingRef = useRef(false); const animationTimeoutRef = useRef | null>(null); const isInitialMountRef = useRef(true); - // When true, the ResizeObserver should sync trackedSettingsHeightRef without - // modifying node dimensions (the height is already accounted for from a + // When true, the ResizeObserver syncs trackedSettingsHeightRef without + // modifying node dimensions (height already includes the panel from a // previous mount before onlyRenderVisibleElements unmounted us). - const skipFirstObserveRef = useRef(false); + const suppressObserverRef = useRef(false); + const suppressTimerRef = useRef | null>(null); // Adjust node height when settings expand or collapse useLayoutEffect(() => { // On initial mount with settings already expanded (e.g. remount after // onlyRenderVisibleElements), the node height already includes the panel. - // Tell the ResizeObserver to sync without adding height. + // Suppress ResizeObserver dimension changes while the panel settles. if (isInitialMountRef.current) { isInitialMountRef.current = false; if (settingsExpanded && settingsPanel) { - skipFirstObserveRef.current = true; + suppressObserverRef.current = true; + // Allow suppression for long enough that the panel fully renders + suppressTimerRef.current = setTimeout(() => { + suppressObserverRef.current = false; + suppressTimerRef.current = null; + }, 300); } return; } @@ -187,8 +193,7 @@ export function BaseNode({ // On remount with settings already expanded, the node height already // includes the panel. Just sync the ref without modifying dimensions. - if (skipFirstObserveRef.current) { - skipFirstObserveRef.current = false; + if (suppressObserverRef.current) { trackedSettingsHeightRef.current = newPanelHeight; continue; } @@ -231,12 +236,15 @@ export function BaseNode({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [settingsExpanded, settingsPanel]); - // Cleanup animation timeout on unmount + // Cleanup timeouts on unmount useLayoutEffect(() => { return () => { if (animationTimeoutRef.current) { clearTimeout(animationTimeoutRef.current); } + if (suppressTimerRef.current) { + clearTimeout(suppressTimerRef.current); + } }; }, []); diff --git a/src/components/nodes/InlineParameterPanel.tsx b/src/components/nodes/InlineParameterPanel.tsx index b0964ba2..b9e280c8 100644 --- a/src/components/nodes/InlineParameterPanel.tsx +++ b/src/components/nodes/InlineParameterPanel.tsx @@ -21,10 +21,15 @@ function InlineParameterPanelInner({ }: InlineParameterPanelProps) { const contentRef = useRef(null); const [contentHeight, setContentHeight] = useState(0); + // When mounted with expanded=true (e.g. remount after scrolling back into + // view), render at full height immediately instead of animating from 0. + const mountedExpandedRef = useRef(expanded); useEffect(() => { if (expanded && contentRef.current) { setContentHeight(contentRef.current.scrollHeight); + // After first measurement, future changes should animate normally + mountedExpandedRef.current = false; } }, [expanded, children]); @@ -57,8 +62,8 @@ function InlineParameterPanelInner({ {/* Content area — smooth height animation */}
{children}