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
ref={zoom.svgRef}
className={styles.svg}
onWheel={zoom.onWheel}
onPointerDown={zoom.onPointerDown}
onPointerMove={zoom.onPointerMove}
onPointerUp={zoom.onPointerUp}

View File

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