Carousel With Scroll Snap

Coding | January 2024Read in Outline

<template>
    <Stack position="relative" py="1">
        <HStack
            ref="scroller"
            py="4"
            px="5"
            overflow="scroll"
            gap="3"
            scrollSnapType="x mandatory"
            alignItems="flex-start"
            @scrollend="handleScroll"
        >
            <TicketBookInfo
                v-for="(book, i) in data.flatMap(({ books, ...rest }) =>
                    books.map((b) => ({ ...b, bookGroup: rest }))
                )"
                :key="book.id"
                :book="book"
                :divisionLabel="book.bookGroup.divisions_label"
                :bookGroupId="book.bookGroup.id"
                :data-index="i"
            />
        </HStack>
        <HStack gap="1" justifyContent="center">
            <Box
                v-for="i in data.flatMap((b) => b.books).length"
                :key="i"
                rounded="full"
                :bgColor="{
                    base: 'fg.subtle',
                    _selected: 'accent.default'
                }"
                :data-selected="i - 1 === index ? true : undefined"
                w="3"
                h="3"
                @click="handleSelect(i - 1)"
            />
        </HStack>
    </Stack>
</template>

<script lang="ts" setup>
    import type Stack from '../core/layout/Stack'
    import type { BookGroup } from '~/types/maas/book-groups'

    const scroller = ref<ComponentPublicInstance<typeof Stack> | null>(null)
    const index = ref(0)
    defineProps<{ data: BookGroup[] }>()

    let lastScrollPos = -1

    const handleSelect = (index: number) => {
        const container = scroller.value?.$el as HTMLDivElement

        if (!container) {
            return
        }

        const element = Array.from(container.children).find(
            (child) => child.getAttribute('data-index') === index.toString()
        )
        element?.scrollIntoView({
            block: 'center',
            behavior: 'smooth'
        })
    }
    const handleScroll = (e: UIEvent) => {
        const target = e?.target as HTMLDivElement
        const container = scroller.value?.$el as HTMLDivElement

        if (
            !e ||
            !target ||
            !container ||
            target.scrollLeft === lastScrollPos
        ) {
            return undefined
        }
        lastScrollPos = target.scrollLeft

        const viewportWidth = window.innerWidth
        container.style.gap = '100vw'
        container.getBoundingClientRect()
        const value = Array.from(container.children)
            .find((child) => {
                const { left } = child.getBoundingClientRect()
                return left > 0 && left < viewportWidth
            })
            ?.attributes.getNamedItem('data-index')?.value
        container.style.gap = ''
        if (!value === undefined) {
            return
        }

        index.value = Number(value)
    }
</script>

© 2023-2024 HamP, Assets used in the site belongs to respective owner | View Source