From 242a50a652f70df73a9ea4c696c2ed00e55e5bae Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Sat, 25 Jul 2020 12:32:21 +0100 Subject: [PATCH] Fix issue with capture phase non-bubbling events (#19452) --- .../__tests__/ReactDOMEventListener-test.js | 32 +++++++++++++++++++ .../src/events/DOMPluginEventSystem.js | 7 ++++ .../src/events/plugins/SimpleEventPlugin.js | 13 +++++++- 3 files changed, 51 insertions(+), 1 deletion(-) 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;