@@ -16,19 +16,106 @@ import type {ReactSyntheticEvent} from '../ReactSyntheticEventType';
1616import { registerTwoPhaseEvent } from '../EventRegistry' ;
1717import { SyntheticUIEvent } from '../SyntheticEvent' ;
1818
19+ import { canUseDOM } from 'shared/ExecutionEnvironment' ;
20+ import isEventSupported from '../isEventSupported' ;
21+
1922import { IS_CAPTURE_PHASE } from '../EventSystemFlags' ;
2023
21- import { accumulateSinglePhaseListeners } from '../DOMPluginEventSystem' ;
24+ import { batchedUpdates } from '../ReactDOMUpdateBatching' ;
25+ import {
26+ processDispatchQueue ,
27+ accumulateSinglePhaseListeners ,
28+ accumulateTwoPhaseListeners ,
29+ } from '../DOMPluginEventSystem' ;
30+
31+ import {
32+ getScrollEndTimer ,
33+ setScrollEndTimer ,
34+ clearScrollEndTimer ,
35+ } from '../../client/ReactDOMComponentTree' ;
36+
37+ import { enableScrollEndPolyfill } from 'shared/ReactFeatureFlags' ;
38+
39+ const isScrollEndEventSupported =
40+ enableScrollEndPolyfill && canUseDOM && isEventSupported ( 'scrollend' ) ;
41+
42+ let isTouchStarted = false ;
43+ let isMouseDown = false ;
2244
2345function registerEvents ( ) {
2446 registerTwoPhaseEvent ( 'onScrollEnd' , [
2547 'scroll' ,
2648 'scrollend' ,
2749 'touchstart' ,
50+ 'touchcancel' ,
2851 'touchend' ,
52+ 'mousedown' ,
53+ 'mouseup' ,
2954 ] ) ;
3055}
3156
57+ function manualDispatchScrollEndEvent (
58+ inst : Fiber ,
59+ nativeEvent : AnyNativeEvent ,
60+ target : EventTarget ,
61+ ) {
62+ const dispatchQueue : DispatchQueue = [ ] ;
63+ const listeners = accumulateTwoPhaseListeners ( inst , 'onScrollEnd' ) ;
64+ if ( listeners . length > 0 ) {
65+ const event : ReactSyntheticEvent = new SyntheticUIEvent (
66+ 'onScrollEnd' ,
67+ 'scrollend' ,
68+ null ,
69+ nativeEvent , // This will be the "scroll" event.
70+ target ,
71+ ) ;
72+ dispatchQueue . push ( { event, listeners} ) ;
73+ }
74+ batchedUpdates ( runEventInBatch , dispatchQueue ) ;
75+ }
76+
77+ function runEventInBatch ( dispatchQueue : DispatchQueue ) {
78+ processDispatchQueue ( dispatchQueue , 0 ) ;
79+ }
80+
81+ function fireScrollEnd (
82+ targetInst : Fiber ,
83+ nativeEvent : AnyNativeEvent ,
84+ nativeEventTarget : EventTarget ,
85+ ) : void {
86+ clearScrollEndTimer ( nativeEventTarget ) ;
87+ if ( isMouseDown || isTouchStarted ) {
88+ // If mouse or touch is down, try again later in case this is due to having an
89+ // active scroll but it's not currently moving.
90+ debounceScrollEnd ( targetInst , nativeEvent , nativeEventTarget ) ;
91+ return ;
92+ }
93+ manualDispatchScrollEndEvent ( targetInst , nativeEvent , nativeEventTarget ) ;
94+ }
95+
96+ // When scrolling slows down the frequency of new scroll events can be quite low.
97+ // This timeout seems high enough to cover those cases but short enough to not
98+ // fire the event way too late.
99+ const DEBOUNCE_TIMEOUT = 200 ;
100+
101+ function debounceScrollEnd (
102+ targetInst : null | Fiber ,
103+ nativeEvent : AnyNativeEvent ,
104+ nativeEventTarget : EventTarget ,
105+ ) {
106+ const existingTimer = getScrollEndTimer ( nativeEventTarget ) ;
107+ if ( existingTimer != null ) {
108+ clearTimeout ( existingTimer ) ;
109+ }
110+ if ( targetInst !== null ) {
111+ const newTimer = setTimeout (
112+ fireScrollEnd . bind ( null , targetInst , nativeEvent , nativeEventTarget ) ,
113+ DEBOUNCE_TIMEOUT ,
114+ ) ;
115+ setScrollEndTimer ( nativeEventTarget , newTimer ) ;
116+ }
117+ }
118+
32119/**
33120 * This plugin creates an `onScrollEnd` event polyfill when the native one
34121 * is not available.
@@ -42,21 +129,68 @@ function extractEvents(
42129 eventSystemFlags : EventSystemFlags ,
43130 targetContainer : null | EventTarget ,
44131) {
45- if ( domEventName !== 'scrollend' ) {
132+ if ( ! enableScrollEndPolyfill ) {
46133 return ;
47134 }
48135
49- const reactName = 'onScrollEnd' ;
50-
51136 const inCapturePhase = ( eventSystemFlags & IS_CAPTURE_PHASE ) !== 0 ;
52137
138+ if ( domEventName !== 'scrollend' ) {
139+ if ( ! isScrollEndEventSupported && inCapturePhase ) {
140+ switch ( domEventName ) {
141+ case 'scroll' : {
142+ if ( nativeEventTarget !== null ) {
143+ debounceScrollEnd ( targetInst , nativeEvent , nativeEventTarget ) ;
144+ }
145+ break ;
146+ }
147+ case 'touchstart' : {
148+ isTouchStarted = true ;
149+ break ;
150+ }
151+ case 'touchcancel' :
152+ case 'touchend' : {
153+ // Note we cannot use pointer events for this because they get
154+ // cancelled when native scrolling takes control.
155+ isTouchStarted = false ;
156+ break ;
157+ }
158+ case 'mousedown' : {
159+ isMouseDown = true ;
160+ break ;
161+ }
162+ case 'mouseup' : {
163+ isMouseDown = false ;
164+ break ;
165+ }
166+ }
167+ }
168+ return ;
169+ }
170+
171+ if ( ! isScrollEndEventSupported && nativeEventTarget !== null ) {
172+ const existingTimer = getScrollEndTimer ( nativeEventTarget ) ;
173+ if ( existingTimer != null ) {
174+ // If we do get a native scrollend event fired, we cancel the polyfill.
175+ // This could happen if our feature detection is broken or if there's another
176+ // polyfill calling dispatchEvent to fire it before we fire ours.
177+ clearTimeout ( existingTimer ) ;
178+ clearScrollEndTimer ( nativeEventTarget ) ;
179+ } else {
180+ // If we didn't receive a 'scroll' event first, we ignore this event to avoid
181+ // double firing. Such as if we fired our onScrollEnd polyfill and then
182+ // we also observed a native one afterwards.
183+ return ;
184+ }
185+ }
186+
53187 // In React onScrollEnd doesn't bubble.
54188 const accumulateTargetOnly = ! inCapturePhase ;
55189
56190 const listeners = accumulateSinglePhaseListeners (
57191 targetInst ,
58- reactName ,
59- nativeEvent . type ,
192+ 'onScrollEnd' ,
193+ 'scrollend' ,
60194 inCapturePhase ,
61195 accumulateTargetOnly ,
62196 nativeEvent ,
@@ -65,8 +199,8 @@ function extractEvents(
65199 if ( listeners . length > 0 ) {
66200 // Intentionally create event lazily.
67201 const event : ReactSyntheticEvent = new SyntheticUIEvent (
68- reactName ,
69- domEventName ,
202+ 'onScrollEnd' ,
203+ 'scrollend' ,
70204 null ,
71205 nativeEvent ,
72206 nativeEventTarget ,
0 commit comments