[macOS] Fix touch events never being delivered (#4390)
## Description
`onTouchesMove` and `onTouchesUp` callbacks never fired on macOS — a
gesture produced a single `onTouchesDown` and then a cancel sweep when
the recognizer reset, never a move or up event. Found while testing the
`manualActivation` fix (#4389), which this bug masks entirely in its
real-world form (activating from `onTouchesMove`).
`RNGestureHandlerPointerTracker` tracks pointers by storing the touch
object at registration and matching subsequent events by object identity
(`_trackedPointers[index] == touch`). That's correct on iOS, where a
`UITouch` is one stable object for the whole lifetime of a touch. On
macOS the recognizers forward `NSEvent`s, and every event in a mouse
sequence is a fresh instance — so `findTouchIndex:` / `unregisterTouch:`
never matched, move/up events were dropped with `changedCount == 0`, and
the registered pointer leaked until the tracker's `reset` swept it out
via `cancelPointers` (which is why JS saw down → cancel instead of down
→ moves → up).
Since macOS has exactly one mouse pointer, the tracker now matches the
tracked slot itself on macOS instead of comparing object identity, and
`touchesMoved` replaces the stored event with the latest one so
`extractAllTouches` reports the pointer's current position rather than
where the sequence started.
## 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: down -> move stream -> up, onFinalize canceled=true, never onActivate.
const neverActivatePan = usePanGesture({
manualActivation: true,
onBegin: () => console.log('[never-activate] onBegin'),
onActivate: () => console.log('[never-activate] onActivate (SHOULD NOT HAPPEN)'),
onTouchesDown: () => console.log('[never-activate] onTouchesDown'),
onTouchesMove: () => console.log('[never-activate] onTouchesMove'),
onTouchesUp: () => console.log('[never-activate] onTouchesUp'),
onDeactivate: () => console.log('[never-activate] onDeactivate'),
onFinalize: (e) => console.log(`[never-activate] onFinalize canceled=${e.canceled}`),
});
// Box B: manualActivation, activates from onTouchesMove — the real-world pattern.
// Expected: onActivate on first movement (tx ~0), onUpdate while dragging,
// onTouchesUp + onDeactivate + onFinalize canceled=false on release.
const selfTag = useSharedValue(-1);
const selfActivatePan = usePanGesture({
manualActivation: true,
onTouchesDown: () => console.log(`[self-activate] onTouchesDown tag=${selfTag.value}`),
onTouchesMove: () => {
console.log(`[self-activate] onTouchesMove tag=${selfTag.value}`);
if (selfTag.value !== -1) {
GestureStateManager.activate(selfTag.value);
}
},
onTouchesUp: () => console.log('[self-activate] onTouchesUp'),
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 on touch move — drag</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
bc605eb9be8d8638daf901d0298eb2d7cdfe7d6b
Parent: 787f165
Committed by GitHub <noreply@github.com>
on 8/6/2026, 9:01:44 AM