Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import { VirtualDetector } from '../v3/detectors/VirtualDetector/VirtualDetector';

jest.mock('react-native-worklets', () =>
require('react-native-worklets/src/mock')

Check warning on line 15 in packages/react-native-gesture-handler/src/__tests__/Errors.test.tsx

View workflow job for this annotation

GitHub Actions / check

Unsafe return of an `any` typed value
);

beforeEach(() => cleanup());
Expand Down Expand Up @@ -83,6 +83,71 @@
});
});

describe('Sharing a gesture across detectors', () => {
beforeEach(() => (findNodeHandle as jest.Mock).mockReturnValue(123));

test('throws when the same gesture is attached to two detectors', () => {
function SameGestureTwoDetectors() {
const tap = useTapGesture({});
return (
<GestureHandlerRootView>
<GestureDetector gesture={tap}>
<View />
</GestureDetector>
<GestureDetector gesture={tap}>
<View />
</GestureDetector>
</GestureHandlerRootView>
);
}

expect(() => render(<SameGestureTwoDetectors />)).toThrow(
'Using the same gesture instance across multiple GestureDetectors is not possible. Create a separate gesture for each detector.'
);
});

test('does not throw when the same gesture is reattached to another detector', () => {
// Changing the key unmounts the previous detector and mounts a new one with
// the same gesture - the old detector must release the gesture before the
// new one claims it.
function ReattachedGesture({ detectorKey }: { detectorKey: string }) {
const tap = useTapGesture({});
return (
<GestureHandlerRootView>
<GestureDetector key={detectorKey} gesture={tap}>
<View />
</GestureDetector>
</GestureHandlerRootView>
);
}

const { rerender } = render(<ReattachedGesture detectorKey="a" />);
expect(() => rerender(<ReattachedGesture detectorKey="b" />)).not.toThrow();
});
Comment on lines +109 to +126

test('throws when the same gesture is attached to two virtual detectors', () => {
function SameGestureTwoVirtualDetectors() {
const tap = useTapGesture({});
return (
<GestureHandlerRootView>
<InterceptingGestureDetector>
<VirtualDetector gesture={tap}>
<View />
</VirtualDetector>
<VirtualDetector gesture={tap}>
<View />
</VirtualDetector>
</InterceptingGestureDetector>
</GestureHandlerRootView>
);
}

expect(() => render(<SameGestureTwoVirtualDetectors />)).toThrow(
'Using the same gesture instance across multiple GestureDetectors is not possible. Create a separate gesture for each detector.'
);
});
});

describe('Check if descendant of root view', () => {
test('gesture detector', () => {
function GestureDetectorNoRootView() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { NativeDetectorProps } from './common';
import { AnimatedNativeDetector, nativeDetectorStyles } from './common';
import HostGestureDetector from './HostGestureDetector';
import { ReanimatedNativeDetector } from './ReanimatedNativeDetector';
import { useDetectorAttachmentGuard } from './useDetectorAttachmentGuard';
import { useGestureRelationsUpdater } from './useGestureRelationsUpdater';
import { ensureNativeDetectorComponent } from './utils';

Expand Down Expand Up @@ -38,6 +39,8 @@ export function NativeDetector<
: [gesture.handlerTag];
}, [gesture]);

useDetectorAttachmentGuard(handlerTags);

// On web, we're triggering Reanimated callbacks ourselves, based on the type.
// To handle this properly, we need to provide all three callbacks, so we set
// all three to the Reanimated event handler.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type { InterceptingGestureDetectorProps } from '../common';
import { AnimatedNativeDetector, nativeDetectorStyles } from '../common';
import HostGestureDetector from '../HostGestureDetector';
import { ReanimatedNativeDetector } from '../ReanimatedNativeDetector';
import { useDetectorAttachmentGuard } from '../useDetectorAttachmentGuard';
import { useEnsureGestureHandlerRootView } from '../useEnsureGestureHandlerRootView';
import { useGestureRelationsUpdater } from '../useGestureRelationsUpdater';
import { ensureNativeDetectorComponent } from '../utils';
Expand Down Expand Up @@ -233,6 +234,8 @@ export function InterceptingGestureDetector<
return [];
}, [gesture]);

useDetectorAttachmentGuard(handlerTags);

// On web, we're triggering Reanimated callbacks ourselves, based on the type.
// To handle this properly, we need to provide all three callbacks, so we set
// all three to the Reanimated event handler.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { findNodeHandle, Platform } from 'react-native';

import { Wrap } from '../../../handlers/gestures/GestureDetector/Wrap';
import { tagMessage } from '../../../utils';
import { isComposedGesture } from '../../hooks/utils/relationUtils';
import type { DetectorCallbacks, VirtualChild } from '../../types';
import type { VirtualDetectorProps } from '../common';
import { useDetectorAttachmentGuard } from '../useDetectorAttachmentGuard';
import { useGestureRelationsUpdater } from '../useGestureRelationsUpdater';
import { useNativeGestureRole } from '../useNativeGestureRole';
import {
Expand Down Expand Up @@ -57,15 +58,21 @@ export function VirtualDetector<

useNativeGestureRole(viewRef, props.children);

const handlerTags = useMemo(
() =>
isComposedGesture(props.gesture)
? props.gesture.handlerTags
: [props.gesture.handlerTag],
[props.gesture]
);

useDetectorAttachmentGuard(handlerTags);

useEffect(() => {
if (viewTag === -1) {
return;
}

const handlerTags = isComposedGesture(props.gesture)
? props.gesture.handlerTags
: [props.gesture.handlerTag];

if (props.gesture.config.dispatchesAnimatedEvents) {
throw new Error(
tagMessage(
Expand Down Expand Up @@ -96,6 +103,7 @@ export function VirtualDetector<
}, [
viewTag,
props.gesture,
handlerTags,
props.userSelect,
props.touchAction,
props.enableContextMenu,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { useEffect, useRef } from 'react';

import { tagMessage } from '../../utils';

// Maps a gesture's handlerTag to the id of the detector currently rendering it.
// Ownership is claimed in an effect and released in its cleanup (on unmount or
// when handlerTags change), so moving a gesture instance between detectors works
// as long as it isn't rendered by more than one detector at the same time.
const detectorByHandlerTag = new Map<number, number>();

let nextDetectorId = 0;

export function useDetectorAttachmentGuard(handlerTags: number[]) {
const detectorId = useRef(-1);

if (detectorId.current === -1) {
detectorId.current = nextDetectorId++;
}

useEffect(() => {
if (!__DEV__) {
return;
}

const id = detectorId.current;

for (const tag of handlerTags) {
const owner = detectorByHandlerTag.get(tag);

if (owner !== undefined && owner !== id) {
throw new Error(
tagMessage(
'Using the same gesture instance across multiple GestureDetectors is not possible. Create a separate gesture for each detector.'
)
);
}
}

for (const tag of handlerTags) {
detectorByHandlerTag.set(tag, id);
}

return () => {
for (const tag of handlerTags) {
if (detectorByHandlerTag.get(tag) === id) {
detectorByHandlerTag.delete(tag);
}
}
};
}, [handlerTags]);
}
Loading