SIGN IN SIGN UP

[macOS] Fix `manualActivation` never blocking gesture activation (#4389)

## Description

`manualActivation: true` has never worked on macOS — the handler
activated on its own as if the flag wasn't set. Found while verifying a
review comment on #4387. Four stacked defects in
`RNManualActivationRecognizer`, all macOS-specific:

1. The failure requirement was never established. The blocking mechanism
relies on `shouldBeRequiredToFailByGestureRecognizer:`, which is a
`UIGestureRecognizer` *subclass* hook (`UIGestureRecognizerSubclass.h`).
`NSGestureRecognizer` has no such hook, so AppKit never called it and
the handler's recognizer never waited for the blocker — a pan activated
straight from mouse-down. AppKit only consults the *delegate*; since the
blocker is already its own `NSGestureRecognizerDelegate`, the two-arg
delegate callback now forwards to the shared logic.

2. AppKit does not reliably deny the dependent recognizer when the
blocker recognizes. On iOS, the blocker completing (`Began` → `Ended` in
its action handler) causes UIKit to fail the recognizer that required it
to fail — that's how a released-without-`activate()` gesture is
discarded. On AppKit, completing the blocker can instead *flush* the
dependent recognizer's buffered recognition: with the requirement in
place, releasing after a drag delivered the pan's entire withheld
sequence (`Began`/`Changed`/`Ended` with the full accumulated
translation) instead of discarding it, while a motionless press-release
was discarded correctly (traced with state-transition logging). The
blocker now explicitly fails the handler's recognizer before completing.

3. The release-without-activation cleanup was dead code. The original
macOS port (#2588) crossed the `touchesBegan`/`touchesEnded` bodies:
`mouseDown` incremented `_activePointers` and then checked `if
(_activePointers == 0)` — never true after an increment — while
`mouseUp` only decremented. The zero-check now lives in `mouseUp`,
mirroring `touchesEnded` on iOS.

4. Stale pointer count after a JS `activate()`. `stopActivationBlocker`
disables the blocker, so it misses the subsequent mouse-up; iOS recovers
in `touchesCancelled`, which has no AppKit equivalent. The count would
stay at 1 and the cleanup in (3) would never fire again. `reset` now
zeroes `_activePointers`.


## Test plan

<details>
<summary>Tested on the following code:</summary>

```tsx
import React, { useEffect } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import {
  GestureDetector,
  GestureStateManager,
  usePanGesture,
} from 'react-native-gesture-handler';
import { useSharedValue } from 'react-native-reanimated';

export default function EmptyExample() {
  // Box A: manualActivation, JS never calls activate().
  // Expected: every press/drag/release cycle logs onBegin -> onFinalize canceled=true,
  // never onActivate, and behaves identically on every repeat.
  const neverActivatePan = usePanGesture({
    manualActivation: true,
    onBegin: () => console.log('[never-activate] onBegin'),
    onActivate: () => console.log('[never-activate] onActivate (SHOULD NOT HAPPEN)'),
    onDeactivate: () => console.log('[never-activate] onDeactivate'),
    onFinalize: (e) => console.log(`[never-activate] onFinalize canceled=${e.canceled}`),
  });

  // Box B: manualActivation, activates itself from a timer while the button is held
  // (onTouchesMove is not delivered on macOS — separate bug).
  // Expected: hold past 300 ms -> onActivate (tx ~0), onUpdate while dragging,
  // onDeactivate + onFinalize canceled=false on release. Release before 300 ms ->
  // canceled=true and the late activate() is a no-op.
  const selfTag = useSharedValue(-1);
  const selfActivatePan = usePanGesture({
    manualActivation: true,
    onTouchesDown: () => {
      const tag = selfTag.value;
      setTimeout(() => {
        console.log(`[self-activate] calling activate(${tag})`);
        GestureStateManager.activate(tag);
      }, 300);
    },
    onBegin: () => console.log('[self-activate] onBegin'),
    onActivate: (e) =>
      console.log(`[self-activate] onActivate tx=${e.translationX.toFixed(1)} ty=${e.translationY.toFixed(1)}`),
    onUpdate: (e) =>
      console.log(`[self-activate] onUpdate tx=${e.translationX.toFixed(1)} ty=${e.translationY.toFixed(1)}`),
    onDeactivate: () => console.log('[self-activate] onDeactivate'),
    onFinalize: (e) => console.log(`[self-activate] onFinalize canceled=${e.canceled}`),
  });

  useEffect(() => {
    selfTag.value = selfActivatePan.handlerTag;
  }, [selfActivatePan.handlerTag, selfTag]);

  return (
    <View style={styles.container}>
      <Text style={styles.label}>A: manualActivation, never activated — click & drag, release</Text>
      <GestureDetector gesture={neverActivatePan}>
        <View style={[styles.box, { backgroundColor: 'darkorange' }]} />
      </GestureDetector>

      <Text style={styles.label}>B: manualActivation, self-activates 300 ms after press</Text>
      <GestureDetector gesture={selfActivatePan}>
        <View style={[styles.box, { backgroundColor: 'mediumpurple' }]} />
      </GestureDetector>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: 'center', alignItems: 'center', gap: 8 },
  label: { marginTop: 16, fontSize: 15, opacity: 0.6 },
  box: { width: 150, height: 150, borderRadius: 12 },
});
```

</details>
M
Michał Bert committed
10235ad6011b4c5ab2ceb8bdd1e2ef5723f21151
Parent: 6819385
Committed by GitHub <noreply@github.com> on 8/6/2026, 10:52:04 AM