mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
[Flare] Bring Flare support to React Native Fabric (#15887)
This commit is contained in:
+3
-3
@@ -428,19 +428,19 @@ export function unhideTextInstance(textInstance, text): void {
|
||||
}
|
||||
|
||||
export function mountEventComponent(
|
||||
eventComponentInstance: ReactEventComponentInstance,
|
||||
eventComponentInstance: ReactEventComponentInstance<any, any, any>,
|
||||
) {
|
||||
throw new Error('Not yet implemented.');
|
||||
}
|
||||
|
||||
export function updateEventComponent(
|
||||
eventComponentInstance: ReactEventComponentInstance,
|
||||
eventComponentInstance: ReactEventComponentInstance<any, any, any>,
|
||||
) {
|
||||
throw new Error('Not yet implemented.');
|
||||
}
|
||||
|
||||
export function unmountEventComponent(
|
||||
eventComponentInstance: ReactEventComponentInstance,
|
||||
eventComponentInstance: ReactEventComponentInstance<any, any, any>,
|
||||
): void {
|
||||
throw new Error('Not yet implemented.');
|
||||
}
|
||||
|
||||
+1
-1
@@ -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';
|
||||
|
||||
@@ -231,7 +231,7 @@ const eventResponderContext: ReactDOMResponderContext = {
|
||||
}
|
||||
return false;
|
||||
},
|
||||
isTargetWithinElement(
|
||||
isTargetWithinNode(
|
||||
childTarget: Element | Document,
|
||||
parentTarget: Element | Document,
|
||||
): boolean {
|
||||
|
||||
+1
-1
@@ -332,7 +332,7 @@ export function dispatchEvent(
|
||||
targetInst,
|
||||
);
|
||||
} else {
|
||||
// Responder event system (experimental event API)
|
||||
// React Flare event system
|
||||
dispatchEventForResponderEventSystem(
|
||||
topLevelType,
|
||||
targetInst,
|
||||
|
||||
@@ -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`.
|
||||
|
||||
|
||||
+3
-3
@@ -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);
|
||||
}
|
||||
|
||||
+683
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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<PartialEventObject>;
|
||||
|
||||
type PartialEventObject = {
|
||||
target: ReactNativeEventTarget,
|
||||
type: string,
|
||||
};
|
||||
|
||||
type ResponderTimeout = {|
|
||||
id: TimeoutID,
|
||||
timers: Map<number, ResponderTimer>,
|
||||
|};
|
||||
|
||||
type ResponderTimer = {|
|
||||
isHook: boolean,
|
||||
instance: ReactNativeEventComponentInstance,
|
||||
func: () => void,
|
||||
id: number,
|
||||
timeStamp: number,
|
||||
|};
|
||||
|
||||
type EventQueue = {
|
||||
events: Array<EventObjectType>,
|
||||
eventPriority: EventPriority,
|
||||
};
|
||||
|
||||
type ReactNativeEventResponder = ReactEventResponder<
|
||||
ReactNativeEventResponderEventType,
|
||||
ReactNativeResponderEvent,
|
||||
ReactNativeResponderContext,
|
||||
>;
|
||||
|
||||
type ReactNativeEventComponentInstance = ReactEventComponentInstance<
|
||||
ReactNativeEventResponderEventType,
|
||||
ReactNativeResponderEvent,
|
||||
ReactNativeResponderContext,
|
||||
>;
|
||||
|
||||
const {measureInWindow} = nativeFabricUIManager;
|
||||
|
||||
const activeTimeouts: Map<number, ResponderTimeout> = new Map();
|
||||
const rootEventTypesToEventComponentInstances: Map<
|
||||
ReactNativeEventResponderEventType | string,
|
||||
Set<ReactNativeEventComponentInstance>,
|
||||
> = new Map();
|
||||
const targetEventTypeCached: Map<
|
||||
Array<ReactNativeEventResponderEventType>,
|
||||
Set<ReactNativeEventResponderEventType>,
|
||||
> = new Map();
|
||||
const ownershipChangeListeners: Set<
|
||||
ReactNativeEventComponentInstance,
|
||||
> = new Set();
|
||||
const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
|
||||
const eventListeners:
|
||||
| WeakMap
|
||||
| Map<
|
||||
$Shape<PartialEventObject>,
|
||||
($Shape<PartialEventObject>) => 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<PartialEventObject>) => 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<ReactNativeEventResponderEventType>,
|
||||
): 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<ReactNativeEventResponderEventType>,
|
||||
): 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<number, ResponderTimer>,
|
||||
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<PartialEventObject>): void {
|
||||
const type = event.type;
|
||||
const listener = ((eventListeners.get(event): any): (
|
||||
$Shape<PartialEventObject>,
|
||||
) => 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<EventObjectType>): void {
|
||||
for (let i = 0, length = events.length; i < length; i++) {
|
||||
processEvent(events[i]);
|
||||
}
|
||||
}
|
||||
|
||||
function getFabricTargetEventTypesSet(
|
||||
eventTypes: Array<ReactNativeEventResponderEventType>,
|
||||
): Set<ReactNativeEventResponderEventType> {
|
||||
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<ReactNativeEventComponentInstance> {
|
||||
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<ReactNativeEventResponder>,
|
||||
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<ReactNativeEventResponder>,
|
||||
): 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<ReactNativeEventComponentInstance> {
|
||||
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<ReactNativeEventResponder> = 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<ReactNativeEventResponderEventType>,
|
||||
): void {
|
||||
for (let i = 0; i < rootEventTypes.length; i++) {
|
||||
const rootEventType = rootEventTypes[i];
|
||||
registerRootEventType(rootEventType, eventComponentInstance);
|
||||
}
|
||||
}
|
||||
+35
-8
@@ -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<any, any, any>,
|
||||
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<any, any, any>,
|
||||
eventComponentInstance: ReactNativeEventComponentInstance,
|
||||
) {
|
||||
throw new Error('Not yet implemented.');
|
||||
// NO-OP, why might use this in the future
|
||||
}
|
||||
|
||||
export function unmountEventComponent(
|
||||
eventComponentInstance: ReactEventComponentInstance<any, any, any>,
|
||||
eventComponentInstance: ReactNativeEventComponentInstance,
|
||||
): void {
|
||||
throw new Error('Not yet implemented.');
|
||||
if (enableFlareAPI) {
|
||||
// TODO stop listening to targetEventTypes
|
||||
unmountEventResponder(eventComponentInstance);
|
||||
}
|
||||
}
|
||||
|
||||
export function getEventTargetChildElement(
|
||||
|
||||
+12
-5
@@ -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<any, any, any>,
|
||||
eventComponentInstance: ReactNativeEventComponentInstance,
|
||||
) {
|
||||
throw new Error('Not yet implemented.');
|
||||
}
|
||||
|
||||
export function updateEventComponent(
|
||||
eventComponentInstance: ReactEventComponentInstance<any, any, any>,
|
||||
eventComponentInstance: ReactNativeEventComponentInstance,
|
||||
) {
|
||||
throw new Error('Not yet implemented.');
|
||||
}
|
||||
|
||||
export function unmountEventComponent(
|
||||
eventComponentInstance: ReactEventComponentInstance<any, any, any>,
|
||||
eventComponentInstance: ReactNativeEventComponentInstance,
|
||||
): void {
|
||||
throw new Error('Not yet implemented.');
|
||||
}
|
||||
|
||||
+93
-3
@@ -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<ReactFaricEventTouch>,
|
||||
changedTouches: Array<ReactFaricEventTouch>,
|
||||
targetTouches: Array<ReactFaricEventTouch>,
|
||||
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<ReactNativeEventResponderEventType>,
|
||||
) => void,
|
||||
removeRootEventTypes: (
|
||||
rootEventTypes: Array<ReactNativeEventResponderEventType>,
|
||||
) => 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;
|
||||
|
||||
@@ -55,7 +55,7 @@ export type ReactDOMResponderContext = {
|
||||
listener: (Object) => void,
|
||||
eventPriority: EventPriority,
|
||||
) => void,
|
||||
isTargetWithinElement: (
|
||||
isTargetWithinNode: (
|
||||
childTarget: Element | Document,
|
||||
parentTarget: Element | Document,
|
||||
) => boolean,
|
||||
|
||||
Reference in New Issue
Block a user