import React , { useState , useEffect , useRef , useCallback , useImperativeHandle } from 'react' ; import clsx from 'clsx' ; import { CarouselItem } from './Item' ; import { setTransform , setTransition } from '../../utils/style' ; import canUse from '../../utils/canUse' ; export interface CarouselProps { children : React . ReactNode ; className ? : string ; startIndex ? : number ; draggable ? : boolean ; clickDragThreshold ? : number ; duration ? : number ; easing ? : string ; threshold ? : number ; loop ? : boolean ; rtl ? : boolean ; autoPlay ? : boolean ; interval ? : number ; dots ? : boolean ; onChange ? : ( activeIndex ? : number ) => void ; autoplay ? : boolean ; autoplaySpeed ? : number ; indicators ? : boolean ; } export interface CarouselHandle { goTo : ( n : number ) => void ; prev : ( ) => void ; next : ( ) => void ; } interface State { first : boolean ; wrapWidth : number ; hover : boolean ; startX : number ; endX : number ; startY : number ; canMove : boolean | null ; pressDown : boolean ; } type DragEvent = React . TouchEvent < HTMLDivElement > | React . MouseEvent < HTMLDivElement , MouseEvent > ; const formElements = [ 'TEXTAREA' , 'OPTION' , 'INPUT' , 'SELECT' ] ; const canTouch = canUse ( 'touch' ) ; export const Carousel = React . forwardRef < CarouselHandle , CarouselProps > ( ( props , ref ) => { const { className , startIndex = 0 , draggable = true , duration = 300 , easing = 'ease' , threshold = 20 , clickDragThreshold = 10 , loop = true , rtl = false , autoPlay = props . autoplay || false , interval = props . autoplaySpeed || 4000 , dots = props . indicators || true , onChange , children , } = props ; const count = React . Children . count ( children ) ; const itemWith = `${ 100 / count }%` ; const wrapperRef = useRef < HTMLDivElement > ( null ! ) ; const innerRef = useRef < HTMLDivElement > ( null ! ) ; const autoPlayTimerRef = useRef < any > ( null ) ; const stateRef = useRef < State > ( { first : true , wrapWidth : 0 , hover : false , startX : 0 , endX : 0 , startY : 0 , canMove : null , pressDown : false , } ) ; const getIndex = useCallback ( ( idx ) => ( loop ? idx % count : Math . max ( 0 , Math . min ( idx , count - 1 ) ) ) , [ count , loop ] ) ; const [ activeIndex , setActiveIndex ] = useState ( getIndex ( startIndex ) ) ; const [ isDragging , setDragging ] = useState ( false ) ; const enableTransition = useCallback ( ( ) => { setTransition ( innerRef . current , `transform ${ duration }ms ${ easing }` ) ; } , [ duration , easing ] ) ; const disableTransition = ( ) => { setTransition ( innerRef . current , 'transform 0s' ) ; } ; const moveX = ( x ) => { setTransform ( innerRef . current , `translate3d(${ x }px, 0, 0)` ) ; } ; const slideTo = useCallback ( ( idx , smooth ? ) => { const nextIndex = loop ? idx + 1 : idx ; const offset = ( rtl ? 1 : - 1 ) * nextIndex * stateRef . current . wrapWidth ; if ( smooth ) { requestAnimationFrame ( ( ) => { requestAnimationFrame ( ( ) => { enableTransition ( ) ; moveX ( offset ) ; } ) ; } ) ; } else { moveX ( offset ) ; } } , [ enableTransition , loop , rtl ] ) ; const goTo = useCallback ( ( idx ) => { if ( count <= 1 ) { return ; } const nextIndex = getIndex ( idx ) ; if ( nextIndex !== activeIndex ) { setActiveIndex ( nextIndex ) ; } } , [ activeIndex , count , getIndex ] ) ; const prev = useCallback ( ( ) => { if ( count <= 1 ) { return ; } let nextIndex = activeIndex - 1 ; if ( loop ) { if ( nextIndex < 0 ) { const state = stateRef . current ; const moveTo = count + 1 ; const offset = ( rtl ? 1 : - 1 ) * moveTo * state . wrapWidth ; const dragDist = draggable ? state . endX - state . startX : 0 ; disableTransition ( ) ; moveX ( offset + dragDist ) ; nextIndex = count - 1 ; } } else { nextIndex = Math . max ( nextIndex , 0 ) ; } if ( nextIndex !== activeIndex ) { setActiveIndex ( nextIndex ) ; } } , [ activeIndex , count , draggable , loop , rtl ] ) ; const next = useCallback ( ( ) => { if ( count <= 1 ) { return ; } let nextIndex = activeIndex + 1 ; if ( loop ) { const isClone = nextIndex > count - 1 ; if ( isClone ) { nextIndex = 0 ; const state = stateRef . current ; const dragDist = draggable ? state . endX - state . startX : 0 ; disableTransition ( ) ; moveX ( dragDist ) ; } } else { nextIndex = Math . min ( nextIndex , count - 1 ) ; } if ( nextIndex !== activeIndex ) { setActiveIndex ( nextIndex ) ; } } , [ activeIndex , count , draggable , loop ] ) ; const doAutoPlay = useCallback ( ( ) => { if ( ! autoPlay || stateRef . current . hover ) { return ; } autoPlayTimerRef . current = setTimeout ( ( ) => { enableTransition ( ) ; next ( ) ; } , interval ) ; } , [ autoPlay , interval , enableTransition , next ] ) ; const clearAutoPlay = ( ) => { clearTimeout ( autoPlayTimerRef . current ) ; } ; const resetToCurrent = ( ) => { slideTo ( activeIndex , true ) ; doAutoPlay ( ) ; } ; const updateAfterDrag = ( ) => { const state = stateRef . current ; const offset = ( rtl ? - 1 : 1 ) * ( state . endX - state . startX ) ; const offsetDist = Math . abs ( offset ) ; const isClone1 = offset > 0 && activeIndex - 1 < 0 ; const isClone2 = offset < 0 && activeIndex + 1 > count - 1 ; if ( isClone1 || isClone2 ) { if ( loop ) { if ( isClone1 ) { prev ( ) ; } else { next ( ) ; } } else { resetToCurrent ( ) ; } } else if ( offset > 0 && offsetDist > threshold && count > 1 ) { prev ( ) ; } else if ( offset < 0 && offsetDist > threshold && count > 1 ) { next ( ) ; } else { resetToCurrent ( ) ; } } ; const resetDrag = ( ) => { const state = stateRef . current ; state . startX = 0 ; state . endX = 0 ; state . startY = 0 ; state . canMove = null ; state . pressDown = false ; } ; const dragStart = ( e ) => { if ( formElements . includes ( ( e . target as Element ) . nodeName ) ) { return ; } e . preventDefault ( ) ; e . stopPropagation ( ) ; const ev = 'touches' in e ? e . touches [ 0 ] : e ; const state = stateRef . current ; state . pressDown = true ; state . startX = ev . pageX ; state . startY = ev . pageY ; clearAutoPlay ( ) ; } ; const dragMove = ( e ) => { e . stopPropagation ( ) ; const ev = 'touches' in e ? e . touches [ 0 ] : e ; const state = stateRef . current ; if ( state . pressDown ) { if ( 'touches' in e ) { if ( state . canMove === null ) { state . canMove = Math . abs ( state . startY - ev . pageY ) < Math . abs ( state . startX - ev . pageX ) ; } if ( ! state . canMove ) { return ; } } e . preventDefault ( ) ; disableTransition ( ) ; state . endX = ev . pageX ; const nextIndex = loop ? activeIndex + 1 : activeIndex ; const nextOffset = nextIndex * state . wrapWidth ; const dragOffset = state . endX - state . startX ; if ( ! isDragging && Math . abs ( dragOffset ) > clickDragThreshold ) { setDragging ( true ) ; } const offset = rtl ? nextOffset + dragOffset : dragOffset - nextOffset ; moveX ( offset ) ; } } ; const dragEnd = ( e ) => { e . stopPropagation ( ) ; const state = stateRef . current ; state . pressDown = false ; setDragging ( false ) ; enableTransition ( ) ; if ( state . endX ) { updateAfterDrag ( ) ; } else { doAutoPlay ( ) ; } resetDrag ( ) ; } ; const onMouseEnter = ( ) => { stateRef . current . hover = true ; clearAutoPlay ( ) ; } ; const onMouseLeave = ( e ) => { const state = stateRef . current ; state . hover = false ; if ( state . pressDown ) { state . pressDown = false ; state . endX = e . pageX ; enableTransition ( ) ; updateAfterDrag ( ) ; resetDrag ( ) ; } doAutoPlay ( ) ; } ; const handleClickDot = ( e ) => { const { slideTo : i } = e . currentTarget . dataset ; if ( i ) { const idx = parseInt ( i , 10 ) ; goTo ( idx ) ; } e . preventDefault ( ) ; } ; useImperativeHandle ( ref , ( ) => ( { goTo , prev , next , } ) , [ goTo , prev , next ] ) ; useEffect ( ( ) => { function handleResize ( ) { stateRef . current . wrapWidth = wrapperRef . current . offsetWidth ; slideTo ( activeIndex ) ; } if ( stateRef . current . first ) { handleResize ( ) ; } window . addEventListener ( 'resize' , handleResize ) ; return ( ) => { window . removeEventListener ( 'resize' , handleResize ) ; } ; } , [ activeIndex , slideTo ] ) ; useEffect ( ( ) => { if ( onChange && ! stateRef . current . first ) { onChange ( activeIndex ) ; } } , [ activeIndex , onChange ] ) ; useEffect ( ( ) => { if ( stateRef . current . first ) { slideTo ( activeIndex ) ; stateRef . current . first = false ; } else { slideTo ( activeIndex , true ) ; } } , [ activeIndex , slideTo ] ) ; useEffect ( ( ) => { doAutoPlay ( ) ; return ( ) => { clearAutoPlay ( ) ; } ; } , [ autoPlay , activeIndex , doAutoPlay ] ) ; let events ; if ( draggable ) { events = canTouch ? { onTouchStart : dragStart , onTouchMove : dragMove , onTouchEnd : dragEnd , } : { onMouseDown : dragStart , onMouseMove : dragMove , onMouseUp : dragEnd , onMouseEnter , onMouseLeave , } ; } else { events = { onMouseEnter , onMouseLeave , } ; } return ( < div className = { clsx ( 'Carousel' , { 'Carousel--draggable' : draggable , 'Carousel--rtl' : rtl , 'Carousel--dragging' : isDragging , } , className ) } ref = { wrapperRef } { ... events } > < div className = "Carousel-inner" style = { { width : `${ loop ? count + 2 : count }00%` } } ref = { innerRef } > { loop && ( < CarouselItem width = { itemWith } > { React . Children . toArray ( children ) [ count - 1 ] } < /CarouselItem>)} { React . Children . map ( children , ( item , i ) => ( < CarouselItem width = { itemWith } key = { i } > { item } < /CarouselItem>))} { loop && ( < CarouselItem width = { itemWith } > { React . Children . toArray ( children ) [ 0 ] } < /CarouselItem>)} < /div> { dots && ( < ol className = "Carousel-dots" > { React . Children . map ( children , ( _ , i ) => ( < li key = { i } > < button className = { clsx ( 'Carousel-dot' , { active : activeIndex === i } ) } type = "button" aria - label = { `Go to slide ${ i + 1 }` } data - slide - to = { i } onClick = { handleClickDot } / > < /li>))} < /ol>)} < /div>); } ) ;