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