Skip to content

Make buttons activate immediately#4036

Open
m-bert wants to merge 13 commits intomainfrom
@mbert/unify-native-handler-callbacks
Open

Make buttons activate immediately#4036
m-bert wants to merge 13 commits intomainfrom
@mbert/unify-native-handler-callbacks

Conversation

@m-bert
Copy link
Contributor

@m-bert m-bert commented Mar 23, 2026

Description

Native gesture is specific and its behavior differs across platforms. This leads to strange workarounds in our codebase (e.g. buttons).

In this PR unifies buttons behavior by changing Native gesture.

Test plan

Tested on expo-example app (buttons / Pressable)

Copilot AI review requested due to automatic review settings March 23, 2026 09:30
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adjusts iOS Native gesture event/state emission to better align with Android behavior, reducing platform-specific workarounds in JS components (notably Pressable/buttons).

Changes:

  • Update v3 Pressable to avoid using Native.onActivate on iOS and to always run finalize cleanup.
  • Simplify v3 GestureButtons long-press scheduling to consistently start from onBegin (removing platform branching).
  • Modify iOS RNNativeViewHandler to emit BEGAN on touch-down and to forward additional drag-inside/outside pointer updates; update Pressable iOS state machine config accordingly.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
packages/react-native-gesture-handler/src/v3/components/Pressable.tsx Adjusts iOS handling to rely on onBegin/touch-down tracking instead of onActivate, and unifies finalize cleanup.
packages/react-native-gesture-handler/src/v3/components/GestureButtons.tsx Removes platform-specific workaround by starting long-press logic from onBegin for all platforms.
packages/react-native-gesture-handler/src/components/Pressable/stateDefinitions.ts Switches iOS Pressable state machine to trigger handlePressIn on NATIVE_BEGIN instead of NATIVE_START.
packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm Changes iOS UIControl-based native handler state emission (touch-down now BEGAN) and adds drag-inside/outside event forwarding.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +193 to 196
[self sendEventsInState:RNGestureHandlerStateBegan
forViewWithTag:sender.reactTag
withExtraData:[RNGestureHandlerEventExtraData forPointerInside:YES
withNumberOfTouches:event.allTouches.count
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handleTouchDown now emits RNGestureHandlerStateBegan instead of ...StateActive. With the current JS button implementations (both legacy src/components/GestureButtons.tsx and v3 src/v3/components/GestureButtons.tsx), the “pressed/active” UI feedback is driven by the gesture entering ACTIVE while the finger is down. If iOS stays in BEGAN until drag/finish, onActiveStateChange(true)/pressed underlay/opacity won’t update on touch down (it may only flash at release due to the synthetic ACTIVE sent right before END in sendEventsInState). If the intent is to start sending BEGAN for parity but keep pressed feedback, consider sending BEGAN and then immediately ACTIVE on touch down (or otherwise ensuring iOS enters ACTIVE at press start).

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@m-bert m-bert marked this pull request as ready for review March 23, 2026 11:52
@m-bert m-bert requested a review from j-piasecki March 23, 2026 11:52
Comment on lines +193 to 195
[self sendEventsInState:RNGestureHandlerStateBegan
forViewWithTag:sender.reactTag
withExtraData:[RNGestureHandlerEventExtraData forPointerInside:YES
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, maybe the "correct" approach would be to send begin and active immediately when pressed down (and do the same on other platforms)?

This may be weird that the button isn't "active" unless the pointer moves.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also thought about it. This would also match current web logic.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So are we doing this or no?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we are (ad05004)

@m-bert m-bert requested a review from j-piasecki March 24, 2026 15:55
@m-bert m-bert changed the title [iOS] Change when Native gesture states are changed Make Buttons activate immediately Mar 25, 2026
@m-bert m-bert changed the title Make Buttons activate immediately Make buttons activate immediately Mar 25, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (2)

packages/react-native-gesture-handler/src/components/Pressable/stateDefinitions.ts:66

  • getIosStatesConfig now expects LONG_PRESS_TOUCHES_DOWN before NATIVE_BEGIN so it can reuse the touch payload for handlePressIn. However, in v3 Pressable the NATIVE_BEGIN event is emitted from the native gesture’s onBegin (no payload) and can occur before the long-press onTouchesDown. If that happens, the state machine will never reach this callback and handlePressIn won’t fire. To make this robust, either (a) emit NATIVE_BEGIN with a payload, or (b) reorder the iOS steps so NATIVE_BEGIN is first and handlePressIn is triggered on the event that reliably carries a payload.
  return [
    {
      eventName: StateMachineEvent.LONG_PRESS_TOUCHES_DOWN,
    },
    {
      eventName: StateMachineEvent.NATIVE_BEGIN,
      callback: handlePressIn,
    },
    {
      eventName: StateMachineEvent.FINALIZE,
      callback: handlePressOut,
    },

packages/react-native-gesture-handler/src/v3/components/GestureButtons.tsx:62

  • onBegin now starts the long-press timeout and sets the active/pressed state, but there’s no handler that reacts to subsequent pointerInside changes while the NativeViewGestureHandler stays ACTIVE (iOS drag in/out sends ACTIVE events without a state change). As a result, dragging outside and holding can still fire onLongPress, and onActiveStateChange(false) won’t run until finalize. Consider handling pointerInside updates via onUpdate (or another callback that receives continuous ACTIVE events) to clear the timeout and update active state when the pointer leaves/enters.
  const onBegin = (e: CallbackEventType) => {
    if (!e.pointerInside) {
      return;
    }

    onActiveStateChange?.(true);

    longPressDetected.current = false;
    if (onLongPress) {
      longPressTimeout.current = setTimeout(wrappedLongPress, delayLongPress);
    }

    props.onBegin?.(e);
  };

  const onActivate = (e: CallbackEventType) => {
    if (!e.pointerInside && longPressTimeout.current !== undefined) {
      clearTimeout(longPressTimeout.current);
      longPressTimeout.current = undefined;
    }

    props.onActivate?.(e);
  };

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 307 to 315
onBegin: () => {
stateMachine.handleEvent(StateMachineEvent.NATIVE_BEGIN);
},
onActivate: () => {
if (Platform.OS !== 'android') {
if (Platform.OS !== 'android' && Platform.OS !== 'ios') {
// Native.onActivate is broken with Android + hitSlop
// On iOS, onActivate fires on drag (not touch down), so we use onBegin + LONG_PRESS_TOUCHES_DOWN instead
stateMachine.handleEvent(StateMachineEvent.NATIVE_START);
}
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On iOS, the state machine now uses NATIVE_BEGIN to trigger handlePressIn, but buttonGesture.onBegin calls stateMachine.handleEvent(NATIVE_BEGIN) without providing a PressableEvent payload. If NATIVE_BEGIN arrives before LONG_PRESS_TOUCHES_DOWN (as it does on Android), the state machine will consume the begin step with no payload and handlePressIn will never run, which can suppress onPressIn/pressed state on iOS. Consider passing an event payload into NATIVE_BEGIN (e.g. derive one from the onBegin event via gestureToPressableEvent) or adjusting the iOS state sequence so it can’t miss the begin event.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants