[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
};