Skip to content

Commit

Permalink
[Flare] Switch from currentTarget model to responderTarget model (#16082
Browse files Browse the repository at this point in the history
)
  • Loading branch information
trueadm authored Jul 8, 2019
1 parent 67e3f3f commit 2253bc8
Show file tree
Hide file tree
Showing 6 changed files with 116 additions and 104 deletions.
184 changes: 84 additions & 100 deletions packages/react-dom/src/events/DOMEventResponderSystem.js
Original file line number Diff line number Diff line change
Expand Up @@ -503,12 +503,12 @@ function createDOMResponderEvent(
}

return {
currentTarget: nativeEventTarget,
nativeEvent: nativeEvent,
passive,
passiveSupported,
pointerId,
pointerType: eventPointerType,
responderTarget: null,
target: nativeEventTarget,
type: topLevelType,
};
Expand Down Expand Up @@ -580,81 +580,43 @@ function responderEventTypesContainType(
return false;
}

function handleTargetEventResponderInstance(
function validateEventTargetTypesForResponder(
eventType: string,
responderEvent: ReactDOMResponderEvent,
eventComponentInstance: ReactDOMEventComponentInstance,
hookComponentResponderValidation: null | Set<ReactDOMEventResponder>,
propagatedEventResponders: null | Set<ReactDOMEventResponder>,
): void {
const responder = eventComponentInstance.responder;
responder: ReactDOMEventResponder,
): boolean {
const targetEventTypes = responder.targetEventTypes;
// Validate the target event type exists on the responder
if (targetEventTypes !== undefined) {
if (responderEventTypesContainType(targetEventTypes, eventType)) {
if (hookComponentResponderValidation !== null) {
hookComponentResponderValidation.add(responder);
}
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>),
);
}
}
}
}
}

function shouldSkipEventComponent(
eventResponderInstance: ReactDOMEventComponentInstance,
responder: ReactDOMEventResponder,
propagatedEventResponders: null | Set<ReactDOMEventResponder>,
isHook: boolean,
): boolean {
if (propagatedEventResponders !== null && !isHook) {
if (propagatedEventResponders.has(responder)) {
return true;
}
propagatedEventResponders.add(responder);
}
if (globalOwner && globalOwner !== eventResponderInstance) {
return true;
return responderEventTypesContainType(targetEventTypes, eventType);
}
return false;
}

function checkForLocalPropagationContinuation(
function handleTargetEventResponderInstance(
responderEvent: ReactDOMResponderEvent,
eventComponentInstance: ReactDOMEventComponentInstance,
responder: ReactDOMEventResponder,
propagatedEventResponders: Set<ReactDOMEventResponder>,
): void {
if (continueLocalPropagation === true) {
propagatedEventResponders.delete(responder);
continueLocalPropagation = false;
const {isHook, props, state} = eventComponentInstance;
const onEvent = responder.onEvent;
if (onEvent !== undefined) {
currentInstance = eventComponentInstance;
currentlyInHook = isHook;
onEvent(responderEvent, eventResponderContext, props, state);
}
}

function validateOwnership(
eventComponentInstance: ReactDOMEventComponentInstance,
): boolean {
return globalOwner === null || globalOwner === eventComponentInstance;
}

function traverseAndHandleEventResponderInstances(
topLevelType: string,
targetFiber: null | Fiber,
nativeEvent: AnyNativeEvent,
nativeEventTarget: EventTarget,
nativeEventTarget: Document | Element,
eventSystemFlags: EventSystemFlags,
): void {
const isPassiveEvent = (eventSystemFlags & IS_PASSIVE) !== 0;
Expand All @@ -669,60 +631,87 @@ function traverseAndHandleEventResponderInstances(
const responderEvent = createDOMResponderEvent(
topLevelType,
nativeEvent,
((nativeEventTarget: any): Element | Document),
nativeEventTarget,
isPassiveEvent,
isPassiveSupported,
);
const propagatedEventResponders: Set<ReactDOMEventResponder> = new Set();

// 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();
const responderTargets = new Map();
const allowLocalPropagation = 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.
let node = targetFiber;
let currentTarget = nativeEventTarget;
while (node !== null) {
const {dependencies, stateNode, tag} = node;
if (tag === HostComponent) {
responderEvent.currentTarget = stateNode;
currentTarget = stateNode;
} else if (tag === EventComponent) {
const eventComponentInstance = stateNode;
// Switch to the current fiber tree
node = eventComponentInstance.currentFiber;
handleTargetEventResponderInstance(
eventType,
responderEvent,
eventComponentInstance,
hookComponentResponderValidation,
propagatedEventResponders,
);
if (validateOwnership(eventComponentInstance)) {
const responder = eventComponentInstance.responder;
let responderTarget = responderTargets.get(responder);
let skipCurrentNode = false;

if (responderTarget === undefined) {
if (validateEventTargetTypesForResponder(eventType, responder)) {
responderTarget = currentTarget;
responderTargets.set(responder, currentTarget);
} else {
skipCurrentNode = true;
}
} else if (allowLocalPropagation.has(responder)) {
// TODO: remove continueLocalPropagation
allowLocalPropagation.delete(responder);
} else {
skipCurrentNode = true;
}
if (!skipCurrentNode) {
responderEvent.responderTarget = ((responderTarget: any):
| Document
| Element);
// Switch to the current fiber tree
node = eventComponentInstance.currentFiber;
handleTargetEventResponderInstance(
responderEvent,
eventComponentInstance,
responder,
);
// TODO: remove continueLocalPropagation
if (continueLocalPropagation) {
continueLocalPropagation = false;
allowLocalPropagation.add(responder);
}
}
}
} 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(
eventType,
responderEvent,
eventComponentInstance,
null,
null,
);
if (validateOwnership(eventComponentInstance)) {
const responder = eventComponentInstance.responder;
const responderTarget = responderTargets.get(responder);
if (responderTarget !== undefined) {
responderEvent.responderTarget = responderTarget;
handleTargetEventResponderInstance(
responderEvent,
eventComponentInstance,
responder,
);
// TODO: remove continueLocalPropagation
if (continueLocalPropagation) {
continueLocalPropagation = false;
allowLocalPropagation.add(responder);
}
}
}
}
}
}
node = node.return;
}
// Reset currentTarget to be null
responderEvent.currentTarget = null;
// Root phase
const rootEventInstances = rootEventTypesToEventComponentInstances.get(
eventType,
Expand All @@ -732,21 +721,16 @@ function traverseAndHandleEventResponderInstances(

for (let i = 0; i < rootEventComponentInstances.length; i++) {
const rootEventComponentInstance = rootEventComponentInstances[i];
if (!validateOwnership(rootEventComponentInstance)) {
continue;
}
const {isHook, props, responder, state} = rootEventComponentInstance;
const onRootEvent = responder.onRootEvent;
if (onRootEvent !== undefined) {
if (
shouldSkipEventComponent(
rootEventComponentInstance,
responder,
null,
isHook,
)
) {
continue;
}
currentInstance = rootEventComponentInstance;
currentlyInHook = isHook;
const responderTarget = responderTargets.get(responder);
responderEvent.responderTarget = responderTarget || null;
onRootEvent(responderEvent, eventResponderContext, props, state);
}
}
Expand Down Expand Up @@ -858,7 +842,7 @@ export function dispatchEventForResponderEventSystem(
topLevelType: string,
targetFiber: null | Fiber,
nativeEvent: AnyNativeEvent,
nativeEventTarget: EventTarget,
nativeEventTarget: Document | Element,
eventSystemFlags: EventSystemFlags,
): void {
if (enableFlareAPI) {
Expand Down
2 changes: 1 addition & 1 deletion packages/react-events/src/dom/Focus.js
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,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 = event.currentTarget;
state.focusTarget = event.responderTarget;
if (state.focusTarget === target) {
state.isFocused = true;
state.isLocalFocusVisible = isGlobalFocusVisible;
Expand Down
2 changes: 1 addition & 1 deletion packages/react-events/src/dom/Hover.js
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ const HoverResponder: ReactDOMEventResponder = {
if (isEmulatedMouseEvent(event, state)) {
return;
}
state.hoverTarget = event.currentTarget;
state.hoverTarget = event.responderTarget;
state.ignoreEmulatedMouseEvents = true;
dispatchHoverStartEvents(event, context, props, state);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/react-events/src/dom/Press.js
Original file line number Diff line number Diff line change
Expand Up @@ -696,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;
const pressTarget = (state.pressTarget = event.currentTarget);
const pressTarget = (state.pressTarget = event.responderTarget);
if (isPointerEvent) {
state.activePointerId = pointerId;
} else if (isTouchEvent) {
Expand Down
28 changes: 28 additions & 0 deletions packages/react-events/src/dom/__tests__/Press-test.internal.js
Original file line number Diff line number Diff line change
Expand Up @@ -3119,6 +3119,34 @@ describe('Event responder: Press', () => {
expect(pointerDownEvent).toHaveBeenCalledTimes(0);
});

it('has the correct press target when used with event hook', () => {
const ref = React.createRef();
const onPress = jest.fn();
const Component = () => {
React.unstable_useEvent(Press, {onPress});

return (
<div>
<Press>
<a href="#" ref={ref} />
</Press>
</div>
);
};
ReactDOM.render(<Component />, container);

ref.current.dispatchEvent(
createEvent('pointerdown', {pointerType: 'mouse', button: 0}),
);
ref.current.dispatchEvent(
createEvent('pointerup', {pointerType: 'mouse', button: 0}),
);
expect(onPress).toHaveBeenCalledTimes(1);
expect(onPress).toHaveBeenCalledWith(
expect.objectContaining({target: ref.current}),
);
});

it('warns when stopPropagation is used in an event hook', () => {
const ref = React.createRef();
const Component = () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/shared/ReactDOMTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ export type PointerType =
| 'trackpad';

export type ReactDOMResponderEvent = {
currentTarget: null | Element | Document,
nativeEvent: AnyNativeEvent,
passive: boolean,
passiveSupported: boolean,
pointerId: null | number,
pointerType: PointerType,
responderTarget: null | Element | Document,
target: Element | Document,
type: string,
};
Expand Down

0 comments on commit 2253bc8

Please sign in to comment.