Skip to content

Commit 87296dc

Browse files
committed
Merge branch 'main' into @jpiasecki/yields-to-continous-gestures
2 parents 1a8b841 + e9eeb89 commit 87296dc

13 files changed

Lines changed: 224 additions & 26 deletions

File tree

.eslintrc.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"@typescript-eslint/ban-types": "warn",
4646
"@typescript-eslint/consistent-type-imports": "error",
4747
"@typescript-eslint/consistent-type-exports": "error",
48+
"@typescript-eslint/no-unsafe-enum-comparison": "off",
4849

4950
// common
5051
"@typescript-eslint/explicit-module-boundary-types": "off",
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
---
2+
id: wrapped-components
3+
title: Wrapped Components
4+
sidebar_label: Wrapped Components
5+
---
6+
7+
Some components come with a [`Native`](/docs/gestures/use-native-gesture) gesture pre-applied. This allows them to participate in the gesture recognition process. Have a look at the example below.
8+
9+
```tsx
10+
import { useState } from 'react';
11+
import { Switch } from 'react-native';
12+
import {
13+
GestureDetector,
14+
GestureHandlerRootView,
15+
useTapGesture,
16+
Switch as RNGHSwitch,
17+
} from 'react-native-gesture-handler';
18+
19+
export default function App() {
20+
const [isEnabled, setIsEnabled] = useState(false);
21+
22+
const tap1 = useTapGesture({
23+
onDeactivate: () => {
24+
console.log('Tapped!');
25+
},
26+
});
27+
28+
const tap2 = useTapGesture({
29+
onDeactivate: () => {
30+
console.log('Tapped!');
31+
},
32+
});
33+
34+
return (
35+
<GestureHandlerRootView style={{ flex: 1, paddingTop: 100 }}>
36+
<GestureDetector gesture={tap1}>
37+
<Switch value={isEnabled} onValueChange={setIsEnabled} />
38+
</GestureDetector>
39+
<GestureDetector gesture={tap2}>
40+
<RNGHSwitch value={isEnabled} onValueChange={setIsEnabled} />
41+
</GestureDetector>
42+
</GestureHandlerRootView>
43+
);
44+
}
45+
```
46+
47+
On Android, in this scenario, the `Switch` from React Native cannot be toggled on because the `tap1` gesture intercepts it. However, using `RNGHSwitch` makes it capable of participating in the gesture recognition process. This setup allows the switch to be toggled on while still enabling `tap2` to recognize taps on it.
48+
49+
## List of wrapped components
50+
51+
Components listed below come with a pre-applied `Native` gesture.
52+
53+
- `FlatList`
54+
- `ScrollView`
55+
- `RefreshControl`
56+
- `TextInput`
57+
- `Switch`
58+
59+
## onGestureUpdate_CAN_CAUSE_INFINITE_RERENDER
60+
61+
:::danger
62+
This callback may lead to infinite re-renders if not used carefully.
63+
64+
```tsx
65+
export default function App() {
66+
const [gesture, setGesture] = useState<NativeGesture | null>(null);
67+
68+
const updateGesture = (g: NativeGesture) => {
69+
// ❌ Wrong usage: calling setState here triggers a re-render,
70+
// which re-creates the ScrollView's Native gesture, which fires
71+
// this callback again → infinite re-render loop.
72+
setGesture(g);
73+
};
74+
75+
return (
76+
<GestureHandlerRootView style={{ flex: 1 }}>
77+
<ScrollView onGestureUpdate_CAN_CAUSE_INFINITE_RERENDER={updateGesture} />
78+
</GestureHandlerRootView>
79+
);
80+
}
81+
```
82+
:::
83+
84+
Those components also receive an additional prop named `onGestureUpdate_CAN_CAUSE_INFINITE_RERENDER`.
85+
86+
```ts
87+
onGestureUpdate_CAN_CAUSE_INFINITE_RERENDER?: (gesture: NativeGesture) => void;
88+
```
89+
90+
This callback is invoked when the wrapped component's underlying `Native` gesture instance or configuration changes, providing access to the underlying gesture. This can be helpful when setting up [relations](/docs/fundamentals/gesture-composition) with other gestures. You can check example usage in our [`ScrollView`](https://github.com/software-mansion/react-native-gesture-handler/blob/main/packages/react-native-gesture-handler/src/v3/components/GestureComponents.tsx#L78) component.

packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/PinchGestureHandler.kt

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,17 @@ class PinchGestureHandler : GestureHandler() {
7070
}
7171

7272
if (state == STATE_UNDETERMINED) {
73-
initialize(event, sourceEvent)
74-
begin()
73+
when (sourceEvent.actionMasked) {
74+
MotionEvent.ACTION_DOWN -> {
75+
initialize(event, sourceEvent)
76+
}
77+
78+
MotionEvent.ACTION_POINTER_DOWN -> {
79+
begin()
80+
}
81+
}
7582
}
83+
7684
scaleGestureDetector?.onTouchEvent(sourceEvent)
7785
scaleGestureDetector?.let {
7886
val point = transformPoint(PointF(it.focusX, it.focusY))
@@ -81,10 +89,10 @@ class PinchGestureHandler : GestureHandler() {
8189
}
8290

8391
if (sourceEvent.actionMasked == MotionEvent.ACTION_UP) {
84-
if (state == STATE_ACTIVE) {
85-
end()
86-
} else {
87-
fail()
92+
when (state) {
93+
STATE_UNDETERMINED -> cancel()
94+
STATE_ACTIVE -> end()
95+
else -> fail()
8896
}
8997
}
9098
}

packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/RotationGestureHandler.kt

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,17 @@ class RotationGestureHandler : GestureHandler() {
6161
}
6262

6363
if (state == STATE_UNDETERMINED) {
64-
initialize(event, sourceEvent)
65-
begin()
64+
when (sourceEvent.actionMasked) {
65+
MotionEvent.ACTION_DOWN -> {
66+
initialize(event, sourceEvent)
67+
}
68+
69+
MotionEvent.ACTION_POINTER_DOWN -> {
70+
begin()
71+
}
72+
}
6673
}
74+
6775
rotationGestureDetector?.onTouchEvent(sourceEvent)
6876
rotationGestureDetector?.let {
6977
val point = transformPoint(PointF(it.anchorX, it.anchorY))
@@ -73,8 +81,11 @@ class RotationGestureHandler : GestureHandler() {
7381

7482
// ACTION_UP is already handled in rotationGestureDetector.onTouchEvent (and effectively in onRotationEnd)
7583
// if more than one pointer was used
76-
if (sourceEvent.actionMasked == MotionEvent.ACTION_UP && state == STATE_BEGAN) {
77-
fail()
84+
if (sourceEvent.actionMasked == MotionEvent.ACTION_UP) {
85+
when (state) {
86+
STATE_UNDETERMINED -> cancel()
87+
STATE_BEGAN -> fail()
88+
}
7889
}
7990
}
8091

packages/react-native-gesture-handler/src/components/GestureHandlerButton.web.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as React from 'react';
22
import type { ColorValue, NativeSyntheticEvent, ViewProps } from 'react-native';
33
import { View } from 'react-native';
44

5+
import { NativeGestureRole } from '../web/interfaces';
56
import { GestureLifecycleEvent } from '../web/tools/GestureLifecycleEvents';
67

78
type ButtonProps = ViewProps & {
@@ -216,4 +217,6 @@ export const ButtonComponent = ({
216217
);
217218
};
218219

220+
ButtonComponent.displayName = NativeGestureRole.Button;
221+
219222
export default ButtonComponent;

packages/react-native-gesture-handler/src/v3/detectors/HostGestureDetector.web.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import type {
99
} from '../../handlers/gestureHandlerCommon';
1010
import RNGestureHandlerModule from '../../RNGestureHandlerModule.web';
1111
import { tagMessage } from '../../utils';
12-
import type { PropsRef } from '../../web/interfaces';
12+
import { type PropsRef } from '../../web/interfaces';
13+
import { useNativeGestureRole } from './useNativeGestureRole';
1314

1415
export interface GestureHandlerDetectorProps extends PropsRef {
1516
handlerTags: number[];
@@ -103,6 +104,8 @@ const HostGestureDetector = (props: GestureHandlerDetectorProps) => {
103104
});
104105
};
105106

107+
useNativeGestureRole(viewRef, children);
108+
106109
useEffect(() => {
107110
const shouldUpdateDOMProps =
108111
propsRef.current.userSelect !== props.userSelect ||

packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/VirtualDetector.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { tagMessage } from '../../../utils';
66
import { isComposedGesture } from '../../hooks/utils/relationUtils';
77
import type { DetectorCallbacks, VirtualChild } from '../../types';
88
import type { VirtualDetectorProps } from '../common';
9+
import { useNativeGestureRole } from '../useNativeGestureRole';
910
import { configureRelations } from '../utils';
1011
import {
1112
InterceptingDetectorMode,
@@ -54,6 +55,8 @@ export function VirtualDetector<
5455
[props.children]
5556
);
5657

58+
useNativeGestureRole(viewRef, props.children);
59+
5760
useEffect(() => {
5861
if (viewTag === -1) {
5962
return;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import type { ReactNode, RefObject } from 'react';
2+
3+
export function useNativeGestureRole(
4+
_viewRef: RefObject<unknown>,
5+
_children: ReactNode
6+
): void {
7+
// No-op on native; the web implementation lives in `useNativeGestureRole.web.ts`.
8+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import type { ReactNode, RefObject } from 'react';
2+
import { useEffect } from 'react';
3+
4+
import { NATIVE_GESTURE_ROLE_ATTRIBUTE } from '../../web/constants';
5+
import { NativeGestureRole } from '../../web/interfaces';
6+
7+
export function useNativeGestureRole(
8+
viewRef: RefObject<Element | null>,
9+
children: ReactNode
10+
): void {
11+
useEffect(() => {
12+
const child = viewRef.current?.firstChild as HTMLElement | undefined;
13+
14+
if (!child) {
15+
return;
16+
}
17+
18+
// @ts-ignore This exists on React.ReactNode
19+
const displayName = children?.type?.displayName as string;
20+
21+
if (displayName === NativeGestureRole.ScrollView) {
22+
child.setAttribute(
23+
NATIVE_GESTURE_ROLE_ATTRIBUTE,
24+
NativeGestureRole.ScrollView
25+
);
26+
} else if (displayName === NativeGestureRole.Switch) {
27+
child.setAttribute(
28+
NATIVE_GESTURE_ROLE_ATTRIBUTE,
29+
NativeGestureRole.Switch
30+
);
31+
} else if (displayName === NativeGestureRole.Button) {
32+
child.setAttribute(
33+
NATIVE_GESTURE_ROLE_ATTRIBUTE,
34+
NativeGestureRole.Button
35+
);
36+
}
37+
38+
return () => {
39+
child.removeAttribute(NATIVE_GESTURE_ROLE_ATTRIBUTE);
40+
};
41+
}, [children, viewRef]);
42+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export const DEFAULT_TOUCH_SLOP = 15;
22
export const MINIMAL_RECOGNIZABLE_MAGNITUDE = 0.1;
3+
export const NATIVE_GESTURE_ROLE_ATTRIBUTE = 'rngh-role';

0 commit comments

Comments
 (0)