Skip to content

[Web] v3 typing and using handler data #3651

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 70 commits into
base: next
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
ea8bd73
basic component
akwasniewski Jul 25, 2025
32fea07
Merge branch 'next' into @akwasniewski/native-detector-web
akwasniewski Jul 25, 2025
968112c
better useffect triggers:
akwasniewski Jul 25, 2025
6a69cd7
added todos
akwasniewski Jul 25, 2025
2b404af
moved Gesture handler detectors
akwasniewski Jul 25, 2025
9c30c1a
path fix
akwasniewski Jul 25, 2025
d7dd057
extracted host gesture detector
akwasniewski Jul 25, 2025
56b93d3
not extending view props
akwasniewski Jul 25, 2025
ad8ef64
native detector action type
akwasniewski Jul 25, 2025
0304177
moved implementation of web handler
akwasniewski Jul 28, 2025
4aaf700
memoisation
akwasniewski Jul 28, 2025
eeddd24
detaching handlers
akwasniewski Jul 28, 2025
3242ffd
forgot to asign to oldHandlerTags
akwasniewski Jul 28, 2025
7147ab3
removed unnecessary types
akwasniewski Jul 28, 2025
cc78f74
native detector animated event action type
akwasniewski Jul 28, 2025
067becb
removed console log
akwasniewski Jul 28, 2025
d782c95
handling animated events in sendEvents
akwasniewski Jul 29, 2025
86046bd
first refactor
akwasniewski Jul 29, 2025
6c34d33
using native detector animated event
akwasniewski Jul 29, 2025
cd793b3
refactor 2
akwasniewski Jul 29, 2025
580494d
use callback in native detector
akwasniewski Jul 30, 2025
eb98bf4
tagMessage
akwasniewski Jul 30, 2025
af663c8
should prevent drop
akwasniewski Jul 30, 2025
42e84b4
old api compatibility
akwasniewski Jul 30, 2025
079fdb4
not throwing strings
akwasniewski Jul 30, 2025
cb72d7e
removed unnessary check
akwasniewski Jul 30, 2025
de98017
even more refactoring
akwasniewski Jul 30, 2025
e193ff4
throwing when propsref is null
akwasniewski Jul 30, 2025
2ba053a
helper action type functions
akwasniewski Jul 30, 2025
8ab666b
separate paths on touch events
akwasniewski Jul 30, 2025
c445262
removed unsafe memoisation
akwasniewski Jul 30, 2025
46a9570
props extending
akwasniewski Jul 30, 2025
37c4070
failing on detach
akwasniewski Jul 31, 2025
20cbc9e
one more refactor
akwasniewski Aug 1, 2025
58f8d60
display contents
akwasniewski Aug 1, 2025
532f35a
Merge branch 'next' into @akwasniewski/native-detector-web
akwasniewski Aug 1, 2025
7d205dc
removed action type native detector animated event
akwasniewski Aug 1, 2025
fb7af53
handler data
akwasniewski Aug 1, 2025
cc3b639
fixed cyclic dependency
akwasniewski Aug 4, 2025
0c6f200
Update packages/react-native-gesture-handler/src/web/tools/GestureHan…
akwasniewski Aug 4, 2025
00738d6
minor fixes
akwasniewski Aug 4, 2025
9c17a96
type predicate
akwasniewski Aug 4, 2025
2353a84
readded memoisation
akwasniewski Aug 4, 2025
2443971
changed dependencies
akwasniewski Aug 5, 2025
f3198f5
moved forAnimated to config
akwasniewski Aug 5, 2025
686fadd
removed forAnimated from attachHandlers
akwasniewski Aug 5, 2025
b0d384d
type predicate using protected props ref
akwasniewski Aug 5, 2025
2ba5ae8
native gesture bugfix
akwasniewski Aug 5, 2025
38b240a
Merge branch 'next' into @akwasniewski/native-detector-web
akwasniewski Aug 5, 2025
7c00105
removed forAnimated
akwasniewski Aug 5, 2025
dceba3b
Merge branch 'next' into @akwasniewski/native-detector-web
akwasniewski Aug 5, 2025
14190fc
Merge branch '@akwasniewski/native-detector-web' into @akwasniewski/v…
akwasniewski Aug 5, 2025
b738254
removed usecallback
akwasniewski Aug 6, 2025
1f62d5c
private isInitialized
akwasniewski Aug 6, 2025
4d916f5
removed type predicate
akwasniewski Aug 6, 2025
0b16cdb
removed unnecessary type indications
akwasniewski Aug 6, 2025
8579092
typing wherever possible
akwasniewski Aug 6, 2025
089466e
Merge branch '@akwasniewski/native-detector-web' into @akwasniewski/v…
akwasniewski Aug 6, 2025
04ec35f
v2 compatibility
akwasniewski Aug 8, 2025
5837159
old state
akwasniewski Aug 8, 2025
0c96928
safe old state
akwasniewski Aug 8, 2025
4d6d1f2
merge with parent branch
akwasniewski Aug 8, 2025
6391704
better oldState handling
akwasniewski Aug 8, 2025
9e6fa97
specific transform event functions
akwasniewski Aug 11, 2025
323acf6
Merge branch 'next' into @akwasniewski/v3-web-typing
akwasniewski Aug 11, 2025
3336d48
Merge branch 'next' into @akwasniewski/v3-web-typing
akwasniewski Aug 11, 2025
4a75f5f
Merge branch 'next' into @akwasniewski/v3-web-typing
akwasniewski Aug 12, 2025
a5148ac
fixing v2?
akwasniewski Aug 14, 2025
1cd5eef
remove merge remnants
akwasniewski Aug 14, 2025
2e47b1a
removed obsolete comment
akwasniewski Aug 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { StylusData } from '../web/interfaces';
import { StylusData } from './gestureHandlerCommon';

export type FlingGestureHandlerEventPayload = {
x: number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,14 @@ export type GestureTouchEvent = {
pointerType: PointerType;
};

export interface StylusData {
tiltX: number;
tiltY: number;
azimuthAngle: number;
altitudeAngle: number;
pressure: number;
Comment on lines +168 to +173
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure whether this is the right place for it, but I had to put it somewhere else than web/interfaces , and this looked like the most promising spot.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without moving StylusData there is a circular dependency. Moving this interface seemed the most sensible option to resolve it. gestureHandlerCommon contains common interfaces for all platforms.

}

export type GestureUpdateEvent<GestureEventPayloadT = Record<string, unknown>> =
GestureEventPayload & GestureEventPayloadT;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,16 @@ import {
import { tapGestureHandlerProps } from '../../TapGestureHandler';
import { hoverGestureHandlerProps } from '../hoverGesture';
import { nativeViewGestureHandlerProps } from '../../NativeViewGestureHandler';
import {
HandlerStateChangeEvent,
baseGestureHandlerWithDetectorProps,
} from '../../gestureHandlerCommon';
import { baseGestureHandlerWithDetectorProps } from '../../gestureHandlerCommon';
import { RNRenderer } from '../../../RNRenderer';
import { useCallback, useRef, useState } from 'react';
import { Reanimated } from '../reanimatedWrapper';
import { onGestureHandlerEvent } from '../eventReceiver';
import { PropsRef } from '../../../web/interfaces';
import {
GestureHandlerNativeEvent,
PropsRef,
ResultEvent,
} from '../../../web/interfaces';

export const ALLOWED_PROPS = [
...baseGestureHandlerWithDetectorProps,
Expand Down Expand Up @@ -168,11 +169,11 @@ export function useForceRender() {

export function useWebEventHandlers() {
return useRef<PropsRef>({
onGestureHandlerEvent: (e: HandlerStateChangeEvent<unknown>) => {
onGestureHandlerEvent(e.nativeEvent);
onGestureHandlerEvent: (e: ResultEvent) => {
onGestureHandlerEvent(e.nativeEvent as GestureHandlerNativeEvent);
},
onGestureHandlerStateChange: (e: HandlerStateChangeEvent<unknown>) => {
onGestureHandlerEvent(e.nativeEvent);
onGestureHandlerStateChange: (e: ResultEvent) => {
onGestureHandlerEvent(e.nativeEvent as GestureHandlerNativeEvent);
},
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import {
PropsRef,
ResultEvent,
PointerData,
ResultTouchEvent,
TouchEventType,
EventTypes,
GestureHandlerNativeEvent,
} from '../interfaces';
import EventManager from '../tools/EventManager';
import GestureHandlerOrchestrator from '../tools/GestureHandlerOrchestrator';
Expand All @@ -20,6 +20,7 @@ import { PointerType } from '../../PointerType';
import { GestureHandlerDelegate } from '../tools/GestureHandlerDelegate';
import { ActionType } from '../../ActionType';
import { tagMessage } from '../../utils';
import { StateChangeEvent, TouchEvent, UpdateEvent } from '../../v3/types';

export default abstract class GestureHandler implements IGestureHandler {
private lastSentState: State | null = null;
Expand Down Expand Up @@ -369,7 +370,7 @@ export default abstract class GestureHandler implements IGestureHandler {
const { onGestureHandlerEvent, onGestureHandlerTouchEvent }: PropsRef =
this.propsRef!.current;

const touchEvent: ResultTouchEvent | undefined =
const touchEvent: ResultEvent<TouchEvent> | undefined =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure that TouchEvent will have correct fields and won't expose something that we do not provide?

Copy link
Contributor Author

@akwasniewski akwasniewski Aug 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, gestureTouchEvent defined here, on which TouchEvent is a union has the same fields as the old nativeTouchEvent defined within web/interfaces.

Copy link
Contributor Author

@akwasniewski akwasniewski Aug 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now when I think about it we could probably do the same for GestureHandlerNativeEvent, is there a reason why v2 redeclared those types for the web, when we already have similar types in commons?
EDIT: ok, nevermind they have conflicting type

this.transformTouchEvent(event);

if (touchEvent) {
Expand All @@ -395,12 +396,14 @@ export default abstract class GestureHandler implements IGestureHandler {
onGestureHandlerStateChange,
onGestureHandlerAnimatedEvent,
}: PropsRef = this.propsRef!.current;
const resultEvent: ResultEvent = this.transformEventData(
newState,
oldState
);

// In the new API oldState field has to be undefined, unless we send event state changed
const resultEvent: ResultEvent =
this.actionType !== ActionType.NATIVE_DETECTOR
? this.transformEventData(newState, oldState)
: this.lastSentState !== newState
? this.transformStateChangeEvent(newState, oldState)
: this.transformUpdateEvent(newState);

// In the v2 API oldState field has to be undefined, unless we send event state changed
// Here the order is flipped to avoid workarounds such as making backup of the state and setting it to undefined first, then changing it back
// Flipping order with setting oldState to undefined solves issue, when events were being sent twice instead of once
// However, this may cause trouble in the future (but for now we don't know that)
Expand All @@ -410,18 +413,22 @@ export default abstract class GestureHandler implements IGestureHandler {
invokeNullableMethod(onGestureHandlerStateChange, resultEvent);
}
if (this.state === State.ACTIVE) {
resultEvent.nativeEvent.oldState = undefined;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I remember this was necessary in order to keep compatibility with old API. Right, @j-piasecki?

Copy link
Contributor Author

@akwasniewski akwasniewski Aug 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is the case referred in the comment above as

this may cause trouble in the future

Due to how new events are structures (no oldState field in update) we could either do a weird conditionality check as in 0c9692 or change the new types so they all include 'oldState'. Both solutions seem off, do you maybe have any better ideas?

if (this.actionType !== ActionType.NATIVE_DETECTOR) {
(resultEvent.nativeEvent as GestureHandlerNativeEvent).oldState =
undefined;
}
if (onGestureHandlerAnimatedEvent && this.forAnimated) {
invokeNullableMethod(onGestureHandlerAnimatedEvent, resultEvent);
}
invokeNullableMethod(onGestureHandlerEvent, resultEvent);
}
};

private transformEventData(newState: State, oldState: State): ResultEvent {
if (!this.viewRef) {
throw new Error(tagMessage('Cannot handle event when target is null'));
}
private transformEventData(
newState: State,
oldState: State
): ResultEvent<GestureHandlerNativeEvent> {
this.ensureViewRef(this.viewRef);
return {
nativeEvent: {
numberOfPointers: this.tracker.trackedPointersCount,
Expand All @@ -439,9 +446,55 @@ export default abstract class GestureHandler implements IGestureHandler {
};
}

private transformStateChangeEvent(
newState: State,
oldState: State
): ResultEvent<StateChangeEvent<unknown>> {
this.ensureViewRef(this.viewRef);
return {
nativeEvent: {
state: newState,
handlerTag: this.handlerTag,
pointerType: this.pointerType,
oldState: oldState,
numberOfPointers: this.tracker.trackedPointersCount,
handlerData: {
pointerInside: this.delegate.isPointerInBounds(
this.tracker.getAbsoluteCoordsAverage()
),
...this.transformNativeEvent(),
target: this.viewRef,
},
},
timeStamp: Date.now(),
};
}

private transformUpdateEvent(
newState: State
): ResultEvent<UpdateEvent<unknown>> {
this.ensureViewRef(this.viewRef);
return {
nativeEvent: {
state: newState,
handlerTag: this.handlerTag,
pointerType: this.pointerType,
numberOfPointers: this.tracker.trackedPointersCount,
handlerData: {
pointerInside: this.delegate.isPointerInBounds(
this.tracker.getAbsoluteCoordsAverage()
),
...this.transformNativeEvent(),
target: this.viewRef,
},
},
timeStamp: Date.now(),
};
}

private transformTouchEvent(
event: AdaptedEvent
): ResultTouchEvent | undefined {
): ResultEvent<TouchEvent> | undefined {
const rect = this.delegate.measureView();

const all: PointerData[] = [];
Expand Down Expand Up @@ -571,7 +624,7 @@ export default abstract class GestureHandler implements IGestureHandler {
});
});

const cancelEvent: ResultTouchEvent = {
const cancelEvent: ResultEvent<TouchEvent> = {
nativeEvent: {
handlerTag: this.handlerTag,
state: this.state,
Expand All @@ -597,6 +650,12 @@ export default abstract class GestureHandler implements IGestureHandler {
}
}

private ensureViewRef(viewRef: any): asserts viewRef is number {
if (!viewRef) {
throw new Error(tagMessage('Cannot handle event when target is null'));
}
}
Comment on lines +653 to +657
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, little deja vu - wasn't it added in other PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar function, ensurePropsRef was added here


protected transformNativeEvent(): Record<string, unknown> {
// Those properties are shared by most handlers and if not this method will be overriden
const lastCoords = this.tracker.getAbsoluteCoordsAverage();
Expand Down Expand Up @@ -888,10 +947,10 @@ export default abstract class GestureHandler implements IGestureHandler {

function invokeNullableMethod(
method:
| ((event: ResultEvent | ResultTouchEvent) => void)
| { __getHandler: () => (event: ResultEvent | ResultTouchEvent) => void }
| ((event: ResultEvent) => void)
| { __getHandler: () => (event: ResultEvent) => void }
| { __nodeConfig: { argMapping: unknown[] } },
event: ResultEvent | ResultTouchEvent
event: ResultEvent
): void {
if (!method) {
return;
Expand Down Expand Up @@ -923,7 +982,7 @@ function invokeNullableMethod(
}

// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const nativeValue = event.nativeEvent[key];
const nativeValue = (event.nativeEvent as any)[key];

// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (value?.setValue) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { State } from '../../State';
import { AdaptedEvent, StylusData } from '../interfaces';
import { AdaptedEvent } from '../interfaces';
import GestureHandlerOrchestrator from '../tools/GestureHandlerOrchestrator';
import GestureHandler from './GestureHandler';
import { StylusData } from '../../handlers/gestureHandlerCommon';

export default class HoverGestureHandler extends GestureHandler {
private stylusData: StylusData | undefined;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { State } from '../../State';
import { DEFAULT_TOUCH_SLOP } from '../constants';
import { AdaptedEvent, Config, StylusData, WheelDevice } from '../interfaces';
import { AdaptedEvent, Config, WheelDevice } from '../interfaces';
import { StylusData } from '../../handlers/gestureHandlerCommon';

import GestureHandler from './GestureHandler';

Expand Down
48 changes: 14 additions & 34 deletions packages/react-native-gesture-handler/src/web/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import {
ActiveCursor,
MouseButton,
TouchAction,
StylusData,
} from '../handlers/gestureHandlerCommon';
import { Directions } from '../Directions';
import { State } from '../State';
import { PointerType } from '../PointerType';
import { GestureHandlerEvent } from '../v3/types';
import { State } from '../State';

export interface HitSlop {
left?: number;
Expand Down Expand Up @@ -83,7 +85,8 @@ export interface Config extends Record<string, ConfigArgs> {
}

type NativeEventArgs = number | State | boolean | undefined;
interface NativeEvent extends Record<string, NativeEventArgs> {
export interface GestureHandlerNativeEvent
extends Record<string, NativeEventArgs> {
numberOfPointers: number;
state: State;
pointerInside: boolean | undefined;
Expand All @@ -106,42 +109,19 @@ export interface PointerData {
absoluteY: number;
}

type TouchNativeArgs = number | State | TouchEventType | PointerData[];

interface NativeTouchEvent extends Record<string, TouchNativeArgs> {
handlerTag: number;
state: State;
eventType: TouchEventType;
changedTouches: PointerData[];
allTouches: PointerData[];
numberOfTouches: number;
pointerType: PointerType;
}

export interface ResultEvent extends Record<string, NativeEvent | number> {
nativeEvent: NativeEvent;
timeStamp: number;
}

export interface ResultTouchEvent
extends Record<string, NativeTouchEvent | number> {
nativeEvent: NativeTouchEvent;
// Native event has to stay for v2 compatibility
type ResultEventType = GestureHandlerEvent<unknown> | GestureHandlerNativeEvent;
export interface ResultEvent<T extends ResultEventType = ResultEventType>
extends Record<string, T | number> {
nativeEvent: T;
Comment on lines +113 to +116
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct me if I'm wrong, but GestureHandlerEvent already has nativeEvent field (look here for context). In that case, we will have event type where it is possible to have nativeEvent.nativeEvent

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also I find it quite confusing with this default value, wouldn't it be easier to read if we had just ResultEvent<T> and then pass either GestureHandlerEvent, GestureHandlerNativeEvent or TouchEvent?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct me if I'm wrong, but GestureHandlerEvent already has nativeEvent

No, types in GestureHandlerEvent are wrappers on payload types defined here, and only add handlerData. Those types do not have nativeEvent field

Also I find it quite confusing with this default value

Ok, it was inspired by this. I thought that it would simplify the code as we would not have to write <unknown> everywhere. I don't have a strong preference, if you think it's better to add <unknown> i can change it.

timeStamp: number;
}

export interface PropsRef {
onGestureHandlerEvent: (e: any) => void;
onGestureHandlerAnimatedEvent?: (e: any) => void;
onGestureHandlerStateChange: (e: any) => void;
onGestureHandlerTouchEvent?: (e: any) => void;
}

export interface StylusData {
tiltX: number;
tiltY: number;
azimuthAngle: number;
altitudeAngle: number;
pressure: number;
onGestureHandlerEvent: (e: ResultEvent) => void;
onGestureHandlerAnimatedEvent?: (e: ResultEvent) => void;
onGestureHandlerStateChange: (e: ResultEvent) => void;
onGestureHandlerTouchEvent?: (e: ResultEvent) => void;
}

export interface AdaptedEvent {
Expand Down
8 changes: 2 additions & 6 deletions packages/react-native-gesture-handler/src/web/utils.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { PointerType } from '../PointerType';
import type {
GestureHandlerRef,
Point,
StylusData,
SVGRef,
} from './interfaces';
import type { GestureHandlerRef, Point, SVGRef } from './interfaces';
import { StylusData } from '../handlers/gestureHandlerCommon';

export function isPointerInBounds(view: HTMLElement, { x, y }: Point): boolean {
const rect: DOMRect = view.getBoundingClientRect();
Expand Down
Loading