From 2ddda6617866d50ba2c461d8d61ccebfd7d250c4 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Wed, 29 Apr 2026 09:25:43 +0200 Subject: [PATCH 1/4] Activate ios buttons on pointer up --- .../apple/Handlers/RNNativeViewHandler.mm | 11 ++++++----- .../src/components/GestureButtons.tsx | 3 ++- .../src/components/touchables/GenericTouchable.tsx | 7 ++----- .../src/v3/components/GestureButtons.tsx | 8 ++++---- 4 files changed, 14 insertions(+), 15 deletions(-) diff --git a/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm b/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm index da03a2565e..51d30657d6 100644 --- a/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm +++ b/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm @@ -275,11 +275,12 @@ - (void)handleTouchUpOutside:(UIView *)sender forEvent:(UIEvent *)event - (void)handleTouchUpInside:(UIView *)sender forEvent:(UIEvent *)event { - [self sendEventsInState:RNGestureHandlerStateEnd - forViewWithTag:sender.reactTag - withExtraData:[RNGestureHandlerEventExtraData forPointerInside:YES - withNumberOfTouches:event.allTouches.count - withPointerType:_pointerType]]; + RNGestureHandlerEventExtraData *extraData = [RNGestureHandlerEventExtraData forPointerInside:YES + withNumberOfTouches:event.allTouches.count + withPointerType:_pointerType]; + + [self sendActiveStateEventIfChangedForView:sender extraData:extraData]; + [self sendEventsInState:RNGestureHandlerStateEnd forViewWithTag:sender.reactTag withExtraData:extraData]; } - (void)handleDragExit:(UIView *)sender forEvent:(UIEvent *)event diff --git a/packages/react-native-gesture-handler/src/components/GestureButtons.tsx b/packages/react-native-gesture-handler/src/components/GestureButtons.tsx index 151e2da48c..3395cb8f4b 100644 --- a/packages/react-native-gesture-handler/src/components/GestureButtons.tsx +++ b/packages/react-native-gesture-handler/src/components/GestureButtons.tsx @@ -50,7 +50,8 @@ class InnerBaseButton extends React.Component { nativeEvent, }: HandlerStateChangeEvent) => { const { state, oldState, pointerInside } = nativeEvent; - const active = pointerInside && state === State.ACTIVE; + const active = + pointerInside && (state === State.BEGAN || state === State.ACTIVE); if (active !== this.lastActive && this.props.onActiveStateChange) { this.props.onActiveStateChange(active); diff --git a/packages/react-native-gesture-handler/src/components/touchables/GenericTouchable.tsx b/packages/react-native-gesture-handler/src/components/touchables/GenericTouchable.tsx index 22dfd99654..5fde5c90f2 100644 --- a/packages/react-native-gesture-handler/src/components/touchables/GenericTouchable.tsx +++ b/packages/react-native-gesture-handler/src/components/touchables/GenericTouchable.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { Component } from 'react'; -import { Animated, Platform } from 'react-native'; +import { Animated } from 'react-native'; import type { GestureEvent, @@ -174,10 +174,7 @@ export default class GenericTouchable extends Component< // Need to handle case with external cancellation (e.g. by ScrollView) this.moveToState(TOUCHABLE_STATE.UNDETERMINED); } else if ( - // This platform check is an implication of slightly different behavior of handlers on different platform. - // And Android "Active" state is achieving on first move of a finger, not on press in. - // On iOS event on "Began" is not delivered. - state === (Platform.OS !== 'android' ? State.ACTIVE : State.BEGAN) && + state === State.BEGAN && this.STATE === TOUCHABLE_STATE.UNDETERMINED ) { // Moving inside requires diff --git a/packages/react-native-gesture-handler/src/v3/components/GestureButtons.tsx b/packages/react-native-gesture-handler/src/v3/components/GestureButtons.tsx index fb9caab3b3..b7e088b39e 100644 --- a/packages/react-native-gesture-handler/src/v3/components/GestureButtons.tsx +++ b/packages/react-native-gesture-handler/src/v3/components/GestureButtons.tsx @@ -69,16 +69,16 @@ export const BaseButton = (props: BaseButtonProps) => { }; const onDeactivate = (e: EndCallbackEventType) => { + props.onDeactivate?.(e); + }; + + const onFinalize = (e: EndCallbackEventType) => { onActiveStateChange?.(false); if (!e.canceled && !longPressDetected.current) { onPress?.(e.pointerInside); } - props.onDeactivate?.(e); - }; - - const onFinalize = (e: EndCallbackEventType) => { if (longPressTimeout.current !== undefined) { clearTimeout(longPressTimeout.current); longPressTimeout.current = undefined; From 276beec223a989b8afcbacad00118e28207ef886 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Wed, 29 Apr 2026 10:28:38 +0200 Subject: [PATCH 2/4] Fix rebase --- .../apple/Handlers/RNNativeViewHandler.mm | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm b/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm index 51d30657d6..d54be9db61 100644 --- a/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm +++ b/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm @@ -253,11 +253,6 @@ - (void)handleTouchDown:(UIView *)sender forEvent:(UIEvent *)event withExtraData:[RNGestureHandlerEventExtraData forPointerInside:YES withNumberOfTouches:event.allTouches.count withPointerType:_pointerType]]; - - [self sendActiveStateEventIfChangedForView:sender - extraData:[RNGestureHandlerEventExtraData forPointerInside:YES - withNumberOfTouches:event.allTouches.count - withPointerType:_pointerType]]; } - (void)handleTouchUpOutside:(UIView *)sender forEvent:(UIEvent *)event From 93d71acc4cdab129ee287b5ea5850bd58e3b3bbb Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Wed, 29 Apr 2026 10:28:53 +0200 Subject: [PATCH 3/4] Update comment --- .../src/__tests__/api_v3.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsx b/packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsx index e579863201..aba80c8a8f 100644 --- a/packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsx +++ b/packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsx @@ -134,7 +134,7 @@ describe('[API v3] Components', () => { >; const { jsEventHandler } = gesture.detectorCallbacks; - // Fire BEGAN + // Fire BEGAN — long press timer starts here act(() => { jsEventHandler?.({ oldState: State.UNDETERMINED, @@ -145,7 +145,7 @@ describe('[API v3] Components', () => { }); }); - // Fire ACTIVE — long press timer starts here (on iOS / non-Android) + // Fire ACTIVE act(() => { jsEventHandler?.({ oldState: State.BEGAN, From 575f6eee87e9cd25d8fd5c4a7d12b3d0472f9d04 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Wed, 29 Apr 2026 12:43:16 +0200 Subject: [PATCH 4/4] Rename local var --- .../src/components/GestureButtons.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/react-native-gesture-handler/src/components/GestureButtons.tsx b/packages/react-native-gesture-handler/src/components/GestureButtons.tsx index 3395cb8f4b..aeef28dca5 100644 --- a/packages/react-native-gesture-handler/src/components/GestureButtons.tsx +++ b/packages/react-native-gesture-handler/src/components/GestureButtons.tsx @@ -36,13 +36,13 @@ class InnerBaseButton extends React.Component { delayLongPress: 600, }; - private lastActive: boolean; + private lastIsPressed: boolean; private longPressTimeout: ReturnType | undefined; private longPressDetected: boolean; constructor(props: BaseButtonWithRefProps) { super(props); - this.lastActive = false; + this.lastIsPressed = false; this.longPressDetected = false; } @@ -50,24 +50,24 @@ class InnerBaseButton extends React.Component { nativeEvent, }: HandlerStateChangeEvent) => { const { state, oldState, pointerInside } = nativeEvent; - const active = + const isPressed = pointerInside && (state === State.BEGAN || state === State.ACTIVE); - if (active !== this.lastActive && this.props.onActiveStateChange) { - this.props.onActiveStateChange(active); + if (isPressed !== this.lastIsPressed && this.props.onActiveStateChange) { + this.props.onActiveStateChange(isPressed); } if ( !this.longPressDetected && oldState === State.ACTIVE && state !== State.CANCELLED && - this.lastActive && + this.lastIsPressed && this.props.onPress ) { this.props.onPress(pointerInside); } - if (!this.lastActive && state === State.BEGAN && pointerInside) { + if (!this.lastIsPressed && state === State.BEGAN && pointerInside) { this.longPressDetected = false; if (this.props.onLongPress) { this.longPressTimeout = setTimeout( @@ -94,7 +94,7 @@ class InnerBaseButton extends React.Component { this.longPressTimeout = undefined; } - this.lastActive = active; + this.lastIsPressed = isPressed; }; private onLongPress = () => {