diff --git a/package.json b/package.json index 848104764..68f03fa92 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "dev": "vite", "build": "vite build", "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", - "preview": "vite preview" + "preview": "vite preview", + "test": "node --test" }, "dependencies": { "@dagrejs/dagre": "^3.1.1", diff --git a/src/components/EditorCanvas/Canvas.jsx b/src/components/EditorCanvas/Canvas.jsx index 976dcd33c..a11614fe7 100644 --- a/src/components/EditorCanvas/Canvas.jsx +++ b/src/components/EditorCanvas/Canvas.jsx @@ -32,6 +32,7 @@ import { useTranslation } from "react-i18next"; import { useEventListener } from "usehooks-ts"; import { areFieldsCompatible, getTableHeight } from "../../utils/utils"; import { getRectFromEndpoints, isInsideRect } from "../../utils/rect"; +import { getWheelPanDelta } from "../../utils/wheel"; import { State, noteWidth } from "../../data/constants"; import { nanoid } from "nanoid"; @@ -697,20 +698,14 @@ export default function Canvas() { }, zoom: e.deltaY <= 0 ? prev.zoom * 1.05 : prev.zoom / 1.05, })); - } else if (e.shiftKey) { - setTransform((prev) => ({ - ...prev, - pan: { - ...prev.pan, - x: prev.pan.x + e.deltaY / prev.zoom, - }, - })); } else { + const delta = getWheelPanDelta(e); setTransform((prev) => ({ ...prev, pan: { ...prev.pan, - y: prev.pan.y + e.deltaY / prev.zoom, + x: prev.pan.x + delta.x / prev.zoom, + y: prev.pan.y + delta.y / prev.zoom, }, })); } diff --git a/src/utils/wheel.js b/src/utils/wheel.js new file mode 100644 index 000000000..5834781c6 --- /dev/null +++ b/src/utils/wheel.js @@ -0,0 +1,17 @@ +/** + * Resolve the canvas pan movement produced by a wheel event. + * + * Browsers disagree on how Shift + wheel is reported: Chromium on macOS + * translates the vertical wheel input into a horizontal event (deltaX with + * deltaY === 0) while other platforms keep the movement in deltaY. Native + * horizontal wheels and trackpads also report deltaX without Shift. + * + * @param {WheelEvent} wheelEvent + * @returns {{x: number, y: number}} Pan delta in diagram units before zoom scaling. + */ +export function getWheelPanDelta(wheelEvent) { + if (wheelEvent.shiftKey) { + return { x: wheelEvent.deltaX || wheelEvent.deltaY, y: 0 }; + } + return { x: wheelEvent.deltaX, y: wheelEvent.deltaY }; +} diff --git a/src/utils/wheel.test.js b/src/utils/wheel.test.js new file mode 100644 index 000000000..208ba0def --- /dev/null +++ b/src/utils/wheel.test.js @@ -0,0 +1,40 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; + +import { getWheelPanDelta } from "./wheel.js"; + +test("Shift + wheel keeps vertical delta on browsers without axis translation", () => { + const delta = getWheelPanDelta({ shiftKey: true, deltaX: 0, deltaY: 120 }); + + assert.deepEqual(delta, { x: 120, y: 0 }); +}); + +test("Shift + wheel uses deltaX on browsers that translate the axis", () => { + const delta = getWheelPanDelta({ shiftKey: true, deltaX: 120, deltaY: 0 }); + + assert.deepEqual(delta, { x: 120, y: 0 }); +}); + +test("wheel pans horizontally when Shift is not held", () => { + const delta = getWheelPanDelta({ shiftKey: false, deltaX: 120, deltaY: 0 }); + + assert.deepEqual(delta, { x: 120, y: 0 }); +}); + +test("wheel pans vertically when Shift is not held", () => { + const delta = getWheelPanDelta({ shiftKey: false, deltaX: 0, deltaY: 120 }); + + assert.deepEqual(delta, { x: 0, y: 120 }); +}); + +test("wheel pans diagonally when both axes report movement", () => { + const delta = getWheelPanDelta({ shiftKey: false, deltaX: 40, deltaY: 80 }); + + assert.deepEqual(delta, { x: 40, y: 80 }); +}); + +test("wheel with no movement produces no pan", () => { + const delta = getWheelPanDelta({ shiftKey: false, deltaX: 0, deltaY: 0 }); + + assert.deepEqual(delta, { x: 0, y: 0 }); +});