diff --git a/packages/react-dom/src/events/DOMModernPluginEventSystem.js b/packages/react-dom/src/events/DOMModernPluginEventSystem.js
index 32c56b15c6..daea82b498 100644
--- a/packages/react-dom/src/events/DOMModernPluginEventSystem.js
+++ b/packages/react-dom/src/events/DOMModernPluginEventSystem.js
@@ -19,6 +19,8 @@ import {batchedEventUpdates} from 'legacy-events/ReactGenericBatching';
import {executeDispatchesInOrder} from 'legacy-events/EventPluginUtils';
import {plugins} from 'legacy-events/EventPluginRegistry';
+import {HostRoot, HostPortal} from 'shared/ReactWorkTags';
+
import {trapEventForPluginEventSystem} from './ReactDOMEventListener';
import getEventTarget from './getEventTarget';
import {getListenerMapForElement} from './DOMEventListenerMap';
@@ -56,7 +58,8 @@ import {
TOP_PROGRESS,
TOP_PLAYING,
} from './DOMTopLevelEventTypes';
-import {DOCUMENT_NODE} from '../shared/HTMLNodeType';
+import {getClosestInstanceFromNode} from '../client/ReactDOMComponentTree';
+import {DOCUMENT_NODE, COMMENT_NODE} from '../shared/HTMLNodeType';
import {enableLegacyFBPrimerSupport} from 'shared/ReactFeatureFlags';
@@ -206,6 +209,17 @@ function willDeferLaterForFBLegacyPrimer(nativeEvent: any): boolean {
return false;
}
+function isMatchingRootContainer(
+ grandContainer: Element,
+ rootContainer: Document | Element,
+): boolean {
+ return (
+ grandContainer === rootContainer ||
+ (grandContainer.nodeType === COMMENT_NODE &&
+ grandContainer.parentNode === rootContainer)
+ );
+}
+
export function dispatchEventForPluginEventSystem(
topLevelType: DOMTopLevelEventType,
eventSystemFlags: EventSystemFlags,
@@ -224,6 +238,56 @@ export function dispatchEventForPluginEventSystem(
) {
return;
}
+ // The below logic attempts to work out if we need to change
+ // the target fiber to a different ancestor. We had similar logic
+ // in the legacy event system, except the big difference between
+ // systems is that the modern event system now has an event listener
+ // attached to each React Root and React Portal Root. Together,
+ // the DOM nodes representing these roots are the "rootContainer".
+ // To figure out which ancestor instance we should use, we traverse
+ // up the fiber tree from the target instance and attempt to find
+ // root boundaries that match that of our current "rootContainer".
+ // If we find that "rootContainer", we find the parent fiber
+ // sub-tree for that root and make that our ancestor instance.
+ let node = targetInst;
+
+ while (true) {
+ if (node === null) {
+ return;
+ }
+ if (node.tag === HostRoot || node.tag === HostPortal) {
+ const container = node.stateNode.containerInfo;
+ if (isMatchingRootContainer(container, rootContainer)) {
+ break;
+ }
+ if (node.tag === HostPortal) {
+ // The target is a portal, but it's not the rootContainer we're looking for.
+ // Normally portals handle their own events all the way down to the root.
+ // So we should be able to stop now. However, we don't know if this portal
+ // was part of *our* root.
+ let grandNode = node.return;
+ while (grandNode !== null) {
+ if (grandNode.tag === HostRoot || grandNode.tag === HostPortal) {
+ const grandContainer = grandNode.stateNode.containerInfo;
+ if (isMatchingRootContainer(grandContainer, rootContainer)) {
+ // This is the rootContainer we're looking for and we found it as
+ // a parent of the Portal. That means we can ignore it because the
+ // Portal will bubble through to us.
+ return;
+ }
+ }
+ grandNode = grandNode.return;
+ }
+ }
+ const parentSubtreeInst = getClosestInstanceFromNode(container);
+ if (parentSubtreeInst === null) {
+ return;
+ }
+ node = ancestorInst = parentSubtreeInst;
+ continue;
+ }
+ node = node.return;
+ }
}
batchedEventUpdates(() =>
diff --git a/packages/react-dom/src/events/__tests__/DOMModernPluginEventSystem-test.internal.js b/packages/react-dom/src/events/__tests__/DOMModernPluginEventSystem-test.internal.js
index 009b74ef69..31ab9e26d5 100644
--- a/packages/react-dom/src/events/__tests__/DOMModernPluginEventSystem-test.internal.js
+++ b/packages/react-dom/src/events/__tests__/DOMModernPluginEventSystem-test.internal.js
@@ -12,6 +12,8 @@
let React;
let ReactFeatureFlags;
let ReactDOM;
+let ReactDOMServer;
+let Scheduler;
function dispatchClickEvent(element) {
const event = document.createEvent('Event');
@@ -29,6 +31,8 @@ describe('DOMModernPluginEventSystem', () => {
React = require('react');
ReactDOM = require('react-dom');
+ Scheduler = require('scheduler');
+ ReactDOMServer = require('react-dom/server');
container = document.createElement('div');
document.body.appendChild(container);
});
@@ -77,6 +81,500 @@ describe('DOMModernPluginEventSystem', () => {
expect(log[5]).toEqual(['bubble', buttonElement]);
});
+ it('handle propagation of click events between roots', () => {
+ const buttonRef = React.createRef();
+ const divRef = React.createRef();
+ const childRef = React.createRef();
+ const log = [];
+ const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
+ const onClickCapture = jest.fn(e => log.push(['capture', e.currentTarget]));
+
+ function Child() {
+ return (
+
+ Click me!
+
+ );
+ }
+
+ function Parent() {
+ return (
+
+
+
+ );
+ }
+
+ ReactDOM.render( , container);
+ ReactDOM.render( , childRef.current);
+
+ let buttonElement = buttonRef.current;
+ dispatchClickEvent(buttonElement);
+ expect(onClick).toHaveBeenCalledTimes(1);
+ expect(onClickCapture).toHaveBeenCalledTimes(1);
+ expect(log[0]).toEqual(['capture', buttonElement]);
+ expect(log[1]).toEqual(['bubble', buttonElement]);
+
+ let divElement = divRef.current;
+ dispatchClickEvent(divElement);
+ expect(onClick).toHaveBeenCalledTimes(3);
+ expect(onClickCapture).toHaveBeenCalledTimes(3);
+ expect(log[2]).toEqual(['capture', divElement]);
+ expect(log[3]).toEqual(['bubble', divElement]);
+ expect(log[4]).toEqual(['capture', buttonElement]);
+ expect(log[5]).toEqual(['bubble', buttonElement]);
+ });
+
+ it('handle propagation of click events between disjointed roots', () => {
+ const buttonRef = React.createRef();
+ const divRef = React.createRef();
+ const log = [];
+ const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
+ const onClickCapture = jest.fn(e => log.push(['capture', e.currentTarget]));
+
+ function Child() {
+ return (
+
+ Click me!
+
+ );
+ }
+
+ function Parent() {
+ return (
+
+ );
+ }
+
+ const disjointedNode = document.createElement('div');
+ ReactDOM.render( , container);
+ buttonRef.current.appendChild(disjointedNode);
+ ReactDOM.render( , disjointedNode);
+
+ let buttonElement = buttonRef.current;
+ dispatchClickEvent(buttonElement);
+ expect(onClick).toHaveBeenCalledTimes(1);
+ expect(onClickCapture).toHaveBeenCalledTimes(1);
+ expect(log[0]).toEqual(['capture', buttonElement]);
+ expect(log[1]).toEqual(['bubble', buttonElement]);
+
+ let divElement = divRef.current;
+ dispatchClickEvent(divElement);
+ expect(onClick).toHaveBeenCalledTimes(3);
+ expect(onClickCapture).toHaveBeenCalledTimes(3);
+ expect(log[2]).toEqual(['capture', divElement]);
+ expect(log[3]).toEqual(['bubble', divElement]);
+ expect(log[4]).toEqual(['capture', buttonElement]);
+ expect(log[5]).toEqual(['bubble', buttonElement]);
+ });
+
+ it('handle propagation of click events between disjointed comment roots', () => {
+ const buttonRef = React.createRef();
+ const divRef = React.createRef();
+ const log = [];
+ const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
+ const onClickCapture = jest.fn(e => log.push(['capture', e.currentTarget]));
+
+ function Child() {
+ return (
+
+ Click me!
+
+ );
+ }
+
+ function Parent() {
+ return (
+
+ );
+ }
+
+ // We use a comment node here, then mount to it
+ const disjointedNode = document.createComment(
+ ' react-mount-point-unstable ',
+ );
+ ReactDOM.render( , container);
+ buttonRef.current.appendChild(disjointedNode);
+ ReactDOM.render( , disjointedNode);
+
+ let buttonElement = buttonRef.current;
+ dispatchClickEvent(buttonElement);
+ expect(onClick).toHaveBeenCalledTimes(1);
+ expect(onClickCapture).toHaveBeenCalledTimes(1);
+ expect(log[0]).toEqual(['capture', buttonElement]);
+ expect(log[1]).toEqual(['bubble', buttonElement]);
+
+ let divElement = divRef.current;
+ dispatchClickEvent(divElement);
+ expect(onClick).toHaveBeenCalledTimes(3);
+ expect(onClickCapture).toHaveBeenCalledTimes(3);
+ expect(log[2]).toEqual(['capture', divElement]);
+ expect(log[3]).toEqual(['bubble', divElement]);
+ expect(log[4]).toEqual(['capture', buttonElement]);
+ expect(log[5]).toEqual(['bubble', buttonElement]);
+ });
+
+ it('handle propagation of click events between disjointed comment roots #2', () => {
+ const buttonRef = React.createRef();
+ const divRef = React.createRef();
+ const spanRef = React.createRef();
+ const log = [];
+ const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
+ const onClickCapture = jest.fn(e => log.push(['capture', e.currentTarget]));
+
+ function Child() {
+ return (
+
+ Click me!
+
+ );
+ }
+
+ function Parent() {
+ return (
+
+
+
+ );
+ }
+
+ // We use a comment node here, then mount to it
+ const disjointedNode = document.createComment(
+ ' react-mount-point-unstable ',
+ );
+ ReactDOM.render( , container);
+ spanRef.current.appendChild(disjointedNode);
+ ReactDOM.render( , disjointedNode);
+
+ let buttonElement = buttonRef.current;
+ dispatchClickEvent(buttonElement);
+ expect(onClick).toHaveBeenCalledTimes(1);
+ expect(onClickCapture).toHaveBeenCalledTimes(1);
+ expect(log[0]).toEqual(['capture', buttonElement]);
+ expect(log[1]).toEqual(['bubble', buttonElement]);
+
+ let divElement = divRef.current;
+ dispatchClickEvent(divElement);
+ expect(onClick).toHaveBeenCalledTimes(3);
+ expect(onClickCapture).toHaveBeenCalledTimes(3);
+ expect(log[2]).toEqual(['capture', divElement]);
+ expect(log[3]).toEqual(['bubble', divElement]);
+ expect(log[4]).toEqual(['capture', buttonElement]);
+ expect(log[5]).toEqual(['bubble', buttonElement]);
+ });
+
+ it('handle propagation of click events between portals', () => {
+ const buttonRef = React.createRef();
+ const divRef = React.createRef();
+ const log = [];
+ const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
+ const onClickCapture = jest.fn(e => log.push(['capture', e.currentTarget]));
+
+ const portalElement = document.createElement('div');
+ document.body.appendChild(portalElement);
+
+ function Child() {
+ return (
+
+ Click me!
+
+ );
+ }
+
+ function Parent() {
+ return (
+
+ {ReactDOM.createPortal( , portalElement)}
+
+ );
+ }
+
+ ReactDOM.render( , container);
+
+ let buttonElement = buttonRef.current;
+ dispatchClickEvent(buttonElement);
+ expect(onClick).toHaveBeenCalledTimes(1);
+ expect(onClickCapture).toHaveBeenCalledTimes(1);
+ expect(log[0]).toEqual(['capture', buttonElement]);
+ expect(log[1]).toEqual(['bubble', buttonElement]);
+
+ let divElement = divRef.current;
+ dispatchClickEvent(divElement);
+ expect(onClick).toHaveBeenCalledTimes(3);
+ expect(onClickCapture).toHaveBeenCalledTimes(3);
+ expect(log[2]).toEqual(['capture', buttonElement]);
+ expect(log[3]).toEqual(['capture', divElement]);
+ expect(log[4]).toEqual(['bubble', divElement]);
+ expect(log[5]).toEqual(['bubble', buttonElement]);
+
+ document.body.removeChild(portalElement);
+ });
+
+ it('handle click events on document.body portals', () => {
+ const log = [];
+
+ function Child({label}) {
+ return log.push(label)}>{label}
;
+ }
+
+ function Parent() {
+ return (
+ <>
+ {ReactDOM.createPortal( , document.body)}
+ {ReactDOM.createPortal( , document.body)}
+ >
+ );
+ }
+
+ ReactDOM.render( , container);
+
+ const second = document.body.lastChild;
+ expect(second.textContent).toEqual('second');
+ dispatchClickEvent(second);
+
+ expect(log).toEqual(['second']);
+
+ const first = second.previousSibling;
+ expect(first.textContent).toEqual('first');
+ dispatchClickEvent(first);
+
+ expect(log).toEqual(['second', 'first']);
+ });
+
+ it.experimental(
+ 'does not invoke an event on a parent tree when a subtree is dehydrated',
+ async () => {
+ let suspend = false;
+ let resolve;
+ let promise = new Promise(resolvePromise => (resolve = resolvePromise));
+
+ let clicks = 0;
+ let childSlotRef = React.createRef();
+
+ function Parent() {
+ return clicks++} ref={childSlotRef} />;
+ }
+
+ function Child({text}) {
+ if (suspend) {
+ throw promise;
+ } else {
+ return
Click me ;
+ }
+ }
+
+ function App() {
+ // The root is a Suspense boundary.
+ return (
+
+
+
+ );
+ }
+
+ suspend = false;
+ let finalHTML = ReactDOMServer.renderToString(
);
+
+ let parentContainer = document.createElement('div');
+ let childContainer = document.createElement('div');
+
+ // We need this to be in the document since we'll dispatch events on it.
+ document.body.appendChild(parentContainer);
+
+ // We're going to use a different root as a parent.
+ // This lets us detect whether an event goes through React's event system.
+ let parentRoot = ReactDOM.createRoot(parentContainer);
+ parentRoot.render(
);
+ Scheduler.unstable_flushAll();
+
+ childSlotRef.current.appendChild(childContainer);
+
+ childContainer.innerHTML = finalHTML;
+
+ let a = childContainer.getElementsByTagName('a')[0];
+
+ suspend = true;
+
+ // Hydrate asynchronously.
+ let root = ReactDOM.createRoot(childContainer, {hydrate: true});
+ root.render(
);
+ jest.runAllTimers();
+ Scheduler.unstable_flushAll();
+
+ // The Suspense boundary is not yet hydrated.
+ a.click();
+ expect(clicks).toBe(0);
+
+ // Resolving the promise so that rendering can complete.
+ suspend = false;
+ resolve();
+ await promise;
+
+ Scheduler.unstable_flushAll();
+ jest.runAllTimers();
+
+ // We're now full hydrated.
+
+ expect(clicks).toBe(1);
+
+ document.body.removeChild(parentContainer);
+ },
+ );
+
+ it('handle click events on dynamic portals', () => {
+ const log = [];
+
+ function Parent() {
+ const ref = React.useRef(null);
+ const [portal, setPortal] = React.useState(null);
+
+ React.useEffect(() => {
+ setPortal(
+ ReactDOM.createPortal(
+
log.push('child')} id="child" />,
+ ref.current,
+ ),
+ );
+ });
+
+ return (
+ log.push('parent')} id="parent">
+ {portal}
+
+ );
+ }
+
+ ReactDOM.render( , container);
+
+ const parent = container.lastChild;
+ expect(parent.id).toEqual('parent');
+ dispatchClickEvent(parent);
+
+ expect(log).toEqual(['parent']);
+
+ const child = parent.lastChild;
+ expect(child.id).toEqual('child');
+ dispatchClickEvent(child);
+
+ // we add both 'child' and 'parent' due to bubbling
+ expect(log).toEqual(['parent', 'child', 'parent']);
+ });
+
+ // Slight alteration to the last test, to catch
+ // a subtle difference in traversal.
+ it('handle click events on dynamic portals #2', () => {
+ const log = [];
+
+ function Parent() {
+ const ref = React.useRef(null);
+ const [portal, setPortal] = React.useState(null);
+
+ React.useEffect(() => {
+ setPortal(
+ ReactDOM.createPortal(
+ log.push('child')} id="child" />,
+ ref.current,
+ ),
+ );
+ });
+
+ return (
+ log.push('parent')} id="parent">
+
{portal}
+
+ );
+ }
+
+ ReactDOM.render( , container);
+
+ const parent = container.lastChild;
+ expect(parent.id).toEqual('parent');
+ dispatchClickEvent(parent);
+
+ expect(log).toEqual(['parent']);
+
+ const child = parent.lastChild;
+ expect(child.id).toEqual('child');
+ dispatchClickEvent(child);
+
+ // we add both 'child' and 'parent' due to bubbling
+ expect(log).toEqual(['parent', 'child', 'parent']);
+ });
+
+ it('native stopPropagation on click events between portals', () => {
+ const buttonRef = React.createRef();
+ const divRef = React.createRef();
+ const middelDivRef = React.createRef();
+ const log = [];
+ const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
+ const onClickCapture = jest.fn(e => log.push(['capture', e.currentTarget]));
+
+ const portalElement = document.createElement('div');
+ document.body.appendChild(portalElement);
+
+ function Child() {
+ return (
+
+ );
+ }
+
+ function Parent() {
+ React.useLayoutEffect(() => {
+ // This should prevent the portalElement listeners from
+ // capturing the events in the bubble phase.
+ middelDivRef.current.addEventListener('click', e => {
+ e.stopPropagation();
+ });
+ });
+
+ return (
+
+ {ReactDOM.createPortal( , portalElement)}
+
+ );
+ }
+
+ ReactDOM.render( , container);
+
+ let buttonElement = buttonRef.current;
+ dispatchClickEvent(buttonElement);
+ expect(onClick).toHaveBeenCalledTimes(1);
+ expect(onClickCapture).toHaveBeenCalledTimes(1);
+ expect(log[0]).toEqual(['capture', buttonElement]);
+ expect(log[1]).toEqual(['bubble', buttonElement]);
+
+ let divElement = divRef.current;
+ dispatchClickEvent(divElement);
+ expect(onClick).toHaveBeenCalledTimes(1);
+ expect(onClickCapture).toHaveBeenCalledTimes(1);
+
+ document.body.removeChild(portalElement);
+ });
+
it('handle propagation of focus events', () => {
const buttonRef = React.createRef();
const divRef = React.createRef();
@@ -120,6 +618,377 @@ describe('DOMModernPluginEventSystem', () => {
expect(log[5]).toEqual(['bubble', buttonElement]);
});
+ it('handle propagation of focus events between roots', () => {
+ const buttonRef = React.createRef();
+ const divRef = React.createRef();
+ const childRef = React.createRef();
+ const log = [];
+ const onFocus = jest.fn(e => log.push(['bubble', e.currentTarget]));
+ const onFocusCapture = jest.fn(e => log.push(['capture', e.currentTarget]));
+
+ function Child() {
+ return (
+
+ Click me!
+
+ );
+ }
+
+ function Parent() {
+ return (
+
+
+
+ );
+ }
+
+ ReactDOM.render( , container);
+ ReactDOM.render( , childRef.current);
+
+ let buttonElement = buttonRef.current;
+ buttonElement.focus();
+ expect(onFocus).toHaveBeenCalledTimes(1);
+ expect(onFocusCapture).toHaveBeenCalledTimes(1);
+ expect(log[0]).toEqual(['capture', buttonElement]);
+ expect(log[1]).toEqual(['bubble', buttonElement]);
+
+ let divElement = divRef.current;
+ divElement.focus();
+ expect(onFocus).toHaveBeenCalledTimes(3);
+ expect(onFocusCapture).toHaveBeenCalledTimes(3);
+ expect(log[2]).toEqual(['capture', buttonElement]);
+ expect(log[3]).toEqual(['bubble', buttonElement]);
+ expect(log[4]).toEqual(['capture', divElement]);
+ expect(log[5]).toEqual(['bubble', divElement]);
+ });
+
+ it('handle propagation of focus events between portals', () => {
+ const buttonRef = React.createRef();
+ const divRef = React.createRef();
+ const log = [];
+ const onFocus = jest.fn(e => log.push(['bubble', e.currentTarget]));
+ const onFocusCapture = jest.fn(e => log.push(['capture', e.currentTarget]));
+
+ const portalElement = document.createElement('div');
+ document.body.appendChild(portalElement);
+
+ function Child() {
+ return (
+
+ Click me!
+
+ );
+ }
+
+ function Parent() {
+ return (
+
+ {ReactDOM.createPortal( , portalElement)}
+
+ );
+ }
+
+ ReactDOM.render( , container);
+
+ let buttonElement = buttonRef.current;
+ buttonElement.focus();
+ expect(onFocus).toHaveBeenCalledTimes(1);
+ expect(onFocusCapture).toHaveBeenCalledTimes(1);
+ expect(log[0]).toEqual(['capture', buttonElement]);
+ expect(log[1]).toEqual(['bubble', buttonElement]);
+
+ let divElement = divRef.current;
+ divElement.focus();
+ expect(onFocus).toHaveBeenCalledTimes(3);
+ expect(onFocusCapture).toHaveBeenCalledTimes(3);
+ expect(log[2]).toEqual(['capture', buttonElement]);
+ expect(log[3]).toEqual(['capture', divElement]);
+ expect(log[4]).toEqual(['bubble', divElement]);
+ expect(log[5]).toEqual(['bubble', buttonElement]);
+
+ document.body.removeChild(portalElement);
+ });
+
+ it('native stopPropagation on focus events between portals', () => {
+ const buttonRef = React.createRef();
+ const divRef = React.createRef();
+ const middelDivRef = React.createRef();
+ const log = [];
+ const onFocus = jest.fn(e => log.push(['bubble', e.currentTarget]));
+ const onFocusCapture = jest.fn(e => log.push(['capture', e.currentTarget]));
+
+ const portalElement = document.createElement('div');
+ document.body.appendChild(portalElement);
+
+ function Child() {
+ return (
+
+ );
+ }
+
+ function Parent() {
+ React.useLayoutEffect(() => {
+ // This should prevent the portalElement listeners from
+ // capturing the events in the bubble phase.
+ middelDivRef.current.addEventListener('click', e => {
+ e.stopPropagation();
+ });
+ });
+
+ return (
+
+ {ReactDOM.createPortal( , portalElement)}
+
+ );
+ }
+
+ ReactDOM.render( , container);
+
+ let buttonElement = buttonRef.current;
+ buttonElement.focus();
+ expect(onFocus).toHaveBeenCalledTimes(1);
+ expect(onFocusCapture).toHaveBeenCalledTimes(1);
+ expect(log[0]).toEqual(['capture', buttonElement]);
+ expect(log[1]).toEqual(['bubble', buttonElement]);
+
+ let divElement = divRef.current;
+ divElement.focus();
+ expect(onFocus).toHaveBeenCalledTimes(1);
+ expect(onFocusCapture).toHaveBeenCalledTimes(1);
+
+ document.body.removeChild(portalElement);
+ });
+
+ it('handle propagation of enter and leave events between portals', () => {
+ const buttonRef = React.createRef();
+ const divRef = React.createRef();
+ const log = [];
+ const onMouseEnter = jest.fn(e => log.push(e.currentTarget));
+ const onMouseLeave = jest.fn(e => log.push(e.currentTarget));
+
+ const portalElement = document.createElement('div');
+ document.body.appendChild(portalElement);
+
+ function Child() {
+ return (
+
+ );
+ }
+
+ function Parent() {
+ return (
+
+ {ReactDOM.createPortal( , portalElement)}
+
+ );
+ }
+
+ ReactDOM.render( , container);
+
+ let buttonElement = buttonRef.current;
+ buttonElement.dispatchEvent(
+ new MouseEvent('mouseover', {
+ bubbles: true,
+ cancelable: true,
+ relatedTarget: null,
+ }),
+ );
+ expect(onMouseEnter).toHaveBeenCalledTimes(1);
+ expect(onMouseLeave).toHaveBeenCalledTimes(0);
+ expect(log[0]).toEqual(buttonElement);
+
+ let divElement = divRef.current;
+ buttonElement.dispatchEvent(
+ new MouseEvent('mouseout', {
+ bubbles: true,
+ cancelable: true,
+ relatedTarget: divElement,
+ }),
+ );
+ divElement.dispatchEvent(
+ new MouseEvent('mouseover', {
+ bubbles: true,
+ cancelable: true,
+ relatedTarget: buttonElement,
+ }),
+ );
+ expect(onMouseEnter).toHaveBeenCalledTimes(2);
+ expect(onMouseLeave).toHaveBeenCalledTimes(0);
+ expect(log[1]).toEqual(divElement);
+
+ document.body.removeChild(portalElement);
+ });
+
+ it('handle propagation of enter and leave events between portals #2', () => {
+ const buttonRef = React.createRef();
+ const divRef = React.createRef();
+ const portalRef = React.createRef();
+ const log = [];
+ const onMouseEnter = jest.fn(e => log.push(e.currentTarget));
+ const onMouseLeave = jest.fn(e => log.push(e.currentTarget));
+
+ function Child() {
+ return (
+
+ );
+ }
+
+ function Parent() {
+ const [portal, setPortal] = React.useState(null);
+
+ React.useLayoutEffect(() => {
+ setPortal(ReactDOM.createPortal( , portalRef.current));
+ }, []);
+
+ return (
+
+ {portal}
+
+ );
+ }
+
+ ReactDOM.render( , container);
+
+ let buttonElement = buttonRef.current;
+ buttonElement.dispatchEvent(
+ new MouseEvent('mouseover', {
+ bubbles: true,
+ cancelable: true,
+ relatedTarget: null,
+ }),
+ );
+ expect(onMouseEnter).toHaveBeenCalledTimes(1);
+ expect(onMouseLeave).toHaveBeenCalledTimes(0);
+ expect(log[0]).toEqual(buttonElement);
+
+ let divElement = divRef.current;
+ buttonElement.dispatchEvent(
+ new MouseEvent('mouseout', {
+ bubbles: true,
+ cancelable: true,
+ relatedTarget: divElement,
+ }),
+ );
+ divElement.dispatchEvent(
+ new MouseEvent('mouseover', {
+ bubbles: true,
+ cancelable: true,
+ relatedTarget: buttonElement,
+ }),
+ );
+ expect(onMouseEnter).toHaveBeenCalledTimes(2);
+ expect(onMouseLeave).toHaveBeenCalledTimes(0);
+ expect(log[1]).toEqual(divElement);
+ });
+
+ it('should preserve bubble/capture order between roots and nested portals', () => {
+ const targetRef = React.createRef();
+ let log = [];
+ const onClickRoot = jest.fn(e => log.push('bubble root'));
+ const onClickCaptureRoot = jest.fn(e => log.push('capture root'));
+ const onClickPortal = jest.fn(e => log.push('bubble portal'));
+ const onClickCapturePortal = jest.fn(e => log.push('capture portal'));
+
+ function Portal() {
+ return (
+
+ Click me!
+
+ );
+ }
+
+ const portalContainer = document.createElement('div');
+
+ let shouldStopPropagation = false;
+ portalContainer.addEventListener(
+ 'click',
+ e => {
+ if (shouldStopPropagation) {
+ e.stopPropagation();
+ }
+ },
+ false,
+ );
+
+ function Root() {
+ let portalTargetRef = React.useRef(null);
+ React.useLayoutEffect(() => {
+ portalTargetRef.current.appendChild(portalContainer);
+ });
+ return (
+
+
+ {ReactDOM.createPortal(
, portalContainer)}
+
+ );
+ }
+
+ ReactDOM.render( , container);
+
+ let divElement = targetRef.current;
+ dispatchClickEvent(divElement);
+ expect(log).toEqual([
+ 'capture root',
+ 'capture portal',
+ 'bubble portal',
+ 'bubble root',
+ ]);
+
+ log = [];
+
+ shouldStopPropagation = true;
+ dispatchClickEvent(divElement);
+ expect(log).toEqual([
+ // The events on root probably shouldn't fire if a non-React intermediated. but current behavior is that they do.
+ 'capture root',
+ 'capture portal',
+ 'bubble portal',
+ 'bubble root',
+ ]);
+ });
+
it('handle propagation of click events correctly with FB primer', () => {
ReactFeatureFlags.enableLegacyFBPrimerSupport = true;
const aRef = React.createRef();
@@ -164,8 +1033,9 @@ describe('DOMModernPluginEventSystem', () => {
);
}
ReactDOM.render( , container);
+ aElement = aRef.current;
dispatchClickEvent(aElement);
- expect(onClick).toHaveBeenCalledTimes(1);
+ expect(onClick).toHaveBeenCalledTimes(2);
expect(log).toEqual([]);
expect(onDivClick).toHaveBeenCalledTimes(0);
});