import React , { useRef , useCallback , useState , useEffect } from 'react' ; import WheelHandler from 'dom-lib/WheelHandler' ; import scrollLeft from 'dom-lib/scrollLeft' ; import scrollTop from 'dom-lib/scrollTop' ; import on from 'dom-lib/on' ; import removeStyle from 'dom-lib/removeStyle' ; import { requestAnimationTimeout , cancelAnimationTimeout } from './requestAnimationTimeout' ; import useUpdateEffect from './useUpdateEffect' ; import useMount from './useMount' ; import { SCROLLBAR_WIDTH , TRANSITION_DURATION , BEZIER } from '../constants' ; import type { ScrollbarInstance } from '../Scrollbar' ; import type { ListenerCallback , RowDataType } from '../@types/common' ; import isSupportTouchEvent from './isSupportTouchEvent' ; const momentumTimeThreshold = 300 ; const momentumYThreshold = 15 ; interface ScrollListenerProps { rtl : boolean ; data : RowDataType [ ] ; height : number ; getTableHeight : ( ) => number ; contentHeight : React . RefObject < number > ; headerHeight : number ; autoHeight : boolean ; tableBodyRef : React . RefObject < HTMLDivElement > ; scrollbarXRef : React . RefObject < ScrollbarInstance > ; scrollbarYRef : React . RefObject < ScrollbarInstance > ; disabledScroll : boolean ; loading : boolean ; tableRef : React . RefObject < HTMLDivElement > ; contentWidth : React . RefObject < number > ; tableWidth : React . RefObject < number > ; scrollY : React . RefObject < number > ; minScrollY : React . RefObject < number > ; minScrollX : React . RefObject < number > ; scrollX : React . RefObject < number > ; setScrollX : ( v : number ) => void ; setScrollY : ( v : number ) => void ; virtualized : boolean ; forceUpdatePosition : ( extDuration ? : number , nextBezier ? : string ) => void ; onScroll : ( scrollX : number , scrollY : number ) => void ; onTouchStart ? : ( event : React . TouchEvent ) => void ; onTouchMove ? : ( event : React . TouchEvent ) => void ; onTouchEnd ? : ( event : React . TouchEvent ) => void ; } /**
 * Calculate the distance of inertial sliding.
 */ const momentum = ( current , start , duration ) => { const deceleration = 0.003 ; const distance = current - start ; const speed = ( 2 * Math . abs ( distance ) ) / duration ; const destination = current + ( speed / deceleration ) * ( distance < 0 ? - 1 : 1 ) ; return { delta : current - destination , duration : TRANSITION_DURATION , bezier : BEZIER } ; } ; param props returns const useScrollListener = ( props ) => { const { data , autoHeight , tableBodyRef , scrollbarXRef , scrollbarYRef , disabledScroll , loading , tableRef , contentWidth , tableWidth , scrollY , minScrollY , minScrollX , scrollX , setScrollX , setScrollY , virtualized , forceUpdatePosition , onScroll , onTouchMove , onTouchStart , onTouchEnd , height , getTableHeight , contentHeight , headerHeight , rtl } = props ; const wheelListener = useRef < ListenerCallback > ( ) ; const touchStartListener = useRef < ListenerCallback > ( ) ; const touchMoveListener = useRef < ListenerCallback > ( ) ; const touchEndListener = useRef < ListenerCallback > ( ) ; const [ isScrolling , setScrolling ] = useState ( false ) ; const touchX = useRef ( 0 ) ; const touchY = useRef ( 0 ) ; const disableEventsTimeoutId = useRef ( null ) ; const isTouching = useRef ( false ) ; const momentumStartTime = useRef ( 0 ) ; const momentumStartY = useRef ( 0 ) ; const shouldHandleWheelX = useCallback ( ( delta ) => { if ( delta === 0 || disabledScroll || loading ) { return false ; } return true ; } , [ disabledScroll , loading ] ) ; const shouldHandleWheelY = useCallback ( ( delta ) => { if ( delta === 0 || disabledScroll || loading ) { return false ; } return ( ( delta >= 0 && scrollY . current > minScrollY . current ) || ( delta < 0 && scrollY . current < 0 ) ) ; } , [ disabledScroll , loading , minScrollY , scrollY ] ) ; const debounceScrollEndedCallback = useCallback ( ( ) => { disableEventsTimeoutId . current = null ; setScrolling ( false ) ; } , [ ] ) ; param deltaX param deltaY param momentumOptions const handleWheel = useCallback ( ( deltaX , deltaY , momentumOptions ? ) => { if ( ! tableRef . current ) { return ; } const nextScrollX = contentWidth . current <= tableWidth . current ? 0 : scrollX . current - deltaX ; const nextScrollY = scrollY . current - deltaY ; const y = Math . min ( 0 , nextScrollY < minScrollY . current ? minScrollY . current : nextScrollY ) ; const x = Math . min ( 0 , nextScrollX < minScrollX . current ? minScrollX . current : nextScrollX ) ; setScrollX ( x ) ; setScrollY ( y ) ; onScroll ?. ( Math . abs ( x ) , Math . abs ( y ) ) ; forceUpdatePosition ( momentumOptions ?. duration , momentumOptions ?. bezier ) ; if ( virtualized ) { setScrolling ( true ) ; if ( disableEventsTimeoutId . current ) { cancelAnimationTimeout ( disableEventsTimeoutId . current ) ; } disableEventsTimeoutId . current = requestAnimationTimeout ( debounceScrollEndedCallback , momentumOptions ?. duration ? 50 : 0 ) ; } } , [ tableRef , contentWidth , tableWidth , scrollX , scrollY , minScrollY , minScrollX , setScrollX , setScrollY , onScroll , forceUpdatePosition , virtualized , debounceScrollEndedCallback ] ) ; const onWheel = useCallback ( ( deltaX , deltaY , momentumOptions ? ) => { handleWheel ( deltaX , deltaY , momentumOptions ) ; scrollbarXRef . current ?. onWheelScroll ?. ( deltaX ) ; scrollbarYRef . current ?. onWheelScroll ?. ( deltaY , momentumOptions ?. duration ? true : false ) ; } , [ handleWheel , scrollbarXRef , scrollbarYRef ] ) ; const wheelHandler = useRef < WheelHandler > ( ) ; const stopScroll = useCallback ( ( ) => { const wheelElement = tableBodyRef . current . querySelector ( '.rs-table-body-wheel-area' ) ; const handleElement = scrollbarYRef . current . handle ; const transitionCSS = [ 'transition-duration' , 'transition-timing-function' ] ; if ( ! virtualized ) { const matrix = window . getComputedStyle ( wheelElement ) . getPropertyValue ( 'transform' ) ; const offsetY = Math . round ( + matrix . split ( ')' ) [ 0 ] . split ( ', ' ) [ 5 ] ) ; setScrollY ( offsetY ) ; } removeStyle ( wheelElement , transitionCSS ) ; removeStyle ( handleElement , transitionCSS ) ; } , [ scrollbarYRef , setScrollY , tableBodyRef , virtualized ] ) ; const handleTouchStart = useCallback ( ( event ) => { const { pageX , pageY } = event . touches [ 0 ] ; touchX . current = pageX ; touchY . current = pageY ; momentumStartTime . current = new Date ( ) . getTime ( ) ; momentumStartY . current = scrollY . current ; isTouching . current = true ; onTouchStart ?. ( event ) ; stopScroll ( ) ; } , [ onTouchStart , scrollY , stopScroll ] ) ; const handleTouchMove = useCallback ( ( event ) => { if ( ! isTouching . current ) { return ; } const { pageX , pageY } = event . touches [ 0 ] ; const deltaX = touchX . current - pageX ; const deltaY = autoHeight ? 0 : touchY . current - pageY ; if ( ! shouldHandleWheelY ( deltaY ) && ! shouldHandleWheelX ( deltaX ) ) { return ; } /**
         * Prevent the default touch event when the table is scrollable.
         * fix: https://github.com/rsuite/rsuite-table/commit/21785fc18f430519ab5885c44540d9ffc30de366#commitcomment-36236425
         */ if ( ! autoHeight && shouldHandleWheelY ( deltaY ) ) { event . preventDefault ?. ( ) ; } const now = new Date ( ) . getTime ( ) ; onWheel ( deltaX , deltaY ) ; touchX . current = pageX ; touchY . current = pageY ; if ( now - momentumStartTime . current > momentumTimeThreshold ) { momentumStartY . current = scrollY . current ; momentumStartTime . current = now ; } onTouchMove ?. ( event ) ; } , [ autoHeight , onWheel , onTouchMove , scrollY , shouldHandleWheelX , shouldHandleWheelY ] ) ; const handleTouchEnd = useCallback ( ( event ) => { if ( ! isTouching . current ) { return ; } isTouching . current = false ; const touchDuration = new Date ( ) . getTime ( ) - momentumStartTime . current ; const absDeltaY = Math . abs ( scrollY . current - momentumStartY . current ) ; if ( touchDuration < momentumTimeThreshold && absDeltaY > momentumYThreshold ) { const { delta , duration , bezier } = momentum ( scrollY . current , momentumStartY . current , touchDuration ) ; onWheel ( 0 , delta , { duration , bezier } ) ; onTouchEnd ?. ( event ) ; } } , [ onWheel , onTouchEnd , scrollY ] ) ; /**
     * When the user uses the tab key in the Table, the onScroll event is triggered,
     * and the scroll bar position should be updated at the same time.
     * https://github.com/rsuite/rsuite/issues/234
     */ const onScrollBody = useCallback ( event => { if ( event . target !== tableBodyRef . current ) { return ; } const left = scrollLeft ( event . target ) ; const top = scrollTop ( event . target ) ; if ( top === 0 && left === 0 ) { return ; } onWheel ( left , top ) ; scrollLeft ( event . target , 0 ) ; scrollTop ( event . target , 0 ) ; } , [ onWheel , tableBodyRef ] ) ; const getControlledScrollTopValue = useCallback ( value => { if ( autoHeight ) { return [ 0 , 0 ] ; } const height = getTableHeight ( ) ; value = Math . min ( value , Math . max ( 0 , contentHeight . current - ( height - headerHeight ) ) ) ; return [ - value , ( value / contentHeight . current ) * ( height - headerHeight ) ] ; } , [ autoHeight , contentHeight , getTableHeight , headerHeight ] ) ; const rerender = ( ) => { setScrolling ( true ) ; setTimeout ( ( ) => setScrolling ( false ) , 0 ) ; } ; const getControlledScrollLeftValue = value => { value = Math . min ( value , Math . max ( 0 , contentWidth . current - tableWidth . current ) ) ; return [ - value , ( value / contentWidth . current ) * tableWidth . current ] ; } ; const onScrollTop = ( top = 0 ) => { const [ nextScrollY , handleScrollY ] = getControlledScrollTopValue ( top ) ; setScrollY ( nextScrollY ) ; scrollbarYRef ?. current ?. resetScrollBarPosition ?. ( handleScrollY ) ; forceUpdatePosition ( ) ; ! loading && onScroll ?. ( Math . abs ( scrollX . current ) , Math . abs ( nextScrollY ) ) ; /**
         * After calling `scrollTop`, a white screen will appear when `virtualized` is true.
         * The reason is that the coordinates of the DOM are directly manipulated,
         * but the component is not re-rendered. Need to call `rerender`.
         * Fix: rsuite#1044
         */ if ( virtualized && contentHeight . current > height ) { rerender ( ) ; } } ; const onScrollLeft = ( left = 0 ) => { const [ nextScrollX , scrollbarX ] = getControlledScrollLeftValue ( left ) ; setScrollX ( nextScrollX ) ; ! loading && onScroll ?. ( Math . abs ( nextScrollX ) , Math . abs ( scrollY . current ) ) ; scrollbarXRef ?. current ?. resetScrollBarPosition ?. ( scrollbarX ) ; forceUpdatePosition ( ) ; } ; const onScrollTo = ( coord ) => { const { x , y } = coord || { } ; if ( typeof x === 'number' ) { onScrollLeft ( x ) ; } if ( typeof y === 'number' ) { onScrollTop ( y ) ; } } ; useUpdateEffect ( ( ) => { if ( scrollY . current !== 0 ) { onScrollTop ( Math . abs ( scrollY . current ) ) ; } } , [ height , data ] ) ; const releaseListeners = useCallback ( ( ) => { wheelHandler . current = null ; wheelListener . current ?. off ( ) ; touchStartListener . current ?. off ( ) ; touchMoveListener . current ?. off ( ) ; touchEndListener . current ?. off ( ) ; } , [ ] ) ; useEffect ( ( ) => { const options = { passive : false } ; const tableBody = tableBodyRef . current ; if ( tableBody ) { releaseListeners ( ) ; wheelHandler . current = new WheelHandler ( onWheel , shouldHandleWheelX , shouldHandleWheelY , false ) ; wheelListener . current = on ( tableBody , 'wheel' , wheelHandler . current . onWheel , options ) ; if ( isSupportTouchEvent ( ) ) { touchStartListener . current = on ( tableBody , 'touchstart' , handleTouchStart , options ) ; touchMoveListener . current = on ( tableBody , 'touchmove' , handleTouchMove , options ) ; touchEndListener . current = on ( tableBody , 'touchend' , handleTouchEnd , options ) ; } } return releaseListeners ; } , [ handleTouchEnd , handleTouchMove , handleTouchStart , onWheel , releaseListeners , shouldHandleWheelX , shouldHandleWheelY , tableBodyRef ] ) ; useMount ( ( ) => { if ( rtl ) { setScrollX ( tableWidth . current - contentWidth . current - SCROLLBAR_WIDTH ) ; scrollbarXRef ?. current ?. resetScrollBarPosition ?. ( - scrollX . current ) ; forceUpdatePosition ( ) ; } } ) ; const onScrollHorizontal = useCallback ( ( delta ) => handleWheel ( delta , 0 ) , [ handleWheel ] ) ; const onScrollVertical = useCallback ( ( delta ) => handleWheel ( 0 , delta ) , [ handleWheel ] ) ; return { isScrolling , onScrollHorizontal , onScrollVertical , onScrollBody , onScrollTop , onScrollLeft , onScrollTo } ; } ; export default useScrollListener ;