Skip to content

Commit 6c5a921

Browse files
authored
Carousel (#1711)
* fix: carousel navigation * carousel styling with classes * fix: rabbitcode suggestions * fix: more rabbitcode suggestions * fix: even more rabbitcode suggestions * fix: even more rabbitcode suggestions
1 parent 8cdd0e9 commit 6c5a921

File tree

9 files changed

+105
-110
lines changed

9 files changed

+105
-110
lines changed

src/lib/carousel/Carousel.svelte

Lines changed: 38 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,47 @@
11
<script lang="ts">
22
import { type CarouselProps, type State, Slide } from "$lib";
3-
import { getTheme } from "$lib/theme/themeUtils";
3+
import { getTheme, warnThemeDeprecation } from "$lib/theme/themeUtils";
44
import clsx from "clsx";
55
import { onMount, setContext } from "svelte";
66
import { canChangeSlide } from "./CarouselSlide";
77
import { carousel } from "./theme";
88

99
const SLIDE_DURATION_RATIO = 0.25;
1010

11-
let { children, slide, images, index = $bindable(0), slideDuration = 1000, transition, duration = 0, "aria-label": ariaLabel = "Draggable Carousel", disableSwipe = false, imgClass = "", class: className, onchange, divClass, isPreload = false, ...restProps }: CarouselProps = $props();
11+
let { children, slide, images, index = $bindable(0), slideDuration = 1000, slideFit, transition, duration = 0, "aria-label": ariaLabel = "Draggable Carousel", disableSwipe = false, imgClass = "", class: className, classes, onchange, isPreload = false, ...restProps }: CarouselProps = $props();
1212

13+
warnThemeDeprecation("Carousel", { imgClass }, { imgClass: "slide" });
14+
15+
const styling = $derived(classes ?? { slide: imgClass });
16+
17+
// // Theme context
1318
const theme = getTheme("carousel");
1419

15-
const _state: State = $state({ images, index: index ?? 0, forward: true, slideDuration, lastSlideChange: new Date() });
20+
let { base, slide: slideCls } = $derived(carousel());
21+
22+
const changeSlide = (n: number) => {
23+
if (images.length === 0) return;
24+
25+
if (n % images.length === _state.index) return;
26+
27+
if (!canChangeSlide({ lastSlideChange: _state.lastSlideChange, slideDuration, slideDurationRatio: SLIDE_DURATION_RATIO })) return;
28+
29+
_state.forward = n >= _state.index;
30+
_state.index = (images.length + n) % images.length;
31+
_state.lastSlideChange = new Date();
32+
33+
index = _state.index; // Update the bindable index
34+
onchange?.(images[_state.index]);
35+
};
36+
37+
const _state: State = $state({ images, index: index ?? 0, forward: true, slideDuration, lastSlideChange: new Date(), changeSlide });
1638

1739
setContext("state", _state);
1840

1941
let initialized = false;
2042

2143
$effect(() => {
22-
index = _state.index;
23-
onchange?.(images[index]);
44+
changeSlide(index);
2445
});
2546

2647
onMount(() => {
@@ -29,23 +50,11 @@
2950
});
3051

3152
const nextSlide = () => {
32-
if (!canChangeSlide({ lastSlideChange: _state.lastSlideChange, slideDuration, slideDurationRatio: SLIDE_DURATION_RATIO })) return _state;
33-
34-
_state.forward = true;
35-
_state.index = _state.index >= images.length - 1 ? 0 : _state.index + 1;
36-
_state.lastSlideChange = new Date();
37-
38-
return _state;
53+
changeSlide(_state.index + 1);
3954
};
4055

4156
const prevSlide = () => {
42-
if (!canChangeSlide({ lastSlideChange: _state.lastSlideChange, slideDuration, slideDurationRatio: SLIDE_DURATION_RATIO })) return _state;
43-
44-
_state.forward = false;
45-
_state.index = _state.index <= 0 ? images.length - 1 : _state.index - 1;
46-
_state.lastSlideChange = new Date();
47-
48-
return _state;
57+
changeSlide(_state.index - 1);
4958
};
5059

5160
const loop = (node: HTMLElement) => {
@@ -79,7 +88,7 @@
7988

8089
const getPositionFromEvent = (evt: MouseEvent | TouchEvent) => {
8190
const mousePos = (evt as MouseEvent)?.clientX;
82-
if (mousePos) return mousePos;
91+
if (mousePos !== undefined) return mousePos;
8392

8493
let touchEvt = evt as TouchEvent;
8594
if (/^touch/.test(touchEvt?.type)) {
@@ -164,15 +173,14 @@
164173

165174
<!-- The move listeners go here, so things keep working if the touch strays out of the element. -->
166175
<svelte:document onmousemove={onDragMove} onmouseup={onDragStop} ontouchmove={onDragMove} ontouchend={onDragStop} />
167-
<div bind:this={carouselDiv} class={clsx("relative", divClass)} onmousedown={onDragStart} ontouchstart={onDragStart} onmousemove={onDragMove} onmouseup={onDragStop} ontouchmove={onDragMove} ontouchend={onDragStop} role="button" aria-label={ariaLabel} tabindex="0">
168-
<div {...restProps} class={carousel({ class: clsx(activeDragGesture === undefined ? "transition-transform" : "", theme, className) })} {@attach loop}>
169-
{#if slide}
170-
{@render slide({ index, Slide })}
171-
{:else}
172-
<Slide image={images[index]} class={clsx(imgClass)} {transition} />
173-
{/if}
174-
</div>
175-
{@render children?.(index)}
176+
<div bind:this={carouselDiv} onmousedown={onDragStart} ontouchstart={onDragStart} onmousemove={onDragMove} onmouseup={onDragStop} ontouchmove={onDragMove} ontouchend={onDragStop} role="button" aria-label={ariaLabel} tabindex="0" {...restProps} class={base({ class: clsx(activeDragGesture === undefined ? "transition-transform" : "", theme?.base, className) })} {@attach loop}>
177+
{#if slide}
178+
{@render slide({ index: _state.index, Slide })}
179+
{:else}
180+
<Slide image={images[_state.index]} fit={slideFit} class={slideCls({ class: clsx(theme?.slide, styling.slide) })} {transition} />
181+
{/if}
182+
183+
{@render children?.(_state.index)}
176184
</div>
177185

178186
<!--
@@ -190,7 +198,7 @@
190198
@prop duration = 0
191199
@prop "aria-label": ariaLabel = "Draggable Carousel"
192200
@prop disableSwipe = false
193-
@prop imgClass = ""
201+
@prop imgClass = "" // deprecated; use classes.slide instead
194202
@prop class: className
195203
@prop onchange
196204
@prop divClass

src/lib/carousel/CarouselIndicators.svelte

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,14 @@
1313
const { base, indicator } = $derived(carouselIndicators({ position }));
1414

1515
function goToIndex(newIndex: number) {
16-
const currentIndex = _state.index;
17-
_state.index = newIndex;
18-
_state.forward = newIndex >= currentIndex;
19-
_state.lastSlideChange = new Date();
16+
_state.changeSlide(newIndex);
2017
}
2118
</script>
2219

2320
<div class={base({ class: clsx(theme?.base, className) })} {...restProps}>
2421
{#each _state.images as _, idx}
2522
{@const selected = _state.index === idx}
26-
<button onclick={() => goToIndex(idx)}>
23+
<button type="button" onclick={() => goToIndex(idx)} aria-current={selected ? "true" : undefined} aria-label={`Go to slide ${idx + 1}`}>
2724
{#if children}
2825
{@render children({ selected, index: idx })}
2926
{:else}

src/lib/carousel/Controls.svelte

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import { getTheme } from "$lib/theme/themeUtils";
44
import clsx from "clsx";
55
import { getContext } from "svelte";
6-
import { canChangeSlide } from "./CarouselSlide";
76

87
let { children, class: className, ...restProps }: ControlsProps = $props();
98

@@ -12,20 +11,7 @@
1211
const _state = getContext<State>("state");
1312

1413
function changeSlide(forward: boolean) {
15-
const { lastSlideChange, slideDuration } = _state;
16-
if (!canChangeSlide({ lastSlideChange, slideDuration, slideDurationRatio: 0.75 })) {
17-
return;
18-
}
19-
20-
if (forward) {
21-
_state.forward = true;
22-
_state.index = _state.index >= _state.images.length - 1 ? 0 : _state.index + 1;
23-
} else {
24-
_state.forward = false;
25-
_state.index = _state.index <= 0 ? _state.images.length - 1 : _state.index - 1;
26-
}
27-
_state.lastSlideChange = new Date();
28-
return _state;
14+
_state.changeSlide(forward ? _state.index + 1 : _state.index - 1);
2915
}
3016
</script>
3117

src/lib/carousel/Slide.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
const _state = getContext<State>("state");
1010

11-
let { image, transition, class: className, ...restProps }: SlideProps = $props();
11+
let { image, transition, fit, class: className, ...restProps }: SlideProps = $props();
1212

1313
const theme = getTheme("slide");
1414

@@ -28,7 +28,7 @@
2828
duration: _state.slideDuration
2929
});
3030

31-
let imgClass = slide({ class: clsx(theme, className) });
31+
let imgClass = slide({ fit, class: clsx(theme, className) });
3232
</script>
3333

3434
{#if transition}

src/lib/carousel/Thumbnails.svelte

Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,38 @@
11
<script lang="ts">
2-
import { type State, Thumbnail, type ThumbnailsProps } from "$lib";
2+
import { Thumbnail, type ThumbnailsProps } from "$lib";
33
import { getTheme } from "$lib/theme/themeUtils";
44
import clsx from "clsx";
5-
import { getContext } from "svelte";
65
import { thumbnails } from "./theme";
76

87
let { children, images = [], index = $bindable(), ariaLabel = "Click to view image", imgClass, throttleDelay = 650, class: className }: ThumbnailsProps = $props();
98

109
const theme = getTheme("thumbnails");
1110

12-
const _state = getContext<State>("state");
13-
if (!_state) {
14-
console.error("State is undefined. Make sure to provide state context or pass it as a prop.");
15-
}
16-
17-
let lastClickedAt = new Date();
11+
// Initialize so the first click is never throttled
12+
let lastClickedAt = -Infinity;
1813

1914
const btnClick = (newIndex: number) => {
20-
if (new Date().getTime() - lastClickedAt.getTime() < throttleDelay) {
15+
const now = Date.now();
16+
if (now - lastClickedAt < throttleDelay) {
2117
console.warn("Thumbnail action throttled");
2218
return;
2319
}
24-
if (_state) {
25-
const currentIndex = _state.index;
26-
27-
_state.index = newIndex;
28-
_state.forward = newIndex >= currentIndex;
29-
_state.lastSlideChange = new Date();
3020

31-
// Update the bound index
32-
index = newIndex;
33-
} else {
34-
// Fallback behavior if state is not available
35-
index = newIndex;
36-
lastClickedAt = new Date();
37-
console.warn("State update skipped - no valid state available");
38-
}
21+
lastClickedAt = now;
22+
index = newIndex;
3923
};
4024

4125
$effect(() => {
42-
index = (index + images.length) % images.length;
26+
if (images.length > 0) {
27+
index = (index + images.length) % images.length;
28+
}
4329
});
4430
</script>
4531

4632
<div class={thumbnails({ class: clsx(theme, className) })}>
4733
{#each images as image, idx}
4834
{@const selected = index === idx}
49-
<button onclick={() => btnClick(idx)} aria-label={ariaLabel}>
35+
<button onclick={() => btnClick(idx)} aria-label={ariaLabel} aria-current={selected ? "true" : undefined}>
5036
{#if children}
5137
{@render children({ image, selected, imgClass: clsx(imgClass), Thumbnail })}
5238
{:else}

src/lib/carousel/theme.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1+
import type { Classes } from "$lib/theme/themeUtils";
12
import { tv, type VariantProps } from "tailwind-variants";
23

3-
export type CarouselVariants = VariantProps<typeof carousel>;
4+
export type CarouselVariants = VariantProps<typeof carousel> & Classes<typeof carousel>;
45

56
export const carousel = tv({
6-
base: "grid overflow-hidden relative rounded-lg h-56 sm:h-64 xl:h-80 2xl:h-96",
7+
slots: {
8+
base: "grid overflow-hidden relative rounded-lg h-56 sm:h-64 xl:h-80 2xl:h-96",
9+
slide: ""
10+
},
711
variants: {},
812
compoundVariants: [],
913
defaultVariants: {}
@@ -22,7 +26,6 @@ export const carouselIndicators = tv({
2226
position: {
2327
top: { base: "top-5" },
2428
bottom: { base: "bottom-5" },
25-
withThumbnails: { base: "bottom-24" }
2629
}
2730
}
2831
});
@@ -57,6 +60,20 @@ export const thumbnail = tv({
5760
}
5861
});
5962

63+
export type SlideVariants = VariantProps<typeof slide>;
64+
6065
export const slide = tv({
61-
base: "absolute block w-full! h-full object-cover"
66+
base: "absolute block w-full h-full",
67+
variants: {
68+
fit: {
69+
contain: "object-contain",
70+
cover: "object-cover",
71+
fill: "object-fill",
72+
none: "object-none",
73+
"scale-down": "object-scale-down"
74+
}
75+
},
76+
defaultVariants: {
77+
fit: "cover"
78+
}
6279
});

0 commit comments

Comments
 (0)