diff --git a/packages/react-dom/src/events/DOMModernPluginEventSystem.js b/packages/react-dom/src/events/DOMModernPluginEventSystem.js
index b8c07cc4e91..09fc54e7f5d 100644
--- a/packages/react-dom/src/events/DOMModernPluginEventSystem.js
+++ b/packages/react-dom/src/events/DOMModernPluginEventSystem.js
@@ -56,6 +56,11 @@ import {
TOP_PROGRESS,
TOP_PLAYING,
} from './DOMTopLevelEventTypes';
+import {getClosestInstanceFromNode} from '../client/ReactDOMComponentTree';
+import {DOCUMENT_NODE, COMMENT_NODE} from '../shared/HTMLNodeType';
+
+import {enableLegacyFBPrimerSupport} from 'shared/ReactFeatureFlags';
+import {HostRoot, HostPortal} from 'shared/ReactWorkTags';
const capturePhaseEvents = new Set([
TOP_FOCUS,
@@ -165,6 +170,25 @@ export function listenToEvent(
}
}
+function willDelegateLaterForFBLegacyPrimer(nativeEvent: any): boolean {
+ let node = nativeEvent.target;
+
+ while (node !== null) {
+ if (node.tagName === 'A' && node.rel) {
+ const legacyFBSupport = true;
+ trapEventForPluginEventSystem(
+ document,
+ nativeEvent.type,
+ false,
+ legacyFBSupport,
+ );
+ return true;
+ }
+ node = node.parentNode;
+ }
+ return false;
+}
+
export function dispatchEventForPluginEventSystem(
topLevelType: DOMTopLevelEventType,
eventSystemFlags: EventSystemFlags,
@@ -173,6 +197,38 @@ export function dispatchEventForPluginEventSystem(
rootContainer: Document | Element,
): void {
let ancestorInst = targetInst;
+ if (rootContainer.nodeType !== DOCUMENT_NODE) {
+ // FB only
+ if (
+ enableLegacyFBPrimerSupport &&
+ willDelegateLaterForFBLegacyPrimer(nativeEvent)
+ ) {
+ return;
+ }
+ let node = targetInst;
+
+ while (true) {
+ if (node === null) {
+ return;
+ } else if (node.tag === HostRoot || node.tag === HostPortal) {
+ const container = node.stateNode.containerInfo;
+ if (
+ container === rootContainer ||
+ (container.nodeType === COMMENT_NODE &&
+ container.parentNode === rootContainer)
+ ) {
+ break;
+ }
+ const parentSubtreeInst = getClosestInstanceFromNode(container);
+ if (parentSubtreeInst === null) {
+ return;
+ }
+ node = ancestorInst = parentSubtreeInst;
+ continue;
+ }
+ node = node.return;
+ }
+ }
batchedEventUpdates(() =>
dispatchEventsForPlugins(
diff --git a/packages/react-dom/src/events/ReactDOMEventListener.js b/packages/react-dom/src/events/ReactDOMEventListener.js
index e6913dbeed5..e4feb40b806 100644
--- a/packages/react-dom/src/events/ReactDOMEventListener.js
+++ b/packages/react-dom/src/events/ReactDOMEventListener.js
@@ -65,6 +65,7 @@ import {
import {getEventPriorityForPluginSystem} from './DOMEventProperties';
import {dispatchEventForLegacyPluginEventSystem} from './DOMLegacyEventPluginSystem';
import {dispatchEventForPluginEventSystem} from './DOMModernPluginEventSystem';
+import {enableLegacyFBPrimerSupport} from 'shared/ReactFeatureFlags';
const {
unstable_UserBlockingPriority: UserBlockingPriority,
@@ -143,6 +144,7 @@ export function trapEventForPluginEventSystem(
container: Document | Element,
topLevelType: DOMTopLevelEventType,
capture: boolean,
+ legacyFBSupport?: boolean,
): void {
let listener;
let listenerWrapper;
@@ -164,12 +166,29 @@ export function trapEventForPluginEventSystem(
PLUGIN_EVENT_SYSTEM,
container,
);
-
const rawEventName = getRawEventName(topLevelType);
+ let fbListener;
+ if (enableLegacyFBPrimerSupport && legacyFBSupport) {
+ const originalListener = listener;
+ listener = function(...p) {
+ try {
+ return originalListener.apply(this, p);
+ } finally {
+ if (fbListener) {
+ fbListener.remove();
+ } else {
+ container.removeEventListener(
+ ((rawEventName: any): string),
+ (listener: any),
+ );
+ }
+ }
+ };
+ }
if (capture) {
- addEventCaptureListener(container, rawEventName, listener);
+ fbListener = addEventCaptureListener(container, rawEventName, listener);
} else {
- addEventBubbleListener(container, rawEventName, listener);
+ fbListener = addEventBubbleListener(container, rawEventName, listener);
}
}
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 66c670b7ca3..10711989303 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.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.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();
@@ -119,4 +617,306 @@ describe('DOMModernPluginEventSystem', () => {
expect(log[4]).toEqual(['bubble', divElement]);
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.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.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.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 (
+
+ );
+ }
+
+ 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);
+ });
});
diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js
index 412ec5f9eef..f4e95b44d27 100644
--- a/packages/shared/ReactFeatureFlags.js
+++ b/packages/shared/ReactFeatureFlags.js
@@ -128,3 +128,6 @@ export const warnUnstableRenderSubtreeIntoContainer = false;
// Modern event system where events get registered at roots
export const enableModernEventSystem = false;
+
+// Support legacy Primer support on internal FB www
+export const enableLegacyFBPrimerSupport = false;
diff --git a/packages/shared/ReactTreeTraversal.js b/packages/shared/ReactTreeTraversal.js
index 08a1a3d87c2..5c26ac213bf 100644
--- a/packages/shared/ReactTreeTraversal.js
+++ b/packages/shared/ReactTreeTraversal.js
@@ -5,21 +5,54 @@
* LICENSE file in the root directory of this source tree.
*/
-import {HostComponent} from './ReactWorkTags';
+import type {Fiber} from 'react-reconciler/src/ReactFiber';
-function getParent(inst) {
- do {
- inst = inst.return;
- // TODO: If this is a HostRoot we might want to bail out.
- // That is depending on if we want nested subtrees (layers) to bubble
- // events to their parent. We could also go through parentNode on the
- // host node but that wouldn't work for React Native and doesn't let us
- // do the portal feature.
- } while (inst && inst.tag !== HostComponent);
- if (inst) {
- return inst;
+import {HostComponent, HostPortal, HostRoot} from './ReactWorkTags';
+import {enableModernEventSystem} from './ReactFeatureFlags';
+
+export function getParent(
+ inst: Fiber,
+ alwaysTraversePortals?: boolean,
+): null | Fiber {
+ if (enableModernEventSystem) {
+ let node = inst.return;
+
+ while (node !== null) {
+ if (node.tag === HostPortal && !alwaysTraversePortals) {
+ let grandNode = node;
+ const portalNode = node.stateNode.containerInfo;
+ while (grandNode !== null) {
+ // If we find a root that is actually a parent in the DOM tree
+ // then we don't continue with getting the parent, as that root
+ // will have its own event listener.
+ if (
+ grandNode.tag === HostRoot &&
+ grandNode.stateNode.containerInfo.contains(portalNode)
+ ) {
+ return null;
+ }
+ grandNode = grandNode.return;
+ }
+ } else if (node.tag === HostComponent) {
+ return node;
+ }
+ node = node.return;
+ }
+ return null;
+ } else {
+ do {
+ inst = inst.return;
+ // TODO: If this is a HostRoot we might want to bail out.
+ // That is depending on if we want nested subtrees (layers) to bubble
+ // events to their parent. We could also go through parentNode on the
+ // host node but that wouldn't work for React Native and doesn't let us
+ // do the portal feature.
+ } while (inst && inst.tag !== HostComponent);
+ if (inst) {
+ return inst;
+ }
+ return null;
}
- return null;
}
/**
@@ -28,23 +61,23 @@ function getParent(inst) {
*/
export function getLowestCommonAncestor(instA, instB) {
let depthA = 0;
- for (let tempA = instA; tempA; tempA = getParent(tempA)) {
+ for (let tempA = instA; tempA; tempA = getParent(tempA, true)) {
depthA++;
}
let depthB = 0;
- for (let tempB = instB; tempB; tempB = getParent(tempB)) {
+ for (let tempB = instB; tempB; tempB = getParent(tempB, true)) {
depthB++;
}
// If A is deeper, crawl up.
while (depthA - depthB > 0) {
- instA = getParent(instA);
+ instA = getParent(instA, true);
depthA--;
}
// If B is deeper, crawl up.
while (depthB - depthA > 0) {
- instB = getParent(instB);
+ instB = getParent(instB, true);
depthB--;
}
@@ -54,8 +87,8 @@ export function getLowestCommonAncestor(instA, instB) {
if (instA === instB || instA === instB.alternate) {
return instA;
}
- instA = getParent(instA);
- instB = getParent(instB);
+ instA = getParent(instA, true);
+ instB = getParent(instB, true);
}
return null;
}
@@ -68,7 +101,7 @@ export function isAncestor(instA, instB) {
if (instA === instB || instA === instB.alternate) {
return true;
}
- instB = getParent(instB);
+ instB = getParent(instB, true);
}
return false;
}
@@ -120,7 +153,7 @@ export function traverseEnterLeave(from, to, fn, argFrom, argTo) {
break;
}
pathFrom.push(from);
- from = getParent(from);
+ from = getParent(from, true);
}
const pathTo = [];
while (true) {
@@ -135,7 +168,7 @@ export function traverseEnterLeave(from, to, fn, argFrom, argTo) {
break;
}
pathTo.push(to);
- to = getParent(to);
+ to = getParent(to, true);
}
for (let i = 0; i < pathFrom.length; i++) {
fn(pathFrom[i], 'bubbled', argFrom);
diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js
index bce5af20b23..922074b1454 100644
--- a/packages/shared/forks/ReactFeatureFlags.native-fb.js
+++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js
@@ -44,6 +44,7 @@ export const deferPassiveEffectCleanupDuringUnmount = false;
export const runAllPassiveEffectDestroysBeforeCreates = false;
export const enableModernEventSystem = false;
export const warnAboutSpreadingKeyToJSX = false;
+export const enableLegacyFBPrimerSupport = false;
// Internal-only attempt to debug a React Native issue. See D20130868.
export const throwEarlyForMysteriousError = true;
diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js
index c30e273b75e..e37ca75167d 100644
--- a/packages/shared/forks/ReactFeatureFlags.native-oss.js
+++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js
@@ -43,6 +43,7 @@ export const deferPassiveEffectCleanupDuringUnmount = false;
export const runAllPassiveEffectDestroysBeforeCreates = false;
export const enableModernEventSystem = false;
export const warnAboutSpreadingKeyToJSX = false;
+export const enableLegacyFBPrimerSupport = false;
// Internal-only attempt to debug a React Native issue. See D20130868.
export const throwEarlyForMysteriousError = false;
diff --git a/packages/shared/forks/ReactFeatureFlags.persistent.js b/packages/shared/forks/ReactFeatureFlags.persistent.js
index 5bd1551a9ce..e0a9d877597 100644
--- a/packages/shared/forks/ReactFeatureFlags.persistent.js
+++ b/packages/shared/forks/ReactFeatureFlags.persistent.js
@@ -43,6 +43,7 @@ export const deferPassiveEffectCleanupDuringUnmount = false;
export const runAllPassiveEffectDestroysBeforeCreates = false;
export const enableModernEventSystem = false;
export const warnAboutSpreadingKeyToJSX = false;
+export const enableLegacyFBPrimerSupport = false;
// Internal-only attempt to debug a React Native issue. See D20130868.
export const throwEarlyForMysteriousError = false;
diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js
index a785da9ec12..e303ea72d6d 100644
--- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js
+++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js
@@ -43,6 +43,7 @@ export const deferPassiveEffectCleanupDuringUnmount = false;
export const runAllPassiveEffectDestroysBeforeCreates = false;
export const enableModernEventSystem = false;
export const warnAboutSpreadingKeyToJSX = false;
+export const enableLegacyFBPrimerSupport = false;
// Internal-only attempt to debug a React Native issue. See D20130868.
export const throwEarlyForMysteriousError = false;
diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
index c00f54a5e89..e72b97e23d8 100644
--- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
@@ -43,6 +43,7 @@ export const deferPassiveEffectCleanupDuringUnmount = false;
export const runAllPassiveEffectDestroysBeforeCreates = false;
export const enableModernEventSystem = false;
export const warnAboutSpreadingKeyToJSX = false;
+export const enableLegacyFBPrimerSupport = true;
// Internal-only attempt to debug a React Native issue. See D20130868.
export const throwEarlyForMysteriousError = false;
diff --git a/packages/shared/forks/ReactFeatureFlags.testing.js b/packages/shared/forks/ReactFeatureFlags.testing.js
index c8e7e724841..c4b82f4d2b4 100644
--- a/packages/shared/forks/ReactFeatureFlags.testing.js
+++ b/packages/shared/forks/ReactFeatureFlags.testing.js
@@ -43,6 +43,7 @@ export const deferPassiveEffectCleanupDuringUnmount = false;
export const runAllPassiveEffectDestroysBeforeCreates = false;
export const enableModernEventSystem = false;
export const warnAboutSpreadingKeyToJSX = false;
+export const enableLegacyFBPrimerSupport = false;
// Internal-only attempt to debug a React Native issue. See D20130868.
export const throwEarlyForMysteriousError = false;
diff --git a/packages/shared/forks/ReactFeatureFlags.testing.www.js b/packages/shared/forks/ReactFeatureFlags.testing.www.js
index 4b4db206546..2fcb0aaf01c 100644
--- a/packages/shared/forks/ReactFeatureFlags.testing.www.js
+++ b/packages/shared/forks/ReactFeatureFlags.testing.www.js
@@ -43,6 +43,7 @@ export const deferPassiveEffectCleanupDuringUnmount = false;
export const runAllPassiveEffectDestroysBeforeCreates = false;
export const enableModernEventSystem = false;
export const warnAboutSpreadingKeyToJSX = false;
+export const enableLegacyFBPrimerSupport = true;
// Internal-only attempt to debug a React Native issue. See D20130868.
export const throwEarlyForMysteriousError = false;
diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js
index aea6f295961..33f62e8b4f1 100644
--- a/packages/shared/forks/ReactFeatureFlags.www.js
+++ b/packages/shared/forks/ReactFeatureFlags.www.js
@@ -97,6 +97,8 @@ export const warnUnstableRenderSubtreeIntoContainer = false;
export const enableModernEventSystem = false;
+export const enableLegacyFBPrimerSupport = false;
+
// Internal-only attempt to debug a React Native issue. See D20130868.
export const throwEarlyForMysteriousError = false;