From 07f8fe3966cbffd29b0a78d4ed72815f41b5cb3c Mon Sep 17 00:00:00 2001 From: Laurent Cozic Date: Wed, 18 Mar 2026 14:45:48 +0000 Subject: [PATCH] update --- .../gui/hooks/useCtrlWheelZoom.test.ts | 54 ++++++++++++++++--- .../app-desktop/gui/hooks/useCtrlWheelZoom.ts | 35 +++++++++++- 2 files changed, 79 insertions(+), 10 deletions(-) diff --git a/packages/app-desktop/gui/hooks/useCtrlWheelZoom.test.ts b/packages/app-desktop/gui/hooks/useCtrlWheelZoom.test.ts index f80b6b0a44..ad61f08624 100644 --- a/packages/app-desktop/gui/hooks/useCtrlWheelZoom.test.ts +++ b/packages/app-desktop/gui/hooks/useCtrlWheelZoom.test.ts @@ -13,34 +13,72 @@ const dispatchWheel = (options: WheelEventInit) => { document.dispatchEvent(new WheelEvent('wheel', { bubbles: true, ...options })); }; +const dispatchKeyDown = (key: string) => { + document.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true })); +}; + +const dispatchKeyUp = (key: string) => { + document.dispatchEvent(new KeyboardEvent('keyup', { key, bubbles: true })); +}; + describe('useCtrlWheelZoom', () => { beforeEach(() => { jest.clearAllMocks(); }); - test('should zoom on Ctrl/Meta+Wheel', () => { + test('should zoom when Ctrl key is pressed and wheel is scrolled', () => { renderHook(() => useCtrlWheelZoom()); - dispatchWheel({ deltaY: -100, ctrlKey: true }); + dispatchKeyDown('Control'); + dispatchWheel({ deltaY: -100 }); expect(Setting.incValue).toHaveBeenCalledWith('windowContentZoomFactor', 10); jest.clearAllMocks(); - dispatchWheel({ deltaY: 100, ctrlKey: true }); + dispatchWheel({ deltaY: 100 }); expect(Setting.incValue).toHaveBeenCalledWith('windowContentZoomFactor', -10); - jest.clearAllMocks(); - - dispatchWheel({ deltaY: -100, metaKey: true }); - expect(Setting.incValue).toHaveBeenCalledWith('windowContentZoomFactor', 10); + dispatchKeyUp('Control'); }); - test('should not zoom on wheel without modifier', () => { + test('should zoom when Meta key is pressed and wheel is scrolled', () => { + renderHook(() => useCtrlWheelZoom()); + + dispatchKeyDown('Meta'); + dispatchWheel({ deltaY: -100 }); + expect(Setting.incValue).toHaveBeenCalledWith('windowContentZoomFactor', 10); + + dispatchKeyUp('Meta'); + }); + + test('should not zoom on wheel without modifier key pressed', () => { renderHook(() => useCtrlWheelZoom()); dispatchWheel({ deltaY: -100 }); + expect(Setting.incValue).not.toHaveBeenCalled(); + }); + test('should not zoom when only ctrlKey flag is set on wheel event (trackpad pinch)', () => { + // On macOS, trackpad pinch gestures send wheel events with ctrlKey=true + // but without actually pressing the Ctrl key + renderHook(() => useCtrlWheelZoom()); + + dispatchWheel({ deltaY: -100, ctrlKey: true }); + expect(Setting.incValue).not.toHaveBeenCalled(); + }); + + test('should stop zooming after key is released', () => { + renderHook(() => useCtrlWheelZoom()); + + dispatchKeyDown('Control'); + dispatchWheel({ deltaY: -100 }); + expect(Setting.incValue).toHaveBeenCalledTimes(1); + + jest.clearAllMocks(); + + dispatchKeyUp('Control'); + dispatchWheel({ deltaY: -100 }); expect(Setting.incValue).not.toHaveBeenCalled(); }); }); diff --git a/packages/app-desktop/gui/hooks/useCtrlWheelZoom.ts b/packages/app-desktop/gui/hooks/useCtrlWheelZoom.ts index 8027b994e8..b5e17be93f 100644 --- a/packages/app-desktop/gui/hooks/useCtrlWheelZoom.ts +++ b/packages/app-desktop/gui/hooks/useCtrlWheelZoom.ts @@ -3,14 +3,45 @@ import Setting from '@joplin/lib/models/Setting'; const useCtrlWheelZoom = () => { useEffect(() => { + // Track whether modifier keys are actually pressed via keyboard events. + // This is needed because on macOS, trackpad pinch-to-zoom gestures are + // reported as wheel events with ctrlKey=true, even though Ctrl isn't pressed. + let ctrlPressed = false; + let metaPressed = false; + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Control') ctrlPressed = true; + if (e.key === 'Meta') metaPressed = true; + }; + + const handleKeyUp = (e: KeyboardEvent) => { + if (e.key === 'Control') ctrlPressed = false; + if (e.key === 'Meta') metaPressed = false; + }; + + const handleBlur = () => { + ctrlPressed = false; + metaPressed = false; + }; + const handleWheel = (e: WheelEvent) => { - if (e.ctrlKey || e.metaKey) { + if (ctrlPressed || metaPressed) { e.preventDefault(); Setting.incValue('windowContentZoomFactor', e.deltaY < 0 ? 10 : -10); } }; + + document.addEventListener('keydown', handleKeyDown); + document.addEventListener('keyup', handleKeyUp); + window.addEventListener('blur', handleBlur); document.addEventListener('wheel', handleWheel, { passive: false }); - return () => document.removeEventListener('wheel', handleWheel); + + return () => { + document.removeEventListener('keydown', handleKeyDown); + document.removeEventListener('keyup', handleKeyUp); + window.removeEventListener('blur', handleBlur); + document.removeEventListener('wheel', handleWheel); + }; }, []); };