Skip to content

Commit c3e491f

Browse files
committed
✨ Add React Carousel
1 parent f2cb11d commit c3e491f

File tree

14 files changed

+375
-56
lines changed

14 files changed

+375
-56
lines changed

public/img/placeholder1.png

85.4 KB
Loading

public/img/placeholder2.png

86.2 KB
Loading

public/img/placeholder3.png

69.9 KB
Loading

src/components/Carousel/Carousel.astro

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const {
1919
progress,
2020
pagination,
2121
effect,
22+
debounce = 20,
2223
className,
2324
wrapperClassName,
2425
paginationClassName
@@ -58,7 +59,11 @@ const style = visibleItems > 1
5859
---
5960

6061
<section class:list={classes}>
61-
<div class:list={containerClasses} data-id="w-carousel">
62+
<div
63+
class:list={containerClasses}
64+
data-id="w-carousel"
65+
data-debounce={debounce !== 20 && debounce}
66+
>
6267
<ul
6368
class:list={wrapperClasses}
6469
style={style}
@@ -98,7 +103,7 @@ const style = visibleItems > 1
98103

99104
const carousels = Array.from(document.querySelectorAll('[data-id="w-carousel"]'))
100105

101-
const scroll = debounce((event: Event) => {
106+
const scroll = (event: Event) => {
102107
const target = event.target as HTMLDivElement
103108

104109
if (!target.dataset.paginated) {
@@ -133,10 +138,17 @@ const style = visibleItems > 1
133138
} else {
134139
target.removeAttribute('data-paginated')
135140
}
136-
}, 20)
141+
}
137142

138143
carousels.forEach(carousel => {
139-
carousel.addEventListener('scroll', scroll)
144+
const carouselElement = carousel as HTMLDivElement
145+
const debounceAmount = carouselElement.dataset.debounce
146+
? Number(carouselElement.dataset.debounce)
147+
: 20
148+
149+
carousel.addEventListener('scroll', debounce((event: Event) => {
150+
scroll(event)
151+
}, debounceAmount))
140152
})
141153

142154
listen('paginate', event => {

src/components/Carousel/Carousel.svelte

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import Progress from '../Progress/Progress.svelte'
88
99
import { classNames } from '../../utils/classNames'
10-
import { debounce } from '../../utils/debounce'
10+
import { debounce as debounceScroll } from '../../utils/debounce'
1111
1212
import styles from './carousel.module.scss'
1313
@@ -20,9 +20,11 @@
2020
export let progress: SvelteCarouselProps['progress'] = false
2121
export let pagination: SvelteCarouselProps['pagination'] = {}
2222
export let effect: SvelteCarouselProps['effect'] = null
23+
export let debounce: SvelteCarouselProps['debounce'] = 20
2324
export let className: SvelteCarouselProps['className'] = ''
2425
export let wrapperClassName: SvelteCarouselProps['wrapperClassName'] = ''
2526
export let paginationClassName: SvelteCarouselProps['paginationClassName'] = ''
27+
export let onScroll: SvelteCarouselProps['onScroll'] = () => {}
2628
2729
let carouselContainer: HTMLDivElement
2830
let carousel: HTMLUListElement
@@ -80,9 +82,11 @@
8082
8183
progressValue = percentage * (currentPage - 1)
8284
}
85+
86+
onScroll?.(currentPage)
8387
}
8488
85-
const scroll = debounce((event: Event) => {
89+
const scroll = debounceScroll((event: Event) => {
8690
if (paginated) {
8791
paginated = false
8892
} else {
@@ -95,7 +99,7 @@
9599
96100
updateValues()
97101
}
98-
}, 20)
102+
}, debounce)
99103
100104
const paginate = (event: PaginationEventType) => {
101105
const liElement = carouselItems[event.page - 1] as HTMLLIElement
@@ -134,10 +138,7 @@
134138
class={paginationWrapperClasses}
135139
>
136140
{#if progress}
137-
<Progress
138-
className="w-carousel-progress"
139-
value={progressValue}
140-
/>
141+
<Progress value={progressValue} />
141142
{/if}
142143
<Pagination
143144
type="arrows"

src/components/Carousel/Carousel.tsx

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import React, { useEffect, useRef, useState } from 'react'
2+
import type { ReactCarouselProps } from './carousel'
3+
4+
import ConditionalWrapper from '../ConditionalWrapper/ConditionalWrapper.tsx'
5+
import Pagination from '../Pagination/Pagination.tsx'
6+
import Progress from '../Progress/Progress.tsx'
7+
8+
import { classNames } from '../../utils/classNames'
9+
import { debounce as debounceScroll } from '../../utils/debounce'
10+
11+
import styles from './carousel.module.scss'
12+
13+
import type { PaginationEventType } from '../Pagination/pagination'
14+
15+
const Carousel = ({
16+
items,
17+
visibleItems = 1,
18+
subText,
19+
scrollSnap = true,
20+
progress,
21+
pagination,
22+
effect,
23+
debounce = 20,
24+
className,
25+
wrapperClassName,
26+
paginationClassName,
27+
onScroll,
28+
children
29+
}: ReactCarouselProps) => {
30+
const carouselContainer = useRef<HTMLDivElement>(null)
31+
const carousel = useRef<HTMLUListElement>(null)
32+
const carouselItems = useRef<any>(null)
33+
const paginated = useRef(false)
34+
const currentPage = useRef(1)
35+
36+
const [progressValue, setProgressValue] = useState(0)
37+
const [updatedSubText, setUpdatedSubText] = useState(subText)
38+
39+
const classes = classNames([
40+
styles.carousel,
41+
className
42+
])
43+
44+
const containerClasses = classNames([
45+
styles.container,
46+
scrollSnap && styles.snap
47+
])
48+
49+
const wrapperClasses = classNames([
50+
styles.wrapper,
51+
effect && styles[effect],
52+
wrapperClassName
53+
])
54+
55+
const paginationWrapperClasses = classNames([
56+
styles['pagination-wrapper'],
57+
paginationClassName
58+
])
59+
60+
const paginationClasses = classNames([
61+
styles.pagination,
62+
!subText && paginationClassName
63+
])
64+
65+
const totalPages = Math.ceil(items / visibleItems!)
66+
const subTextValue = subText?.match(/\{0\}|\{1\}/g) ? subText : undefined
67+
const style = visibleItems > 1
68+
? { '--w-slide-width': `${100 / visibleItems}%;` } as React.CSSProperties
69+
: undefined
70+
71+
const updateValues = (page: number) => {
72+
const activeElement = carouselItems.current[page - 1]
73+
74+
Array.from(carouselItems.current).forEach(li => (li as HTMLLIElement).removeAttribute('data-active'))
75+
activeElement.dataset.active = 'true'
76+
77+
if (subTextValue) {
78+
setUpdatedSubText(
79+
subTextValue
80+
.replace('{0}', String(page))
81+
.replace('{1}', String(totalPages))
82+
)
83+
}
84+
85+
if (progress) {
86+
const percentage = (100 / (totalPages - 1))
87+
88+
setProgressValue(percentage * (page - 1))
89+
}
90+
91+
onScroll?.(page)
92+
}
93+
94+
const scroll = debounceScroll((event: Event) => {
95+
if (paginated.current) {
96+
paginated.current = false
97+
} else {
98+
const target = event.target as HTMLDivElement
99+
const scrollLeft = target.scrollLeft
100+
const itemWidth = target.children[0].clientWidth
101+
const page = Math.round(scrollLeft / itemWidth) + 1
102+
103+
currentPage.current = page
104+
105+
updateValues(page)
106+
}
107+
}, debounce)
108+
109+
const paginate = (event: PaginationEventType) => {
110+
const liElement = carouselItems.current[event.page - 1]
111+
112+
liElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
113+
114+
currentPage.current = event.page
115+
paginated.current = true
116+
117+
updateValues(event.page)
118+
}
119+
120+
useEffect(() => {
121+
const usedInAstro = carousel.current?.children[0].nodeName === 'ASTRO-SLOT'
122+
123+
carouselItems.current = usedInAstro
124+
? carousel.current.querySelectorAll('li')
125+
: carousel.current?.children
126+
127+
carouselContainer.current?.addEventListener('scroll', scroll)
128+
129+
return () => {
130+
carouselContainer.current?.removeEventListener('scroll', scroll)
131+
}
132+
}, [])
133+
134+
return (
135+
<section className={classes}>
136+
<div className={containerClasses} ref={carouselContainer}>
137+
<ul className={wrapperClasses} style={style} ref={carousel}>
138+
{children}
139+
</ul>
140+
</div>
141+
<ConditionalWrapper
142+
condition={!!(subText || progress)}
143+
wrapper={children => (
144+
<div className={paginationWrapperClasses}>{children}</div>
145+
)}
146+
>
147+
{progress && (
148+
<Progress value={progressValue} />
149+
)}
150+
<Pagination
151+
type="arrows"
152+
{...pagination}
153+
currentPage={currentPage.current}
154+
totalPages={totalPages}
155+
className={paginationClasses}
156+
onChange={paginate}
157+
/>
158+
{updatedSubText && (
159+
<span className={styles.subtext}>
160+
{updatedSubText
161+
.replace('{0}', '1')
162+
.replace('{1}', String(totalPages))
163+
}
164+
</span>
165+
)}
166+
</ConditionalWrapper>
167+
</section>
168+
)
169+
}
170+
171+
export default Carousel

src/components/Carousel/carousel.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,5 @@
11
import type { PaginationProps } from '../Pagination/pagination'
22

3-
export type CarouselEventType = {
4-
from: string
5-
to: string
6-
current: string
7-
}
8-
93
export type CarouselProps = {
104
items: number
115
visibleItems?: number
@@ -16,16 +10,17 @@ export type CarouselProps = {
1610
progress?: boolean
1711
pagination?: PaginationProps
1812
effect?: 'opacity' | 'saturate' | null
13+
debounce?: number
1914
className?: string
2015
wrapperClassName?: string
2116
paginationClassName?: string
2217
}
2318

2419
export type SvelteCarouselProps = {
25-
onScroll?: (event: CarouselEventType) => void
20+
onScroll?: (event: number) => void
2621
} & CarouselProps
2722

2823
export type ReactCarouselProps = {
29-
onScroll?: (event: CarouselEventType) => void
24+
onScroll?: (event: number) => void
3025
children?: React.ReactNode
3126
} & CarouselProps

0 commit comments

Comments
 (0)