fix: use non-passive wheel listener to prevent page scroll during diagram zoom

React's onWheel is passive by default, so preventDefault() doesn't stop
page scrolling. Attach native wheel listener with { passive: false } via
useEffect instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-27 19:24:09 +01:00
parent 021a52e56b
commit f675451384
2 changed files with 15 additions and 9 deletions

View File

@@ -193,8 +193,8 @@ export function ProcessDiagram({
)} )}
<svg <svg
ref={zoom.svgRef}
className={styles.svg} className={styles.svg}
onWheel={zoom.onWheel}
onPointerDown={zoom.onPointerDown} onPointerDown={zoom.onPointerDown}
onPointerMove={zoom.onPointerMove} onPointerMove={zoom.onPointerMove}
onPointerUp={zoom.onPointerUp} onPointerUp={zoom.onPointerUp}

View File

@@ -1,4 +1,4 @@
import { useCallback, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
interface ZoomPanState { interface ZoomPanState {
scale: number; scale: number;
@@ -20,19 +20,24 @@ export function useZoomPan() {
const isPanning = useRef(false); const isPanning = useRef(false);
const panStart = useRef({ x: 0, y: 0 }); const panStart = useRef({ x: 0, y: 0 });
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const svgRef = useRef<SVGSVGElement>(null);
const clampScale = (s: number) => Math.min(MAX_SCALE, Math.max(MIN_SCALE, s)); const clampScale = (s: number) => Math.min(MAX_SCALE, Math.max(MIN_SCALE, s));
/** Returns the CSS transform string for the content <g> element. */ /** Returns the CSS transform string for the content <g> element. */
const transform = `translate(${state.translateX}px, ${state.translateY}px) scale(${state.scale})`; const transform = `translate(${state.translateX}px, ${state.translateY}px) scale(${state.scale})`;
const onWheel = useCallback( // Attach wheel listener with { passive: false } so preventDefault() stops page scroll.
(e: React.WheelEvent<SVGSVGElement>) => { // React's onWheel is passive by default and cannot prevent scrolling.
useEffect(() => {
const svg = svgRef.current;
if (!svg) return;
const handler = (e: WheelEvent) => {
e.preventDefault(); e.preventDefault();
const direction = e.deltaY < 0 ? 1 : -1; const direction = e.deltaY < 0 ? 1 : -1;
const factor = 1 + direction * ZOOM_STEP; const factor = 1 + direction * ZOOM_STEP;
const rect = (e.currentTarget as SVGSVGElement).getBoundingClientRect(); const rect = svg.getBoundingClientRect();
const cursorX = e.clientX - rect.left; const cursorX = e.clientX - rect.left;
const cursorY = e.clientY - rect.top; const cursorY = e.clientY - rect.top;
@@ -45,9 +50,10 @@ export function useZoomPan() {
translateY: cursorY - scaleRatio * (cursorY - prev.translateY), translateY: cursorY - scaleRatio * (cursorY - prev.translateY),
}; };
}); });
}, };
[], svg.addEventListener('wheel', handler, { passive: false });
); return () => svg.removeEventListener('wheel', handler);
}, []);
const onPointerDown = useCallback( const onPointerDown = useCallback(
(e: React.PointerEvent<SVGSVGElement>) => { (e: React.PointerEvent<SVGSVGElement>) => {
@@ -158,10 +164,10 @@ export function useZoomPan() {
return { return {
state, state,
containerRef, containerRef,
svgRef,
transform, transform,
panTo, panTo,
resetView, resetView,
onWheel,
onPointerDown, onPointerDown,
onPointerMove, onPointerMove,
onPointerUp, onPointerUp,