I created my own carousel from scratch and it's working perfectly fine except for one issue - clicking on the navigation circles.
When using the interval/infinite loop prop, the circles update to the correct active slide as expected. The same goes for pressing the side left and right buttons. However, when trying to click directly on the circles, things go haywire and the carousel breaks.
It seems to be related to the switchSlide
function I wrote for that action, but I'm struggling to figure it out.
Any help or suggestions would be greatly appreciated!
Cheers!
You can find the Codesandbox here
NOTE: I only added the index inside each circle to ensure the proper amount were rendering.
Carousel.vue
<template>
<div class="audition-hero-carousel">
<div class="carousel-inner">
<div v-if="navigation" class="carousel-circles">
<button
v-for="(fileInfo, index) in modelValue"
class="carousel-circle-item"
:style="{ background: `${color}` }"
:class="{ active: currentSlide === index }"
:key="index"
:name="index"
@click="(index) => switchSlide(index)"
></button>
</div>
<TransitionGroup :name="transitionEffect">
<div
v-show="currentSlide === index"
v-for="(fileInfo, index) in modelValue"
:key="index"
:name="index"
class="carousel-item"
@mouseenter="() => stopSlideTimer()"
@mouseout="() => startSlideTimer()"
>
<img
v-if="fileInfo.type.startsWith('image')"
:src="fileInfo.url"
class="_img"
draggable="false"
/>
</div>
</TransitionGroup>
<div v-if="controls">
<button
:style="{ color: `${color}` }"
class="carousel-control left"
@click="() => prev()"
>
←
</button>
<button
:style="{ color: `${color}` }"
class="carousel-control right"
@click="() => next()"
>
→
</button>
</div>
</div>
</div>
<!-- {{modelValue}} -->
</template>
<script>
export default {
name: "CAuditionHeroCarousel2",
props: {
/**
* @type {{ type: 'image' | 'video' | 'youtube', url: string }[]}
*/
modelValue: { type: Array },
controls: {
type: Boolean,
default: false,
},
navigation: {
type: Boolean,
default: false,
},
interval: {
type: Number,
default: 5000,
},
infinite: {
type: Boolean,
default: true,
},
color: {
type: String,
default: "#9759ff",
},
},
data() {
return {
currentSlide: 0,
direction: "right",
};
},
methods: {
setCurrentSlide(index) {
this.currentSlide = index;
},
prev(step = -1) {
const index =
this.currentSlide > 0
? this.currentSlide + step
: this.modelValue.length - 1;
this.setCurrentSlide(index);
this.direction = "left";
this.startSlideTimer();
},
_next(step = 1) {
const index =
this.currentSlide < this.modelValue.length - 1
? this.currentSlide + step
: 0;
this.setCurrentSlide(index);
this.direction = "right";
},
next(step = 1) {
this._next(step);
this.startSlideTimer();
},
startSlideTimer() {
if (this.infinite) {
this.stopSlideTimer();
setInterval(() => {
this._next();
}, this.interval);
}
},
stopSlideTimer() {
clearInterval(this.interval);
},
switchSlide(index) {
const step = index - this.currentSlide;
if (step > 0) {
this.next(step);
} else {
this.prev(step);
}
},
},
computed: {
transitionEffect() {
return this.direction === "right" ? "slide-out" : "slide-in";
},
},
mounted() {
if (this.infinite) {
this.startSlideTimer();
}
},
beforeUnmount() {
this.stopSlideTimer();
},
};
</script>
<style lang="sass" scoped>
// Carousel Main
.audition-hero-carousel
display: flex
justify-content: center
min-width: 0px
width: 450px
height: 200px
.carousel-inner
position: relative
width: 100%
height: 100%
overflow: hidden
.carousel-circles
position: absolute
display: flex
justify-content: center
transform: translateX(-50%)
left: 50%
bottom: 2em
width: 100%
z-index: 2
.carousel-circle-item
width: 15px
height: 15px
border: none
opacity: 0.65
margin-right: 20px
border-radius: 50%
cursor: pointer
&:hover
cursor: pointer
&:last-child
margin-right: 0px
._img,
._video
width: 100%
height: 100%
._img
object-fit: cover
._video > *
height: 100%
width: 100%
.active
opacity: 1
/**
Carousel Item styles
*/
.carousel-item
position: absolute
top: 0
left: 0
right: 0
bottom: 0
.slide-in-enter-active,
.slide-in-leave-active,
.slide-out-enter-active,
.slide-out-leave-active
transition: all 300ms ease-in-out
.slide-in-enter-from
transform: translateX(-100%)
.slide-in-leave-to
transform: translateX(100%)
.slide-out-enter-from
transform: translateX(100%)
.slide-out-leave-to
transform: translateX(-100%)
/**
Controls
*/
.carousel-control
outline: none
border: none
background: transparent
display: inline-block
position: absolute
height: 50px
width: 70px
top: calc(50% - 20px)
cursor: pointer
.left
left: 0
.right
right: 0
</style>
App.vue
<template>
<Carousel
:modelValue="slides"
:interval="3000"
:navigation="true"
:controls="true"
:infinite="false"
:color="'#fff'"
/>
</template>
<script>
import Carousel from "./components/Carousel.vue";
export default {
name: "App",
components: {
Carousel,
},
data() {
return {
slides: [
{ type: "image", url: "https://picsum.photos/id/1032/900/400" },
{ type: "image", url: "https://picsum.photos/id/1033/900/400" },
{ type: "image", url: "https://picsum.photos/id/1037/900/400" },
{ type: "image", url: "https://picsum.photos/id/1035/900/400" },
{ type: "image", url: "https://picsum.photos/id/1036/900/400" },
],
};
},
};
</script>