Cherry-pick bug fixes and improvements from main to v2-stable#4053
Closed
Cherry-pick bug fixes and improvements from main to v2-stable#4053
Conversation
## Description After #3715 v3 api relations did not work. This PR fixes this issue, now relations work in both APIs. ## Test plan <details> <summary>old api</summary> ```ts import React from 'react'; import { View, StyleSheet } from 'react-native'; import { GestureDetector, Gesture, GestureHandlerRootView, } from 'react-native-gesture-handler'; export default function Example() { const innerTap = Gesture.Tap() .onStart(() => { console.log('inner tap'); }); const outerTap = Gesture.Tap() .onStart(() => { console.log('outer tap'); }) .simultaneousWithExternalGesture(innerTap); return ( <GestureHandlerRootView style={styles.container}> <GestureDetector gesture={outerTap}> <View style={styles.outer}> <GestureDetector gesture={innerTap}> <View style={styles.inner} /> </GestureDetector> </View> </GestureDetector> </GestureHandlerRootView> ); } const styles = StyleSheet.create({ container: { flex: 1, alignItems: 'center', justifyContent: 'center', }, outer: { width: 250, height: 250, backgroundColor: 'lightblue', }, inner: { width: 100, height: 100, backgroundColor: 'blue', alignSelf: 'center', }, }); ``` </details> <details> <summary>new api</summary> ```ts import * as React from 'react'; import { Animated, Button, useAnimatedValue } from 'react-native'; import { GestureHandlerRootView, NativeDetector, useSimultaneous, useGesture, useExclusive, useRace, SingleGestureName, } from 'react-native-gesture-handler'; export default function App() { const [visible, setVisible] = React.useState(true); const av = React.useRef(new Animated.Value(0)).current const event = Animated.event( [{ nativeEvent: { handlerData: { translationX: av } } }], { useNativeDriver: true, } ); const tap1 = useGesture(SingleGestureName.Tap, { onEnd: () => { // 'worklet'; console.log('Tap 1'); }, numberOfTaps: 1, disableReanimated: true, }); const tap2 = useGesture(SingleGestureName.Tap, { onEnd: () => { // 'worklet'; console.log('Tap 2'); }, numberOfTaps: 2, disableReanimated: true, }); // const tap1 = useGesture('TapGestureHandler', { // onEnd: () => { // 'worklet'; // console.log('Tap 1'); // }, // numberOfTaps: 1, // // disableReanimated: true, // requireExternalGestureToFail: tap2, // }); // const pan1 = useGesture(SingleGestureName.Pan, { // // onUpdate: event, // onUpdate: (e) => { // 'worklet'; // console.log('Pan 1'); // }, // // disableReanimated: true, // }); // // const pan2 = useGesture(SingleGestureName.Pan, { // onUpdate: (e) => { // 'worklet'; // console.log('Pan 2'); // }, // simultaneousWithExternalGesture: pan1, // // requireExternalGestureToFail: pan1, // // blocksExternalGesture: pan1, // // disableReanimated: true, // }); // const composedGesture = useSimultaneous(pan1, pan2); const composedGesture = useExclusive(tap2, tap1); // const composedGesture = useExclusive(pan2, pan1); // For Animated.Event // const composedGesture = useExclusive(pan1, pan2); // For Animated.Event // const composedGesture = useRace(pan1, pan2); // const composedGesture = useRace(pan2, pan1); // const composedGesture = useExclusive(tap1, useSimultaneous(pan1, pan2)); return ( <GestureHandlerRootView style={{ flex: 1, backgroundColor: 'white', paddingTop: 8 }}> <Button title="Toggle visibility" onPress={() => { setVisible(!visible); }} /> {visible && ( <NativeDetector gesture={composedGesture}> <Animated.View style={[ { width: 150, height: 150, backgroundColor: 'blue', opacity: 0.5, borderWidth: 10, borderColor: 'green', marginTop: 20, marginLeft: 40, display: 'flex', alignItems: 'center', justifyContent: 'space-around', }, { transform: [{ translateX: av }] }, ]}> </Animated.View> </NativeDetector> )} </GestureHandlerRootView> ); } ``` </details>
## Description This PR removes the following warning when building the app in release mode for Android: ``` > Task :react-native-gesture-handler:compileReleaseKotlin w: file:///Users/tomekzaw/(...)/node_modules/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootView.kt:41:43 The corresponding parameter in the supertype 'ReactViewGroup' is named 'ev'. This may cause problems when calling this function with named arguments. ``` ## Test plan <!-- Describe how did you test this change here. -->
## Description This PR fixes the `Pressable`'s `onPress` callback being called despite the press being cancelled. Credit for finding and fixing this issue goes to @gigobyte. Fixes #3609 ## Test plan - Run the following example: https://gist.github.com/gigobyte/5d1d0bf9e5f188f6590210a21fc6eb3a - Swipe the pink `Pressable` to the right. - Before this PR, the `onPress` callback would be called without the finger being lifted. - Since this PR, the `onPress` is no longer called when the finger isn't lifted.
In `metro` 0.83.2 `exclusionList` was changed to `default` export (see [this commit](facebook/metro@8610b2c#diff-a6e36af38cd6fde5360c601ce8f2f02e4e6b350b7fea9b0f049ab63f3b8f5709)). This means that we have to update our configs, or otherwise we won't be able to start server. > [!NOTE] > `macos-example` uses older versions of `react-native` so we don't have to update its config. 1. `yarn clean` 2. `yarn` 3. `yarn start` in both examples
…ble (#3666) **Summary** Fixes `SwipeDirection` being `undefined` at runtime in `ReanimatedSwipeable` when the package is consumed from TypeScript source (e.g. in some Metro configs). **Details** `SwipeDirection` was imported from the local barrel (`./index.ts`), which re-exports it as a type-only export. This removes the value in the compiled JS, causing it to be `undefined` at runtime. This PR imports `SwipeDirection` directly from `ReanimatedSwipeableProps`, which exports it as a value. See closed issue for details: [#3665](#3665) **Impact** No API changes. Only affects internal import, ensures enum value is present at runtime. --------- Co-authored-by: Christian Bach <christian.bach@carnegie.se>
## Description This PR simplifies logic that handles `mouseButton` property. I've changed `event.button` to `event.buttons`. These values now correctly align with our `MouseButton` enum, so `Map` is no longer necessary. | Button | `event.button` | `MouseButton` | `event.buttons` | |--------|--------------|-------------|---------------| | Left | 0 | 1 | 1 | | Right | 2 | 2 | 2 | | Middle | 1 | 4 | 4 | | Btn_4 | 3 | 8 | 8 | | Btn_5 | 4 | 16 | 16 | You can find values in MDN docs for: - `event.button` [here](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button#value) - `event.buttons` [here](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons#value) > [!NOTE] > `event.buttons` also performs logical `or` operation on buttons, so for example using both **left** and **right** buttons at the same time results in value of `3`. > [!IMPORTANT] > I've tested behavior when trying to activate handler with different buttons. In case where handler has `mouseButton(MouseButton.LEFT)` clicking first with left button, then adding other button results in activation. This is fine and aligns with what we have now. Clicking with other button and then adding left button does not result in activation. This is also fine. > > Now, clicking with both buttons at the same time does not result in activation. This is because in that case there's no `pointerdown` event. Turns out that `pointerdown` is called only for the first button, all the other pointers result in `pointermove` event. In case where both are pressed at the same time, there's only `pointermove` event. ## Test plan Tested on **MouseButtons** example
## Description
This PR adds `name` property to handlers on web, so that we can avoid checks like in `InteractionsManager`:
```js
const isNativeHandler =
otherHandler.constructor.name === 'NativeViewGestureHandler';
```
## Test plan
Tested on **Swipeable** example on mobile version in chrome.
## Description
This PR adds `changeEventCalculator` functions into continuous handlers.
## Test plan
<details>
<summary>Tested on the following example:</summary>
```tsx
import * as React from 'react';
import { Animated, Button } from 'react-native';
import {
GestureHandlerRootView,
NativeDetector,
usePan,
} from 'react-native-gesture-handler';
export default function App() {
const [visible, setVisible] = React.useState(true);
const gesture = usePan({
onUpdate: (e) => {
'worklet';
console.log(e.handlerData.changeX);
},
});
return (
<GestureHandlerRootView
style={{ flex: 1, backgroundColor: 'white', paddingTop: 8 }}>
<Button
title="Toggle visibility"
onPress={() => {
setVisible(!visible);
}}
/>
{visible && (
<NativeDetector gesture={gesture}>
<Animated.View
style={[
{
width: 150,
height: 150,
backgroundColor: 'blue',
opacity: 0.5,
borderWidth: 10,
borderColor: 'green',
marginTop: 20,
marginLeft: 40,
},
]}
/>
</NativeDetector>
)}
</GestureHandlerRootView>
);
}
```
</details>
## Description
This PR updates types to accept `SharedValues` into config objects.
## Test plan
<details>
<summary>Tested on the following example:</summary>
```tsx
import * as React from 'react';
import { Animated, Button } from 'react-native';
import {
GestureHandlerRootView,
MouseButton,
NativeDetector,
usePan,
} from 'react-native-gesture-handler';
import { useSharedValue } from 'react-native-reanimated';
export default function App() {
const [visible, setVisible] = React.useState(true);
const sv1 = useSharedValue(0);
const sv2 = useSharedValue(MouseButton.LEFT);
const gesture = usePan({
onEnd: (e) => {
'worklet';
console.log('tap', e.handlerData);
},
activeOffsetX: [sv1, 120],
mouseButton: sv2,
});
return (
<GestureHandlerRootView
style={{ flex: 1, backgroundColor: 'white', paddingTop: 8 }}>
<Button
title="Toggle visibility"
onPress={() => {
setVisible(!visible);
}}
/>
{visible && (
<NativeDetector gesture={gesture}>
<Animated.View
style={[
{
width: 150,
height: 150,
backgroundColor: 'blue',
opacity: 0.5,
borderWidth: 10,
borderColor: 'green',
marginTop: 20,
marginLeft: 40,
},
]}
/>
</NativeDetector>
)}
</GestureHandlerRootView>
);
}
```
</details>
## Description
This PR implements new component -> `LogicDetector`. It resolves the
issue of attaching gestures to inner SVG components. `LogicDetector`
communicates with a `NativeDetector` higher in the hierarchy, which will
be responsible for attaching gestures.
**Note:** attaching `Native` gestures to `LogicDetector` will be a added
in a follow up, as it is a niche feature - thus not a priority - and we
don't want to block this PR.
**Note 2:** Reanimated handlers currently only work on web for
reanimated: ^4.1
## Test plan
tested on the following code
```tsx
import React from 'react';
import { Text, View, StyleSheet } from 'react-native';
import { NativeDetector, LogicDetector, useGesture, SingleGestureName } from 'react-native-gesture-handler';
import Svg, { Circle, Rect } from 'react-native-svg';
export default function SvgExample() {
const circleElementTap = useGesture(SingleGestureName.Tap, {
onStart: () => {
'worklet';
console.log('RNGH: clicked circle')
}
});
const rectElementTap = useGesture(SingleGestureName.Tap, {
onStart: () => {
'worklet';
console.log('RNGH: clicked parallelogram')
}
});
const containerTap = useGesture(SingleGestureName.Tap, {
onStart: () => {
'worklet';
console.log('RNGH: clicked container')
}
});
const vbContainerTap = useGesture(SingleGestureName.Tap, {
onStart: () => {
'worklet';
console.log('RNGH: clicked viewbox container')
}
});
const vbInnerContainerTap = useGesture(SingleGestureName.Tap, {
onStart: () => {
'worklet';
console.log('RNGH: clicked inner viewbox container')
}
});
const vbCircleTap = useGesture(SingleGestureName.Tap, {
onStart: () => {
'worklet';
console.log('RNGH: clicked viewbox circle')
}
});
return (
<View>
<View style={styles.container}>
<Text style={styles.header}>
Overlapping SVGs with gesture detectors
</Text>
<View style={{ backgroundColor: 'tomato' }}>
<NativeDetector gesture={containerTap}>
<Svg
height="250"
width="250"
onPress={() => console.log('SVG: clicked container')}>
<LogicDetector gesture={circleElementTap}>
<Circle
cx="125"
cy="125"
r="125"
fill="green"
onPress={() => console.log('SVG: clicked circle')}
/>
</LogicDetector>
<LogicDetector gesture={rectElementTap}>
<Rect
skewX="45"
width="125"
height="250"
fill="yellow"
onPress={() => console.log('SVG: clicked parallelogram')}
/>
</LogicDetector>
</Svg>
</NativeDetector>
</View>
<Text>
Tapping each color should read to a different console.log output
</Text>
</View>
<View style={styles.container}>
<Text style={styles.header}>SvgView with SvgView with ViewBox</Text>
<View style={{ backgroundColor: 'tomato' }}>
<NativeDetector gesture={vbContainerTap}>
<Svg
height="250"
width="250"
viewBox="-50 -50 150 150"
onPress={() => console.log('SVG: clicked viewbox container')}>
<LogicDetector gesture={vbInnerContainerTap}>
<Svg
height="250"
width="250"
viewBox="-300 -300 600 600"
onPress={() =>
console.log('SVG: clicked inner viewbox container')
}>
<Rect
x="-300"
y="-300"
width="600"
height="600"
fill="yellow"
/>
<LogicDetector gesture={vbCircleTap}>
<Circle
r="300"
fill="green"
onPress={() => console.log('SVG: clicked viewbox circle')}
/>
</LogicDetector>
</Svg>
</LogicDetector>
</Svg>
</NativeDetector>
</View>
<Text>The viewBox property remaps SVG's coordinate space</Text>
</View>
</View >
);
}
const styles = StyleSheet.create({
container: {
alignItems: 'center',
justifyContent: 'center',
marginBottom: 48,
},
header: {
fontSize: 18,
fontWeight: 'bold',
margin: 10,
},
});
```
## Description
I've noticed that typescript throws error when using composed gestures. This PR fixes this problem.
> [!NOTE]
> We decided that it is not worth to spend much time on changing those types, as the only thing that composition does is merging gestures into one object.
## Test plan
<details>
<summary>Tested on the following example:</summary>
```tsx
import * as React from 'react';
import { SafeAreaView, Platform, View } from 'react-native';
import {
GestureHandlerRootView,
usePan,
useSimultaneous,
useTap,
NativeDetector,
} from 'react-native-gesture-handler';
export default function App() {
const pan = usePan({
onUpdate: (_e) => {
console.log('pan');
},
});
const tap = useTap({});
const g = useSimultaneous(pan, tap);
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<SafeAreaView
style={[{ flex: 1 }, Platform.OS === 'android' && { paddingTop: 50 }]}>
<NativeDetector gesture={g}>
<View />
</NativeDetector>
</SafeAreaView>
</GestureHandlerRootView>
);
}
```
</details>
## Description
This PR adds logic that filters `config` object before passing it to native side.
## Test plan
<details>
<summary>Tested on the following code:</summary>
```jsx
import * as React from 'react';
import { Animated, Button } from 'react-native';
import {
GestureHandlerRootView,
NativeDetector,
useTap,
} from 'react-native-gesture-handler';
export default function App() {
const [visible, setVisible] = React.useState(true);
const gesture = useTap({
onEnd: (e) => {
'worklet';
console.log('tap', e.handlerData);
},
numberOfTaps: 2,
});
return (
<GestureHandlerRootView
style={{ flex: 1, backgroundColor: 'white', paddingTop: 8 }}>
<Button
title="Toggle visibility"
onPress={() => {
setVisible(!visible);
}}
/>
{visible && (
<NativeDetector gesture={gesture}>
<Animated.View
style={[
{
width: 150,
height: 150,
backgroundColor: 'blue',
opacity: 0.5,
borderWidth: 10,
borderColor: 'green',
marginTop: 20,
marginLeft: 40,
},
]}
/>
</NativeDetector>
)}
</GestureHandlerRootView>
);
}
```
</details>
> [!NOTE] > Supersede #3230 # Description This PR contains cherry-picked commits from #3230. I've decided to recreate this PR step-by-step instead of fixing conflicts after migration to monorepo (though if you prefer merge/rebase into the former let me know). # Original description ## Description removes the following components: - `DrawerLayout` - `Swipeable` - `BetterHorizontalDrawer` draft component - `Swipeable` draft component These components were also removed from all files referencing them, such as examples, index files, and the `App.tsx` of the common example app ## Test plan - see how these components are no longer accessible - see how there are no errors, and no new warnings thrown as compared to the `main` branch --------- Co-authored-by: Ignacy Łątka <latkaignacy@gmail.com>
## Description This PR removes `handleGestureEvent` from `onGestureHandlerAnimatedEvent`. This function was trying to call `Animated.Event`, which is not possible since it is an object. This takes out part of `LogicDetector` logic, however it didn't work anyway and at least standard `NativeDetector` works as it should. > [!NOTE] > It was previously a part of #3745 ## Test plan Tested on current `basic-example`
## Description This PR fixes issue where using `Animated.Event` without `worklet` callbacks was throwing error. This was because current implementation of `hasWorkletEventHandlers` was checking all functions present in `config`, including `changeEventCalculator` (which is in fact `worklet`). > [!NOTE] > It previously contained #3748 ## Test plan Tested on the current `basic-example` app
…#3727) ## Description I've noticed that `onPointerMove` callback is triggered on each gesture, even if correct button is not pressed. Though events are not send, it still means that handlers do unnecessary work under the hood. This PR changes `PointerEventManager` so that it doesn't trigger `onPointerMove` callback without active pointer on gestures other than `Hover` ## Test plan Tested on example app with `console.log` added into `onPointerMove` in `PanGestureHandler`
## Description
This PR adds `runOnJS` to gesture config.
## Test plan
<details>
<summary>Tested on the following example:</summary>
```tsx
import * as React from 'react';
import { Animated, Button, StyleSheet, View, Text } from 'react-native';
import {
GestureHandlerRootView,
NativeDetector,
usePan,
useSimultaneous,
} from 'react-native-gesture-handler';
import { useSharedValue } from 'react-native-reanimated';
const runtimeKind = (_worklet: boolean) => {
'worklet';
return _worklet ? 'worklet' : 'JS';
};
function SingleExampleWithState() {
const [js, setJs] = React.useState(false);
const gesture = usePan({
onUpdate: () => {
'worklet';
console.log(
`[SingleExampleWithState] I run on a ${runtimeKind(globalThis._WORKLET)} runtime!`
);
},
runOnJS: js,
});
return (
<View style={[styles.container, styles.center]}>
<Button
title="Change runtime"
onPress={() => {
setJs((v) => !v);
}}
/>
<NativeDetector gesture={gesture}>
<Animated.View style={[styles.box]} />
</NativeDetector>
</View>
);
}
function SingleExampleWithSharedValue() {
const js = useSharedValue(false);
const gesture = usePan({
onUpdate: () => {
'worklet';
console.log(
`[SingleExampleWithSharedValue] I run on a ${runtimeKind(globalThis._WORKLET)} runtime!`
);
},
runOnJS: js,
});
return (
<View style={[styles.container, styles.center]}>
<Button
title="Change runtime"
onPress={() => {
js.value = !js.value;
}}
/>
<NativeDetector gesture={gesture}>
<Animated.View style={[styles.box]} />
</NativeDetector>
</View>
);
}
function ComposedExample() {
const [pan1JS, setPan1JS] = React.useState(false);
const pan2JS = useSharedValue(false);
const pan1 = usePan({
onUpdate: () => {
'worklet';
console.log(
`[ComposedExample | Pan1] I run on a ${runtimeKind(globalThis._WORKLET)} runtime!`
);
},
runOnJS: pan1JS,
});
const pan2 = usePan({
onUpdate: () => {
'worklet';
console.log(
`[ComposedExample | Pan2] I run on a ${runtimeKind(globalThis._WORKLET)} runtime!`
);
},
runOnJS: pan2JS,
});
const gesture = useSimultaneous(pan1, pan2);
return (
<View style={[styles.container, styles.center]}>
<View style={[styles.center, { flexDirection: 'row', gap: 10 }]}>
<Button
title="Change Pan1 runtime"
onPress={() => {
setPan1JS((v) => !v);
}}
/>
<Button
title="Change Pan2 runtime"
onPress={() => {
pan2JS.value = !pan2JS.value;
}}
/>
</View>
<NativeDetector gesture={gesture}>
<Animated.View style={[styles.box]} />
</NativeDetector>
</View>
);
}
export default function App() {
return (
<GestureHandlerRootView style={[{ flex: 1 }, styles.center]}>
<SingleExampleWithState />
<SingleExampleWithSharedValue />
<ComposedExample />
</GestureHandlerRootView>
);
}
const styles = StyleSheet.create({
container: { gap: 10 },
center: {
display: 'flex',
justifyContent: 'space-around',
alignItems: 'center',
},
box: {
width: 150,
height: 150,
borderRadius: 20,
backgroundColor: 'pink',
},
});
```
</details>
## Description While writing examples I noticed that some types would be useful. This includes types for each gesture and their events. This PR adds those types. **Important**: The v3 gesture types take over the names of old v2 types, the v2 types are getting a `Legacy` prefix. This change will have to be added to the migration guide. Added this as a [task](https://github.com/orgs/software-mansion/projects/31/views/1?pane=issue&itemId=132253302) to roadmap. ## Test plan `yarn lint-js`
## Description This PR splits `types.ts` file into multiple files to improve readability. ## Test plan 1. `yarn ts-check` 2. `yarn lint-js` 3. `yarn circular-dependency-check`
## Description
This PR fixes incorrect implementation of `hasWorkletEventHandlers`
function. Namely:
1. Changes `in` to `has`, as `in` cannot be used to check if `Set`
contains given element;
2. Correctly wraps `some` parameter into `[]` - earlier `value` was
actually an index.
This problems resulted in callbacks being run on `JS`, even when they
should be run on `UI`.
## Test plan
<details>
<summary>Tested on the following code:</summary>
```tsx
import * as React from 'react';
import { Animated, Button } from 'react-native';
import {
GestureHandlerRootView,
NativeDetector,
usePan,
} from 'react-native-gesture-handler';
import { getRuntimeKind } from 'react-native-worklets';
export default function App() {
const [visible, setVisible] = React.useState(true);
const gesture = usePan({
onUpdate: () => {
'worklet';
console.log(getRuntimeKind());
},
});
return (
<GestureHandlerRootView
style={{ flex: 1, backgroundColor: 'white', paddingTop: 8 }}>
<Button
title="Toggle visibility"
onPress={() => {
setVisible(!visible);
}}
/>
{visible && (
<NativeDetector gesture={gesture}>
<Animated.View
style={[
{
width: 150,
height: 150,
backgroundColor: 'blue',
opacity: 0.5,
borderWidth: 10,
borderColor: 'green',
marginTop: 20,
marginLeft: 40,
},
]}
/>
</NativeDetector>
)}
</GestureHandlerRootView>
);
}
```
</details>
## Description
Right now, if one changes `enabled` property of handlers to `false`,
they can never activate again. In the following example:
```ts
const [enabled, setEnabled] = useState(false);
const pan = Gesture.Pan().enabled(enabled)
```
gesture will never become active, even if `enabled` changes to `true`.
Same happens when `enabled` is set to `true` and then changed to `false`
and `true` again.
This happens because we set `enabled` to `true` by default in
`setGestureConfig`, thus the following check:
```ts
if (config.enabled !== undefined && this.enabled !== config.enabled) { ... }
```
passes only when `config.enabled` is `false`.
I've set `enabled` to `null` by default, and then if `config.enabled` is
defined, it is assigned given value. Else default will be `true`. To fix
the issue, I've removed reset of `enabled` in `resetConfig` method.
Alternative approach would be to follow Android implementation and
always react to changes in `enabled` even in cases like `false`
$\rightarrow$ `false` and `true` $\rightarrow$ `true`.
## Test plan
<details>
<summary>Tested on the following code:</summary>
```tsx
import React from 'react';
import { Button, StyleSheet, View } from 'react-native';
import {
Gesture,
GestureDetector,
GestureHandlerRootView,
NativeDetector,
usePan,
} from 'react-native-gesture-handler';
import Animated, {
useAnimatedStyle,
useSharedValue,
} from 'react-native-reanimated';
export default function EmptyExample() {
const [showDetector, setShowDetector] = React.useState(true);
const [enablePan1, setEnablePan1] = React.useState(true);
const enablePan2 = useSharedValue(true);
const pan1 = Gesture.Pan().enabled(enablePan1);
const pan2 = usePan({
enabled: enablePan2,
});
const as = useAnimatedStyle(() => {
return {
backgroundColor: enablePan2.value ? 'lightgreen' : 'crimson',
};
});
return (
<GestureHandlerRootView style={styles.container}>
<Button
title="Toggle old API enabled!"
onPress={() => {
setEnablePan1((prev) => !prev);
}}
/>
<GestureDetector gesture={pan1}>
<View
style={[
styles.box,
{ backgroundColor: enablePan1 ? 'lightgreen' : 'crimson' },
]}
/>
</GestureDetector>
<Button
title="Toggle new API enabled!"
onPress={() => {
enablePan2.value = !enablePan2.value;
}}
/>
<Button
title="Toggle NativeDetetor"
onPress={() => {
setShowDetector((prev) => !prev);
}}
/>
{showDetector && (
<NativeDetector gesture={pan2}>
<Animated.View style={[styles.box, as]} />
</NativeDetector>
)}
</GestureHandlerRootView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
box: {
width: 200,
height: 200,
borderRadius: 20,
},
});
```
</details>
## Description Seems like I forgot to change it while copying `MouseButton` tsdoc 😅 ## Test plan ### Before <img width="916" height="168" alt="image" src="https://github.com/user-attachments/assets/207a340d-d0c5-4673-9854-7c6e2d5aed4a" /> ### After <img width="920" height="165" alt="image" src="https://github.com/user-attachments/assets/5ffab693-bfc6-4b34-b27f-9c6fe5fc0d13" />
> [!IMPORTANT] > No specific changes were necessary to make **Gesture Handler** compatible with **React Native 0.82**. This PR bumps basic-example app to React Native 0.82. - ### 0.82.0 ✅ - 🏅 0.82.0-rc.5 🏅 ✅ - 0.82.0-rc.4 ✅ - 0.82.0-rc.3 ✅ - 0.82.0-rc.2 🤯 - 0.82.0-rc.1 ✅ - 0.82.0-rc.0 ✅ Tested that `basic-example` builds and works correctly.
> [!NOTE] > Supersede #3335 This PR contains cherry picked commits from #3335 as merging changes after migrating into monorepo became quite painful 😞 1. `appProject` seems to be left unused after #3091 2. adding `configureEach` allows for [lazy task initialization](https://docs.gradle.org/current/userguide/task_configuration_avoidance.html#sec:old_vs_new_configuration_api_overview) This PR optimises pre-build time of Gesture Handler android, taking it down from ~7s to ~0.6s, a 11x improvement. - baseline: - time 6.776s ([link](https://gradle.com/s/h5iselfgzhafi)) - time 7.391s ([link](https://scans.gradle.com/s/6sz4ssuepvema)) - optimised: - time 0.687s ([link](https://gradle.com/s/zwiujfjblp64s)) - time 0.560s ([link](https://gradle.com/s/i5xqega7pazja)) - time 0.596s ([link](https://gradle.com/s/ohcnspquhmau4)) fixes: #2865 Optimisation of the `example` app pre-build time will be done in a separate PR, so far i took it down from 13s to 0.1s (caching & on-demand building). - Run `./gradlew help --scan` to see pre-build performance - Build app to see everything still works
## Description
<!--
Description and motivation for this PR.
Include 'Fixes #<number>' if this is fixing some issue.
-->
This fixes an issue where the manual activation of a handler wouldn't
work.
I unfortunately didn't had time to make a full reproduction. The
scenario roughly looks like this:
```ts
const gesture = useMemo(() -> Gesture.Pan() // Note happens also without useMemo
.manualActivation(true)
.onTouchesMove((event, manager) => {
// Some criteria to met:
manager.activate()
})
.onUpdate(() => {
// Wouldn't get called on a remount
})
return <GestureDetector gesture={gesture}> // ...
```
This is being used in a component on a screen. When we navigate from the
screen the component gets unmounted and I can see that his logic is
being triggered:
https://github.com/software-mansion/react-native-gesture-handler/blob/1e1f4e035ea6d4c38684c4abc43d91dbf5b46dc6/packages/react-native-gesture-handler/src/handlers/gestures/GestureDetector/index.tsx#L152-L168
```
attachHandler 1
dropHandler 1
```
I think the screen might get frozen instead of unmounted, but not
entirely sure, but the cleanup function gets called and the handler gets
dropped.
Now, when reopening the screen the handlers will reattach, but have a
different handler tag now.
```
attachHandler 1
dropHandler 1
attachHandler 2
```
When I now run the gesture I can see that the `manager` still has the
**previous handler tag**, and thus on the native side it can't find the
handler to activate. Its calling `setGestureHandlerState(1 /* previous
id */)` here (when I call `manager.activate`):
https://github.com/software-mansion/react-native-gesture-handler/blob/1e1f4e035ea6d4c38684c4abc43d91dbf5b46dc6/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerModule.kt#L108
I found that attaching the handler tag to the GestureStateMangaer and
recreating it if it has changed fixes the issue.
If you need a full reproduction to proceed with this change I can
understand, just let me know then I can try to find time to create one 👍
## Test plan
<!--
Describe how did you test this change here.
-->
Well, shamefully I didn't create a reproduction so I guess the test plan
is "trust me bro" 😅 not the best I know - sorry.
---------
Co-authored-by: Michał Bert <63123542+m-bert@users.noreply.github.com>
Co-authored-by: Michał <michal.bert@swmansion.com>
## Description On android unction `setPressed` grabs the delagate without any checks. As a result, in specific cases - such as in #3735 - pressed is registered when it should not be. ## Test plan The same as in #3735, tested on the following code. <details> ``` import { useEffect, useState } from "react"; import { StyleSheet, Text, View } from "react-native"; import { GestureHandlerRootView, Pressable, } from "react-native-gesture-handler"; import { KeyboardProvider, KeyboardStickyView, } from "react-native-keyboard-controller"; import { SafeAreaProvider, useSafeAreaInsets, } from "react-native-safe-area-context"; const rippleConfig = { color: "#666666", borderless: true, foreground: true, }; function App() { return ( <SafeAreaProvider> <GestureHandlerRootView> <KeyboardProvider statusBarTranslucent={true} navigationBarTranslucent={true} preserveEdgeToEdge={true} > <AppContent /> </KeyboardProvider> </GestureHandlerRootView> </SafeAreaProvider> ); } function Snackbar({ visible, setSnackbarVisible, }: { visible?: boolean; setSnackbarVisible: (visible: boolean) => void; }) { const safeAreaInsets = useSafeAreaInsets(); useEffect(() => { const timeout = setTimeout(() => { setSnackbarVisible(false); }, 3000); return () => clearTimeout(timeout); }, [visible, setSnackbarVisible]); return ( <KeyboardStickyView offset={{ closed: -safeAreaInsets.bottom, opened: 0 }}> {visible && ( <View style={[styles.snackbar]}> <Text style={styles.snackbarText}>Snackbar</Text> </View> )} </KeyboardStickyView> ); } function Screen({ setSnackbarVisible, }: { setSnackbarVisible: (visible: boolean) => void; }) { const safeAreaInsets = useSafeAreaInsets(); const [count, setCount] = useState(0); const incrementCount = () => { setCount(count + 1); }; const handleButtonPress = () => { setSnackbarVisible(true); }; return ( <View style={[ styles.container, { flex: 1, paddingTop: safeAreaInsets.top, paddingBottom: safeAreaInsets.bottom, }, ]} > <View style={styles.centerContent}> <Text style={styles.countText}>Count: {count}</Text> <Pressable style={styles.button} onPress={incrementCount} android_ripple={rippleConfig} > <Text style={styles.buttonText}>Increment</Text> </Pressable> </View> <View style={styles.bottomContent}> <Pressable style={styles.button} onPress={handleButtonPress} android_ripple={rippleConfig} > <Text style={styles.buttonText}>Show snackbar</Text> </Pressable> </View> </View> ); } function AppContent() { const [snackbarVisible, setSnackbarVisible] = useState(false); return ( <> <Screen setSnackbarVisible={setSnackbarVisible} /> <Snackbar visible={snackbarVisible} setSnackbarVisible={setSnackbarVisible} /> </> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: "#f5f5f5", }, centerContent: { flex: 1, justifyContent: "center", alignItems: "center", paddingHorizontal: 20, }, bottomContent: { paddingHorizontal: 20, paddingBottom: 20, flexDirection: "row", justifyContent: "center", }, countText: { fontSize: 24, fontWeight: "bold", marginBottom: 20, color: "#333", }, button: { backgroundColor: "#007AFF", paddingHorizontal: 30, paddingVertical: 15, borderRadius: 8, alignItems: "center", justifyContent: "center", shadowColor: "#000", shadowOffset: { width: 0, height: 2, }, shadowOpacity: 0.25, shadowRadius: 3.84, elevation: 5, }, buttonText: { color: "white", fontSize: 20, fontWeight: "600", }, disabledButton: { backgroundColor: "#CCCCCC", shadowOpacity: 0.1, elevation: 2, }, disabledButtonText: { color: "#666666", }, snackbar: { position: "absolute", left: 15, right: 15, bottom: 30, height: 50, backgroundColor: "#008000", paddingHorizontal: 15, paddingVertical: 12, borderRadius: 8, alignItems: "center", justifyContent: "center", shadowColor: "#000", shadowOffset: { width: 0, height: 2, }, shadowOpacity: 0.25, shadowRadius: 3.84, elevation: 8, }, snackbarText: { color: "white", fontSize: 14, }, }); export default App; ``` </details> * install `react-native-keyboard-controller` * Press on "Increment" to see that the button/pressable works. * Press on "Show snackbar" to show the snackbar. * While the snackbar is visible, press the "Show snackbar" button again. * When the snackbar is gone, observe that the ripple effect on the "Show snackbar" button is still visible. Don't touch this button. * Press on "Increment" and observe that it's not working anymore.
## Description There's a change in gesture recognizers introduced in `iOS` 26. Now when `reset` method is called, recognizers go back to `UIGestureRecognizerStatePossible` state. This breaks our current behavior, because this state is mapped into `RNGestureHandlerStateBegan`, so if for example `Pan` fails, it tries to send event with `Began` state. Unfortunately, changing recognizer state is not possible outside of `touches*` methods, therefore we had to move `triggerAction` into those callbacks. Let me know if you see a different approach into this problem. Fixes #3733 > [!WARNING] > `triggerAction` call was already present in `Tap` right before `reset` ([see here](https://github.com/software-mansion/react-native-gesture-handler/blob/21c4943d5769d3fff60f7bf0550c5810f6011e13/packages/react-native-gesture-handler/apple/Handlers/RNTapHandler.m#L120)). Looks like it was called twice for some reason (but I believe that [check for _lastState](https://github.com/software-mansion/react-native-gesture-handler/blob/21c4943d5769d3fff60f7bf0550c5810f6011e13/packages/react-native-gesture-handler/apple/RNGestureHandler.mm#L311) prevented any problems with this redundancy). For now I have not included second call. If you think it is required, let me know. ## Test plan Tested on the code provided below, on the following platforms: - [x] iOS 26.0 (iPhone 17 Pro) - [x] iOS 18.5 (iPhone 16e) - [x] OSX (macOS 15.6.1) <details> <summary>Test code:</summary> ```tsx import { StyleSheet, View, Text } from 'react-native'; import { GestureHandlerRootView, Gesture, GestureDetector, GestureType, } from 'react-native-gesture-handler'; function TestBox({ gestureType, bgColor, }: { gestureType: GestureType; bgColor: string; }) { const handlerName = gestureType.handlerName; const gesture = gestureType .onEnd(() => { console.log(`[${handlerName}] onEnd`); }) .onFinalize(() => { console.log(`[${handlerName}] onFinalize`); }) .runOnJS(true); return ( <View style={styles.center}> <Text>{handlerName}</Text> <GestureDetector gesture={gesture}> <View style={[styles.box, { backgroundColor: bgColor }]} /> </GestureDetector> </View> ); } export default function App() { return ( <GestureHandlerRootView style={[{ flex: 1, padding: 50 }, styles.center]}> <TestBox gestureType={Gesture.Pan()} bgColor="#b58df1" /> <TestBox gestureType={Gesture.LongPress()} bgColor="#f1a85d" /> <TestBox gestureType={Gesture.Fling()} bgColor="#5df1a8" /> <TestBox gestureType={Gesture.Tap()} bgColor="#5d8ef1" /> </GestureHandlerRootView> ); } const styles = StyleSheet.create({ center: { display: 'flex', justifyContent: 'space-around', alignItems: 'center', }, box: { height: 100, width: 100, backgroundColor: '#b58df1', borderRadius: 20, marginBottom: 30, }, }); ``` </details>
## Description In `Pan` gesture, offset properties are handled differently than other props. Before they're sent to native side, they are mapped to separate `start` and `end` offset properties. This happens when offset props are present. However, if one explicitly sets value of offset property to `undefined`, e.g.: ```ts activeOffsetY: Platform.OS === 'android' ? [-75, 75] : undefined, ``` the following warning can be observed: ``` WARN [react-native-gesture-handler] activeOffsetY is not a valid property for PanGestureHandler and will be ignored. ``` This PR adds offset properties to our filter. With this change, warning no longer appears. Fixes #4021 > [!NOTE] > It could also be solved by dynamically deleting `undefined` fields from config, but simply ignoring them sounds safer. ## Test plan <details> <summary>Tested on the following code:</summary> ```tsx import React from 'react'; import { StyleSheet, Text, View, Platform } from 'react-native'; import { usePanGesture } from 'react-native-gesture-handler'; export default function EmptyExample() { const panGesture = usePanGesture({ activeOffsetY: Platform.OS === 'android' ? [-75, 75] : undefined, }); return ( <View style={styles.container}> <Text style={{ fontSize: 64, opacity: 0.25 }}>😞</Text> <Text style={{ fontSize: 24, opacity: 0.25 }}>It's so empty here</Text> </View> ); } const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', alignItems: 'center', }, }); ``` </details>
## Description Current logic dedicated to handling `RNRootViewGestureRecognizer` on iOS looks only for RN's native roots. When gestures are used inside Screens' `FullWindowOverlay`, the logic doesn't do anything, but the overlay has its own [`RCTSurfaceTouchHandler`](https://github.com/software-mansion/react-native-screens/blob/be64b6d9a17c3a4647806f252e075b96b9f690cc/ios/RNSFullWindowOverlay.mm#L158). This PR updates the traversal logic so it handles `RNSFullWindowOverlayContainer` the same way as `RCTSurfaceView`. ## Test plan I wasn't able to reproduce the problem, but in theory, it's possible that without this change, recognizers from RNGH and RCTSurfaceTouchHandler could run simultaneously, since this path never ran: https://github.com/software-mansion/react-native-gesture-handler/blob/5587435679eabe3f8690f077ba7c2ecc3e354a14/packages/react-native-gesture-handler/apple/RNRootViewGestureRecognizer.m#L55-L63
## Description This PR bumps Reanimated version, along with worklets. Since in `common-app` we still had Reanimated 3.18, I've also introduced necessary changes to types. Also, it turned out that our current setup cannot correctly resolve dependencies - I've updated `metro.config.js` to also block `common-app`, since this "app" only contains sources for examples and their `node_modules` shouldn't be used. ## Test plan Build and run example apps.
…rictly on hitslop instead (#4038) ## Description Apple enforces a stupidly large retention offset in the default UIControl touch tracking loop, which was conflicting with `shouldCancelWhenOutside` and `hitSlop` props. This PR overrides the default tracking loop to rely only on our props instead. ## Test plan Tested on the example added in #4018
## Description `Native` gesture is specific and its behavior differs across platforms. This leads to strange workarounds in our codebase (e.g. [buttons](https://github.com/software-mansion/react-native-gesture-handler/blob/4a7639d8c83d5d467403cac702ee14ca0a479c4e/packages/react-native-gesture-handler/src/components/GestureButtons.tsx#L71)). In this PR unifies buttons behavior by changing Native gesture. ## Test plan Tested on expo-example app (buttons / Pressable)
## Description This PR introduces new `Clickable` component, which is meant to be a replacement for buttons. > [!NOTE] > Docs for `Clickable` will be added in #4022, as I don't want to release them right away after merging this PR. ### `borderless` For now, `borderless` doesn't work. I've tested clickable with some changes that allow `borderless` ripple to be visible, however we don't want to introduce them here because it would break other things. Also it should be generl fix, not in the PR with new component. ## Stress test Render list with 2000 buttons 25 times (50ms delay between renders), drop 3 best and 3 worst results. Then calculate average. Stress test example is available in this PR. #### Android | | $t_{Button}$ | $t_{Clickable}$ | $\Delta{t}$ | |------------------|--------------|-----------------|--------------| | `BaseButton` | 1196,18 | 1292,3 | 96,12 | | `RectButton` | 1672,6 | 1275,68 | -396,92 | | `BorderlessButton` | 1451,34 | 1290,74 | -160,6 | #### iOS | | $t_{Button}$ | $t_{Clickable}$ | $\Delta{t}$ | |------------------|--------------|-----------------|--------------| | `BaseButton` | 1101,37 | 1154,6 | 53,23 | | `RectButton` | 1528,07 | 1160,07 | -368 | | `BorderlessButton` | 1330,24 | 1172,69 | -157,55 | #### Web | | $t_{Button}$ | $t_{Clickable}$ | $\Delta{t}$ | |------------------|--------------|-----------------|--------------| | `BaseButton` | 64,18 | 95,57 | 31,39 | | `RectButton` | 104,58 | 97,95 | -6,63 | | `BorderlessButton` | 81,11 | 98,64 | 17,53 | ## Test plan New examples --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
## Summary The `+load` method in `RNGestureHandlerButtonComponentView` is unconditional, but its parent `RCTViewComponentView` correctly guards its `+load` behind `#ifdef RCT_DYNAMIC_FRAMEWORKS`. Without the guard, the child's `+load` runs even when dynamic frameworks are not used, causing conflicts with third-party SDKs (e.g. Akamai BMP) that also use `+load` — particularly on x86_64 simulators where the load order differs from arm64. This matches the pattern used by React Native core in `RCTViewComponentView.mm` (see facebook/react-native#37274). ## Test plan - Build xcframework with static linking (no `USE_FRAMEWORKS=dynamic`) - Verify `RNGestureHandlerButtonComponentView +load` is not in the binary (`nm` check) - Run on x86_64 simulator alongside Akamai BMP — no crash at launch - Run on arm64 simulator — gesture handler works normally
…#3964) On New Architecture, when a parent view has display: 'none' and siblings change while hidden, the native UIView backing a component may be recycled and replaced. The React view tag stays the same but the UIView instance changes, causing gesture recognizers to be lost. This adds a reattachHandlersIfNeeded check in flushOperations that re-binds handlers whose native view has changed. The check is a fast no-op (pointer comparison) when views haven't been recycled. Fixes #3937 Tested with both v2 (Gesture.Tap()) and v3 (useTapGesture) APIs on iOS simulator (New Architecture). Minimal repro to verify the fix: ```ts import React, { useCallback, useState } from 'react'; import { Button, StyleSheet, Text, View } from 'react-native'; import { Gesture, GestureDetector, useTapGesture, } from 'react-native-gesture-handler'; export default function DisplayNone() { const [tapCountV2, setTapCountV2] = useState(0); const [tapCountV3, setTapCountV3] = useState(0); const tapV2 = Gesture.Tap() .runOnJS(true) .onStart(() => { setTapCountV2((c) => c + 1); }); const tapV3 = useTapGesture({ runOnJS: true, onActivate: () => { setTapCountV3((c) => c + 1); }, }); const [visible, setVisible] = useState(true); const [randomViews, setRandomViews] = useState(100); const runTest = useCallback(() => { setVisible(false); setTimeout(() => { setRandomViews(Math.random() * 100); setTimeout(() => { setVisible(true); }, 500); }, 500); }, []); return ( <View style={styles.container}> <Text style={[ styles.status, { color: tapCountV2 > 0 ? 'green' : 'red', fontSize: 24 }, ]}> v2 Tap count: {tapCountV2} </Text> <Text style={[ styles.status, { color: tapCountV3 > 0 ? 'green' : 'red', fontSize: 24 }, ]}> v3 Tap count: {tapCountV3} </Text> <Button title="Run Test (Hide→Change→Show)" onPress={runTest} /> <View style={[ styles.wrapper, { display: visible ? 'flex' : 'none', }, ]}> <View style={styles.row}> {Array.from({ length: randomViews }).map((_, i) => ( <View key={i} style={styles.dot} /> ))} </View> <View style={styles.boxRow}> <GestureDetector gesture={tapV2}> <View style={styles.blueBox}> <Text style={styles.blueBoxText}>v2</Text> </View> </GestureDetector> <GestureDetector gesture={tapV3}> <View style={styles.greenBox}> <Text style={styles.blueBoxText}>v3</Text> </View> </GestureDetector> </View> </View> </View> ); } const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#ecf0f1', padding: 8, }, status: { fontSize: 16, fontWeight: 'bold', marginBottom: 20, textAlign: 'center', }, wrapper: { marginTop: 50, gap: 20, backgroundColor: 'yellow', }, row: { flexDirection: 'row', }, dot: { width: 2, height: 2, backgroundColor: 'black', }, boxRow: { flexDirection: 'row', gap: 20, }, blueBox: { height: 50, width: 50, backgroundColor: 'blue', justifyContent: 'center', alignItems: 'center', }, greenBox: { height: 50, width: 50, backgroundColor: 'green', justifyContent: 'center', alignItems: 'center', }, blueBoxText: { color: 'white', fontWeight: 'bold', }, }); ``` Steps: - Tap the box — count increments - Press "Hide → Change → Show" to trigger display:none + sibling change + show - Tap again — count should still increment (was broken before this fix) - Repeat multiple cycles to confirm reliability Co-authored-by: m-bert <63123542+m-bert@users.noreply.github.com>
`Pinch` and `Rotation` are broken because they fail on [this line](https://github.com/software-mansion/react-native-gesture-handler/blob/0e74233db6a30dee547c3782aefc4f90f0551141/packages/react-native-gesture-handler/apple/RNGestureHandler.mm#L306). This happens because `handleGesture:fromReset` is called automatically via [`action` property](https://github.com/software-mansion/react-native-gesture-handler/blob/0e74233db6a30dee547c3782aefc4f90f0551141/packages/react-native-gesture-handler/apple/Handlers/RNPinchHandler.m#L36), but when UIKit calls it, it only passes `recognizer` as first parameter - `fromReset` is some garbage from memory. It gets treated as truthy value, even though it shouldn't be. Now, `handleGesture:fromRecognizer` will not be called automatically when recognizer resets its state, so it should be safe to assume that default `fromReset` value is `NO`. Tested on the **transformations** example and example code from #3855 on both, iOS 18.5 and iOS 26.0 Co-authored-by: m-bert <63123542+m-bert@users.noreply.github.com>
This PR fixes a memory leak by breaking a retain cycle between RNGestureHandler and RNManualActivationRecognizer. The RNGestureHandler owns the _manualActivationRecognizer with a strong reference, and previously the recognizer was holding a strong reference back to the handler, creating a cycle that prevented both objects from being deallocated. Co-authored-by: m-bert <63123542+m-bert@users.noreply.github.com>
## Description `Hover` gesture events contain wrong pointer type. Normally, we map `UITouchTypePencil` to `RNGestureHandlerStylus`, but `Hover` used unmapped `UITouchTypePencil` instead. Because those enums map to different values (`UITouchTypePencil = 2` & `RNGestureHandlerStylus = 1`), it caused wrong pointer type to be returned. Fixes #3977 ## Test plan Not yet as now I don't have access to iPad 😞 Co-authored-by: m-bert <63123542+m-bert@users.noreply.github.com>
## Description <!-- Description and motivation for this PR. Include 'Fixes #<number>' if this is fixing some issue. --> Improves the hover gesture on iOS by distinguishing between a mouse and a stylus, if possible. With these changes the handler will now assume a mouse (just like previously with the enum bug), but if `zOffset` is available (iOS 16.1) and it is over 0.0 it will instead be recognised as styus. ~*Maybe it makes more sense to invert the logic and assume a mouse as default, and stylus if > 0. Let me know what you think.*~ See #3977 for more context. Fixes #3977 ## Test plan <!-- Describe how did you test this change here. --> Here's a small test page that can be added to the basic-example app for testing; ```jsx import { useState } from 'react'; import { View, Text } from 'react-native'; import { GestureDetector, GestureHandlerRootView, useHoverGesture, useSimultaneousGestures, useTapGesture, } from 'react-native-gesture-handler'; import { runOnJS } from 'react-native-worklets'; const POINTER_TYPES: Record<number, string> = { 0: "TOUCH", 1: "STYLUS", 2: "MOUSE", 3: "KEY", 4: "OTHER", } export default function Hover() { const [hoverPointerType, setHoverPointerType] = useState(-1); const [tapPointerType, setTapPointerType] = useState(-1); const hoverGesture = useHoverGesture({ onUpdate: (event) => { runOnJS(setHoverPointerType)(event.pointerType); }, }); const tapGesture = useTapGesture({ onFinalize: (event) => { runOnJS(setTapPointerType)(event.pointerType); }, }); const gesture = useSimultaneousGestures(hoverGesture, tapGesture); return ( <GestureHandlerRootView style={{ flex: 1, backgroundColor: 'white', justifyContent: 'center', alignItems: 'center', }}> <Text>Hover pointer type: {POINTER_TYPES[hoverPointerType] ?? "N/A"}</Text> <Text>Tap pointer type: {POINTER_TYPES[tapPointerType] ?? "N/A"}</Text> <GestureDetector gesture={gesture}> <View style={{ width: 200, height: 200, backgroundColor: 'salmon' }}></View> </GestureDetector> </GestureHandlerRootView> ); } ``` Mouse is testable in the iPad simulator by enabling `I/O -> Input -> Send Pointer to Device` in the top menus. Mouse is also testable on a real iPad by Linking keyboard and mouse to it from a mac in the Displays settings. Apple Pencil is only testable with an actual pencil on a real iPad. --------- Co-authored-by: Michał <michal.bert@swmansion.com> Co-authored-by: m-bert <63123542+m-bert@users.noreply.github.com>
## Description When one pointer is placed on the background, handlers don't to other pointers. Instead, they fail on [this line](https://github.com/software-mansion/react-native-gesture-handler/blob/41fbd374a85def52eb7051f96e3d4c63e6eb0724/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt#L295). This is because on Android events with `ACTION_MOVE` are batched and all pointers are contained within one event. As [Android docs say](https://developer.android.com/reference/android/view/MotionEvent#getActionIndex()), `actionIndex` is fine to check for up and down actions. However, we use it for move action and it always returns `0`. To solve this problem, I've added loop that checks whether any pointer from event is tracked by the handler. Fixes #3995 ## Test plan Tested on example code from #3995 Co-authored-by: m-bert <63123542+m-bert@users.noreply.github.com>
Move `@types/react-test-renderer` from `devDependencies` to `dependencies`. The published `lib/typescript/jestUtils/jestUtils.d.ts` imports `ReactTestInstance` from `react-test-renderer` (line 1). Since `@types/react-test-renderer` is only in `devDependencies`, it's not installed for consumers — causing `TS2307: Cannot find module 'react-test-renderer'` for projects with `skipLibCheck: false`. The [TypeScript handbook](https://www.typescriptlang.org/docs/handbook/declaration-files/publishing.html) is explicit: when published `.d.ts` files reference `@types/*` packages, those packages must be in `dependencies` so consumers resolve them automatically. Several major packages have fixed this exact pattern: - [webpack/schema-utils#97](webpack/schema-utils#97) — moved `@types/json-schema` to `dependencies` for the same reason - [@mui/material#14508](mui/material-ui#14508) — keeps `@types/react-transition-group` in `dependencies` because published types extend it - [react-native-testing-library#1534](callstack/react-native-testing-library#1534) — shipped `.d.ts` broke `skipLibCheck: false` consumers, fixed within 2 days This repo dealt with the same class of issue in #1990 (move `@types/react-native` to `dependencies` — closed only because RN 0.71+ bundled its own types, not because the approach was rejected) and #2259 (shipped `.d.ts` type errors). Note: `react-test-renderer` is [deprecated in React 19](https://react.dev/warnings/react-test-renderer). As more projects drop it from their own dependencies, this breakage will increase for downstream consumers. **Consumer reproduction:** ```bash mkdir test-rngh && cd test-rngh npm init -y npm install react react-native react-native-gesture-handler typescript @types/react ``` `tsconfig.json`: ```json { "compilerOptions": { "strict": true, "noEmit": true, "skipLibCheck": false, "moduleResolution": "bundler", "module": "esnext", "target": "esnext", "jsx": "react-jsx" }, "include": ["*.ts"] } ``` `repro.ts`: ```typescript import type { fireGestureHandler } from 'react-native-gesture-handler/lib/typescript/jestUtils/jestUtils'; export type { fireGestureHandler }; ``` ```bash npx tsc --noEmit npm install @types/react-test-renderer npx tsc --noEmit ``` **Upstream quality gates:** ```bash yarn workspace react-native-gesture-handler ts-check # PASS yarn workspace react-native-gesture-handler build # PASS — module + commonjs + typescript ``` Zero regressions. Co-authored-by: m-bert <63123542+m-bert@users.noreply.github.com>
This PR enables TypeScript's [`exactOptionalPropertyTypes`](https://www.typescriptlang.org/tsconfig/#exactOptionalPropertyTypes) flag in `react-native-gesture-handler` and fixes all 60 resulting type errors across 22 files. `exactOptionalPropertyTypes` is a strict TypeScript flag (not included in `strict: true`) that distinguishes between "property is missing" and "property is explicitly `undefined`". When enabled, `prop?: T` means the property can be omitted but cannot be set to `undefined` — the fix is `prop?: T | undefined`. This matters for downstream projects that enable this flag — without this change, they get type errors when using gesture-handler components. This is currently blocking [react-navigation from enabling the flag](react-navigation/react-navigation#12995). Three categories of fixes across v2 (class-based), v3 (hooks-based), and web handler code: 1. **Type declarations** — Added `| undefined` to optional properties in interfaces and types (`HitSlop`, `CommonGestureConfig`, `BaseGestureHandlerProps`, `ButtonProps`, `ExternalRelations`, `VirtualChild`, `IGestureHandler`, etc.) 2. **Class property types** — Updated class property declarations in `GestureHandler.ts` to include `| undefined` for optional config properties (`_testID`, `hitSlop`, `mouseButton`, `_activeCursor`, etc.) 3. **`WithSharedValueRecursive` mapped type** — Fixed the boolean special case in `ReanimatedTypes.ts` to preserve `undefined` from optional properties using `| Extract<T[K], undefined>` All changes are type annotations only — zero runtime changes. The `src/specs/` files are consumed by `@react-native/codegen`. One spec file was modified (`RNGestureHandlerDetectorNativeComponent.ts` — 7 `DirectEventHandler` event props). Adding `| undefined` to these is safe — the codegen parser ([`parseTopLevelType.js`](https://github.com/facebook/react-native/blob/main/packages/react-native-codegen/src/parsers/typescript/parseTopLevelType.js)) explicitly strips `undefined` from union types and treats the result identically to `prop?: T`. I verified that the codegen schema output is **byte-identical** between `main` and this branch: ```bash node node_modules/@react-native/codegen/lib/cli/combine/combine-js-to-schema-cli.js \ /tmp/schema.json --libraryName rngesturehandler_codegen \ packages/react-native-gesture-handler/src/specs/RNGestureHandlerDetectorNativeComponent.ts \ packages/react-native-gesture-handler/src/specs/RNGestureHandlerButtonNativeComponent.ts ``` All quality gates pass: - `yarn workspace react-native-gesture-handler ts-check` — 0 errors - `yarn workspace react-native-gesture-handler lint-js` — 0 errors - `yarn workspace react-native-gesture-handler test` — 5 suites, 41 tests pass - `yarn workspace react-native-gesture-handler circular-dependency-check` — pass Co-authored-by: m-bert <63123542+m-bert@users.noreply.github.com>
`numberOfPointers` was not included in `Native` gesture events on `iOS`. This PR fixes this problem. I've also run `yarn format:apple` to fix formatting. Tested on expo-example (logging events from `RectButton` on main screen) --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: m-bert <63123542+m-bert@users.noreply.github.com>
## Description
On iOS, `onFinalize` callback is not triggered when `Tap` fails. Turns
out that `handleGesture:` is not called from `interactionsCancelled`
method. This PR fixes this problem.
## Test plan
Tested on iOS 26.1 and 18.5.
<details>
<summary>Tested on the following example:</summary>
```tsx
import { StyleSheet, View } from 'react-native';
import {
GestureDetector,
GestureHandlerRootView,
useTapGesture,
} from 'react-native-gesture-handler';
export default function App() {
const tap = useTapGesture({
onFinalize: () => {
console.log('[tap] onFinalize');
},
onTouchesCancel: () => {
console.log('[tap] onTouchesCancel');
},
});
return (
<GestureHandlerRootView style={styles.container}>
<GestureDetector gesture={tap}>
<View style={styles.box} />
</GestureDetector>
</GestureHandlerRootView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
box: {
width: 100,
height: 100,
backgroundColor: 'crimson',
borderRadius: 10,
},
});
```
</details>
Co-authored-by: m-bert <63123542+m-bert@users.noreply.github.com>
## Summary The `+load` method in `RNGestureHandlerButtonComponentView` is unconditional, but its parent `RCTViewComponentView` correctly guards its `+load` behind `#ifdef RCT_DYNAMIC_FRAMEWORKS`. Without the guard, the child's `+load` runs even when dynamic frameworks are not used, causing conflicts with third-party SDKs (e.g. Akamai BMP) that also use `+load` — particularly on x86_64 simulators where the load order differs from arm64. This matches the pattern used by React Native core in `RCTViewComponentView.mm` (see facebook/react-native#37274). ## Test plan - Build xcframework with static linking (no `USE_FRAMEWORKS=dynamic`) - Verify `RNGestureHandlerButtonComponentView +load` is not in the binary (`nm` check) - Run on x86_64 simulator alongside Akamai BMP — no crash at launch - Run on arm64 simulator — gesture handler works normally Co-authored-by: m-bert <63123542+m-bert@users.noreply.github.com>
Co-authored-by: m-bert <63123542+m-bert@users.noreply.github.com>
Copilot created this pull request from a session on behalf of
m-bert
April 2, 2026 12:07
View session
Collaborator
|
Seems like technology is not there yet |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
v2-stableThe PR was auto-created targeting
main, but it should targetv2-stable. Please click "Edit" next to the base branch and change it tov2-stable. The diff will then show only the 33 files with cherry-picked changes.Description
This PR cherry-picks 14 PRs from
maintov2-stable, sorted by merge date. V3-specific changes (files undersrc/v3/,src/specs/RNGestureHandlerDetectorNativeComponent.ts, etc.) were excluded during conflict resolution since they don't apply to the v2 API.Cherry-picked PRs (in merge-date order):
handleGesture:fromReset:#3983 - [iOS] Add wrapper forhandleGesture:fromReset:pointerTypeinHovergesture #3989 - [iOS] Fix wrongpointerTypeinHovergestureTextcomponent #4003 - MemoizeTextcomponent@types/react-test-rendererto dependenciesexactOptionalPropertyTypessupport #4012 - EnableexactOptionalPropertyTypessupport (V3-only files skipped)numberOfPointerstoNativegesture events #4023 - [iOS] AddnumberOfPointerstoNativegesture eventsonFinalizecallback inTap#4029 - [iOS] Fix missingonFinalizecallback inTapFullWindowOverlayas the native root #4039 - [iOS] HandleFullWindowOverlayas the native root (already on v2-stable, skipped)+loadwith#ifdef RCT_DYNAMIC_FRAMEWORKSNotes on conflict resolution:
src/v3/andsrc/specs/RNGestureHandlerDetectorNativeComponent.tswere removed from cherry-picks since they don't exist on v2-stablereattachHandlersIfNeededto v2 API (removedhostDetectorparameter,usesNativeOrVirtualDetectorcheck)Textcomponent #4003: Kept component asText(notLegacyTextwhich is v3-specific rename)exactOptionalPropertyTypessupport #4012: AppliedexactOptionalPropertyTypeschanges only to files that exist in v2-stable; kept v2'sConfigobject pattern for web handlersnumberOfPointerstoNativegesture events #4023: Kept v2-specifichandleDragInside/handleDragOutsidemethods; removed v3'sfromManualStateChangeparameter additionsFullWindowOverlayas the native root #4039: Changes were already present on v2-stable, resulting in empty cherry-picks (skipped)Test plan
Individual PRs were tested on their respective platforms. This cherry-pick preserves the same changes adapted for the v2 API surface.