[Flare] Add currentTarget and unify RN and DOM codepaths (#16066)

This commit is contained in:
Dominic Gannaway
2019-07-08 11:50:21 +01:00
committed by GitHub
parent 35d2b3bb5e
commit aa519c17cc
8 changed files with 215 additions and 278 deletions
+82 -134
View File
@@ -372,29 +372,6 @@ const eventResponderContext: ReactDOMResponderContext = {
},
getActiveDocument,
objectAssign: Object.assign,
getEventCurrentTarget(event: ReactDOMResponderEvent): Element {
validateResponderContext();
const target = event.target;
let fiber = getClosestInstanceFromNode(target);
let hostComponent = target;
const currentResponder = ((currentInstance: any): ReactDOMEventComponentInstance)
.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): Element);
},
getTimeStamp(): number {
validateResponderContext();
return currentTimeStamp;
@@ -553,6 +530,7 @@ function createDOMResponderEvent(
}
return {
currentTarget: nativeEventTarget,
nativeEvent: nativeEvent,
passive,
passiveSupported,
@@ -650,11 +628,12 @@ function getDOMTargetEventTypesSet(
return cachedSet;
}
function storeTargetEventResponderInstance(
function handleTargetEventResponderInstance(
listeningName: string,
responderEvent: ReactDOMResponderEvent,
eventComponentInstance: ReactDOMEventComponentInstance,
eventResponderInstances: Array<ReactDOMEventComponentInstance>,
eventComponentResponders: null | Set<ReactDOMEventResponder>,
hookComponentResponderValidation: null | Set<ReactDOMEventResponder>,
propagatedEventResponders: null | Set<ReactDOMEventResponder>,
): void {
const responder = eventComponentInstance.responder;
const targetEventTypes = responder.targetEventTypes;
@@ -662,74 +641,34 @@ function storeTargetEventResponderInstance(
if (targetEventTypes !== undefined) {
const targetEventTypesSet = getDOMTargetEventTypesSet(targetEventTypes);
if (targetEventTypesSet.has(listeningName)) {
eventResponderInstances.push(eventComponentInstance);
if (eventComponentResponders !== null) {
eventComponentResponders.add(responder);
if (hookComponentResponderValidation !== null) {
hookComponentResponderValidation.add(responder);
}
}
}
}
function getTargetEventResponderInstances(
listeningName: string,
targetFiber: null | Fiber,
): Array<ReactDOMEventComponentInstance> {
// We use this to know if we should check add hooks. If there are
// no event targets, then we don't add the hook forms.
const eventComponentResponders = new Set();
const eventResponderInstances = [];
let node = targetFiber;
while (node !== null) {
// Traverse up the fiber tree till we find event component fibers.
const tag = node.tag;
const dependencies = node.dependencies;
if (tag === EventComponent) {
const eventComponentInstance = node.stateNode;
// Switch to the current fiber tree
node = eventComponentInstance.currentFiber;
storeTargetEventResponderInstance(
listeningName,
eventComponentInstance,
eventResponderInstances,
eventComponentResponders,
);
} else if (tag === FunctionComponent && dependencies !== null) {
const events = dependencies.events;
if (events !== null) {
for (let i = 0; i < events.length; i++) {
const eventComponentInstance = events[i];
if (eventComponentResponders.has(eventComponentInstance.responder)) {
storeTargetEventResponderInstance(
listeningName,
eventComponentInstance,
eventResponderInstances,
null,
);
}
const {isHook, props, state} = eventComponentInstance;
const onEvent = responder.onEvent;
if (onEvent !== undefined) {
if (
shouldSkipEventComponent(
eventComponentInstance,
((responder: any): ReactDOMEventResponder),
propagatedEventResponders,
isHook,
)
) {
return;
}
currentInstance = eventComponentInstance;
currentlyInHook = isHook;
onEvent(responderEvent, eventResponderContext, props, state);
if (!isHook) {
checkForLocalPropagationContinuation(
responder,
((propagatedEventResponders: any): Set<ReactDOMEventResponder>),
);
}
}
}
node = node.return;
}
return eventResponderInstances;
}
function getRootEventResponderInstances(
listeningName: string,
): Array<ReactDOMEventComponentInstance> {
const eventResponderInstances = [];
const rootEventInstances = rootEventTypesToEventComponentInstances.get(
listeningName,
);
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;
}
function shouldSkipEventComponent(
@@ -778,10 +717,6 @@ function traverseAndHandleEventResponderInstances(
// - Bubble target phase
// - Root phase
const targetEventResponderInstances = getTargetEventResponderInstances(
listeningName,
targetFiber,
);
const responderEvent = createDOMResponderEvent(
((topLevelType: any): string),
nativeEvent,
@@ -790,57 +725,70 @@ function traverseAndHandleEventResponderInstances(
isPassiveSupported,
);
const propagatedEventResponders: Set<ReactDOMEventResponder> = new Set();
let length = targetEventResponderInstances.length;
let i;
// We use this to know if we should check add hooks. If there are
// no event targets, then we don't add the hook forms.
const hookComponentResponderValidation = new Set();
// 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 propagation is localized to a single
// phase, rather than both phases.
if (length > 0) {
// Bubble target phase
for (i = 0; i < length; i++) {
const targetEventResponderInstance = targetEventResponderInstances[i];
const {isHook, props, responder, state} = targetEventResponderInstance;
const eventListener = responder.onEvent;
if (eventListener !== undefined) {
if (
shouldSkipEventComponent(
targetEventResponderInstance,
((responder: any): ReactDOMEventResponder),
propagatedEventResponders,
isHook,
)
) {
continue;
}
currentInstance = targetEventResponderInstance;
currentlyInHook = isHook;
eventListener(responderEvent, eventResponderContext, props, state);
if (!isHook) {
checkForLocalPropagationContinuation(
responder,
propagatedEventResponders,
);
// through processing event component instances.
let node = targetFiber;
while (node !== null) {
const {dependencies, stateNode, tag} = node;
if (tag === HostComponent) {
responderEvent.currentTarget = stateNode;
} else if (tag === EventComponent) {
const eventComponentInstance = stateNode;
// Switch to the current fiber tree
node = eventComponentInstance.currentFiber;
handleTargetEventResponderInstance(
listeningName,
responderEvent,
eventComponentInstance,
hookComponentResponderValidation,
propagatedEventResponders,
);
} else if (tag === FunctionComponent && dependencies !== null) {
const events = dependencies.events;
if (events !== null) {
for (let i = 0; i < events.length; i++) {
const eventComponentInstance = events[i];
if (
hookComponentResponderValidation.has(
eventComponentInstance.responder,
)
) {
handleTargetEventResponderInstance(
listeningName,
responderEvent,
eventComponentInstance,
null,
null,
);
}
}
}
}
node = node.return;
}
// Reset currentTarget to be null
responderEvent.currentTarget = null;
// Root phase
const rootEventResponderInstances = getRootEventResponderInstances(
const rootEventInstances = rootEventTypesToEventComponentInstances.get(
listeningName,
);
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 (rootEventInstances !== undefined) {
const rootEventComponentInstances = Array.from(rootEventInstances);
for (let i = 0; i < rootEventComponentInstances.length; i++) {
const rootEventComponentInstance = rootEventComponentInstances[i];
const {isHook, props, responder, state} = rootEventComponentInstance;
const onRootEvent = responder.onRootEvent;
if (onRootEvent !== undefined) {
if (
shouldSkipEventComponent(
rootEventResponderInstance,
rootEventComponentInstance,
responder,
null,
isHook,
@@ -848,9 +796,9 @@ function traverseAndHandleEventResponderInstances(
) {
continue;
}
currentInstance = rootEventResponderInstance;
currentInstance = rootEventComponentInstance;
currentlyInHook = isHook;
eventListener(responderEvent, eventResponderContext, props, state);
onRootEvent(responderEvent, eventResponderContext, props, state);
}
}
}
+4 -2
View File
@@ -207,8 +207,10 @@ function handleRootPointerEvent(
// Focus should stop being visible if a pointer is used on the element
// after it was focused using a keyboard.
const focusTarget = state.focusTarget;
if (
state.focusTarget === context.getEventCurrentTarget(event) &&
focusTarget !== null &&
context.isTargetWithinNode(event.target, focusTarget) &&
(type === 'mousedown' || type === 'touchstart' || type === 'pointerdown')
) {
dispatchFocusVisibleOutEvent(context, props, state);
@@ -253,7 +255,7 @@ const FocusResponder: ReactDOMEventResponder = {
if (!state.isFocused) {
// Limit focus events to the direct child of the event component.
// Browser focus is not expected to bubble.
state.focusTarget = context.getEventCurrentTarget(event);
state.focusTarget = event.currentTarget;
if (state.focusTarget === target) {
state.isFocused = true;
state.isLocalFocusVisible = isGlobalFocusVisible;
+1 -1
View File
@@ -321,7 +321,7 @@ const HoverResponder: ReactDOMEventResponder = {
if (isEmulatedMouseEvent(event, state)) {
return;
}
state.hoverTarget = context.getEventCurrentTarget(event);
state.hoverTarget = event.currentTarget;
state.ignoreEmulatedMouseEvents = true;
dispatchHoverStartEvents(event, context, props, state);
}
+24 -13
View File
@@ -57,7 +57,7 @@ type PressState = {
isPressWithinResponderRegion: boolean,
longPressTimeout: null | number,
pointerType: PointerType,
pressTarget: null | Element,
pressTarget: null | Element | Document,
pressEndTimeout: null | number,
pressStartTimeout: null | number,
responderRegionOnActivation: null | $ReadOnly<{|
@@ -619,6 +619,11 @@ function handleStopPropagation(
}
}
function targetIsDocument(target: null | Node): boolean {
// When target is null, it is the root
return target === null || target.nodeType === 9;
}
const PressResponder: ReactDOMEventResponder = {
displayName: 'Press',
targetEventTypes,
@@ -691,7 +696,7 @@ const PressResponder: ReactDOMEventResponder = {
// We set these here, before the button check so we have this
// data around for handling of the context menu
state.pointerType = pointerType;
state.pressTarget = context.getEventCurrentTarget(event);
const pressTarget = (state.pressTarget = event.currentTarget);
if (isPointerEvent) {
state.activePointerId = pointerId;
} else if (isTouchEvent) {
@@ -712,12 +717,14 @@ const PressResponder: ReactDOMEventResponder = {
) {
return;
}
state.responderRegionOnActivation = calculateResponderRegion(
context,
state.pressTarget,
props,
);
// Exclude document targets
if (!targetIsDocument(pressTarget)) {
state.responderRegionOnActivation = calculateResponderRegion(
context,
((pressTarget: any): Element),
props,
);
}
state.responderRegionOnDeactivation = null;
state.isPressWithinResponderRegion = true;
dispatchPressStartEvents(event, context, props, state);
@@ -824,11 +831,13 @@ const PressResponder: ReactDOMEventResponder = {
}
state.touchEvent = touchEvent;
}
const pressTarget = state.pressTarget;
if (
state.pressTarget !== null &&
pressTarget !== null &&
!targetIsDocument(pressTarget) &&
(pointerType !== 'mouse' ||
!context.isTargetWithinNode(target, state.pressTarget))
!context.isTargetWithinNode(target, pressTarget))
) {
// Calculate the responder region we use for deactivation, as the
// element dimensions may have changed since activation.
@@ -934,14 +943,16 @@ const PressResponder: ReactDOMEventResponder = {
}
const wasLongPressed = state.isLongPressed;
const pressTarget = state.pressTarget;
dispatchPressEndEvents(event, context, props, state);
if (state.pressTarget !== null && props.onPress) {
if (pressTarget !== null && props.onPress) {
if (
!isKeyboardEvent &&
state.pressTarget !== null &&
pressTarget !== null &&
!targetIsDocument(pressTarget) &&
(pointerType !== 'mouse' ||
!context.isTargetWithinNode(target, state.pressTarget))
!context.isTargetWithinNode(target, 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
+1 -3
View File
@@ -537,9 +537,7 @@ const PressResponder: ReactNativeEventResponder = {
if (type === 'topTouchStart') {
if (!state.isPressed) {
state.pointerType = 'touch';
const pressTarget = (state.pressTarget = context.getEventCurrentTarget(
event,
));
const pressTarget = (state.pressTarget = event.currentTarget);
const touchEvent = getTouchFromPressEvent(nativeEvent);
if (touchEvent === null) {
@@ -7,7 +7,11 @@
* @flow
*/
import {EventComponent, HostComponent} from 'shared/ReactWorkTags';
import {
EventComponent,
HostComponent,
FunctionComponent,
} from 'shared/ReactWorkTags';
import type {Fiber} from 'react-reconciler/src/ReactFiber';
import {
batchedEventUpdates,
@@ -192,31 +196,6 @@ const eventResponderContext: ReactNativeResponderContext = {
}
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: ({
@@ -320,7 +299,7 @@ function getFiberFromTarget(
if (target === null) {
return null;
}
return ((target.canonical._internalInstanceHandle: any): Fiber);
return ((target.canonical._internalInstanceHandle: any): Fiber) || null;
}
function processTimers(
@@ -355,15 +334,12 @@ function createFabricResponderEvent(
nativeEvent: ReactFaricEvent,
target: null | ReactNativeEventTarget,
): ReactNativeResponderEvent {
const responderEvent = {
return {
currentTarget: target,
nativeEvent,
target,
type: topLevelType,
};
if (__DEV__) {
Object.freeze(responderEvent);
}
return responderEvent;
}
function validateResponderContext(): void {
@@ -490,35 +466,6 @@ function getFabricTargetEventTypesSet(
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(
@@ -551,22 +498,47 @@ function checkForLocalPropagationContinuation(
// 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);
function handleTargetEventResponderInstance(
topLevelType: ReactNativeEventResponderEventType,
responderEvent: ReactNativeResponderEvent,
eventComponentInstance: ReactNativeEventComponentInstance,
hookComponentResponderValidation: null | Set<ReactNativeEventResponder>,
propagatedEventResponders: null | Set<ReactNativeEventResponder>,
): void {
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)) {
if (hookComponentResponderValidation !== null) {
hookComponentResponderValidation.add(responder);
}
const {isHook, props, state} = eventComponentInstance;
const eventListener = responder.onEvent;
if (eventListener !== undefined) {
if (
shouldSkipEventComponent(
eventComponentInstance,
((responder: any): ReactNativeEventResponder),
propagatedEventResponders,
isHook,
)
) {
return;
}
currentInstance = eventComponentInstance;
currentlyInHook = isHook;
eventListener(responderEvent, eventResponderContext, props, state);
if (!isHook) {
checkForLocalPropagationContinuation(
responder,
((propagatedEventResponders: any): Set<ReactNativeEventResponder>),
);
}
}
}
}
return eventResponderInstances;
}
// TODO this function is almost an exact copy of the DOM version, we should
@@ -580,10 +552,6 @@ function traverseAndHandleEventResponderInstances(
// - Bubble target phase
// - Root phase
const targetEventResponderInstances = getTargetEventResponderInstances(
topLevelType,
targetFiber,
);
const responderEvent = createFabricResponderEvent(
topLevelType,
nativeEvent,
@@ -592,58 +560,70 @@ function traverseAndHandleEventResponderInstances(
: null,
);
const propagatedEventResponders: Set<ReactNativeEventResponder> = new Set();
let length = targetEventResponderInstances.length;
let i;
// We use this to know if we should check add hooks. If there are
// no event targets, then we don't add the hook forms.
const hookComponentResponderValidation = new Set();
// 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 propagation is localized to a single
// phase, rather than both phases.
if (length > 0) {
// 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,
);
// through processing event component instances.
let node = targetFiber;
while (node !== null) {
const {dependencies, stateNode, tag} = node;
if (tag === HostComponent) {
responderEvent.currentTarget = stateNode;
} else if (tag === EventComponent) {
const eventComponentInstance = stateNode;
// Switch to the current fiber tree
node = eventComponentInstance.currentFiber;
handleTargetEventResponderInstance(
topLevelType,
responderEvent,
eventComponentInstance,
hookComponentResponderValidation,
propagatedEventResponders,
);
} else if (tag === FunctionComponent && dependencies !== null) {
const events = dependencies.events;
if (events !== null) {
for (let i = 0; i < events.length; i++) {
const eventComponentInstance = events[i];
if (
hookComponentResponderValidation.has(
eventComponentInstance.responder,
)
) {
handleTargetEventResponderInstance(
topLevelType,
responderEvent,
eventComponentInstance,
null,
null,
);
}
}
}
}
node = node.return;
}
// Reset currentTarget to be null
responderEvent.currentTarget = null;
// Root phase
const rootEventResponderInstances = getRootEventResponderInstances(
const rootEventInstances = rootEventTypesToEventComponentInstances.get(
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 (rootEventInstances !== undefined) {
const rootEventComponentInstances = Array.from(rootEventInstances);
for (let i = 0; i < rootEventComponentInstances.length; i++) {
const rootEventComponentInstance = rootEventComponentInstances[i];
const {isHook, props, responder, state} = rootEventComponentInstance;
const onRootEvent = responder.onRootEvent;
if (onRootEvent !== undefined) {
if (
shouldSkipEventComponent(
rootEventResponderInstance,
rootEventComponentInstance,
responder,
null,
isHook,
@@ -651,9 +631,9 @@ function traverseAndHandleEventResponderInstances(
) {
continue;
}
currentInstance = rootEventResponderInstance;
currentInstance = rootEventComponentInstance;
currentlyInHook = isHook;
eventListener(responderEvent, eventResponderContext, props, state);
onRootEvent(responderEvent, eventResponderContext, props, state);
}
}
}
+1 -3
View File
@@ -199,6 +199,7 @@ export type ReactFaricEvent = {
};
export type ReactNativeResponderEvent = {
currentTarget: null | ReactNativeEventTarget,
nativeEvent: ReactFaricEvent,
target: null | ReactNativeEventTarget,
type: ReactNativeEventResponderEventType,
@@ -229,9 +230,6 @@ export type ReactNativeResponderContext = {
removeRootEventTypes: (
rootEventTypes: Array<ReactNativeEventResponderEventType>,
) => void,
getEventCurrentTarget(
event: ReactNativeResponderEvent,
): ReactNativeEventTarget,
setTimeout: (func: () => void, timeout: number) => number,
clearTimeout: (timerId: number) => void,
getTimeStamp: () => number,
+1 -1
View File
@@ -28,6 +28,7 @@ export type PointerType =
| 'trackpad';
export type ReactDOMResponderEvent = {
currentTarget: null | Element | Document,
nativeEvent: AnyNativeEvent,
passive: boolean,
passiveSupported: boolean,
@@ -75,7 +76,6 @@ export type ReactDOMResponderContext = {
getFocusableElementsInScope(): Array<HTMLElement>,
getActiveDocument(): Document,
objectAssign: Function,
getEventCurrentTarget(event: ReactDOMResponderEvent): Element,
getTimeStamp: () => number,
isTargetWithinHostComponent: (
target: Element | Document,