/
githubmr
/
facebook-react
Обзор
Документация
Войти
/
githubmr
/
facebook-react
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
main
packages/react-dom-bindings/src/events/plugins/ScrollEndEventPlugin.js
212 строк
6 KB
Sebastian Markbåge
Polyfill onScrollEnd Event in Safari (#32427)
03 мар 2025, 22:24
Не верифицирован
03 мар 2025, 22:24
605a880
Код
Авторство
О чём код?
/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow */ import type {AnyNativeEvent} from '../PluginModuleType'; import type {DOMEventName} from '../DOMEventNames'; import type {DispatchQueue} from '../DOMPluginEventSystem'; import type {EventSystemFlags} from '../EventSystemFlags'; import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; import type {ReactSyntheticEvent} from '../ReactSyntheticEventType'; import {registerTwoPhaseEvent} from '../EventRegistry'; import {SyntheticUIEvent} from '../SyntheticEvent'; import {canUseDOM} from 'shared/ExecutionEnvironment'; import isEventSupported from '../isEventSupported'; import {IS_CAPTURE_PHASE} from '../EventSystemFlags'; import {batchedUpdates} from '../ReactDOMUpdateBatching'; import { processDispatchQueue, accumulateSinglePhaseListeners, accumulateTwoPhaseListeners, } from '../DOMPluginEventSystem'; import { getScrollEndTimer, setScrollEndTimer, clearScrollEndTimer, } from '../../client/ReactDOMComponentTree'; import {enableScrollEndPolyfill} from 'shared/ReactFeatureFlags'; const isScrollEndEventSupported = enableScrollEndPolyfill && canUseDOM && isEventSupported('scrollend'); let isTouchStarted = false; let isMouseDown = false; function registerEvents() { registerTwoPhaseEvent('onScrollEnd', [ 'scroll', 'scrollend', 'touchstart', 'touchcancel', 'touchend', 'mousedown', 'mouseup', ]); } function manualDispatchScrollEndEvent( inst: Fiber, nativeEvent: AnyNativeEvent, target: EventTarget, ) { const dispatchQueue: DispatchQueue = []; const listeners = accumulateTwoPhaseListeners(inst, 'onScrollEnd'); if (listeners.length > 0) { const event: ReactSyntheticEvent = new SyntheticUIEvent( 'onScrollEnd', 'scrollend', null, nativeEvent, // This will be the "scroll" event. target, ); dispatchQueue.push({event, listeners}); } batchedUpdates(runEventInBatch, dispatchQueue); } function runEventInBatch(dispatchQueue: DispatchQueue) { processDispatchQueue(dispatchQueue, 0); } function fireScrollEnd( targetInst: Fiber, nativeEvent: AnyNativeEvent, nativeEventTarget: EventTarget, ): void { clearScrollEndTimer(nativeEventTarget); if (isMouseDown || isTouchStarted) { // If mouse or touch is down, try again later in case this is due to having an // active scroll but it's not currently moving. debounceScrollEnd(targetInst, nativeEvent, nativeEventTarget); return; } manualDispatchScrollEndEvent(targetInst, nativeEvent, nativeEventTarget); } // When scrolling slows down the frequency of new scroll events can be quite low. // This timeout seems high enough to cover those cases but short enough to not // fire the event way too late. const DEBOUNCE_TIMEOUT = 200; function debounceScrollEnd( targetInst: null | Fiber, nativeEvent: AnyNativeEvent, nativeEventTarget: EventTarget, ) { const existingTimer = getScrollEndTimer(nativeEventTarget); if (existingTimer != null) { clearTimeout(existingTimer); } if (targetInst !== null) { const newTimer = setTimeout( fireScrollEnd.bind(null, targetInst, nativeEvent, nativeEventTarget), DEBOUNCE_TIMEOUT, ); setScrollEndTimer(nativeEventTarget, newTimer); } } /** * This plugin creates an `onScrollEnd` event polyfill when the native one * is not available. */ function extractEvents( dispatchQueue: DispatchQueue, domEventName: DOMEventName, targetInst: null | Fiber, nativeEvent: AnyNativeEvent, nativeEventTarget: null | EventTarget, eventSystemFlags: EventSystemFlags, targetContainer: null | EventTarget, ) { if (!enableScrollEndPolyfill) { return; } const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0; if (domEventName !== 'scrollend') { if (!isScrollEndEventSupported && inCapturePhase) { switch (domEventName) { case 'scroll': { if (nativeEventTarget !== null) { debounceScrollEnd(targetInst, nativeEvent, nativeEventTarget); } break; } case 'touchstart': { isTouchStarted = true; break; } case 'touchcancel': case 'touchend': { // Note we cannot use pointer events for this because they get // cancelled when native scrolling takes control. isTouchStarted = false; break; } case 'mousedown': { isMouseDown = true; break; } case 'mouseup': { isMouseDown = false; break; } } } return; } if (!isScrollEndEventSupported && nativeEventTarget !== null) { const existingTimer = getScrollEndTimer(nativeEventTarget); if (existingTimer != null) { // If we do get a native scrollend event fired, we cancel the polyfill. // This could happen if our feature detection is broken or if there's another // polyfill calling dispatchEvent to fire it before we fire ours. clearTimeout(existingTimer); clearScrollEndTimer(nativeEventTarget); } else { // If we didn't receive a 'scroll' event first, we ignore this event to avoid // double firing. Such as if we fired our onScrollEnd polyfill and then // we also observed a native one afterwards. return; } } // In React onScrollEnd doesn't bubble. const accumulateTargetOnly = !inCapturePhase; const listeners = accumulateSinglePhaseListeners( targetInst, 'onScrollEnd', 'scrollend', inCapturePhase, accumulateTargetOnly, nativeEvent, ); if (listeners.length > 0) { // Intentionally create event lazily. const event: ReactSyntheticEvent = new SyntheticUIEvent( 'onScrollEnd', 'scrollend', null, nativeEvent, nativeEventTarget, ); dispatchQueue.push({event, listeners}); } } export {registerEvents, extractEvents};