Skip to content

Commit 242a50a

Browse files
authored
Fix issue with capture phase non-bubbling events (facebook#19452)
1 parent ef22aec commit 242a50a

File tree

3 files changed

+51
-1
lines changed

3 files changed

+51
-1
lines changed

packages/react-dom/src/__tests__/ReactDOMEventListener-test.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -590,4 +590,36 @@ describe('ReactDOMEventListener', () => {
590590
document.body.removeChild(container);
591591
}
592592
});
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+
});
593625
});

packages/react-dom/src/events/DOMPluginEventSystem.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -712,6 +712,7 @@ export function accumulateSinglePhaseListeners(
712712
dispatchQueue: DispatchQueue,
713713
event: ReactSyntheticEvent,
714714
inCapturePhase: boolean,
715+
accumulateTargetOnly: boolean,
715716
): void {
716717
const bubbled = event._reactName;
717718
const captured = bubbled !== null ? bubbled + 'Capture' : null;
@@ -809,6 +810,12 @@ export function accumulateSinglePhaseListeners(
809810
}
810811
}
811812
}
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+
}
812819
instance = instance.return;
813820
}
814821
if (listeners.length !== 0) {

packages/react-dom/src/events/plugins/SimpleEventPlugin.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ import {
4040
import {IS_EVENT_HANDLE_NON_MANAGED_NODE} from '../EventSystemFlags';
4141

4242
import getEventCharCode from '../getEventCharCode';
43-
import {IS_CAPTURE_PHASE} from '../EventSystemFlags';
43+
import {IS_CAPTURE_PHASE, IS_NON_DELEGATED} from '../EventSystemFlags';
4444

4545
import {enableCreateEventHandleAPI} from 'shared/ReactFeatureFlags';
4646

@@ -165,12 +165,23 @@ function extractEvents(
165165
inCapturePhase,
166166
);
167167
} 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+
168178
// We traverse only capture or bubble phase listeners
169179
accumulateSinglePhaseListeners(
170180
targetInst,
171181
dispatchQueue,
172182
event,
173183
inCapturePhase,
184+
accumulateTargetOnly,
174185
);
175186
}
176187
return event;

0 commit comments

Comments
 (0)