Add interactive Carousel component
This commit is contained in:
@@ -18,6 +18,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dayjs": "^1.10.7",
|
"dayjs": "^1.10.7",
|
||||||
|
"embla-carousel": "^5.0.1",
|
||||||
"focus-visible": "^5.2.0",
|
"focus-visible": "^5.2.0",
|
||||||
"sanitize.css": "^13.0.0"
|
"sanitize.css": "^13.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
@@ -7,6 +7,7 @@ specifiers:
|
|||||||
'@typescript-eslint/eslint-plugin': ^5.2.0
|
'@typescript-eslint/eslint-plugin': ^5.2.0
|
||||||
'@typescript-eslint/parser': ^5.2.0
|
'@typescript-eslint/parser': ^5.2.0
|
||||||
dayjs: ^1.10.7
|
dayjs: ^1.10.7
|
||||||
|
embla-carousel: ^5.0.1
|
||||||
eslint: ^8.1.0
|
eslint: ^8.1.0
|
||||||
eslint-plugin-svelte3: ^3.2.1
|
eslint-plugin-svelte3: ^3.2.1
|
||||||
focus-visible: ^5.2.0
|
focus-visible: ^5.2.0
|
||||||
@@ -20,6 +21,7 @@ specifiers:
|
|||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
dayjs: 1.10.7
|
dayjs: 1.10.7
|
||||||
|
embla-carousel: 5.0.1
|
||||||
focus-visible: 5.2.0
|
focus-visible: 5.2.0
|
||||||
sanitize.css: 13.0.0
|
sanitize.css: 13.0.0
|
||||||
|
|
||||||
@@ -467,6 +469,10 @@ packages:
|
|||||||
esutils: 2.0.3
|
esutils: 2.0.3
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/embla-carousel/5.0.1:
|
||||||
|
resolution: {integrity: sha512-pFvUI9mI/pxU92+4VDkPx0yP4Bs3VqJuRX/aw6ESYJdRBtzLx+6X2kXMu9aXK+SwO2zYsD2WURb1SeBaAv6zQA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/enquirer/2.3.6:
|
/enquirer/2.3.6:
|
||||||
resolution: {integrity: sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==}
|
resolution: {integrity: sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==}
|
||||||
engines: {node: '>=8.6'}
|
engines: {node: '>=8.6'}
|
||||||
|
|||||||
116
src/components/organisms/Carousel.svelte
Normal file
116
src/components/organisms/Carousel.svelte
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte'
|
||||||
|
import { writable } from 'svelte/store'
|
||||||
|
import { throttle } from '$utils/functions'
|
||||||
|
import EmblaCarousel, { EmblaCarouselType } from 'embla-carousel'
|
||||||
|
// Components
|
||||||
|
import Image from '$components/atoms/Image.svelte'
|
||||||
|
|
||||||
|
export let slides: any
|
||||||
|
|
||||||
|
let arrowEl: HTMLElement
|
||||||
|
let carouselEl: HTMLElement
|
||||||
|
let carousel: EmblaCarouselType
|
||||||
|
let currentSlide = 0
|
||||||
|
let arrowDirection: string = null
|
||||||
|
|
||||||
|
|
||||||
|
/** Navigate to specific slide */
|
||||||
|
const goToSlide = (index: number = 0) => {
|
||||||
|
carousel.scrollTo(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Move and change arrow direction when moving */
|
||||||
|
const arrowPosition = writable({ x: 0, y: 0 })
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
/** Move arrow and define direction on mousemove */
|
||||||
|
const handleArrowMove = ({ offsetX, offsetY }: MouseEvent) => {
|
||||||
|
// Define direction
|
||||||
|
const { width } = carouselEl.getBoundingClientRect()
|
||||||
|
arrowDirection = offsetX < Math.round(width / 2) ? 'prev' : 'next'
|
||||||
|
|
||||||
|
// Move arrow
|
||||||
|
arrowPosition.set({
|
||||||
|
x: offsetX - 12,
|
||||||
|
y: offsetY - 56,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Go to prev or next slide depending on direction */
|
||||||
|
const handleArrowClick = () => {
|
||||||
|
if (!carousel.clickAllowed()) return
|
||||||
|
|
||||||
|
// Click only if carousel if being dragged
|
||||||
|
if (arrowDirection === 'prev') {
|
||||||
|
carousel.scrollPrev()
|
||||||
|
} else {
|
||||||
|
carousel.scrollNext()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
// Init carousel
|
||||||
|
carousel = EmblaCarousel(carouselEl, {
|
||||||
|
loop: false
|
||||||
|
})
|
||||||
|
carousel.on('select', () => {
|
||||||
|
currentSlide = carousel.selectedScrollSnap()
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// Destroy
|
||||||
|
return () => {
|
||||||
|
carousel.destroy()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="carousel {$$props.class ? $$props.class : ''}">
|
||||||
|
{#if slides.length}
|
||||||
|
<div class="carousel__viewport"
|
||||||
|
bind:this={carouselEl}
|
||||||
|
on:mousemove={throttle(handleArrowMove, 50)}
|
||||||
|
on:click={handleArrowClick}
|
||||||
|
>
|
||||||
|
<div class="carousel__slides"
|
||||||
|
>
|
||||||
|
{#each slides as { id, alt }}
|
||||||
|
<Image
|
||||||
|
class="carousel__slide"
|
||||||
|
id={id}
|
||||||
|
sizeKey="product"
|
||||||
|
sizes={{
|
||||||
|
small: { width: 300 },
|
||||||
|
medium: { width: 550 },
|
||||||
|
large: { width: 800 },
|
||||||
|
}}
|
||||||
|
ratio={1.5}
|
||||||
|
alt={alt}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="carousel__dots">
|
||||||
|
{#each slides as _, index}
|
||||||
|
<li class:is-active={index === currentSlide}>
|
||||||
|
<button on:click={() => goToSlide(index)} aria-label="Go to slide #{index + 1}" />
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<span class="carousel__arrow" bind:this={arrowEl}
|
||||||
|
style="--x: {$arrowPosition.x}px; --y: {$arrowPosition.y}px;"
|
||||||
|
class:is-flipped={arrowDirection === 'prev'}
|
||||||
|
>
|
||||||
|
<svg width="29" height="32" viewBox="0 0 29 32" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.82 28.275a2.182 2.182 0 0 0 3.086 3.086l13.818-13.818a2.182 2.182 0 0 0 0-3.086L13.906.64a2.182 2.182 0 1 0-3.085 3.086l10.093 10.093H2.182a2.182 2.182 0 1 0 0 4.364h18.732L10.821 28.275Z" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
112
src/style/organisms/_carousel.scss
Normal file
112
src/style/organisms/_carousel.scss
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
.carousel {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: $color-primary-tertiary20;
|
||||||
|
|
||||||
|
// Slides
|
||||||
|
&__slides {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
&__slide {
|
||||||
|
position: relative;
|
||||||
|
flex: 0 0 100%;
|
||||||
|
color: $color-text;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
img {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dots
|
||||||
|
&__dots {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 20px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 12px;
|
||||||
|
background: rgba($color-tertiary, 0.5);
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
border-radius: 100vh;
|
||||||
|
|
||||||
|
@include bp (sm) {
|
||||||
|
bottom: 28px;
|
||||||
|
height: 40px;
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: block;
|
||||||
|
margin: 0 4px;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
$color-shadow: rgba(#533331, 0.1);
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 100%;
|
||||||
|
background: #fff;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 1px 1px $color-shadow, 0 2px 2px $color-shadow;
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 125%;
|
||||||
|
height: 125%;
|
||||||
|
border-radius: 100%;
|
||||||
|
background: $color-secondary-light;
|
||||||
|
transform: translate3d(-50%, -50%, 0) scale3d(0,0,0);
|
||||||
|
transform-origin: 50% 50%;
|
||||||
|
transition: transform 1s var(--ease-quart);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Active
|
||||||
|
.is-active {
|
||||||
|
button:after {
|
||||||
|
transform: translate3d(-50%, -50%, 0) scale3d(1,1,1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Arrow
|
||||||
|
&__arrow {
|
||||||
|
$color-shadow: rgba(#000, 0.075);
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
display: block;
|
||||||
|
transform: translate3d(var(--x), var(--y), 0);
|
||||||
|
transition: transform 0.6s var(--ease-quart), opacity 0.6s var(--ease-quart);
|
||||||
|
transform-origin: 50% 50%;
|
||||||
|
filter: drop-shadow(0 2px 2px $color-shadow) drop-shadow(0 8px 8px $color-shadow) drop-shadow(0 16px 16px $color-shadow);
|
||||||
|
|
||||||
|
// Flipped for previous direction
|
||||||
|
&.is-flipped {
|
||||||
|
transform: translate3d(var(--x), var(--y), 0) rotate(-180deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show arrow on hover
|
||||||
|
&:hover {
|
||||||
|
.carousel__arrow {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -57,6 +57,7 @@
|
|||||||
@import "organisms/locations";
|
@import "organisms/locations";
|
||||||
@import "organisms/house";
|
@import "organisms/house";
|
||||||
@import "organisms/newsletter";
|
@import "organisms/newsletter";
|
||||||
|
@import "organisms/carousel";
|
||||||
@import "organisms/shop";
|
@import "organisms/shop";
|
||||||
@import "organisms/poster-product";
|
@import "organisms/poster-product";
|
||||||
@import "organisms/footer";
|
@import "organisms/footer";
|
||||||
|
|||||||
Reference in New Issue
Block a user