main
js 212 lines 5.94 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @flow
8 */
9 import type {AnyNativeEvent} from '../PluginModuleType';
10 import type {DOMEventName} from '../DOMEventNames';
11 import type {DispatchQueue} from '../DOMPluginEventSystem';
12 import type {EventSystemFlags} from '../EventSystemFlags';
13 import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
14 import type {ReactSyntheticEvent} from '../ReactSyntheticEventType';
15
16 import {registerTwoPhaseEvent} from '../EventRegistry';
17 import {SyntheticUIEvent} from '../SyntheticEvent';
18
19 import {canUseDOM} from 'shared/ExecutionEnvironment';
20 import isEventSupported from '../isEventSupported';
21
22 import {IS_CAPTURE_PHASE} from '../EventSystemFlags';
23
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;
44
45 function registerEvents() {
46 registerTwoPhaseEvent('onScrollEnd', [
47 'scroll',
48 'scrollend',
49 'touchstart',
50 'touchcancel',
51 'touchend',
52 'mousedown',
53 'mouseup',
54 ]);
55 }
56
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
119 /**
120 * This plugin creates an `onScrollEnd` event polyfill when the native one
121 * is not available.
122 */
123 function extractEvents(
124 dispatchQueue: DispatchQueue,
125 domEventName: DOMEventName,
126 targetInst: null | Fiber,
127 nativeEvent: AnyNativeEvent,
128 nativeEventTarget: null | EventTarget,
129 eventSystemFlags: EventSystemFlags,
130 targetContainer: null | EventTarget,
131 ) {
132 if (!enableScrollEndPolyfill) {
133 return;
134 }
135
136 const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
137
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
187 // In React onScrollEnd doesn't bubble.
188 const accumulateTargetOnly = !inCapturePhase;
189
190 const listeners = accumulateSinglePhaseListeners(
191 targetInst,
192 'onScrollEnd',
193 'scrollend',
194 inCapturePhase,
195 accumulateTargetOnly,
196 nativeEvent,
197 );
198
199 if (listeners.length > 0) {
200 // Intentionally create event lazily.
201 const event: ReactSyntheticEvent = new SyntheticUIEvent(
202 'onScrollEnd',
203 'scrollend',
204 null,
205 nativeEvent,
206 nativeEventTarget,
207 );
208 dispatchQueue.push({event, listeners});
209 }
210 }
211
212 export {registerEvents, extractEvents};