diff --git a/packages/react-native-gesture-handler/src/web/handlers/PanGestureHandler.ts b/packages/react-native-gesture-handler/src/web/handlers/PanGestureHandler.ts index 3f21333011..2a56357853 100644 --- a/packages/react-native-gesture-handler/src/web/handlers/PanGestureHandler.ts +++ b/packages/react-native-gesture-handler/src/web/handlers/PanGestureHandler.ts @@ -334,14 +334,13 @@ export default class PanGestureHandler extends GestureHandler { } } - private scheduleWheelEnd(event: AdaptedEvent) { + private scheduleWheelEnd() { clearTimeout(this.endWheelTimeout); this.endWheelTimeout = setTimeout(() => { if (this.state === State.ACTIVE) { this.end(); - this.tracker.removeFromTracker(event.pointerId); - this.state = State.UNDETERMINED; + this.reset(); } this.wheelDevice = WheelDevice.UNDETERMINED; @@ -363,7 +362,7 @@ export default class PanGestureHandler extends GestureHandler { : WheelDevice.MOUSE; if (this.wheelDevice === WheelDevice.MOUSE) { - this.scheduleWheelEnd(event); + this.scheduleWheelEnd(); return; } @@ -383,7 +382,7 @@ export default class PanGestureHandler extends GestureHandler { this.updateVelocity(event.pointerId); this.tryToSendMoveEvent(false, event); - this.scheduleWheelEnd(event); + this.scheduleWheelEnd(); } private shouldActivate(): boolean { diff --git a/packages/react-native-gesture-handler/src/web/handlers/__tests__/PanGestureHandler.test.ts b/packages/react-native-gesture-handler/src/web/handlers/__tests__/PanGestureHandler.test.ts index d2da183f8a..ad0f588117 100644 --- a/packages/react-native-gesture-handler/src/web/handlers/__tests__/PanGestureHandler.test.ts +++ b/packages/react-native-gesture-handler/src/web/handlers/__tests__/PanGestureHandler.test.ts @@ -3,15 +3,24 @@ import { PointerType } from '../../../PointerType'; import { State } from '../../../State'; import type { AdaptedEvent } from '../../interfaces'; import { EventTypes } from '../../interfaces'; +import type EventManager from '../../tools/EventManager'; import type { GestureHandlerDelegate } from '../../tools/GestureHandlerDelegate'; import GestureHandlerOrchestrator from '../../tools/GestureHandlerOrchestrator'; +import WheelEventManager from '../../tools/WheelEventManager'; import type IGestureHandler from '../IGestureHandler'; import PanGestureHandler from '../PanGestureHandler'; class TestPanGestureHandler extends PanGestureHandler { + public readonly wheelEvents: AdaptedEvent[] = []; + public wheel(event: AdaptedEvent): void { this.onWheel(event); } + + protected override onWheel(event: AdaptedEvent): void { + this.wheelEvents.push(event); + super.onWheel(event); + } } function wheelEvent(): AdaptedEvent { @@ -29,11 +38,15 @@ function wheelEvent(): AdaptedEvent { }; } -function createHandler() { +function createHandler(eventManagers: EventManager[] = []) { const delegate = { init: jest.fn(), detach: jest.fn(), - reset: jest.fn(), + // Mirrors GestureHandlerWebDelegate.reset(), which forwards the reset to + // every event manager attached to the handler. + reset: jest.fn(() => + eventManagers.forEach((manager) => manager.resetManager()) + ), onBegin: jest.fn(), onActivate: jest.fn(), onFail: jest.fn(), @@ -60,16 +73,46 @@ function createHandler() { return handler; } -describe('PanGestureHandler config reset', () => { - afterEach(() => { - // The orchestrator is a singleton, drop handlers recorded by the test. - ( - GestureHandlerOrchestrator.instance as unknown as { - gestureHandlers: IGestureHandler[]; - } - ).gestureHandlers = []; - }); +afterEach(() => { + // The orchestrator is a singleton, drop handlers recorded by the test. + ( + GestureHandlerOrchestrator.instance as unknown as { + gestureHandlers: IGestureHandler[]; + } + ).gestureHandlers = []; +}); + +class FakeView { + private readonly listeners = new Map void>>(); + + public addEventListener(type: string, listener: (event: unknown) => void) { + const listeners = this.listeners.get(type) ?? new Set(); + listeners.add(listener); + this.listeners.set(type, listeners); + } + + public removeEventListener(type: string, listener: (event: unknown) => void) { + this.listeners.get(type)?.delete(listener); + } + public wheel(deltaY: number): void { + this.listeners.get('wheel')?.forEach((listener) => + listener({ + clientX: 0, + clientY: 0, + offsetX: 0, + offsetY: 0, + deltaX: 0, + deltaY, + timeStamp: 0, + // Not a multiple of 120, so the wheel is recognized as a touchpad. + wheelDeltaY: 13, + }) + ); + } +} + +describe('PanGestureHandler config reset', () => { test('a config without enableTrackpadTwoFingerGesture restores the disabled default', () => { const handler = createHandler(); @@ -97,3 +140,40 @@ describe('PanGestureHandler config reset', () => { expect(handler.state).toBe(State.ACTIVE); }); }); + +describe('PanGestureHandler trackpad gesture end', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + // The gesture schedules its end on every wheel event, drop the last one. + jest.clearAllTimers(); + jest.useRealTimers(); + }); + + test('the next trackpad gesture starts from the wheel event coordinates', () => { + const view = new FakeView(); + const manager = new WheelEventManager(view as unknown as HTMLElement); + const handler = createHandler([manager]); + + handler.setGestureConfig({ + enabled: true, + enableTrackpadTwoFingerGesture: true, + }); + handler.attachEventManager(manager); + + view.wheel(100); + expect(handler.state).toBe(State.ACTIVE); + + // A trackpad gesture ends when the wheel goes quiet for 30ms. + jest.advanceTimersByTime(30); + expect(handler.state).toBe(State.UNDETERMINED); + + view.wheel(30); + + // A wheel does not move the cursor, so the manager synthesizes coordinates + // by accumulating deltas. The gesture that just ended must not contribute. + expect(handler.wheelEvents[1].y).toBe(30); + }); +}); diff --git a/packages/react-native-gesture-handler/src/web/tools/WheelEventManager.ts b/packages/react-native-gesture-handler/src/web/tools/WheelEventManager.ts index d7430c021a..f405656f0d 100644 --- a/packages/react-native-gesture-handler/src/web/tools/WheelEventManager.ts +++ b/packages/react-native-gesture-handler/src/web/tools/WheelEventManager.ts @@ -45,5 +45,6 @@ export default class WheelEventManager extends EventManager { public override resetManager(): void { super.resetManager(); + this.wheelDelta = { x: 0, y: 0 }; } }