diff --git a/ui/src/components/ProcessDiagram/useZoomPan.ts b/ui/src/components/ProcessDiagram/useZoomPan.ts index e3ae155f..1e5162a4 100644 --- a/ui/src/components/ProcessDiagram/useZoomPan.ts +++ b/ui/src/components/ProcessDiagram/useZoomPan.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef, useState, type RefCallback } from 'react'; interface ZoomPanState { scale: number; @@ -23,7 +23,13 @@ export function useZoomPan() { const didPan = useRef(false); const panStart = useRef({ x: 0, y: 0 }); const containerRef = useRef(null); - const svgRef = useRef(null); + const svgElRef = useRef(null); + const [svgReady, setSvgReady] = useState(false); + + const svgRef: RefCallback = useCallback((el: SVGSVGElement | null) => { + svgElRef.current = el; + setSvgReady(!!el); + }, []); const clampScale = (s: number) => Math.min(MAX_SCALE, Math.max(MIN_SCALE, s)); @@ -32,8 +38,9 @@ export function useZoomPan() { // Attach wheel listener with { passive: false } so preventDefault() stops page scroll. // React's onWheel is passive by default and cannot prevent scrolling. + // Re-runs when the SVG element becomes available (e.g. after loading state clears). useEffect(() => { - const svg = svgRef.current; + const svg = svgElRef.current; if (!svg) return; const handler = (e: WheelEvent) => { e.preventDefault(); @@ -56,7 +63,7 @@ export function useZoomPan() { }; svg.addEventListener('wheel', handler, { passive: false }); return () => svg.removeEventListener('wheel', handler); - }, []); + }, [svgReady]); const onPointerDown = useCallback( (e: React.PointerEvent) => {