@samitouri / QOS-React-2 / commits / 2253bc81d0

[Flare] Switch from currentTarget model to responderTarget model (#16082)

Dominic Gannaway committed Jul 8, 2019 at 17:03 UTC 2253bc81d052110e14fd0eeb335d47d580ace82b
6 files changed +116 -104
packages/react-dom/src/events/DOMEventResponderSystem.js
+84 -100
@@ -503,12 +503,12 @@ function createDOMResponderEvent(
503 }
504
505 return {
506 - currentTarget: nativeEventTarget,
506 nativeEvent: nativeEvent,
507 passive,
508 passiveSupported,
509 pointerId,
510 pointerType: eventPointerType,
511 + responderTarget: null,
512 target: nativeEventTarget,
513 type: topLevelType,
514 };
@@ -580,81 +580,43 @@ function responderEventTypesContainType(
580 return false;
581 }
582
583 -function handleTargetEventResponderInstance(
583 +function validateEventTargetTypesForResponder(
584 eventType: string,
585 - responderEvent: ReactDOMResponderEvent,
586 - eventComponentInstance: ReactDOMEventComponentInstance,
587 - hookComponentResponderValidation: null | Set<ReactDOMEventResponder>,
588 - propagatedEventResponders: null | Set<ReactDOMEventResponder>,
589 -): void {
590 - const responder = eventComponentInstance.responder;
585 + responder: ReactDOMEventResponder,
586 +): boolean {
587 const targetEventTypes = responder.targetEventTypes;
588 // Validate the target event type exists on the responder
589 if (targetEventTypes !== undefined) {
594 - if (responderEventTypesContainType(targetEventTypes, eventType)) {
595 - if (hookComponentResponderValidation !== null) {
596 - hookComponentResponderValidation.add(responder);
597 - }
598 - const {isHook, props, state} = eventComponentInstance;
599 - const onEvent = responder.onEvent;
600 - if (onEvent !== undefined) {
601 - if (
602 - shouldSkipEventComponent(
603 - eventComponentInstance,
604 - ((responder: any): ReactDOMEventResponder),
605 - propagatedEventResponders,
606 - isHook,
607 - )
608 - ) {
609 - return;
610 - }
611 - currentInstance = eventComponentInstance;
612 - currentlyInHook = isHook;
613 - onEvent(responderEvent, eventResponderContext, props, state);
614 - if (!isHook) {
615 - checkForLocalPropagationContinuation(
616 - responder,
617 - ((propagatedEventResponders: any): Set<ReactDOMEventResponder>),
618 - );
619 - }
620 - }
621 - }
622 - }
623 -}
624 -
625 -function shouldSkipEventComponent(
626 - eventResponderInstance: ReactDOMEventComponentInstance,
627 - responder: ReactDOMEventResponder,
628 - propagatedEventResponders: null | Set<ReactDOMEventResponder>,
629 - isHook: boolean,
630 -): boolean {
631 - if (propagatedEventResponders !== null && !isHook) {
632 - if (propagatedEventResponders.has(responder)) {
633 - return true;
634 - }
635 - propagatedEventResponders.add(responder);
636 - }
637 - if (globalOwner && globalOwner !== eventResponderInstance) {
638 - return true;
590 + return responderEventTypesContainType(targetEventTypes, eventType);
591 }
592 return false;
593 }
594
643 -function checkForLocalPropagationContinuation(
595 +function handleTargetEventResponderInstance(
596 + responderEvent: ReactDOMResponderEvent,
597 + eventComponentInstance: ReactDOMEventComponentInstance,
598 responder: ReactDOMEventResponder,
645 - propagatedEventResponders: Set<ReactDOMEventResponder>,
599 ): void {
647 - if (continueLocalPropagation === true) {
648 - propagatedEventResponders.delete(responder);
649 - continueLocalPropagation = false;
600 + const {isHook, props, state} = eventComponentInstance;
601 + const onEvent = responder.onEvent;
602 + if (onEvent !== undefined) {
603 + currentInstance = eventComponentInstance;
604 + currentlyInHook = isHook;
605 + onEvent(responderEvent, eventResponderContext, props, state);
606 }
607 }
608
609 +function validateOwnership(
610 + eventComponentInstance: ReactDOMEventComponentInstance,
611 +): boolean {
612 + return globalOwner === null || globalOwner === eventComponentInstance;
613 +}
614 +
615 function traverseAndHandleEventResponderInstances(
616 topLevelType: string,
617 targetFiber: null | Fiber,
618 nativeEvent: AnyNativeEvent,
657 - nativeEventTarget: EventTarget,
619 + nativeEventTarget: Document | Element,
620 eventSystemFlags: EventSystemFlags,
621 ): void {
622 const isPassiveEvent = (eventSystemFlags & IS_PASSIVE) !== 0;
@@ -669,60 +631,87 @@ function traverseAndHandleEventResponderInstances(
631 const responderEvent = createDOMResponderEvent(
632 topLevelType,
633 nativeEvent,
672 - ((nativeEventTarget: any): Element | Document),
634 + nativeEventTarget,
635 isPassiveEvent,
636 isPassiveSupported,
637 );
676 - const propagatedEventResponders: Set<ReactDOMEventResponder> = new Set();
677 -
678 - // We use this to know if we should check add hooks. If there are
679 - // no event targets, then we don't add the hook forms.
680 - const hookComponentResponderValidation = new Set();
638 + const responderTargets = new Map();
639 + const allowLocalPropagation = new Set();
640
641 // Bubbled event phases have the notion of local propagation.
642 // This means that the propgation chain can be stopped part of the the way
643 // through processing event component instances.
644 let node = targetFiber;
645 + let currentTarget = nativeEventTarget;
646 while (node !== null) {
647 const {dependencies, stateNode, tag} = node;
648 if (tag === HostComponent) {
689 - responderEvent.currentTarget = stateNode;
649 + currentTarget = stateNode;
650 } else if (tag === EventComponent) {
651 const eventComponentInstance = stateNode;
692 - // Switch to the current fiber tree
693 - node = eventComponentInstance.currentFiber;
694 - handleTargetEventResponderInstance(
695 - eventType,
696 - responderEvent,
697 - eventComponentInstance,
698 - hookComponentResponderValidation,
699 - propagatedEventResponders,
700 - );
652 + if (validateOwnership(eventComponentInstance)) {
653 + const responder = eventComponentInstance.responder;
654 + let responderTarget = responderTargets.get(responder);
655 + let skipCurrentNode = false;
656 +
657 + if (responderTarget === undefined) {
658 + if (validateEventTargetTypesForResponder(eventType, responder)) {
659 + responderTarget = currentTarget;
660 + responderTargets.set(responder, currentTarget);
661 + } else {
662 + skipCurrentNode = true;
663 + }
664 + } else if (allowLocalPropagation.has(responder)) {
665 + // TODO: remove continueLocalPropagation
666 + allowLocalPropagation.delete(responder);
667 + } else {
668 + skipCurrentNode = true;
669 + }
670 + if (!skipCurrentNode) {
671 + responderEvent.responderTarget = ((responderTarget: any):
672 + | Document
673 + | Element);
674 + // Switch to the current fiber tree
675 + node = eventComponentInstance.currentFiber;
676 + handleTargetEventResponderInstance(
677 + responderEvent,
678 + eventComponentInstance,
679 + responder,
680 + );
681 + // TODO: remove continueLocalPropagation
682 + if (continueLocalPropagation) {
683 + continueLocalPropagation = false;
684 + allowLocalPropagation.add(responder);
685 + }
686 + }
687 + }
688 } else if (tag === FunctionComponent && dependencies !== null) {
689 const events = dependencies.events;
690 if (events !== null) {
691 for (let i = 0; i < events.length; i++) {
692 const eventComponentInstance = events[i];
706 - if (
707 - hookComponentResponderValidation.has(
708 - eventComponentInstance.responder,
709 - )
710 - ) {
711 - handleTargetEventResponderInstance(
712 - eventType,
713 - responderEvent,
714 - eventComponentInstance,
715 - null,
716 - null,
717 - );
693 + if (validateOwnership(eventComponentInstance)) {
694 + const responder = eventComponentInstance.responder;
695 + const responderTarget = responderTargets.get(responder);
696 + if (responderTarget !== undefined) {
697 + responderEvent.responderTarget = responderTarget;
698 + handleTargetEventResponderInstance(
699 + responderEvent,
700 + eventComponentInstance,
701 + responder,
702 + );
703 + // TODO: remove continueLocalPropagation
704 + if (continueLocalPropagation) {
705 + continueLocalPropagation = false;
706 + allowLocalPropagation.add(responder);
707 + }
708 + }
709 }
710 }
711 }
712 }
713 node = node.return;
714 }
724 - // Reset currentTarget to be null
725 - responderEvent.currentTarget = null;
715 // Root phase
716 const rootEventInstances = rootEventTypesToEventComponentInstances.get(
717 eventType,
@@ -732,21 +721,16 @@ function traverseAndHandleEventResponderInstances(
721
722 for (let i = 0; i < rootEventComponentInstances.length; i++) {
723 const rootEventComponentInstance = rootEventComponentInstances[i];
724 + if (!validateOwnership(rootEventComponentInstance)) {
725 + continue;
726 + }
727 const {isHook, props, responder, state} = rootEventComponentInstance;
728 const onRootEvent = responder.onRootEvent;
729 if (onRootEvent !== undefined) {
738 - if (
739 - shouldSkipEventComponent(
740 - rootEventComponentInstance,
741 - responder,
742 - null,
743 - isHook,
744 - )
745 - ) {
746 - continue;
747 - }
730 currentInstance = rootEventComponentInstance;
731 currentlyInHook = isHook;
732 + const responderTarget = responderTargets.get(responder);
733 + responderEvent.responderTarget = responderTarget || null;
734 onRootEvent(responderEvent, eventResponderContext, props, state);
735 }
736 }
@@ -858,7 +842,7 @@ export function dispatchEventForResponderEventSystem(
842 topLevelType: string,
843 targetFiber: null | Fiber,
844 nativeEvent: AnyNativeEvent,
861 - nativeEventTarget: EventTarget,
845 + nativeEventTarget: Document | Element,
846 eventSystemFlags: EventSystemFlags,
847 ): void {
848 if (enableFlareAPI) {
packages/react-events/src/dom/Focus.js
+1 -1
@@ -252,7 +252,7 @@ const FocusResponder: ReactDOMEventResponder = {
252 if (!state.isFocused) {
253 // Limit focus events to the direct child of the event component.
254 // Browser focus is not expected to bubble.
255 - state.focusTarget = event.currentTarget;
255 + state.focusTarget = event.responderTarget;
256 if (state.focusTarget === target) {
257 state.isFocused = true;
258 state.isLocalFocusVisible = isGlobalFocusVisible;
packages/react-events/src/dom/Hover.js
+1 -1
@@ -321,7 +321,7 @@ const HoverResponder: ReactDOMEventResponder = {
321 if (isEmulatedMouseEvent(event, state)) {
322 return;
323 }
324 - state.hoverTarget = event.currentTarget;
324 + state.hoverTarget = event.responderTarget;
325 state.ignoreEmulatedMouseEvents = true;
326 dispatchHoverStartEvents(event, context, props, state);
327 }
packages/react-events/src/dom/Press.js
+1 -1
@@ -696,7 +696,7 @@ const PressResponder: ReactDOMEventResponder = {
696 // We set these here, before the button check so we have this
697 // data around for handling of the context menu
698 state.pointerType = pointerType;
699 - const pressTarget = (state.pressTarget = event.currentTarget);
699 + const pressTarget = (state.pressTarget = event.responderTarget);
700 if (isPointerEvent) {
701 state.activePointerId = pointerId;
702 } else if (isTouchEvent) {
packages/react-events/src/dom/__tests__/Press-test.internal.js
+28
@@ -3119,6 +3119,34 @@ describe('Event responder: Press', () => {
3119 expect(pointerDownEvent).toHaveBeenCalledTimes(0);
3120 });
3121
3122 + it('has the correct press target when used with event hook', () => {
3123 + const ref = React.createRef();
3124 + const onPress = jest.fn();
3125 + const Component = () => {
3126 + React.unstable_useEvent(Press, {onPress});
3127 +
3128 + return (
3129 + <div>
3130 + <Press>
3131 + <a href="#" ref={ref} />
3132 + </Press>
3133 + </div>
3134 + );
3135 + };
3136 + ReactDOM.render(<Component />, container);
3137 +
3138 + ref.current.dispatchEvent(
3139 + createEvent('pointerdown', {pointerType: 'mouse', button: 0}),
3140 + );
3141 + ref.current.dispatchEvent(
3142 + createEvent('pointerup', {pointerType: 'mouse', button: 0}),
3143 + );
3144 + expect(onPress).toHaveBeenCalledTimes(1);
3145 + expect(onPress).toHaveBeenCalledWith(
3146 + expect.objectContaining({target: ref.current}),
3147 + );
3148 + });
3149 +
3150 it('warns when stopPropagation is used in an event hook', () => {
3151 const ref = React.createRef();
3152 const Component = () => {
packages/shared/ReactDOMTypes.js
+1 -1
@@ -24,12 +24,12 @@ export type PointerType =
24 | 'trackpad';
25
26 export type ReactDOMResponderEvent = {
27 - currentTarget: null | Element | Document,
27 nativeEvent: AnyNativeEvent,
28 passive: boolean,
29 passiveSupported: boolean,
30 pointerId: null | number,
31 pointerType: PointerType,
32 + responderTarget: null | Element | Document,
33 target: Element | Document,
34 type: string,
35 };