diff --git a/packages/react-dom/src/events/DOMEventResponderSystem.js b/packages/react-dom/src/events/DOMEventResponderSystem.js
index 14bd5f5fff734..2d22140d6f12c 100644
--- a/packages/react-dom/src/events/DOMEventResponderSystem.js
+++ b/packages/react-dom/src/events/DOMEventResponderSystem.js
@@ -12,7 +12,7 @@ import {
PASSIVE_NOT_SUPPORTED,
} from 'legacy-events/EventSystemFlags';
import type {AnyNativeEvent} from 'legacy-events/PluginModuleType';
-import {HostComponent, ScopeComponent} from 'shared/ReactWorkTags';
+import {HostComponent, ScopeComponent, HostPortal} from 'shared/ReactWorkTags';
import type {EventPriority} from 'shared/ReactTypes';
import type {
ReactDOMEventResponder,
@@ -451,9 +451,12 @@ function traverseAndHandleEventResponderInstances(
isPassiveSupported,
);
let node = targetFiber;
+ let insidePortal = false;
while (node !== null) {
const {dependencies, tag} = node;
- if (
+ if (tag === HostPortal) {
+ insidePortal = true;
+ } else if (
(tag === HostComponent || tag === ScopeComponent) &&
dependencies !== null
) {
@@ -465,7 +468,8 @@ function traverseAndHandleEventResponderInstances(
const {props, responder, state} = responderInstance;
if (
!visitedResponders.has(responder) &&
- validateResponderTargetEventTypes(eventType, responder)
+ validateResponderTargetEventTypes(eventType, responder) &&
+ (!insidePortal || responder.targetPortalPropagation)
) {
visitedResponders.add(responder);
const onEvent = responder.onEvent;
diff --git a/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js b/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js
index 83f17d0c1723d..c695a19fd3f61 100644
--- a/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js
+++ b/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js
@@ -28,6 +28,7 @@ function createEventResponder({
onMount,
onUnmount,
getInitialState,
+ targetPortalPropagation,
}) {
return React.unstable_createResponder('TestEventResponder', {
targetEventTypes,
@@ -37,6 +38,7 @@ function createEventResponder({
onMount,
onUnmount,
getInitialState,
+ targetPortalPropagation,
});
}
@@ -1034,4 +1036,51 @@ describe('DOMEventResponderSystem', () => {
},
]);
});
+
+ it('should not propagate target events through portals by default', () => {
+ const buttonRef = React.createRef();
+ const onEvent = jest.fn();
+ const TestResponder = createEventResponder({
+ targetEventTypes: ['click'],
+ onEvent,
+ });
+ const domNode = document.createElement('div');
+ document.body.appendChild(domNode);
+ const Component = () => {
+ const listener = React.unstable_useResponder(TestResponder, {});
+ return (
+
+ {ReactDOM.createPortal(, domNode)}
+
+ );
+ };
+ ReactDOM.render(, container);
+ dispatchClickEvent(buttonRef.current);
+ document.body.removeChild(domNode);
+ expect(onEvent).not.toBeCalled();
+ });
+
+ it('should propagate target events through portals when enabled', () => {
+ const buttonRef = React.createRef();
+ const onEvent = jest.fn();
+ const TestResponder = createEventResponder({
+ targetPortalPropagation: true,
+ targetEventTypes: ['click'],
+ onEvent,
+ });
+ const domNode = document.createElement('div');
+ document.body.appendChild(domNode);
+ const Component = () => {
+ const listener = React.unstable_useResponder(TestResponder, {});
+ return (
+
+ {ReactDOM.createPortal(, domNode)}
+
+ );
+ };
+ ReactDOM.render(, container);
+ dispatchClickEvent(buttonRef.current);
+ document.body.removeChild(domNode);
+ expect(onEvent).toBeCalled();
+ });
});
diff --git a/packages/react-interactions/events/src/dom/Focus.js b/packages/react-interactions/events/src/dom/Focus.js
index 8ae33233ad33e..434ac2370fa3b 100644
--- a/packages/react-interactions/events/src/dom/Focus.js
+++ b/packages/react-interactions/events/src/dom/Focus.js
@@ -293,6 +293,7 @@ function unmountFocusResponder(
const focusResponderImpl = {
targetEventTypes,
+ targetPortalPropagation: true,
rootEventTypes,
getInitialState(): FocusState {
return {
@@ -430,6 +431,7 @@ function unmountFocusWithinResponder(
const focusWithinResponderImpl = {
targetEventTypes,
+ targetPortalPropagation: true,
rootEventTypes,
getInitialState(): FocusState {
return {
diff --git a/packages/react-interactions/events/src/dom/Keyboard.js b/packages/react-interactions/events/src/dom/Keyboard.js
index 08cc19d1d3bfc..9f4cbc56e9e92 100644
--- a/packages/react-interactions/events/src/dom/Keyboard.js
+++ b/packages/react-interactions/events/src/dom/Keyboard.js
@@ -180,6 +180,7 @@ function dispatchKeyboardEvent(
const keyboardResponderImpl = {
targetEventTypes,
+ targetPortalPropagation: true,
getInitialState(): KeyboardState {
return {
isActive: false,
diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js
index 5e9a6b11df1d6..2e8a891bc2ead 100644
--- a/packages/shared/ReactTypes.js
+++ b/packages/shared/ReactTypes.js
@@ -96,6 +96,7 @@ export type ReactEventResponder = {
$$typeof: Symbol | number,
displayName: string,
targetEventTypes: null | Array,
+ targetPortalPropagation: boolean,
rootEventTypes: null | Array,
getInitialState: null | ((props: Object) => Object),
onEvent:
diff --git a/packages/shared/createEventResponder.js b/packages/shared/createEventResponder.js
index 02373b9574668..c18b1595c4966 100644
--- a/packages/shared/createEventResponder.js
+++ b/packages/shared/createEventResponder.js
@@ -22,6 +22,7 @@ export default function createEventResponder(
onRootEvent,
rootEventTypes,
targetEventTypes,
+ targetPortalPropagation,
} = responderConfig;
const eventResponder = {
$$typeof: REACT_RESPONDER_TYPE,
@@ -33,6 +34,7 @@ export default function createEventResponder(
onUnmount: onUnmount || null,
rootEventTypes: rootEventTypes || null,
targetEventTypes: targetEventTypes || null,
+ targetPortalPropagation: targetPortalPropagation || false,
};
// We use responder as a Map key later on. When we have a bad
// polyfill, then we can't use it as a key as the polyfill tries