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