diff --git a/packages/react-dom/src/__tests__/ReactDOMEventListener-test.js b/packages/react-dom/src/__tests__/ReactDOMEventListener-test.js index cbe9bd985c..1b92c0ff53 100644 --- a/packages/react-dom/src/__tests__/ReactDOMEventListener-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMEventListener-test.js @@ -590,4 +590,36 @@ describe('ReactDOMEventListener', () => { document.body.removeChild(container); } }); + + it('should handle non-bubbling capture events correctly', () => { + const container = document.createElement('div'); + const innerRef = React.createRef(); + const outerRef = React.createRef(); + const onPlayCapture = jest.fn(); + document.body.appendChild(container); + try { + ReactDOM.render( +
+
+
+
+
, + container, + ); + innerRef.current.dispatchEvent( + new Event('play', { + bubbles: false, + }), + ); + expect(onPlayCapture).toHaveBeenCalledTimes(3); + outerRef.current.dispatchEvent( + new Event('play', { + bubbles: false, + }), + ); + expect(onPlayCapture).toHaveBeenCalledTimes(4); + } finally { + document.body.removeChild(container); + } + }); }); diff --git a/packages/react-dom/src/events/DOMPluginEventSystem.js b/packages/react-dom/src/events/DOMPluginEventSystem.js index 60f7e5b828..161f3931df 100644 --- a/packages/react-dom/src/events/DOMPluginEventSystem.js +++ b/packages/react-dom/src/events/DOMPluginEventSystem.js @@ -712,6 +712,7 @@ export function accumulateSinglePhaseListeners( dispatchQueue: DispatchQueue, event: ReactSyntheticEvent, inCapturePhase: boolean, + accumulateTargetOnly: boolean, ): void { const bubbled = event._reactName; const captured = bubbled !== null ? bubbled + 'Capture' : null; @@ -809,6 +810,12 @@ export function accumulateSinglePhaseListeners( } } } + // If we are only accumulating events for the target, then we don't + // continue to propagate through the React fiber tree to find other + // listeners. + if (accumulateTargetOnly) { + break; + } instance = instance.return; } if (listeners.length !== 0) { diff --git a/packages/react-dom/src/events/plugins/SimpleEventPlugin.js b/packages/react-dom/src/events/plugins/SimpleEventPlugin.js index 6d96d42de7..a09d2d7bc7 100644 --- a/packages/react-dom/src/events/plugins/SimpleEventPlugin.js +++ b/packages/react-dom/src/events/plugins/SimpleEventPlugin.js @@ -40,7 +40,7 @@ import { import {IS_EVENT_HANDLE_NON_MANAGED_NODE} from '../EventSystemFlags'; import getEventCharCode from '../getEventCharCode'; -import {IS_CAPTURE_PHASE} from '../EventSystemFlags'; +import {IS_CAPTURE_PHASE, IS_NON_DELEGATED} from '../EventSystemFlags'; import {enableCreateEventHandleAPI} from 'shared/ReactFeatureFlags'; @@ -165,12 +165,23 @@ function extractEvents( inCapturePhase, ); } else { + // When we encounter a non-delegated event in the capture phase, + // we shouldn't emuluate capture bubbling. This is because we'll + // add a native capture event listener to each element directly, + // not the root, and native capture listeners always fire even + // if the event doesn't bubble. + const isNonDelegatedEvent = (eventSystemFlags & IS_NON_DELEGATED) !== 0; + // TODO: We may also want to re-use the accumulateTargetOnly flag to + // special case bubbling for onScroll/media events at a later point. + const accumulateTargetOnly = inCapturePhase && isNonDelegatedEvent; + // We traverse only capture or bubble phase listeners accumulateSinglePhaseListeners( targetInst, dispatchQueue, event, inCapturePhase, + accumulateTargetOnly, ); } return event;