import React , { useRef , useEffect } from 'react' ; import { useSprings , animated , SpringConfig } from 'react-spring' ; import { useDrag } from 'react-use-gesture' ; import useResizeObserver from 'use-resize-observer' ; type SliderProps = { children : React . ReactNode [ ] ; index : number ; onIndexChange : ( newIndex : number ) => void ; className ? : string ; style ? : React . CSSProperties ; slideClassName ? : string ; slideStyle ? : React . CSSProperties | ( ( index : number ) => React . CSSProperties ) ; indexRange ? : [ number , number ] ; onDragStart ? : ( pressedIndex : number ) => void ; onDragEnd ? : ( pressedIndex : number ) => void ; onTap ? : ( pressedIndex : number ) => void ; } & typeof defaultProps ; const defaultProps = { enabled : true , vertical : false , slideAlign : 'center' , draggedScale : 1 , draggedSpring : { tension : 1200 , friction : 40 } as SpringConfig , trailingSpring : { tension : 120 , friction : 30 } as SpringConfig , releaseSpring : { tension : 120 , friction : 30 } as SpringConfig , trailingDelay : 50 } ; const slidesWrapperStyle = ( vertical ) : React . CSSProperties => ( { display : 'flex' , flexWrap : 'nowrap' , alignItems : 'stretch' , position : 'relative' , WebkitUserSelect : 'none' , userSelect : 'none' , WebkitTouchCallout : 'none' , flexDirection : vertical ? 'column' : 'row' , touchAction : vertical ? 'pan-x' : 'pan-y' } ) ; const clamp = ( num , clamp , higher ) => Math . min ( Math . max ( num , clamp ) , higher ) ; export const Slider = ( { children , index , onIndexChange , className , style , slideStyle , slideClassName , enabled , vertical , indexRange , slideAlign , draggedScale , draggedSpring , releaseSpring , trailingSpring , trailingDelay , onDragStart , onDragEnd , onTap } ) => { const slideStyleFunc = typeof slideStyle === 'function' ? slideStyle : ( ) => slideStyle ; const root = useRef < HTMLInputElement > ( null ) ; const { width , height } = useResizeObserver ( { ref : root } ) ; const axis = vertical ? 'y' : 'x' ; const size = vertical ? height : width ; let [ minIndex , maxIndex ] = indexRange || [ 0 , children . length - 1 ] ; maxIndex = maxIndex > 0 ? maxIndex : children . length - 1 + maxIndex ; const indexRef = useRef ( index ) ; const restPos = useRef ( 0 ) ; const velocity = useRef ( 0 ) ; const visibleIndexes = useRef ( new Set < number > ( ) ) ; const firstVisibleIndex = useRef ( 0 ) ; const lastVisibleIndex = useRef ( 0 ) ; const cb = slides => { slides . forEach ( ( { isIntersecting , target } ) => visibleIndexes . current [ isIntersecting ? 'add' : 'delete' ] ( Number ( target . getAttribute ( 'data-index' ) ) ) ) ; const visibles = Array . from ( visibleIndexes . current ) . sort ( ) ; firstVisibleIndex . current = visibles [ 0 ] ; lastVisibleIndex . current = visibles [ visibles . length - 1 ] ; } ; const observer = useRef < IntersectionObserver | null > ( null ) ; useEffect ( ( ) => { if ( ! observer . current ) observer . current = new IntersectionObserver ( cb ) ; Array . from ( root . current ! . children ) . forEach ( t => observer . current ! . observe ( t ) ) ; return ( ) => observer . current ! . disconnect ( ) ; } , [ children . length , root ] ) ; const [ springs , set ] = useSprings ( children . length , _i => { return { x : vertical ? 0 : restPos . current , y : vertical ? restPos . current : 0 , s : 1 , zIndex : 0 , immediate : key => key === 'zIndex' } ; } ) ; useEffect ( ( ) => { if ( ! width || ! height ) return ; if ( vertical ) { const { offsetTop , offsetHeight } = root . current ! . children [ index ] as HTMLElement ; restPos . current = Math . round ( - offsetTop + ( height - offsetHeight ) / 2 ) ; } else { const { offsetLeft , offsetWidth } = root . current ! . children [ index ] as HTMLElement ; restPos . current = Math . round ( - offsetLeft + ( width - offsetWidth ) / 2 ) ; } if ( indexRef . current === index ) { set ( _i => ( { [ axis ] : restPos . current , s : 1 , config : { ... releaseSpring , velocity : velocity . current } } ) ) ; } else { const dir = index < indexRef . current ? - 1 : 1 ; const firstToMove = dir > 0 ? firstVisibleIndex . current : lastVisibleIndex . current ; set ( i => { return { [ axis ] : restPos . current , s : 1 , config : releaseSpring , delay : i * dir < firstToMove * dir ? 0 : Math . abs ( firstToMove - i ) * trailingDelay } ; } ) ; } indexRef . current = index ; } , [ index , set , root , vertical , axis , height , width , releaseSpring , draggedSpring , trailingDelay ] ) ; const bind = useDrag ( ( { first , last , tap , vxvy : [ vx , vy ] , delta : [ dx , dy ] , swipe : [ sx , sy ] , movement : [ movX , movY ] , args : [ pressedIndex ] , memo = springs [ pressedIndex ] [ axis ] . getValue ( ) } ) => { if ( tap ) { onTap && onTap ( pressedIndex ) ; return ; } const v = vertical ? vy : vx ; const dir = - Math . sign ( vertical ? dy : dx ) ; const mov = vertical ? movY : movX ; const swipe = vertical ? sy : sx ; if ( first ) { set ( i => { return { [ axis ] : memo + mov , s : draggedScale , config : key => key === axis && i === pressedIndex ? draggedSpring : trailingSpring , zIndex : i === pressedIndex ? 10 : 0 } ; } ) ; onDragStart && onDragStart ( pressedIndex ) ; } else if ( last ) { if ( Math . abs ( mov ) > size ! / 2 || swipe !== 0 ) { indexRef . current = clamp ( indexRef . current + ( mov > 0 ? - 1 : 1 ) , minIndex , maxIndex ) ; } if ( index !== indexRef . current ) { velocity . current = v ; requestAnimationFrame ( ( ) => onIndexChange ( indexRef . current ) ) ; } else set ( ( ) => ( { [ axis ] : restPos . current , s : 1 , config : releaseSpring } ) ) ; onDragEnd && onDragEnd ( pressedIndex ) ; } else { const firstToMove = dir > 0 ? firstVisibleIndex . current : lastVisibleIndex . current ; set ( i => { return { [ axis ] : mov + memo , delay : i * dir < firstToMove * dir || i === pressedIndex ? 0 : Math . abs ( firstToMove - i ) * trailingDelay , config : key => key === axis && i === pressedIndex ? draggedSpring : trailingSpring } ; } ) ; } return memo ; } , { enabled , axis , filterTaps : true } ) ; const rootStyle = slidesWrapperStyle ( vertical ) ; if ( ! className ) rootStyle . width = '100%' ; return ( < div ref = { root } className = { className } style = { { ... rootStyle , ... style } } > { springs . map ( ( { [ axis ] : pos , s , zIndex } , i ) => ( < animated . div { ... bind ( i ) } key = { i } data - index = { i } className = { slideClassName } style = { { [ vertical ? 'justifyContent' : 'alignItems' ] : slideAlign , display : 'flex' , ... slideStyleFunc ( i ) , zIndex , [ axis ] : pos , scale : s , willChange : 'transform' } } > { children [ i ] } < /animated.div>))} < /div>); } ; Slider . defaultProps = defaultProps ;