From eb2ace1281611933ff1d7bd6f757d9bb17b2f5e1 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Fri, 28 Jun 2019 01:22:32 +0100 Subject: [PATCH] [Flare] Bring Flare support to React Native Fabric (#15887) --- packages/react-art/src/ReactARTHostConfig.js | 6 +- .../src/client/ReactDOMHostConfig.js | 2 +- .../src/events/DOMEventResponderSystem.js | 2 +- .../src/events/ReactDOMEventListener.js | 2 +- packages/react-events/README.md | 2 +- packages/react-events/src/dom/Press.js | 6 +- packages/react-events/src/rn/Press.js | 683 ++++++++++++++ .../src/ReactFabricEventEmitter.js | 11 + .../src/ReactFabricEventResponderSystem.js | 835 ++++++++++++++++++ .../src/ReactFabricHostConfig.js | 43 +- .../src/ReactNativeHostConfig.js | 17 +- .../src/ReactNativeTypes.js | 96 +- packages/shared/ReactDOMTypes.js | 2 +- 13 files changed, 1680 insertions(+), 27 deletions(-) create mode 100644 packages/react-events/src/rn/Press.js create mode 100644 packages/react-native-renderer/src/ReactFabricEventResponderSystem.js diff --git a/packages/react-art/src/ReactARTHostConfig.js b/packages/react-art/src/ReactARTHostConfig.js index 107daecf9b..a351d261b5 100644 --- a/packages/react-art/src/ReactARTHostConfig.js +++ b/packages/react-art/src/ReactARTHostConfig.js @@ -428,19 +428,19 @@ export function unhideTextInstance(textInstance, text): void { } export function mountEventComponent( - eventComponentInstance: ReactEventComponentInstance, + eventComponentInstance: ReactEventComponentInstance, ) { throw new Error('Not yet implemented.'); } export function updateEventComponent( - eventComponentInstance: ReactEventComponentInstance, + eventComponentInstance: ReactEventComponentInstance, ) { throw new Error('Not yet implemented.'); } export function unmountEventComponent( - eventComponentInstance: ReactEventComponentInstance, + eventComponentInstance: ReactEventComponentInstance, ): void { throw new Error('Not yet implemented.'); } diff --git a/packages/react-dom/src/client/ReactDOMHostConfig.js b/packages/react-dom/src/client/ReactDOMHostConfig.js index 1d94c7d843..ef223abf52 100644 --- a/packages/react-dom/src/client/ReactDOMHostConfig.js +++ b/packages/react-dom/src/client/ReactDOMHostConfig.js @@ -32,7 +32,6 @@ import { setEnabled as ReactBrowserEventEmitterSetEnabled, } from '../events/ReactBrowserEventEmitter'; import {getChildNamespace} from '../shared/DOMNamespaces'; -import {addRootEventTypesForComponentInstance} from '../events/DOMEventResponderSystem'; import { ELEMENT_NODE, TEXT_NODE, @@ -48,6 +47,7 @@ import type { ReactDOMEventComponentInstance, } from 'shared/ReactDOMTypes'; import { + addRootEventTypesForComponentInstance, mountEventResponder, unmountEventResponder, } from '../events/DOMEventResponderSystem'; diff --git a/packages/react-dom/src/events/DOMEventResponderSystem.js b/packages/react-dom/src/events/DOMEventResponderSystem.js index 8bae6b006a..b8a7c91b58 100644 --- a/packages/react-dom/src/events/DOMEventResponderSystem.js +++ b/packages/react-dom/src/events/DOMEventResponderSystem.js @@ -231,7 +231,7 @@ const eventResponderContext: ReactDOMResponderContext = { } return false; }, - isTargetWithinElement( + isTargetWithinNode( childTarget: Element | Document, parentTarget: Element | Document, ): boolean { diff --git a/packages/react-dom/src/events/ReactDOMEventListener.js b/packages/react-dom/src/events/ReactDOMEventListener.js index 37c7f4dc74..775519e11b 100644 --- a/packages/react-dom/src/events/ReactDOMEventListener.js +++ b/packages/react-dom/src/events/ReactDOMEventListener.js @@ -332,7 +332,7 @@ export function dispatchEvent( targetInst, ); } else { - // Responder event system (experimental event API) + // React Flare event system dispatchEventForResponderEventSystem( topLevelType, targetInst, diff --git a/packages/react-events/README.md b/packages/react-events/README.md index be4e238ad6..711f4f98af 100644 --- a/packages/react-events/README.md +++ b/packages/react-events/README.md @@ -121,7 +121,7 @@ Component instance. Returns `true` if the instance has taken ownership of the responder. -### isTargetWithinElement(target: Element, element: Element): boolean +### isTargetWithinNode(target: Element, element: Element): boolean Returns `true` if `target` is a child of `element`. diff --git a/packages/react-events/src/dom/Press.js b/packages/react-events/src/dom/Press.js index 677a644d05..c8738521ce 100644 --- a/packages/react-events/src/dom/Press.js +++ b/packages/react-events/src/dom/Press.js @@ -823,7 +823,7 @@ const PressResponder: ReactDOMEventResponder = { if ( state.pressTarget !== null && (pointerType !== 'mouse' || - !context.isTargetWithinElement(target, state.pressTarget)) + !context.isTargetWithinNode(target, state.pressTarget)) ) { // Calculate the responder region we use for deactivation, as the // element dimensions may have changed since activation. @@ -936,7 +936,7 @@ const PressResponder: ReactDOMEventResponder = { !isKeyboardEvent && state.pressTarget !== null && (pointerType !== 'mouse' || - !context.isTargetWithinElement(target, state.pressTarget)) + !context.isTargetWithinNode(target, state.pressTarget)) ) { // If the event target isn't within the press target, check if we're still // within the responder region. The region may have changed if the @@ -993,7 +993,7 @@ const PressResponder: ReactDOMEventResponder = { if ( pressTarget !== null && (scrollTarget === doc || - context.isTargetWithinElement(pressTarget, scrollTarget)) + context.isTargetWithinNode(pressTarget, scrollTarget)) ) { dispatchCancel(event, context, props, state); } diff --git a/packages/react-events/src/rn/Press.js b/packages/react-events/src/rn/Press.js new file mode 100644 index 0000000000..a911b25029 --- /dev/null +++ b/packages/react-events/src/rn/Press.js @@ -0,0 +1,683 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type { + ReactNativeResponderEvent, + ReactNativeResponderContext, + ReactNativeEventResponderEventType, + ReactNativeEventTarget, + PointerType, + ReactFaricEventTouch, + EventPriority, +} from 'react-native-renderer/src/ReactNativeTypes'; +import type {ReactEventResponder} from 'shared/ReactTypes'; +import React from 'react'; +import { + DiscreteEvent, + UserBlockingEvent, +} from 'react-native-renderer/src/ReactNativeTypes'; + +type ReactNativeEventResponder = ReactEventResponder< + ReactNativeEventResponderEventType, + ReactNativeResponderEvent, + ReactNativeResponderContext, +>; + +type PressProps = { + disabled: boolean, + delayLongPress: number, + delayPressEnd: number, + delayPressStart: number, + onContextMenu: (e: PressEvent) => void, + onLongPress: (e: PressEvent) => void, + onLongPressChange: boolean => void, + onLongPressShouldCancelPress: () => boolean, + onPress: (e: PressEvent) => void, + onPressChange: boolean => void, + onPressEnd: (e: PressEvent) => void, + onPressMove: (e: PressEvent) => void, + onPressStart: (e: PressEvent) => void, + pressRetentionOffset: { + top: number, + right: number, + bottom: number, + left: number, + }, +}; + +type PressEvent = {| + target: ReactNativeEventTarget, + type: PressEventType, + pointerType: PointerType, + timeStamp: number, + locationX: null | number, + locationY: null | number, + pageX: null | number, + pageY: null | number, + screenX: null | number, + screenY: null | number, + force: null | number, +|}; + +type PressState = { + activationPosition: null | $ReadOnly<{| + x: number, + y: number, + |}>, + addedRootEvents: boolean, + isActivePressed: boolean, + isActivePressStart: boolean, + isLongPressed: boolean, + isPressed: boolean, + isPressWithinResponderRegion: boolean, + longPressTimeout: null | number, + pointerType: PointerType, + pressTarget: null | ReactNativeEventTarget, + pressEndTimeout: null | number, + pressStartTimeout: null | number, + responderRegionOnActivation: null | $ReadOnly<{| + bottom: number, + left: number, + right: number, + top: number, + |}>, + responderRegionOnDeactivation: null | $ReadOnly<{| + bottom: number, + left: number, + right: number, + top: number, + |}>, + activePointerId: null | number, + touchEvent: null | ReactFaricEventTouch, +}; + +type PressEventType = + | 'press' + | 'pressmove' + | 'pressstart' + | 'pressend' + | 'presschange' + | 'longpress' + | 'longpresschange'; + +const DEFAULT_PRESS_END_DELAY_MS = 0; +const DEFAULT_PRESS_START_DELAY_MS = 0; +const DEFAULT_LONG_PRESS_DELAY_MS = 500; +const DEFAULT_PRESS_RETENTION_OFFSET = { + bottom: 20, + top: 20, + left: 20, + right: 20, +}; + +const targetEventTypes = ['topTouchStart']; +const rootEventTypes = ['topTouchMove', 'topTouchEnd', 'topTouchCancel']; + +function calculateDelayMS(delay: ?number, min = 0, fallback = 0) { + const maybeNumber = delay == null ? null : delay; + return Math.max(min, maybeNumber != null ? maybeNumber : fallback); +} + +function createPressEvent( + context: ReactNativeResponderContext, + type: PressEventType, + target: ReactNativeEventTarget, + pointerType: PointerType, + touchEvent: null | ReactFaricEventTouch, +): PressEvent { + let timestamp = context.getTimeStamp(); + let locationX = null; + let locationY = null; + let pageX = null; + let pageY = null; + let screenX = null; + let screenY = null; + let force = null; + + if (touchEvent) { + ({ + force, + pageX, + pageY, + screenX, + screenY, + timestamp, + locationX, + locationY, + } = touchEvent); + } + return { + target, + type, + pointerType, + timeStamp: timestamp, + pageX, + pageY, + screenX, + screenY, + locationX, + locationY, + force, + }; +} + +function dispatchEvent( + event: ?ReactNativeResponderEvent, + context: ReactNativeResponderContext, + state: PressState, + name: PressEventType, + listener: (e: Object) => void, + eventPriority: EventPriority, +): void { + const target = ((state.pressTarget: any): ReactNativeEventTarget); + const pointerType = state.pointerType; + const syntheticEvent = createPressEvent( + context, + name, + target, + pointerType, + state.touchEvent, + ); + context.dispatchEvent(syntheticEvent, listener, eventPriority); +} + +function dispatchCancel(event, context, props, state): void { + state.touchEvent = null; + if (state.isPressed) { + dispatchPressEndEvents(event, context, props, state); + } + removeRootEventTypes(context, state); +} + +function addRootEventTypes(context, state): void { + if (!state.addedRootEvents) { + state.addedRootEvents = true; + context.addRootEventTypes(rootEventTypes); + } +} + +function removeRootEventTypes(context, state): void { + if (state.addedRootEvents) { + state.addedRootEvents = false; + context.removeRootEventTypes(rootEventTypes); + } +} + +function getTouchFromPressEvent(nativeEvent): ReactFaricEventTouch | null { + const targetTouches = nativeEvent.targetTouches; + if (targetTouches.length > 0) { + return targetTouches[0]; + } + return null; +} + +function getTouchById(nativeEvent, pointerId): null | ReactFaricEventTouch { + const changedTouches = nativeEvent.changedTouches; + for (let i = 0; i < changedTouches.length; i++) { + const touch = changedTouches[i]; + if (touch.identifier === pointerId) { + return touch; + } + } + return null; +} + +function calculateResponderRegion( + context: ReactNativeResponderContext, + target: ReactNativeEventTarget, + props: PressProps, + cb: ({| + bottom: number, + top: number, + left: number, + right: number, + |}) => void, +) { + const pressRetentionOffset = Object.assign( + {}, + DEFAULT_PRESS_RETENTION_OFFSET, + props.pressRetentionOffset, + ); + + context.getTargetBoundingRect(target, rect => { + let {left, right, bottom, top} = rect; + + if (pressRetentionOffset) { + if (pressRetentionOffset.bottom != null) { + bottom += pressRetentionOffset.bottom; + } + if (pressRetentionOffset.left != null) { + left -= pressRetentionOffset.left; + } + if (pressRetentionOffset.right != null) { + right += pressRetentionOffset.right; + } + if (pressRetentionOffset.top != null) { + top -= pressRetentionOffset.top; + } + } + + cb({ + bottom, + top, + left, + right, + }); + }); +} + +function dispatchPressChangeEvent(event, context, props, state): void { + const bool = state.isActivePressed; + const listener = () => { + props.onPressChange(bool); + }; + dispatchEvent(event, context, state, 'presschange', listener, DiscreteEvent); +} + +function dispatchLongPressChangeEvent(event, context, props, state): void { + const bool = state.isLongPressed; + const listener = () => { + props.onLongPressChange(bool); + }; + dispatchEvent( + event, + context, + state, + 'longpresschange', + listener, + DiscreteEvent, + ); +} + +function activate(event: ReactNativeResponderEvent, context, props, state) { + const {pageX: x, pageY: y} = ((state.touchEvent: any): ReactFaricEventTouch); + const wasActivePressed = state.isActivePressed; + state.isActivePressed = true; + if (x !== null && y !== null) { + state.activationPosition = {x, y}; + } + + if (props.onPressStart) { + dispatchEvent( + event, + context, + state, + 'pressstart', + props.onPressStart, + DiscreteEvent, + ); + } + if (!wasActivePressed && props.onPressChange) { + dispatchPressChangeEvent(event, context, props, state); + } +} + +function deactivate(event, context, props, state) { + const wasLongPressed = state.isLongPressed; + state.isActivePressed = false; + state.isLongPressed = false; + + if (props.onPressEnd) { + dispatchEvent( + event, + context, + state, + 'pressend', + props.onPressEnd, + DiscreteEvent, + ); + } + if (props.onPressChange) { + dispatchPressChangeEvent(event, context, props, state); + } + if (wasLongPressed && props.onLongPressChange) { + dispatchLongPressChangeEvent(event, context, props, state); + } +} + +function validateRegion(touchEvent, state) { + const {responderRegionOnActivation, responderRegionOnDeactivation} = state; + let left, top, right, bottom; + + if (responderRegionOnActivation != null) { + left = responderRegionOnActivation.left; + top = responderRegionOnActivation.top; + right = responderRegionOnActivation.right; + bottom = responderRegionOnActivation.bottom; + + if (responderRegionOnDeactivation != null) { + left = Math.min(left, responderRegionOnDeactivation.left); + top = Math.min(top, responderRegionOnDeactivation.top); + right = Math.max(right, responderRegionOnDeactivation.right); + bottom = Math.max(bottom, responderRegionOnDeactivation.bottom); + } + } + const {pageX: x, pageY: y} = touchEvent; + + state.isPressWithinResponderRegion = + left != null && + right != null && + top != null && + bottom != null && + x !== null && + y !== null && + (x >= left && x <= right && y >= top && y <= bottom); +} + +function updateIsPressWithinResponderRegion( + target, + touchEvent, + context, + props, + state, + cb, +): void { + const pressTarget = state.pressTarget; + if (pressTarget != null) { + // Calculate the responder region we use for deactivation if not + // already done during move event. + if (state.responderRegionOnDeactivation == null) { + calculateResponderRegion(context, pressTarget, props, obj => { + state.responderRegionOnDeactivation = obj; + validateRegion(touchEvent, state); + cb(); + }); + return; + } else { + validateRegion(touchEvent, state); + } + } else { + state.isPressWithinResponderRegion = true; + } + cb(); +} + +function dispatchPressStartEvents(event, context, props, state) { + state.isPressed = true; + + if (state.pressEndTimeout !== null) { + context.clearTimeout(state.pressEndTimeout); + state.pressEndTimeout = null; + } + + const dispatch = () => { + state.isActivePressStart = true; + activate(event, context, props, state); + + if ( + (props.onLongPress || props.onLongPressChange) && + !state.isLongPressed + ) { + const delayLongPress = calculateDelayMS( + props.delayLongPress, + 10, + DEFAULT_LONG_PRESS_DELAY_MS, + ); + state.longPressTimeout = context.setTimeout(() => { + state.isLongPressed = true; + state.longPressTimeout = null; + if (props.onLongPress) { + dispatchEvent( + event, + context, + state, + 'longpress', + props.onLongPress, + DiscreteEvent, + ); + } + if (props.onLongPressChange) { + dispatchLongPressChangeEvent(event, context, props, state); + } + }, delayLongPress); + } + }; + + if (!state.isActivePressStart) { + const delayPressStart = calculateDelayMS( + props.delayPressStart, + 0, + DEFAULT_PRESS_START_DELAY_MS, + ); + if (delayPressStart > 0) { + state.pressStartTimeout = context.setTimeout(() => { + state.pressStartTimeout = null; + dispatch(); + }, delayPressStart); + } else { + dispatch(); + } + } +} + +function dispatchPressEndEvents(event, context, props, state): void { + const wasActivePressStart = state.isActivePressStart; + let activationWasForced = false; + + state.isActivePressStart = false; + state.isPressed = false; + + if (state.longPressTimeout !== null) { + context.clearTimeout(state.longPressTimeout); + state.longPressTimeout = null; + } + + if (!wasActivePressStart && state.pressStartTimeout !== null) { + context.clearTimeout(state.pressStartTimeout); + state.pressStartTimeout = null; + // don't activate if a press has moved beyond the responder region + if (state.isPressWithinResponderRegion && event != null) { + // if we haven't yet activated (due to delays), activate now + activate(event, context, props, state); + activationWasForced = true; + } + } + + if (state.isActivePressed) { + const delayPressEnd = calculateDelayMS( + props.delayPressEnd, + // if activation and deactivation occur during the same event there's no + // time for visual user feedback therefore a small delay is added before + // deactivating. + activationWasForced ? 10 : 0, + DEFAULT_PRESS_END_DELAY_MS, + ); + if (delayPressEnd > 0) { + state.pressEndTimeout = context.setTimeout(() => { + state.pressEndTimeout = null; + deactivate(event, context, props, state); + }, delayPressEnd); + } else { + deactivate(event, context, props, state); + } + } + + state.responderRegionOnDeactivation = null; +} + +const PressResponder: ReactNativeEventResponder = { + displayName: 'Press', + targetEventTypes, + allowEventHooks: true, + allowMultipleHostChildren: false, + createInitialState(): PressState { + return { + activationPosition: null, + activePointerId: null, + addedRootEvents: false, + isActivePressed: false, + isActivePressStart: false, + isLongPressed: false, + isPressed: false, + isPressWithinResponderRegion: false, + longPressTimeout: null, + pointerType: '', + pressEndTimeout: null, + pressStartTimeout: null, + pressTarget: null, + responderRegionOnActivation: null, + responderRegionOnDeactivation: null, + touchEvent: null, + }; + }, + onEvent( + event: ReactNativeResponderEvent, + context: ReactNativeResponderContext, + props: PressProps, + state: PressState, + ): void { + const {nativeEvent, type} = event; + + if (type === 'topTouchStart') { + if (!state.isPressed) { + state.pointerType = 'touch'; + const pressTarget = (state.pressTarget = context.getEventCurrentTarget( + event, + )); + + const touchEvent = getTouchFromPressEvent(nativeEvent); + if (touchEvent === null) { + return; + } + state.touchEvent = touchEvent; + state.activePointerId = touchEvent.identifier; + calculateResponderRegion( + context, + ((pressTarget: any): ReactNativeEventTarget), + props, + obj => { + state.responderRegionOnActivation = obj; + }, + ); + state.responderRegionOnDeactivation = null; + state.isPressWithinResponderRegion = true; + + dispatchPressStartEvents(event, context, props, state); + addRootEventTypes(context, state); + } + } + }, + onRootEvent( + event: ReactNativeResponderEvent, + context: ReactNativeResponderContext, + props: PressProps, + state: PressState, + ): void { + const {nativeEvent, target, type} = event; + const activePointerId = state.activePointerId; + + switch (type) { + case 'topTouchMove': { + const touchEvent = getTouchById(nativeEvent, activePointerId); + if (touchEvent === null) { + return; + } + state.touchEvent = touchEvent; + + updateIsPressWithinResponderRegion( + target, + touchEvent, + context, + props, + state, + () => { + if (state.isPressWithinResponderRegion) { + if (state.isPressed) { + if (props.onPressMove) { + dispatchEvent( + event, + context, + state, + 'pressmove', + props.onPressMove, + UserBlockingEvent, + ); + } + if ( + state.activationPosition != null && + state.longPressTimeout != null + ) { + const deltaX = state.activationPosition.x - touchEvent.pageX; + const deltaY = state.activationPosition.y - touchEvent.pageY; + if ( + Math.hypot(deltaX, deltaY) > 10 && + state.longPressTimeout != null + ) { + context.clearTimeout(state.longPressTimeout); + } + } + } else { + dispatchPressStartEvents(event, context, props, state); + } + } else { + dispatchPressEndEvents(event, context, props, state); + } + }, + ); + break; + } + case 'topTouchEnd': { + if (state.isPressed) { + const touchEvent = getTouchById(nativeEvent, activePointerId); + if (touchEvent === null) { + return; + } + state.touchEvent = touchEvent; + const wasLongPressed = state.isLongPressed; + dispatchPressEndEvents(event, context, props, state); + + if (state.pressTarget !== null && props.onPress) { + // If the event target isn't within the press target, check if we're still + // within the responder region. The region may have changed if the + // element's layout was modified after activation. + updateIsPressWithinResponderRegion( + target, + touchEvent, + context, + props, + state, + () => { + if (state.isPressWithinResponderRegion) { + if ( + !( + wasLongPressed && + props.onLongPressShouldCancelPress && + props.onLongPressShouldCancelPress() + ) + ) { + dispatchEvent( + event, + context, + state, + 'press', + props.onPress, + DiscreteEvent, + ); + } + } + }, + ); + } + } + state.touchEvent = null; + removeRootEventTypes(context, state); + break; + } + case 'topTouchCancel': { + dispatchCancel(event, context, props, state); + } + } + }, +}; + +export const Press = React.unstable_createEvent(PressResponder); + +export function usePress(props: PressProps): void { + React.unstable_useEvent(Press, props); +} diff --git a/packages/react-native-renderer/src/ReactFabricEventEmitter.js b/packages/react-native-renderer/src/ReactFabricEventEmitter.js index eead4ba1d1..78ea499df0 100644 --- a/packages/react-native-renderer/src/ReactFabricEventEmitter.js +++ b/packages/react-native-renderer/src/ReactFabricEventEmitter.js @@ -17,7 +17,9 @@ import {registrationNameModules} from 'events/EventPluginRegistry'; import {batchedUpdates} from 'events/ReactGenericBatching'; import type {AnyNativeEvent} from 'events/PluginModuleType'; +import {enableFlareAPI} from 'shared/ReactFeatureFlags'; import type {TopLevelType} from 'events/TopLevelEventTypes'; +import {dispatchEventForResponderEventSystem} from './ReactFabricEventResponderSystem'; export {getListener, registrationNameModules as registrationNames}; @@ -27,7 +29,16 @@ export function dispatchEvent( nativeEvent: AnyNativeEvent, ) { const targetFiber = (target: null | Fiber); + if (enableFlareAPI) { + // React Flare event system + dispatchEventForResponderEventSystem( + (topLevelType: any), + target, + (nativeEvent: any), + ); + } batchedUpdates(function() { + // Heritage plugin event system runExtractedPluginEventsInBatch( topLevelType, targetFiber, diff --git a/packages/react-native-renderer/src/ReactFabricEventResponderSystem.js b/packages/react-native-renderer/src/ReactFabricEventResponderSystem.js new file mode 100644 index 0000000000..9170875fc9 --- /dev/null +++ b/packages/react-native-renderer/src/ReactFabricEventResponderSystem.js @@ -0,0 +1,835 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import {EventComponent, HostComponent} from 'shared/ReactWorkTags'; +import type {Fiber} from 'react-reconciler/src/ReactFiber'; +import { + batchedEventUpdates, + discreteUpdates, + flushDiscreteUpdatesIfNeeded, +} from 'events/ReactGenericBatching'; +import type { + ReactEventResponder, + ReactEventComponentInstance, +} from 'shared/ReactTypes'; +import type { + ReactNativeEventResponderEventType, + ReactNativeResponderContext, + ReactNativeResponderEvent, + EventPriority, + ReactNativeEventTarget, + ReactFaricEvent, +} from './ReactNativeTypes'; +import { + ContinuousEvent, + UserBlockingEvent, + DiscreteEvent, +} from './ReactNativeTypes'; +import {invokeGuardedCallbackAndCatchFirstError} from 'shared/ReactErrorUtils'; +import {enableUserBlockingEvents} from 'shared/ReactFeatureFlags'; +import warning from 'shared/warning'; +import invariant from 'shared/invariant'; + +// Intentionally not named imports because Rollup would use dynamic dispatch for +// CommonJS interop named imports. +import * as Scheduler from 'scheduler'; +const { + unstable_UserBlockingPriority: UserBlockingPriority, + unstable_runWithPriority: runWithPriority, +} = Scheduler; + +type EventObjectType = $Shape; + +type PartialEventObject = { + target: ReactNativeEventTarget, + type: string, +}; + +type ResponderTimeout = {| + id: TimeoutID, + timers: Map, +|}; + +type ResponderTimer = {| + isHook: boolean, + instance: ReactNativeEventComponentInstance, + func: () => void, + id: number, + timeStamp: number, +|}; + +type EventQueue = { + events: Array, + eventPriority: EventPriority, +}; + +type ReactNativeEventResponder = ReactEventResponder< + ReactNativeEventResponderEventType, + ReactNativeResponderEvent, + ReactNativeResponderContext, +>; + +type ReactNativeEventComponentInstance = ReactEventComponentInstance< + ReactNativeEventResponderEventType, + ReactNativeResponderEvent, + ReactNativeResponderContext, +>; + +const {measureInWindow} = nativeFabricUIManager; + +const activeTimeouts: Map = new Map(); +const rootEventTypesToEventComponentInstances: Map< + ReactNativeEventResponderEventType | string, + Set, +> = new Map(); +const targetEventTypeCached: Map< + Array, + Set, +> = new Map(); +const ownershipChangeListeners: Set< + ReactNativeEventComponentInstance, +> = new Set(); +const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map; +const eventListeners: + | WeakMap + | Map< + $Shape, + ($Shape) => void, + > = new PossiblyWeakMap(); + +let globalOwner = null; +let continueLocalPropagation = false; + +let currentTimeStamp = 0; +let currentTimers = new Map(); +let currentInstance: null | ReactNativeEventComponentInstance = null; +let currentEventQueue: null | EventQueue = null; +let currentTimerIDCounter = 0; +let currentlyInHook = false; + +const eventResponderContext: ReactNativeResponderContext = { + dispatchEvent( + possibleEventObject: Object, + listener: ($Shape) => void, + eventPriority: EventPriority, + ): void { + validateResponderContext(); + const {target, type, timeStamp} = possibleEventObject; + + if (target == null || type == null || timeStamp == null) { + throw new Error( + 'context.dispatchEvent: "target", "timeStamp", and "type" fields on event object are required.', + ); + } + const showWarning = name => { + if (__DEV__) { + warning( + false, + '%s is not available on event objects created from event responder modules (React Flare). ' + + 'Try wrapping in a conditional, i.e. `if (event.type !== "press") { event.%s }`', + name, + name, + ); + } + }; + possibleEventObject.preventDefault = () => { + if (__DEV__) { + showWarning('preventDefault()'); + } + }; + possibleEventObject.stopPropagation = () => { + if (__DEV__) { + showWarning('stopPropagation()'); + } + }; + possibleEventObject.isDefaultPrevented = () => { + if (__DEV__) { + showWarning('isDefaultPrevented()'); + } + }; + possibleEventObject.isPropagationStopped = () => { + if (__DEV__) { + showWarning('isPropagationStopped()'); + } + }; + // $FlowFixMe: we don't need value, Flow thinks we do + Object.defineProperty(possibleEventObject, 'nativeEvent', { + get() { + if (__DEV__) { + showWarning('nativeEvent'); + } + }, + }); + + const eventObject = ((possibleEventObject: any): $Shape< + PartialEventObject, + >); + const eventQueue = ((currentEventQueue: any): EventQueue); + eventQueue.eventPriority = eventPriority; + eventListeners.set(eventObject, listener); + eventQueue.events.push(eventObject); + }, + isTargetWithinNode( + childTarget: ReactNativeEventTarget, + parentTarget: ReactNativeEventTarget, + ) { + validateResponderContext(); + const childFiber = getFiberFromTarget(childTarget); + const parentFiber = getFiberFromTarget(parentTarget); + + let node = childFiber; + while (node !== null) { + if (node === parentFiber) { + return true; + } + node = node.return; + } + return false; + }, + getEventCurrentTarget( + event: ReactNativeResponderEvent, + ): ReactNativeEventTarget { + validateResponderContext(); + const target = event.target; + let fiber = getFiberFromTarget(target); + let hostComponent = target; + const currentResponder = ((currentInstance: any): ReactNativeEventComponentInstance) + .responder; + + while (fiber !== null) { + const stateNode = fiber.stateNode; + if ( + fiber.tag === EventComponent && + (stateNode === null || stateNode.responder === currentResponder) + ) { + break; + } + if (fiber.tag === HostComponent) { + hostComponent = fiber.stateNode; + } + fiber = fiber.return; + } + return ((hostComponent: any): ReactNativeEventTarget); + }, + getTargetBoundingRect( + target: ReactNativeEventTarget, + callback: ({ + left: number, + right: number, + top: number, + bottom: number, + }) => void, + ) { + measureInWindow(target.node, (x, y, width, height) => { + callback({ + left: x, + right: x + width, + top: y, + bottom: y + height, + }); + }); + }, + addRootEventTypes( + rootEventTypes: Array, + ): void { + validateResponderContext(); + for (let i = 0; i < rootEventTypes.length; i++) { + const rootEventType = rootEventTypes[i]; + const eventComponentInstance = ((currentInstance: any): ReactNativeEventComponentInstance); + registerRootEventType(rootEventType, eventComponentInstance); + } + }, + removeRootEventTypes( + rootEventTypes: Array, + ): void { + validateResponderContext(); + for (let i = 0; i < rootEventTypes.length; i++) { + const rootEventType = rootEventTypes[i]; + + let rootEventComponents = rootEventTypesToEventComponentInstances.get( + rootEventType, + ); + let rootEventTypesSet = ((currentInstance: any): ReactNativeEventComponentInstance) + .rootEventTypes; + if (rootEventTypesSet !== null) { + rootEventTypesSet.delete(rootEventType); + } + if (rootEventComponents !== undefined) { + rootEventComponents.delete( + ((currentInstance: any): ReactNativeEventComponentInstance), + ); + } + } + }, + setTimeout(func: () => void, delay): number { + validateResponderContext(); + if (currentTimers === null) { + currentTimers = new Map(); + } + let timeout = currentTimers.get(delay); + + const timerId = currentTimerIDCounter++; + if (timeout === undefined) { + const timers = new Map(); + const id = setTimeout(() => { + processTimers(timers, delay); + }, delay); + timeout = { + id, + timers, + }; + currentTimers.set(delay, timeout); + } + timeout.timers.set(timerId, { + isHook: currentlyInHook, + instance: ((currentInstance: any): ReactNativeEventComponentInstance), + func, + id: timerId, + timeStamp: currentTimeStamp, + }); + activeTimeouts.set(timerId, timeout); + return timerId; + }, + clearTimeout(timerId: number): void { + validateResponderContext(); + const timeout = activeTimeouts.get(timerId); + + if (timeout !== undefined) { + const timers = timeout.timers; + timers.delete(timerId); + if (timers.size === 0) { + clearTimeout(timeout.id); + } + } + }, + getTimeStamp(): number { + validateResponderContext(); + return currentTimeStamp; + }, +}; + +function getFiberFromTarget( + target: null | ReactNativeEventTarget, +): Fiber | null { + if (target === null) { + return null; + } + return ((target.canonical._internalInstanceHandle: any): Fiber); +} + +function processTimers( + timers: Map, + delay: number, +): void { + const timersArr = Array.from(timers.values()); + currentEventQueue = createEventQueue(); + try { + for (let i = 0; i < timersArr.length; i++) { + const {isHook, instance, func, id, timeStamp} = timersArr[i]; + currentInstance = instance; + currentTimeStamp = timeStamp + delay; + currentlyInHook = isHook; + try { + func(); + } finally { + activeTimeouts.delete(id); + } + } + processEventQueue(); + } finally { + currentTimers = null; + currentInstance = null; + currentEventQueue = null; + currentTimeStamp = 0; + } +} + +function createFabricResponderEvent( + topLevelType: ReactNativeEventResponderEventType, + nativeEvent: ReactFaricEvent, + target: null | ReactNativeEventTarget, +): ReactNativeResponderEvent { + const responderEvent = { + nativeEvent, + target, + type: topLevelType, + }; + if (__DEV__) { + Object.freeze(responderEvent); + } + return responderEvent; +} + +function validateResponderContext(): void { + invariant( + currentEventQueue && currentInstance, + 'An event responder context was used outside of an event cycle. ' + + 'Use context.setTimeout() to use asynchronous responder context outside of event cycle .', + ); +} + +// TODO this function is almost an exact copy of the DOM version, we should +// somehow share the logic +function createEventQueue(): EventQueue { + return { + events: [], + eventPriority: ContinuousEvent, + }; +} + +// TODO this function is almost an exact copy of the DOM version, we should +// somehow share the logic +function processEventQueue(): void { + const {events, eventPriority} = ((currentEventQueue: any): EventQueue); + + if (events.length === 0) { + return; + } + + switch (eventPriority) { + case DiscreteEvent: { + flushDiscreteUpdatesIfNeeded(currentTimeStamp); + discreteUpdates(() => { + batchedEventUpdates(processEvents, events); + }); + break; + } + case UserBlockingEvent: { + if (enableUserBlockingEvents) { + runWithPriority( + UserBlockingPriority, + batchedEventUpdates.bind(null, processEvents, events), + ); + } else { + batchedEventUpdates(processEvents, events); + } + break; + } + case ContinuousEvent: { + batchedEventUpdates(processEvents, events); + break; + } + } +} + +// TODO this function is almost an exact copy of the DOM version, we should +// somehow share the logic +function triggerOwnershipListeners(): void { + const listeningInstances = Array.from(ownershipChangeListeners); + const previousInstance = currentInstance; + const previouslyInHook = currentlyInHook; + currentEventQueue = createEventQueue(); + try { + for (let i = 0; i < listeningInstances.length; i++) { + const instance = listeningInstances[i]; + const {isHook, props, responder, state} = instance; + currentInstance = instance; + currentlyInHook = isHook; + const onOwnershipChange = ((responder: any): ReactNativeEventResponder) + .onOwnershipChange; + if (onOwnershipChange !== undefined) { + onOwnershipChange(eventResponderContext, props, state); + } + } + processEventQueue(); + } finally { + currentInstance = previousInstance; + currentlyInHook = previouslyInHook; + } +} + +// TODO this function is almost an exact copy of the DOM version, we should +// somehow share the logic +function releaseOwnershipForEventComponentInstance( + eventComponentInstance: ReactNativeEventComponentInstance, +): boolean { + if (globalOwner === eventComponentInstance) { + globalOwner = null; + triggerOwnershipListeners(); + return true; + } + return false; +} + +// TODO this function is almost an exact copy of the DOM version, we should +// somehow share the logic +function processEvent(event: $Shape): void { + const type = event.type; + const listener = ((eventListeners.get(event): any): ( + $Shape, + ) => void); + invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event); +} + +// TODO this function is almost an exact copy of the DOM version, we should +// somehow share the logic +function processEvents(events: Array): void { + for (let i = 0, length = events.length; i < length; i++) { + processEvent(events[i]); + } +} + +function getFabricTargetEventTypesSet( + eventTypes: Array, +): Set { + let cachedSet = targetEventTypeCached.get(eventTypes); + + if (cachedSet === undefined) { + cachedSet = new Set(); + for (let i = 0; i < eventTypes.length; i++) { + cachedSet.add(eventTypes[i]); + } + targetEventTypeCached.set(eventTypes, cachedSet); + } + return cachedSet; +} + +// TODO this function is almost an exact copy of the DOM version, we should +// somehow share the logic +function getTargetEventResponderInstances( + topLevelType: ReactNativeEventResponderEventType, + targetFiber: null | Fiber, +): Array { + const eventResponderInstances = []; + let node = targetFiber; + while (node !== null) { + // Traverse up the fiber tree till we find event component fibers. + if (node.tag === EventComponent) { + const eventComponentInstance = node.stateNode; + const responder = eventComponentInstance.responder; + const targetEventTypes = responder.targetEventTypes; + // Validate the target event type exists on the responder + if (targetEventTypes !== undefined) { + const targetEventTypesSet = getFabricTargetEventTypesSet( + targetEventTypes, + ); + if (targetEventTypesSet.has(topLevelType)) { + eventResponderInstances.push(eventComponentInstance); + } + } + } + node = node.return; + } + return eventResponderInstances; +} + +// TODO this function is almost an exact copy of the DOM version, we should +// somehow share the logic +function shouldSkipEventComponent( + eventResponderInstance: ReactNativeEventComponentInstance, + responder: ReactNativeEventResponder, + propagatedEventResponders: null | Set, + isHook: boolean, +): boolean { + if (propagatedEventResponders !== null && !isHook) { + if (propagatedEventResponders.has(responder)) { + return true; + } + propagatedEventResponders.add(responder); + } + if (globalOwner && globalOwner !== eventResponderInstance) { + return true; + } + return false; +} + +function checkForLocalPropagationContinuation( + responder: ReactNativeEventResponder, + propagatedEventResponders: Set, +): void { + if (continueLocalPropagation === true) { + propagatedEventResponders.delete(responder); + continueLocalPropagation = false; + } +} + +// TODO this function is almost an exact copy of the DOM version, we should +// somehow share the logic +function getRootEventResponderInstances( + topLevelType: string, +): Array { + const eventResponderInstances = []; + const rootEventInstances = rootEventTypesToEventComponentInstances.get( + topLevelType, + ); + if (rootEventInstances !== undefined) { + const rootEventComponentInstances = Array.from(rootEventInstances); + + for (let i = 0; i < rootEventComponentInstances.length; i++) { + const rootEventComponentInstance = rootEventComponentInstances[i]; + eventResponderInstances.push(rootEventComponentInstance); + } + } + return eventResponderInstances; +} + +// TODO this function is almost an exact copy of the DOM version, we should +// somehow share the logic +function traverseAndHandleEventResponderInstances( + topLevelType: ReactNativeEventResponderEventType, + targetFiber: null | Fiber, + nativeEvent: ReactFaricEvent, +): void { + // Trigger event responders in this order: + // - Capture target phase + // - Bubble target phase + // - Root phase + + const targetEventResponderInstances = getTargetEventResponderInstances( + topLevelType, + targetFiber, + ); + const responderEvent = createFabricResponderEvent( + topLevelType, + nativeEvent, + targetFiber !== null + ? ((targetFiber.stateNode: any): ReactNativeEventTarget) + : null, + ); + const propagatedEventResponders: Set = new Set(); + let length = targetEventResponderInstances.length; + let i; + + // Captured and bubbled event phases have the notion of local propagation. + // This means that the propgation chain can be stopped part of the the way + // through processing event component instances. The major difference to other + // events systems is that the stopping of propgation is localized to a single + // phase, rather than both phases. + if (length > 0) { + // Capture target phase + for (i = length; i-- > 0; ) { + const targetEventResponderInstance = targetEventResponderInstances[i]; + const {isHook, responder, props, state} = targetEventResponderInstance; + const eventListener = ((responder: any): ReactNativeEventResponder) + .onEventCapture; + if (eventListener !== undefined) { + if ( + shouldSkipEventComponent( + targetEventResponderInstance, + ((responder: any): ReactNativeEventResponder), + propagatedEventResponders, + isHook, + ) + ) { + continue; + } + currentInstance = targetEventResponderInstance; + currentlyInHook = isHook; + eventListener(responderEvent, eventResponderContext, props, state); + if (!isHook) { + checkForLocalPropagationContinuation( + ((responder: any): ReactNativeEventResponder), + propagatedEventResponders, + ); + } + } + } + // We clean propagated event responders between phases. + propagatedEventResponders.clear(); + // Bubble target phase + for (i = 0; i < length; i++) { + const targetEventResponderInstance = targetEventResponderInstances[i]; + const {isHook, responder, props, state} = targetEventResponderInstance; + const eventListener = ((responder: any): ReactNativeEventResponder) + .onEvent; + if (eventListener !== undefined) { + if ( + shouldSkipEventComponent( + targetEventResponderInstance, + ((responder: any): ReactNativeEventResponder), + propagatedEventResponders, + isHook, + ) + ) { + continue; + } + currentInstance = targetEventResponderInstance; + currentlyInHook = isHook; + eventListener(responderEvent, eventResponderContext, props, state); + if (!isHook) { + checkForLocalPropagationContinuation( + ((responder: any): ReactNativeEventResponder), + propagatedEventResponders, + ); + } + } + } + } + // Root phase + const rootEventResponderInstances = getRootEventResponderInstances( + topLevelType, + ); + length = rootEventResponderInstances.length; + if (length > 0) { + for (i = 0; i < length; i++) { + const rootEventResponderInstance = rootEventResponderInstances[i]; + const {isHook, props, responder, state} = rootEventResponderInstance; + const eventListener = responder.onRootEvent; + if (eventListener !== undefined) { + if ( + shouldSkipEventComponent( + rootEventResponderInstance, + responder, + null, + isHook, + ) + ) { + continue; + } + currentInstance = rootEventResponderInstance; + currentlyInHook = isHook; + eventListener(responderEvent, eventResponderContext, props, state); + } + } + } +} + +// TODO this function is almost an exact copy of the DOM version, we should +// somehow share the logic +export function dispatchEventForResponderEventSystem( + topLevelType: ReactNativeEventResponderEventType, + targetFiber: null | Fiber, + nativeEvent: ReactFaricEvent, +): void { + const previousEventQueue = currentEventQueue; + const previousInstance = currentInstance; + const previousTimers = currentTimers; + const previousTimeStamp = currentTimeStamp; + const previouslyInHook = currentlyInHook; + currentTimers = null; + currentEventQueue = createEventQueue(); + // We might want to control timeStamp another way here + currentTimeStamp = Date.now(); + try { + traverseAndHandleEventResponderInstances( + topLevelType, + targetFiber, + nativeEvent, + ); + processEventQueue(); + } finally { + currentTimers = previousTimers; + currentInstance = previousInstance; + currentEventQueue = previousEventQueue; + currentTimeStamp = previousTimeStamp; + currentlyInHook = previouslyInHook; + } +} + +// TODO this function is almost an exact copy of the DOM version, we should +// somehow share the logic +export function mountEventResponder( + eventComponentInstance: ReactNativeEventComponentInstance, +) { + const responder = ((eventComponentInstance.responder: any): ReactNativeEventResponder); + if (responder.onOwnershipChange !== undefined) { + ownershipChangeListeners.add(eventComponentInstance); + } + const onMount = responder.onMount; + if (onMount !== undefined) { + let {isHook, props, state} = eventComponentInstance; + currentEventQueue = createEventQueue(); + currentInstance = eventComponentInstance; + currentlyInHook = isHook; + try { + onMount(eventResponderContext, props, state); + processEventQueue(); + } finally { + currentEventQueue = null; + currentInstance = null; + currentTimers = null; + } + } +} + +// TODO this function is almost an exact copy of the DOM version, we should +// somehow share the logic +export function unmountEventResponder( + eventComponentInstance: ReactNativeEventComponentInstance, +): void { + const responder = ((eventComponentInstance.responder: any): ReactNativeEventResponder); + const onUnmount = responder.onUnmount; + if (onUnmount !== undefined) { + let {isHook, props, state} = eventComponentInstance; + currentEventQueue = createEventQueue(); + currentInstance = eventComponentInstance; + currentlyInHook = isHook; + try { + onUnmount(eventResponderContext, props, state); + processEventQueue(); + } finally { + currentEventQueue = null; + currentInstance = null; + currentTimers = null; + } + } + try { + currentEventQueue = createEventQueue(); + releaseOwnershipForEventComponentInstance(eventComponentInstance); + processEventQueue(); + } finally { + currentEventQueue = null; + } + if (responder.onOwnershipChange !== undefined) { + ownershipChangeListeners.delete(eventComponentInstance); + } + const rootEventTypesSet = eventComponentInstance.rootEventTypes; + if (rootEventTypesSet !== null) { + const rootEventTypes = Array.from(rootEventTypesSet); + + for (let i = 0; i < rootEventTypes.length; i++) { + const topLevelEventType = rootEventTypes[i]; + let rootEventComponentInstances = rootEventTypesToEventComponentInstances.get( + topLevelEventType, + ); + if (rootEventComponentInstances !== undefined) { + rootEventComponentInstances.delete(eventComponentInstance); + } + } + } +} + +function registerRootEventType( + rootEventType: ReactNativeEventResponderEventType, + eventComponentInstance: ReactNativeEventComponentInstance, +) { + let rootEventComponentInstances = rootEventTypesToEventComponentInstances.get( + rootEventType, + ); + if (rootEventComponentInstances === undefined) { + rootEventComponentInstances = new Set(); + rootEventTypesToEventComponentInstances.set( + rootEventType, + rootEventComponentInstances, + ); + } + let rootEventTypesSet = eventComponentInstance.rootEventTypes; + if (rootEventTypesSet === null) { + rootEventTypesSet = eventComponentInstance.rootEventTypes = new Set(); + } + invariant( + !rootEventTypesSet.has(rootEventType), + 'addRootEventTypes() found a duplicate root event ' + + 'type of "%s". This might be because the event type exists in the event responder "rootEventTypes" ' + + 'array or because of a previous addRootEventTypes() using this root event type.', + rootEventType, + ); + rootEventTypesSet.add(rootEventType); + rootEventComponentInstances.add(eventComponentInstance); +} + +export function addRootEventTypesForComponentInstance( + eventComponentInstance: ReactNativeEventComponentInstance, + rootEventTypes: Array, +): void { + for (let i = 0; i < rootEventTypes.length; i++) { + const rootEventType = rootEventTypes[i]; + registerRootEventType(rootEventType, eventComponentInstance); + } +} diff --git a/packages/react-native-renderer/src/ReactFabricHostConfig.js b/packages/react-native-renderer/src/ReactFabricHostConfig.js index 9833f8c6c1..46561a0ef9 100644 --- a/packages/react-native-renderer/src/ReactFabricHostConfig.js +++ b/packages/react-native-renderer/src/ReactFabricHostConfig.js @@ -13,7 +13,9 @@ import type { MeasureOnSuccessCallback, NativeMethodsMixinType, ReactNativeBaseComponentViewConfig, - ReactNativeEventResponder, + ReactNativeEventResponderEventType, + ReactNativeResponderEvent, + ReactNativeResponderContext, } from './ReactNativeTypes'; import type {ReactEventComponentInstance} from 'shared/ReactTypes'; @@ -24,6 +26,13 @@ import invariant from 'shared/invariant'; import warningWithoutStack from 'shared/warningWithoutStack'; import {dispatchEvent} from './ReactFabricEventEmitter'; +import { + addRootEventTypesForComponentInstance, + mountEventResponder, + unmountEventResponder, +} from './ReactFabricEventResponderSystem'; + +import {enableFlareAPI} from 'shared/ReactFeatureFlags'; // Modules provided by RN: import { @@ -56,6 +65,12 @@ const {get: getViewConfigForType} = ReactNativeViewConfigRegistry; // This means that they never overlap. let nextReactTag = 2; +type ReactNativeEventComponentInstance = ReactEventComponentInstance< + ReactNativeEventResponderEventType, + ReactNativeResponderEvent, + ReactNativeResponderContext, +>; + type Node = Object; export type Type = string; export type Props = Object; @@ -77,7 +92,6 @@ export type UpdatePayload = Object; export type TimeoutHandle = TimeoutID; export type NoTimeout = -1; -export type EventResponder = ReactNativeEventResponder; // TODO: Remove this conditional once all changes have propagated. if (registerEventHandler) { @@ -431,21 +445,34 @@ export function replaceContainerChildren( ): void {} export function mountEventComponent( - eventComponentInstance: ReactEventComponentInstance, + eventComponentInstance: ReactNativeEventComponentInstance, ) { - throw new Error('Not yet implemented.'); + if (enableFlareAPI) { + const responder = eventComponentInstance.responder; + const {rootEventTypes} = responder; + if (rootEventTypes !== undefined) { + addRootEventTypesForComponentInstance( + eventComponentInstance, + rootEventTypes, + ); + } + mountEventResponder(eventComponentInstance); + } } export function updateEventComponent( - eventComponentInstance: ReactEventComponentInstance, + eventComponentInstance: ReactNativeEventComponentInstance, ) { - throw new Error('Not yet implemented.'); + // NO-OP, why might use this in the future } export function unmountEventComponent( - eventComponentInstance: ReactEventComponentInstance, + eventComponentInstance: ReactNativeEventComponentInstance, ): void { - throw new Error('Not yet implemented.'); + if (enableFlareAPI) { + // TODO stop listening to targetEventTypes + unmountEventResponder(eventComponentInstance); + } } export function getEventTargetChildElement( diff --git a/packages/react-native-renderer/src/ReactNativeHostConfig.js b/packages/react-native-renderer/src/ReactNativeHostConfig.js index d7373d2497..1f3cbea15a 100644 --- a/packages/react-native-renderer/src/ReactNativeHostConfig.js +++ b/packages/react-native-renderer/src/ReactNativeHostConfig.js @@ -9,7 +9,9 @@ import type { ReactNativeBaseComponentViewConfig, - ReactNativeEventResponder, + ReactNativeEventResponderEventType, + ReactNativeResponderEvent, + ReactNativeResponderContext, } from './ReactNativeTypes'; import type {ReactEventComponentInstance} from 'shared/ReactTypes'; @@ -32,6 +34,12 @@ import ReactNativeFiberHostComponent from './ReactNativeFiberHostComponent'; const {get: getViewConfigForType} = ReactNativeViewConfigRegistry; +type ReactNativeEventComponentInstance = ReactEventComponentInstance< + ReactNativeEventResponderEventType, + ReactNativeResponderEvent, + ReactNativeResponderContext, +>; + export type Type = string; export type Props = Object; export type Container = number; @@ -51,7 +59,6 @@ export type ChildSet = void; // Unused export type TimeoutHandle = TimeoutID; export type NoTimeout = -1; -export type EventResponder = ReactNativeEventResponder; const UPDATE_SIGNAL = {}; if (__DEV__) { @@ -490,19 +497,19 @@ export function unhideTextInstance( } export function mountEventComponent( - eventComponentInstance: ReactEventComponentInstance, + eventComponentInstance: ReactNativeEventComponentInstance, ) { throw new Error('Not yet implemented.'); } export function updateEventComponent( - eventComponentInstance: ReactEventComponentInstance, + eventComponentInstance: ReactNativeEventComponentInstance, ) { throw new Error('Not yet implemented.'); } export function unmountEventComponent( - eventComponentInstance: ReactEventComponentInstance, + eventComponentInstance: ReactNativeEventComponentInstance, ): void { throw new Error('Not yet implemented.'); } diff --git a/packages/react-native-renderer/src/ReactNativeTypes.js b/packages/react-native-renderer/src/ReactNativeTypes.js index ab198cb766..999fe94ced 100644 --- a/packages/react-native-renderer/src/ReactNativeTypes.js +++ b/packages/react-native-renderer/src/ReactNativeTypes.js @@ -154,9 +154,99 @@ export type ReactFabricType = { callback: ?Function, ): any, unmountComponentAtNode(containerTag: number): any, - __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: SecretInternalsFabricType, }; -// TODO will be addressed with upcoming React Flare support -export type ReactNativeEventResponder = any; +export type ReactNativeEventResponderEventType = + | 'topMouseDown' + | 'topMouseMove' + | 'topMouseUp' + | 'topScroll' + | 'topSelectionChange' + | 'topTouchCancel' + | 'topTouchEnd' + | 'topTouchMove' + | 'topTouchStart'; + +export type ReactNativeEventTarget = { + node: Object, + canonical: { + _nativeTag: number, + viewConfig: ReactNativeBaseComponentViewConfig<>, + currentProps: Object, + _internalInstanceHandle: Object, + }, +}; + +export type ReactFaricEventTouch = { + identifier: number, + locationX: number, + locationY: number, + pageX: number, + pageY: number, + screenX: number, + screenY: number, + target: number, + timestamp: number, + force: number, +}; + +export type ReactFaricEvent = { + touches: Array, + changedTouches: Array, + targetTouches: Array, + target: number, +}; + +export type ReactNativeResponderEvent = { + nativeEvent: ReactFaricEvent, + target: null | ReactNativeEventTarget, + type: ReactNativeEventResponderEventType, +}; + +export type ReactNativeResponderContext = { + dispatchEvent: ( + eventObject: Object, + listener: (Object) => void, + eventPriority: EventPriority, + ) => void, + isTargetWithinNode: ( + childTarget: ReactNativeEventTarget, + parentTarget: ReactNativeEventTarget, + ) => boolean, + getTargetBoundingRect( + target: ReactNativeEventTarget, + cb: ({ + left: number, + right: number, + top: number, + bottom: number, + }) => void, + ): void, + addRootEventTypes: ( + rootEventTypes: Array, + ) => void, + removeRootEventTypes: ( + rootEventTypes: Array, + ) => void, + getEventCurrentTarget( + event: ReactNativeResponderEvent, + ): ReactNativeEventTarget, + setTimeout: (func: () => void, timeout: number) => number, + clearTimeout: (timerId: number) => void, + getTimeStamp: () => number, +}; + +export type PointerType = + | '' + | 'mouse' + | 'keyboard' + | 'pen' + | 'touch' + | 'trackpad'; + +export opaque type EventPriority = 0 | 1 | 2; + +export const DiscreteEvent: EventPriority = 0; +export const UserBlockingEvent: EventPriority = 1; +export const ContinuousEvent: EventPriority = 2; diff --git a/packages/shared/ReactDOMTypes.js b/packages/shared/ReactDOMTypes.js index d5738a4f0a..5f14ecf2b0 100644 --- a/packages/shared/ReactDOMTypes.js +++ b/packages/shared/ReactDOMTypes.js @@ -55,7 +55,7 @@ export type ReactDOMResponderContext = { listener: (Object) => void, eventPriority: EventPriority, ) => void, - isTargetWithinElement: ( + isTargetWithinNode: ( childTarget: Element | Document, parentTarget: Element | Document, ) => boolean,