Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 823dc58

Browse files
authored
Modern Event System: fix EnterLeave plugin logic (facebook#18830)
1 parent 40e6029 commit 823dc58

File tree

5 files changed

+71
-23
lines changed

5 files changed

+71
-23
lines changed

packages/react-dom/src/client/ReactDOMComponent.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ if (__DEV__) {
269269
};
270270
}
271271

272-
function ensureListeningTo(
272+
export function ensureListeningTo(
273273
rootContainerInstance: Element | Node,
274274
registrationName: string,
275275
): void {
@@ -280,7 +280,7 @@ function ensureListeningTo(
280280
rootContainerInstance.nodeType === COMMENT_NODE
281281
? rootContainerInstance.parentNode
282282
: rootContainerInstance;
283-
// Containers can only ever be element nodes. We do not
283+
// Containers should only ever be element nodes. We do not
284284
// want to register events to document fragments or documents
285285
// with the modern plugin event system.
286286
invariant(

packages/react-dom/src/client/ReactDOMRoot.js

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import {
4242
DOCUMENT_NODE,
4343
DOCUMENT_FRAGMENT_NODE,
4444
} from '../shared/HTMLNodeType';
45+
import {ensureListeningTo} from './ReactDOMComponent';
4546

4647
import {
4748
createContainer,
@@ -54,6 +55,8 @@ import {
5455
LegacyRoot,
5556
} from 'react-reconciler/src/ReactRootTags';
5657

58+
import {enableModernEventSystem} from 'shared/ReactFeatureFlags';
59+
5760
function ReactDOMRoot(container: Container, options: void | RootOptions) {
5861
this._internalRoot = createRootImpl(container, ConcurrentRoot, options);
5962
}
@@ -123,12 +126,22 @@ function createRootImpl(
123126
(options != null && options.hydrationOptions) || null;
124127
const root = createContainer(container, tag, hydrate, hydrationCallbacks);
125128
markContainerAsRoot(root.current, container);
129+
const containerNodeType = container.nodeType;
130+
126131
if (hydrate && tag !== LegacyRoot) {
127132
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');
132145
}
133146
return root;
134147
}

packages/react-dom/src/events/DOMModernPluginEventSystem.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -677,7 +677,7 @@ function accumulateEnterLeaveListenersForEvent(
677677
export function accumulateEnterLeaveListeners(
678678
dispatchQueue: DispatchQueue,
679679
leaveEvent: ReactSyntheticEvent,
680-
enterEvent: ReactSyntheticEvent,
680+
enterEvent: null | ReactSyntheticEvent,
681681
from: Fiber | null,
682682
to: Fiber | null,
683683
): void {
@@ -692,7 +692,7 @@ export function accumulateEnterLeaveListeners(
692692
false,
693693
);
694694
}
695-
if (to !== null) {
695+
if (to !== null && enterEvent !== null) {
696696
accumulateEnterLeaveListenersForEvent(
697697
dispatchQueue,
698698
enterEvent,

packages/react-dom/src/events/plugins/ModernEnterLeaveEventPlugin.js

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -65,18 +65,16 @@ const EnterLeaveEventPlugin = {
6565
topLevelType === TOP_MOUSE_OVER || topLevelType === TOP_POINTER_OVER;
6666
const isOutEvent =
6767
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;
8078
}
8179
}
8280

@@ -103,7 +101,6 @@ const EnterLeaveEventPlugin = {
103101
let to;
104102
if (isOutEvent) {
105103
from = targetInst;
106-
const related = nativeEvent.relatedTarget || nativeEvent.toElement;
107104
to = related ? getClosestInstanceFromNode(related) : null;
108105
if (to !== null) {
109106
const nearestMounted = getNearestMountedFiber(to);
@@ -155,7 +152,7 @@ const EnterLeaveEventPlugin = {
155152
leave.target = fromNode;
156153
leave.relatedTarget = toNode;
157154

158-
const enter = eventInterface.getPooled(
155+
let enter = eventInterface.getPooled(
159156
enterEventType,
160157
to,
161158
nativeEvent,
@@ -165,6 +162,14 @@ const EnterLeaveEventPlugin = {
165162
enter.target = toNode;
166163
enter.relatedTarget = fromNode;
167164

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+
168173
accumulateEnterLeaveListeners(dispatchQueue, leave, enter, from, to);
169174
},
170175
};

packages/react-dom/src/events/plugins/__tests__/ModernEnterLeaveEventPlugin-test.internal.js

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ describe('EnterLeaveEventPlugin', () => {
240240
ReactDOM.render(<Parent />, container);
241241
});
242242

243-
it('should work with portals outside of the root', () => {
243+
it('should work with portals outside of the root that has onMouseLeave', () => {
244244
const divRef = React.createRef();
245245
const onMouseLeave = jest.fn();
246246

@@ -265,4 +265,34 @@ describe('EnterLeaveEventPlugin', () => {
265265

266266
expect(onMouseLeave).toHaveBeenCalledTimes(1);
267267
});
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+
});
268298
});

0 commit comments

Comments
 (0)