replace el-carousel with Carousel component

This commit is contained in:
pa
2026-01-11 21:55:19 +09:00
committed by Natsumi
parent 5fa2d4d465
commit 6222becd3d
11 changed files with 1184 additions and 104 deletions

View File

@@ -0,0 +1,56 @@
<script setup>
import { cn } from '@/lib/utils';
import { useProvideCarousel } from './useCarousel';
const props = defineProps({
opts: { type: Object, required: false },
plugins: { type: null, required: false },
orientation: { type: String, required: false, default: 'horizontal' },
class: { type: null, required: false }
});
const emits = defineEmits(['init-api']);
const { canScrollNext, canScrollPrev, carouselApi, carouselRef, orientation, scrollNext, scrollPrev } =
useProvideCarousel(props, emits);
defineExpose({
canScrollNext,
canScrollPrev,
carouselApi,
carouselRef,
orientation,
scrollNext,
scrollPrev
});
function onKeyDown(event) {
const prevKey = props.orientation === 'vertical' ? 'ArrowUp' : 'ArrowLeft';
const nextKey = props.orientation === 'vertical' ? 'ArrowDown' : 'ArrowRight';
if (event.key === prevKey) {
event.preventDefault();
scrollPrev();
return;
}
if (event.key === nextKey) {
event.preventDefault();
scrollNext();
}
}
</script>
<template>
<div
data-slot="carousel"
:class="cn('relative', props.class)"
role="region"
aria-roledescription="carousel"
tabindex="0"
@keydown="onKeyDown">
<slot :can-scroll-next :can-scroll-prev :carousel-api :carousel-ref :orientation :scroll-next :scroll-prev />
</div>
</template>

View File

@@ -0,0 +1,25 @@
<script setup>
import { cn } from '@/lib/utils';
import { useCarousel } from './useCarousel';
defineOptions({
inheritAttrs: false
});
const props = defineProps({
class: { type: null, required: false }
});
const { carouselRef, orientation } = useCarousel();
</script>
<template>
<div ref="carouselRef" data-slot="carousel-content" class="overflow-hidden">
<div
:class="cn('flex', orientation === 'horizontal' ? '-ml-4' : '-mt-4 flex-col', props.class)"
v-bind="$attrs">
<slot />
</div>
</div>
</template>

View File

@@ -0,0 +1,21 @@
<script setup>
import { cn } from '@/lib/utils';
import { useCarousel } from './useCarousel';
const props = defineProps({
class: { type: null, required: false }
});
const { orientation } = useCarousel();
</script>
<template>
<div
data-slot="carousel-item"
role="group"
aria-roledescription="slide"
:class="cn('min-w-0 shrink-0 grow-0 basis-full', orientation === 'horizontal' ? 'pl-4' : 'pt-4', props.class)">
<slot />
</div>
</template>

View File

@@ -0,0 +1,38 @@
<script setup>
import { ArrowRight } from 'lucide-vue-next';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { useCarousel } from './useCarousel';
const props = defineProps({
variant: { type: String, required: false, default: 'outline' },
size: { type: String, required: false, default: 'icon' },
class: { type: null, required: false }
});
const { orientation, canScrollNext, scrollNext } = useCarousel();
</script>
<template>
<Button
data-slot="carousel-next"
:disabled="!canScrollNext"
:class="
cn(
'absolute size-8 rounded-full',
orientation === 'horizontal'
? 'top-1/2 -right-12 -translate-y-1/2'
: '-bottom-12 left-1/2 -translate-x-1/2 rotate-90',
props.class
)
"
:variant="variant"
:size="size"
@click="scrollNext">
<slot>
<ArrowRight />
<span class="sr-only">Next Slide</span>
</slot>
</Button>
</template>

View File

@@ -0,0 +1,38 @@
<script setup>
import { ArrowLeft } from 'lucide-vue-next';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { useCarousel } from './useCarousel';
const props = defineProps({
variant: { type: String, required: false, default: 'outline' },
size: { type: String, required: false, default: 'icon' },
class: { type: null, required: false }
});
const { orientation, canScrollPrev, scrollPrev } = useCarousel();
</script>
<template>
<Button
data-slot="carousel-previous"
:disabled="!canScrollPrev"
:class="
cn(
'absolute size-8 rounded-full',
orientation === 'horizontal'
? 'top-1/2 -left-12 -translate-y-1/2'
: '-top-12 left-1/2 -translate-x-1/2 rotate-90',
props.class
)
"
:variant="variant"
:size="size"
@click="scrollPrev">
<slot>
<ArrowLeft />
<span class="sr-only">Previous Slide</span>
</slot>
</Button>
</template>

View File

@@ -0,0 +1,7 @@
export { default as Carousel } from './Carousel.vue';
export { default as CarouselContent } from './CarouselContent.vue';
export { default as CarouselItem } from './CarouselItem.vue';
export { default as CarouselNext } from './CarouselNext.vue';
export { default as CarouselPrevious } from './CarouselPrevious.vue';
export { useCarousel } from './useCarousel';

View File

@@ -0,0 +1,63 @@
import { onMounted, ref } from 'vue';
import { createInjectionState } from '@vueuse/core';
import emblaCarouselVue from 'embla-carousel-vue';
const [useProvideCarousel, useInjectCarousel] = createInjectionState(
(props, emits) => {
const { opts, orientation, plugins } = props;
const [emblaNode, emblaApi] = emblaCarouselVue(
{
...opts,
axis: orientation === 'horizontal' ? 'x' : 'y'
},
plugins
);
function scrollPrev() {
emblaApi.value?.scrollPrev();
}
function scrollNext() {
emblaApi.value?.scrollNext();
}
const canScrollNext = ref(false);
const canScrollPrev = ref(false);
function onSelect(api) {
canScrollNext.value = api?.canScrollNext() || false;
canScrollPrev.value = api?.canScrollPrev() || false;
}
onMounted(() => {
if (!emblaApi.value) return;
emblaApi.value?.on('init', onSelect);
emblaApi.value?.on('reInit', onSelect);
emblaApi.value?.on('select', onSelect);
emits('init-api', emblaApi.value);
});
return {
carouselRef: emblaNode,
carouselApi: emblaApi,
canScrollPrev,
canScrollNext,
scrollPrev,
scrollNext,
orientation
};
}
);
function useCarousel() {
const carouselState = useInjectCarousel();
if (!carouselState)
throw new Error('useCarousel must be used within a <Carousel />');
return carouselState;
}
export { useCarousel, useProvideCarousel };