Skip to content

Commit 1c2db6b

Browse files
committed
Don't emulate bubbling of the scroll event
1 parent 217ecf5 commit 1c2db6b

2 files changed

Lines changed: 105 additions & 15 deletions

File tree

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

Lines changed: 95 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -539,7 +539,6 @@ describe('ReactDOMEventListener', () => {
539539
const container = document.createElement('div');
540540
const ref = React.createRef();
541541
const onPlay = jest.fn();
542-
const onScroll = jest.fn();
543542
const onCancel = jest.fn();
544543
const onClose = jest.fn();
545544
const onToggle = jest.fn();
@@ -548,14 +547,12 @@ describe('ReactDOMEventListener', () => {
548547
ReactDOM.render(
549548
<div
550549
onPlay={onPlay}
551-
onScroll={onScroll}
552550
onCancel={onCancel}
553551
onClose={onClose}
554552
onToggle={onToggle}>
555553
<div
556554
ref={ref}
557555
onPlay={onPlay}
558-
onScroll={onScroll}
559556
onCancel={onCancel}
560557
onClose={onClose}
561558
onToggle={onToggle}
@@ -568,11 +565,6 @@ describe('ReactDOMEventListener', () => {
568565
bubbles: false,
569566
}),
570567
);
571-
ref.current.dispatchEvent(
572-
new Event('scroll', {
573-
bubbles: false,
574-
}),
575-
);
576568
ref.current.dispatchEvent(
577569
new Event('cancel', {
578570
bubbles: false,
@@ -591,7 +583,6 @@ describe('ReactDOMEventListener', () => {
591583
// Regression test: ensure we still emulate bubbling with non-bubbling
592584
// media
593585
expect(onPlay).toHaveBeenCalledTimes(2);
594-
expect(onScroll).toHaveBeenCalledTimes(2);
595586
expect(onCancel).toHaveBeenCalledTimes(2);
596587
expect(onClose).toHaveBeenCalledTimes(2);
597588
expect(onToggle).toHaveBeenCalledTimes(2);
@@ -643,4 +634,99 @@ describe('ReactDOMEventListener', () => {
643634
document.body.removeChild(container);
644635
}
645636
});
637+
638+
// We're moving towards aligning more closely with the browser.
639+
// Currently we emulate bubbling for all non-bubbling events except scroll.
640+
// We may expand this list in the future, removing emulated bubbling altogether.
641+
it('should not emulate bubbling of scroll events', () => {
642+
const container = document.createElement('div');
643+
const ref = React.createRef();
644+
const log = [];
645+
const onScroll = jest.fn(e =>
646+
log.push(['bubble', e.currentTarget.className]),
647+
);
648+
const onScrollCapture = jest.fn(e =>
649+
log.push(['capture', e.currentTarget.className]),
650+
);
651+
document.body.appendChild(container);
652+
try {
653+
ReactDOM.render(
654+
<div
655+
className="grand"
656+
onScroll={onScroll}
657+
onScrollCapture={onScrollCapture}>
658+
<div
659+
className="parent"
660+
onScroll={onScroll}
661+
onScrollCapture={onScrollCapture}>
662+
<div
663+
className="child"
664+
onScroll={onScroll}
665+
onScrollCapture={onScrollCapture}
666+
ref={ref}
667+
/>
668+
</div>
669+
</div>,
670+
container,
671+
);
672+
ref.current.dispatchEvent(
673+
new Event('scroll', {
674+
bubbles: false,
675+
}),
676+
);
677+
expect(log).toEqual([
678+
['capture', 'grand'],
679+
['capture', 'parent'],
680+
['capture', 'child'],
681+
['bubble', 'child'],
682+
]);
683+
} finally {
684+
document.body.removeChild(container);
685+
}
686+
});
687+
688+
// We're moving towards aligning more closely with the browser.
689+
// Currently we emulate bubbling for all non-bubbling events except scroll.
690+
// We may expand this list in the future, removing emulated bubbling altogether.
691+
it('should not emulate bubbling of scroll events (no own handler)', () => {
692+
const container = document.createElement('div');
693+
const ref = React.createRef();
694+
const log = [];
695+
const onScroll = jest.fn(e =>
696+
log.push(['bubble', e.currentTarget.className]),
697+
);
698+
const onScrollCapture = jest.fn(e =>
699+
log.push(['capture', e.currentTarget.className]),
700+
);
701+
document.body.appendChild(container);
702+
try {
703+
ReactDOM.render(
704+
<div
705+
className="grand"
706+
onScroll={onScroll}
707+
onScrollCapture={onScrollCapture}>
708+
<div
709+
className="parent"
710+
onScroll={onScroll}
711+
onScrollCapture={onScrollCapture}>
712+
{/* Intentionally no handler on the child: */}
713+
<div className="child" ref={ref} />
714+
</div>
715+
</div>,
716+
container,
717+
);
718+
ref.current.dispatchEvent(
719+
new Event('scroll', {
720+
bubbles: false,
721+
}),
722+
);
723+
//
724+
expect(log).toEqual([
725+
['capture', 'grand'],
726+
['capture', 'parent'],
727+
]);
728+
} finally {
729+
document.body.removeChild(container);
730+
}
731+
});
646732
});

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

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -165,13 +165,17 @@ function extractEvents(
165165
inCapturePhase,
166166
);
167167
} else {
168-
// TODO: We may also want to re-use the accumulateTargetOnly flag to
169-
// special case bubbling for onScroll/media events at a later point.
170-
// In which case we will want to make this flag boolean and ensure
171-
// we change the targetInst to be of the container instance. Like:
172-
const accumulateTargetOnly = false;
168+
// Some events don't bubble in the browser.
169+
// In the past, React has always bubbled them, but this can be surprising.
170+
// We're going to try aligning closer to the browser behavior by not bubbling
171+
// them in React either. We'll start by not bubbling onScroll, and then expand.
172+
const accumulateTargetOnly =
173+
!inCapturePhase &&
174+
// TODO: ideally, we'd eventually add all events from
175+
// nonDelegatedEvents list in DOMPluginEventSystem.
176+
// Then we can remove this special list.
177+
topLevelType === DOMTopLevelEventTypes.TOP_SCROLL;
173178

174-
// We traverse only capture or bubble phase listeners
175179
accumulateSinglePhaseListeners(
176180
targetInst,
177181
dispatchQueue,

0 commit comments

Comments
 (0)