Skip to content

Commit

Permalink
Merge pull request #444 from Tofandel/fix/ssr-clones
Browse files Browse the repository at this point in the history
Fix SSR of cloned slides
  • Loading branch information
ismail9k authored Dec 9, 2024
2 parents ff404e6 + 700b2dd commit ce4f211
Show file tree
Hide file tree
Showing 10 changed files with 243 additions and 43 deletions.
1 change: 1 addition & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pnpm lint
pnpm test
4 changes: 2 additions & 2 deletions docs/examples/ExampleActiveClasses.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup>
import { Carousel, Pagination, Navigation, Slide } from '../../dist/carousel.es'
import { Carousel, Navigation, Slide } from '../../dist/carousel.es'
import '../../dist/carousel.css'
const config = {
Expand All @@ -24,7 +24,7 @@ const config = {

<style scoped>
.carousel__slide {
padding: 5;
padding: 5px;
}
.carousel__viewport {
Expand Down
61 changes: 33 additions & 28 deletions src/components/Carousel/Carousel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ import {
computed,
h,
watch,
cloneVNode,
VNode,
SetupContext,
Ref,
ComputedRef,
ComponentInternalInstance,
watchEffect,
shallowReactive,
pushScopeId,
popScopeId,
} from 'vue'

import { ARIA as ARIAComponent } from '@/components/ARIA'
Expand All @@ -35,6 +36,7 @@ import {
mapNumberToRange,
getScrolledIndex,
getTransformValues,
createCloneSlides,
} from '@/utils'

import {
Expand Down Expand Up @@ -74,8 +76,6 @@ export const Carousel = defineComponent({
// current active config
const config = reactive<CarouselConfig>({ ...fallbackConfig.value })

watch(fallbackConfig, () => Object.assign(config, fallbackConfig.value))

// slides
const currentSlideIndex = ref(props.modelValue ?? 0)
const prevSlideIndex = ref(0)
Expand Down Expand Up @@ -118,29 +118,38 @@ export const Carousel = defineComponent({
const isReversed = computed(() => ['rtl', 'btt'].includes(normalizedDir.value))
const isVertical = computed(() => ['ttb', 'btt'].includes(normalizedDir.value))

const clonedSlidesCount = computed(() => config.itemsToShow + 1)
const clonedSlidesCount = computed(() => Math.ceil(config.itemsToShow) + 1)

function updateBreakpointsConfig(): void {
// Determine the width source based on the 'breakpointMode' config
const widthSource =
(config.breakpointMode === 'carousel'
(fallbackConfig.value.breakpointMode === 'carousel'
? root.value?.getBoundingClientRect().width
: window.innerWidth) || 0
: typeof window !== 'undefined'
? window.innerWidth
: 0) || 0

const breakpointsArray = Object.keys(props.breakpoints || {})
.map((key) => Number(key))
.sort((a, b) => +b - +a)

let newConfig = { ...fallbackConfig.value } as CarouselConfig
const newConfig: Partial<CarouselConfig> = {}
breakpointsArray.some((breakpoint) => {
if (widthSource >= breakpoint) {
newConfig = { ...newConfig, ...props.breakpoints?.[breakpoint] }
Object.assign(newConfig, props.breakpoints![breakpoint])
if (newConfig.i18n) {
Object.assign(
newConfig.i18n,
fallbackConfig.value.i18n,
props.breakpoints![breakpoint].i18n
)
}
return true
}
return false
})

Object.assign(config, newConfig)
Object.assign(config, fallbackConfig.value, newConfig)
}

const handleResize = throttle(() => {
Expand Down Expand Up @@ -171,7 +180,7 @@ export const Carousel = defineComponent({
if (config.height !== 'auto') {
const height =
typeof config.height === 'string' && isNaN(parseInt(config.height))
? viewport.value.getBoundingClientRect().height
? viewport.value.getBoundingClientRect().height
: parseInt(config.height as string)

slideSize.value = (height - totalGap.value) / config.itemsToShow
Expand Down Expand Up @@ -227,8 +236,11 @@ export const Carousel = defineComponent({
}
}

updateBreakpointsConfig()
onMounted((): void => {
updateBreakpointsConfig()
if (fallbackConfig.value.breakpointMode === 'carousel') {
updateBreakpointsConfig()
}
initAutoplay()

if (document) {
Expand Down Expand Up @@ -563,7 +575,7 @@ export const Carousel = defineComponent({

// Update the carousel on props change
watch(
() => props.breakpoints,
() => [fallbackConfig.value, props.breakpoints],
() => updateBreakpointsConfig(),
{ deep: true }
)
Expand Down Expand Up @@ -650,6 +662,7 @@ export const Carousel = defineComponent({
const slotAddons = slots.addons

let output: VNode[] | Array<Array<VNode>> = slotSlides?.(data) || []

if (!config.enabled || !output.length) {
return h(
'section',
Expand All @@ -664,22 +677,14 @@ export const Carousel = defineComponent({
const addonsElements = slotAddons?.(data) || []

if (config.wrapAround) {
const toShow = Math.ceil(clonedSlidesCount.value)
const slidesBefore = slides.slice(-toShow).map(({ vnode }, index: number) =>
cloneVNode(vnode, {
index: -slides.length + toShow + index,
isClone: true,
key: `clone-before-${String(vnode.key)}`,
})
)
const slidesAfter = slides.slice(0, toShow).map(({ vnode }, index: number) =>
cloneVNode(vnode, {
index: slides.length + index,
isClone: true,
key: `clone-after-${String(vnode.key)}`,
})
)
output = [slidesBefore, output, slidesAfter]
// Ensure scoped CSS tracks properly
const scopeId = output.length > 0 ? output[0].scopeId : null
pushScopeId(scopeId)
const toShow = clonedSlidesCount.value
const slidesBefore = createCloneSlides({ slides, position: 'before', toShow })
const slidesAfter = createCloneSlides({ slides, position: 'after', toShow })
popScopeId()
output = [...slidesBefore, ...output, ...slidesAfter]
}

const trackEl = h(
Expand Down
6 changes: 6 additions & 0 deletions src/components/Slide/Slide.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
useId,
onMounted,
VNode,
onUpdated,
watch,
} from 'vue'

import { injectCarousel } from '@/injectSymbols'
Expand Down Expand Up @@ -43,6 +45,7 @@ export const Slide = defineComponent({
}

const index = ref(props.index)
watch(() => props.index, (i) => index.value = i)

const isActive: ComputedRef<boolean> = computed(
() => index.value === carousel.currentSlide
Expand Down Expand Up @@ -97,6 +100,9 @@ export const Slide = defineComponent({
onMounted(() => {
makeUnfocusable(instance.vnode)
})
onUpdated(() => {
makeUnfocusable(instance.vnode)
})
}

expose({
Expand Down
32 changes: 32 additions & 0 deletions src/utils/createCloneSlides.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { cloneVNode, ComponentInternalInstance, h } from 'vue'

import { Slide } from '@/components/Slide'

type CreateCloneSlidesArgs = {
slides: Array<ComponentInternalInstance>
position: 'before' | 'after'
toShow: number
}

export function createCloneSlides({ slides, position, toShow }: CreateCloneSlidesArgs) {
const clones = []
const isBefore = position === 'before'
const start = isBefore ? -toShow : 0
const end = isBefore ? 0 : toShow

for (let i = start; i < end; i++) {
const index = isBefore ? i : slides.length > 0 ? i + slides.length : i + 99999
const props = {
index,
isClone: true,
key: `clone-${position}-${i}`,
}
clones.push(
slides.length > 0
? cloneVNode(slides[(i + slides.length) % slides.length].vnode, props)
: h(Slide, props)
)
}

return clones
}
1 change: 1 addition & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export * from './mapNumberToRange'
export * from './i18nFormatter'
export * from './throttle'
export * from './getTransformValues'
export * from './createCloneSlides'
4 changes: 3 additions & 1 deletion tests/components/BasicApp.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,20 @@ import {
Slide,
Navigation as CarouselNavigation,
Pagination as CarouselPagination,
Breakpoints,
} from '@/index'
const { slideNum = 5, itemsToShow = 1 } = defineProps<{
slideNum?: number
itemsToShow?: number
wrapAround?: boolean
breakpoints?: Breakpoints
}>()
const vModel = defineModel<number>({ default: 0 })
</script>

<template>
<Carousel v-model="vModel" :items-to-show="itemsToShow" :wrap-around="wrapAround">
<Carousel v-model="vModel" :items-to-show="itemsToShow" :wrap-around="wrapAround" :breakpoints="breakpoints">
<Slide v-for="slide in slideNum" :key="slide">
{{ slide }}
<input type="text" />
Expand Down
4 changes: 2 additions & 2 deletions tests/components/SlottedApp.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
<script setup lang="ts">
import Carousel from './Carousel.vue'
import Slides from './Slides.vue'
const { slideNum = 5 } = defineProps<{ slideNum: number }>()
const { slideNum = 5 } = defineProps<{ slideNum: number, wrapAround?: boolean }>()
</script>

<template>
<Carousel>
<Carousel :wrap-around="wrapAround">
<Slides :slide-num="slideNum"></Slides>
</Carousel>
</template>
Expand Down
63 changes: 62 additions & 1 deletion tests/integration/__snapshots__/carousel.spec.ts.snap
Original file line number Diff line number Diff line change
@@ -1,6 +1,67 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`Wrap around Carousel.ts > It renders wrapAround correctly 1`] = `
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">3 <input type="text" tabindex="-1"></li>
<li style="width: 50%;" class="carousel__slide carousel__slide--clone" aria-hidden="true">4 <input type="text" tabindex="-1"></li>
<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>
<li style="width: 50%;" class="carousel__slide carousel__slide--clone" aria-hidden="true">3 <input type="text" tabindex="-1"></li>
</ol>
</div><button type="button" class="carousel__prev" aria-label="Navigate to previous slide" title="Navigate to previous slide"><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" class="carousel__next" aria-label="Navigate to next slide" title="Navigate to next slide"><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" style="" 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"></li><li style="width:50%;" class="carousel__slide carousel__slide--clone" aria-hidden="true"></li><li style="width:50%;" class="carousel__slide carousel__slide--clone" aria-hidden="true"></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"></li><li style="width:50%;" class="carousel__slide carousel__slide--clone" aria-hidden="true"></li><li style="width:50%;" class="carousel__slide carousel__slide--clone" aria-hidden="true"></li></ol></div><!--[--><!--[--><button type="button" class="carousel__prev" aria-label="Navigate to previous slide" title="Navigate to previous slide"><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" class="carousel__next" aria-label="Navigate to next slide" title="Navigate to next slide"><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);">
<li style="width: 100%;" class="carousel__slide carousel__slide--clone" aria-hidden="true">4</li>
<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>
<li style="width: 100%;" class="carousel__slide carousel__slide--clone" aria-hidden="true">2</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" style="" 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--clone" aria-hidden="true"></li><li style="width:100%;" class="carousel__slide carousel__slide--clone carousel__slide--prev" aria-hidden="true"></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"></li><li style="width:100%;" class="carousel__slide carousel__slide--clone" aria-hidden="true"></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);">
Expand Down
Loading

0 comments on commit ce4f211

Please sign in to comment.