SIGN IN SIGN UP

[macOS] Fix `Pan` activation criteria being ignored (#4387)

## Description

On macOS, all of `Pan`'s custom activation criteria — `minDistance`,
`activeOffsetX/Y`, `minVelocity(X/Y)` — were silently ignored: the
handler activated the moment the mouse button went down.

Two things combined to cause this:

1. Both custom-activation code paths in `RNPanHandler.m` were compiled
out for macOS. The early-activation guard in `interactionsBegan` and the
`shouldActivateUnderCustomCriteria` check in `interactionsMoved` were
wrapped in `#if !TARGET_OS_TV && !TARGET_OS_OSX`, so the config values
were stored but never evaluated. (Only the fail criteria worked —
`shouldFailUnderCustomCriteria` runs unconditionally.)
2. `NSPanGestureRecognizer` begins on mouse-down. Unlike
`UIPanGestureRecognizer`, which has a built-in ~10 pt hysteresis, the
AppKit recognizer transitions to `Began` immediately, so without the
check there is nothing preventing a click from activating the handler.

The iOS implementation holds the recognizer back with the
`minimumNumberOfTouches = 20` trick, which has no AppKit equivalent.
Instead, on macOS the recognizer is held in the `Possible` state
explicitly:

- When a mouse-down arrives and custom activation criteria are
configured, a `_blockAutomaticActivation` flag is set.
- A `setState:` override swallows the superclass's `Began`/`Changed`
transitions while the flag is up. The superclass keeps receiving events,
so `translationInView:` / `velocityInView:` stay valid — which also
keeps `failOffsetX/Y` and the JS event payload working.
- `interactionsMoved` now evaluates `shouldActivateUnderCustomCriteria`
on macOS and, once it passes, clears the flag, sets `Began` and resets
the translation to zero — same semantics as iOS/Android (translation
counts from the activation point).
- A mouse-up before the criteria are met arrives at `setState:` as
`Ended` and is rewritten to `Failed`, so the gesture finalizes correctly
(`onFinalize` with `canceled=true`).
- The flag is cleared in `reset` and in `activateAfterLongPress` (which
already forces `minDistSq >= 100`, so the block is active while waiting
for the long-press timer).

## Test plan

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

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

export default function EmptyExample() {
  const minDistPan = usePanGesture({
    minDistance: 50,
    onBegin: () => console.log('[minDist] onBegin'),
    onActivate: (e) =>
      console.log(`[minDist] onActivate tx=${e.translationX.toFixed(1)} ty=${e.translationY.toFixed(1)}`),
    onDeactivate: () => console.log('[minDist] onDeactivate'),
    onFinalize: (e) => console.log(`[minDist] onFinalize canceled=${e.canceled}`),
  });

  const activeOffsetPan = usePanGesture({
    activeOffsetX: [-50, 50],
    onBegin: () => console.log('[activeOffsetX] onBegin'),
    onActivate: (e) =>
      console.log(`[activeOffsetX] onActivate tx=${e.translationX.toFixed(1)} ty=${e.translationY.toFixed(1)}`),
    onDeactivate: () => console.log('[activeOffsetX] onDeactivate'),
    onFinalize: (e) => console.log(`[activeOffsetX] onFinalize canceled=${e.canceled}`),
  });

  const minVelocityPan = usePanGesture({
    minVelocity: 800,
    onBegin: () => console.log('[minVelocity] onBegin'),
    onActivate: (e) =>
      console.log(`[minVelocity] onActivate tx=${e.translationX.toFixed(1)} ty=${e.translationY.toFixed(1)}`),
    onDeactivate: () => console.log('[minVelocity] onDeactivate'),
    onFinalize: (e) => console.log(`[minVelocity] onFinalize canceled=${e.canceled}`),
  });

  return (
    <View style={styles.container}>
      <Text style={styles.label}>minDistance: 50</Text>
      <GestureDetector gesture={minDistPan}>
        <View style={[styles.box, { backgroundColor: 'tomato' }]} />
      </GestureDetector>

      <Text style={styles.label}>activeOffsetX: [-50, 50]</Text>
      <GestureDetector gesture={activeOffsetPan}>
        <View style={[styles.box, { backgroundColor: 'mediumseagreen' }]} />
      </GestureDetector>

      <Text style={styles.label}>minVelocity: 800</Text>
      <GestureDetector gesture={minVelocityPan}>
        <View style={[styles.box, { backgroundColor: 'steelblue' }]} />
      </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: 140, height: 140, borderRadius: 12 },
});
```

</details>
M
Michał Bert committed
6819385fca53c3414feeef4024dc99f5c68d445a
Parent: 991ccb5
Committed by GitHub <noreply@github.com> on 8/6/2026, 9:56:47 AM