Skip to content

Commit f2cb11d

Browse files
committed
✨ Add Carousel component for Svelte
1 parent c40747b commit f2cb11d

File tree

6 files changed

+379
-1
lines changed

6 files changed

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

src/components/Carousel/carousel.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export type CarouselProps = {
1515
scrollSnap?: boolean
1616
progress?: boolean
1717
pagination?: PaginationProps
18-
effect?: 'opacity' | 'saturate'
18+
effect?: 'opacity' | 'saturate' | null
1919
className?: string
2020
wrapperClassName?: string
2121
paginationClassName?: string

src/pages/carousel.astro

+159
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
---
2+
import Box from '@static/Box.astro'
3+
import ComponentWrapper from '@static/ComponentWrapper.astro'
4+
import Layout from '@static/Layout.astro'
5+
6+
import AstroCarousel from '@components/Carousel/Carousel.astro'
7+
import SvelteCarousel from '@components/Carousel/Carousel.svelte'
8+
import ReactCarousel from '@components/Carousel/Carousel.tsx'
9+
10+
import { getSections } from '@helpers'
11+
12+
const sections = getSections({
13+
title: 'carousels',
14+
components: [AstroCarousel, SvelteCarousel, ReactCarousel],
15+
showSubTitle: true
16+
})
17+
---
18+
19+
<Layout>
20+
<h1>Carousel</h1>
21+
<div class="grid md-2 lg-3">
22+
<ComponentWrapper type="Astro">
23+
<AstroCarousel
24+
items={3}
25+
visibleItems={1}
26+
subText="Slide {0} of {1}"
27+
progress={true}
28+
pagination={{ type: 'dots' }}
29+
effect="opacity"
30+
>
31+
<li data-active="true"><Box fullWidth={true}>1</Box></li>
32+
<li><Box fullWidth={true}>2</Box></li>
33+
<li><Box fullWidth={true}>3</Box></li>
34+
</AstroCarousel>
35+
</ComponentWrapper>
36+
37+
<ComponentWrapper type="Svelte">
38+
<SvelteCarousel
39+
client:idle
40+
items={3}
41+
visibleItems={1}
42+
subText="Slide {0} of {1}"
43+
progress={true}
44+
pagination={{ type: 'dots' }}
45+
effect="opacity"
46+
>
47+
<li data-active="true"><Box fullWidth={true}>1</Box></li>
48+
<li><Box fullWidth={true}>2</Box></li>
49+
<li><Box fullWidth={true}>3</Box></li>
50+
</SvelteCarousel>
51+
</ComponentWrapper>
52+
53+
<ComponentWrapper type="React">
54+
<ReactCarousel
55+
client:idle
56+
items={3}
57+
visibleItems={1}
58+
subText="Slide {0} of {1}"
59+
progress={true}
60+
pagination={{ type: 'dots' }}
61+
effect="opacity"
62+
>
63+
<li data-active="true"><Box fullWidth={true}>1</Box></li>
64+
<li><Box fullWidth={true}>2</Box></li>
65+
<li><Box fullWidth={true}>3</Box></li>
66+
</ReactCarousel>
67+
</ComponentWrapper>
68+
</div>
69+
70+
{sections.map(section => (
71+
<h1>{section.title}</h1>
72+
<Fragment>
73+
{section.subTitle && <h2 set:html={section.subTitle} />}
74+
</Fragment>
75+
<div class="grid md-2 lg-3">
76+
<ComponentWrapper title="Default">
77+
<section.component items={3}>
78+
<li><Box fullWidth={true}>1</Box></li>
79+
<li><Box fullWidth={true}>2</Box></li>
80+
<li><Box fullWidth={true}>3</Box></li>
81+
</section.component>
82+
</ComponentWrapper>
83+
84+
<ComponentWrapper title="Carousel with dynamic text">
85+
<section.component items={3} subText="Slide {0} of {1}">
86+
<li><Box fullWidth={true}>1</Box></li>
87+
<li><Box fullWidth={true}>2</Box></li>
88+
<li><Box fullWidth={true}>3</Box></li>
89+
</section.component>
90+
</ComponentWrapper>
91+
92+
<ComponentWrapper title="Scroll snap disabled (mobile only)">
93+
<section.component items={3} scrollSnap={false}>
94+
<li><Box fullWidth={true}>1</Box></li>
95+
<li><Box fullWidth={true}>2</Box></li>
96+
<li><Box fullWidth={true}>3</Box></li>
97+
</section.component>
98+
</ComponentWrapper>
99+
100+
<ComponentWrapper title="Carousel with progress">
101+
<section.component
102+
items={3}
103+
subText="Slide {0} of {1}"
104+
progress={true}
105+
>
106+
<li><Box fullWidth={true}>1</Box></li>
107+
<li><Box fullWidth={true}>2</Box></li>
108+
<li><Box fullWidth={true}>3</Box></li>
109+
</section.component>
110+
</ComponentWrapper>
111+
112+
<ComponentWrapper title="Dots pagination">
113+
<section.component
114+
items={3}
115+
subText="Slide {0} of {1}"
116+
progress={true}
117+
pagination={{ type: 'dots' }}
118+
>
119+
<li><Box fullWidth={true}>1</Box></li>
120+
<li><Box fullWidth={true}>2</Box></li>
121+
<li><Box fullWidth={true}>3</Box></li>
122+
</section.component>
123+
</ComponentWrapper>
124+
125+
<ComponentWrapper title="Custom pagination">
126+
<section.component
127+
items={3}
128+
subText="Slide {0} of {1}"
129+
progress={true}
130+
pagination={{
131+
type: null,
132+
pages: [
133+
{ label: 1, active: true },
134+
{ label: 2 },
135+
{ label: 3 }
136+
]
137+
}}
138+
>
139+
<li><Box fullWidth={true}>1</Box></li>
140+
<li><Box fullWidth={true}>2</Box></li>
141+
<li><Box fullWidth={true}>3</Box></li>
142+
</section.component>
143+
</ComponentWrapper>
144+
145+
<ComponentWrapper title="Wiht opacity transition">
146+
<section.component
147+
items={3}
148+
subText="Slide {0} of {1}"
149+
progress={true}
150+
effect="opacity"
151+
>
152+
<li data-active="true"><Box fullWidth={true}>1</Box></li>
153+
<li><Box fullWidth={true}>2</Box></li>
154+
<li><Box fullWidth={true}>3</Box></li>
155+
</section.component>
156+
</ComponentWrapper>
157+
</div>
158+
))}
159+
</Layout>

src/pages/index.astro

+15
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
---
2+
import Box from '@static/Box.astro'
23
import CardWrapper from '@static/CardWrapper.astro'
34
import Examples from '@static/Examples.astro'
45
import Layout from '@static/Layout.astro'
@@ -8,6 +9,7 @@ import Alert from '@components/Alert/Alert.astro'
89
import Avatar from '@components/Avatar/Avatar.astro'
910
import Badge from '@components/Badge/Badge.astro'
1011
import Button from '@components/Button/Button.astro'
12+
import Carousel from '@components/Carousel/Carousel.astro'
1113
import Checkbox from '@components/Checkbox/Checkbox.astro'
1214
import Collapsible from '@components/Collapsible/Collapsible.astro'
1315
import DataTable from '@components/DataTable/DataTable.astro'
@@ -107,6 +109,15 @@ const tabItems = [{
107109
<CardWrapper title="Card" href="/card">
108110
<p>Paragraph inside a card</p>
109111
</CardWrapper>
112+
<CardWrapper title="Carousel" href="/carousel">
113+
<Carousel
114+
items={3}
115+
pagination={{ type: 'dots' }}
116+
className="carousel-example"
117+
>
118+
<li><Box fullWidth={true}>1</Box></li>
119+
</Carousel>
120+
</CardWrapper>
110121
<CardWrapper title="Checkbox" href="/checkbox">
111122
<Checkbox checked={true} label="Accept terms and conditions" />
112123
</CardWrapper>
@@ -285,6 +296,10 @@ const tabItems = [{
285296
gap: 10px;
286297
}
287298

299+
.carousel-example {
300+
width: 100%;
301+
}
302+
288303
.menu-example {
289304
z-index: 0;
290305
}

0 commit comments

Comments
 (0)