diff --git a/packages/react-dom/src/__tests__/ReactDOMEventListener-test.js b/packages/react-dom/src/__tests__/ReactDOMEventListener-test.js index 3733005fb223..38ce20b1c9e4 100644 --- a/packages/react-dom/src/__tests__/ReactDOMEventListener-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMEventListener-test.js @@ -539,7 +539,6 @@ describe('ReactDOMEventListener', () => { const container = document.createElement('div'); const ref = React.createRef(); const onPlay = jest.fn(); - const onScroll = jest.fn(); const onCancel = jest.fn(); const onClose = jest.fn(); const onToggle = jest.fn(); @@ -548,14 +547,12 @@ describe('ReactDOMEventListener', () => { ReactDOM.render(
{ bubbles: false, }), ); - ref.current.dispatchEvent( - new Event('scroll', { - bubbles: false, - }), - ); ref.current.dispatchEvent( new Event('cancel', { bubbles: false, @@ -591,7 +583,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); expect(onToggle).toHaveBeenCalledTimes(2); @@ -643,4 +634,99 @@ 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 281065828322..97b595d6c49e 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 DOMPluginEventSystem. + // Then we can remove this special list. + topLevelType === DOMTopLevelEventTypes.TOP_SCROLL; - // We traverse only capture or bubble phase listeners accumulateSinglePhaseListeners( targetInst, dispatchQueue,