11import React , {
2+ Children ,
23 createContext ,
34 forwardRef ,
45 HTMLAttributes ,
@@ -9,9 +10,17 @@ import React, {
910import PropTypes from 'prop-types'
1011import classNames from 'classnames'
1112
12- import { CCarouselControl } from './CCarouselControl'
13- import { CCarouselIndicators } from './CCarouselIndicators'
14- import { CCarouselInner } from './CCarouselInner'
13+ import { useForkedRef } from '../../utils/hooks'
14+
15+ const isVisible = ( element : HTMLDivElement ) => {
16+ const rect = element . getBoundingClientRect ( )
17+ return (
18+ rect . top >= 0 &&
19+ rect . left >= 0 &&
20+ rect . bottom <= ( window . innerHeight || document . documentElement . clientHeight ) &&
21+ rect . right <= ( window . innerWidth || document . documentElement . clientWidth )
22+ )
23+ }
1524
1625export interface CCarouselProps extends HTMLAttributes < HTMLDivElement > {
1726 /**
@@ -47,26 +56,29 @@ export interface CCarouselProps extends HTMLAttributes<HTMLDivElement> {
4756 */
4857 onSlideChange ?: ( a : number | string | null ) => void
4958 /**
50- * On slide change callback. [docs]
59+ * If set to 'hover', pauses the cycling of the carousel on mouseenter and resumes the cycling of the carousel on mouseleave. If set to false, hovering over the carousel won't pause it.
60+ */
61+ pause ?: boolean | 'hover'
62+ /**
63+ * Set type of the transition. [docs]
5164 *
5265 * @type {'slide' | 'crossfade' }
5366 * @default 'slide'
5467 */
5568 transition ?: 'slide' | 'crossfade'
69+ /**
70+ * Set whether the carousel should cycle continuously or have hard stops.
71+ */
72+ wrap ?: boolean
5673}
5774
5875interface DataType {
5976 timeout ?: null | ReturnType < typeof setTimeout >
6077}
6178
6279export interface ContextProps {
63- itemsNumber : number
64- state : [ number | null , number , string ?]
65- animating : boolean
66- animate ?: boolean
67- setItemsNumber : ( a : number ) => void
6880 setAnimating : ( a : boolean ) => void
69- setState : ( a : [ number | null , number , string ? ] ) => void
81+ setCustomInterval : ( a : boolean | number ) => void
7082}
7183
7284export const CCarouselContext = createContext ( { } as ContextProps )
@@ -83,40 +95,47 @@ export const CCarousel = forwardRef<HTMLDivElement, CCarouselProps>(
8395 indicators,
8496 interval = 5000 ,
8597 onSlideChange,
98+ pause = 'hover' ,
8699 transition,
100+ wrap = true ,
87101 ...rest
88102 } ,
89103 ref ,
90104 ) => {
91- const [ state , setState ] = useState < [ number | null , number , string ?] > ( [ null , index ] )
92- const [ itemsNumber , setItemsNumber ] = useState < number > ( 0 )
105+ const carouselRef = useRef < HTMLDivElement > ( null )
106+ const forkedRef = useForkedRef ( ref , carouselRef )
107+ const data = useRef < DataType > ( { } ) . current
108+
109+ const [ active , setActive ] = useState < number > ( 0 )
93110 const [ animating , setAnimating ] = useState < boolean > ( false )
111+ const [ customInterval , setCustomInterval ] = useState < boolean | number > ( )
112+ const [ direction , setDirection ] = useState < string > ( 'next' )
113+ const [ itemsNumber , setItemsNumber ] = useState < number > ( 0 )
114+ const [ visible , setVisible ] = useState < boolean > ( )
94115
95- const data = useRef < DataType > ( { } ) . current
116+ useEffect ( ( ) => {
117+ setItemsNumber ( Children . toArray ( children ) . length )
118+ } )
96119
97- const cycle = ( ) => {
98- pause ( )
99- if ( typeof interval === 'number' ) {
100- data . timeout = setTimeout ( ( ) => nextItem ( ) , interval )
101- }
102- }
103- const pause = ( ) => data . timeout && clearTimeout ( data . timeout )
104- const nextItem = ( ) => {
105- if ( typeof state [ 1 ] === 'number' )
106- setState ( [ state [ 1 ] , itemsNumber === state [ 1 ] + 1 ? 0 : state [ 1 ] + 1 , 'next' ] )
107- }
120+ useEffect ( ( ) => {
121+ visible && cycle ( )
122+ } , [ visible ] )
123+
124+ useEffect ( ( ) => {
125+ ! animating && cycle ( )
126+ } , [ animating ] )
108127
109128 useEffect ( ( ) => {
110- setState ( [ state [ 1 ] , index ] )
111- } , [ index ] )
129+ onSlideChange && onSlideChange ( active )
130+ } , [ active ] )
112131
113132 useEffect ( ( ) => {
114- onSlideChange && onSlideChange ( state [ 1 ] )
115- cycle ( )
133+ window . addEventListener ( 'scroll' , handleScroll )
134+
116135 return ( ) => {
117- pause ( )
136+ window . removeEventListener ( 'scroll' , handleScroll )
118137 }
119- } , [ state ] )
138+ } )
120139
121140 const _className = classNames (
122141 'carousel slide' ,
@@ -125,25 +144,119 @@ export const CCarousel = forwardRef<HTMLDivElement, CCarouselProps>(
125144 className ,
126145 )
127146
147+ const cycle = ( ) => {
148+ _pause ( )
149+ if ( ! wrap && active === itemsNumber - 1 ) {
150+ return
151+ }
152+
153+ if ( typeof interval === 'number' ) {
154+ data . timeout = setTimeout (
155+ ( ) => nextItemWhenVisible ( ) ,
156+ typeof customInterval === 'number' ? customInterval : interval ,
157+ )
158+ }
159+ }
160+ const _pause = ( ) => pause && data . timeout && clearTimeout ( data . timeout )
161+
162+ const nextItemWhenVisible = ( ) => {
163+ // Don't call next when the page isn't visible
164+ // or the carousel or its parent isn't visible
165+ if ( ! document . hidden && carouselRef . current && isVisible ( carouselRef . current ) ) {
166+ if ( animating ) {
167+ return
168+ }
169+ handleControlClick ( 'next' )
170+ }
171+ }
172+
173+ const handleControlClick = ( direction : string ) => {
174+ if ( animating ) {
175+ return
176+ }
177+ setDirection ( direction )
178+ if ( direction === 'next' ) {
179+ active === itemsNumber - 1 ? setActive ( 0 ) : setActive ( active + 1 )
180+ } else {
181+ active === 0 ? setActive ( itemsNumber - 1 ) : setActive ( active - 1 )
182+ }
183+ }
184+
185+ const handleIndicatorClick = ( index : number ) => {
186+ if ( active === index ) {
187+ return
188+ }
189+
190+ if ( active < index ) {
191+ setDirection ( 'next' )
192+ setActive ( index )
193+ return
194+ }
195+
196+ if ( active > index ) {
197+ setDirection ( 'prev' )
198+ setActive ( index )
199+ }
200+ }
201+
202+ const handleScroll = ( ) => {
203+ if ( ! document . hidden && carouselRef . current && isVisible ( carouselRef . current ) ) {
204+ setVisible ( true )
205+ } else {
206+ setVisible ( false )
207+ }
208+ }
209+
128210 return (
129- < div className = { _className } onMouseEnter = { pause } onMouseLeave = { cycle } { ...rest } ref = { ref } >
211+ < div
212+ className = { _className }
213+ onMouseEnter = { _pause }
214+ onMouseLeave = { cycle }
215+ { ...rest }
216+ ref = { forkedRef }
217+ >
130218 < CCarouselContext . Provider
131219 value = { {
132- state,
133- setState,
134- animate,
135- itemsNumber,
136- setItemsNumber,
137- animating,
138220 setAnimating,
221+ setCustomInterval,
139222 } }
140223 >
141- { indicators && < CCarouselIndicators /> }
142- < CCarouselInner > { children } </ CCarouselInner >
224+ { indicators && (
225+ < ol className = "carousel-indicators" >
226+ { Array . from ( { length : itemsNumber } , ( _ , i ) => i ) . map ( ( index ) => {
227+ return (
228+ < li
229+ key = { `indicator${ index } ` }
230+ onClick = { ( ) => {
231+ ! animating && handleIndicatorClick ( index )
232+ } }
233+ className = { active === index ? 'active' : '' }
234+ data-coreui-target = ""
235+ />
236+ )
237+ } ) }
238+ </ ol >
239+ ) }
240+ < div className = "carousel-inner" >
241+ { Children . map ( children , ( child , index ) => {
242+ if ( React . isValidElement ( child ) ) {
243+ return React . cloneElement ( child , {
244+ active : active === index ? true : false ,
245+ direction : direction ,
246+ key : index ,
247+ } )
248+ }
249+ return
250+ } ) }
251+ </ div >
143252 { controls && (
144253 < >
145- < CCarouselControl direction = "prev" />
146- < CCarouselControl direction = "next" />
254+ < button className = "carousel-control-prev" onClick = { ( ) => handleControlClick ( 'prev' ) } >
255+ < span className = { `carousel-control-prev-icon` } aria-label = "prev" />
256+ </ button >
257+ < button className = "carousel-control-next" onClick = { ( ) => handleControlClick ( 'next' ) } >
258+ < span className = { `carousel-control-next-icon` } aria-label = "next" />
259+ </ button >
147260 </ >
148261 ) }
149262 </ CCarouselContext . Provider >
@@ -162,7 +275,9 @@ CCarousel.propTypes = {
162275 indicators : PropTypes . bool ,
163276 interval : PropTypes . oneOfType ( [ PropTypes . bool , PropTypes . number ] ) ,
164277 onSlideChange : PropTypes . func ,
278+ pause : PropTypes . oneOf ( [ false , 'hover' ] ) ,
165279 transition : PropTypes . oneOf ( [ 'slide' , 'crossfade' ] ) ,
280+ wrap : PropTypes . bool ,
166281}
167282
168283CCarousel . displayName = 'CCarousel'
0 commit comments