From 45ee18d29a8515e9098c38f339ff1328efeebcbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Thu, 5 Nov 2026 11:23:57 +0100 Subject: [PATCH] [Web] Check only the handlers recorded for the same pointer --- .../src/web/handlers/GestureHandler.ts | 5 +- .../handlers/__tests__/GestureHandler.test.ts | 97 +++++++++++++++++++ .../web/tools/GestureHandlerOrchestrator.ts | 16 ++- .../src/web/tools/PointerTracker.ts | 8 +- 4 files changed, 121 insertions(+), 5 deletions(-) diff --git a/packages/react-native-gesture-handler/src/web/handlers/GestureHandler.ts b/packages/react-native-gesture-handler/src/web/handlers/GestureHandler.ts index c549215d19..901645196e 100644 --- a/packages/react-native-gesture-handler/src/web/handlers/GestureHandler.ts +++ b/packages/react-native-gesture-handler/src/web/handlers/GestureHandler.ts @@ -340,7 +340,10 @@ export default abstract class GestureHandler implements IGestureHandler { // protected onPointerDown(event: AdaptedEvent): void { - GestureHandlerOrchestrator.instance.recordHandlerIfNotPresent(this); + GestureHandlerOrchestrator.instance.recordHandlerIfNotPresent( + this, + event.pointerId + ); this._pointerType = event.pointerType; if (this.pointerType === PointerType.TOUCH) { diff --git a/packages/react-native-gesture-handler/src/web/handlers/__tests__/GestureHandler.test.ts b/packages/react-native-gesture-handler/src/web/handlers/__tests__/GestureHandler.test.ts index dc9e8e693e..8d5abbad6a 100644 --- a/packages/react-native-gesture-handler/src/web/handlers/__tests__/GestureHandler.test.ts +++ b/packages/react-native-gesture-handler/src/web/handlers/__tests__/GestureHandler.test.ts @@ -231,6 +231,103 @@ describe('GestureHandlerOrchestrator idle handlers', () => { }); }); +describe('GestureHandlerOrchestrator recorded handlers passed to a new one', () => { + // Mimics a button: refuses to begin next to any handler it was told about. + class ExclusiveTestGestureHandler extends TestGestureHandler { + public override shouldBeginWithRecordedHandlers( + recorded: IGestureHandler[] + ): boolean { + return recorded.length === 0; + } + } + + const delegate = { + onEnabledChange: jest.fn(), + onCancel: jest.fn(), + reset: jest.fn(), + } as unknown as GestureHandlerDelegate; + + const pointerDown = (pointerId: number): AdaptedEvent => ({ + x: 10, + y: 10, + offsetX: 10, + offsetY: 10, + pointerId, + eventType: EventTypes.DOWN, + pointerType: PointerType.TOUCH, + time: 0, + }); + + function setup() { + const other = new TestGestureHandler(delegate); + const button = new ExclusiveTestGestureHandler(delegate); + other.setGestureConfig({ enabled: true }); + button.setGestureConfig({ enabled: true }); + other.sendEvent = jest.fn(); + button.sendEvent = jest.fn(); + const cancel = jest.spyOn(button, 'cancel'); + + return { other, button, cancel }; + } + + afterEach(() => { + ( + GestureHandlerOrchestrator.instance as unknown as { + gestureHandlers: IGestureHandler[]; + } + ).gestureHandlers = []; + }); + + test('a handler left in BEGAN with no pointers does not block a new one', () => { + const { other, button, cancel } = setup(); + + other.tracker.addToTracker(pointerDown(1)); + GestureHandlerOrchestrator.instance.recordHandlerIfNotPresent(other, 1); + other.begin(); + other.tracker.removeFromTracker(1); + + button.tracker.addToTracker(pointerDown(1)); + GestureHandlerOrchestrator.instance.recordHandlerIfNotPresent(button, 1); + + expect(cancel).not.toHaveBeenCalled(); + }); + + test('a handler tracking another pointer does not block a new one', () => { + const { other, button, cancel } = setup(); + + other.tracker.addToTracker(pointerDown(1)); + GestureHandlerOrchestrator.instance.recordHandlerIfNotPresent(other, 1); + + button.tracker.addToTracker(pointerDown(2)); + GestureHandlerOrchestrator.instance.recordHandlerIfNotPresent(button, 2); + + expect(cancel).not.toHaveBeenCalled(); + }); + + test('a handler tracking the same pointer is passed to the new one', () => { + const { other, button, cancel } = setup(); + + other.tracker.addToTracker(pointerDown(1)); + GestureHandlerOrchestrator.instance.recordHandlerIfNotPresent(other, 1); + + button.tracker.addToTracker(pointerDown(1)); + GestureHandlerOrchestrator.instance.recordHandlerIfNotPresent(button, 1); + + expect(cancel).toHaveBeenCalledTimes(1); + }); + + test('recording without a pointer passes no handlers', () => { + const { other, button, cancel } = setup(); + + other.tracker.addToTracker(pointerDown(1)); + GestureHandlerOrchestrator.instance.recordHandlerIfNotPresent(other, 1); + + GestureHandlerOrchestrator.instance.recordHandlerIfNotPresent(button); + + expect(cancel).not.toHaveBeenCalled(); + }); +}); + describe('GestureHandler web touch events', () => { afterEach(() => { ( diff --git a/packages/react-native-gesture-handler/src/web/tools/GestureHandlerOrchestrator.ts b/packages/react-native-gesture-handler/src/web/tools/GestureHandlerOrchestrator.ts index 732be5432a..58e0d6f567 100644 --- a/packages/react-native-gesture-handler/src/web/tools/GestureHandlerOrchestrator.ts +++ b/packages/react-native-gesture-handler/src/web/tools/GestureHandlerOrchestrator.ts @@ -291,7 +291,10 @@ export default class GestureHandlerOrchestrator { } } - public recordHandlerIfNotPresent(handler: IGestureHandler): void { + public recordHandlerIfNotPresent( + handler: IGestureHandler, + pointerId?: number + ): void { this.dropIdleHandlers(); if (this.isHandlerRecorded(handler)) { @@ -302,7 +305,16 @@ export default class GestureHandlerOrchestrator { handler.awaiting = false; handler.activationIndex = Number.MAX_SAFE_INTEGER; - if (!handler.shouldBeginWithRecordedHandlers(this.gestureHandlers)) { + // Only handlers recorded for the same pointer (they already track it) may + // block this one + const recorded = + pointerId === undefined + ? [] + : this.gestureHandlers.filter((other) => + other.tracker.isTrackingPointer(pointerId) + ); + + if (!handler.shouldBeginWithRecordedHandlers(recorded)) { handler.cancel(); } diff --git a/packages/react-native-gesture-handler/src/web/tools/PointerTracker.ts b/packages/react-native-gesture-handler/src/web/tools/PointerTracker.ts index 263920166c..31cff348f8 100644 --- a/packages/react-native-gesture-handler/src/web/tools/PointerTracker.ts +++ b/packages/react-native-gesture-handler/src/web/tools/PointerTracker.ts @@ -32,8 +32,12 @@ export default class PointerTracker { } } + public isTrackingPointer(pointerId: number): boolean { + return this.trackedPointers.has(pointerId); + } + public addToTracker(event: AdaptedEvent): void { - if (this.trackedPointers.has(event.pointerId)) { + if (this.isTrackingPointer(event.pointerId)) { return; } @@ -121,7 +125,7 @@ export default class PointerTracker { } public getVelocityAt(pointerId: number, time: number) { - if (!this.trackedPointers.has(pointerId)) { + if (!this.isTrackingPointer(pointerId)) { return null; }