diff --git a/.github/workflows/check-relations-traversal-algorithm.yml b/.github/workflows/rngh-api-v3.yml
similarity index 83%
rename from .github/workflows/check-relations-traversal-algorithm.yml
rename to .github/workflows/rngh-api-v3.yml
index e15de660ac..12b208aeaf 100644
--- a/.github/workflows/check-relations-traversal-algorithm.yml
+++ b/.github/workflows/rngh-api-v3.yml
@@ -1,10 +1,11 @@
-name: Test relations traversal algorithm
+name: Test Gesture Handler 3 API
on:
pull_request:
paths:
- packages/react-native-gesture-handler/src/v3/**
- packages/react-native-gesture-handler/src/__tests__/RelationsTraversal.test.tsx
+ - packages/react-native-gesture-handler/src/__tests__/API_V3.test.tsx
push:
branches:
- main
@@ -34,4 +35,4 @@ jobs:
- name: Run tests
working-directory: packages/react-native-gesture-handler
- run: yarn test RelationsTraversal
+ run: yarn test RelationsTraversal API_V3
diff --git a/packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsx b/packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsx
new file mode 100644
index 0000000000..f3f8758f0c
--- /dev/null
+++ b/packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsx
@@ -0,0 +1,60 @@
+import { usePanGesture } from '../v3/hooks/gestures';
+import { render, renderHook } from '@testing-library/react-native';
+import { fireGestureHandler, getByGestureTestId } from '../jestUtils';
+import { State } from '../State';
+import GestureHandlerRootView from '../components/GestureHandlerRootView';
+import { RectButton } from '../v3/components';
+import { act } from 'react';
+
+describe('[API v3] Hooks', () => {
+ test('Pan gesture', () => {
+ const onBegin = jest.fn();
+ const onStart = jest.fn();
+
+ const panGesture = renderHook(() =>
+ usePanGesture({
+ disableReanimated: true,
+ onBegin: (e) => onBegin(e),
+ onActivate: (e) => onStart(e),
+ })
+ ).result.current;
+
+ fireGestureHandler(panGesture, [
+ { oldState: State.UNDETERMINED, state: State.BEGAN },
+ { oldState: State.BEGAN, state: State.ACTIVE },
+ { oldState: State.ACTIVE, state: State.ACTIVE },
+ { oldState: State.ACTIVE, state: State.END },
+ ]);
+
+ expect(onBegin).toHaveBeenCalledTimes(1);
+ expect(onStart).toHaveBeenCalledTimes(1);
+ });
+});
+
+describe('[API v3] Components', () => {
+ test('Rect Button', () => {
+ const pressFn = jest.fn();
+
+ const RectButtonExample = () => {
+ return (
+
+
+
+ );
+ };
+
+ render();
+
+ const nativeGesture = getByGestureTestId('btn');
+
+ act(() => {
+ fireGestureHandler(nativeGesture, [
+ { oldState: State.UNDETERMINED, state: State.BEGAN },
+ { oldState: State.BEGAN, state: State.ACTIVE },
+ { oldState: State.ACTIVE, state: State.END },
+ ]);
+ });
+
+ expect(pressFn).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/packages/react-native-gesture-handler/src/handlers/handlersRegistry.ts b/packages/react-native-gesture-handler/src/handlers/handlersRegistry.ts
index b60fc2d79d..0996d75075 100644
--- a/packages/react-native-gesture-handler/src/handlers/handlersRegistry.ts
+++ b/packages/react-native-gesture-handler/src/handlers/handlersRegistry.ts
@@ -1,12 +1,37 @@
import { isTestEnv } from '../utils';
import { GestureType } from './gestures/gesture';
import { GestureEvent, HandlerStateChangeEvent } from './gestureHandlerCommon';
+import { SingleGesture } from '../v3/types';
export const handlerIDToTag: Record = {};
+
+// There were attempts to create types that merge possible HandlerData and Config,
+// but ts was not able to infer them properly in many cases, so we use any here.
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+const hookGestures = new Map>();
const gestures = new Map();
const oldHandlers = new Map();
const testIDs = new Map();
+export function registerGesture(
+ handlerTag: number,
+ gesture: SingleGesture
+) {
+ if (isTestEnv() && gesture.config.testID) {
+ hookGestures.set(handlerTag, gesture);
+ testIDs.set(gesture.config.testID, handlerTag);
+ }
+}
+
+export function unregisterGesture(handlerTag: number) {
+ const gesture = hookGestures.get(handlerTag);
+
+ if (gesture && isTestEnv() && gesture.config.testID) {
+ testIDs.delete(gesture.config.testID);
+ hookGestures.delete(handlerTag);
+ }
+}
+
export function registerHandler(
handlerTag: number,
handler: GestureType,
@@ -40,6 +65,10 @@ export function findHandler(handlerTag: number) {
return gestures.get(handlerTag);
}
+export function findGesture(handlerTag: number) {
+ return hookGestures.get(handlerTag);
+}
+
export function findOldGestureHandler(handlerTag: number) {
return oldHandlers.get(handlerTag);
}
@@ -47,7 +76,7 @@ export function findOldGestureHandler(handlerTag: number) {
export function findHandlerByTestID(testID: string) {
const handlerTag = testIDs.get(testID);
if (handlerTag !== undefined) {
- return findHandler(handlerTag) ?? null;
+ return findHandler(handlerTag) ?? findGesture(handlerTag) ?? null;
}
return null;
}
diff --git a/packages/react-native-gesture-handler/src/jestUtils/jestUtils.ts b/packages/react-native-gesture-handler/src/jestUtils/jestUtils.ts
index 6f30923caf..ee7a951c57 100644
--- a/packages/react-native-gesture-handler/src/jestUtils/jestUtils.ts
+++ b/packages/react-native-gesture-handler/src/jestUtils/jestUtils.ts
@@ -60,6 +60,8 @@ import {
} from '../handlers/TapGestureHandler';
import { State } from '../State';
import { hasProperty, withPrevAndCurrent } from '../utils';
+import type { SingleGesture } from '../v3/types';
+import { maybeUnpackValue } from '../v3/hooks/utils';
// Load fireEvent conditionally, so RNGH may be used in setups without testing-library
let fireEvent = (
@@ -164,11 +166,17 @@ const handlersDefaultEvents: DefaultEventsMapping = {
};
function isGesture(
- componentOrGesture: ReactTestInstance | GestureType
+ componentOrGesture: ReactTestInstance | GestureType | SingleGesture
): componentOrGesture is GestureType {
return componentOrGesture instanceof BaseGesture;
}
+function isHookGesture(
+ componentOrGesture: ReactTestInstance | SingleGesture
+): componentOrGesture is SingleGesture {
+ return 'detectorCallbacks' in componentOrGesture;
+}
+
interface WrappedGestureHandlerTestEvent {
nativeEvent: GestureHandlerTestEvent;
}
@@ -408,7 +416,7 @@ interface HandlerData {
enabled: boolean | undefined;
}
function getHandlerData(
- componentOrGesture: ReactTestInstance | GestureType
+ componentOrGesture: ReactTestInstance | GestureType | SingleGesture
): HandlerData {
if (isGesture(componentOrGesture)) {
const gesture = componentOrGesture;
@@ -421,6 +429,33 @@ function getHandlerData(
enabled: gesture.config.enabled,
};
}
+
+ if (isHookGesture(componentOrGesture)) {
+ return {
+ handlerType: componentOrGesture.type as HandlerNames,
+ handlerTag: componentOrGesture.tag,
+ enabled: maybeUnpackValue(componentOrGesture.config.enabled),
+ emitEvent: (eventName, args) => {
+ const { state, oldState, handlerTag, ...rest } = args.nativeEvent;
+
+ const event = {
+ state,
+ handlerTag,
+ handlerData: { ...rest },
+ };
+
+ if (eventName === 'onGestureHandlerStateChange') {
+ componentOrGesture.detectorCallbacks.onGestureHandlerStateChange({
+ oldState: oldState as State,
+ ...event,
+ });
+ } else if (eventName === 'onGestureHandlerEvent') {
+ componentOrGesture.detectorCallbacks.onGestureHandlerEvent?.(event);
+ }
+ },
+ };
+ }
+
const gestureHandlerComponent = componentOrGesture;
return {
emitEvent: (eventName, args) => {
@@ -465,7 +500,7 @@ type ExtractConfig =
: Record;
export function fireGestureHandler(
- componentOrGesture: ReactTestInstance | GestureType,
+ componentOrGesture: ReactTestInstance | GestureType | SingleGesture,
eventList: Partial>>[] = []
): void {
const { emitEvent, handlerType, handlerTag, enabled } =
diff --git a/packages/react-native-gesture-handler/src/v3/hooks/useGesture.ts b/packages/react-native-gesture-handler/src/v3/hooks/useGesture.ts
index 3f0bdbee05..98b49879a8 100644
--- a/packages/react-native-gesture-handler/src/v3/hooks/useGesture.ts
+++ b/packages/react-native-gesture-handler/src/v3/hooks/useGesture.ts
@@ -10,6 +10,11 @@ import {
} from './utils';
import { tagMessage } from '../../utils';
import { BaseGestureConfig, SingleGesture, SingleGestureName } from '../types';
+import { scheduleFlushOperations } from '../../handlers/utils';
+import {
+ registerGesture,
+ unregisterGesture,
+} from '../../handlers/handlersRegistry';
import { Platform } from 'react-native';
import { NativeProxy } from '../NativeProxy';
@@ -64,25 +69,8 @@ export function useGesture(
NativeProxy.createGestureHandler(type, tag, {});
}
- useEffect(() => {
- return () => {
- NativeProxy.dropGestureHandler(tag);
- };
- }, [type, tag]);
-
- useEffect(() => {
- const preparedConfig = prepareConfigForNativeSide(type, config);
- NativeProxy.setGestureHandlerConfig(tag, preparedConfig);
-
- bindSharedValues(config, tag);
-
- return () => {
- unbindSharedValues(config, tag);
- };
- }, [tag, config, type]);
-
- return useMemo(
- (): SingleGesture => ({
+ const gesture = useMemo(
+ () => ({
tag,
type,
config,
@@ -121,4 +109,27 @@ export function useGesture(
gestureRelations,
]
);
+
+ useEffect(() => {
+ return () => {
+ NativeProxy.dropGestureHandler(tag);
+ scheduleFlushOperations();
+ };
+ }, [type, tag]);
+
+ useEffect(() => {
+ const preparedConfig = prepareConfigForNativeSide(type, config);
+ NativeProxy.setGestureHandlerConfig(tag, preparedConfig);
+ scheduleFlushOperations();
+
+ bindSharedValues(config, tag);
+ registerGesture(tag, gesture);
+
+ return () => {
+ unbindSharedValues(config, tag);
+ unregisterGesture(tag);
+ };
+ }, [tag, config, type, gesture]);
+
+ return gesture;
}
diff --git a/packages/react-native-gesture-handler/src/v3/hooks/utils/propsWhiteList.ts b/packages/react-native-gesture-handler/src/v3/hooks/utils/propsWhiteList.ts
index 29a19e52e2..e59ae78643 100644
--- a/packages/react-native-gesture-handler/src/v3/hooks/utils/propsWhiteList.ts
+++ b/packages/react-native-gesture-handler/src/v3/hooks/utils/propsWhiteList.ts
@@ -24,6 +24,7 @@ const CommonConfig = new Set([
'mouseButton',
'enableContextMenu',
'touchAction',
+ 'testID',
]);
const ExternalRelationsConfig = new Set([
diff --git a/packages/react-native-gesture-handler/src/v3/types/ConfigTypes.ts b/packages/react-native-gesture-handler/src/v3/types/ConfigTypes.ts
index aa043e8249..697589e213 100644
--- a/packages/react-native-gesture-handler/src/v3/types/ConfigTypes.ts
+++ b/packages/react-native-gesture-handler/src/v3/types/ConfigTypes.ts
@@ -45,6 +45,7 @@ export type InternalConfigProps = {
export type CommonGestureConfig = {
disableReanimated?: boolean;
useAnimated?: boolean;
+ testID?: string;
} & WithSharedValue<
{
runOnJS?: boolean;