@@ -565,11 +559,6 @@ describe('ReactDOMEventListener', () => {
bubbles: false,
}),
);
- ref.current.dispatchEvent(
- new Event('scroll', {
- bubbles: false,
- }),
- );
ref.current.dispatchEvent(
new Event('cancel', {
bubbles: false,
@@ -583,7 +572,6 @@ describe('ReactDOMEventListener', () => {
// Regression test: ensure we still emulate bubbling with non-bubbling
// media
expect(onPlay).toHaveBeenCalledTimes(2);
- expect(onScroll).toHaveBeenCalledTimes(2);
expect(onCancel).toHaveBeenCalledTimes(2);
expect(onClose).toHaveBeenCalledTimes(2);
} finally {
@@ -634,4 +622,100 @@ describe('ReactDOMEventListener', () => {
document.body.removeChild(container);
}
});
+
+ // We're moving towards aligning more closely with the browser.
+ // Currently we emulate bubbling for all non-bubbling events except scroll.
+ // We may expand this list in the future, removing emulated bubbling altogether.
+ it('should not emulate bubbling of scroll events', () => {
+ const container = document.createElement('div');
+ const ref = React.createRef();
+ const log = [];
+ const onScroll = jest.fn(e =>
+ log.push(['bubble', e.currentTarget.className]),
+ );
+ const onScrollCapture = jest.fn(e =>
+ log.push(['capture', e.currentTarget.className]),
+ );
+ document.body.appendChild(container);
+ try {
+ ReactDOM.render(
+
,
+ container,
+ );
+ ref.current.dispatchEvent(
+ new Event('scroll', {
+ bubbles: false,
+ }),
+ );
+ //
+ expect(log).toEqual([
+ ['capture', 'grand'],
+ ['capture', 'parent'],
+ ['capture', 'child'],
+ ['bubble', 'child'],
+ ]);
+ } finally {
+ document.body.removeChild(container);
+ }
+ });
+
+ // We're moving towards aligning more closely with the browser.
+ // Currently we emulate bubbling for all non-bubbling events except scroll.
+ // We may expand this list in the future, removing emulated bubbling altogether.
+ it('should not emulate bubbling of scroll events (no own handler)', () => {
+ const container = document.createElement('div');
+ const ref = React.createRef();
+ const log = [];
+ const onScroll = jest.fn(e =>
+ log.push(['bubble', e.currentTarget.className]),
+ );
+ const onScrollCapture = jest.fn(e =>
+ log.push(['capture', e.currentTarget.className]),
+ );
+ document.body.appendChild(container);
+ try {
+ ReactDOM.render(
+
+
+ {/* Intentionally no handler on the child: */}
+
+
+
,
+ container,
+ );
+ ref.current.dispatchEvent(
+ new Event('scroll', {
+ bubbles: false,
+ }),
+ );
+ //
+ expect(log).toEqual([
+ ['capture', 'grand'],
+ ['capture', 'parent'],
+ ]);
+ } finally {
+ document.body.removeChild(container);
+ }
+ });
});
diff --git a/packages/react-dom/src/events/plugins/SimpleEventPlugin.js b/packages/react-dom/src/events/plugins/SimpleEventPlugin.js
index 281065828322a..0bcb4dc2845c6 100644
--- a/packages/react-dom/src/events/plugins/SimpleEventPlugin.js
+++ b/packages/react-dom/src/events/plugins/SimpleEventPlugin.js
@@ -165,13 +165,17 @@ function extractEvents(
inCapturePhase,
);
} else {
- // TODO: We may also want to re-use the accumulateTargetOnly flag to
- // special case bubbling for onScroll/media events at a later point.
- // In which case we will want to make this flag boolean and ensure
- // we change the targetInst to be of the container instance. Like:
- const accumulateTargetOnly = false;
+ // Some events don't bubble in the browser.
+ // In the past, React has always bubbled them, but this can be surprising.
+ // We're going to try aligning closer to the browser behavior by not bubbling
+ // them in React either. We'll start by not bubbling onScroll, and then expand.
+ const accumulateTargetOnly =
+ !inCapturePhase &&
+ // TODO: ideally, we'd eventually add all events from
+ // nonDelegatedEvents list in ReactDOMEventListener.
+ // Then we can remove this special list.
+ topLevelType === DOMTopLevelEventTypes.TOP_SCROLL;
- // We traverse only capture or bubble phase listeners
accumulateSinglePhaseListeners(
targetInst,
dispatchQueue,