@samitouri / QOS-React-2 / commits / 823dc581fe

Modern Event System: fix EnterLeave plugin logic (#18830)

Dominic Gannaway committed May 5, 2020 at 22:10 UTC 823dc581fea8814a904579e85a62da6d18258830
5 files changed +71 -23
packages/react-dom/src/client/ReactDOMComponent.js
+2 -2
@@ -269,7 +269,7 @@ if (__DEV__) {
269 };
270 }
271
272 -function ensureListeningTo(
272 +export function ensureListeningTo(
273 rootContainerInstance: Element | Node,
274 registrationName: string,
275 ): void {
@@ -280,7 +280,7 @@ function ensureListeningTo(
280 rootContainerInstance.nodeType === COMMENT_NODE
281 ? rootContainerInstance.parentNode
282 : rootContainerInstance;
283 - // Containers can only ever be element nodes. We do not
283 + // Containers should only ever be element nodes. We do not
284 // want to register events to document fragments or documents
285 // with the modern plugin event system.
286 invariant(
packages/react-dom/src/client/ReactDOMRoot.js
+17 -4
@@ -42,6 +42,7 @@ import {
42 DOCUMENT_NODE,
43 DOCUMENT_FRAGMENT_NODE,
44 } from '../shared/HTMLNodeType';
45 +import {ensureListeningTo} from './ReactDOMComponent';
46
47 import {
48 createContainer,
@@ -54,6 +55,8 @@ import {
55 LegacyRoot,
56 } from 'react-reconciler/src/ReactRootTags';
57
58 +import {enableModernEventSystem} from 'shared/ReactFeatureFlags';
59 +
60 function ReactDOMRoot(container: Container, options: void | RootOptions) {
61 this._internalRoot = createRootImpl(container, ConcurrentRoot, options);
62 }
@@ -123,12 +126,22 @@ function createRootImpl(
126 (options != null && options.hydrationOptions) || null;
127 const root = createContainer(container, tag, hydrate, hydrationCallbacks);
128 markContainerAsRoot(root.current, container);
129 + const containerNodeType = container.nodeType;
130 +
131 if (hydrate && tag !== LegacyRoot) {
132 const doc =
128 - container.nodeType === DOCUMENT_NODE
129 - ? container
130 - : container.ownerDocument;
131 - eagerlyTrapReplayableEvents(container, doc);
133 + containerNodeType === DOCUMENT_NODE ? container : container.ownerDocument;
134 + // We need to cast this because Flow doesn't work
135 + // with the hoisted containerNodeType. If we inline
136 + // it, then Flow doesn't complain. We intentionally
137 + // hoist it to reduce code-size.
138 + eagerlyTrapReplayableEvents(container, ((doc: any): Document));
139 + } else if (
140 + enableModernEventSystem &&
141 + containerNodeType !== DOCUMENT_FRAGMENT_NODE &&
142 + containerNodeType !== DOCUMENT_NODE
143 + ) {
144 + ensureListeningTo(container, 'onMouseEnter');
145 }
146 return root;
147 }
packages/react-dom/src/events/DOMModernPluginEventSystem.js
+2 -2
@@ -677,7 +677,7 @@ function accumulateEnterLeaveListenersForEvent(
677 export function accumulateEnterLeaveListeners(
678 dispatchQueue: DispatchQueue,
679 leaveEvent: ReactSyntheticEvent,
680 - enterEvent: ReactSyntheticEvent,
680 + enterEvent: null | ReactSyntheticEvent,
681 from: Fiber | null,
682 to: Fiber | null,
683 ): void {
@@ -692,7 +692,7 @@ export function accumulateEnterLeaveListeners(
692 false,
693 );
694 }
695 - if (to !== null) {
695 + if (to !== null && enterEvent !== null) {
696 accumulateEnterLeaveListenersForEvent(
697 dispatchQueue,
698 enterEvent,
packages/react-dom/src/events/plugins/ModernEnterLeaveEventPlugin.js
+19 -14
@@ -65,18 +65,16 @@ const EnterLeaveEventPlugin = {
65 topLevelType === TOP_MOUSE_OVER || topLevelType === TOP_POINTER_OVER;
66 const isOutEvent =
67 topLevelType === TOP_MOUSE_OUT || topLevelType === TOP_POINTER_OUT;
68 -
69 - if (isOverEvent && (eventSystemFlags & IS_REPLAYED) === 0) {
70 - const related = nativeEvent.relatedTarget || nativeEvent.fromElement;
71 - if (related) {
72 - // Due to the fact we don't add listeners to the document with the
73 - // modern event system and instead attach listeners to roots, we
74 - // need to handle the over event case. To ensure this, we just need to
75 - // make sure the node that we're coming from is managed by React.
76 - const inst = getClosestInstanceFromNode(related);
77 - if (inst !== null) {
78 - return;
79 - }
68 + const related = nativeEvent.relatedTarget || nativeEvent.fromElement;
69 +
70 + if (isOverEvent && (eventSystemFlags & IS_REPLAYED) === 0 && related) {
71 + // Due to the fact we don't add listeners to the document with the
72 + // modern event system and instead attach listeners to roots, we
73 + // need to handle the over event case. To ensure this, we just need to
74 + // make sure the node that we're coming from is managed by React.
75 + const inst = getClosestInstanceFromNode(related);
76 + if (inst !== null) {
77 + return;
78 }
79 }
80
@@ -103,7 +101,6 @@ const EnterLeaveEventPlugin = {
101 let to;
102 if (isOutEvent) {
103 from = targetInst;
106 - const related = nativeEvent.relatedTarget || nativeEvent.toElement;
104 to = related ? getClosestInstanceFromNode(related) : null;
105 if (to !== null) {
106 const nearestMounted = getNearestMountedFiber(to);
@@ -155,7 +152,7 @@ const EnterLeaveEventPlugin = {
152 leave.target = fromNode;
153 leave.relatedTarget = toNode;
154
158 - const enter = eventInterface.getPooled(
155 + let enter = eventInterface.getPooled(
156 enterEventType,
157 to,
158 nativeEvent,
@@ -165,6 +162,14 @@ const EnterLeaveEventPlugin = {
162 enter.target = toNode;
163 enter.relatedTarget = fromNode;
164
165 + // If we are not processing the first ancestor, then we
166 + // should not process the same nativeEvent again, as we
167 + // will have already processed it in the first ancestor.
168 + const nativeTargetInst = getClosestInstanceFromNode(nativeEventTarget);
169 + if (nativeTargetInst !== targetInst) {
170 + enter = null;
171 + }
172 +
173 accumulateEnterLeaveListeners(dispatchQueue, leave, enter, from, to);
174 },
175 };
packages/react-dom/src/events/plugins/__tests__/ModernEnterLeaveEventPlugin-test.internal.js
+31 -1
@@ -240,7 +240,7 @@ describe('EnterLeaveEventPlugin', () => {
240 ReactDOM.render(<Parent />, container);
241 });
242
243 - it('should work with portals outside of the root', () => {
243 + it('should work with portals outside of the root that has onMouseLeave', () => {
244 const divRef = React.createRef();
245 const onMouseLeave = jest.fn();
246
@@ -265,4 +265,34 @@ describe('EnterLeaveEventPlugin', () => {
265
266 expect(onMouseLeave).toHaveBeenCalledTimes(1);
267 });
268 +
269 + it('should work with portals that have onMouseEnter outside of the root ', () => {
270 + const divRef = React.createRef();
271 + const otherDivRef = React.createRef();
272 + const onMouseEnter = jest.fn();
273 +
274 + function Component() {
275 + return (
276 + <div ref={divRef}>
277 + {ReactDOM.createPortal(
278 + <div ref={otherDivRef} onMouseEnter={onMouseEnter} />,
279 + document.body,
280 + )}
281 + </div>
282 + );
283 + }
284 +
285 + ReactDOM.render(<Component />, container);
286 +
287 + // Leave from the portal div
288 + divRef.current.dispatchEvent(
289 + new MouseEvent('mouseout', {
290 + bubbles: true,
291 + cancelable: true,
292 + relatedTarget: otherDivRef.current,
293 + }),
294 + );
295 +
296 + expect(onMouseEnter).toHaveBeenCalledTimes(1);
297 + });
298 });