Fix issue with capture phase non-bubbling events (#19452)
Dominic Gannaway committed
Jul 25, 2020 at 12:32 UTC
242a50a652f70df73a9ea4c696c2ed00e55e5bae
3 files changed
+51
-1
packages/react-dom/src/__tests__/ReactDOMEventListener-test.js
+32
@@ -590,4 +590,36 @@ describe('ReactDOMEventListener', () => {
590
document.body.removeChild(container);
591
}
592
});
593
+
594
+ it('should handle non-bubbling capture events correctly', () => {
595
+ const container = document.createElement('div');
596
+ const innerRef = React.createRef();
597
+ const outerRef = React.createRef();
598
+ const onPlayCapture = jest.fn();
599
+ document.body.appendChild(container);
600
+ try {
601
+ ReactDOM.render(
602
+ <div ref={outerRef} onPlayCapture={onPlayCapture}>
603
+ <div onPlayCapture={onPlayCapture}>
604
+ <div ref={innerRef} onPlayCapture={onPlayCapture} />
605
+ </div>
606
+ </div>,
607
+ container,
608
+ );
609
+ innerRef.current.dispatchEvent(
610
+ new Event('play', {
611
+ bubbles: false,
612
+ }),
613
+ );
614
+ expect(onPlayCapture).toHaveBeenCalledTimes(3);
615
+ outerRef.current.dispatchEvent(
616
+ new Event('play', {
617
+ bubbles: false,
618
+ }),
619
+ );
620
+ expect(onPlayCapture).toHaveBeenCalledTimes(4);
621
+ } finally {
622
+ document.body.removeChild(container);
623
+ }
624
+ });
625
});
packages/react-dom/src/events/DOMPluginEventSystem.js
+7
@@ -712,6 +712,7 @@ export function accumulateSinglePhaseListeners(
712
dispatchQueue: DispatchQueue,
713
event: ReactSyntheticEvent,
714
inCapturePhase: boolean,
715
+ accumulateTargetOnly: boolean,
716
): void {
717
const bubbled = event._reactName;
718
const captured = bubbled !== null ? bubbled + 'Capture' : null;
@@ -809,6 +810,12 @@ export function accumulateSinglePhaseListeners(
810
}
811
}
812
}
813
+ // If we are only accumulating events for the target, then we don't
814
+ // continue to propagate through the React fiber tree to find other
815
+ // listeners.
816
+ if (accumulateTargetOnly) {
817
+ break;
818
+ }
819
instance = instance.return;
820
}
821
if (listeners.length !== 0) {
packages/react-dom/src/events/plugins/SimpleEventPlugin.js
+12
-1
@@ -40,7 +40,7 @@ import {
40
import {IS_EVENT_HANDLE_NON_MANAGED_NODE} from '../EventSystemFlags';
41
42
import getEventCharCode from '../getEventCharCode';
43
-import {IS_CAPTURE_PHASE} from '../EventSystemFlags';
43
+import {IS_CAPTURE_PHASE, IS_NON_DELEGATED} from '../EventSystemFlags';
44
45
import {enableCreateEventHandleAPI} from 'shared/ReactFeatureFlags';
46
@@ -165,12 +165,23 @@ function extractEvents(
165
inCapturePhase,
166
);
167
} else {
168
+ // When we encounter a non-delegated event in the capture phase,
169
+ // we shouldn't emuluate capture bubbling. This is because we'll
170
+ // add a native capture event listener to each element directly,
171
+ // not the root, and native capture listeners always fire even
172
+ // if the event doesn't bubble.
173
+ const isNonDelegatedEvent = (eventSystemFlags & IS_NON_DELEGATED) !== 0;
174
+ // TODO: We may also want to re-use the accumulateTargetOnly flag to
175
+ // special case bubbling for onScroll/media events at a later point.
176
+ const accumulateTargetOnly = inCapturePhase && isNonDelegatedEvent;
177
+
178
// We traverse only capture or bubble phase listeners
179
accumulateSinglePhaseListeners(
180
targetInst,
181
dispatchQueue,
182
event,
183
inCapturePhase,
184
+ accumulateTargetOnly,
185
);
186
}
187
return event;