From 8bac43cdb245929e69d34ddbf295a93f695bb7c4 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Fri, 4 Sep 2026 12:52:26 +0200 Subject: [PATCH] [Web] Reset NativeViewGestureHandler config on a full config replace ## Description The web NativeViewGestureHandler did not override resetConfig, so its props kept old values after setGestureConfig dropped them. It also forced shouldCancelWhenOutside to true in init, overriding an explicit false. The defaults now live in resetConfig, matching Android. ## Test plan Added tests in `webNativeViewGestureHandler.test.ts`. --- .../webNativeViewGestureHandler.test.ts | 75 ++++++++++++++++++- .../web/handlers/NativeViewGestureHandler.ts | 12 ++- 2 files changed, 83 insertions(+), 4 deletions(-) diff --git a/packages/react-native-gesture-handler/src/__tests__/webNativeViewGestureHandler.test.ts b/packages/react-native-gesture-handler/src/__tests__/webNativeViewGestureHandler.test.ts index 14db42919d..5190134946 100644 --- a/packages/react-native-gesture-handler/src/__tests__/webNativeViewGestureHandler.test.ts +++ b/packages/react-native-gesture-handler/src/__tests__/webNativeViewGestureHandler.test.ts @@ -4,7 +4,7 @@ import { State } from '../State'; import { NATIVE_GESTURE_ROLE_ATTRIBUTE } from '../web/constants'; import type IGestureHandler from '../web/handlers/IGestureHandler'; import NativeViewGestureHandler from '../web/handlers/NativeViewGestureHandler'; -import type { AdaptedEvent } from '../web/interfaces'; +import type { AdaptedEvent, Config } from '../web/interfaces'; import { EventTypes, NativeGestureRole } from '../web/interfaces'; import type { GestureHandlerDelegate } from '../web/tools/GestureHandlerDelegate'; import GestureHandlerOrchestrator from '../web/tools/GestureHandlerOrchestrator'; @@ -78,7 +78,10 @@ function touchEvent(x: number, y: number, eventType: EventTypes): AdaptedEvent { }; } -function createHandler(view: FakeHTMLElement) { +function createHandler( + view: FakeHTMLElement, + config: Config = { enabled: true } +) { const delegate = { view, init: jest.fn(), @@ -93,7 +96,7 @@ function createHandler(view: FakeHTMLElement) { } as unknown as GestureHandlerDelegate; const handler = new TestNativeViewGestureHandler(delegate); - handler.setGestureConfig({ enabled: true }); + handler.setGestureConfig(config); handler.init(1, { current: {} } as never, ActionType.NATIVE_DETECTOR); // Route scroll events the same way the real delegate does. @@ -213,3 +216,69 @@ describe('NativeViewGestureHandler activation', () => { expect(handler.state).toBe(State.ACTIVE); }); }); + +describe('NativeViewGestureHandler config reset', () => { + afterEach(() => { + ( + GestureHandlerOrchestrator.instance as unknown as { + gestureHandlers: IGestureHandler[]; + } + ).gestureHandlers = []; + }); + + function createButtonHandler(config?: Config) { + const view = new FakeHTMLElement(); + view.setAttribute(NATIVE_GESTURE_ROLE_ATTRIBUTE, NativeGestureRole.Button); + return createHandler(view, config); + } + + test('shouldActivateOnStart activates a button on pointer down', () => { + const handler = createButtonHandler({ + enabled: true, + shouldActivateOnStart: true, + }); + + handler.pointerDown(touchEvent(100, 100, EventTypes.DOWN)); + expect(handler.state).toBe(State.ACTIVE); + }); + + test('a config without shouldActivateOnStart restores the default', () => { + const handler = createButtonHandler({ + enabled: true, + shouldActivateOnStart: true, + }); + handler.setGestureConfig({ enabled: true }); + + handler.pointerDown(touchEvent(100, 100, EventTypes.DOWN)); + expect(handler.state).toBe(State.BEGAN); + }); + + test('a config without disallowInterruption restores the default', () => { + const handler = createButtonHandler({ + enabled: true, + disallowInterruption: true, + }); + handler.setGestureConfig({ enabled: true }); + + expect(handler.disallowsInterruption()).toBe(false); + }); + + test('a config without shouldCancelWhenOutside restores the native view default', () => { + const handler = createButtonHandler({ + enabled: true, + shouldCancelWhenOutside: false, + }); + handler.setGestureConfig({ enabled: true }); + + expect(handler.shouldCancelWhenOutside).toBe(true); + }); + + test('an explicit shouldCancelWhenOutside survives init', () => { + const handler = createButtonHandler({ + enabled: true, + shouldCancelWhenOutside: false, + }); + + expect(handler.shouldCancelWhenOutside).toBe(false); + }); +}); diff --git a/packages/react-native-gesture-handler/src/web/handlers/NativeViewGestureHandler.ts b/packages/react-native-gesture-handler/src/web/handlers/NativeViewGestureHandler.ts index 20176a665d..95a31d8dcf 100644 --- a/packages/react-native-gesture-handler/src/web/handlers/NativeViewGestureHandler.ts +++ b/packages/react-native-gesture-handler/src/web/handlers/NativeViewGestureHandler.ts @@ -69,7 +69,6 @@ export default class NativeViewGestureHandler extends GestureHandler { ): void { super.init(ref, propsRef, actionType, hostDetector); - this.shouldCancelWhenOutside = true; this.isScrollDriven = false; const view = this.delegate.view; @@ -121,6 +120,17 @@ export default class NativeViewGestureHandler extends GestureHandler { } } + protected override resetConfig(): void { + super.resetConfig(); + + this.shouldCancelWhenOutside = true; + this.shouldActivateOnStart = false; + this.disallowInterruption = false; + this.yieldsToContinuousGestures = false; + this.hasLongPressHandler = false; + this.longPressDuration = -1; + } + private restoreViewStyles(view: HTMLElement | SVGElement) { if (!view) { return;