From 648719a0483e39b7c68e949d44dc7a2e47e3ad13 Mon Sep 17 00:00:00 2001 From: tlauw Date: Thu, 1 Oct 2026 12:54:20 -0700 Subject: [PATCH 1/3] [Web] Align Tap maxDelay default with native (200 ms) --- .../docs/gestures/use-tap-gesture.mdx | 2 +- .../src/handlers/TapGestureHandler.ts | 2 +- .../src/v3/hooks/gestures/tap/TapTypes.ts | 2 +- .../src/web/handlers/TapGestureHandler.ts | 2 +- .../__tests__/TapGestureHandler.test.ts | 124 ++++++++++++++++++ 5 files changed, 128 insertions(+), 4 deletions(-) create mode 100644 packages/react-native-gesture-handler/src/web/handlers/__tests__/TapGestureHandler.test.ts diff --git a/packages/docs-gesture-handler/docs/gestures/use-tap-gesture.mdx b/packages/docs-gesture-handler/docs/gestures/use-tap-gesture.mdx index b12b97362d..fc48ad2f3d 100644 --- a/packages/docs-gesture-handler/docs/gestures/use-tap-gesture.mdx +++ b/packages/docs-gesture-handler/docs/gestures/use-tap-gesture.mdx @@ -109,7 +109,7 @@ Maximum time, expressed in milliseconds, that defines how fast a finger must be maxDelay: number | SharedValue; ``` -Maximum time, expressed in milliseconds, that can pass before the next tap — if many taps are required. The default value is 500. +Maximum time, expressed in milliseconds, that can pass before the next tap — if many taps are required. The default value is 200. ### numberOfTaps diff --git a/packages/react-native-gesture-handler/src/handlers/TapGestureHandler.ts b/packages/react-native-gesture-handler/src/handlers/TapGestureHandler.ts index c6816bcec9..d46c3e30ad 100644 --- a/packages/react-native-gesture-handler/src/handlers/TapGestureHandler.ts +++ b/packages/react-native-gesture-handler/src/handlers/TapGestureHandler.ts @@ -29,7 +29,7 @@ export interface TapGestureConfig { /** * Maximum time, expressed in milliseconds, that can pass before the next tap - * if many taps are required. The default value is 500. + * if many taps are required. The default value is 200. */ maxDelayMs?: number; diff --git a/packages/react-native-gesture-handler/src/v3/hooks/gestures/tap/TapTypes.ts b/packages/react-native-gesture-handler/src/v3/hooks/gestures/tap/TapTypes.ts index c9ccb8ad01..9ba4844979 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/gestures/tap/TapTypes.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/gestures/tap/TapTypes.ts @@ -46,7 +46,7 @@ export type TapGestureExternalConfig = CommonTapGestureConfig & { /** * Maximum time, expressed in milliseconds, that can pass before the next tap - * if many taps are required. The default value is 500. + * if many taps are required. The default value is 200. */ maxDelay?: number; diff --git a/packages/react-native-gesture-handler/src/web/handlers/TapGestureHandler.ts b/packages/react-native-gesture-handler/src/web/handlers/TapGestureHandler.ts index 080f3a4c08..c2187379ee 100644 --- a/packages/react-native-gesture-handler/src/web/handlers/TapGestureHandler.ts +++ b/packages/react-native-gesture-handler/src/web/handlers/TapGestureHandler.ts @@ -7,7 +7,7 @@ import GestureHandler from './GestureHandler'; import type IGestureHandler from './IGestureHandler'; const DEFAULT_MAX_DURATION_MS = 500; -const DEFAULT_MAX_DELAY_MS = 500; +const DEFAULT_MAX_DELAY_MS = 200; const DEFAULT_NUMBER_OF_TAPS = 1; const DEFAULT_MIN_NUMBER_OF_POINTERS = 1; diff --git a/packages/react-native-gesture-handler/src/web/handlers/__tests__/TapGestureHandler.test.ts b/packages/react-native-gesture-handler/src/web/handlers/__tests__/TapGestureHandler.test.ts new file mode 100644 index 0000000000..898ba5d5e2 --- /dev/null +++ b/packages/react-native-gesture-handler/src/web/handlers/__tests__/TapGestureHandler.test.ts @@ -0,0 +1,124 @@ +import { ActionType } from '../../../ActionType'; +import { PointerType } from '../../../PointerType'; +import type { AdaptedEvent } from '../../interfaces'; +import { EventTypes } from '../../interfaces'; +import type { GestureHandlerDelegate } from '../../tools/GestureHandlerDelegate'; +import GestureHandlerOrchestrator from '../../tools/GestureHandlerOrchestrator'; +import type IGestureHandler from '../IGestureHandler'; +import TapGestureHandler from '../TapGestureHandler'; + +class TestTapGestureHandler extends TapGestureHandler { + public pointerDown(event: AdaptedEvent): void { + this.onPointerDown(event); + } + + public pointerUp(event: AdaptedEvent): void { + this.onPointerUp(event); + } +} + +function touchEvent(eventType: EventTypes): AdaptedEvent { + return { + x: 100, + y: 100, + offsetX: 100, + offsetY: 100, + pointerId: 0, + eventType, + pointerType: PointerType.TOUCH, + time: 0, + }; +} + +function createHandler() { + const onFail = jest.fn(); + const delegate = { + init: jest.fn(), + detach: jest.fn(), + reset: jest.fn(), + onActivate: jest.fn(), + onFail, + onCancel: jest.fn(), + onEnd: jest.fn(), + onEnabledChange: jest.fn(), + updateDOM: jest.fn(), + } as unknown as GestureHandlerDelegate; + + const handler = new TestTapGestureHandler(delegate); + handler.init(1, { current: {} } as never, ActionType.JS_FUNCTION_OLD_API); + + // The full event pipeline is not under test, silence event emission. + handler.sendEvent = jest.fn(); + + return { handler, onFail }; +} + +// Lands the first tap of a two tap gesture and lets `ms` pass without a second one. +function tapOnceAndWait(handler: TestTapGestureHandler, ms: number) { + handler.pointerDown(touchEvent(EventTypes.DOWN)); + handler.pointerUp(touchEvent(EventTypes.UP)); + + jest.advanceTimersByTime(ms); +} + +describe('TapGestureHandler maxDelay', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + + // The orchestrator is a singleton, drop handlers recorded by the test. + ( + GestureHandlerOrchestrator.instance as unknown as { + gestureHandlers: IGestureHandler[]; + } + ).gestureHandlers = []; + }); + + test('defaults to 200 ms, matching Android and iOS', () => { + const { handler, onFail } = createHandler(); + + handler.setGestureConfig({ enabled: true, numberOfTaps: 2 }); + tapOnceAndWait(handler, 300); + + expect(onFail).toHaveBeenCalledTimes(1); + }); + + test('keeps waiting for the next tap within the default delay', () => { + const { handler, onFail } = createHandler(); + + handler.setGestureConfig({ enabled: true, numberOfTaps: 2 }); + tapOnceAndWait(handler, 100); + + expect(onFail).not.toHaveBeenCalled(); + }); + + test('an explicit maxDelayMs overrides the default', () => { + const { handler, onFail } = createHandler(); + + handler.setGestureConfig({ + enabled: true, + numberOfTaps: 2, + maxDelayMs: 500, + }); + tapOnceAndWait(handler, 300); + + expect(onFail).not.toHaveBeenCalled(); + }); + + test('a config without maxDelayMs restores the default', () => { + const { handler, onFail } = createHandler(); + + handler.setGestureConfig({ + enabled: true, + numberOfTaps: 2, + maxDelayMs: 500, + }); + handler.setGestureConfig({ enabled: true, numberOfTaps: 2 }); + tapOnceAndWait(handler, 300); + + expect(onFail).toHaveBeenCalledTimes(1); + }); +}); From 97e50ecd6e2748f1f993f0bc4bd5b8bd8f1e1683 Mon Sep 17 00:00:00 2001 From: tlauw Date: Fri, 2 Oct 2026 06:56:33 -0700 Subject: [PATCH 2/3] Remove the TapGestureHandler test file --- .../__tests__/TapGestureHandler.test.ts | 124 ------------------ 1 file changed, 124 deletions(-) delete mode 100644 packages/react-native-gesture-handler/src/web/handlers/__tests__/TapGestureHandler.test.ts diff --git a/packages/react-native-gesture-handler/src/web/handlers/__tests__/TapGestureHandler.test.ts b/packages/react-native-gesture-handler/src/web/handlers/__tests__/TapGestureHandler.test.ts deleted file mode 100644 index 898ba5d5e2..0000000000 --- a/packages/react-native-gesture-handler/src/web/handlers/__tests__/TapGestureHandler.test.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { ActionType } from '../../../ActionType'; -import { PointerType } from '../../../PointerType'; -import type { AdaptedEvent } from '../../interfaces'; -import { EventTypes } from '../../interfaces'; -import type { GestureHandlerDelegate } from '../../tools/GestureHandlerDelegate'; -import GestureHandlerOrchestrator from '../../tools/GestureHandlerOrchestrator'; -import type IGestureHandler from '../IGestureHandler'; -import TapGestureHandler from '../TapGestureHandler'; - -class TestTapGestureHandler extends TapGestureHandler { - public pointerDown(event: AdaptedEvent): void { - this.onPointerDown(event); - } - - public pointerUp(event: AdaptedEvent): void { - this.onPointerUp(event); - } -} - -function touchEvent(eventType: EventTypes): AdaptedEvent { - return { - x: 100, - y: 100, - offsetX: 100, - offsetY: 100, - pointerId: 0, - eventType, - pointerType: PointerType.TOUCH, - time: 0, - }; -} - -function createHandler() { - const onFail = jest.fn(); - const delegate = { - init: jest.fn(), - detach: jest.fn(), - reset: jest.fn(), - onActivate: jest.fn(), - onFail, - onCancel: jest.fn(), - onEnd: jest.fn(), - onEnabledChange: jest.fn(), - updateDOM: jest.fn(), - } as unknown as GestureHandlerDelegate; - - const handler = new TestTapGestureHandler(delegate); - handler.init(1, { current: {} } as never, ActionType.JS_FUNCTION_OLD_API); - - // The full event pipeline is not under test, silence event emission. - handler.sendEvent = jest.fn(); - - return { handler, onFail }; -} - -// Lands the first tap of a two tap gesture and lets `ms` pass without a second one. -function tapOnceAndWait(handler: TestTapGestureHandler, ms: number) { - handler.pointerDown(touchEvent(EventTypes.DOWN)); - handler.pointerUp(touchEvent(EventTypes.UP)); - - jest.advanceTimersByTime(ms); -} - -describe('TapGestureHandler maxDelay', () => { - beforeEach(() => { - jest.useFakeTimers(); - }); - - afterEach(() => { - jest.useRealTimers(); - - // The orchestrator is a singleton, drop handlers recorded by the test. - ( - GestureHandlerOrchestrator.instance as unknown as { - gestureHandlers: IGestureHandler[]; - } - ).gestureHandlers = []; - }); - - test('defaults to 200 ms, matching Android and iOS', () => { - const { handler, onFail } = createHandler(); - - handler.setGestureConfig({ enabled: true, numberOfTaps: 2 }); - tapOnceAndWait(handler, 300); - - expect(onFail).toHaveBeenCalledTimes(1); - }); - - test('keeps waiting for the next tap within the default delay', () => { - const { handler, onFail } = createHandler(); - - handler.setGestureConfig({ enabled: true, numberOfTaps: 2 }); - tapOnceAndWait(handler, 100); - - expect(onFail).not.toHaveBeenCalled(); - }); - - test('an explicit maxDelayMs overrides the default', () => { - const { handler, onFail } = createHandler(); - - handler.setGestureConfig({ - enabled: true, - numberOfTaps: 2, - maxDelayMs: 500, - }); - tapOnceAndWait(handler, 300); - - expect(onFail).not.toHaveBeenCalled(); - }); - - test('a config without maxDelayMs restores the default', () => { - const { handler, onFail } = createHandler(); - - handler.setGestureConfig({ - enabled: true, - numberOfTaps: 2, - maxDelayMs: 500, - }); - handler.setGestureConfig({ enabled: true, numberOfTaps: 2 }); - tapOnceAndWait(handler, 300); - - expect(onFail).toHaveBeenCalledTimes(1); - }); -}); From b75a6a6e11ecb5301f6c8edc6ea2ecf4d29e6938 Mon Sep 17 00:00:00 2001 From: tlauw Date: Fri, 2 Oct 2026 06:58:59 -0700 Subject: [PATCH 3/3] Update the Tap maxDelay default in the v2 builder comment --- .../src/handlers/gestures/tapGesture.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-native-gesture-handler/src/handlers/gestures/tapGesture.ts b/packages/react-native-gesture-handler/src/handlers/gestures/tapGesture.ts index 735306da80..b4ac378f87 100644 --- a/packages/react-native-gesture-handler/src/handlers/gestures/tapGesture.ts +++ b/packages/react-native-gesture-handler/src/handlers/gestures/tapGesture.ts @@ -58,7 +58,7 @@ export class TapGesture extends BaseGesture { /** * Maximum time, expressed in milliseconds, that can pass before the next tap — if many taps are required. - * The default value is 500. + * The default value is 200. * @param delay */ maxDelay(delay: number) {