Skip to content

Commit 50e26b5

Browse files
committed
✨ Add responsive itemsPerSlide to Carousel
1 parent 3d5e10c commit 50e26b5

File tree

18 files changed

+373
-47
lines changed

18 files changed

+373
-47
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ The `setup` mixin can also accept the following options:
145145
| `includeUtilities` | `true` | Adds utility classes for CSS. Read more about the available utility classes [here](https://webcoreui.dev/docs/layout). |
146146
| `includeTooltip` | `true` | Adds styles for using tooltips.
147147
| `includeScrollbarStyles` | `true` | Adds styles for scrollbars.
148+
| `includeBreakpoints` | `true` | Exposes breakpoint variables in CSS for JS. Used by components for responsiveness.
148149

149150
Default component styles can be changed by overriding the following CSS variables:
150151

scripts/utilityTypes.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
export const utilityTypes = `
2+
export type Breakpoint = 'xs'
3+
| 'sm'
4+
| 'md'
5+
| 'lg'
6+
27
export type Gap = 'none'
38
| 'xxs'
49
| 'xs'
@@ -153,6 +158,8 @@ declare module 'webcoreui' {
153158
remove: () => void
154159
}
155160
161+
export const getBreakpoint: () => string
162+
156163
export const getLayoutClasses: (config: getLayoutClassesConfig) => string
157164
158165
export const clamp: (num: number, min: number, max: number) => number
@@ -164,6 +171,8 @@ declare module 'webcoreui' {
164171
output: [start: number, end: number]
165172
) => number
166173
174+
export const isOneOf: <T extends string>(values: readonly T[]) => (value: string) => value is T
175+
167176
export const modal: (config: Modal | string) => {
168177
open: () => void
169178
remove: () => void

src/components/Carousel/Carousel.astro

Lines changed: 79 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,14 @@ const containerClasses = [
3535
scrollSnap && styles.snap
3636
]
3737
38+
const getItemsPerSlide = () => typeof itemsPerSlide === 'number'
39+
? itemsPerSlide
40+
: itemsPerSlide.default || 1
41+
3842
const wrapperClasses = [
3943
styles.wrapper,
4044
effect && styles[effect],
41-
itemsPerSlide > 1 && styles['no-snap'],
45+
getItemsPerSlide() > 1 && styles['no-snap'],
4246
wrapperClassName
4347
]
4448
@@ -52,10 +56,10 @@ const paginationClasses = classNames([
5256
!subText && paginationClassName
5357
])
5458
55-
const totalPages = Math.ceil(items / itemsPerSlide)
59+
const totalPages = Math.ceil(items / getItemsPerSlide())
5660
const subTextValue = subText?.match(/\{0\}|\{1\}/g) ? subText : undefined
57-
const style = itemsPerSlide > 1
58-
? `--w-slide-width: calc(${100 / itemsPerSlide}% - 5px);`
61+
const style = getItemsPerSlide() > 1
62+
? `--w-slide-width: calc(${100 / getItemsPerSlide()}% - 5px);`
5963
: null
6064
---
6165

@@ -68,7 +72,8 @@ const style = itemsPerSlide > 1
6872
<ul
6973
class:list={wrapperClasses}
7074
style={style}
71-
data-visible-items={itemsPerSlide > 1 ? itemsPerSlide : null}
75+
data-visible-items={getItemsPerSlide() > 1 ? getItemsPerSlide() : null}
76+
data-breakpoint-items={typeof itemsPerSlide !== 'number' ? JSON.stringify(itemsPerSlide) : null}
7277
>
7378
<slot />
7479
</ul>
@@ -101,6 +106,7 @@ const style = itemsPerSlide > 1
101106
<script>
102107
import { debounce } from '../../utils/debounce'
103108
import { listen } from '../../utils/event'
109+
import { getBreakpoint } from '../../utils/getBreakpoint'
104110

105111
const addEventListeners = () => {
106112
const carousels = Array.from(document.querySelectorAll('[data-id="w-carousel"]'))
@@ -127,7 +133,7 @@ const style = itemsPerSlide > 1
127133
}
128134

129135
for (let i = 0; i < diff; i++) {
130-
triggerButton.click()
136+
triggerButton?.click()
131137
}
132138

133139
Array.from(carouselElement.children).forEach(li => {
@@ -140,12 +146,72 @@ const style = itemsPerSlide > 1
140146
}
141147
}
142148

149+
const updateOnResize = (entries: ResizeObserverEntry[]) => {
150+
const target = entries[0].target
151+
const carousel = target.querySelector<HTMLUListElement>('ul')
152+
const pagination = target.parentElement?.querySelector<HTMLUListElement>('[data-id="w-pagination"]')
153+
const breakpoint = getBreakpoint()
154+
155+
if (!target || !carousel || !pagination) {
156+
return
157+
}
158+
159+
if (carousel.dataset.currentBreakpoint === breakpoint) {
160+
return
161+
}
162+
163+
const progress = target.parentElement?.querySelector<HTMLDivElement>('.w-carousel-progress')
164+
const prevPageButton = pagination.querySelector<HTMLButtonElement>('[data-page="prev"]')
165+
const nextPageButton = pagination.querySelector<HTMLButtonElement>('[data-page="next"]')
166+
const subText = pagination.nextElementSibling
167+
const breakpoints = JSON.parse(carousel.dataset.breakpointItems || '')
168+
const itemsPerSlide = breakpoints[breakpoint] || breakpoints.default
169+
const totalItems = carousel.children.length
170+
const totalPages = Math.ceil(totalItems / itemsPerSlide)
171+
172+
carousel.dataset.currentBreakpoint = breakpoint
173+
carousel.dataset.visibleItems = itemsPerSlide
174+
carousel.children[0].scrollIntoView({ behavior: 'smooth', block: 'nearest' })
175+
carousel.style.setProperty('--w-slide-width', `calc(${100 / itemsPerSlide}% - 5px)`)
176+
177+
pagination.dataset.totalPages = String(totalPages)
178+
179+
if (!(prevPageButton && nextPageButton)) {
180+
pagination.innerHTML = Array.from({ length: totalPages }, (_, i) => i + 1)
181+
.map(i => `
182+
<li>
183+
<button
184+
data-active="${i - 1 === 0}"
185+
data-page="${i}"
186+
aria-label="${`page ${i}`}"
187+
></button>
188+
</li>
189+
`).join('')
190+
}
191+
192+
if (progress && progress.children[0] instanceof HTMLDivElement) {
193+
progress.children[0].style.setProperty('--w-progress-width', '0%')
194+
}
195+
196+
if (subText instanceof HTMLSpanElement && subText?.dataset.text) {
197+
subText.innerText = subText.dataset.text
198+
.replace('{0}', '1')
199+
.replace('{1}', String(totalPages))
200+
}
201+
}
202+
143203
carousels.forEach(carousel => {
144204
const carouselElement = carousel as HTMLDivElement
205+
const carouselList = carousel.querySelector<HTMLUListElement>('ul')
206+
const observer = new ResizeObserver(updateOnResize)
145207
const debounceAmount = carouselElement.dataset.debounce
146208
? Number(carouselElement.dataset.debounce)
147209
: 20
148210

211+
if (carouselList?.dataset.breakpointItems) {
212+
observer.observe(carouselElement)
213+
}
214+
149215
carousel.addEventListener('scroll', debounce((event: Event) => {
150216
scroll(event)
151217
}, debounceAmount))
@@ -169,6 +235,10 @@ const style = itemsPerSlide > 1
169235
.filter(index => index < totalItems)
170236
})
171237

238+
if (indexes.length < event.page) {
239+
return
240+
}
241+
172242
const pageIndex = event.direction === 'prev'
173243
? indexes[event.page - 1][0]
174244
: indexes[event.page - 1][indexes[event.page - 1].length - 1]
@@ -194,15 +264,15 @@ const style = itemsPerSlide > 1
194264
}
195265

196266
if (event.trusted) {
197-
const carouselContaienr = target.closest('section').querySelector('[data-id="w-carousel"]')
267+
const carouselContainer = target.closest('section').querySelector('[data-id="w-carousel"]')
198268

199269
liElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
200270
liElement.dataset.active = 'true'
201271

202-
carouselContaienr.dataset.paginated = 'true'
272+
carouselContainer.dataset.paginated = 'true'
203273

204274
setTimeout(() => {
205-
carouselContaienr.removeAttribute('data-paginated')
275+
carouselContainer.removeAttribute('data-paginated')
206276
}, 300)
207277
}
208278
})

src/components/Carousel/Carousel.svelte

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
99
import { classNames } from '../../utils/classNames'
1010
import { debounce as debounceScroll } from '../../utils/debounce'
11+
import { getBreakpoint } from '../../utils/getBreakpoint'
1112
1213
import styles from './carousel.module.scss'
1314
@@ -29,12 +30,28 @@
2930
onScroll
3031
}: SvelteCarouselProps = $props()
3132
33+
const getItemsPerSlide = () => {
34+
if (carousel) {
35+
return typeof itemsPerSlide === 'number'
36+
? itemsPerSlide
37+
: itemsPerSlide[getBreakpoint()] || itemsPerSlide.default || 1
38+
}
39+
40+
return typeof itemsPerSlide === 'number'
41+
? itemsPerSlide
42+
: itemsPerSlide.default || 1
43+
}
44+
3245
let carouselContainer: HTMLDivElement
3346
let carousel: HTMLUListElement
3447
let carouselItems: HTMLCollection | NodeListOf<HTMLLIElement>
3548
let progressValue = $state(0)
3649
let paginated = false
3750
let currentPage = $state(1)
51+
let totalPages = $state(Math.ceil(items / getItemsPerSlide()))
52+
let style = $state(getItemsPerSlide() > 1
53+
? `--w-slide-width: calc(${100 / getItemsPerSlide()}% - 5px);`
54+
: null)
3855
3956
const classes = classNames([
4057
styles.carousel,
@@ -49,7 +66,7 @@
4966
const wrapperClasses = classNames([
5067
styles.wrapper,
5168
effect && styles[effect],
52-
itemsPerSlide! > 1 && styles['no-snap'],
69+
getItemsPerSlide() > 1 && styles['no-snap'],
5370
wrapperClassName
5471
])
5572
@@ -63,11 +80,7 @@
6380
!subText && paginationClassName
6481
])
6582
66-
const totalPages = Math.ceil(items / itemsPerSlide!)
6783
const subTextValue = subText?.match(/\{0\}|\{1\}/g) ? subText : undefined
68-
const style = itemsPerSlide! > 1
69-
? `--w-slide-width: calc(${100 / itemsPerSlide!}% - 5px);`
70-
: null
7184
7285
const updateValues = () => {
7386
const activeElement = carouselItems[currentPage - 1] as HTMLLIElement
@@ -104,8 +117,8 @@
104117
}, debounce)
105118
106119
const paginate = (event: PaginationEventType) => {
107-
const indexes = Array.from({ length: Math.ceil(items / itemsPerSlide!) }, (_, i) => {
108-
return Array.from({ length: itemsPerSlide! }, (_, j) => (i * itemsPerSlide!) + j)
120+
const indexes = Array.from({ length: Math.ceil(items / getItemsPerSlide()) }, (_, i) => {
121+
return Array.from({ length: getItemsPerSlide() }, (_, j) => (i * getItemsPerSlide()) + j)
109122
.filter(index => index < items)
110123
})
111124
@@ -126,15 +139,33 @@
126139
}, 300)
127140
}
128141
142+
const updateOnResize = () => {
143+
currentPage = 1
144+
progressValue = 0
145+
totalPages = Math.ceil(items / getItemsPerSlide())
146+
style = `--w-slide-width: calc(${100 / getItemsPerSlide()}% - 5px);`
147+
148+
if (subTextValue) {
149+
subText = subTextValue
150+
.replace('{0}', '1')
151+
.replace('{1}', String(totalPages))
152+
}
153+
}
154+
129155
onMount(() => {
130156
const usedInAstro = carousel.children[0].nodeName === 'ASTRO-SLOT'
131-
132-
carouselContainer.addEventListener('scroll', scroll)
157+
const observer = new ResizeObserver(updateOnResize)
133158
134159
carouselItems = usedInAstro
135160
? carousel.querySelectorAll('li')
136161
: carousel.children
137162
163+
carouselContainer.addEventListener('scroll', scroll)
164+
165+
if (typeof itemsPerSlide !== 'number') {
166+
observer.observe(carouselContainer)
167+
}
168+
138169
return () => {
139170
carouselContainer.removeEventListener('scroll', scroll)
140171
}

0 commit comments

Comments
 (0)