diff --git a/packages/react-native-reanimated/src/WorkletEventHandler.ts b/packages/react-native-reanimated/src/WorkletEventHandler.ts index 439efaad7..8b10e1810 100644 --- a/packages/react-native-reanimated/src/WorkletEventHandler.ts +++ b/packages/react-native-reanimated/src/WorkletEventHandler.ts @@ -1,26 +1,30 @@ 'use strict'; -import type { NativeSyntheticEvent } from 'react-native'; import { registerEventHandler, unregisterEventHandler } from './core'; import type { - EventPayload, ReanimatedEvent, IWorkletEventHandler, + JSEvent, + JSHandler, } from './hook/commonTypes'; import { shouldBeUseWeb } from './PlatformChecker'; const SHOULD_BE_USE_WEB = shouldBeUseWeb(); -type JSEvent = NativeSyntheticEvent>; - // In JS implementation (e.g. for web) we don't use Reanimated's // event emitter, therefore we have to handle here // the event that came from React Native and convert it. function jsListener( eventName: string, - handler: (event: ReanimatedEvent) => void + handler?: (event: ReanimatedEvent) => void, + handlerJS?: JSHandler ) { return (evt: JSEvent) => { - handler({ ...evt.nativeEvent, eventName } as ReanimatedEvent); + if (handler) { + handler({ ...evt.nativeEvent, eventName } as ReanimatedEvent); + } + if (handlerJS) { + handlerJS(evt); + } }; } @@ -29,25 +33,30 @@ class WorkletEventHandlerNative { eventNames: string[]; worklet: (event: ReanimatedEvent) => void; + JSHandlers: Record>; // for native platforms we just need to keep them so PropFilter sets them to props #viewTags: Set; #registrations: Map; // keys are viewTags, values are arrays of registration ID's for each viewTag constructor( worklet: (event: ReanimatedEvent) => void, - eventNames: string[] + eventNames: string[], + JSHandlers: Record> ) { this.worklet = worklet; this.eventNames = eventNames; + this.JSHandlers = JSHandlers; this.#viewTags = new Set(); this.#registrations = new Map(); } updateEventHandler( newWorklet: (event: ReanimatedEvent) => void, - newEvents: string[] + newEvents: string[], + newJSHandlers: Record> ): void { // Update worklet and event names this.worklet = newWorklet; this.eventNames = newEvents; + this.JSHandlers = newJSHandlers; // Detach all events this.#registrations.forEach((registrationIDs) => { @@ -99,32 +108,74 @@ class WorkletEventHandlerWeb | Record>) => void> | Record) => void>; + JSHandlers: Record>; // web JS Handlers need to be merged with Worklet handlers worklet: (event: ReanimatedEvent) => void; constructor( worklet: (event: ReanimatedEvent) => void, - eventNames: string[] = [] + eventNames: string[] = [], + JSHandlers: Record> ) { this.worklet = worklet; this.eventNames = eventNames; + this.JSHandlers = JSHandlers; this.listeners = {}; this.setupWebListeners(); } setupWebListeners() { this.listeners = {}; - this.eventNames.forEach((eventName) => { + + const eventsFromJSHandlers = Object.keys(this.JSHandlers); + + // events that are both from JS and Worklet handlers + const sharedEvents = eventsFromJSHandlers.filter((value) => + this.eventNames.includes(value) + ); + + // events from only Worklet handlers + const restWorkletEvents = this.eventNames.filter( + (value) => !eventsFromJSHandlers.includes(value) + ); + + // events from only JS handlers + const restJSEvents = eventsFromJSHandlers.filter( + (value) => !this.eventNames.includes(value) + ); + + // shared events get JS and Worklet handlers merged into a listener to put into a prop + sharedEvents.forEach((eventName) => { + this.listeners[eventName] = jsListener( + eventName, + this.worklet, + this.JSHandlers[eventName] + ); + }); + + // only Worklet events get their own listeners + restWorkletEvents.forEach((eventName) => { this.listeners[eventName] = jsListener(eventName, this.worklet); }); + + // only JS events get their own listeners + restJSEvents.forEach((eventName) => { + this.listeners[eventName] = jsListener( + eventName, + undefined, + this.JSHandlers[eventName] + ); + }); } updateEventHandler( newWorklet: (event: ReanimatedEvent) => void, - newEvents: string[] + newEvents: string[], + newJSHandlers: Record> ): void { // Update worklet and event names this.worklet = newWorklet; this.eventNames = newEvents; + this.JSHandlers = newJSHandlers; this.setupWebListeners(); } diff --git a/packages/react-native-reanimated/src/createAnimatedComponent/PropsFilter.tsx b/packages/react-native-reanimated/src/createAnimatedComponent/PropsFilter.tsx index 9eeb6bb5c..ee4e6f5e9 100644 --- a/packages/react-native-reanimated/src/createAnimatedComponent/PropsFilter.tsx +++ b/packages/react-native-reanimated/src/createAnimatedComponent/PropsFilter.tsx @@ -67,16 +67,32 @@ export class PropsFilter implements IPropsFilter { has('workletEventHandler', value) && value.workletEventHandler instanceof WorkletEventHandler ) { - if (value.workletEventHandler.eventNames.length > 0) { - value.workletEventHandler.eventNames.forEach((eventName) => { - props[eventName] = has('listeners', value.workletEventHandler) - ? ( - value.workletEventHandler.listeners as Record - )[eventName] - : dummyListener; - }); + const handler = value.workletEventHandler; + const isWebHandler = has('listeners', handler); + const hasJSHandlers = Object.keys(handler.JSHandlers).length > 0; + + if (hasJSHandlers) { + if (isWebHandler) { + // on web, our and JS handlers are merged in listeners object + Object.keys(handler.listeners).forEach((eventName) => { + props[eventName] = handler.listeners[eventName]; + }); + } else { + // on mobile platforms, we just set the JS handlers to the props + Object.keys(handler.JSHandlers).forEach((eventName) => { + props[eventName] = handler.JSHandlers[eventName]; + }); + } } else { - props[key] = dummyListener; + if (handler.eventNames.length > 0) { + handler.eventNames.forEach((eventName) => { + props[eventName] = isWebHandler + ? (handler.listeners as Record)[eventName] + : dummyListener; + }); + } else { + props[key] = dummyListener; + } } } else if (isSharedValue(value)) { if (component._isFirstRender) { diff --git a/packages/react-native-reanimated/src/hook/commonTypes.ts b/packages/react-native-reanimated/src/hook/commonTypes.ts index a56ee922c..2df596a62 100644 --- a/packages/react-native-reanimated/src/hook/commonTypes.ts +++ b/packages/react-native-reanimated/src/hook/commonTypes.ts @@ -64,7 +64,7 @@ export type ReanimatedEvent = ReanimatedPayload & ? NativeEvent : Event); -export type EventPayload = Event extends { +type EventPayload = Event extends { nativeEvent: infer NativeEvent extends object; } ? NativeEvent @@ -80,10 +80,17 @@ export type RNNativeScrollEvent = NativeSyntheticEvent; export type ReanimatedScrollEvent = ReanimatedEvent; +export type JSEvent = NativeSyntheticEvent< + EventPayload +>; + +export type JSHandler = (event: JSEvent) => void; + export interface IWorkletEventHandler { updateEventHandler: ( newWorklet: (event: ReanimatedEvent) => void, - newEvents: string[] + newEvents: string[], + newJSHandlers: Record> ) => void; registerForEvents: (viewTag: number, fallbackEventName?: string) => void; unregisterFromEvents: (viewTag: number) => void; @@ -114,3 +121,10 @@ export type UseAnimatedStyleInternal