Skip to content

Commit

Permalink
feat: enhance Carousel component with cloned slide offset adjustments…
Browse files Browse the repository at this point in the history
… and refactor index calculations
  • Loading branch information
ismail9k committed Dec 26, 2024
1 parent 7321b40 commit 168558a
Show file tree
Hide file tree
Showing 5 changed files with 172 additions and 148 deletions.
12 changes: 12 additions & 0 deletions src/components/Carousel/Carousel.css
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,15 @@
flex-direction: column-reverse;
}
}

.carousel.is-vertical {
.carousel__slide--clone:first-child {
margin-block-start: var(--vc-trk-cloned-offset);
}
}

.carousel:not(.is-vertical) {
.carousel__slide--clone:first-child {
margin-inline-start: var(--vc-trk-cloned-offset);
}
}
65 changes: 36 additions & 29 deletions src/components/Carousel/Carousel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,13 @@ export const Carousel = defineComponent({
effectiveSlideSize: effectiveSlideSize.value,
})

activeSlideIndex.value = currentSlideIndex.value + draggedSlides
activeSlideIndex.value = config.wrapAround
? currentSlideIndex.value + draggedSlides
: getNumberInRange({
val: currentSlideIndex.value + draggedSlides,
max: maxSlideIndex.value,
min: minSlideIndex.value,
})

Check warning on line 411 in src/components/Carousel/Carousel.ts

View workflow job for this annotation

GitHub Actions / coverage-report

These lines are not covered by a test

// Emit a drag event for further customization if needed
emit('drag', { deltaX: dragged.x, deltaY: dragged.y })
Expand Down Expand Up @@ -473,14 +479,22 @@ export const Carousel = defineComponent({
}

let targetIndex = slideIndex
let mappedIndex = slideIndex

prevSlideIndex.value = currentSlideIndex.value

// Calculate shortest path in wrap-around mode
if (!config.wrapAround) {
targetIndex = getNumberInRange({
val: targetIndex,
max: maxSlideIndex.value,
min: minSlideIndex.value,
})
} else {
mappedIndex = mapNumberToRange({
val: targetIndex,
max: maxSlideIndex.value,
min: 0,
})

Check warning on line 497 in src/components/Carousel/Carousel.ts

View workflow job for this annotation

GitHub Actions / coverage-report

These lines are not covered by a test
}

emit('slide-start', {
Expand All @@ -491,29 +505,20 @@ export const Carousel = defineComponent({
})

stopAutoplay()
prevSlideIndex.value = currentSlideIndex.value

const mappedNumber = config.wrapAround
? mapNumberToRange({
val: targetIndex,
max: maxSlideIndex.value,
min: 0,
})
: targetIndex

isSliding.value = true
currentSlideIndex.value = targetIndex

if (mappedNumber !== targetIndex) {
currentSlideIndex.value = targetIndex
if (mappedIndex !== targetIndex) {
modelWatcher.pause()
}
emit('update:modelValue', mappedNumber)
emit('update:modelValue', mappedIndex)

transitionTimer = setTimeout((): void => {
const transitionCallback = (): void => {
if (config.wrapAround) {
if (mappedNumber !== targetIndex) {
if (mappedIndex !== targetIndex) {

Check warning on line 518 in src/components/Carousel/Carousel.ts

View workflow job for this annotation

GitHub Actions / coverage-report

This line is not covered by a test
modelWatcher.resume()
currentSlideIndex.value = mappedNumber

currentSlideIndex.value = mappedIndex

Check warning on line 521 in src/components/Carousel/Carousel.ts

View workflow job for this annotation

GitHub Actions / coverage-report

This line is not covered by a test
emit('loop', {
currentSlideIndex: currentSlideIndex.value,
slidingToIndex: slideIndex,
Expand All @@ -529,7 +534,9 @@ export const Carousel = defineComponent({

isSliding.value = false
resetAutoplay()
}, config.transition)
}

transitionTimer = setTimeout(transitionCallback, config.transition)
}

function next(skipTransition = false): void {
Expand Down Expand Up @@ -660,17 +667,19 @@ export const Carousel = defineComponent({
return { before: 0, after: 0 }
}

const slidesToClone = Math.ceil(config.itemsToShow)
const slidesToClone = Math.ceil(config.itemsToShow + (config.itemsToScroll - 1))
const before = slidesToClone - activeSlideIndex.value
const after = slidesToClone - (slidesCount.value - (activeSlideIndex.value + 1))

return {
before: Math.max(slidesToClone, slidesToClone - activeSlideIndex.value),
after: Math.max(
slidesToClone,
slidesToClone - (slidesCount.value - (activeSlideIndex.value + 1))
),
before: Math.max(0, before),
after: Math.max(0, after),
}
})

const clonedSlidesOffset = computed(
() => clonedSlidesCount.value.before * effectiveSlideSize.value * -1
)
const trackTransform: ComputedRef<string> = computed(() => {
const directionMultiplier = isReversed.value ? 1 : -1
const translateAxis = isVertical.value ? 'Y' : 'X'
Expand All @@ -679,13 +688,10 @@ export const Carousel = defineComponent({
const scrolledOffset =
scrolledIndex.value * effectiveSlideSize.value * directionMultiplier

const clonedSlidesOffset =
clonedSlidesCount.value.before * effectiveSlideSize.value * -1

// Include user drag interaction offset
const dragOffset = isVertical.value ? dragged.y : dragged.x

const totalOffset = scrolledOffset + clonedSlidesOffset + dragOffset
const totalOffset = scrolledOffset + dragOffset

return `translate${translateAxis}(${totalOffset}px)`
})
Expand All @@ -695,6 +701,7 @@ export const Carousel = defineComponent({
'transition-duration': isSliding.value ? `${config.transition}ms` : undefined,
gap: config.gap > 0 ? `${config.gap}px` : undefined,
'--vc-trk-height': trackHeight.value,
'--vc-trk-cloned-offset': `${clonedSlidesOffset.value}px`,
}))

return () => {
Expand Down
35 changes: 17 additions & 18 deletions src/utils/getScrolledIndex.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { CarouselConfig } from '@/shared'

import { getNumberInRange } from './getNumberInRange'
import { mapNumberToRange } from './mapNumberToRange'

type GetScrolledIndexArgs = {
config: Pick<CarouselConfig, 'itemsToShow' | 'wrapAround' | 'snapAlign'>
Expand All @@ -12,11 +11,16 @@ type GetScrolledIndexArgs = {
export const calculateOffset = (snapAlign: string, itemsToShow: number): number => {
switch (snapAlign) {
default:
case 'start': return 0
case 'center': return (itemsToShow - 1) / 2
case 'center-odd': return (itemsToShow - 1) / 2
case 'center-even': return (itemsToShow - 2) / 2
case 'end': return itemsToShow - 1
case 'start':
return 0
case 'center':
return (itemsToShow - 1) / 2
case 'center-odd':
return (itemsToShow - 1) / 2
case 'center-even':
return (itemsToShow - 2) / 2
case 'end':
return itemsToShow - 1
}
}

Expand All @@ -31,17 +35,12 @@ export function getScrolledIndex({
const offset = calculateOffset(snapAlign, itemsToShow)

// Compute the index with or without wrapAround
if (!wrapAround) {
return getNumberInRange({
val: currentSlide - offset,
max: slidesCount - itemsToShow,
min: 0,
})
} else {
return mapNumberToRange({
val: currentSlide - offset,
max: slidesCount + itemsToShow,
min: 0 - itemsToShow,
})
if (wrapAround) {
return currentSlide - offset
}
return getNumberInRange({
val: currentSlide - offset,
max: slidesCount - itemsToShow,
min: 0,
})
}
17 changes: 5 additions & 12 deletions tests/integration/__snapshots__/carousel.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,13 @@ exports[`SSR Carousel > renders server side properly 1`] = `
"<div id="app">
<section class="carousel is-ltr" dir="ltr" aria-label="Gallery" tabindex="0">
<div class="carousel__viewport">
<ol class="carousel__track" style="transform: translateX(0px);">
<li style="width: 50%;" class="carousel__slide carousel__slide--clone" aria-hidden="true">4 <input type="text" tabindex="-1"></li>
<ol class="carousel__track" style="transform: translateX(0px); --vc-trk-cloned-offset: 0px;">
<li style="width: 50%;" class="carousel__slide carousel__slide--clone" aria-hidden="true">5 <input type="text" tabindex="-1"></li>
<li style="width: 50%;" class="carousel__slide carousel__slide--visible carousel__slide--prev" id="v-0">1 <input type="text"></li>
<li style="width: 50%;" class="carousel__slide carousel__slide--visible carousel__slide--active" id="v-1">2 <input type="text"></li>
<li style="width: 50%;" class="carousel__slide carousel__slide--visible carousel__slide--next" id="v-2">3 <input type="text"></li>
<li style="width: 50%;" class="carousel__slide" id="v-3">4 <input type="text"></li>
<li style="width: 50%;" class="carousel__slide" id="v-4">5 <input type="text"></li>
<li style="width: 50%;" class="carousel__slide carousel__slide--clone" aria-hidden="true">1 <input type="text" tabindex="-1"></li>
<li style="width: 50%;" class="carousel__slide carousel__slide--clone" aria-hidden="true">2 <input type="text" tabindex="-1"></li>
</ol>
</div><button type="button" aria-label="Navigate to previous slide" title="Navigate to previous slide" class="carousel__prev"><svg class="carousel__icon" viewBox="0 0 24 24" role="img" aria-label="Arrow pointing to the left">
<title>Arrow pointing to the left</title>
Expand All @@ -34,36 +31,32 @@ exports[`SSR Carousel > renders server side properly 1`] = `
</div>"
`;
exports[`SSR Carousel > renders server side properly 2`] = `"<div id="app"><section class="carousel is-ltr" dir="ltr" aria-label="Gallery" tabindex="0"><div class="carousel__viewport"><ol class="carousel__track" style="transform:translateX(0px);"><!--[--><li style="width:50%;" class="carousel__slide carousel__slide--visible carousel__slide--prev" id="v-0">1 <input type="text"></li><li style="width:50%;" class="carousel__slide carousel__slide--visible carousel__slide--active" id="v-1">2 <input type="text"></li><li style="width:50%;" class="carousel__slide carousel__slide--visible carousel__slide--next" id="v-2">3 <input type="text"></li><li style="width:50%;" class="carousel__slide" id="v-3">4 <input type="text"></li><li style="width:50%;" class="carousel__slide" id="v-4">5 <input type="text"></li><!--]--></ol></div><!--[--><!--[--><button type="button" aria-label="Navigate to previous slide" title="Navigate to previous slide" class="carousel__prev"><svg class="carousel__icon" viewBox="0 0 24 24" role="img" aria-label="Arrow pointing to the left"><title>Arrow pointing to the left</title><path d="M15.41 16.59L10.83 12l4.58-4.59L14 6l-6 6 6 6 1.41-1.41z"></path></svg></button><button type="button" aria-label="Navigate to next slide" title="Navigate to next slide" class="carousel__next"><svg class="carousel__icon" viewBox="0 0 24 24" role="img" aria-label="Arrow pointing to the right"><title>Arrow pointing to the right</title><path d="M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41z"></path></svg></button><!--]--><ol class="carousel__pagination"><li class="carousel__pagination-item"><button type="button" class="carousel__pagination-button" aria-label="Navigate to slide 1" aria-pressed="false" aria-controls="v-0" title="Navigate to slide 1"></button></li><li class="carousel__pagination-item"><button type="button" class="carousel__pagination-button carousel__pagination-button--active" aria-label="Navigate to slide 2" aria-pressed="true" aria-controls="v-1" title="Navigate to slide 2"></button></li><li class="carousel__pagination-item"><button type="button" class="carousel__pagination-button" aria-label="Navigate to slide 3" aria-pressed="false" aria-controls="v-2" title="Navigate to slide 3"></button></li><li class="carousel__pagination-item"><button type="button" class="carousel__pagination-button" aria-label="Navigate to slide 4" aria-pressed="false" aria-controls="v-3" title="Navigate to slide 4"></button></li><li class="carousel__pagination-item"><button type="button" class="carousel__pagination-button" aria-label="Navigate to slide 5" aria-pressed="false" aria-controls="v-4" title="Navigate to slide 5"></button></li></ol><!--]--><div class="carousel__liveregion carousel__sr-only" aria-live="polite" aria-atomic="true">Item 2 of 5</div></section></div>"`;
exports[`SSR Carousel > renders server side properly 2`] = `"<div id="app"><section class="carousel is-ltr" dir="ltr" aria-label="Gallery" tabindex="0"><div class="carousel__viewport"><ol class="carousel__track" style="transform:translateX(0px);--vc-trk-cloned-offset:0px;"><!--[--><li style="width:50%;" class="carousel__slide carousel__slide--visible carousel__slide--prev" id="v-0">1 <input type="text"></li><li style="width:50%;" class="carousel__slide carousel__slide--visible carousel__slide--active" id="v-1">2 <input type="text"></li><li style="width:50%;" class="carousel__slide carousel__slide--visible carousel__slide--next" id="v-2">3 <input type="text"></li><li style="width:50%;" class="carousel__slide" id="v-3">4 <input type="text"></li><li style="width:50%;" class="carousel__slide" id="v-4">5 <input type="text"></li><!--]--></ol></div><!--[--><!--[--><button type="button" aria-label="Navigate to previous slide" title="Navigate to previous slide" class="carousel__prev"><svg class="carousel__icon" viewBox="0 0 24 24" role="img" aria-label="Arrow pointing to the left"><title>Arrow pointing to the left</title><path d="M15.41 16.59L10.83 12l4.58-4.59L14 6l-6 6 6 6 1.41-1.41z"></path></svg></button><button type="button" aria-label="Navigate to next slide" title="Navigate to next slide" class="carousel__next"><svg class="carousel__icon" viewBox="0 0 24 24" role="img" aria-label="Arrow pointing to the right"><title>Arrow pointing to the right</title><path d="M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41z"></path></svg></button><!--]--><ol class="carousel__pagination"><li class="carousel__pagination-item"><button type="button" class="carousel__pagination-button" aria-label="Navigate to slide 1" aria-pressed="false" aria-controls="v-0" title="Navigate to slide 1"></button></li><li class="carousel__pagination-item"><button type="button" class="carousel__pagination-button carousel__pagination-button--active" aria-label="Navigate to slide 2" aria-pressed="true" aria-controls="v-1" title="Navigate to slide 2"></button></li><li class="carousel__pagination-item"><button type="button" class="carousel__pagination-button" aria-label="Navigate to slide 3" aria-pressed="false" aria-controls="v-2" title="Navigate to slide 3"></button></li><li class="carousel__pagination-item"><button type="button" class="carousel__pagination-button" aria-label="Navigate to slide 4" aria-pressed="false" aria-controls="v-3" title="Navigate to slide 4"></button></li><li class="carousel__pagination-item"><button type="button" class="carousel__pagination-button" aria-label="Navigate to slide 5" aria-pressed="false" aria-controls="v-4" title="Navigate to slide 5"></button></li></ol><!--]--><div class="carousel__liveregion carousel__sr-only" aria-live="polite" aria-atomic="true">Item 2 of 5</div></section></div>"`;
exports[`SSR Carousel > renders slotted server side properly 1`] = `
"<div id="app">
<section class="carousel is-ltr" dir="ltr" aria-label="Gallery" tabindex="0">
<div class="carousel__viewport">
<ol class="carousel__track" style="transform: translateX(0px);">
<ol class="carousel__track" style="transform: translateX(0px); --vc-trk-cloned-offset: 0px;">
<li style="width: 100%;" class="carousel__slide carousel__slide--clone carousel__slide--prev" aria-hidden="true">5</li>
<li style="width: 100%;" class="carousel__slide carousel__slide--visible carousel__slide--active" id="v-0">1</li>
<li style="width: 100%;" class="carousel__slide carousel__slide--next" id="v-1">2</li>
<li style="width: 100%;" class="carousel__slide" id="v-2">3</li>
<li style="width: 100%;" class="carousel__slide" id="v-3">4</li>
<li style="width: 100%;" class="carousel__slide" id="v-4">5</li>
<li style="width: 100%;" class="carousel__slide carousel__slide--clone" aria-hidden="true">1</li>
</ol>
</div>
<div class="carousel__liveregion carousel__sr-only" aria-live="polite" aria-atomic="true">Item 1 of 5</div>
</section>
</div>"
`;
exports[`SSR Carousel > renders slotted server side properly 2`] = `"<div id="app"><section class="carousel is-ltr" dir="ltr" aria-label="Gallery" tabindex="0"><div class="carousel__viewport"><ol class="carousel__track" style="transform:translateX(0px);"><!--[--><!--[--><li style="width:100%;" class="carousel__slide carousel__slide--visible carousel__slide--active" id="v-0">1</li><li style="width:100%;" class="carousel__slide carousel__slide--next" id="v-1">2</li><li style="width:100%;" class="carousel__slide" id="v-2">3</li><li style="width:100%;" class="carousel__slide" id="v-3">4</li><li style="width:100%;" class="carousel__slide" id="v-4">5</li><!--]--><!--]--></ol></div><!--[--><!--]--><div class="carousel__liveregion carousel__sr-only" aria-live="polite" aria-atomic="true">Item 1 of 5</div></section></div>"`;
exports[`SSR Carousel > renders slotted server side properly 2`] = `"<div id="app"><section class="carousel is-ltr" dir="ltr" aria-label="Gallery" tabindex="0"><div class="carousel__viewport"><ol class="carousel__track" style="transform:translateX(0px);--vc-trk-cloned-offset:0px;"><!--[--><!--[--><li style="width:100%;" class="carousel__slide carousel__slide--visible carousel__slide--active" id="v-0">1</li><li style="width:100%;" class="carousel__slide carousel__slide--next" id="v-1">2</li><li style="width:100%;" class="carousel__slide" id="v-2">3</li><li style="width:100%;" class="carousel__slide" id="v-3">4</li><li style="width:100%;" class="carousel__slide" id="v-4">5</li><!--]--><!--]--></ol></div><!--[--><!--]--><div class="carousel__liveregion carousel__sr-only" aria-live="polite" aria-atomic="true">Item 1 of 5</div></section></div>"`;
exports[`Wrap around Carousel.ts > renders wrapAround correctly 1`] = `
"<section class="carousel is-ltr" dir="ltr" aria-label="Gallery" tabindex="0">
<div class="carousel__viewport">
<ol class="carousel__track" style="transform: translateX(0px);">
<li style="width: 33.333333333333336%;" class="carousel__slide carousel__slide--clone" aria-hidden="true">7 <input type="text" tabindex="-1"></li>
<li style="width: 33.333333333333336%;" class="carousel__slide carousel__slide--clone" aria-hidden="true">8 <input type="text" tabindex="-1"></li>
<li style="width: 33.333333333333336%;" class="carousel__slide carousel__slide--clone" aria-hidden="true">9 <input type="text" tabindex="-1"></li>
<ol class="carousel__track" style="transform: translateX(0px); --vc-trk-cloned-offset: 0px;">
<li style="width: 33.333333333333336%;" class="carousel__slide" id="v-0">1 <input type="text"></li>
<li style="width: 33.333333333333336%;" class="carousel__slide" id="v-1">2 <input type="text"></li>
<li style="width: 33.333333333333336%;" class="carousel__slide" id="v-2">3 <input type="text"></li>
Expand Down
Loading

0 comments on commit 168558a

Please sign in to comment.