1
1
import React , {
2
+ Children ,
2
3
createContext ,
3
4
forwardRef ,
4
5
HTMLAttributes ,
@@ -9,9 +10,17 @@ import React, {
9
10
import PropTypes from 'prop-types'
10
11
import classNames from 'classnames'
11
12
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
+ }
15
24
16
25
export interface CCarouselProps extends HTMLAttributes < HTMLDivElement > {
17
26
/**
@@ -47,26 +56,29 @@ export interface CCarouselProps extends HTMLAttributes<HTMLDivElement> {
47
56
*/
48
57
onSlideChange ?: ( a : number | string | null ) => void
49
58
/**
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]
51
64
*
52
65
* @type {'slide' | 'crossfade' }
53
66
* @default 'slide'
54
67
*/
55
68
transition ?: 'slide' | 'crossfade'
69
+ /**
70
+ * Set whether the carousel should cycle continuously or have hard stops.
71
+ */
72
+ wrap ?: boolean
56
73
}
57
74
58
75
interface DataType {
59
76
timeout ?: null | ReturnType < typeof setTimeout >
60
77
}
61
78
62
79
export interface ContextProps {
63
- itemsNumber : number
64
- state : [ number | null , number , string ?]
65
- animating : boolean
66
- animate ?: boolean
67
- setItemsNumber : ( a : number ) => void
68
80
setAnimating : ( a : boolean ) => void
69
- setState : ( a : [ number | null , number , string ? ] ) => void
81
+ setCustomInterval : ( a : boolean | number ) => void
70
82
}
71
83
72
84
export const CCarouselContext = createContext ( { } as ContextProps )
@@ -83,40 +95,47 @@ export const CCarousel = forwardRef<HTMLDivElement, CCarouselProps>(
83
95
indicators,
84
96
interval = 5000 ,
85
97
onSlideChange,
98
+ pause = 'hover' ,
86
99
transition,
100
+ wrap = true ,
87
101
...rest
88
102
} ,
89
103
ref ,
90
104
) => {
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 )
93
110
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 > ( )
94
115
95
- const data = useRef < DataType > ( { } ) . current
116
+ useEffect ( ( ) => {
117
+ setItemsNumber ( Children . toArray ( children ) . length )
118
+ } )
96
119
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 ] )
108
127
109
128
useEffect ( ( ) => {
110
- setState ( [ state [ 1 ] , index ] )
111
- } , [ index ] )
129
+ onSlideChange && onSlideChange ( active )
130
+ } , [ active ] )
112
131
113
132
useEffect ( ( ) => {
114
- onSlideChange && onSlideChange ( state [ 1 ] )
115
- cycle ( )
133
+ window . addEventListener ( 'scroll' , handleScroll )
134
+
116
135
return ( ) => {
117
- pause ( )
136
+ window . removeEventListener ( 'scroll' , handleScroll )
118
137
}
119
- } , [ state ] )
138
+ } )
120
139
121
140
const _className = classNames (
122
141
'carousel slide' ,
@@ -125,25 +144,119 @@ export const CCarousel = forwardRef<HTMLDivElement, CCarouselProps>(
125
144
className ,
126
145
)
127
146
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
+
128
210
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
+ >
130
218
< CCarouselContext . Provider
131
219
value = { {
132
- state,
133
- setState,
134
- animate,
135
- itemsNumber,
136
- setItemsNumber,
137
- animating,
138
220
setAnimating,
221
+ setCustomInterval,
139
222
} }
140
223
>
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 >
143
252
{ controls && (
144
253
< >
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 >
147
260
</ >
148
261
) }
149
262
</ CCarouselContext . Provider >
@@ -162,7 +275,9 @@ CCarousel.propTypes = {
162
275
indicators : PropTypes . bool ,
163
276
interval : PropTypes . oneOfType ( [ PropTypes . bool , PropTypes . number ] ) ,
164
277
onSlideChange : PropTypes . func ,
278
+ pause : PropTypes . oneOf ( [ false , 'hover' ] ) ,
165
279
transition : PropTypes . oneOf ( [ 'slide' , 'crossfade' ] ) ,
280
+ wrap : PropTypes . bool ,
166
281
}
167
282
168
283
CCarousel . displayName = 'CCarousel'
0 commit comments