From fe9741708c9f4769d22ee4c656cf4c55234edd8a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 12 Mar 2026 01:33:46 +0000 Subject: [PATCH] Fix inline parameter panel causing node height growth on remount MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two issues caused nodes to grow taller each time they scrolled back into view with onlyRenderVisibleElements: 1. InlineParameterPanel initialized contentHeight to 0 and animated from maxHeight:0 to the measured height on every remount. This CSS transition caused the ResizeObserver in BaseNode to see the panel growing and add its height to the node — even though the node already included it. Fix: when mounted with expanded=true, render with maxHeight:"none" (no transition) so the panel appears at full size instantly. 2. The ResizeObserver skip in BaseNode only caught a single observation. The panel settling could fire multiple times during the CSS transition. Fix: use a time-based suppression (300ms) that syncs the tracked height ref without modifying node dimensions for all observations during the settle window. https://claude.ai/code/session_01MvD1n4QeXutgwUpKJuDGHa --- src/components/nodes/BaseNode.tsx | 24 ++++++++++++------- src/components/nodes/InlineParameterPanel.tsx | 9 +++++-- 2 files changed, 23 insertions(+), 10 deletions(-) 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}