chore: add mobile app

This commit is contained in:
2026-02-07 22:33:02 +01:00
parent db34640acc
commit e89384ad4e
29327 changed files with 3886944 additions and 12 deletions
@@ -0,0 +1,64 @@
import type { FlingGestureHandlerEventPayload } from './GestureHandlerEventPayload';
import createHandler from './createHandler';
import {
BaseGestureHandlerProps,
baseGestureHandlerProps,
} from './gestureHandlerCommon';
export const flingGestureHandlerProps = [
'numberOfPointers',
'direction',
] as const;
export interface FlingGestureConfig {
/**
* Expressed allowed direction of movement. It's possible to pass one or many
* directions in one parameter:
*
* ```js
* direction={Directions.RIGHT | Directions.LEFT}
* ```
*
* or
*
* ```js
* direction={Directions.DOWN}
* ```
*/
direction?: number;
/**
* Determine exact number of points required to handle the fling gesture.
*/
numberOfPointers?: number;
}
/**
* @deprecated FlingGestureHandler will be removed in the future version of Gesture Handler. Use `Gesture.Fling()` instead.
*/
export interface FlingGestureHandlerProps
extends BaseGestureHandlerProps<FlingGestureHandlerEventPayload>,
FlingGestureConfig {}
export const flingHandlerName = 'FlingGestureHandler';
/**
* @deprecated FlingGestureHandler will be removed in the future version of Gesture Handler. Use `Gesture.Fling()` instead.
*/
export type FlingGestureHandler = typeof FlingGestureHandler;
/**
* @deprecated FlingGestureHandler will be removed in the future version of Gesture Handler. Use `Gesture.Fling()` instead.
*/
// eslint-disable-next-line @typescript-eslint/no-redeclare -- backward compatibility; see description on the top of gestureHandlerCommon.ts file
export const FlingGestureHandler = createHandler<
FlingGestureHandlerProps,
FlingGestureHandlerEventPayload
>({
name: flingHandlerName,
allowedProps: [
...baseGestureHandlerProps,
...flingGestureHandlerProps,
] as const,
config: {},
});
@@ -0,0 +1,88 @@
import React, { PropsWithChildren } from 'react';
import { tagMessage } from '../utils';
import PlatformConstants from '../PlatformConstants';
import createHandler from './createHandler';
import {
BaseGestureHandlerProps,
baseGestureHandlerProps,
} from './gestureHandlerCommon';
import type { ForceTouchGestureHandlerEventPayload } from './GestureHandlerEventPayload';
export const forceTouchGestureHandlerProps = [
'minForce',
'maxForce',
'feedbackOnActivation',
] as const;
// implicit `children` prop has been removed in @types/react^18.0.0
class ForceTouchFallback extends React.Component<PropsWithChildren<unknown>> {
static forceTouchAvailable = false;
componentDidMount() {
console.warn(
tagMessage(
'ForceTouchGestureHandler is not available on this platform. Please use ForceTouchGestureHandler.forceTouchAvailable to conditionally render other components that would provide a fallback behavior specific to your usecase'
)
);
}
render() {
return this.props.children;
}
}
export interface ForceTouchGestureConfig {
/**
*
* A minimal pressure that is required before handler can activate. Should be a
* value from range `[0.0, 1.0]`. Default is `0.2`.
*/
minForce?: number;
/**
* A maximal pressure that could be applied for handler. If the pressure is
* greater, handler fails. Should be a value from range `[0.0, 1.0]`.
*/
maxForce?: number;
/**
* Boolean value defining if haptic feedback has to be performed on
* activation.
*/
feedbackOnActivation?: boolean;
}
/**
* @deprecated ForceTouchGestureHandler will be removed in the future version of Gesture Handler. Use `Gesture.ForceTouch()` instead.
*/
export interface ForceTouchGestureHandlerProps
extends BaseGestureHandlerProps<ForceTouchGestureHandlerEventPayload>,
ForceTouchGestureConfig {}
/**
* @deprecated ForceTouchGestureHandler will be removed in the future version of Gesture Handler. Use `Gesture.ForceTouch()` instead.
*/
export type ForceTouchGestureHandler = typeof ForceTouchGestureHandler & {
forceTouchAvailable: boolean;
};
export const forceTouchHandlerName = 'ForceTouchGestureHandler';
/**
* @deprecated ForceTouchGestureHandler will be removed in the future version of Gesture Handler. Use `Gesture.ForceTouch()` instead.
*/
// eslint-disable-next-line @typescript-eslint/no-redeclare -- backward compatibility; see description on the top of gestureHandlerCommon.ts file
export const ForceTouchGestureHandler = PlatformConstants?.forceTouchAvailable
? createHandler<
ForceTouchGestureHandlerProps,
ForceTouchGestureHandlerEventPayload
>({
name: forceTouchHandlerName,
allowedProps: [
...baseGestureHandlerProps,
...forceTouchGestureHandlerProps,
] as const,
config: {},
})
: ForceTouchFallback;
(ForceTouchGestureHandler as ForceTouchGestureHandler).forceTouchAvailable =
PlatformConstants?.forceTouchAvailable || false;
@@ -0,0 +1,229 @@
import { StylusData } from '../web/interfaces';
export type FlingGestureHandlerEventPayload = {
x: number;
y: number;
absoluteX: number;
absoluteY: number;
};
/**
* @deprecated ForceTouch gesture is deprecated and will be removed in the future.
*/
export type ForceTouchGestureHandlerEventPayload = {
x: number;
y: number;
absoluteX: number;
absoluteY: number;
/**
* The pressure of a touch.
*/
force: number;
};
export type LongPressGestureHandlerEventPayload = {
/**
* X coordinate, expressed in points, of the current position of the pointer
* (finger or a leading pointer when there are multiple fingers placed)
* relative to the view attached to the handler.
*/
x: number;
/**
* Y coordinate, expressed in points, of the current position of the pointer
* (finger or a leading pointer when there are multiple fingers placed)
* relative to the view attached to the handler.
*/
y: number;
/**
* X coordinate, expressed in points, of the current position of the pointer
* (finger or a leading pointer when there are multiple fingers placed)
* relative to the window. It is recommended to use `absoluteX` instead of
* `x` in cases when the view attached to the handler can be transformed as an
* effect of the gesture.
*/
absoluteX: number;
/**
* Y coordinate, expressed in points, of the current position of the pointer
* (finger or a leading pointer when there are multiple fingers placed)
* relative to the window. It is recommended to use `absoluteY` instead of
* `y` in cases when the view attached to the handler can be transformed as an
* effect of the gesture.
*/
absoluteY: number;
/**
* Duration of the long press (time since the start of the event), expressed
* in milliseconds.
*/
duration: number;
};
export type NativeViewGestureHandlerPayload = {
/**
* True if gesture was performed inside of containing view, false otherwise.
*/
pointerInside: boolean;
};
export type PanGestureHandlerEventPayload = {
/**
* X coordinate of the current position of the pointer (finger or a leading
* pointer when there are multiple fingers placed) relative to the view
* attached to the handler. Expressed in point units.
*/
x: number;
/**
* Y coordinate of the current position of the pointer (finger or a leading
* pointer when there are multiple fingers placed) relative to the view
* attached to the handler. Expressed in point units.
*/
y: number;
/**
* X coordinate of the current position of the pointer (finger or a leading
* pointer when there are multiple fingers placed) relative to the window.
* The value is expressed in point units. It is recommended to use it instead
* of `x` in cases when the original view can be transformed as an effect of
* the gesture.
*/
absoluteX: number;
/**
* Y coordinate of the current position of the pointer (finger or a leading
* pointer when there are multiple fingers placed) relative to the window.
* The value is expressed in point units. It is recommended to use it instead
* of `y` in cases when the original view can be transformed as an
* effect of the gesture.
*/
absoluteY: number;
/**
* Translation of the pan gesture along X axis accumulated over the time of
* the gesture. The value is expressed in the point units.
*/
translationX: number;
/**
* Translation of the pan gesture along Y axis accumulated over the time of
* the gesture. The value is expressed in the point units.
*/
translationY: number;
/**
* Velocity of the pan gesture along the X axis in the current moment. The
* value is expressed in point units per second.
*/
velocityX: number;
/**
* Velocity of the pan gesture along the Y axis in the current moment. The
* value is expressed in point units per second.
*/
velocityY: number;
/**
* Object containing additional stylus data.
*/
stylusData?: StylusData;
};
export type PinchGestureHandlerEventPayload = {
/**
* The scale factor relative to the points of the two touches in screen
* coordinates.
*/
scale: number;
/**
* Position expressed in points along X axis of center anchor point of
* gesture.
*/
focalX: number;
/**
* Position expressed in points along Y axis of center anchor point of
* gesture.
*/
focalY: number;
/**
*
* Velocity of the pan gesture the current moment. The value is expressed in
* point units per second.
*/
velocity: number;
};
export type TapGestureHandlerEventPayload = {
x: number;
y: number;
absoluteX: number;
absoluteY: number;
};
export type RotationGestureHandlerEventPayload = {
/**
* Amount rotated, expressed in radians, from the gesture's focal point
* (anchor).
*/
rotation: number;
/**
* X coordinate, expressed in points, of the gesture's central focal point
* (anchor).
*/
anchorX: number;
/**
* Y coordinate, expressed in points, of the gesture's central focal point
* (anchor).
*/
anchorY: number;
/**
*
* Instantaneous velocity, expressed in point units per second, of the
* gesture.
*/
velocity: number;
};
export type HoverGestureHandlerEventPayload = {
/**
* X coordinate of the current position of the pointer relative to the view
* attached to the handler. Expressed in point units.
*/
x: number;
/**
* Y coordinate of the current position of the pointer relative to the view
* attached to the handler. Expressed in point units.
*/
y: number;
/**
* X coordinate of the current position of the pointer relative to the window.
* The value is expressed in point units. It is recommended to use it instead
* of `x` in cases when the original view can be transformed as an
* effect of the gesture.
*/
absoluteX: number;
/**
* Y coordinate of the current position of the pointer relative to the window.
* The value is expressed in point units. It is recommended to use it instead
* of `y` in cases when the original view can be transformed as an
* effect of the gesture.
*/
absoluteY: number;
/**
* Object containing additional stylus data.
*/
stylusData?: StylusData;
};
@@ -0,0 +1,65 @@
import { LongPressGestureHandlerEventPayload } from './GestureHandlerEventPayload';
import createHandler from './createHandler';
import {
BaseGestureHandlerProps,
baseGestureHandlerProps,
} from './gestureHandlerCommon';
export const longPressGestureHandlerProps = [
'minDurationMs',
'maxDist',
'numberOfPointers',
] as const;
export interface LongPressGestureConfig {
/**
* Minimum time, expressed in milliseconds, that a finger must remain pressed on
* the corresponding view. The default value is 500.
*/
minDurationMs?: number;
/**
* Maximum distance, expressed in points, that defines how far the finger is
* allowed to travel during a long press gesture. If the finger travels
* further than the defined distance and the handler hasn't yet activated, it
* will fail to recognize the gesture. The default value is 10.
*/
maxDist?: number;
/**
* Determine exact number of points required to handle the long press gesture.
*/
numberOfPointers?: number;
}
/**
* @deprecated LongPressGestureHandler will be removed in the future version of Gesture Handler. Use `Gesture.LongPress()` instead.
*/
export interface LongPressGestureHandlerProps
extends BaseGestureHandlerProps<LongPressGestureHandlerEventPayload>,
LongPressGestureConfig {}
export const longPressHandlerName = 'LongPressGestureHandler';
/**
* @deprecated LongPressGestureHandler will be removed in the future version of Gesture Handler. Use `Gesture.LongPress()` instead.
*/
export type LongPressGestureHandler = typeof LongPressGestureHandler;
/**
* @deprecated LongPressGestureHandler will be removed in the future version of Gesture Handler. Use `Gesture.LongPress()` instead.
*/
// eslint-disable-next-line @typescript-eslint/no-redeclare -- backward compatibility; see description on the top of gestureHandlerCommon.ts file
export const LongPressGestureHandler = createHandler<
LongPressGestureHandlerProps,
LongPressGestureHandlerEventPayload
>({
name: longPressHandlerName,
allowedProps: [
...baseGestureHandlerProps,
...longPressGestureHandlerProps,
] as const,
config: {
shouldCancelWhenOutside: true,
},
});
@@ -0,0 +1,59 @@
import type { NativeViewGestureHandlerPayload } from './GestureHandlerEventPayload';
import createHandler from './createHandler';
import {
BaseGestureHandlerProps,
baseGestureHandlerProps,
} from './gestureHandlerCommon';
export const nativeViewGestureHandlerProps = [
'shouldActivateOnStart',
'disallowInterruption',
] as const;
export interface NativeViewGestureConfig {
/**
* Android only.
*
* Determines whether the handler should check for an existing touch event on
* instantiation.
*/
shouldActivateOnStart?: boolean;
/**
* When `true`, cancels all other gesture handlers when this
* `NativeViewGestureHandler` receives an `ACTIVE` state event.
*/
disallowInterruption?: boolean;
}
/**
* @deprecated NativeViewGestureHandler will be removed in the future version of Gesture Handler. Use `Gesture.Native()` instead.
*/
export interface NativeViewGestureHandlerProps
extends BaseGestureHandlerProps<NativeViewGestureHandlerPayload>,
NativeViewGestureConfig {}
export const nativeViewProps = [
...baseGestureHandlerProps,
...nativeViewGestureHandlerProps,
] as const;
export const nativeViewHandlerName = 'NativeViewGestureHandler';
/**
* @deprecated NativeViewGestureHandler will be removed in the future version of Gesture Handler. Use `Gesture.Native()` instead.
*/
export type NativeViewGestureHandler = typeof NativeViewGestureHandler;
/**
* @deprecated NativeViewGestureHandler will be removed in the future version of Gesture Handler. Use `Gesture.Native()` instead.
*/
// eslint-disable-next-line @typescript-eslint/no-redeclare -- backward compatibility; see description on the top of gestureHandlerCommon.ts file
export const NativeViewGestureHandler = createHandler<
NativeViewGestureHandlerProps,
NativeViewGestureHandlerPayload
>({
name: nativeViewHandlerName,
allowedProps: nativeViewProps,
config: {},
});
@@ -0,0 +1,282 @@
import type { PanGestureHandlerEventPayload } from './GestureHandlerEventPayload';
import createHandler from './createHandler';
import {
BaseGestureHandlerProps,
baseGestureHandlerProps,
} from './gestureHandlerCommon';
export const panGestureHandlerProps = [
'activeOffsetY',
'activeOffsetX',
'failOffsetY',
'failOffsetX',
'minDist',
'minVelocity',
'minVelocityX',
'minVelocityY',
'minPointers',
'maxPointers',
'avgTouches',
'enableTrackpadTwoFingerGesture',
'activateAfterLongPress',
] as const;
export const panGestureHandlerCustomNativeProps = [
'activeOffsetYStart',
'activeOffsetYEnd',
'activeOffsetXStart',
'activeOffsetXEnd',
'failOffsetYStart',
'failOffsetYEnd',
'failOffsetXStart',
'failOffsetXEnd',
] as const;
interface CommonPanProperties {
/**
* Minimum distance the finger (or multiple finger) need to travel before the
* handler activates. Expressed in points.
*/
minDist?: number;
/**
* Android only.
*/
avgTouches?: boolean;
/**
* Enables two-finger gestures on supported devices, for example iPads with
* trackpads. If not enabled the gesture will require click + drag, with
* enableTrackpadTwoFingerGesture swiping with two fingers will also trigger
* the gesture.
*/
enableTrackpadTwoFingerGesture?: boolean;
/**
* A number of fingers that is required to be placed before handler can
* activate. Should be a higher or equal to 0 integer.
*/
minPointers?: number;
/**
* When the given number of fingers is placed on the screen and handler hasn't
* yet activated it will fail recognizing the gesture. Should be a higher or
* equal to 0 integer.
*/
maxPointers?: number;
minVelocity?: number;
minVelocityX?: number;
minVelocityY?: number;
activateAfterLongPress?: number;
}
export interface PanGestureConfig extends CommonPanProperties {
activeOffsetYStart?: number;
activeOffsetYEnd?: number;
activeOffsetXStart?: number;
activeOffsetXEnd?: number;
failOffsetYStart?: number;
failOffsetYEnd?: number;
failOffsetXStart?: number;
failOffsetXEnd?: number;
}
/**
* @deprecated PanGestureHandler will be removed in the future version of Gesture Handler. Use `Gesture.Pan()` instead.
*/
export interface PanGestureHandlerProps
extends BaseGestureHandlerProps<PanGestureHandlerEventPayload>,
CommonPanProperties {
/**
* Range along X axis (in points) where fingers travels without activation of
* handler. Moving outside of this range implies activation of handler. Range
* can be given as an array or a single number. If range is set as an array,
* first value must be lower or equal to 0, a the second one higher or equal
* to 0. If only one number `p` is given a range of `(-inf, p)` will be used
* if `p` is higher or equal to 0 and `(-p, inf)` otherwise.
*/
activeOffsetY?:
| number
| [activeOffsetYStart: number, activeOffsetYEnd: number];
/**
* Range along X axis (in points) where fingers travels without activation of
* handler. Moving outside of this range implies activation of handler. Range
* can be given as an array or a single number. If range is set as an array,
* first value must be lower or equal to 0, a the second one higher or equal
* to 0. If only one number `p` is given a range of `(-inf, p)` will be used
* if `p` is higher or equal to 0 and `(-p, inf)` otherwise.
*/
activeOffsetX?:
| number
| [activeOffsetXStart: number, activeOffsetXEnd: number];
/**
* When the finger moves outside this range (in points) along Y axis and
* handler hasn't yet activated it will fail recognizing the gesture. Range
* can be given as an array or a single number. If range is set as an array,
* first value must be lower or equal to 0, a the second one higher or equal
* to 0. If only one number `p` is given a range of `(-inf, p)` will be used
* if `p` is higher or equal to 0 and `(-p, inf)` otherwise.
*/
failOffsetY?: number | [failOffsetYStart: number, failOffsetYEnd: number];
/**
* When the finger moves outside this range (in points) along X axis and
* handler hasn't yet activated it will fail recognizing the gesture. Range
* can be given as an array or a single number. If range is set as an array,
* first value must be lower or equal to 0, a the second one higher or equal
* to 0. If only one number `p` is given a range of `(-inf, p)` will be used
* if `p` is higher or equal to 0 and `(-p, inf)` otherwise.
*/
failOffsetX?: number | [failOffsetXStart: number, failOffsetXEnd: number];
}
export const panHandlerName = 'PanGestureHandler';
/**
* @deprecated PanGestureHandler will be removed in the future version of Gesture Handler. Use `Gesture.Pan()` instead.
*/
export type PanGestureHandler = typeof PanGestureHandler;
/**
* @deprecated PanGestureHandler will be removed in the future version of Gesture Handler. Use `Gesture.Pan()` instead.
*/
// eslint-disable-next-line @typescript-eslint/no-redeclare -- backward compatibility; see description on the top of gestureHandlerCommon.ts file
export const PanGestureHandler = createHandler<
PanGestureHandlerProps,
PanGestureHandlerEventPayload
>({
name: panHandlerName,
allowedProps: [
...baseGestureHandlerProps,
...panGestureHandlerProps,
] as const,
config: {},
transformProps: managePanProps,
customNativeProps: panGestureHandlerCustomNativeProps,
});
function validatePanGestureHandlerProps(props: PanGestureHandlerProps) {
if (
Array.isArray(props.activeOffsetX) &&
(props.activeOffsetX[0] > 0 || props.activeOffsetX[1] < 0)
) {
throw new Error(
`First element of activeOffsetX should be negative, a the second one should be positive`
);
}
if (
Array.isArray(props.activeOffsetY) &&
(props.activeOffsetY[0] > 0 || props.activeOffsetY[1] < 0)
) {
throw new Error(
`First element of activeOffsetY should be negative, a the second one should be positive`
);
}
if (
Array.isArray(props.failOffsetX) &&
(props.failOffsetX[0] > 0 || props.failOffsetX[1] < 0)
) {
throw new Error(
`First element of failOffsetX should be negative, a the second one should be positive`
);
}
if (
Array.isArray(props.failOffsetY) &&
(props.failOffsetY[0] > 0 || props.failOffsetY[1] < 0)
) {
throw new Error(
`First element of failOffsetY should be negative, a the second one should be positive`
);
}
if (props.minDist && (props.failOffsetX || props.failOffsetY)) {
throw new Error(
`It is not supported to use minDist with failOffsetX or failOffsetY, use activeOffsetX and activeOffsetY instead`
);
}
if (props.minDist && (props.activeOffsetX || props.activeOffsetY)) {
throw new Error(
`It is not supported to use minDist with activeOffsetX or activeOffsetY`
);
}
}
function transformPanGestureHandlerProps(props: PanGestureHandlerProps) {
type InternalPanGHKeys =
| 'activeOffsetXStart'
| 'activeOffsetXEnd'
| 'failOffsetXStart'
| 'failOffsetXEnd'
| 'activeOffsetYStart'
| 'activeOffsetYEnd'
| 'failOffsetYStart'
| 'failOffsetYEnd';
type PanGestureHandlerInternalProps = PanGestureHandlerProps &
Partial<Record<InternalPanGHKeys, number>>;
const res: PanGestureHandlerInternalProps = { ...props };
if (props.activeOffsetX !== undefined) {
delete res.activeOffsetX;
if (Array.isArray(props.activeOffsetX)) {
res.activeOffsetXStart = props.activeOffsetX[0];
res.activeOffsetXEnd = props.activeOffsetX[1];
} else if (props.activeOffsetX < 0) {
res.activeOffsetXStart = props.activeOffsetX;
} else {
res.activeOffsetXEnd = props.activeOffsetX;
}
}
if (props.activeOffsetY !== undefined) {
delete res.activeOffsetY;
if (Array.isArray(props.activeOffsetY)) {
res.activeOffsetYStart = props.activeOffsetY[0];
res.activeOffsetYEnd = props.activeOffsetY[1];
} else if (props.activeOffsetY < 0) {
res.activeOffsetYStart = props.activeOffsetY;
} else {
res.activeOffsetYEnd = props.activeOffsetY;
}
}
if (props.failOffsetX !== undefined) {
delete res.failOffsetX;
if (Array.isArray(props.failOffsetX)) {
res.failOffsetXStart = props.failOffsetX[0];
res.failOffsetXEnd = props.failOffsetX[1];
} else if (props.failOffsetX < 0) {
res.failOffsetXStart = props.failOffsetX;
} else {
res.failOffsetXEnd = props.failOffsetX;
}
}
if (props.failOffsetY !== undefined) {
delete res.failOffsetY;
if (Array.isArray(props.failOffsetY)) {
res.failOffsetYStart = props.failOffsetY[0];
res.failOffsetYEnd = props.failOffsetY[1];
} else if (props.failOffsetY < 0) {
res.failOffsetYStart = props.failOffsetY;
} else {
res.failOffsetYEnd = props.failOffsetY;
}
}
return res;
}
export function managePanProps(props: PanGestureHandlerProps) {
if (__DEV__) {
validatePanGestureHandlerProps(props);
}
return transformPanGestureHandlerProps(props);
}
@@ -0,0 +1,32 @@
import { PinchGestureHandlerEventPayload } from './GestureHandlerEventPayload';
import createHandler from './createHandler';
import {
BaseGestureHandlerProps,
baseGestureHandlerProps,
} from './gestureHandlerCommon';
/**
* @deprecated PinchGestureHandler will be removed in the future version of Gesture Handler. Use `Gesture.Pinch()` instead.
*/
export interface PinchGestureHandlerProps
extends BaseGestureHandlerProps<PinchGestureHandlerEventPayload> {}
export const pinchHandlerName = 'PinchGestureHandler';
/**
* @deprecated PinchGestureHandler will be removed in the future version of Gesture Handler. Use `Gesture.Pinch()` instead.
*/
export type PinchGestureHandler = typeof PinchGestureHandler;
/**
* @deprecated PinchGestureHandler will be removed in the future version of Gesture Handler. Use `Gesture.Pinch()` instead.
*/
// eslint-disable-next-line @typescript-eslint/no-redeclare -- backward compatibility; see description on the top of gestureHandlerCommon.ts file
export const PinchGestureHandler = createHandler<
PinchGestureHandlerProps,
PinchGestureHandlerEventPayload
>({
name: pinchHandlerName,
allowedProps: baseGestureHandlerProps,
config: {},
});
@@ -0,0 +1,2 @@
// @ts-ignore it's not exported so we need to import it from path
export { PressabilityDebugView } from 'react-native/Libraries/Pressability/PressabilityDebug';
@@ -0,0 +1,4 @@
// PressabilityDebugView is not implemented in react-native-web
export function PressabilityDebugView() {
return null;
}
@@ -0,0 +1,32 @@
import { RotationGestureHandlerEventPayload } from './GestureHandlerEventPayload';
import createHandler from './createHandler';
import {
BaseGestureHandlerProps,
baseGestureHandlerProps,
} from './gestureHandlerCommon';
/**
* @deprecated RotationGestureHandler will be removed in the future version of Gesture Handler. Use `Gesture.Rotation()` instead.
*/
export interface RotationGestureHandlerProps
extends BaseGestureHandlerProps<RotationGestureHandlerEventPayload> {}
export const rotationHandlerName = 'RotationGestureHandler';
/**
* @deprecated RotationGestureHandler will be removed in the future version of Gesture Handler. Use `Gesture.Rotation()` instead.
*/
export type RotationGestureHandler = typeof RotationGestureHandler;
/**
* @deprecated RotationGestureHandler will be removed in the future version of Gesture Handler. Use `Gesture.Rotation()` instead.
*/
// eslint-disable-next-line @typescript-eslint/no-redeclare -- backward compatibility; see description on the top of gestureHandlerCommon.ts file
export const RotationGestureHandler = createHandler<
RotationGestureHandlerProps,
RotationGestureHandlerEventPayload
>({
name: rotationHandlerName,
allowedProps: baseGestureHandlerProps,
config: {},
});
@@ -0,0 +1,99 @@
import type { TapGestureHandlerEventPayload } from './GestureHandlerEventPayload';
import createHandler from './createHandler';
import {
BaseGestureHandlerProps,
baseGestureHandlerProps,
} from './gestureHandlerCommon';
export const tapGestureHandlerProps = [
'maxDurationMs',
'maxDelayMs',
'numberOfTaps',
'maxDeltaX',
'maxDeltaY',
'maxDist',
'minPointers',
] as const;
export interface TapGestureConfig {
/**
* Minimum number of pointers (fingers) required to be placed before the
* handler activates. Should be a positive integer.
* The default value is 1.
*/
minPointers?: number;
/**
* Maximum time, expressed in milliseconds, that defines how fast a finger
* must be released after a touch. The default value is 500.
*/
maxDurationMs?: number;
/**
* Maximum time, expressed in milliseconds, that can pass before the next tap
* if many taps are required. The default value is 500.
*/
maxDelayMs?: number;
/**
* Number of tap gestures required to activate the handler. The default value
* is 1.
*/
numberOfTaps?: number;
/**
* Maximum distance, expressed in points, that defines how far the finger is
* allowed to travel along the X axis during a tap gesture. If the finger
* travels further than the defined distance along the X axis and the handler
* hasn't yet activated, it will fail to recognize the gesture.
*/
maxDeltaX?: number;
/**
* Maximum distance, expressed in points, that defines how far the finger is
* allowed to travel along the Y axis during a tap gesture. If the finger
* travels further than the defined distance along the Y axis and the handler
* hasn't yet activated, it will fail to recognize the gesture.
*/
maxDeltaY?: number;
/**
* Maximum distance, expressed in points, that defines how far the finger is
* allowed to travel during a tap gesture. If the finger travels further than
* the defined distance and the handler hasn't yet
* activated, it will fail to recognize the gesture.
*/
maxDist?: number;
}
/**
* @deprecated TapGestureHandler will be removed in the future version of Gesture Handler. Use `Gesture.Tap()` instead.
*/
export interface TapGestureHandlerProps
extends BaseGestureHandlerProps<TapGestureHandlerEventPayload>,
TapGestureConfig {}
export const tapHandlerName = 'TapGestureHandler';
/**
* @deprecated TapGestureHandler will be removed in the future version of Gesture Handler. Use `Gesture.Tap()` instead.
*/
export type TapGestureHandler = typeof TapGestureHandler;
/**
* @deprecated TapGestureHandler will be removed in the future version of Gesture Handler. Use `Gesture.Tap()` instead.
*/
// eslint-disable-next-line @typescript-eslint/no-redeclare -- backward compatibility; see description on the top of gestureHandlerCommon.ts file
export const TapGestureHandler = createHandler<
TapGestureHandlerProps,
TapGestureHandlerEventPayload
>({
name: tapHandlerName,
allowedProps: [
...baseGestureHandlerProps,
...tapGestureHandlerProps,
] as const,
config: {
shouldCancelWhenOutside: true,
},
});
@@ -0,0 +1,572 @@
import * as React from 'react';
import {
Platform,
UIManager,
DeviceEventEmitter,
EmitterSubscription,
} from 'react-native';
import { customDirectEventTypes } from './customDirectEventTypes';
import RNGestureHandlerModule from '../RNGestureHandlerModule';
import { State } from '../State';
import {
handlerIDToTag,
registerOldGestureHandler,
unregisterOldGestureHandler,
} from './handlersRegistry';
import { getNextHandlerTag } from './getNextHandlerTag';
import {
BaseGestureHandlerProps,
GestureEvent,
HandlerStateChangeEvent,
} from './gestureHandlerCommon';
import { filterConfig, scheduleFlushOperations } from './utils';
import findNodeHandle from '../findNodeHandle';
import { ValueOf } from '../typeUtils';
import {
deepEqual,
isFabric,
isReact19,
isTestEnv,
tagMessage,
} from '../utils';
import { ActionType } from '../ActionType';
import { PressabilityDebugView } from './PressabilityDebugView';
import GestureHandlerRootViewContext from '../GestureHandlerRootViewContext';
import { ghQueueMicrotask } from '../ghQueueMicrotask';
import { MountRegistry } from '../mountRegistry';
import { ReactElement } from 'react';
const UIManagerAny = UIManager as any;
customDirectEventTypes.topGestureHandlerEvent = {
registrationName: 'onGestureHandlerEvent',
};
const customGHEventsConfigFabricAndroid = {
topOnGestureHandlerEvent: { registrationName: 'onGestureHandlerEvent' },
topOnGestureHandlerStateChange: {
registrationName: 'onGestureHandlerStateChange',
},
};
const customGHEventsConfig = {
onGestureHandlerEvent: { registrationName: 'onGestureHandlerEvent' },
onGestureHandlerStateChange: {
registrationName: 'onGestureHandlerStateChange',
},
// When using React Native Gesture Handler for Animated.event with useNativeDriver: true
// on Android with Fabric enabled, the native part still sends the native events to JS
// but prefixed with "top". We cannot simply rename the events above so they are prefixed
// with "top" instead of "on" because in such case Animated.events would not be registered.
// That's why we need to register another pair of event names.
// The incoming events will be queued but never handled.
// Without this piece of code below, you'll get the following JS error:
// Unsupported top level event type "topOnGestureHandlerEvent" dispatched
...(isFabric() &&
Platform.OS === 'android' &&
customGHEventsConfigFabricAndroid),
};
// Add gesture specific events to genericDirectEventTypes object exported from UIManager
// native module.
// Once new event types are registered with react it is possible to dispatch these
// events to all kind of native views.
UIManagerAny.genericDirectEventTypes = {
...UIManagerAny.genericDirectEventTypes,
...customGHEventsConfig,
};
const UIManagerConstants = UIManagerAny.getViewManagerConfig?.('getConstants');
if (UIManagerConstants) {
UIManagerConstants.genericDirectEventTypes = {
...UIManagerConstants.genericDirectEventTypes,
...customGHEventsConfig,
};
}
// Wrap JS responder calls and notify gesture handler manager
const {
setJSResponder: oldSetJSResponder = () => {
// no-op
},
clearJSResponder: oldClearJSResponder = () => {
// no-op
},
} = UIManagerAny;
UIManagerAny.setJSResponder = (tag: number, blockNativeResponder: boolean) => {
RNGestureHandlerModule.handleSetJSResponder(tag, blockNativeResponder);
oldSetJSResponder(tag, blockNativeResponder);
};
UIManagerAny.clearJSResponder = () => {
RNGestureHandlerModule.handleClearJSResponder();
oldClearJSResponder();
};
let allowTouches = true;
const DEV_ON_ANDROID = __DEV__ && Platform.OS === 'android';
// Toggled inspector blocks touch events in order to allow inspecting on Android
// This needs to be a global variable in order to set initial state for `allowTouches` property in Handler component
if (DEV_ON_ANDROID) {
DeviceEventEmitter.addListener('toggleElementInspector', () => {
allowTouches = !allowTouches;
});
}
type HandlerProps<T extends Record<string, unknown>> = Readonly<
React.PropsWithChildren<BaseGestureHandlerProps<T>>
>;
function hasUnresolvedRefs<T extends Record<string, unknown>>(
props: HandlerProps<T>
) {
// TODO(TS) - add type for extract arg
const extract = (refs: any | any[]) => {
if (!Array.isArray(refs)) {
return refs && refs.current === null;
}
return refs.some((r) => r && r.current === null);
};
return extract(props['simultaneousHandlers']) || extract(props['waitFor']);
}
const stateToPropMappings = {
[State.UNDETERMINED]: undefined,
[State.BEGAN]: 'onBegan',
[State.FAILED]: 'onFailed',
[State.CANCELLED]: 'onCancelled',
[State.ACTIVE]: 'onActivated',
[State.END]: 'onEnded',
} as const;
type CreateHandlerArgs<HandlerPropsT extends Record<string, unknown>> =
Readonly<{
name: string;
allowedProps: Readonly<Extract<keyof HandlerPropsT, string>[]>;
config: Readonly<Record<string, unknown>>;
transformProps?: (props: HandlerPropsT) => HandlerPropsT;
customNativeProps?: Readonly<string[]>;
}>;
// TODO(TS) fix event types
type InternalEventHandlers = {
onGestureHandlerEvent?: (event: any) => void;
onGestureHandlerStateChange?: (event: any) => void;
};
type AttachGestureHandlerWeb = (
handlerTag: number,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
newView: any,
_actionType: ActionType,
propsRef: React.RefObject<unknown>
) => void;
const UNRESOLVED_REFS_RETRY_LIMIT = 1;
// TODO(TS) - make sure that BaseGestureHandlerProps doesn't need other generic parameter to work with custom properties.
export default function createHandler<
T extends BaseGestureHandlerProps<U>,
U extends Record<string, unknown>,
>({
name,
allowedProps = [],
config = {},
transformProps,
customNativeProps = [],
}: CreateHandlerArgs<T>): React.ComponentType<T & React.RefAttributes<any>> {
interface HandlerState {
allowTouches: boolean;
}
class Handler extends React.Component<
T & InternalEventHandlers,
HandlerState
> {
static displayName = name;
static contextType = GestureHandlerRootViewContext;
private handlerTag = -1;
private config: Record<string, unknown>;
private propsRef: React.MutableRefObject<unknown>;
private isMountedRef: React.MutableRefObject<boolean | null>;
private viewNode: any;
private viewTag?: number;
private inspectorToggleListener?: EmitterSubscription;
constructor(props: T & InternalEventHandlers) {
super(props);
this.config = {};
this.propsRef = React.createRef();
this.isMountedRef = React.createRef();
this.state = { allowTouches };
if (props.id) {
if (handlerIDToTag[props.id] !== undefined) {
throw new Error(`Handler with ID "${props.id}" already registered`);
}
handlerIDToTag[props.id] = this.handlerTag;
}
}
componentDidMount() {
const props: HandlerProps<U> = this.props;
this.isMountedRef.current = true;
if (DEV_ON_ANDROID) {
this.inspectorToggleListener = DeviceEventEmitter.addListener(
'toggleElementInspector',
() => {
this.setState((_) => ({ allowTouches }));
this.update(UNRESOLVED_REFS_RETRY_LIMIT);
}
);
}
if (hasUnresolvedRefs(props)) {
// If there are unresolved refs (e.g. ".current" has not yet been set)
// passed as `simultaneousHandlers` or `waitFor`, we enqueue a call to
// _update method that will try to update native handler props using
// queueMicrotask. This makes it so update() function gets called after all
// react components are mounted and we expect the missing ref object to
// be resolved by then.
ghQueueMicrotask(() => {
this.update(UNRESOLVED_REFS_RETRY_LIMIT);
});
}
this.createGestureHandler(
filterConfig(
transformProps ? transformProps(this.props) : this.props,
[...allowedProps, ...customNativeProps],
config
)
);
if (!this.viewNode) {
throw new Error(
`[Gesture Handler] Failed to obtain view for ${Handler.displayName}. Note that old API doesn't support functional components.`
);
}
this.attachGestureHandler(findNodeHandle(this.viewNode) as number); // TODO(TS) - check if this can be null
}
componentDidUpdate() {
const viewTag = findNodeHandle(this.viewNode);
if (this.viewTag !== viewTag) {
this.attachGestureHandler(viewTag as number); // TODO(TS) - check interaction between _viewTag & findNodeHandle
}
this.update(UNRESOLVED_REFS_RETRY_LIMIT);
}
componentWillUnmount() {
this.inspectorToggleListener?.remove();
this.isMountedRef.current = false;
if (Platform.OS !== 'web') {
unregisterOldGestureHandler(this.handlerTag);
}
RNGestureHandlerModule.dropGestureHandler(this.handlerTag);
scheduleFlushOperations();
// We can't use this.props.id directly due to TS generic type narrowing bug, see https://github.com/microsoft/TypeScript/issues/13995 for more context
const handlerID: string | undefined = this.props.id;
if (handlerID) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete handlerIDToTag[handlerID];
}
MountRegistry.gestureHandlerWillUnmount(this);
}
private onGestureHandlerEvent = (event: GestureEvent<U>) => {
if (event.nativeEvent.handlerTag === this.handlerTag) {
if (typeof this.props.onGestureEvent === 'function') {
this.props.onGestureEvent?.(event);
}
} else {
this.props.onGestureHandlerEvent?.(event);
}
};
// TODO(TS) - make sure this is right type for event
private onGestureHandlerStateChange = (
event: HandlerStateChangeEvent<U>
) => {
if (event.nativeEvent.handlerTag === this.handlerTag) {
if (typeof this.props.onHandlerStateChange === 'function') {
this.props.onHandlerStateChange?.(event);
}
const state: ValueOf<typeof State> = event.nativeEvent.state;
const stateEventName = stateToPropMappings[state];
const eventHandler = stateEventName && this.props[stateEventName];
if (eventHandler && typeof eventHandler === 'function') {
eventHandler(event);
}
} else {
this.props.onGestureHandlerStateChange?.(event);
}
};
private refHandler = (node: any) => {
this.viewNode = node;
const child = React.Children.only(this.props.children);
// @ts-ignore Since React 19 ref is accessible as standard prop
// https://react.dev/blog/2024/04/25/react-19-upgrade-guide#deprecated-element-ref
const ref = isReact19() ? (child as ReactElement).props?.ref : child?.ref;
if (!ref) {
return;
}
if (typeof ref === 'function') {
ref(node);
} else {
ref.current = node;
}
};
private createGestureHandler = (
newConfig: Readonly<Record<string, unknown>>
) => {
this.handlerTag = getNextHandlerTag();
this.config = newConfig;
RNGestureHandlerModule.createGestureHandler(
name,
this.handlerTag,
newConfig
);
};
private attachGestureHandler = (newViewTag: number) => {
this.viewTag = newViewTag;
if (Platform.OS === 'web') {
// Typecast due to dynamic resolution, attachGestureHandler should have web version signature in this branch
(
RNGestureHandlerModule.attachGestureHandler as AttachGestureHandlerWeb
)(
this.handlerTag,
newViewTag,
ActionType.JS_FUNCTION_OLD_API, // ignored on web
this.propsRef
);
} else {
registerOldGestureHandler(this.handlerTag, {
onGestureEvent: this.onGestureHandlerEvent,
onGestureStateChange: this.onGestureHandlerStateChange,
});
const actionType = (() => {
const onGestureEvent = this.props?.onGestureEvent;
const isGestureHandlerWorklet =
onGestureEvent &&
('current' in onGestureEvent ||
'workletEventHandler' in onGestureEvent);
const onHandlerStateChange = this.props?.onHandlerStateChange;
const isStateChangeHandlerWorklet =
onHandlerStateChange &&
('current' in onHandlerStateChange ||
'workletEventHandler' in onHandlerStateChange);
const isReanimatedHandler =
isGestureHandlerWorklet || isStateChangeHandlerWorklet;
if (isReanimatedHandler) {
// Reanimated worklet
return ActionType.REANIMATED_WORKLET;
} else if (onGestureEvent && '__isNative' in onGestureEvent) {
// Animated.event with useNativeDriver: true
return ActionType.NATIVE_ANIMATED_EVENT;
} else {
// JS callback or Animated.event with useNativeDriver: false
return ActionType.JS_FUNCTION_OLD_API;
}
})();
RNGestureHandlerModule.attachGestureHandler(
this.handlerTag,
newViewTag,
actionType
);
}
scheduleFlushOperations();
ghQueueMicrotask(() => {
MountRegistry.gestureHandlerWillMount(this);
});
};
private updateGestureHandler = (
newConfig: Readonly<Record<string, unknown>>
) => {
this.config = newConfig;
RNGestureHandlerModule.updateGestureHandler(this.handlerTag, newConfig);
scheduleFlushOperations();
};
private update(remainingTries: number) {
if (!this.isMountedRef.current) {
return;
}
const props: HandlerProps<U> = this.props;
// When ref is set via a function i.e. `ref={(r) => refObject.current = r}` instead of
// `ref={refObject}` it's possible that it won't be resolved in time. Seems like trying
// again is easy enough fix.
if (hasUnresolvedRefs(props) && remainingTries > 0) {
ghQueueMicrotask(() => {
this.update(remainingTries - 1);
});
} else {
const newConfig = filterConfig(
transformProps ? transformProps(this.props) : this.props,
[...allowedProps, ...customNativeProps],
config
);
if (!deepEqual(this.config, newConfig)) {
this.updateGestureHandler(newConfig);
}
}
}
// eslint-disable-next-line @eslint-react/no-unused-class-component-members
setNativeProps(updates: any) {
const mergedProps = { ...this.props, ...updates };
const newConfig = filterConfig(
transformProps ? transformProps(mergedProps) : mergedProps,
[...allowedProps, ...customNativeProps],
config
);
this.updateGestureHandler(newConfig);
}
render() {
if (__DEV__ && !this.context && !isTestEnv() && Platform.OS !== 'web') {
throw new Error(
name +
' must be used as a descendant of GestureHandlerRootView. Otherwise the gestures will not be recognized. See https://docs.swmansion.com/react-native-gesture-handler/docs/fundamentals/installation for more details.'
);
}
let gestureEventHandler = this.onGestureHandlerEvent;
// Another instance of https://github.com/microsoft/TypeScript/issues/13995
type OnGestureEventHandlers = {
onGestureEvent?: BaseGestureHandlerProps<U>['onGestureEvent'];
onGestureHandlerEvent?: InternalEventHandlers['onGestureHandlerEvent'];
};
const { onGestureEvent, onGestureHandlerEvent }: OnGestureEventHandlers =
this.props;
if (onGestureEvent && typeof onGestureEvent !== 'function') {
// If it's not a method it should be an native Animated.event
// object. We set it directly as the handler for the view
// In this case nested handlers are not going to be supported
if (onGestureHandlerEvent) {
throw new Error(
'Nesting touch handlers with native animated driver is not supported yet'
);
}
gestureEventHandler = onGestureEvent;
} else {
if (
onGestureHandlerEvent &&
typeof onGestureHandlerEvent !== 'function'
) {
throw new Error(
'Nesting touch handlers with native animated driver is not supported yet'
);
}
}
let gestureStateEventHandler = this.onGestureHandlerStateChange;
// Another instance of https://github.com/microsoft/TypeScript/issues/13995
type OnGestureStateChangeHandlers = {
onHandlerStateChange?: BaseGestureHandlerProps<U>['onHandlerStateChange'];
onGestureHandlerStateChange?: InternalEventHandlers['onGestureHandlerStateChange'];
};
const {
onHandlerStateChange,
onGestureHandlerStateChange,
}: OnGestureStateChangeHandlers = this.props;
if (onHandlerStateChange && typeof onHandlerStateChange !== 'function') {
// If it's not a method it should be an native Animated.event
// object. We set it directly as the handler for the view
// In this case nested handlers are not going to be supported
if (onGestureHandlerStateChange) {
throw new Error(
'Nesting touch handlers with native animated driver is not supported yet'
);
}
gestureStateEventHandler = onHandlerStateChange;
} else {
if (
onGestureHandlerStateChange &&
typeof onGestureHandlerStateChange !== 'function'
) {
throw new Error(
'Nesting touch handlers with native animated driver is not supported yet'
);
}
}
const events = {
onGestureHandlerEvent: this.state.allowTouches
? gestureEventHandler
: undefined,
onGestureHandlerStateChange: this.state.allowTouches
? gestureStateEventHandler
: undefined,
};
this.propsRef.current = events;
let child: any = null;
try {
child = React.Children.only(this.props.children);
} catch (e) {
throw new Error(
tagMessage(
`${name} got more than one view as a child. If you want the gesture to work on multiple views, wrap them with a common parent and attach the gesture to that view.`
)
);
}
let grandChildren = child.props.children;
if (
__DEV__ &&
child.type &&
(child.type === 'RNGestureHandlerButton' ||
child.type.name === 'View' ||
child.type.displayName === 'View')
) {
grandChildren = React.Children.toArray(grandChildren);
grandChildren.push(
<PressabilityDebugView
key="pressabilityDebugView"
color="mediumspringgreen"
hitSlop={child.props.hitSlop}
/>
);
}
return React.cloneElement(
child,
{
ref: this.refHandler,
collapsable: false,
...(isTestEnv()
? {
handlerType: name,
handlerTag: this.handlerTag,
enabled: this.props.enabled,
}
: {}),
testID: this.props.testID ?? child.props.testID,
...events,
},
grandChildren
);
}
}
return Handler;
}
@@ -0,0 +1,90 @@
import * as React from 'react';
import { useImperativeHandle, useRef } from 'react';
import {
NativeViewGestureHandler,
NativeViewGestureHandlerProps,
nativeViewProps,
} from './NativeViewGestureHandler';
/*
* This array should consist of:
* - All keys in propTypes from NativeGestureHandler
* (and all keys in GestureHandlerPropTypes)
* - 'onGestureHandlerEvent'
* - 'onGestureHandlerStateChange'
*/
const NATIVE_WRAPPER_PROPS_FILTER = [
...nativeViewProps,
'onGestureHandlerEvent',
'onGestureHandlerStateChange',
] as const;
export default function createNativeWrapper<P>(
Component: React.ComponentType<P>,
config: Readonly<NativeViewGestureHandlerProps> = {}
) {
const ComponentWrapper = React.forwardRef<
React.ComponentType<any>,
P & NativeViewGestureHandlerProps
>((props, ref) => {
// Filter out props that should be passed to gesture handler wrapper
const { gestureHandlerProps, childProps } = Object.keys(props).reduce(
(res, key) => {
// TS being overly protective with it's types, see https://github.com/microsoft/TypeScript/issues/26255#issuecomment-458013731 for more info
const allowedKeys: readonly string[] = NATIVE_WRAPPER_PROPS_FILTER;
if (allowedKeys.includes(key)) {
// @ts-ignore FIXME(TS)
res.gestureHandlerProps[key] = props[key];
} else {
// @ts-ignore FIXME(TS)
res.childProps[key] = props[key];
}
return res;
},
{
gestureHandlerProps: { ...config }, // Watch out not to modify config
childProps: {
enabled: props.enabled,
hitSlop: props.hitSlop,
testID: props.testID,
} as P,
}
);
const _ref = useRef<React.ComponentType<P>>(null);
const _gestureHandlerRef = useRef<React.ComponentType<P>>(null);
useImperativeHandle(
ref,
// @ts-ignore TODO(TS) decide how nulls work in this context
() => {
const node = _gestureHandlerRef.current;
// Add handlerTag for relations config
if (_ref.current && node) {
// @ts-ignore FIXME(TS) think about createHandler return type
_ref.current.handlerTag = node.handlerTag;
return _ref.current;
}
return null;
},
[_ref, _gestureHandlerRef]
);
return (
<NativeViewGestureHandler
{...gestureHandlerProps}
// @ts-ignore TODO(TS)
ref={_gestureHandlerRef}>
<Component {...childProps} ref={_ref} />
</NativeViewGestureHandler>
);
});
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
ComponentWrapper.displayName =
Component?.displayName ||
// @ts-ignore if render doesn't exist it will return undefined and go further
Component?.render?.name ||
(typeof Component === 'string' && Component) ||
'ComponentWrapper';
return ComponentWrapper;
}
@@ -0,0 +1,2 @@
// @ts-ignore - its taken straight from RN
export { customDirectEventTypes } from 'react-native/Libraries/Renderer/shims/ReactNativeViewConfigRegistry';
@@ -0,0 +1,5 @@
// customDirectEventTypes doesn't exist in react-native-web, therefore importing it
// directly in createHandler.tsx would end in crash.
const customDirectEventTypes = {};
export { customDirectEventTypes };
@@ -0,0 +1,211 @@
// Previous types exported gesture handlers as classes which creates an interface and variable, both named the same as class.
// Without those types, we'd introduce breaking change, forcing users to prefix every handler type specification with typeof
// e.g. React.createRef<TapGestureHandler> -> React.createRef<typeof TapGestureHandler>.
// See https://www.typescriptlang.org/docs/handbook/classes.html#constructor-functions for reference.
import * as React from 'react';
import { State } from '../State';
import { TouchEventType } from '../TouchEventType';
import { ValueOf } from '../typeUtils';
import { PointerType } from '../PointerType';
const commonProps = [
'id',
'enabled',
'shouldCancelWhenOutside',
'hitSlop',
'cancelsTouchesInView',
'userSelect',
'activeCursor',
'mouseButton',
'enableContextMenu',
'touchAction',
] as const;
const componentInteractionProps = [
'waitFor',
'simultaneousHandlers',
'blocksHandlers',
] as const;
export const baseGestureHandlerProps = [
...commonProps,
...componentInteractionProps,
'onBegan',
'onFailed',
'onCancelled',
'onActivated',
'onEnded',
'onGestureEvent',
'onHandlerStateChange',
] as const;
export const baseGestureHandlerWithDetectorProps = [
...commonProps,
'needsPointerData',
'manualActivation',
];
export interface GestureEventPayload {
handlerTag: number;
numberOfPointers: number;
state: ValueOf<typeof State>;
pointerType: PointerType;
}
export interface HandlerStateChangeEventPayload extends GestureEventPayload {
oldState: ValueOf<typeof State>;
}
export type HitSlop =
| number
| null
| undefined
| Partial<
Record<
'left' | 'right' | 'top' | 'bottom' | 'vertical' | 'horizontal',
number
>
>
| Record<'width' | 'left', number>
| Record<'width' | 'right', number>
| Record<'height' | 'top', number>
| Record<'height' | 'bottom', number>;
export type UserSelect = 'none' | 'auto' | 'text';
export type ActiveCursor =
| 'auto'
| 'default'
| 'none'
| 'context-menu'
| 'help'
| 'pointer'
| 'progress'
| 'wait'
| 'cell'
| 'crosshair'
| 'text'
| 'vertical-text'
| 'alias'
| 'copy'
| 'move'
| 'no-drop'
| 'not-allowed'
| 'grab'
| 'grabbing'
| 'e-resize'
| 'n-resize'
| 'ne-resize'
| 'nw-resize'
| 's-resize'
| 'se-resize'
| 'sw-resize'
| 'w-resize'
| 'ew-resize'
| 'ns-resize'
| 'nesw-resize'
| 'nwse-resize'
| 'col-resize'
| 'row-resize'
| 'all-scroll'
| 'zoom-in'
| 'zoom-out';
export enum MouseButton {
LEFT = 1,
RIGHT = 2,
MIDDLE = 4,
BUTTON_4 = 8,
BUTTON_5 = 16,
ALL = 31,
}
export type TouchAction =
| 'auto'
| 'none'
| 'pan-x'
| 'pan-left'
| 'pan-right'
| 'pan-y'
| 'pan-up'
| 'pan-down'
| 'pinch-zoom'
| 'manipulation'
| 'inherit'
| 'initial'
| 'revert'
| 'revert-layer'
| 'unset';
// TODO(TS) events in handlers
export interface GestureEvent<ExtraEventPayloadT = Record<string, unknown>> {
nativeEvent: Readonly<GestureEventPayload & ExtraEventPayloadT>;
}
export interface HandlerStateChangeEvent<
ExtraEventPayloadT = Record<string, unknown>,
> {
nativeEvent: Readonly<HandlerStateChangeEventPayload & ExtraEventPayloadT>;
}
export type TouchData = {
id: number;
x: number;
y: number;
absoluteX: number;
absoluteY: number;
};
export type GestureTouchEvent = {
handlerTag: number;
numberOfTouches: number;
state: ValueOf<typeof State>;
eventType: TouchEventType;
allTouches: TouchData[];
changedTouches: TouchData[];
pointerType: PointerType;
};
export type GestureUpdateEvent<GestureEventPayloadT = Record<string, unknown>> =
GestureEventPayload & GestureEventPayloadT;
export type GestureStateChangeEvent<
GestureStateChangeEventPayloadT = Record<string, unknown>,
> = HandlerStateChangeEventPayload & GestureStateChangeEventPayloadT;
export type CommonGestureConfig = {
enabled?: boolean;
shouldCancelWhenOutside?: boolean;
hitSlop?: HitSlop;
userSelect?: UserSelect;
activeCursor?: ActiveCursor;
mouseButton?: MouseButton;
enableContextMenu?: boolean;
touchAction?: TouchAction;
};
// Events payloads are types instead of interfaces due to TS limitation.
// See https://github.com/microsoft/TypeScript/issues/15300 for more info.
export type BaseGestureHandlerProps<
ExtraEventPayloadT extends Record<string, unknown> = Record<string, unknown>,
> = CommonGestureConfig & {
id?: string;
waitFor?: React.Ref<unknown> | React.Ref<unknown>[];
simultaneousHandlers?: React.Ref<unknown> | React.Ref<unknown>[];
blocksHandlers?: React.Ref<unknown> | React.Ref<unknown>[];
testID?: string;
cancelsTouchesInView?: boolean;
// TODO(TS) - fix event types
onBegan?: (event: HandlerStateChangeEvent) => void;
onFailed?: (event: HandlerStateChangeEvent) => void;
onCancelled?: (event: HandlerStateChangeEvent) => void;
onActivated?: (event: HandlerStateChangeEvent) => void;
onEnded?: (event: HandlerStateChangeEvent) => void;
// TODO(TS) consider using NativeSyntheticEvent
onGestureEvent?: (event: GestureEvent<ExtraEventPayloadT>) => void;
onHandlerStateChange?: (
event: HandlerStateChangeEvent<ExtraEventPayloadT>
) => void;
// Implicit `children` prop has been removed in @types/react^18.0.0
children?: React.ReactNode;
};
@@ -0,0 +1,101 @@
import type {
BaseButtonProps,
BorderlessButtonProps,
RawButtonProps,
RectButtonProps,
} from '../components/GestureButtonsProps';
import {
GestureEvent,
GestureEventPayload,
HandlerStateChangeEvent,
HandlerStateChangeEventPayload,
} from './gestureHandlerCommon';
import type { FlingGestureHandlerProps } from './FlingGestureHandler';
import type {
FlingGestureHandlerEventPayload,
ForceTouchGestureHandlerEventPayload,
LongPressGestureHandlerEventPayload,
PanGestureHandlerEventPayload,
PinchGestureHandlerEventPayload,
RotationGestureHandlerEventPayload,
TapGestureHandlerEventPayload,
NativeViewGestureHandlerPayload,
} from './GestureHandlerEventPayload';
import type { ForceTouchGestureHandlerProps } from './ForceTouchGestureHandler';
import type { LongPressGestureHandlerProps } from './LongPressGestureHandler';
import type { PanGestureHandlerProps } from './PanGestureHandler';
import type { PinchGestureHandlerProps } from './PinchGestureHandler';
import type { RotationGestureHandlerProps } from './RotationGestureHandler';
import type { TapGestureHandlerProps } from './TapGestureHandler';
import type { NativeViewGestureHandlerProps } from './NativeViewGestureHandler';
// Events
export type GestureHandlerGestureEventNativeEvent = GestureEventPayload;
export type GestureHandlerStateChangeNativeEvent =
HandlerStateChangeEventPayload;
export type GestureHandlerGestureEvent = GestureEvent;
export type GestureHandlerStateChangeEvent = HandlerStateChangeEvent;
// Gesture handlers events
export type NativeViewGestureHandlerGestureEvent =
GestureEvent<NativeViewGestureHandlerPayload>;
export type NativeViewGestureHandlerStateChangeEvent =
HandlerStateChangeEvent<NativeViewGestureHandlerPayload>;
export type TapGestureHandlerGestureEvent =
GestureEvent<TapGestureHandlerEventPayload>;
export type TapGestureHandlerStateChangeEvent =
HandlerStateChangeEvent<TapGestureHandlerEventPayload>;
/**
* @deprecated ForceTouchGestureHandler is deprecated and will be removed in the future.
*/
export type ForceTouchGestureHandlerGestureEvent =
GestureEvent<ForceTouchGestureHandlerEventPayload>;
/**
* @deprecated ForceTouchGestureHandler is deprecated and will be removed in the future.
*/
export type ForceTouchGestureHandlerStateChangeEvent =
HandlerStateChangeEvent<ForceTouchGestureHandlerEventPayload>;
export type LongPressGestureHandlerGestureEvent =
GestureEvent<LongPressGestureHandlerEventPayload>;
export type LongPressGestureHandlerStateChangeEvent =
HandlerStateChangeEvent<LongPressGestureHandlerEventPayload>;
export type PanGestureHandlerGestureEvent =
GestureEvent<PanGestureHandlerEventPayload>;
export type PanGestureHandlerStateChangeEvent =
HandlerStateChangeEvent<PanGestureHandlerEventPayload>;
export type PinchGestureHandlerGestureEvent =
GestureEvent<PinchGestureHandlerEventPayload>;
export type PinchGestureHandlerStateChangeEvent =
HandlerStateChangeEvent<PinchGestureHandlerEventPayload>;
export type RotationGestureHandlerGestureEvent =
GestureEvent<RotationGestureHandlerEventPayload>;
export type RotationGestureHandlerStateChangeEvent =
HandlerStateChangeEvent<RotationGestureHandlerEventPayload>;
export type FlingGestureHandlerGestureEvent =
GestureEvent<FlingGestureHandlerEventPayload>;
export type FlingGestureHandlerStateChangeEvent =
HandlerStateChangeEvent<FlingGestureHandlerEventPayload>;
// Handlers properties
export type NativeViewGestureHandlerProperties = NativeViewGestureHandlerProps;
export type TapGestureHandlerProperties = TapGestureHandlerProps;
export type LongPressGestureHandlerProperties = LongPressGestureHandlerProps;
export type PanGestureHandlerProperties = PanGestureHandlerProps;
export type PinchGestureHandlerProperties = PinchGestureHandlerProps;
export type RotationGestureHandlerProperties = RotationGestureHandlerProps;
export type FlingGestureHandlerProperties = FlingGestureHandlerProps;
/**
* @deprecated ForceTouch gesture is deprecated and will be removed in the future.
*/
export type ForceTouchGestureHandlerProperties = ForceTouchGestureHandlerProps;
// Button props
export type RawButtonProperties = RawButtonProps;
export type BaseButtonProperties = BaseButtonProps;
export type RectButtonProperties = RectButtonProps;
export type BorderlessButtonProperties = BorderlessButtonProps;
@@ -0,0 +1,35 @@
import React from 'react';
import { Reanimated } from '../reanimatedWrapper';
import { tagMessage } from '../../../utils';
export class Wrap extends React.Component<{
onGestureHandlerEvent?: unknown;
// Implicit `children` prop has been removed in @types/react^18.0.0
children?: React.ReactNode;
}> {
render() {
try {
// I don't think that fighting with types over such a simple function is worth it
// The only thing it does is add 'collapsable: false' to the child component
// to make sure it is in the native view hierarchy so the detector can find
// correct viewTag to attach to.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const child: any = React.Children.only(this.props.children);
return React.cloneElement(
child,
{ collapsable: false },
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
child.props.children
);
} catch (e) {
throw new Error(
tagMessage(
`GestureDetector got more than one view as a child. If you want the gesture to work on multiple views, wrap them with a common parent and attach the gesture to that view.`
)
);
}
}
}
export const AnimatedWrap =
Reanimated?.default?.createAnimatedComponent(Wrap) ?? Wrap;
@@ -0,0 +1,42 @@
import React, { forwardRef } from 'react';
import type { LegacyRef, PropsWithChildren } from 'react';
import { tagMessage } from '../../../utils';
import { isRNSVGNode } from '../../../web/utils';
export const Wrap = forwardRef<HTMLDivElement, PropsWithChildren<{}>>(
({ children }, ref) => {
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const child: any = React.Children.only(children);
if (isRNSVGNode(child)) {
const clone = React.cloneElement(
child,
{ ref },
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
child.props.children
);
return clone;
}
return (
<div
ref={ref as LegacyRef<HTMLDivElement>}
style={{ display: 'contents' }}>
{child}
</div>
);
} catch (e) {
throw new Error(
tagMessage(
`GestureDetector got more than one view as a child. If you want the gesture to work on multiple views, wrap them with a common parent and attach the gesture to that view.`
)
);
}
}
);
// On web we never take a path with Reanimated,
// therefore we can simply export Wrap
export const AnimatedWrap = Wrap;
@@ -0,0 +1,112 @@
import React from 'react';
import { GestureType, HandlerCallbacks } from '../gesture';
import { registerHandler } from '../../handlersRegistry';
import RNGestureHandlerModule from '../../../RNGestureHandlerModule';
import { filterConfig, scheduleFlushOperations } from '../../utils';
import { ComposedGesture } from '../gestureComposition';
import { ActionType } from '../../../ActionType';
import { Platform } from 'react-native';
import type RNGestureHandlerModuleWeb from '../../../RNGestureHandlerModule.web';
import { ghQueueMicrotask } from '../../../ghQueueMicrotask';
import { AttachedGestureState, WebEventHandler } from './types';
import {
extractGestureRelations,
checkGestureCallbacksForWorklets,
ALLOWED_PROPS,
} from './utils';
import { MountRegistry } from '../../../mountRegistry';
interface AttachHandlersConfig {
preparedGesture: AttachedGestureState;
gestureConfig: ComposedGesture | GestureType;
gesturesToAttach: GestureType[];
viewTag: number;
webEventHandlersRef: React.RefObject<WebEventHandler>;
}
export function attachHandlers({
preparedGesture,
gestureConfig,
gesturesToAttach,
viewTag,
webEventHandlersRef,
}: AttachHandlersConfig) {
gestureConfig.initialize();
// Use queueMicrotask to extract handlerTags, because all refs should be initialized
// when it's ran
ghQueueMicrotask(() => {
if (!preparedGesture.isMounted) {
return;
}
gestureConfig.prepare();
});
for (const handler of gesturesToAttach) {
checkGestureCallbacksForWorklets(handler);
RNGestureHandlerModule.createGestureHandler(
handler.handlerName,
handler.handlerTag,
filterConfig(handler.config, ALLOWED_PROPS)
);
registerHandler(handler.handlerTag, handler, handler.config.testId);
}
// Use queueMicrotask to extract handlerTags, because all refs should be initialized
// when it's ran
ghQueueMicrotask(() => {
if (!preparedGesture.isMounted) {
return;
}
for (const handler of gesturesToAttach) {
RNGestureHandlerModule.updateGestureHandler(
handler.handlerTag,
filterConfig(
handler.config,
ALLOWED_PROPS,
extractGestureRelations(handler)
)
);
}
scheduleFlushOperations();
});
for (const gesture of gesturesToAttach) {
const actionType = gesture.shouldUseReanimated
? ActionType.REANIMATED_WORKLET
: ActionType.JS_FUNCTION_NEW_API;
if (Platform.OS === 'web') {
(
RNGestureHandlerModule.attachGestureHandler as typeof RNGestureHandlerModuleWeb.attachGestureHandler
)(
gesture.handlerTag,
viewTag,
ActionType.JS_FUNCTION_OLD_API, // Ignored on web
webEventHandlersRef
);
} else {
RNGestureHandlerModule.attachGestureHandler(
gesture.handlerTag,
viewTag,
actionType
);
}
MountRegistry.gestureWillMount(gesture);
}
preparedGesture.attachedGestures = gesturesToAttach;
if (preparedGesture.animatedHandlers) {
const isAnimatedGesture = (g: GestureType) => g.shouldUseReanimated;
preparedGesture.animatedHandlers.value = gesturesToAttach
.filter(isAnimatedGesture)
.map((g) => g.handlers) as unknown as HandlerCallbacks<
Record<string, unknown>
>[];
}
}
@@ -0,0 +1,17 @@
import { unregisterHandler } from '../../handlersRegistry';
import RNGestureHandlerModule from '../../../RNGestureHandlerModule';
import { scheduleFlushOperations } from '../../utils';
import { AttachedGestureState } from './types';
import { MountRegistry } from '../../../mountRegistry';
export function dropHandlers(preparedGesture: AttachedGestureState) {
for (const handler of preparedGesture.attachedGestures) {
RNGestureHandlerModule.dropGestureHandler(handler.handlerTag);
unregisterHandler(handler.handlerTag, handler.config.testId);
MountRegistry.gestureWillUnmount(handler);
}
scheduleFlushOperations();
}
@@ -0,0 +1,191 @@
/* eslint-disable react/no-unused-prop-types */
import React, {
useContext,
useEffect,
useLayoutEffect,
useMemo,
useRef,
} from 'react';
import { Platform } from 'react-native';
import findNodeHandle from '../../../findNodeHandle';
import { GestureType } from '../gesture';
import { UserSelect, TouchAction } from '../../gestureHandlerCommon';
import { ComposedGesture } from '../gestureComposition';
import { isTestEnv } from '../../../utils';
import GestureHandlerRootViewContext from '../../../GestureHandlerRootViewContext';
import { AttachedGestureState, GestureDetectorState } from './types';
import { useAnimatedGesture } from './useAnimatedGesture';
import { attachHandlers } from './attachHandlers';
import { needsToReattach } from './needsToReattach';
import { dropHandlers } from './dropHandlers';
import { useWebEventHandlers } from './utils';
import { Wrap, AnimatedWrap } from './Wrap';
import { useDetectorUpdater } from './useDetectorUpdater';
import { useViewRefHandler } from './useViewRefHandler';
import { useMountReactions } from './useMountReactions';
function propagateDetectorConfig(
props: GestureDetectorProps,
gesture: ComposedGesture | GestureType
) {
const keysToPropagate: (keyof GestureDetectorProps)[] = [
'userSelect',
'enableContextMenu',
'touchAction',
];
for (const key of keysToPropagate) {
const value = props[key];
if (value === undefined) {
continue;
}
for (const g of gesture.toGestureArray()) {
const config = g.config as { [key: string]: unknown };
config[key] = value;
}
}
}
interface GestureDetectorProps {
children?: React.ReactNode;
/**
* A gesture object containing the configuration and callbacks.
* Can be any of:
* - base gestures (`Tap`, `Pan`, ...)
* - `ComposedGesture` (`Race`, `Simultaneous`, `Exclusive`)
*/
gesture: ComposedGesture | GestureType;
/**
* #### Web only
* This parameter allows to specify which `userSelect` property should be applied to underlying view.
* Possible values are `"none" | "auto" | "text"`. Default value is set to `"none"`.
*/
userSelect?: UserSelect;
/**
* #### Web only
* Specifies whether context menu should be enabled after clicking on underlying view with right mouse button.
* Default value is set to `false`.
*/
enableContextMenu?: boolean;
/**
* #### Web only
* This parameter allows to specify which `touchAction` property should be applied to underlying view.
* Supports all CSS touch-action values (e.g. `"none"`, `"pan-y"`). Default value is set to `"none"`.
*/
touchAction?: TouchAction;
}
/**
* `GestureDetector` is responsible for creating and updating native gesture handlers based on the config of provided gesture.
*
* ### Props
* - `gesture`
* - `userSelect` (**Web only**)
* - `enableContextMenu` (**Web only**)
* - `touchAction` (**Web only**)
*
* ### Remarks
* - Gesture Detector will use first native view in its subtree to recognize gestures, however if this view is used only to group its children it may get automatically collapsed.
* - Using the same instance of a gesture across multiple Gesture Detectors is not possible.
*
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/gesture-detector
*/
export const GestureDetector = (props: GestureDetectorProps) => {
const rootViewContext = useContext(GestureHandlerRootViewContext);
if (__DEV__ && !rootViewContext && !isTestEnv() && Platform.OS !== 'web') {
throw new Error(
'GestureDetector must be used as a descendant of GestureHandlerRootView. Otherwise the gestures will not be recognized. See https://docs.swmansion.com/react-native-gesture-handler/docs/fundamentals/installation for more details.'
);
}
// Gesture config should be wrapped with useMemo to prevent unnecessary re-renders
const gestureConfig = props.gesture;
propagateDetectorConfig(props, gestureConfig);
const gesturesToAttach = useMemo(
() => gestureConfig.toGestureArray(),
[gestureConfig]
);
const shouldUseReanimated = gesturesToAttach.some(
(g) => g.shouldUseReanimated
);
const webEventHandlersRef = useWebEventHandlers();
// Store state in ref to prevent unnecessary renders
const state = useRef<GestureDetectorState>({
firstRender: true,
viewRef: null,
previousViewTag: -1,
forceRebuildReanimatedEvent: false,
}).current;
const preparedGesture = React.useRef<AttachedGestureState>({
attachedGestures: [],
animatedEventHandler: null,
animatedHandlers: null,
shouldUseReanimated: shouldUseReanimated,
isMounted: false,
}).current;
const updateAttachedGestures = useDetectorUpdater(
state,
preparedGesture,
gesturesToAttach,
gestureConfig,
webEventHandlersRef
);
const refHandler = useViewRefHandler(state, updateAttachedGestures);
// Reanimated event should be rebuilt only when gestures are reattached, otherwise
// config update will be enough as all necessary items are stored in shared values anyway
const needsToRebuildReanimatedEvent =
state.firstRender ||
state.forceRebuildReanimatedEvent ||
needsToReattach(preparedGesture, gesturesToAttach);
state.forceRebuildReanimatedEvent = false;
useAnimatedGesture(preparedGesture, needsToRebuildReanimatedEvent);
useLayoutEffect(() => {
const viewTag = findNodeHandle(state.viewRef) as number;
preparedGesture.isMounted = true;
attachHandlers({
preparedGesture,
gestureConfig,
gesturesToAttach,
webEventHandlersRef,
viewTag,
});
return () => {
preparedGesture.isMounted = false;
dropHandlers(preparedGesture);
};
}, []);
useEffect(() => {
if (state.firstRender) {
state.firstRender = false;
} else {
updateAttachedGestures();
}
}, [props]);
useMountReactions(updateAttachedGestures, preparedGesture);
if (shouldUseReanimated) {
return (
<AnimatedWrap
ref={refHandler}
onGestureHandlerEvent={preparedGesture.animatedEventHandler}>
{props.children}
</AnimatedWrap>
);
} else {
return <Wrap ref={refHandler}>{props.children}</Wrap>;
}
};
@@ -0,0 +1,27 @@
import { GestureType } from '../gesture';
import { AttachedGestureState } from './types';
// Checks whether the gesture should be reattached to the view, this will happen when:
// - The number of gestures in the preparedGesture is different than the number of gestures in the gesture
// - The handlerName is different in any of the gestures
// - At least one of the gestures changed the thread it runs on
export function needsToReattach(
preparedGesture: AttachedGestureState,
newGestures: GestureType[]
) {
if (newGestures.length !== preparedGesture.attachedGestures.length) {
return true;
}
for (let i = 0; i < newGestures.length; i++) {
if (
newGestures[i].handlerName !==
preparedGesture.attachedGestures[i].handlerName ||
newGestures[i].shouldUseReanimated !==
preparedGesture.attachedGestures[i].shouldUseReanimated
) {
return true;
}
}
return false;
}
@@ -0,0 +1,32 @@
import { GestureType, HandlerCallbacks } from '../gesture';
import { SharedValue } from '../reanimatedWrapper';
import { HandlerStateChangeEvent } from '../../gestureHandlerCommon';
export interface AttachedGestureState {
// Array of gestures that should be attached to the view under that gesture detector
attachedGestures: GestureType[];
// Event handler for the gesture, returned by `useEvent` from Reanimated
animatedEventHandler: unknown;
// Shared value that's responsible for transferring the callbacks to the UI thread handler
animatedHandlers: SharedValue<
HandlerCallbacks<Record<string, unknown>>[] | null
> | null;
// Whether `useAnimatedGesture` should be called inside detector
shouldUseReanimated: boolean;
// Whether the GestureDetector is mounted
isMounted: boolean;
}
export interface GestureDetectorState {
firstRender: boolean;
viewRef: React.Component | null;
previousViewTag: number;
forceRebuildReanimatedEvent: boolean;
}
export interface WebEventHandler {
onGestureHandlerEvent: (event: HandlerStateChangeEvent<unknown>) => void;
onGestureHandlerStateChange?: (
event: HandlerStateChangeEvent<unknown>
) => void;
}
@@ -0,0 +1,92 @@
import { GestureType, HandlerCallbacks } from '../gesture';
import { registerHandler } from '../../handlersRegistry';
import RNGestureHandlerModule from '../../../RNGestureHandlerModule';
import { filterConfig, scheduleFlushOperations } from '../../utils';
import { ComposedGesture } from '../gestureComposition';
import { ghQueueMicrotask } from '../../../ghQueueMicrotask';
import { AttachedGestureState } from './types';
import {
extractGestureRelations,
checkGestureCallbacksForWorklets,
ALLOWED_PROPS,
} from './utils';
export function updateHandlers(
preparedGesture: AttachedGestureState,
gestureConfig: ComposedGesture | GestureType,
newGestures: GestureType[]
) {
gestureConfig.prepare();
for (let i = 0; i < newGestures.length; i++) {
const handler = preparedGesture.attachedGestures[i];
checkGestureCallbacksForWorklets(handler);
// Only update handlerTag when it's actually different, it may be the same
// if gesture config object is wrapped with useMemo
if (newGestures[i].handlerTag !== handler.handlerTag) {
newGestures[i].handlerTag = handler.handlerTag;
newGestures[i].handlers.handlerTag = handler.handlerTag;
}
}
// Store attached gestures to avoid crash when gestures changed after queueing micro task
const attachedGestures = preparedGesture.attachedGestures;
// Use queueMicrotask to extract handlerTags, because when it's ran, all refs should be updated
// and handlerTags in BaseGesture references should be updated in the loop above (we need to wait
// in case of external relations)
ghQueueMicrotask(() => {
if (!preparedGesture.isMounted) {
return;
}
// Stop if attached gestures changed after queueing micro task
if (attachedGestures !== preparedGesture.attachedGestures) {
return;
}
// If amount of gesture configs changes, we need to update the callbacks in shared value
let shouldUpdateSharedValueIfUsed =
attachedGestures.length !== newGestures.length;
for (let i = 0; i < newGestures.length; i++) {
const handler = attachedGestures[i];
// If the gestureId is different (gesture isn't wrapped with useMemo or its dependencies changed),
// we need to update the shared value, assuming the gesture runs on UI thread or the thread changed
if (
handler.handlers.gestureId !== newGestures[i].handlers.gestureId &&
(newGestures[i].shouldUseReanimated || handler.shouldUseReanimated)
) {
shouldUpdateSharedValueIfUsed = true;
}
handler.config = newGestures[i].config;
handler.handlers = newGestures[i].handlers;
RNGestureHandlerModule.updateGestureHandler(
handler.handlerTag,
filterConfig(
handler.config,
ALLOWED_PROPS,
extractGestureRelations(handler)
)
);
registerHandler(handler.handlerTag, handler, handler.config.testId);
}
if (preparedGesture.animatedHandlers && shouldUpdateSharedValueIfUsed) {
const newHandlersValue = attachedGestures
.filter((g) => g.shouldUseReanimated) // Ignore gestures that shouldn't run on UI
.map((g) => g.handlers) as unknown as HandlerCallbacks<
Record<string, unknown>
>[];
preparedGesture.animatedHandlers.value = newHandlersValue;
}
scheduleFlushOperations();
});
}
@@ -0,0 +1,206 @@
import { HandlerCallbacks, CALLBACK_TYPE } from '../gesture';
import { Reanimated } from '../reanimatedWrapper';
import {
GestureTouchEvent,
GestureUpdateEvent,
GestureStateChangeEvent,
} from '../../gestureHandlerCommon';
import {
GestureStateManager,
GestureStateManagerType,
} from '../gestureStateManager';
import { State } from '../../../State';
import { TouchEventType } from '../../../TouchEventType';
import { tagMessage } from '../../../utils';
import { AttachedGestureState } from './types';
function getHandler(
type: CALLBACK_TYPE,
gesture: HandlerCallbacks<Record<string, unknown>>
) {
'worklet';
switch (type) {
case CALLBACK_TYPE.BEGAN:
return gesture.onBegin;
case CALLBACK_TYPE.START:
return gesture.onStart;
case CALLBACK_TYPE.UPDATE:
return gesture.onUpdate;
case CALLBACK_TYPE.CHANGE:
return gesture.onChange;
case CALLBACK_TYPE.END:
return gesture.onEnd;
case CALLBACK_TYPE.FINALIZE:
return gesture.onFinalize;
case CALLBACK_TYPE.TOUCHES_DOWN:
return gesture.onTouchesDown;
case CALLBACK_TYPE.TOUCHES_MOVE:
return gesture.onTouchesMove;
case CALLBACK_TYPE.TOUCHES_UP:
return gesture.onTouchesUp;
case CALLBACK_TYPE.TOUCHES_CANCELLED:
return gesture.onTouchesCancelled;
}
}
function touchEventTypeToCallbackType(
eventType: TouchEventType
): CALLBACK_TYPE {
'worklet';
switch (eventType) {
case TouchEventType.TOUCHES_DOWN:
return CALLBACK_TYPE.TOUCHES_DOWN;
case TouchEventType.TOUCHES_MOVE:
return CALLBACK_TYPE.TOUCHES_MOVE;
case TouchEventType.TOUCHES_UP:
return CALLBACK_TYPE.TOUCHES_UP;
case TouchEventType.TOUCHES_CANCELLED:
return CALLBACK_TYPE.TOUCHES_CANCELLED;
}
return CALLBACK_TYPE.UNDEFINED;
}
function runWorklet(
type: CALLBACK_TYPE,
gesture: HandlerCallbacks<Record<string, unknown>>,
event: GestureStateChangeEvent | GestureUpdateEvent | GestureTouchEvent,
...args: unknown[]
) {
'worklet';
const handler = getHandler(type, gesture);
if (gesture.isWorklet[type]) {
// @ts-ignore Logic below makes sure the correct event is send to the
// correct handler.
handler?.(event, ...args);
} else if (handler) {
console.warn(tagMessage('Animated gesture callback must be a worklet'));
}
}
function isStateChangeEvent(
event: GestureUpdateEvent | GestureStateChangeEvent | GestureTouchEvent
): event is GestureStateChangeEvent {
'worklet';
// @ts-ignore Yes, the oldState prop is missing on GestureTouchEvent, that's the point
return event.oldState != null;
}
function isTouchEvent(
event: GestureUpdateEvent | GestureStateChangeEvent | GestureTouchEvent
): event is GestureTouchEvent {
'worklet';
return event.eventType != null;
}
export function useAnimatedGesture(
preparedGesture: AttachedGestureState,
needsRebuild: boolean
) {
if (!Reanimated) {
return;
}
// Hooks are called conditionally, but the condition is whether the
// react-native-reanimated is installed, which shouldn't change while running
// eslint-disable-next-line react-hooks/rules-of-hooks
const sharedHandlersCallbacks = Reanimated.useSharedValue<
HandlerCallbacks<Record<string, unknown>>[] | null
>(null);
// eslint-disable-next-line react-hooks/rules-of-hooks
const lastUpdateEvent = Reanimated.useSharedValue<
(GestureUpdateEvent | undefined)[]
>([]);
// not every gesture needs a state controller, init them lazily
const stateControllers: GestureStateManagerType[] = [];
const callback = (
event: GestureStateChangeEvent | GestureUpdateEvent | GestureTouchEvent
) => {
'worklet';
const currentCallback = sharedHandlersCallbacks.value;
if (!currentCallback) {
return;
}
for (let i = 0; i < currentCallback.length; i++) {
const gesture = currentCallback[i];
if (event.handlerTag !== gesture.handlerTag) {
continue;
}
if (isStateChangeEvent(event)) {
if (
event.oldState === State.UNDETERMINED &&
event.state === State.BEGAN
) {
runWorklet(CALLBACK_TYPE.BEGAN, gesture, event);
} else if (
(event.oldState === State.BEGAN ||
event.oldState === State.UNDETERMINED) &&
event.state === State.ACTIVE
) {
runWorklet(CALLBACK_TYPE.START, gesture, event);
lastUpdateEvent.value[gesture.handlerTag] = undefined;
} else if (
event.oldState !== event.state &&
event.state === State.END
) {
if (event.oldState === State.ACTIVE) {
runWorklet(CALLBACK_TYPE.END, gesture, event, true);
}
runWorklet(CALLBACK_TYPE.FINALIZE, gesture, event, true);
} else if (
(event.state === State.FAILED || event.state === State.CANCELLED) &&
event.state !== event.oldState
) {
if (event.oldState === State.ACTIVE) {
runWorklet(CALLBACK_TYPE.END, gesture, event, false);
}
runWorklet(CALLBACK_TYPE.FINALIZE, gesture, event, false);
}
} else if (isTouchEvent(event)) {
if (!stateControllers[i]) {
stateControllers[i] = GestureStateManager.create(event.handlerTag);
}
if (event.eventType !== TouchEventType.UNDETERMINED) {
runWorklet(
touchEventTypeToCallbackType(event.eventType),
gesture,
event,
stateControllers[i]
);
}
} else {
runWorklet(CALLBACK_TYPE.UPDATE, gesture, event);
if (gesture.onChange && gesture.changeEventCalculator) {
runWorklet(
CALLBACK_TYPE.CHANGE,
gesture,
gesture.changeEventCalculator?.(
event,
lastUpdateEvent.value[gesture.handlerTag]
)
);
lastUpdateEvent.value[gesture.handlerTag] = event;
}
}
}
};
// eslint-disable-next-line react-hooks/rules-of-hooks
const event = Reanimated.useEvent(
callback,
['onGestureHandlerStateChange', 'onGestureHandlerEvent'],
needsRebuild
);
preparedGesture.animatedEventHandler = event;
preparedGesture.animatedHandlers = sharedHandlersCallbacks;
}
@@ -0,0 +1,69 @@
import React, { useCallback } from 'react';
import { GestureType } from '../gesture';
import { ComposedGesture } from '../gestureComposition';
import {
AttachedGestureState,
GestureDetectorState,
WebEventHandler,
} from './types';
import { attachHandlers } from './attachHandlers';
import { updateHandlers } from './updateHandlers';
import { needsToReattach } from './needsToReattach';
import { dropHandlers } from './dropHandlers';
import { useForceRender, validateDetectorChildren } from './utils';
import findNodeHandle from '../../../findNodeHandle';
// Returns a function that's responsible for updating the attached gestures
// If the view has changed, it will reattach the handlers to the new view
// If the view remains the same, it will update the handlers with the new config
export function useDetectorUpdater(
state: GestureDetectorState,
preparedGesture: AttachedGestureState,
gesturesToAttach: GestureType[],
gestureConfig: ComposedGesture | GestureType,
webEventHandlersRef: React.RefObject<WebEventHandler>
) {
const forceRender = useForceRender();
const updateAttachedGestures = useCallback(
// skipConfigUpdate is used to prevent unnecessary updates when only checking if the view has changed
(skipConfigUpdate?: boolean) => {
// If the underlying view has changed we need to reattach handlers to the new view
const viewTag = findNodeHandle(state.viewRef) as number;
const didUnderlyingViewChange = viewTag !== state.previousViewTag;
if (
didUnderlyingViewChange ||
needsToReattach(preparedGesture, gesturesToAttach)
) {
validateDetectorChildren(state.viewRef);
dropHandlers(preparedGesture);
attachHandlers({
preparedGesture,
gestureConfig,
gesturesToAttach,
webEventHandlersRef,
viewTag,
});
if (didUnderlyingViewChange) {
state.previousViewTag = viewTag;
state.forceRebuildReanimatedEvent = true;
forceRender();
}
} else if (!skipConfigUpdate) {
updateHandlers(preparedGesture, gestureConfig, gesturesToAttach);
}
},
[
forceRender,
gestureConfig,
gesturesToAttach,
preparedGesture,
state,
webEventHandlersRef,
]
);
return updateAttachedGestures;
}
@@ -0,0 +1,51 @@
import { transformIntoHandlerTags } from '../../utils';
import { MountRegistry } from '../../../mountRegistry';
import { AttachedGestureState } from './types';
import { useEffect } from 'react';
import { GestureRef } from '../gesture';
function shouldUpdateDetector(
relation: GestureRef[] | undefined,
gesture: { handlerTag: number }
) {
if (relation === undefined) {
return false;
}
for (const tag of transformIntoHandlerTags(relation)) {
if (tag === gesture.handlerTag) {
return true;
}
}
return false;
}
export function useMountReactions(
updateDetector: () => void,
state: AttachedGestureState
) {
useEffect(() => {
return MountRegistry.addMountListener((gesture) => {
// At this point the ref in the gesture config should be updated, so we can check if one of the gestures
// set in a relation with the gesture got mounted. If so, we need to update the detector to propagate
// the changes to the native side.
for (const attachedGesture of state.attachedGestures) {
const blocksHandlers = attachedGesture.config.blocksHandlers;
const requireToFail = attachedGesture.config.requireToFail;
const simultaneousWith = attachedGesture.config.simultaneousWith;
if (
shouldUpdateDetector(blocksHandlers, gesture) ||
shouldUpdateDetector(requireToFail, gesture) ||
shouldUpdateDetector(simultaneousWith, gesture)
) {
updateDetector();
// We can safely return here, if any other gestures should be updated, they will be by the above call
return;
}
}
});
}, [updateDetector, state]);
}
@@ -0,0 +1,54 @@
import { isFabric, tagMessage } from '../../../utils';
import { getShadowNodeFromRef } from '../../../getShadowNodeFromRef';
import { GestureDetectorState } from './types';
import React, { useCallback } from 'react';
import findNodeHandle from '../../../findNodeHandle';
declare const global: {
isViewFlatteningDisabled: (node: unknown) => boolean | null; // JSI function
};
// Ref handler for the Wrap component attached under the GestureDetector.
// It's responsible for setting the viewRef on the state and triggering the reattaching of handlers
// if the view has changed.
export function useViewRefHandler(
state: GestureDetectorState,
updateAttachedGestures: (skipConfigUpdate?: boolean) => void
) {
const refHandler = useCallback(
(ref: React.Component | null) => {
if (ref === null) {
return;
}
state.viewRef = ref;
// if it's the first render, also set the previousViewTag to prevent reattaching gestures when not needed
if (state.previousViewTag === -1) {
state.previousViewTag = findNodeHandle(state.viewRef) as number;
}
// Pass true as `skipConfigUpdate`. Here we only want to trigger the eventual reattaching of handlers
// in case the view has changed. If the view doesn't change, the update will be handled by detector.
if (!state.firstRender) {
updateAttachedGestures(true);
}
if (__DEV__ && isFabric() && global.isViewFlatteningDisabled) {
const node = getShadowNodeFromRef(ref);
if (global.isViewFlatteningDisabled(node) === false) {
console.error(
tagMessage(
'GestureDetector has received a child that may get view-flattened. ' +
'\nTo prevent it from misbehaving you need to wrap the child with a `<View collapsable={false}>`.'
)
);
}
}
},
[state, updateAttachedGestures]
);
return refHandler;
}
@@ -0,0 +1,181 @@
import { Platform } from 'react-native';
import { isTestEnv, tagMessage } from '../../../utils';
import { GestureRef, BaseGesture, GestureType } from '../gesture';
import { flingGestureHandlerProps } from '../../FlingGestureHandler';
import { forceTouchGestureHandlerProps } from '../../ForceTouchGestureHandler';
import { longPressGestureHandlerProps } from '../../LongPressGestureHandler';
import {
panGestureHandlerProps,
panGestureHandlerCustomNativeProps,
} from '../../PanGestureHandler';
import { tapGestureHandlerProps } from '../../TapGestureHandler';
import { hoverGestureHandlerProps } from '../hoverGesture';
import { nativeViewGestureHandlerProps } from '../../NativeViewGestureHandler';
import {
HandlerStateChangeEvent,
baseGestureHandlerWithDetectorProps,
} from '../../gestureHandlerCommon';
import { isNewWebImplementationEnabled } from '../../../EnableNewWebImplementation';
import { RNRenderer } from '../../../RNRenderer';
import { useCallback, useRef, useState } from 'react';
import { Reanimated } from '../reanimatedWrapper';
import { onGestureHandlerEvent } from '../eventReceiver';
import { WebEventHandler } from './types';
export const ALLOWED_PROPS = [
...baseGestureHandlerWithDetectorProps,
...tapGestureHandlerProps,
...panGestureHandlerProps,
...panGestureHandlerCustomNativeProps,
...longPressGestureHandlerProps,
...forceTouchGestureHandlerProps,
...flingGestureHandlerProps,
...hoverGestureHandlerProps,
...nativeViewGestureHandlerProps,
];
function convertToHandlerTag(ref: GestureRef): number {
if (typeof ref === 'number') {
return ref;
} else if (ref instanceof BaseGesture) {
return ref.handlerTag;
} else {
// @ts-ignore in this case it should be a ref either to gesture object or
// a gesture handler component, in both cases handlerTag property exists
return ref.current?.handlerTag ?? -1;
}
}
function extractValidHandlerTags(interactionGroup: GestureRef[] | undefined) {
return (
interactionGroup?.map(convertToHandlerTag)?.filter((tag) => tag > 0) ?? []
);
}
export function extractGestureRelations(gesture: GestureType) {
const requireToFail = extractValidHandlerTags(gesture.config.requireToFail);
const simultaneousWith = extractValidHandlerTags(
gesture.config.simultaneousWith
);
const blocksHandlers = extractValidHandlerTags(gesture.config.blocksHandlers);
return {
waitFor: requireToFail,
simultaneousHandlers: simultaneousWith,
blocksHandlers: blocksHandlers,
};
}
export function checkGestureCallbacksForWorklets(gesture: GestureType) {
if (!__DEV__) {
return;
}
// If a gesture is explicitly marked to run on the JS thread there is no need to check
// if callbacks are worklets as the user is aware they will be ran on the JS thread
if (gesture.config.runOnJS) {
return;
}
const areSomeNotWorklets = gesture.handlers.isWorklet.includes(false);
const areSomeWorklets = gesture.handlers.isWorklet.includes(true);
// If some of the callbacks are worklets and some are not, and the gesture is not
// explicitly marked with `.runOnJS(true)` show an error
if (areSomeNotWorklets && areSomeWorklets) {
console.error(
tagMessage(
`Some of the callbacks in the gesture are worklets and some are not. Either make sure that all calbacks are marked as 'worklet' if you wish to run them on the UI thread or use '.runOnJS(true)' modifier on the gesture explicitly to run all callbacks on the JS thread.`
)
);
}
if (Reanimated === undefined) {
// If Reanimated is not available, we can't run worklets, so we shouldn't show the warning
return;
}
const areAllNotWorklets = !areSomeWorklets && areSomeNotWorklets;
// If none of the callbacks are worklets and the gesture is not explicitly marked with
// `.runOnJS(true)` show a warning
if (areAllNotWorklets && !isTestEnv()) {
console.warn(
tagMessage(
`None of the callbacks in the gesture are worklets. If you wish to run them on the JS thread use '.runOnJS(true)' modifier on the gesture to make this explicit. Otherwise, mark the callbacks as 'worklet' to run them on the UI thread.`
)
);
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function validateDetectorChildren(ref: any) {
// Finds the first native view under the Wrap component and traverses the fiber tree upwards
// to check whether there is more than one native view as a pseudo-direct child of GestureDetector
// i.e. this is not ok:
// Wrap
// |
// / \
// / \
// / \
// / \
// NativeView NativeView
//
// but this is fine:
// Wrap
// |
// NativeView
// |
// / \
// / \
// / \
// / \
// NativeView NativeView
if (__DEV__ && Platform.OS !== 'web') {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const wrapType =
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
ref._reactInternals.elementType;
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
let instance =
RNRenderer.findHostInstance_DEPRECATED(
ref
)._internalFiberInstanceHandleDEV;
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
while (instance && instance.elementType !== wrapType) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (instance.sibling) {
throw new Error(
'GestureDetector has more than one native view as its children. This can happen if you are using a custom component that renders multiple views, like React.Fragment. You should wrap content of GestureDetector with a <View> or <Animated.View>.'
);
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
instance = instance.return;
}
}
}
export function useForceRender() {
const [renderState, setRenderState] = useState(false);
const forceRender = useCallback(() => {
setRenderState(!renderState);
}, [renderState, setRenderState]);
return forceRender;
}
export function useWebEventHandlers() {
return useRef<WebEventHandler>({
onGestureHandlerEvent: (e: HandlerStateChangeEvent<unknown>) => {
onGestureHandlerEvent(e.nativeEvent);
},
onGestureHandlerStateChange: isNewWebImplementationEnabled()
? (e: HandlerStateChangeEvent<unknown>) => {
onGestureHandlerEvent(e.nativeEvent);
}
: undefined,
});
}
@@ -0,0 +1,155 @@
import { DeviceEventEmitter, EmitterSubscription } from 'react-native';
import { State } from '../../State';
import { TouchEventType } from '../../TouchEventType';
import {
GestureTouchEvent,
GestureUpdateEvent,
GestureStateChangeEvent,
} from '../gestureHandlerCommon';
import { findHandler, findOldGestureHandler } from '../handlersRegistry';
import { BaseGesture } from './gesture';
import {
GestureStateManager,
GestureStateManagerType,
} from './gestureStateManager';
let gestureHandlerEventSubscription: EmitterSubscription | null = null;
let gestureHandlerStateChangeEventSubscription: EmitterSubscription | null =
null;
const gestureStateManagers: Map<number, GestureStateManagerType> = new Map<
number,
GestureStateManagerType
>();
const lastUpdateEvent: (GestureUpdateEvent | undefined)[] = [];
function isStateChangeEvent(
event: GestureUpdateEvent | GestureStateChangeEvent | GestureTouchEvent
): event is GestureStateChangeEvent {
// @ts-ignore oldState doesn't exist on GestureTouchEvent and that's the point
return event.oldState != null;
}
function isTouchEvent(
event: GestureUpdateEvent | GestureStateChangeEvent | GestureTouchEvent
): event is GestureTouchEvent {
return event.eventType != null;
}
export function onGestureHandlerEvent(
event: GestureUpdateEvent | GestureStateChangeEvent | GestureTouchEvent
) {
const handler = findHandler(event.handlerTag) as BaseGesture<
Record<string, unknown>
>;
if (handler) {
if (isStateChangeEvent(event)) {
if (
event.oldState === State.UNDETERMINED &&
event.state === State.BEGAN
) {
handler.handlers.onBegin?.(event);
} else if (
(event.oldState === State.BEGAN ||
event.oldState === State.UNDETERMINED) &&
event.state === State.ACTIVE
) {
handler.handlers.onStart?.(event);
lastUpdateEvent[handler.handlers.handlerTag] = event;
} else if (event.oldState !== event.state && event.state === State.END) {
if (event.oldState === State.ACTIVE) {
handler.handlers.onEnd?.(event, true);
}
handler.handlers.onFinalize?.(event, true);
lastUpdateEvent[handler.handlers.handlerTag] = undefined;
} else if (
(event.state === State.FAILED || event.state === State.CANCELLED) &&
event.oldState !== event.state
) {
if (event.oldState === State.ACTIVE) {
handler.handlers.onEnd?.(event, false);
}
handler.handlers.onFinalize?.(event, false);
gestureStateManagers.delete(event.handlerTag);
lastUpdateEvent[handler.handlers.handlerTag] = undefined;
}
} else if (isTouchEvent(event)) {
if (!gestureStateManagers.has(event.handlerTag)) {
gestureStateManagers.set(
event.handlerTag,
GestureStateManager.create(event.handlerTag)
);
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const manager = gestureStateManagers.get(event.handlerTag)!;
switch (event.eventType) {
case TouchEventType.TOUCHES_DOWN:
handler.handlers?.onTouchesDown?.(event, manager);
break;
case TouchEventType.TOUCHES_MOVE:
handler.handlers?.onTouchesMove?.(event, manager);
break;
case TouchEventType.TOUCHES_UP:
handler.handlers?.onTouchesUp?.(event, manager);
break;
case TouchEventType.TOUCHES_CANCELLED:
handler.handlers?.onTouchesCancelled?.(event, manager);
break;
}
} else {
handler.handlers.onUpdate?.(event);
if (handler.handlers.onChange && handler.handlers.changeEventCalculator) {
handler.handlers.onChange?.(
handler.handlers.changeEventCalculator?.(
event,
lastUpdateEvent[handler.handlers.handlerTag]
)
);
lastUpdateEvent[handler.handlers.handlerTag] = event;
}
}
} else {
const oldHandler = findOldGestureHandler(event.handlerTag);
if (oldHandler) {
const nativeEvent = { nativeEvent: event };
if (isStateChangeEvent(event)) {
oldHandler.onGestureStateChange(nativeEvent);
} else {
oldHandler.onGestureEvent(nativeEvent);
}
return;
}
}
}
export function startListening() {
stopListening();
gestureHandlerEventSubscription = DeviceEventEmitter.addListener(
'onGestureHandlerEvent',
onGestureHandlerEvent
);
gestureHandlerStateChangeEventSubscription = DeviceEventEmitter.addListener(
'onGestureHandlerStateChange',
onGestureHandlerEvent
);
}
export function stopListening() {
if (gestureHandlerEventSubscription) {
gestureHandlerEventSubscription.remove();
gestureHandlerEventSubscription = null;
}
if (gestureHandlerStateChangeEventSubscription) {
gestureHandlerStateChangeEventSubscription.remove();
gestureHandlerStateChangeEventSubscription = null;
}
}
@@ -0,0 +1,36 @@
import { BaseGesture, BaseGestureConfig } from './gesture';
import { FlingGestureConfig } from '../FlingGestureHandler';
import type { FlingGestureHandlerEventPayload } from '../GestureHandlerEventPayload';
export class FlingGesture extends BaseGesture<FlingGestureHandlerEventPayload> {
public config: BaseGestureConfig & FlingGestureConfig = {};
constructor() {
super();
this.handlerName = 'FlingGestureHandler';
}
/**
* Determine exact number of points required to handle the fling gesture.
* @param pointers
*/
numberOfPointers(pointers: number) {
this.config.numberOfPointers = pointers;
return this;
}
/**
* Expressed allowed direction of movement.
* Expected values are exported as constants in the Directions object.
* Arguments can be combined using `|` operator. Default value is set to `MouseButton.LEFT`.
* @param direction
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/fling-gesture/#directionvalue-directions
*/
direction(direction: number) {
this.config.direction = direction;
return this;
}
}
export type FlingGestureType = InstanceType<typeof FlingGesture>;
@@ -0,0 +1,95 @@
import { BaseGestureConfig, ContinousBaseGesture } from './gesture';
import { ForceTouchGestureConfig } from '../ForceTouchGestureHandler';
import type { ForceTouchGestureHandlerEventPayload } from '../GestureHandlerEventPayload';
import { GestureUpdateEvent } from '../gestureHandlerCommon';
/**
* @deprecated ForceTouch gesture is deprecated and will be removed in the future.
*/
export type ForceTouchGestureChangeEventPayload = {
forceChange: number;
};
function changeEventCalculator(
current: GestureUpdateEvent<ForceTouchGestureHandlerEventPayload>,
previous?: GestureUpdateEvent<ForceTouchGestureHandlerEventPayload>
) {
'worklet';
let changePayload: ForceTouchGestureChangeEventPayload;
if (previous === undefined) {
changePayload = {
forceChange: current.force,
};
} else {
changePayload = {
forceChange: current.force - previous.force,
};
}
return { ...current, ...changePayload };
}
/**
* @deprecated ForceTouch gesture is deprecated and will be removed in the future.
*/
export class ForceTouchGesture extends ContinousBaseGesture<
ForceTouchGestureHandlerEventPayload,
ForceTouchGestureChangeEventPayload
> {
public config: BaseGestureConfig & ForceTouchGestureConfig = {};
constructor() {
super();
this.handlerName = 'ForceTouchGestureHandler';
}
/**
* A minimal pressure that is required before gesture can activate.
* Should be a value from range [0.0, 1.0]. Default is 0.2.
* @param force
*/
minForce(force: number) {
this.config.minForce = force;
return this;
}
/**
* A maximal pressure that could be applied for gesture.
* If the pressure is greater, gesture fails. Should be a value from range [0.0, 1.0].
* @param force
*/
maxForce(force: number) {
this.config.maxForce = force;
return this;
}
/**
* Value defining if haptic feedback has to be performed on activation.
* @param value
*/
feedbackOnActivation(value: boolean) {
this.config.feedbackOnActivation = value;
return this;
}
onChange(
callback: (
event: GestureUpdateEvent<
GestureUpdateEvent<
ForceTouchGestureHandlerEventPayload &
ForceTouchGestureChangeEventPayload
>
>
) => void
) {
// @ts-ignore TS being overprotective, ForceTouchGestureHandlerEventPayload is Record
this.handlers.changeEventCalculator = changeEventCalculator;
return super.onChange(callback);
}
}
/**
* @deprecated ForceTouch gesture is deprecated and will be removed in the future.
*/
export type ForceTouchGestureType = InstanceType<typeof ForceTouchGesture>;
@@ -0,0 +1,472 @@
import {
HitSlop,
CommonGestureConfig,
GestureTouchEvent,
GestureStateChangeEvent,
GestureUpdateEvent,
ActiveCursor,
MouseButton,
} from '../gestureHandlerCommon';
import { getNextHandlerTag } from '../getNextHandlerTag';
import { GestureStateManagerType } from './gestureStateManager';
import type {
FlingGestureHandlerEventPayload,
ForceTouchGestureHandlerEventPayload,
LongPressGestureHandlerEventPayload,
PanGestureHandlerEventPayload,
PinchGestureHandlerEventPayload,
RotationGestureHandlerEventPayload,
TapGestureHandlerEventPayload,
NativeViewGestureHandlerPayload,
HoverGestureHandlerEventPayload,
} from '../GestureHandlerEventPayload';
import { isRemoteDebuggingEnabled } from '../../utils';
export type GestureType =
| BaseGesture<Record<string, unknown>>
| BaseGesture<Record<string, never>>
| BaseGesture<TapGestureHandlerEventPayload>
| BaseGesture<PanGestureHandlerEventPayload>
| BaseGesture<LongPressGestureHandlerEventPayload>
| BaseGesture<RotationGestureHandlerEventPayload>
| BaseGesture<PinchGestureHandlerEventPayload>
| BaseGesture<FlingGestureHandlerEventPayload>
| BaseGesture<ForceTouchGestureHandlerEventPayload>
| BaseGesture<NativeViewGestureHandlerPayload>
| BaseGesture<HoverGestureHandlerEventPayload>;
export type GestureRef =
| number
| GestureType
| React.RefObject<GestureType | undefined>
| React.RefObject<React.ComponentType | undefined>; // Allow adding a ref to a gesture handler
export interface BaseGestureConfig
extends CommonGestureConfig,
Record<string, unknown> {
ref?: React.MutableRefObject<GestureType | undefined>;
requireToFail?: GestureRef[];
simultaneousWith?: GestureRef[];
blocksHandlers?: GestureRef[];
needsPointerData?: boolean;
manualActivation?: boolean;
runOnJS?: boolean;
testId?: string;
cancelsTouchesInView?: boolean;
}
type TouchEventHandlerType = (
event: GestureTouchEvent,
stateManager: GestureStateManagerType
) => void;
export type HandlerCallbacks<EventPayloadT extends Record<string, unknown>> = {
gestureId: number;
handlerTag: number;
onBegin?: (event: GestureStateChangeEvent<EventPayloadT>) => void;
onStart?: (event: GestureStateChangeEvent<EventPayloadT>) => void;
onEnd?: (
event: GestureStateChangeEvent<EventPayloadT>,
success: boolean
) => void;
onFinalize?: (
event: GestureStateChangeEvent<EventPayloadT>,
success: boolean
) => void;
onUpdate?: (event: GestureUpdateEvent<EventPayloadT>) => void;
onChange?: (event: any) => void;
onTouchesDown?: TouchEventHandlerType;
onTouchesMove?: TouchEventHandlerType;
onTouchesUp?: TouchEventHandlerType;
onTouchesCancelled?: TouchEventHandlerType;
changeEventCalculator?: (
current: GestureUpdateEvent<Record<string, unknown>>,
previous?: GestureUpdateEvent<Record<string, unknown>>
) => GestureUpdateEvent<Record<string, unknown>>;
isWorklet: boolean[];
};
export const CALLBACK_TYPE = {
UNDEFINED: 0,
BEGAN: 1,
START: 2,
UPDATE: 3,
CHANGE: 4,
END: 5,
FINALIZE: 6,
TOUCHES_DOWN: 7,
TOUCHES_MOVE: 8,
TOUCHES_UP: 9,
TOUCHES_CANCELLED: 10,
} as const;
// Allow using CALLBACK_TYPE as object and type
// eslint-disable-next-line @typescript-eslint/no-redeclare
export type CALLBACK_TYPE = (typeof CALLBACK_TYPE)[keyof typeof CALLBACK_TYPE];
export abstract class Gesture {
/**
* Return array of gestures, providing the same interface for creating and updating
* handlers, no matter which object was used to create gesture instance.
*/
abstract toGestureArray(): GestureType[];
/**
* Assign handlerTag to the gesture instance and set ref.current (if a ref is set)
*/
abstract initialize(): void;
/**
* Make sure that values of properties defining relations are arrays. Do any necessary
* preprocessing required to configure relations between handlers. Called just before
* updating the handler on the native side.
*/
abstract prepare(): void;
}
let nextGestureId = 0;
export abstract class BaseGesture<
EventPayloadT extends Record<string, unknown>,
> extends Gesture {
private gestureId = -1;
public handlerTag = -1;
public handlerName = '';
public config: BaseGestureConfig = {};
public handlers: HandlerCallbacks<EventPayloadT> = {
gestureId: -1,
handlerTag: -1,
isWorklet: [],
};
constructor() {
super();
// Used to check whether the gesture config has been updated when wrapping it
// with `useMemo`. Since every config will have a unique id, when the dependencies
// don't change, the config won't be recreated and the id will stay the same.
// If the id is different, it means that the config has changed and the gesture
// needs to be updated.
this.gestureId = nextGestureId++;
this.handlers.gestureId = this.gestureId;
}
private addDependency(
key: 'simultaneousWith' | 'requireToFail' | 'blocksHandlers',
gesture: Exclude<GestureRef, number>
) {
const value = this.config[key];
this.config[key] = value
? Array<GestureRef>().concat(value, gesture)
: [gesture];
}
/**
* Sets a `ref` to the gesture object, allowing for interoperability with the old API.
* @param ref
*/
withRef(ref: React.MutableRefObject<GestureType | undefined>) {
this.config.ref = ref;
return this;
}
// eslint-disable-next-line @typescript-eslint/ban-types
protected isWorklet(callback: Function) {
// @ts-ignore if callback is a worklet, the property will be available, if not then the check will return false
return callback.__workletHash !== undefined;
}
/**
* Set the callback that is being called when given gesture handler starts receiving touches.
* At the moment of this callback the handler is in `BEGAN` state and we don't know yet if it will recognize the gesture at all.
* @param callback
*/
onBegin(callback: (event: GestureStateChangeEvent<EventPayloadT>) => void) {
this.handlers.onBegin = callback;
this.handlers.isWorklet[CALLBACK_TYPE.BEGAN] = this.isWorklet(callback);
return this;
}
/**
* Set the callback that is being called when the gesture is recognized by the handler and it transitions to the `ACTIVE` state.
* @param callback
*/
onStart(callback: (event: GestureStateChangeEvent<EventPayloadT>) => void) {
this.handlers.onStart = callback;
this.handlers.isWorklet[CALLBACK_TYPE.START] = this.isWorklet(callback);
return this;
}
/**
* Set the callback that is being called when the gesture that was recognized by the handler finishes and handler reaches `END` state.
* It will be called only if the handler was previously in the `ACTIVE` state.
* @param callback
*/
onEnd(
callback: (
event: GestureStateChangeEvent<EventPayloadT>,
success: boolean
) => void
) {
this.handlers.onEnd = callback;
// @ts-ignore if callback is a worklet, the property will be available, if not then the check will return false
this.handlers.isWorklet[CALLBACK_TYPE.END] = this.isWorklet(callback);
return this;
}
/**
* Set the callback that is being called when the handler finalizes handling gesture - the gesture was recognized and has finished or it failed to recognize.
* @param callback
*/
onFinalize(
callback: (
event: GestureStateChangeEvent<EventPayloadT>,
success: boolean
) => void
) {
this.handlers.onFinalize = callback;
// @ts-ignore if callback is a worklet, the property will be available, if not then the check will return false
this.handlers.isWorklet[CALLBACK_TYPE.FINALIZE] = this.isWorklet(callback);
return this;
}
/**
* Set the `onTouchesDown` callback which is called every time a pointer is placed on the screen.
* @param callback
*/
onTouchesDown(callback: TouchEventHandlerType) {
this.config.needsPointerData = true;
this.handlers.onTouchesDown = callback;
this.handlers.isWorklet[CALLBACK_TYPE.TOUCHES_DOWN] =
this.isWorklet(callback);
return this;
}
/**
* Set the `onTouchesMove` callback which is called every time a pointer is moved on the screen.
* @param callback
*/
onTouchesMove(callback: TouchEventHandlerType) {
this.config.needsPointerData = true;
this.handlers.onTouchesMove = callback;
this.handlers.isWorklet[CALLBACK_TYPE.TOUCHES_MOVE] =
this.isWorklet(callback);
return this;
}
/**
* Set the `onTouchesUp` callback which is called every time a pointer is lifted from the screen.
* @param callback
*/
onTouchesUp(callback: TouchEventHandlerType) {
this.config.needsPointerData = true;
this.handlers.onTouchesUp = callback;
this.handlers.isWorklet[CALLBACK_TYPE.TOUCHES_UP] =
this.isWorklet(callback);
return this;
}
/**
* Set the `onTouchesCancelled` callback which is called every time a pointer stops being tracked, for example when the gesture finishes.
* @param callback
*/
onTouchesCancelled(callback: TouchEventHandlerType) {
this.config.needsPointerData = true;
this.handlers.onTouchesCancelled = callback;
this.handlers.isWorklet[CALLBACK_TYPE.TOUCHES_CANCELLED] =
this.isWorklet(callback);
return this;
}
/**
* Indicates whether the given handler should be analyzing stream of touch events or not.
* @param enabled
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/pan-gesture#enabledvalue-boolean
*/
enabled(enabled: boolean) {
this.config.enabled = enabled;
return this;
}
/**
* When true the handler will cancel or fail recognition (depending on its current state) whenever the finger leaves the area of the connected view.
* @param value
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/pan-gesture#shouldcancelwhenoutsidevalue-boolean
*/
shouldCancelWhenOutside(value: boolean) {
this.config.shouldCancelWhenOutside = value;
return this;
}
/**
* This parameter enables control over what part of the connected view area can be used to begin recognizing the gesture.
* When a negative number is provided the bounds of the view will reduce the area by the given number of points in each of the sides evenly.
* @param hitSlop
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/pan-gesture#hitslopsettings
*/
hitSlop(hitSlop: HitSlop) {
this.config.hitSlop = hitSlop;
return this;
}
/**
* #### Web only
* This parameter allows to specify which `cursor` should be used when gesture activates.
* Supports all CSS cursor values (e.g. `"grab"`, `"zoom-in"`). Default value is set to `"auto"`.
* @param activeCursor
*/
activeCursor(activeCursor: ActiveCursor) {
this.config.activeCursor = activeCursor;
return this;
}
/**
* #### Web & Android only
* Allows users to choose which mouse button should handler respond to.
* Arguments can be combined using `|` operator, e.g. `mouseButton(MouseButton.LEFT | MouseButton.RIGHT)`.
* Default value is set to `MouseButton.LEFT`.
* @param mouseButton
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/pan-gesture#mousebuttonvalue-mousebutton-web--android-only
*/
mouseButton(mouseButton: MouseButton) {
this.config.mouseButton = mouseButton;
return this;
}
/**
* When `react-native-reanimated` is installed, the callbacks passed to the gestures are automatically workletized and run on the UI thread when called.
* This option allows for changing this behavior: when `true`, all the callbacks will be run on the JS thread instead of the UI thread, regardless of whether they are worklets or not.
* Defaults to `false`.
* @param runOnJS
*/
runOnJS(runOnJS: boolean) {
this.config.runOnJS = runOnJS;
return this;
}
/**
* Allows gestures across different components to be recognized simultaneously.
* @param gestures
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/fundamentals/gesture-composition/#simultaneouswithexternalgesture
*/
simultaneousWithExternalGesture(...gestures: Exclude<GestureRef, number>[]) {
for (const gesture of gestures) {
this.addDependency('simultaneousWith', gesture);
}
return this;
}
/**
* Allows to delay activation of the handler until all handlers passed as arguments to this method fail (or don't begin at all).
* @param gestures
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/fundamentals/gesture-composition/#requireexternalgesturetofail
*/
requireExternalGestureToFail(...gestures: Exclude<GestureRef, number>[]) {
for (const gesture of gestures) {
this.addDependency('requireToFail', gesture);
}
return this;
}
/**
* Works similarily to `requireExternalGestureToFail` but the direction of the relation is reversed - instead of being one-to-many relation, it's many-to-one.
* @param gestures
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/fundamentals/gesture-composition/#blocksexternalgesture
*/
blocksExternalGesture(...gestures: Exclude<GestureRef, number>[]) {
for (const gesture of gestures) {
this.addDependency('blocksHandlers', gesture);
}
return this;
}
/**
* Sets a `testID` property for gesture object, allowing for querying for it in tests.
* @param id
*/
withTestId(id: string) {
this.config.testId = id;
return this;
}
/**
* #### iOS only
* When `true`, the handler will cancel touches for native UI components (`UIButton`, `UISwitch`, etc) it's attached to when it becomes `ACTIVE`.
* Default value is `true`.
* @param value
*/
cancelsTouchesInView(value: boolean) {
this.config.cancelsTouchesInView = value;
return this;
}
initialize() {
this.handlerTag = getNextHandlerTag();
this.handlers = { ...this.handlers, handlerTag: this.handlerTag };
if (this.config.ref) {
this.config.ref.current = this as GestureType;
}
}
toGestureArray(): GestureType[] {
return [this as GestureType];
}
// eslint-disable-next-line @typescript-eslint/no-empty-function
prepare() {}
get shouldUseReanimated(): boolean {
// Use Reanimated when runOnJS isn't set explicitly,
// all defined callbacks are worklets
// and remote debugging is disabled
return (
this.config.runOnJS !== true &&
!this.handlers.isWorklet.includes(false) &&
!isRemoteDebuggingEnabled()
);
}
}
export abstract class ContinousBaseGesture<
EventPayloadT extends Record<string, unknown>,
EventChangePayloadT extends Record<string, unknown>,
> extends BaseGesture<EventPayloadT> {
/**
* Set the callback that is being called every time the gesture receives an update while it's active.
* @param callback
*/
onUpdate(callback: (event: GestureUpdateEvent<EventPayloadT>) => void) {
this.handlers.onUpdate = callback;
this.handlers.isWorklet[CALLBACK_TYPE.UPDATE] = this.isWorklet(callback);
return this;
}
/**
* Set the callback that is being called every time the gesture receives an update while it's active.
* This callback will receive information about change in value in relation to the last received event.
* @param callback
*/
onChange(
callback: (
event: GestureUpdateEvent<EventPayloadT & EventChangePayloadT>
) => void
) {
this.handlers.onChange = callback;
this.handlers.isWorklet[CALLBACK_TYPE.CHANGE] = this.isWorklet(callback);
return this;
}
/**
* When `true` the handler will not activate by itself even if its activation criteria are met.
* Instead you can manipulate its state using state manager.
* @param manualActivation
*/
manualActivation(manualActivation: boolean) {
this.config.manualActivation = manualActivation;
return this;
}
}
@@ -0,0 +1,124 @@
import { BaseGesture, Gesture, GestureRef, GestureType } from './gesture';
function extendRelation(
currentRelation: GestureRef[] | undefined,
extendWith: GestureType[]
) {
if (currentRelation === undefined) {
return [...extendWith];
} else {
return [...currentRelation, ...extendWith];
}
}
export class ComposedGesture extends Gesture {
protected gestures: Gesture[] = [];
protected simultaneousGestures: GestureType[] = [];
protected requireGesturesToFail: GestureType[] = [];
constructor(...gestures: Gesture[]) {
super();
this.gestures = gestures;
}
protected prepareSingleGesture(
gesture: Gesture,
simultaneousGestures: GestureType[],
requireGesturesToFail: GestureType[]
) {
if (gesture instanceof BaseGesture) {
const newConfig = { ...gesture.config };
// No need to extend `blocksHandlers` here, because it's not changed in composition.
// The same effect is achieved by reversing the order of 2 gestures in `Exclusive`
newConfig.simultaneousWith = extendRelation(
newConfig.simultaneousWith,
simultaneousGestures
);
newConfig.requireToFail = extendRelation(
newConfig.requireToFail,
requireGesturesToFail
);
gesture.config = newConfig;
} else if (gesture instanceof ComposedGesture) {
gesture.simultaneousGestures = simultaneousGestures;
gesture.requireGesturesToFail = requireGesturesToFail;
gesture.prepare();
}
}
prepare() {
for (const gesture of this.gestures) {
this.prepareSingleGesture(
gesture,
this.simultaneousGestures,
this.requireGesturesToFail
);
}
}
initialize() {
for (const gesture of this.gestures) {
gesture.initialize();
}
}
toGestureArray(): GestureType[] {
return this.gestures.flatMap((gesture) => gesture.toGestureArray());
}
}
export class SimultaneousGesture extends ComposedGesture {
prepare() {
// This piece of magic works something like this:
// for every gesture in the array
const simultaneousArrays = this.gestures.map((gesture) =>
// we take the array it's in
this.gestures
// and make a copy without it
.filter((x) => x !== gesture)
// then we flatmap the result to get list of raw (not composed) gestures
// this way we don't make the gestures simultaneous with themselves, which is
// important when the gesture is `ExclusiveGesture` - we don't want to make
// exclusive gestures simultaneous
.flatMap((x) => x.toGestureArray())
);
for (let i = 0; i < this.gestures.length; i++) {
this.prepareSingleGesture(
this.gestures[i],
simultaneousArrays[i],
this.requireGesturesToFail
);
}
}
}
export class ExclusiveGesture extends ComposedGesture {
prepare() {
// Transforms the array of gestures into array of grouped raw (not composed) gestures
// i.e. [gesture1, gesture2, ComposedGesture(gesture3, gesture4)] -> [[gesture1], [gesture2], [gesture3, gesture4]]
const gestureArrays = this.gestures.map((gesture) =>
gesture.toGestureArray()
);
let requireToFail: GestureType[] = [];
for (let i = 0; i < this.gestures.length; i++) {
this.prepareSingleGesture(
this.gestures[i],
this.simultaneousGestures,
this.requireGesturesToFail.concat(requireToFail)
);
// Every group gets to wait for all groups before it
requireToFail = requireToFail.concat(gestureArrays[i]);
}
}
}
export type ComposedGestureType = InstanceType<typeof ComposedGesture>;
export type RaceGestureType = ComposedGestureType;
export type SimultaneousGestureType = InstanceType<typeof SimultaneousGesture>;
export type ExclusiveGestureType = InstanceType<typeof ExclusiveGesture>;
@@ -0,0 +1,143 @@
import { FlingGesture } from './flingGesture';
import { ForceTouchGesture } from './forceTouchGesture';
import { Gesture } from './gesture';
import {
ComposedGesture,
ExclusiveGesture,
SimultaneousGesture,
} from './gestureComposition';
import { LongPressGesture } from './longPressGesture';
import { PanGesture } from './panGesture';
import { PinchGesture } from './pinchGesture';
import { RotationGesture } from './rotationGesture';
import { TapGesture } from './tapGesture';
import { NativeGesture } from './nativeGesture';
import { ManualGesture } from './manualGesture';
import { HoverGesture } from './hoverGesture';
/**
* `Gesture` is the object that allows you to create and compose gestures.
*
* ### Remarks
* - Consider wrapping your gesture configurations with `useMemo`, as it will reduce the amount of work Gesture Handler has to do under the hood when updating gestures.
*
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/gesture
*/
export const GestureObjects = {
/**
* A discrete gesture that recognizes one or many taps.
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/tap-gesture
*/
Tap: () => {
return new TapGesture();
},
/**
* A continuous gesture that can recognize a panning (dragging) gesture and track its movement.
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/pan-gesture
*/
Pan: () => {
return new PanGesture();
},
/**
* A continuous gesture that recognizes pinch gesture. It allows for tracking the distance between two fingers and use that information to scale or zoom your content.
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/pinch-gesture
*/
Pinch: () => {
return new PinchGesture();
},
/**
* A continuous gesture that can recognize rotation and track its movement.
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/rotation-gesture
*/
Rotation: () => {
return new RotationGesture();
},
/**
* A discrete gesture that activates when the movement is sufficiently fast.
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/fling-gesture
*/
Fling: () => {
return new FlingGesture();
},
/**
* A discrete gesture that activates when the corresponding view is pressed for a sufficiently long time.
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/long-press-gesture
*/
LongPress: () => {
return new LongPressGesture();
},
/**
* @deprecated ForceTouch gesture is deprecated and will be removed in the future.
*
* #### iOS only
* A continuous gesture that recognizes force of a touch. It allows for tracking pressure of touch on some iOS devices.
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/force-touch-gesture
*/
ForceTouch: () => {
return new ForceTouchGesture();
},
/**
* A gesture that allows other touch handling components to participate in RNGH's gesture system.
* When used, the other component should be the direct child of a `GestureDetector`.
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/native-gesture
*/
Native: () => {
return new NativeGesture();
},
/**
* A plain gesture that has no specific activation criteria nor event data set.
* Its state has to be controlled manually using a state manager.
* It will not fail when all the pointers are lifted from the screen.
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/manual-gesture
*/
Manual: () => {
return new ManualGesture();
},
/**
* A continuous gesture that can recognize hovering above the view it's attached to.
* The hover effect may be activated by moving a mouse or a stylus over the view.
*
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/hover-gesture
*/
Hover: () => {
return new HoverGesture();
},
/**
* Builds a composed gesture consisting of gestures provided as parameters.
* The first one that becomes active cancels the rest of gestures.
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/fundamentals/gesture-composition/#race
*/
Race: (...gestures: Gesture[]) => {
return new ComposedGesture(...gestures);
},
/**
* Builds a composed gesture that allows all base gestures to run simultaneously.
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/fundamentals/gesture-composition/#simultaneous
*/
Simultaneous(...gestures: Gesture[]) {
return new SimultaneousGesture(...gestures);
},
/**
* Builds a composed gesture where only one of the provided gestures can become active.
* Priority is decided through the order of gestures: the first one has higher priority
* than the second one, second one has higher priority than the third one, and so on.
* For example, to make a gesture that recognizes both single and double tap you need
* to call Exclusive(doubleTap, singleTap).
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/fundamentals/gesture-composition/#exclusive
*/
Exclusive(...gestures: Gesture[]) {
return new ExclusiveGesture(...gestures);
},
};
@@ -0,0 +1,72 @@
import { Reanimated } from './reanimatedWrapper';
import { State } from '../../State';
import { tagMessage } from '../../utils';
export interface GestureStateManagerType {
begin: () => void;
activate: () => void;
fail: () => void;
end: () => void;
}
const warningMessage = tagMessage(
'react-native-reanimated is required in order to use synchronous state management'
);
// Check if reanimated module is available, but look for useSharedValue as conditional
// require of reanimated can sometimes return content of `utils.ts` file (?)
const REANIMATED_AVAILABLE = Reanimated?.useSharedValue !== undefined;
const setGestureState = Reanimated?.setGestureState;
function create(handlerTag: number): GestureStateManagerType {
'worklet';
return {
begin: () => {
'worklet';
if (REANIMATED_AVAILABLE) {
// When Reanimated is available, setGestureState should be defined
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
setGestureState!(handlerTag, State.BEGAN);
} else {
console.warn(warningMessage);
}
},
activate: () => {
'worklet';
if (REANIMATED_AVAILABLE) {
// When Reanimated is available, setGestureState should be defined
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
setGestureState!(handlerTag, State.ACTIVE);
} else {
console.warn(warningMessage);
}
},
fail: () => {
'worklet';
if (REANIMATED_AVAILABLE) {
// When Reanimated is available, setGestureState should be defined
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
setGestureState!(handlerTag, State.FAILED);
} else {
console.warn(warningMessage);
}
},
end: () => {
'worklet';
if (REANIMATED_AVAILABLE) {
// When Reanimated is available, setGestureState should be defined
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
setGestureState!(handlerTag, State.END);
} else {
console.warn(warningMessage);
}
},
};
}
export const GestureStateManager = {
create,
};
@@ -0,0 +1,24 @@
import NodeManager from '../../web/tools/NodeManager';
import { GestureStateManagerType } from './gestureStateManager';
export const GestureStateManager = {
create(handlerTag: number): GestureStateManagerType {
return {
begin: () => {
NodeManager.getHandler(handlerTag).begin();
},
activate: () => {
NodeManager.getHandler(handlerTag).activate(true);
},
fail: () => {
NodeManager.getHandler(handlerTag).fail();
},
end: () => {
NodeManager.getHandler(handlerTag).end();
},
};
},
};
@@ -0,0 +1,77 @@
import { BaseGestureConfig, ContinousBaseGesture } from './gesture';
import { GestureUpdateEvent } from '../gestureHandlerCommon';
import type { HoverGestureHandlerEventPayload } from '../GestureHandlerEventPayload';
export type HoverGestureChangeEventPayload = {
changeX: number;
changeY: number;
};
export enum HoverEffect {
NONE = 0,
LIFT = 1,
HIGHLIGHT = 2,
}
export interface HoverGestureConfig {
hoverEffect?: HoverEffect;
}
export const hoverGestureHandlerProps = ['hoverEffect'] as const;
function changeEventCalculator(
current: GestureUpdateEvent<HoverGestureHandlerEventPayload>,
previous?: GestureUpdateEvent<HoverGestureHandlerEventPayload>
) {
'worklet';
let changePayload: HoverGestureChangeEventPayload;
if (previous === undefined) {
changePayload = {
changeX: current.x,
changeY: current.y,
};
} else {
changePayload = {
changeX: current.x - previous.x,
changeY: current.y - previous.y,
};
}
return { ...current, ...changePayload };
}
export class HoverGesture extends ContinousBaseGesture<
HoverGestureHandlerEventPayload,
HoverGestureChangeEventPayload
> {
public config: BaseGestureConfig & HoverGestureConfig = {};
constructor() {
super();
this.handlerName = 'HoverGestureHandler';
}
/**
* #### iOS only
* Sets the visual hover effect.
*/
effect(effect: HoverEffect) {
this.config.hoverEffect = effect;
return this;
}
onChange(
callback: (
event: GestureUpdateEvent<
HoverGestureHandlerEventPayload & HoverGestureChangeEventPayload
>
) => void
) {
// @ts-ignore TS being overprotective, HoverGestureHandlerEventPayload is Record
this.handlers.changeEventCalculator = changeEventCalculator;
return super.onChange(callback);
}
}
export type HoverGestureType = InstanceType<typeof HoverGesture>;
@@ -0,0 +1,45 @@
import { BaseGesture, BaseGestureConfig } from './gesture';
import { LongPressGestureConfig } from '../LongPressGestureHandler';
import type { LongPressGestureHandlerEventPayload } from '../GestureHandlerEventPayload';
export class LongPressGesture extends BaseGesture<LongPressGestureHandlerEventPayload> {
public config: BaseGestureConfig & LongPressGestureConfig = {};
constructor() {
super();
this.handlerName = 'LongPressGestureHandler';
this.shouldCancelWhenOutside(true);
}
/**
* Minimum time, expressed in milliseconds, that a finger must remain pressed on the corresponding view.
* The default value is 500.
* @param duration
*/
minDuration(duration: number) {
this.config.minDurationMs = duration;
return this;
}
/**
* Maximum distance, expressed in points, that defines how far the finger is allowed to travel during a long press gesture.
* @param distance
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/long-press-gesture#maxdistancevalue-number
*/
maxDistance(distance: number) {
this.config.maxDist = distance;
return this;
}
/**
* Determine exact number of points required to handle the long press gesture.
* @param pointers
*/
numberOfPointers(pointers: number) {
this.config.numberOfPointers = pointers;
return this;
}
}
export type LongPressGestureType = InstanceType<typeof LongPressGesture>;
@@ -0,0 +1,31 @@
import { GestureUpdateEvent } from '../gestureHandlerCommon';
import { ContinousBaseGesture } from './gesture';
function changeEventCalculator(
current: GestureUpdateEvent<Record<string, never>>,
_previous?: GestureUpdateEvent<Record<string, never>>
) {
'worklet';
return current;
}
export class ManualGesture extends ContinousBaseGesture<
Record<string, never>,
Record<string, never>
> {
constructor() {
super();
this.handlerName = 'ManualGestureHandler';
}
onChange(
callback: (event: GestureUpdateEvent<Record<string, never>>) => void
) {
// @ts-ignore TS being overprotective, Record<string, never> is Record
this.handlers.changeEventCalculator = changeEventCalculator;
return super.onChange(callback);
}
}
export type ManualGestureType = InstanceType<typeof ManualGesture>;
@@ -0,0 +1,33 @@
import { BaseGestureConfig, BaseGesture } from './gesture';
import { NativeViewGestureConfig } from '../NativeViewGestureHandler';
import type { NativeViewGestureHandlerPayload } from '../GestureHandlerEventPayload';
export class NativeGesture extends BaseGesture<NativeViewGestureHandlerPayload> {
public config: BaseGestureConfig & NativeViewGestureConfig = {};
constructor() {
super();
this.handlerName = 'NativeViewGestureHandler';
}
/**
* When true, underlying handler will activate unconditionally when in `BEGAN` or `UNDETERMINED` state.
* @param value
*/
shouldActivateOnStart(value: boolean) {
this.config.shouldActivateOnStart = value;
return this;
}
/**
* When true, cancels all other gesture handlers when this `NativeViewGestureHandler` receives an `ACTIVE` state event.
* @param value
*/
disallowInterruption(value: boolean) {
this.config.disallowInterruption = value;
return this;
}
}
export type NativeGestureType = InstanceType<typeof NativeGesture>;
@@ -0,0 +1,221 @@
import { BaseGestureConfig, ContinousBaseGesture } from './gesture';
import { GestureUpdateEvent } from '../gestureHandlerCommon';
import { PanGestureConfig } from '../PanGestureHandler';
import type { PanGestureHandlerEventPayload } from '../GestureHandlerEventPayload';
export type PanGestureChangeEventPayload = {
changeX: number;
changeY: number;
};
function changeEventCalculator(
current: GestureUpdateEvent<PanGestureHandlerEventPayload>,
previous?: GestureUpdateEvent<PanGestureHandlerEventPayload>
) {
'worklet';
let changePayload: PanGestureChangeEventPayload;
if (previous === undefined) {
changePayload = {
changeX: current.translationX,
changeY: current.translationY,
};
} else {
changePayload = {
changeX: current.translationX - previous.translationX,
changeY: current.translationY - previous.translationY,
};
}
return { ...current, ...changePayload };
}
export class PanGesture extends ContinousBaseGesture<
PanGestureHandlerEventPayload,
PanGestureChangeEventPayload
> {
public config: BaseGestureConfig & PanGestureConfig = {};
constructor() {
super();
this.handlerName = 'PanGestureHandler';
}
/**
* Range along Y axis (in points) where fingers travels without activation of gesture.
* @param offset
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/pan-gesture#activeoffsetyvalue-number--number
*/
activeOffsetY(
offset: number | [activeOffsetYStart: number, activeOffsetYEnd: number]
) {
if (Array.isArray(offset)) {
this.config.activeOffsetYStart = offset[0];
this.config.activeOffsetYEnd = offset[1];
} else if (offset < 0) {
this.config.activeOffsetYStart = offset;
} else {
this.config.activeOffsetYEnd = offset;
}
return this;
}
/**
* Range along X axis (in points) where fingers travels without activation of gesture.
* @param offset
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/pan-gesture#activeoffsetxvalue-number--number
*/
activeOffsetX(
offset: number | [activeOffsetXStart: number, activeOffsetXEnd: number]
) {
if (Array.isArray(offset)) {
this.config.activeOffsetXStart = offset[0];
this.config.activeOffsetXEnd = offset[1];
} else if (offset < 0) {
this.config.activeOffsetXStart = offset;
} else {
this.config.activeOffsetXEnd = offset;
}
return this;
}
/**
* When the finger moves outside this range (in points) along Y axis and gesture hasn't yet activated it will fail recognizing the gesture.
* @param offset
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/pan-gesture#failoffsetyvalue-number--number
*/
failOffsetY(
offset: number | [failOffsetYStart: number, failOffsetYEnd: number]
) {
if (Array.isArray(offset)) {
this.config.failOffsetYStart = offset[0];
this.config.failOffsetYEnd = offset[1];
} else if (offset < 0) {
this.config.failOffsetYStart = offset;
} else {
this.config.failOffsetYEnd = offset;
}
return this;
}
/**
* When the finger moves outside this range (in points) along X axis and gesture hasn't yet activated it will fail recognizing the gesture.
* @param offset
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/pan-gesture#failoffsetxvalue-number--number
*/
failOffsetX(
offset: number | [failOffsetXStart: number, failOffsetXEnd: number]
) {
if (Array.isArray(offset)) {
this.config.failOffsetXStart = offset[0];
this.config.failOffsetXEnd = offset[1];
} else if (offset < 0) {
this.config.failOffsetXStart = offset;
} else {
this.config.failOffsetXEnd = offset;
}
return this;
}
/**
* A number of fingers that is required to be placed before gesture can activate. Should be a higher or equal to 0 integer.
* @param minPointers
*/
minPointers(minPointers: number) {
this.config.minPointers = minPointers;
return this;
}
/**
* When the given number of fingers is placed on the screen and gesture hasn't yet activated it will fail recognizing the gesture.
* Should be a higher or equal to 0 integer.
* @param maxPointers
*/
maxPointers(maxPointers: number) {
this.config.maxPointers = maxPointers;
return this;
}
/**
* Minimum distance the finger (or multiple finger) need to travel before the gesture activates.
* Expressed in points.
* @param distance
*/
minDistance(distance: number) {
this.config.minDist = distance;
return this;
}
/**
* Minimum velocity the finger has to reach in order to activate handler.
* @param velocity
*/
minVelocity(velocity: number) {
this.config.minVelocity = velocity;
return this;
}
/**
* Minimum velocity along X axis the finger has to reach in order to activate handler.
* @param velocity
*/
minVelocityX(velocity: number) {
this.config.minVelocityX = velocity;
return this;
}
/**
* Minimum velocity along Y axis the finger has to reach in order to activate handler.
* @param velocity
*/
minVelocityY(velocity: number) {
this.config.minVelocityY = velocity;
return this;
}
/**
* #### Android only
* Android, by default, will calculate translation values based on the position of the leading pointer (the first one that was placed on the screen).
* This modifier allows that behavior to be changed to the one that is default on iOS - the averaged position of all active pointers will be used to calculate the translation values.
* @param value
*/
averageTouches(value: boolean) {
this.config.avgTouches = value;
return this;
}
/**
* #### iOS only
* Enables two-finger gestures on supported devices, for example iPads with trackpads.
* @param value
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/pan-gesture/#enabletrackpadtwofingergesturevalue-boolean-ios-only
*/
enableTrackpadTwoFingerGesture(value: boolean) {
this.config.enableTrackpadTwoFingerGesture = value;
return this;
}
/**
* Duration in milliseconds of the LongPress gesture before Pan is allowed to activate.
* @param duration
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/pan-gesture/#activateafterlongpressduration-number
*/
activateAfterLongPress(duration: number) {
this.config.activateAfterLongPress = duration;
return this;
}
onChange(
callback: (
event: GestureUpdateEvent<
PanGestureHandlerEventPayload & PanGestureChangeEventPayload
>
) => void
) {
// @ts-ignore TS being overprotective, PanGestureHandlerEventPayload is Record
this.handlers.changeEventCalculator = changeEventCalculator;
return super.onChange(callback);
}
}
export type PanGestureType = InstanceType<typeof PanGesture>;
@@ -0,0 +1,51 @@
import { ContinousBaseGesture } from './gesture';
import type { PinchGestureHandlerEventPayload } from '../GestureHandlerEventPayload';
import { GestureUpdateEvent } from '../gestureHandlerCommon';
export type PinchGestureChangeEventPayload = {
scaleChange: number;
};
function changeEventCalculator(
current: GestureUpdateEvent<PinchGestureHandlerEventPayload>,
previous?: GestureUpdateEvent<PinchGestureHandlerEventPayload>
) {
'worklet';
let changePayload: PinchGestureChangeEventPayload;
if (previous === undefined) {
changePayload = {
scaleChange: current.scale,
};
} else {
changePayload = {
scaleChange: current.scale / previous.scale,
};
}
return { ...current, ...changePayload };
}
export class PinchGesture extends ContinousBaseGesture<
PinchGestureHandlerEventPayload,
PinchGestureChangeEventPayload
> {
constructor() {
super();
this.handlerName = 'PinchGestureHandler';
}
onChange(
callback: (
event: GestureUpdateEvent<
PinchGestureHandlerEventPayload & PinchGestureChangeEventPayload
>
) => void
) {
// @ts-ignore TS being overprotective, PinchGestureHandlerEventPayload is Record
this.handlers.changeEventCalculator = changeEventCalculator;
return super.onChange(callback);
}
}
export type PinchGestureType = InstanceType<typeof PinchGesture>;
@@ -0,0 +1,57 @@
import { ComponentClass } from 'react';
import {
GestureUpdateEvent,
GestureStateChangeEvent,
} from '../gestureHandlerCommon';
import { tagMessage } from '../../utils';
export interface SharedValue<T> {
value: T;
}
let Reanimated:
| {
default: {
// Slightly modified definition copied from 'react-native-reanimated'
createAnimatedComponent<P extends object>(
component: ComponentClass<P>,
options?: unknown
): ComponentClass<P>;
};
useEvent: (
callback: (event: GestureUpdateEvent | GestureStateChangeEvent) => void,
events: string[],
rebuild: boolean
) => unknown;
useSharedValue: <T>(value: T) => SharedValue<T>;
setGestureState: (handlerTag: number, newState: number) => void;
}
| undefined;
try {
Reanimated = require('react-native-reanimated');
} catch (e) {
// When 'react-native-reanimated' is not available we want to quietly continue
// @ts-ignore TS demands the variable to be initialized
Reanimated = undefined;
}
if (!Reanimated?.useSharedValue) {
// @ts-ignore Make sure the loaded module is actually Reanimated, if it's not
// reset the module to undefined so we can fallback to the default implementation
Reanimated = undefined;
}
if (Reanimated !== undefined && !Reanimated.setGestureState) {
// The loaded module is Reanimated but it doesn't have the setGestureState defined
Reanimated.setGestureState = () => {
'worklet';
console.warn(
tagMessage(
'Please use newer version of react-native-reanimated in order to control state of the gestures.'
)
);
};
}
export { Reanimated };
@@ -0,0 +1,51 @@
import { ContinousBaseGesture } from './gesture';
import type { RotationGestureHandlerEventPayload } from '../GestureHandlerEventPayload';
import { GestureUpdateEvent } from '../gestureHandlerCommon';
type RotationGestureChangeEventPayload = {
rotationChange: number;
};
function changeEventCalculator(
current: GestureUpdateEvent<RotationGestureHandlerEventPayload>,
previous?: GestureUpdateEvent<RotationGestureHandlerEventPayload>
) {
'worklet';
let changePayload: RotationGestureChangeEventPayload;
if (previous === undefined) {
changePayload = {
rotationChange: current.rotation,
};
} else {
changePayload = {
rotationChange: current.rotation - previous.rotation,
};
}
return { ...current, ...changePayload };
}
export class RotationGesture extends ContinousBaseGesture<
RotationGestureHandlerEventPayload,
RotationGestureChangeEventPayload
> {
constructor() {
super();
this.handlerName = 'RotationGestureHandler';
}
onChange(
callback: (
event: GestureUpdateEvent<
RotationGestureHandlerEventPayload & RotationGestureChangeEventPayload
>
) => void
) {
// @ts-ignore TS being overprotective, RotationGestureHandlerEventPayload is Record
this.handlers.changeEventCalculator = changeEventCalculator;
return super.onChange(callback);
}
}
export type RotationGestureType = InstanceType<typeof RotationGesture>;
@@ -0,0 +1,86 @@
import { BaseGestureConfig, BaseGesture } from './gesture';
import { TapGestureConfig } from '../TapGestureHandler';
import type { TapGestureHandlerEventPayload } from '../GestureHandlerEventPayload';
export class TapGesture extends BaseGesture<TapGestureHandlerEventPayload> {
public config: BaseGestureConfig & TapGestureConfig = {};
constructor() {
super();
this.handlerName = 'TapGestureHandler';
this.shouldCancelWhenOutside(true);
}
/**
* Minimum number of pointers (fingers) required to be placed before the gesture activates.
* Should be a positive integer. The default value is 1.
* @param minPointers
*/
minPointers(minPointers: number) {
this.config.minPointers = minPointers;
return this;
}
/**
* Number of tap gestures required to activate the gesture.
* The default value is 1.
* @param count
*/
numberOfTaps(count: number) {
this.config.numberOfTaps = count;
return this;
}
/**
* Maximum distance, expressed in points, that defines how far the finger is allowed to travel during a tap gesture.
* @param maxDist
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/tap-gesture#maxdistancevalue-number
*/
maxDistance(maxDist: number) {
this.config.maxDist = maxDist;
return this;
}
/**
* Maximum time, expressed in milliseconds, that defines how fast a finger must be released after a touch.
* The default value is 500.
* @param duration
*/
maxDuration(duration: number) {
this.config.maxDurationMs = duration;
return this;
}
/**
* Maximum time, expressed in milliseconds, that can pass before the next tap — if many taps are required.
* The default value is 500.
* @param delay
*/
maxDelay(delay: number) {
this.config.maxDelayMs = delay;
return this;
}
/**
* Maximum distance, expressed in points, that defines how far the finger is allowed to travel along the X axis during a tap gesture.
* @param delta
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/tap-gesture#maxdeltaxvalue-number
*/
maxDeltaX(delta: number) {
this.config.maxDeltaX = delta;
return this;
}
/**
* Maximum distance, expressed in points, that defines how far the finger is allowed to travel along the Y axis during a tap gesture.
* @param delta
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/tap-gesture#maxdeltayvalue-number
*/
maxDeltaY(delta: number) {
this.config.maxDeltaY = delta;
return this;
}
}
export type TapGestureType = InstanceType<typeof TapGesture>;
@@ -0,0 +1,5 @@
let handlerTag = 1;
export function getNextHandlerTag(): number {
return handlerTag++;
}
@@ -0,0 +1,58 @@
import { isTestEnv } from '../utils';
import { GestureType } from './gestures/gesture';
import { GestureEvent, HandlerStateChangeEvent } from './gestureHandlerCommon';
export const handlerIDToTag: Record<string, number> = {};
const gestures = new Map<number, GestureType>();
const oldHandlers = new Map<number, GestureHandlerCallbacks>();
const testIDs = new Map<string, number>();
export function registerHandler(
handlerTag: number,
handler: GestureType,
testID?: string
) {
gestures.set(handlerTag, handler);
if (isTestEnv() && testID) {
testIDs.set(testID, handlerTag);
}
}
export function registerOldGestureHandler(
handlerTag: number,
handler: GestureHandlerCallbacks
) {
oldHandlers.set(handlerTag, handler);
}
export function unregisterOldGestureHandler(handlerTag: number) {
oldHandlers.delete(handlerTag);
}
export function unregisterHandler(handlerTag: number, testID?: string) {
gestures.delete(handlerTag);
if (isTestEnv() && testID) {
testIDs.delete(testID);
}
}
export function findHandler(handlerTag: number) {
return gestures.get(handlerTag);
}
export function findOldGestureHandler(handlerTag: number) {
return oldHandlers.get(handlerTag);
}
export function findHandlerByTestID(testID: string) {
const handlerTag = testIDs.get(testID);
if (handlerTag !== undefined) {
return findHandler(handlerTag) ?? null;
}
return null;
}
export interface GestureHandlerCallbacks {
onGestureEvent: (event: GestureEvent<any>) => void;
onGestureStateChange: (event: HandlerStateChangeEvent<any>) => void;
}
@@ -0,0 +1,76 @@
import * as React from 'react';
import { Platform, findNodeHandle as findNodeHandleRN } from 'react-native';
import { handlerIDToTag } from './handlersRegistry';
import { toArray } from '../utils';
import RNGestureHandlerModule from '../RNGestureHandlerModule';
import { ghQueueMicrotask } from '../ghQueueMicrotask';
function isConfigParam(param: unknown, name: string) {
// param !== Object(param) returns false if `param` is a function
// or an object and returns true if `param` is null
return (
param !== undefined &&
(param !== Object(param) ||
!('__isNative' in (param as Record<string, unknown>))) &&
name !== 'onHandlerStateChange' &&
name !== 'onGestureEvent'
);
}
export function filterConfig(
props: Record<string, unknown>,
validProps: string[],
defaults: Record<string, unknown> = {}
) {
const filteredConfig = { ...defaults };
for (const key of validProps) {
let value = props[key];
if (isConfigParam(value, key)) {
if (key === 'simultaneousHandlers' || key === 'waitFor') {
value = transformIntoHandlerTags(props[key]);
} else if (key === 'hitSlop' && typeof value !== 'object') {
value = { top: value, left: value, bottom: value, right: value };
}
filteredConfig[key] = value;
}
}
return filteredConfig;
}
export function transformIntoHandlerTags(handlerIDs: any) {
handlerIDs = toArray(handlerIDs);
if (Platform.OS === 'web') {
return handlerIDs
.map(({ current }: { current: any }) => current)
.filter((handle: any) => handle);
}
// converts handler string IDs into their numeric tags
return handlerIDs
.map(
(handlerID: any) =>
handlerIDToTag[handlerID] || handlerID.current?.handlerTag || -1
)
.filter((handlerTag: number) => handlerTag > 0);
}
export function findNodeHandle(
node: null | number | React.Component<any, any> | React.ComponentClass<any>
): null | number | React.Component<any, any> | React.ComponentClass<any> {
if (Platform.OS === 'web') {
return node;
}
return findNodeHandleRN(node) ?? null;
}
let flushOperationsScheduled = false;
export function scheduleFlushOperations() {
if (!flushOperationsScheduled) {
flushOperationsScheduled = true;
ghQueueMicrotask(() => {
RNGestureHandlerModule.flushOperations();
flushOperationsScheduled = false;
});
}
}