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
259 changes: 255 additions & 4 deletions packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,22 @@
import GestureHandlerRootView from '../components/GestureHandlerRootView';
import { fireGestureHandler, getByGestureTestId } from '../jestUtils';
import { State } from '../State';
import { Pressable, RectButton, ScrollView, Touchable } from '../v3/components';
import { GestureDetector } from '../v3/detectors';
import {
Pressable,
RectButton,
RefreshControl,
ScrollView,
Touchable,
} from '../v3/components';
import {
GestureDetector,
InterceptingGestureDetector,
VirtualGestureDetector,
} from '../v3/detectors';
import { useSimultaneousGestures } from '../v3/hooks';
import {
useLongPressGesture,
useNativeGesture,
usePanGesture,
useTapGesture,
} from '../v3/hooks/gestures';
Expand Down Expand Up @@ -59,8 +70,8 @@
const panGesture = renderHook(() =>
usePanGesture({
disableReanimated: true,
onBegin: (e) => onBegin(e),

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

View workflow job for this annotation

GitHub Actions / check

Unsafe return of an `any` typed value
onActivate: (e) => onStart(e),

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

View workflow job for this annotation

GitHub Actions / check

Unsafe return of an `any` typed value
})
).result.current;

Expand Down Expand Up @@ -88,8 +99,8 @@
const panGesture = renderHook(() =>
usePanGesture({
disableReanimated: true,
onUpdate: (e) => onUpdate(e),

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

View workflow job for this annotation

GitHub Actions / check

Unsafe return of an `any` typed value
onTouchesUp: (e) => onTouchesUp(e),

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

View workflow job for this annotation

GitHub Actions / check

Unsafe return of an `any` typed value
})
).result.current;

Expand Down Expand Up @@ -189,14 +200,24 @@
);
};

const getNativeDetector = (
// Detector host views in document order (outer first).
const getNativeDetectors = (
views: ReturnType<typeof render>['UNSAFE_getAllByType']
) => {
return views(View).find(
return views(View).filter(
({ props }) => props.handlerTags && props.onStartShouldSetResponder
);
};

// The innermost detector: the GH ScrollView is a detector too, and it is
// rendered above the example's one.
const getNativeDetector = (
views: ReturnType<typeof render>['UNSAFE_getAllByType']
) => {
const detectors = getNativeDetectors(views);
return detectors[detectors.length - 1];
};

const TapGestureDetectorExample = () => {
const tap = useTapGesture({ disableReanimated: true });

Expand All @@ -217,6 +238,56 @@
);
};

const VirtualTapDetectorExample = () => {
const tap = useTapGesture({ disableReanimated: true });

return (
<VirtualGestureDetector gesture={tap}>
<View testID="virtual-child" />
</VirtualGestureDetector>
);
};

const VirtualPanDetectorExample = () => {
const pan = usePanGesture({ disableReanimated: true });

return (
<VirtualGestureDetector gesture={pan}>
<View testID="virtual-child" />
</VirtualGestureDetector>
);
};

const VirtualNativeDetectorExample = () => {
const native = useNativeGesture({ disableReanimated: true });

return (
<VirtualGestureDetector gesture={native}>
<View testID="virtual-child" />
</VirtualGestureDetector>
);
};

const InterceptingTapDetectorExample = () => {
const tap = useTapGesture({ disableReanimated: true });

return (
<InterceptingGestureDetector gesture={tap}>
<View />
</InterceptingGestureDetector>
);
};

const InterceptingNativeDetectorExample = () => {
const native = useNativeGesture({ disableReanimated: true });

return (
<InterceptingGestureDetector gesture={native}>
<View />
</InterceptingGestureDetector>
);
};

const SimultaneousGestureDetectorExample = () => {
const tap = useTapGesture({ disableReanimated: true });
const pan = usePanGesture({ disableReanimated: true });
Expand Down Expand Up @@ -370,6 +441,149 @@
expect(scrollViewResponder?.props.onStartShouldSetResponder()).toBe(true);
});

test('handles responder event passed through VirtualDetector for supported gestures', async () => {
const { UNSAFE_getAllByType } = render(
<GestureHandlerRootView>
<ScrollView keyboardShouldPersistTaps="handled">
<VirtualTapDetectorExample />
</ScrollView>
</GestureHandlerRootView>
);

await act(flushImmediate);

const scrollViewResponder = getScrollViewResponder(UNSAFE_getAllByType);
const virtualChild = screen.getByTestId('virtual-child');

expect(scrollViewResponder).toBeDefined();
expect(
scrollViewResponder?.props.onStartShouldSetResponderCapture()
).toBe(false);
expect(virtualChild.props.onStartShouldSetResponderCapture()).toBe(false);
expect(scrollViewResponder?.props.onStartShouldSetResponder()).toBe(true);
});

test('handles responder event passed through VirtualDetector for native gestures', async () => {
const { UNSAFE_getAllByType } = render(
<GestureHandlerRootView>
<ScrollView keyboardShouldPersistTaps="handled">
<VirtualNativeDetectorExample />
</ScrollView>
</GestureHandlerRootView>
);

await act(flushImmediate);

const scrollViewResponder = getScrollViewResponder(UNSAFE_getAllByType);
const virtualChild = screen.getByTestId('virtual-child');

expect(scrollViewResponder).toBeDefined();
expect(
scrollViewResponder?.props.onStartShouldSetResponderCapture()
).toBe(false);
expect(virtualChild.props.onStartShouldSetResponderCapture()).toBe(false);
expect(scrollViewResponder?.props.onStartShouldSetResponder()).toBe(true);
});

test('does not handle responder event passed through VirtualDetector for unsupported gestures', async () => {
const { UNSAFE_getAllByType } = render(
<GestureHandlerRootView>
<ScrollView keyboardShouldPersistTaps="handled">
<VirtualPanDetectorExample />
</ScrollView>
</GestureHandlerRootView>
);

await act(flushImmediate);

const scrollViewResponder = getScrollViewResponder(UNSAFE_getAllByType);
const virtualChild = screen.getByTestId('virtual-child');

expect(scrollViewResponder).toBeDefined();
expect(
scrollViewResponder?.props.onStartShouldSetResponderCapture()
).toBe(false);
expect(virtualChild.props.onStartShouldSetResponderCapture()).toBe(false);
expect(scrollViewResponder?.props.onStartShouldSetResponder()).toBe(
false
);
});

test('handles responder event passed through InterceptingGestureDetector for supported gestures', async () => {
const { UNSAFE_getAllByType } = render(
<GestureHandlerRootView>
<ScrollView keyboardShouldPersistTaps="handled">
<InterceptingTapDetectorExample />
</ScrollView>
</GestureHandlerRootView>
);

await act(flushImmediate);

const detectors = getNativeDetectors(UNSAFE_getAllByType);
const scrollViewResponder = getScrollViewResponder(UNSAFE_getAllByType);
// The GH ScrollView is the outer intercepting detector, the example is the inner one.
const interceptingDetector = detectors[detectors.length - 1];

expect(interceptingDetector).toBeDefined();
expect(scrollViewResponder).toBeDefined();
expect(
scrollViewResponder?.props.onStartShouldSetResponderCapture()
).toBe(false);
expect(interceptingDetector?.props.onStartShouldSetResponder()).toBe(
false
);
expect(scrollViewResponder?.props.onStartShouldSetResponder()).toBe(true);
});

test('does not handle responder event passed through InterceptingGestureDetector for native gestures', async () => {
const { UNSAFE_getAllByType } = render(
<GestureHandlerRootView>
<ScrollView keyboardShouldPersistTaps="handled">
<InterceptingNativeDetectorExample />
</ScrollView>
</GestureHandlerRootView>
);

await act(flushImmediate);

const detectors = getNativeDetectors(UNSAFE_getAllByType);
const scrollViewResponder = getScrollViewResponder(UNSAFE_getAllByType);
const interceptingDetector = detectors[detectors.length - 1];

expect(interceptingDetector).toBeDefined();
expect(scrollViewResponder).toBeDefined();
expect(
scrollViewResponder?.props.onStartShouldSetResponderCapture()
).toBe(false);
expect(interceptingDetector?.props.onStartShouldSetResponder()).toBe(
false
);
expect(scrollViewResponder?.props.onStartShouldSetResponder()).toBe(
false
);
});

test('does not handle responder event passed through the native gesture of ScrollView itself', async () => {
const { UNSAFE_getAllByType } = render(
<GestureHandlerRootView>
<ScrollView keyboardShouldPersistTaps="handled" />
</GestureHandlerRootView>
);

await act(flushImmediate);

const [scrollViewDetector] = getNativeDetectors(UNSAFE_getAllByType);
const scrollViewResponder = getScrollViewResponder(UNSAFE_getAllByType);

expect(scrollViewDetector).toBeDefined();
expect(scrollViewResponder).toBeDefined();
expect(scrollViewDetector?.props.onStartShouldSetResponder()).toBe(false);
expect(scrollViewResponder?.props.onStartShouldSetResponder()).toBe(
false
);
});

test('does not handle responder event passed through NativeDetector for unsupported gestures', async () => {
const { UNSAFE_getAllByType } = render(
<GestureHandlerRootView>
Expand Down Expand Up @@ -489,6 +703,43 @@
false
);
});

test('does not handle responder event passed through RefreshControl', async () => {
const warn = jest
.spyOn(console, 'warn')
.mockImplementation(() => undefined);
const { UNSAFE_root, UNSAFE_getAllByType } = render(
<GestureHandlerRootView>
<ScrollView
keyboardShouldPersistTaps="handled"
refreshControl={<RefreshControl refreshing={false} />}
/>
</GestureHandlerRootView>
);

await act(flushImmediate);

const scrollViewResponder = getScrollViewResponder(UNSAFE_getAllByType);
// The native mock drops the props, so look at the element RN renders
const refreshControl = UNSAFE_root.find(
({ props }) =>
props.refreshing !== undefined &&
props.onStartShouldSetResponderCapture
);

expect(scrollViewResponder).toBeDefined();
expect(refreshControl.props.onStartShouldSetResponderCapture()).toBe(
false
);
expect(scrollViewResponder?.props.onStartShouldSetResponder()).toBe(
false
);
// The opt-out is an internal config prop, it must not reach the native side
expect(warn).not.toHaveBeenCalledWith(
expect.stringContaining('countNativeAsHandled')
);
warn.mockRestore();
});
});

describe('keyboardShouldPersistTaps="never" drop', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ export const RefreshControl: React.ComponentType<
disallowInterruption: true,
shouldCancelWhenOutside: false,
},
GestureDetectorType.Virtual
GestureDetectorType.Virtual,
// On Android it is the parent of the whole list, so it must not mark taps
false
);

// eslint-disable-next-line @typescript-eslint/no-redeclare
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { VirtualDetector } from './detectors/VirtualDetector/VirtualDetector';
import { useNativeGesture } from './hooks/gestures';
import type { NativeGestureConfig } from './hooks/gestures/native/NativeTypes';
import { NativeWrapperProps } from './hooks/utils';
import type { InternalConfigProps } from './types';
import type {
NativeWrapperProperties,
WrapperSpecificProperties,
Expand All @@ -16,7 +17,10 @@ export default function createNativeWrapper<TRef = unknown, TProps = unknown>(
config: Readonly<
Omit<NativeWrapperProperties<TRef>, keyof WrapperSpecificProperties<TRef>>
> = {},
detectorType: GestureDetectorType = GestureDetectorType.Native
detectorType: GestureDetectorType = GestureDetectorType.Native,
// Whether a tap on the wrapped component counts as handled, see
// `useJSResponderHandler`
countNativeAsHandled = true
) {
const ComponentWrapper = (
props: TProps & NativeWrapperProperties<TRef | null>
Expand Down Expand Up @@ -51,6 +55,12 @@ export default function createNativeWrapper<TRef = unknown, TProps = unknown>(
// detector, whatever the caller passed.
gestureHandlerProps.disableReanimated = true;

if (!countNativeAsHandled) {
(
gestureHandlerProps as InternalConfigProps<unknown>
).countNativeAsHandled = false;
}

const native = useNativeGesture(gestureHandlerProps);

useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
import { Reanimated } from '../../../handlers/gestures/reanimatedWrapper';
import { getModuleId } from '../../../moduleId';
import { tagMessage } from '../../../utils';
import { useJSResponderHandler } from '../../hooks/useJSResponderHandler';
import { isComposedGesture } from '../../hooks/utils/relationUtils';
import type {
DetectorCallbacks,
Expand Down Expand Up @@ -55,6 +56,12 @@ export function InterceptingGestureDetector<
TExtendedHandlerData
>) {
useEnsureGestureHandlerRootView();
// A Native gesture here belongs to the container hosting virtual children
// (a scroller, a pager), not to a tap target
const { handleStartShouldSetResponder } = useJSResponderHandler(
gesture,
false
);

const [virtualChildren, setVirtualChildren] = useState<Set<VirtualChild>>(
() => new Set()
Expand Down Expand Up @@ -299,6 +306,7 @@ export function InterceptingGestureDetector<
: undefined
}
handlerTags={handlerTags}
onStartShouldSetResponder={handleStartShouldSetResponder}
style={nativeDetectorStyles.detector}
virtualChildren={strippedVirtualChildren}
moduleId={getModuleId()}>
Expand Down
Loading
Loading