Skip to content

Commit 40d634a

Browse files
committed
feat(CCarousel): add pause, wrap properties, and indivuadl interval for items
1 parent 96b56dc commit 40d634a

12 files changed

+249
-298
lines changed

docs/4.0/api/CCarousel.api.mdx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,6 @@
88
| **indicators** | Adding indicators at the bottom of the carousel for each item. | `boolean` | - |
99
| **interval** | The amount of time to delay between automatically cycling an item. If false, carousel will not automatically cycle. | `number | boolean` | 5000 |
1010
| **onSlideChange** | On slide change callback. | `(a: string | number) => void` | - |
11-
| **transition** | On slide change callback. | `{'slide' | 'crossfade'}` | 'slide' |
11+
| **pause** | 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. | `boolean | "hover"` | hover |
12+
| **transition** | Set type of the transition. | `{'slide' | 'crossfade'}` | 'slide' |
13+
| **wrap** | Set whether the carousel should cycle continuously or have hard stops. | `boolean` | true |

docs/4.0/api/CCarouselControl.api.mdx

Lines changed: 0 additions & 4 deletions
This file was deleted.

docs/4.0/api/CCarouselIndicators.api.mdx

Lines changed: 0 additions & 4 deletions
This file was deleted.

docs/4.0/api/CCarouselInner.api.mdx

Lines changed: 0 additions & 3 deletions
This file was deleted.

docs/4.0/api/CCarouselItem.api.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
| Property | Description | Type | Default |
22
| --- | --- | --- | --- |
33
| **className** | A string of all className you want applied to the base component. | `string` | - |
4+
| **interval** | The amount of time to delay between automatically cycling an item. | `number | boolean` | false |

docs/4.0/components/carousel.mdx

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,6 @@ import {
1010
CCallout,
1111
CCarousel,
1212
CCarouselCaption,
13-
CCarouselControl,
14-
CCarouselIndicators,
15-
CCarouselInner,
1613
CCarouselItem,
1714
CImage,
1815
} from './../../../src/index.ts'
@@ -39,7 +36,7 @@ Carousels don't automatically normalize slide dimensions. As such, you may want
3936
<CImage className="d-block w-100" src={ReactImg} alt="slide 1" />
4037
</CCarouselItem>
4138
<CCarouselItem>
42-
<CImage className="d-block w-100" src={VueImg} alt="slide 2" />
39+
<CImage className="d-block w-100" src={VueImg} alt="slide 2"/>
4340
</CCarouselItem>
4441
<CCarouselItem>
4542
<CImage className="d-block w-100" src={AngularImg} alt="slide 3" />
@@ -279,18 +276,6 @@ Add `dark` property to the `CCarousel` for darker controls, indicators, and capt
279276

280277
`markdown:CCarouselCaption.api.mdx`
281278

282-
### CCarouselControl
283-
284-
`markdown:CCarouselControl.api.mdx`
285-
286-
### CCarouselIndicators
287-
288-
`markdown:CCarouselIndicators.api.mdx`
289-
290-
### CCarouselInner
291-
292-
`markdown:CCarouselInner.api.mdx`
293-
294279
### CCarouselItem
295280

296281
`markdown:CCarouselItem.api.mdx`

src/components/carousel/CCarousel.tsx

Lines changed: 156 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React, {
2+
Children,
23
createContext,
34
forwardRef,
45
HTMLAttributes,
@@ -9,9 +10,17 @@ import React, {
910
import PropTypes from 'prop-types'
1011
import 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

1625
export 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

5875
interface DataType {
5976
timeout?: null | ReturnType<typeof setTimeout>
6077
}
6178

6279
export 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

7284
export 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

168283
CCarousel.displayName = 'CCarousel'

src/components/carousel/CCarouselControl.tsx

Lines changed: 0 additions & 56 deletions
This file was deleted.

0 commit comments

Comments
 (0)