Merge Gesture Handler 3 working branch to main#3954
Merged
j-piasecki merged 237 commits intomainfrom Feb 5, 2026
Merged
Conversation
<!-- Description and motivation for this PR. Include 'Fixes #<number>' if this is fixing some issue. --> Removed legacy `Hammer.JS` web implementation, all it's usages and references. Removed `enableLegacyWebImplementation`. Removed `isNewWebImplementationEnabled`. It's not abundantly clear at first, but it's an internal function.
…d UI runtime (#3207) ## Description Changes how `setGestureState` is exposed to the UI runtime. Instead of the weird conditionally adding Reanimated as a dependency on Android and the weird cast on iOS it uses `_WORKLET_RUNTIME` const injected by Reanimated into the JS runtime. This allows Gesture Handler to decorate the UI runtime without direct dependencies between the libraries. The new approach relies on two methods being added to the global object: - `_setGestureStateAsync` on the JS runtime - `_setGestureStateSync` on the UI runtime which allows for state manipulation also from the JS thread. The basic example has been modified to easily test the new functionality. > [!CAUTION] > This works only on the New Architecture (and breaks the old one) ## Test plan Test the expo example app and the modified basic example app
## Description Merges declaration and initialization of fields in the event builder classes ## Test plan Build android
## Description Remove actions testing build for the old architecture ## Test plan See CI
## Description ### Problem Currently the following configuration: ```jsx <GestureDetector ... > <Text> ... </Text> </GestureDetector> ``` does not work on `iOS`. This is due to change `react-native` introduced in 0.79 - `hitTest` in `RCTParagraphTextView` now returns `nil` by default ([see here](https://github.com/facebook/react-native/blob/dcbbf275cbc4150820691a4fbc254b198cc92bdd/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm#L379)). This results in native `UIGestureRecognizer` not responding to touches. ### Solution We no longer attach native recognizer to `RCTParagraphTextView`, but to its parent - `RCTParagraphComponentView`. The problem with this approach is that `handleGesture` method uses `reactTag` property, which on `RCTParagraphComponentView` is `nil`. This is why we use `reactTag` from `RCTParagraphTextView` when sending event to `JS` side. Fixes #3581 ## Test plan <details> <summary>Tested on the following code:</summary> ```jsx import React from 'react'; import { StyleSheet, Text } from 'react-native'; import { Gesture, GestureDetector, GestureHandlerRootView, } from 'react-native-gesture-handler'; export default function EmptyExample() { const g = Gesture.Tap().onEnd(() => { console.log('Tapped!'); }); return ( <GestureHandlerRootView style={styles.container}> <GestureDetector gesture={g}> <Text> Click me <Text> Me too! </Text> </Text> </GestureDetector> </GestureHandlerRootView> ); } const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', alignItems: 'center', }, }); ``` </details>
…3596) ## Description Logic behind sending touch events on `android` dispatches events into all gesture handlers registered in the orchestrator. This means that interaction with one `GestureDetector` triggers callbacks on the others. This PR adds check for tracked pointer, so that handlers respond only to those that they are tracking. Fixes #3543 ## Test plan <details> <summary>Tested on the following example:</summary> ```jsx import React from 'react'; import { StyleSheet, Text, View } from 'react-native'; import { Gesture, GestureDetector, GestureHandlerRootView, } from 'react-native-gesture-handler'; type BoxProps = { label: string; }; function Box({ label }: BoxProps) { const manual = Gesture.Manual() .onTouchesDown((e) => { console.log('down', label, e.handlerTag); }) .onTouchesUp((e) => { console.log('up', label, e.handlerTag); }) .onTouchesCancelled(() => { console.log('cancelled', label); }); return ( <GestureDetector gesture={manual}> <View style={styles.box}> <Text style={styles.text}>{label}</Text> </View> </GestureDetector> ); } export default function EmptyExample() { return ( <GestureHandlerRootView style={styles.container}> <Box label="1" /> <Box label="2" /> </GestureHandlerRootView> ); } const styles = StyleSheet.create({ container: { flex: 1, alignItems: 'center', justifyContent: 'center', gap: 10, }, box: { width: 100, height: 100, backgroundColor: 'red', alignItems: 'center', justifyContent: 'center', }, text: { color: 'white', fontSize: 20, fontWeight: 'bold', }, }); ``` </details>
## Changes - Replace `forwardRef` with prop `ref` - Fix possible infinite re-render bug - Fix invalid dependency lists - Split `ReanimatedSwipeable` into 3 smaller files: - `index.ts` - `ReanimatedSwipeable.tsx` - `ReanimatedSwipeableProps.tsx` No changes to the core logic have been made. ## Test plan Use available Swipeable examples to test if it works.
This PR adds a Radon IDE banner to the right-hand side part of the documentation.  The banner rotates a couple of different labels and CTAs. It's only visible on desktop. Related to software-mansion/react-native-reanimated#7631 and software-mansion/react-native-reanimated#7634
## Description Some of our users experience crashes that `reversed` is not defined: ``` java.lang.NoSuchMethodError: No virtual method reversed()Ljava/util/List; ``` This PR changes `reversed` occurrences to either `asReversed` (if possible), or `asReversed().toList()`, if array can be modified. Closes #3594 ## Test plan Tested on expo-example (mostly _transformations_, _multitap_).
## Description #3579 introduced changes to `ReanimatedSwipeable`. However, it didn't update path to component in `tsconfig`, thus CI was failing. ## Test plan Check CI (+ `yarn ts-check`)
## Description Implements a native `RNGestureHandlerDetector` component based on `display: contents`: - The view will match the layout of its child - The lifecycle of gestures is moved to hooks instead of being managed by the detector - Detector should attach and detach native gesture handlers to itself as it's mounted or unmounted - Implements sending native events to the detector component - It's able to work with RN's Animated events TODO: - Validate that gestures are correctly attached/detached in all cases (suspense, display: none, navigation) - Validate that the gesture lifecycle is correctly managed in StrictMode - Correctly handle `NativeViewGestureHandler`, which should be attached to the child (I think) - Correctly handle the Text component - Integration with Reanimated ## Test plan Updated basic example
## Description Rendering `Pressable` during an active press resulted in `onPress` never being called. This PR moves `StateMachine` dependencies, such that it is not reset when `handlePressIn` or `handlePressOut` change. Fixes #3593 ## Test plan - Use the repro provided within #3593 - See how `onPress` is called correctly
## Description see https://gitlab.com/IzzyOnDroid/repo/-/wikis/Reproducible-Builds/RB-Hints-for-Developers#no-funny-build-time-generated-ids for context. ## Test plan I've successfully used these changes for a little while on my own app.
## Description The `Pressable` was developed with an incorrect assumption that the `onLayout` prop is not available on the `NativeButton`. This PR defines the `onLayout` on `RawButtonProps`, and makes use of said prop in `Pressable` to remove the need for the `dimensionsAfterResize` property. This PR also marks `dimensionsAfterResize` as deprecated. Fixes #3600 ## Test plan 1. Use the provided test code. 2. Notice how the `Pressable`, despite starting out with `0, 0` dimensions, responds correctly to all press events. 3. Use the `Pressable` examples in our example app to confirm there are no new issues with the component. ## Test code <details> ```tsx import React, { useEffect } from 'react'; import { StyleSheet } from 'react-native'; import { Pressable, GestureHandlerRootView, } from 'react-native-gesture-handler'; import Animated, { useSharedValue, useAnimatedStyle, withTiming, } from 'react-native-reanimated'; export default function EmptyExample() { const opacity = useSharedValue(0); const containerAnimatedStyle = useAnimatedStyle(() => { 'worklet'; return { opacity: opacity.value, transform: [{ scale: opacity.value }], }; }); useEffect(() => { opacity.value = withTiming(1, { duration: 200 }); }, [opacity]); return ( <GestureHandlerRootView style={styles.container}> <Animated.View style={containerAnimatedStyle}> <Pressable style={styles.box} onPress={() => console.log('press')} /> </Animated.View> </GestureHandlerRootView> ); } const styles = StyleSheet.create({ container: { flex: 1, alignItems: 'center', justifyContent: 'center', gap: 10, }, box: { width: 150, height: 150, backgroundColor: 'red', alignItems: 'center', justifyContent: 'center', }, }); ``` </details> --------- Co-authored-by: Michał Bert <63123542+m-bert@users.noreply.github.com>
## Description Currently when we start `expo-example` it uses expo go by default. This PR changes it do use `dev client` instead ## Test plan Run `expo-example`
## Description
Passing `enabled={false}` to buttons and pressable in jest tests now has effect. I
properly configured the corresponding mocks. Change was motivated by
issue #2385.
## Test plan
Tested using React Native 80.1
```ts
import { render, userEvent } from '@testing-library/react-native';
import { RectButton } from 'react-native-gesture-handler';
export const user = userEvent.setup()
describe('Testing disabled Button', () => {
it('onPress does not trigger', async () => {
const onPress = jest.fn();
const { getByTestId } = render(
<RectButton testID="btn" onPress={onPress} enabled={false} />
);
const btn = getByTestId('btn');
expect(onPress).not.toHaveBeenCalled();
await user.press(btn);
expect(onPress).not.toHaveBeenCalled();
});
});
```
## Description Fixes #3608. On web, when gesture was active and its component had been dropped, the gesture was not dropped. This caused conflicts with other handlers, and prevented registering other gestures. I added a method to drop handlers in the `GestureOchestrator` and call it when component is dropped. --> ## Test plan ```ts import React from 'react'; import { useState } from 'react'; import { Pressable, TextInput } from 'react-native-gesture-handler'; export default function EmptyExample() { const [shown, setShown] = useState(true) if (!shown) { return ( <Pressable key="1" testID="other-pressable" style={{ width: 30, height: 30, backgroundColor: 'red' }} onPress={() => console.log('pressed')} /> ) } return ( <Pressable key="2" testID="bad-pressable" onPress={() => { }}> <TextInput style={{ backgroundColor: 'green', width: 100, height: 30 }} onSubmitEditing={() => setShown(false)} /> </Pressable> ) }; ```
Release 2.27.2 🚀
## Description The link in the error message about the missing `GestureHandlerRootView` was outdated, and the page no longer exists. ## Test plan Open the new link
## Description On web, In `EventManagers`, the `registerListeners` was called twice for each handler of a `Button` ## Test plan * Add a console.log to both `registerListners()` and `unregisterListeners()` in `web/tools/KeyboardEventManager` (or any other event manager) * Observe logs in the `Web styles reset` example in the example app
## Description This PR introduces integration with `Reanimated` to `NativeDetector`. When `Reanimated` is installed callbacks are handled via `useEvent` hook. Otherwise, they run on JS. It also creates `v3` directory (name can be changed later) that will be used in further development to separate new code from the old one. ## Test plan Tested on basic-example app.
## Description In #3617 we've merged `onUpdate` and `onChange` callbacks so `onUpdate` now contains information about changes (or better to say, it will when we implement it 😄). However, there are still places in code where you can see `onChange` being referenced. This PR cleans that up. ## Test plan Tested on basic-example.
## Description
This PR automatically assigns `onUpdate` callback to `onGestureHandlerAnimatedEvent` when `Animated.Event` is passed. It also checks whether `change*` fields were used in `Animated.Event` - if so, we throw an error.
## Test plan
Since basic example is already "under construction", I'll copy whole code. Forgive me for I have sinned 🙏
Basically it covers the case where we switch between `Animated.Event` and standard callback, along with using `change*` in `Animated.Event`.
<details>
<summary> Test code </summary>
```tsx
import * as React from 'react';
import { Animated, Button, useAnimatedValue } from 'react-native';
import {
GestureHandlerRootView,
NativeDetector,
useGesture,
} from 'react-native-gesture-handler';
export default function App() {
const [visible, setVisible] = React.useState(true);
const [shouldUseAnimated, setShouldUseAnimated] = React.useState(true);
const value = useAnimatedValue(0);
const animatedEvent = Animated.event(
[
{ nativeEvent: { handlerData: { translationX: value } } },
// { nativeEvent: { handlerData: { changeX: 10 } } },
],
{
useNativeDriver: true,
}
);
const jsCallback = (e: any) => console.log(e.nativeEvent);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const gesture = useGesture('PanGestureHandler', {
// onGestureHandlerAnimatedEvent: event,
// onGestureHandlerEvent: (e: any) =>
// console.log('onGestureHandlerEvent', e.nativeEvent),
onUpdate: shouldUseAnimated ? animatedEvent : jsCallback,
onEnd: (_e: any) => {
setShouldUseAnimated((prev) => !prev);
},
changeEventCalculator: (event: any, _lastUpdateEvent: any) => {
return { ...event.nativeEvent, changeX: 10 };
},
});
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,
},
{ transform: [{ translateX: value }] },
]}
/>
</NativeDetector>
)}
</GestureHandlerRootView>
);
}
```
</details>
## Description We came to a conclusion that current event handling may conflict with gesture relations, such as `simultaneous` when one of the gestures is using `Animated`. This PR removes `AnimatedEvent` action type from `NativeDetector` and adds flag to `GestureHandler`, which represents if handler should send events for `Animated`. ## Status - ### Android ✅ - ### iOS ✅ ## Test plan Test `pan1`, `pan2` and `gesture` from the code below. >[!NOTE] > Since `simultaneous` relation is not yet implemented, only first pan works when `gesture` is used. On Android you can solve that ba commenting out `otherHandler.cancel()` in `makeActive` function ([this line](https://github.com/software-mansion/react-native-gesture-handler/blob/0e5d58efcb2bd0ee2bd4eefa78fd366e003eea41/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt#L205)) <details> <summary>Tested on the following code:</summary> ```tsx import * as React from 'react'; import { Animated, Button, useAnimatedValue } from 'react-native'; import { GestureHandlerRootView, NativeDetector, useGesture, } from 'react-native-gesture-handler'; export default function App() { const [visible, setVisible] = React.useState(true); const value = useAnimatedValue(0); const event = Animated.event( [{ nativeEvent: { handlerData: { translationX: value } } }], { useNativeDriver: true, } ); const pan1 = useGesture('PanGestureHandler', { onUpdate: (e: any) => { console.log('Pan1 update:', e); }, }); const pan2 = useGesture('PanGestureHandler', { onUpdate: event, dispatchesAnimatedEvents: true, }); const gesture = { tag: [pan1.tag, pan2.tag], gestureEvents: { onGestureHandlerStateChange: (e) => { pan1.gestureEvents.onGestureHandlerStateChange(e); pan2.gestureEvents.onGestureHandlerStateChange(e); }, onGestureHandlerEvent: pan1.gestureEvents.onGestureHandlerEvent, onGestureHandlerAnimatedEvent: pan2.gestureEvents.onGestureHandlerAnimatedEvent, onGestureHandlerTouchEvent: (e) => { pan1.gestureEvents.onGestureHandlerTouchEvent(e); pan2.gestureEvents.onGestureHandlerTouchEvent(e); }, }, dispatchesAnimatedEvents: true, // Assuming this is always true for this example }; 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, }, { transform: [{ translateX: value }] }, ]} /> </NativeDetector> )} </GestureHandlerRootView> ); } ``` </details>
## Description #3646 removed _animated native detector_ action type. However, now we have to pass `dispatchesAnimatedEvents` to gesture config, what was not done automatically. This PR fixes it. ## Test plan Tested on `basic-example`
## Description #3646 removed the Animated action type, but there are still references left to `dispatchesAnimatedEvents` in the native detector, while the property has been moved to the config of individual gestures. This PR removes those references. ## Test plan Check the native detector examples
## Description
This PR brings support for `NativeViewGestureHandler` into `NativeDetector`. It does so, by attaching handler into child instead of detector view.
## Status
- ### Android ✅
- ### iOS ✅
## Test plan
<details>
<summary>Tested on the following code:</summary>
```jsx
import * as React from 'react';
import { StyleSheet, Text, View, ScrollView } from 'react-native';
import {
GestureHandlerRootView,
NativeDetector,
useGesture,
} from 'react-native-gesture-handler';
export default function App() {
const items = Array.from({ length: 30 }, (_, index) => `Item ${index + 1}`);
const gesture = useGesture('NativeViewGestureHandler', {
onBegin: (e) => {
console.log('onBegin', e);
},
onStart: (e) => {
console.log('onStart', e);
},
onEnd: (e) => {
console.log('onEnd', e);
},
onFinalize: (e) => {
console.log('onFinalize', e);
},
onTouchesDown: (e) => {
console.log('onTouchesDown', e);
},
onTouchesMove: (e) => {
console.log('onTouchesMove', e);
},
onTouchesCancelled: (e) => {
console.log('onTouchesCancelled', e);
},
onTouchesUp: (e) => {
console.log('onTouchesUp', e);
},
onUpdate: (e) => {
console.log('onUpdate', e);
},
onChange: (e) => {
console.log('onChange', e);
},
onCancel: (e) => {
console.log('onCancel', e);
},
});
return (
<GestureHandlerRootView
style={{ flex: 1, backgroundColor: 'white', paddingTop: 8 }}>
<NativeDetector gesture={gesture}>
<ScrollView style={styles.scrollView}>
{items.map((item, index) => (
<View key={index} style={styles.item}>
<Text style={styles.text}>{item}</Text>
</View>
))}
</ScrollView>
</NativeDetector>
</GestureHandlerRootView>
);
}
const styles = StyleSheet.create({
scrollView: {
backgroundColor: 'lightgrey',
marginHorizontal: 20,
},
item: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
padding: 20,
margin: 2,
backgroundColor: 'white',
borderRadius: 10,
},
text: {
fontSize: 20,
color: 'black',
},
});
```
</details>
## Description This PR adds a web version of `NativeDetector` component following its IOS/android implementations. ## Test plan * copy all react source files from basic-example to expo-example * add a console log, in the `NativeDetector` pan Gesture * check whether the gesture is properly recognised
## Description Currently if `Reanimated` is not installed, running apps on `iOS` results in redbox that says that it `Reanimated` module doesn't exist. It happens because in [this line](https://github.com/software-mansion/react-native-gesture-handler/blob/6825dbbd07bb73730a7c23ab140f96c00b93bb8d/packages/react-native-gesture-handler/apple/RNGestureHandlerModule.mm#L125) in module we call `[self.moduleRegistry moduleForName:"ReanimatedModule"]`. This goes through [RCTModuleRegistry](https://github.com/facebook/react-native/blob/ca520ee4c19dd921f9225c71a9ad872c4bbd7cb9/packages/react-native/React/Base/RCTModuleRegistry.m#L50) to [RCTTurboModuleManager](https://github.com/facebook/react-native/blob/ca520ee4c19dd921f9225c71a9ad872c4bbd7cb9/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModuleManager.mm#L980), where this method is forced to throw redbox if module is not found. To fix this, we move check for `Reanimated` availability to JS side. ## Test plan Tested on basic-example app with and without `Reanimated`
## Description
Context menu was broken on v3, this PR fixes it.
WebDelegate never handled changing contextMenu. when context had been
disabled the `areContextMenuListenersAdded` was set to true, later when
it was enabled it would not change the listener from disabled to enabled
as the `addContextMenuListeners` returned after seeing that the
listeners had already been added.
## Test plan
Tested on the following example:
<details>
```tsx
import React from 'react';
import { StyleSheet, View } from 'react-native';
import {
GestureDetector,
MouseButton,
usePanGesture,
} from 'react-native-gesture-handler';
export default function ContextMenuExample() {
const p1 = usePanGesture({ mouseButton: MouseButton.RIGHT });
const p2 = usePanGesture({});
const p3 = usePanGesture({});
return (
<View style={styles.container}>
<GestureDetector gesture={p1}>
<View style={[styles.box, styles.grandParent]}>
<GestureDetector gesture={p2} enableContextMenu={true}>
<View style={[styles.box, styles.parent]}>
<GestureDetector gesture={p3} enableContextMenu={false}>
<View style={[styles.box, styles.child]} />
</GestureDetector>
</View>
</GestureDetector>
</View>
</GestureDetector>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'space-around',
alignItems: 'center',
},
grandParent: {
width: 300,
height: 300,
backgroundColor: "navy",
},
parent: {
width: 200,
height: 200,
backgroundColor: "purple",
},
child: {
width: 100,
height: 100,
backgroundColor: "blue",
},
box: {
display: 'flex',
justifyContent: 'space-around',
alignItems: 'center',
borderRadius: 20,
},
});
```
</details>
## Description
The new `StateManager` is global and given `handlerTag` it can manually
set the states of an arbitrary gesture. This causes errors, when the
gesture, which state is being set, has not been yet recorded in the
orchestrator. Recording gestures in the orchestrator on android is done
lazily, thus if it never received touches it is not recorded.
It also adds explicit error when trying to manually handled a gesture
not attached to any detector on all platforms.
## Test plan
Tested on the following example
<details>
```ts
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { GestureHandlerRootView, GestureDetector, useLongPressGesture, GestureStateManager, usePanGesture, useSimultaneousGestures, useTapGesture } from 'react-native-gesture-handler';
export default function TwoPressables() {
const longPress = useLongPressGesture({
onTouchesDown: (e) => {
'worklet';
console.log("touches down")
},
onActivate: () => {
'worklet';
console.log("long pressed")
},
minDuration: 100000000,
disableReanimated: true
})
const pan = useTapGesture({
onTouchesDown: () => {
'worklet';
console.log("tap")
GestureStateManager.activate(longPress.tag)
},
disableReanimated: true
});
return (
<GestureHandlerRootView>
<View style={styles.root}>
<GestureDetector gesture={longPress}>
<View style={styles.outer}>
<Text style={styles.label}>Long Press</Text>
</View>
</GestureDetector>
<GestureDetector gesture={pan}>
<View style={styles.outer}>
<Text style={styles.label}>Pan</Text>
</View>
</GestureDetector>
</View>
</GestureHandlerRootView>
);
}
const styles = StyleSheet.create({
root: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#f7f7f7',
},
outer: {
padding: 20,
backgroundColor: '#ddd',
borderRadius: 12,
marginBottom: 50
},
label: {
fontSize: 18,
marginBottom: 10,
},
})
```
</details>
#3936) ## Description Gesture detection on complex views (like Text) relies on `gestureRecognizerShouldBegin` checking for the virtual react tag. The logic itself is fine, but iOS runs it when it wants to see if the gesture should activate, at which point all gestures attached to the composed view have fired their `onBegin` callbacks. This PR explicitly calls this method inside `onTouches(Interactions)Began` to immediately fail handlers that don't pass the test. ## Test plan Test the `basic-example/Text` with `onBegin` instead of `onActivate`.
…3884) Adds an explicit error message when `GestureDetector` is rendered without any gesture. The current behavior is a random error when trying to call a method on `undefined`. Verify that the error is thrown when no gesture is passed.
## Description In #3855 we've introduced `fromReset` argument for `handleGesture`. However, `ManualActivationRecognizer` does not have such signature. This PR removes it from selector as `fromReset` is not passed anyway. ## Test plan Check that on `Pressable` example app no longer crashes when clicking on **press retention** area.
## Description Adds a banner linking to the State of React Native Survey in readme.
## Description Removes a banner linking to the State of React Native Survey from readme.
## Description fixes: #3891 fixes: #3904 This PR implements `pointerEvents` support for `Pressable` component from `react-native-gesture-handler` on iOS. Previously, setting `pointerEvents="box-none"` (or other modes) had no effect on iOS, while it worked correctly <s>on Android</s> and with React Native's `Pressable` on iOS. Android PR: #3927 ### Implementation Details The implementation follows React Native's `hitTest` logic for `pointerEvents`: - For `box-only`: Returns `self` if point is inside (respecting `hitSlop`), `nil` otherwise - For `box-none`: Checks subviews only, returns the hit subview or `nil` - For `none`: Always returns `nil` - For `auto`: Uses standard hit testing with `shouldHandleTouch` logic The implementation respects `hitTestEdgeInsets` (hitSlop) for all modes, ensuring consistent behavior with React Native's `Pressable`. ## Test plan Tested all `pointerEvents` modes on iOS: - ✅ `pointerEvents="none"` - View and subviews don't receive touches - ✅ `pointerEvents="box-none"` - View doesn't receive touches, subviews do - ✅ `pointerEvents="box-only"` - View receives touches, subviews don't - ✅ `pointerEvents="auto"` - Default behavior works as expected I've used https://github.com/huextrat/repro-pressable-gh to test scenarios Tested on both old architecture (Paper) and new architecture (Fabric). edit: `pointerEvents` with RNGH Pressable is not working on Android --------- Co-authored-by: Michał <michal.bert@swmansion.com>
fixes: #3891 fixes: #3904 This PR fixes `pointerEvents` support for `Pressable` component from `react-native-gesture-handler` on Android. Waiting for iOS PR: #3925 as codegen is involved and a small iOS changes is needed Tested all `pointerEvents` modes on Android: - ✅ `pointerEvents="none"` - View and subviews don't receive touches - ✅ `pointerEvents="box-none"` - View doesn't receive touches, subviews do - ✅ `pointerEvents="box-only"` - View receives touches, subviews don't - ✅ `pointerEvents="auto"` - Default behavior works as expected I've used https://github.com/huextrat/repro-pressable-gh to test scenarios Tested on both old architecture (Paper) and new architecture (Fabric).
## Description
Updates the `set-package-version` script to also support beta and
release candidate versions. Since those two may not be published from a
stable branch, they require explicit version to be set.
The version format for those releases would be:
```
{major}.{minor}.{patch}-rc.{rcVersion}
{major}.{minor}.{patch}-beta.{betaVersion}
```
## Test plan
Run the script in different configurations
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
## Description Updates mocks to mock `HostGestureDetector`, and have a separate file for mocking buttons. Moreover it adds tests for correct error throws in V3. ## Test plan `yarn test`
## Description
On android and web gestures activated with state manager were not
cleaned up properly as they were never registered in the gesture
orchestrator.
## Test plan
Tested on the following example
<details>
```tsx
import React from 'react';
import { StyleSheet, View } from 'react-native';
import {
GestureHandlerRootView,
GestureDetector,
useLongPressGesture,
GestureStateManager,
LongPressGesture,
} from 'react-native-gesture-handler';
import Animated, {
useAnimatedStyle,
useSharedValue,
withTiming,
} from 'react-native-reanimated';
export const COLORS = {
offWhite: '#f8f9ff',
headerSeparator: '#eef0ff',
PURPLE: '#b58df1',
NAVY: '#001A72',
RED: '#A41623',
YELLOW: '#F2AF29',
GREEN: '#0F956F',
GRAY: '#ADB1C2',
KINDA_RED: '#FFB2AD',
KINDA_YELLOW: '#FFF096',
KINDA_GREEN: '#C4E7DB',
KINDA_BLUE: '#A0D5EF',
};
export default function TwoPressables() {
const isActivated = [
useSharedValue(0),
useSharedValue(0),
useSharedValue(0),
useSharedValue(0),
];
const gestures: LongPressGesture[] = [];
const createGestureConfig = (index: number) => ({
onActivate: () => {
'worklet';
isActivated[index].value = 1;
console.log(`Box ${index}: long pressed`);
const nextIndex = index + 1;
if (nextIndex < gestures.length) {
const nextGesture = gestures[nextIndex];
if (nextGesture) {
GestureStateManager.activate(nextGesture.handlerTag);
}
}
},
onFinalize: () => {
'worklet';
isActivated[index].value = 0;
const nextIndex = index + 1;
if (nextIndex < gestures.length) {
const nextGesture = gestures[nextIndex];
if (nextGesture) {
GestureStateManager.deactivate(nextGesture.handlerTag);
}
}
},
disableReanimated: true,
});
const g0 = useLongPressGesture(createGestureConfig(0));
const g1 = useLongPressGesture(createGestureConfig(1));
const g2 = useLongPressGesture(createGestureConfig(2));
const g3 = useLongPressGesture(createGestureConfig(3));
gestures[0] = g0;
gestures[1] = g1;
gestures[2] = g2;
gestures[3] = g3;
const colors = [COLORS.PURPLE, COLORS.NAVY, COLORS.GREEN, COLORS.RED];
function Box({ index }: { index: number }) {
const animatedStyle = useAnimatedStyle(() => ({
opacity: isActivated[index].value === 1 ? 0.5 : 1,
transform: [
{ scale: withTiming(isActivated[index].value === 1 ? 0.95 : 1) },
],
}));
return (
<GestureDetector gesture={gestures[index]}>
<Animated.View
style={[
commonStyles.box,
{ backgroundColor: colors[index] },
animatedStyle,
]}
/>
</GestureDetector>
);
}
return (
<GestureHandlerRootView>
<View style={commonStyles.centerView}>
<Box index={0} />
<Box index={1} />
<Box index={2} />
<Box index={3} />
</View>
</GestureHandlerRootView>
);
}
const commonStyles = StyleSheet.create({
centerView: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
box: {
height: 150,
width: 150,
borderRadius: 20,
marginBottom: 30,
},
})
```
## Description Addresses the underlying issue of #3906 The original issue described in the above PR was caused by wrong shadow node dimensions, which were fixed by #3930. After that, the dimensions were good, but the long press was still failing. This was caused by `shouldCancelWhenOutside` checking the dimensions of the detector while the child was moved. This PR changes the logic so that the child's hitbox is checked when using the native detector. This should be enough for iOS, but on Android, further investigation is needed into whether the entire `transformedEvent` should be in the coordinate space of the detector or its child. ## Test plan See #3906 test plan
## Description This PR handles state management in our docs: - Moves information about states to "under the hood" section - Rewrites entire **states & events** page to focus more on callbacks - Updates `GestureStateManager` entry and moves it to **fundamentals** - Merges manual gesture guide with manual gesture docs ## Test plan Read docs 🤓 --------- Co-authored-by: Jakub Piasecki <jakub.piasecki@swmansion.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
## Description This PR removes old migration guide and adds new one. ## Test plan Read docs 🤓 --------- Co-authored-by: Jakub Piasecki <jakub.piasecki@swmansion.com> Co-authored-by: Andrzej Antoni Kwaśniewski <81448793+akwasniewski@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
## Description
Though we allowed `HitSlop` to be `SharedValue` in gesture config, we
excluded it from being split. Therefore if someone did something like:
```ts
const sv = useSharedValue(10);
const pan = usePanGesture({
hitSlop: sv;
})
```
this wouldn't work as `SharedValue<number>` couldn't be assigned to
`SharedValue<HitSlop>`.
This PR changes behavior of `SharedValueOrT<T>` type, so that it uses
[Distributive Conditional
Types](https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types)
to split union into separate types and then apply `SharedValue`.
## Test plan
`yarn ts-check` and `yarn lint-js`
<details>
<summary>Tested on the following code:</summary>
```ts
import { useSharedValue } from 'react-native-reanimated';
import {
ActiveCursor,
MouseButton,
useHoverGesture,
usePanGesture,
HoverEffect,
} from 'react-native-gesture-handler';
export function App() {
const runOnJS_SV = useSharedValue(true);
const enabled_SV = useSharedValue(false);
const shouldCancelWhenOutside_SV = useSharedValue(true);
const hitSlop_SV = useSharedValue(10);
const activeCursor_SV = useSharedValue<ActiveCursor>('pointer');
const mouseButton_SV = useSharedValue(MouseButton.LEFT);
const cancelsTouchesInView_SV = useSharedValue(true);
const manualActivation_SV = useSharedValue(false);
const minDistance_SV = useSharedValue(5);
const offsetStart_SV = useSharedValue(0);
const hoverEffect_SV = useSharedValue(HoverEffect.LIFT);
const pan1 = usePanGesture({
runOnJS: false,
enabled: true,
shouldCancelWhenOutside: true,
hitSlop: 10,
activeCursor: 'pointer',
mouseButton: MouseButton.LEFT,
cancelsTouchesInView: true,
manualActivation: false,
minDistance: 20,
activeOffsetX: [10, 20],
});
const pan2 = usePanGesture({
runOnJS: runOnJS_SV,
enabled: enabled_SV,
shouldCancelWhenOutside: shouldCancelWhenOutside_SV,
hitSlop: hitSlop_SV,
activeCursor: activeCursor_SV,
mouseButton: mouseButton_SV,
cancelsTouchesInView: cancelsTouchesInView_SV,
manualActivation: manualActivation_SV,
minDistance: minDistance_SV,
activeOffsetX: [offsetStart_SV, 20],
});
const hover1 = useHoverGesture({
effect: HoverEffect.LIFT,
});
const hover2 = useHoverGesture({
effect: hoverEffect_SV,
});
console.log(pan1, pan2, hover1, hover2);
}
```
</details>
## Description Made a new PR, as it is easier than attempt to merge the [old one](#3725) with next. This PR adds a toggle in the example app to switch between Legacy and V3 api examples. In the new api section I added split into few sections. Most of them are rewritten old V2 examples with more consistent styling. I also added a Feedback component to showcase features console free. The new examples are: * basic examples for every gestures * lock to showcase combining gestures * sharedValue, nestedText, svg - to showcase VirtualDetector * animated - to showcase cooperation with animated ## Test plan See if the examples in new api section from common-example work
## Description Removes failing tests for the V1 API. Given that it has been deprecated for quite a while, and there are plans to remove it, I feel like getting rid of the tests is justified. It will also enable us to run the tests continuously on GitHub Actions. ## Test plan Run the tests
## Description Overhaul of the release process: - adds `beta` and `rc` options to the previously existing `stable` and `commitly` - allows the release of a specific version, independent from the branch name (optional, will still detect the version from the branch name when unspecified) - verifies that the latest version is either one patch, one minor, or one major higher than the currently published version (ATM throws on major change) - verifies that beta, rc and commitly releases aren't done for an already published version (i.e. will disallow publishing `2.30.0-beta.1` when stable `2.30.0` is published`). - automatic numbering of beta and rc releases Internally: - moved most of the helpers to separate files - moved version numbering helpers to `version-utils` - added tests covering the release scripts ## Test plan Tested on a fork: - Fails when trying to skip minor on latest release: https://github.com/j-piasecki/react-native-gesture-handler/actions/runs/21475940199/job/61859862583 - Fails when trying to skip patch on latest release: https://github.com/j-piasecki/react-native-gesture-handler/actions/runs/21476045075/job/61860214823 - Fails when trying to publish on existing versions (checks base version for beta, rc, and commitly): https://github.com/j-piasecki/react-native-gesture-handler/actions/runs/21476741044/job/61862557674 - Succeeds when not skipping any minor: https://github.com/j-piasecki/react-native-gesture-handler/actions/runs/21476056662/job/61860252565 - Succeeds when not skipping any patch: https://github.com/j-piasecki/react-native-gesture-handler/actions/runs/21476183083/job/61860683061 - Succeeds when publishing a non-latest release: https://github.com/j-piasecki/react-native-gesture-handler/actions/runs/21476510871/job/61861782101
Bumps [github-pages-deploy-action](https://github.com/JamesIves/github-pages-deploy-action) to v4. Previously, the action inconsistently pulled either the latest commit or its predecessor. Run the action and check whether it pulled latest commit
## Description This PR does two things: 1. Renames `onTouchesCancelled` callback to `onTouchesCancel` 2. Adds few information to migration guide: 2.1. Added information that `onChange` was removed 2.2. Added information about removed `state`, `oldState` 2.3. Added information about unified callback events types. ## Test plan Read docs 🤓
## Description `InterceptingDetectorProps` was missing some types from `GestureDetectorProps` this PR makes both types extend `CommonGestureDetectorProps` ## Test plan `yarn ts-check` --------- Co-authored-by: Jakub Piasecki <jakub.piasecki@swmansion.com>
## Description Temporarily disables commitly releases.
Contributor
There was a problem hiding this comment.
Pull request overview
This PR merges the Gesture Handler 3 development branch (next) into main, introducing significant architectural changes and a new API for gesture handling in React Native applications.
Changes:
- Removes Paper-specific generated code and transitions to Fabric-only architecture
- Introduces new Gesture Handler 3 API with hook-based gesture composition
- Updates documentation structure to support versioned docs (2.x and 3.x)
Reviewed changes
Copilot reviewed 232 out of 604 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
packages/react-native-gesture-handler/android/paper/src/main/java/com/facebook/react/viewmanagers/*.java |
Removes Paper architecture generated interface and delegate files |
packages/react-native-gesture-handler/android/noreanimated/src/main/java/com/swmansion/gesturehandler/ReanimatedProxy.kt |
Renames class from ReanimatedEventDispatcher to ReanimatedProxy |
packages/react-native-gesture-handler/android/build.gradle |
Removes Paper/Fabric conditional compilation and architecture checks |
packages/react-native-gesture-handler/android/CMakeLists.txt |
Adds CMake configuration for Fabric codegen compilation |
packages/react-native-gesture-handler/{Swipeable,DrawerLayout}/package.json |
Removes deprecated component package.json files |
packages/react-native-gesture-handler/RNGestureHandler.podspec |
Updates source files to include shared C++ code and simplifies dependency configuration |
packages/docs-gesture-handler/versions.json |
Adds version 2.x to documentation versions |
packages/docs-gesture-handler/versioned_docs/version-2.x/**/*.md |
Adds versioned documentation for Gesture Handler 2.x API |
packages/docs-gesture-handler/src/** |
Updates documentation components and styling for versioned docs support |
apps/** |
Updates example apps to use new API and removes deprecated examples |
.github/workflows/** |
Removes Paper-specific CI workflows and adds GH3 API testing workflow |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
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.
Description
Merges the
nextbranch, where the Gesture Handler 3 was developed intomain.