mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-19 06:21:46 +01:00
feat(Carousel): new component (#927)
Co-authored-by: Michał Hanusek <m.hanusek@myfreak.pl> Co-authored-by: Inesh Bose <dev@inesh.xyz> Co-authored-by: Benjamin Canac <canacb1@gmail.com>
This commit is contained in:
176
src/runtime/components/elements/Carousel.vue
Normal file
176
src/runtime/components/elements/Carousel.vue
Normal file
@@ -0,0 +1,176 @@
|
||||
<template>
|
||||
<div :class="ui.wrapper" v-bind="attrs">
|
||||
<div ref="carouselRef" :class="ui.container" class="no-scrollbar">
|
||||
<div
|
||||
v-for="(item, index) in items"
|
||||
:key="index"
|
||||
:class="ui.item"
|
||||
>
|
||||
<slot :item="item" :index="index" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="arrows" :class="ui.arrows.wrapper">
|
||||
<slot name="prev" :on-click="onClickPrev" :disabled="isFirst">
|
||||
<UButton
|
||||
v-if="prevButton"
|
||||
:disabled="isFirst"
|
||||
v-bind="{ ...ui.default.prevButton, ...prevButton }"
|
||||
:class="twMerge(ui.default.prevButton.class, prevButton?.class)"
|
||||
aria-label="Prev"
|
||||
@click="onClickPrev"
|
||||
/>
|
||||
</slot>
|
||||
|
||||
<slot name="next" :on-click="onClickNext" :disabled="isLast">
|
||||
<UButton
|
||||
v-if="nextButton"
|
||||
:disabled="isLast"
|
||||
v-bind="{ ...ui.default.nextButton, ...nextButton }"
|
||||
:class="twMerge(ui.default.nextButton.class, nextButton?.class)"
|
||||
aria-label="Next"
|
||||
@click="onClickNext"
|
||||
/>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<div v-if="indicators" :class="ui.indicators.wrapper">
|
||||
<template v-for="index in indicatorsCount" :key="index">
|
||||
<slot name="indicator" :on-click="onClick" :active="index === currentIndex" :index="index">
|
||||
<button
|
||||
type="button"
|
||||
:class="[
|
||||
ui.indicators.base,
|
||||
index === currentIndex ? ui.indicators.active : ui.indicators.inactive
|
||||
]"
|
||||
:aria-label="`set slide ${index}`"
|
||||
@click="onClick(index)"
|
||||
/>
|
||||
</slot>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ref, toRef, toRefs, computed, defineComponent } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { mergeConfig } from '../../utils'
|
||||
import UButton from '../elements/Button.vue'
|
||||
import type { Strategy, Button } from '../../types'
|
||||
import { useUI } from '../../composables/useUI'
|
||||
import { useCarouselScroll } from '../../composables/useCarouselScroll'
|
||||
import { useScroll, useResizeObserver, useElementSize } from '@vueuse/core'
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
import { carousel } from '#ui/ui.config'
|
||||
|
||||
const config = mergeConfig<typeof carousel>(appConfig.ui.strategy, appConfig.ui.carousel, carousel)
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
UButton
|
||||
},
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
items: {
|
||||
type: Array as PropType<any[]>,
|
||||
default: () => []
|
||||
},
|
||||
arrows: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
indicators: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
prevButton: {
|
||||
type: Object as PropType<Button & { class?: string }>,
|
||||
default: () => config.default.prevButton as Button & { class?: string }
|
||||
},
|
||||
nextButton: {
|
||||
type: Object as PropType<Button & { class?: string }>,
|
||||
default: () => config.default.nextButton as Button & { class?: string }
|
||||
},
|
||||
class: {
|
||||
type: [String, Object, Array] as PropType<any>,
|
||||
default: () => ''
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
|
||||
default: undefined
|
||||
}
|
||||
},
|
||||
setup (props) {
|
||||
const { ui, attrs } = useUI('carousel', toRef(props, 'ui'), config, toRef(props, 'class'))
|
||||
|
||||
const carouselRef = ref<HTMLElement>()
|
||||
const itemWidth = ref(0)
|
||||
|
||||
const { x, arrivedState } = useScroll(carouselRef, { behavior: 'smooth' })
|
||||
const { width: carouselWidth } = useElementSize(carouselRef)
|
||||
|
||||
const { left: isFirst, right: isLast } = toRefs(arrivedState)
|
||||
|
||||
useCarouselScroll(carouselRef)
|
||||
|
||||
useResizeObserver(carouselRef, (entries) => {
|
||||
const [entry] = entries
|
||||
|
||||
itemWidth.value = entry?.target?.firstElementChild?.clientWidth || 0
|
||||
})
|
||||
|
||||
const currentIndex = computed(() => Math.round(x.value / itemWidth.value) + 1)
|
||||
|
||||
const indicatorsCount = computed(() => {
|
||||
if (!itemWidth.value) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return props.items.length - Math.round(carouselWidth.value / itemWidth.value) + 1
|
||||
})
|
||||
|
||||
function onClickNext () {
|
||||
x.value += itemWidth.value
|
||||
}
|
||||
|
||||
function onClickPrev () {
|
||||
x.value -= itemWidth.value
|
||||
}
|
||||
|
||||
function onClick (index: number) {
|
||||
x.value = (index - 1) * itemWidth.value
|
||||
}
|
||||
|
||||
return {
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
ui,
|
||||
attrs,
|
||||
isFirst,
|
||||
isLast,
|
||||
carouselRef,
|
||||
indicatorsCount,
|
||||
currentIndex,
|
||||
onClickNext,
|
||||
onClickPrev,
|
||||
onClick,
|
||||
twMerge
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||
.no-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Hide scrollbar for IE, Edge and Firefox */
|
||||
.no-scrollbar {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user