Repository navigation
Commit 6819385
authored
[macOS] Fix
## 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>Pan activation criteria being ignored (#4387)1 parent 991ccb5 commit 6819385
1 file changed
Lines changed: 39 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
42 | 42 | | |
43 | 43 | | |
44 | 44 | | |
45 | | - | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
46 | 48 | | |
47 | 49 | | |
48 | 50 | | |
| |||
114 | 116 | | |
115 | 117 | | |
116 | 118 | | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
117 | 122 | | |
118 | 123 | | |
119 | 124 | | |
| |||
144 | 149 | | |
145 | 150 | | |
146 | 151 | | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
147 | 157 | | |
148 | 158 | | |
149 | 159 | | |
| |||
181 | 191 | | |
182 | 192 | | |
183 | 193 | | |
184 | | - | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
185 | 202 | | |
186 | 203 | | |
187 | 204 | | |
| |||
214 | 231 | | |
215 | 232 | | |
216 | 233 | | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
217 | 251 | | |
218 | 252 | | |
219 | 253 | | |
| |||
269 | 303 | | |
270 | 304 | | |
271 | 305 | | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
272 | 309 | | |
273 | 310 | | |
274 | 311 | | |
| |||
0 commit comments