mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-14 20:19:34 +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:
16
docs/components/content/examples/CarouselExample.vue
Normal file
16
docs/components/content/examples/CarouselExample.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
const items = [
|
||||
'https://picsum.photos/600/800?random=1',
|
||||
'https://picsum.photos/600/800?random=2',
|
||||
'https://picsum.photos/600/800?random=3',
|
||||
'https://picsum.photos/600/800?random=4',
|
||||
'https://picsum.photos/600/800?random=5',
|
||||
'https://picsum.photos/600/800?random=6'
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UCarousel v-slot="{ item }" :items="items">
|
||||
<img :src="item" width="300" height="400">
|
||||
</UCarousel>
|
||||
</template>
|
||||
16
docs/components/content/examples/CarouselExampleArrows.vue
Normal file
16
docs/components/content/examples/CarouselExampleArrows.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
const items = [
|
||||
'https://picsum.photos/1920/1080?random=1',
|
||||
'https://picsum.photos/1920/1080?random=2',
|
||||
'https://picsum.photos/1920/1080?random=3',
|
||||
'https://picsum.photos/1920/1080?random=4',
|
||||
'https://picsum.photos/1920/1080?random=5',
|
||||
'https://picsum.photos/1920/1080?random=6'
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UCarousel v-slot="{ item }" :items="items" :ui="{ item: 'basis-full' }" class="rounded-lg overflow-hidden" arrows>
|
||||
<img :src="item" class="w-full">
|
||||
</UCarousel>
|
||||
</template>
|
||||
@@ -0,0 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
const items = [
|
||||
'https://picsum.photos/600/800?random=1',
|
||||
'https://picsum.photos/600/800?random=2',
|
||||
'https://picsum.photos/600/800?random=3',
|
||||
'https://picsum.photos/600/800?random=4',
|
||||
'https://picsum.photos/600/800?random=5',
|
||||
'https://picsum.photos/600/800?random=6'
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UCarousel
|
||||
v-slot="{ item }"
|
||||
:items="items"
|
||||
:ui="{
|
||||
item: 'basis-full',
|
||||
container: 'rounded-lg'
|
||||
}"
|
||||
:prev-button="{
|
||||
color: 'gray',
|
||||
icon: 'i-heroicons-arrow-left-20-solid',
|
||||
class: '-left-12'
|
||||
}"
|
||||
:next-button="{
|
||||
color: 'gray',
|
||||
icon: 'i-heroicons-arrow-right-20-solid',
|
||||
class: '-right-12'
|
||||
}"
|
||||
arrows
|
||||
class="w-64 mx-auto"
|
||||
>
|
||||
<img :src="item" class="w-full">
|
||||
</UCarousel>
|
||||
</template>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
const items = [
|
||||
'https://picsum.photos/1920/1080?random=1',
|
||||
'https://picsum.photos/1920/1080?random=2',
|
||||
'https://picsum.photos/1920/1080?random=3',
|
||||
'https://picsum.photos/1920/1080?random=4',
|
||||
'https://picsum.photos/1920/1080?random=5',
|
||||
'https://picsum.photos/1920/1080?random=6'
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UCarousel v-slot="{ item }" :items="items" :ui="{ item: 'basis-full' }" class="rounded-lg overflow-hidden" indicators>
|
||||
<img :src="item" class="w-full">
|
||||
</UCarousel>
|
||||
</template>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
const items = [
|
||||
'https://picsum.photos/600/600?random=1',
|
||||
'https://picsum.photos/600/600?random=2',
|
||||
'https://picsum.photos/600/600?random=3',
|
||||
'https://picsum.photos/600/600?random=4',
|
||||
'https://picsum.photos/600/600?random=5',
|
||||
'https://picsum.photos/600/600?random=6'
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UCarousel v-slot="{ item }" :items="items" :ui="{ item: 'basis-full md:basis-1/2 lg:basis-1/3' }" indicators class="rounded-lg overflow-hidden">
|
||||
<img :src="item" class="w-full">
|
||||
</UCarousel>
|
||||
</template>
|
||||
16
docs/components/content/examples/CarouselExampleSize.vue
Normal file
16
docs/components/content/examples/CarouselExampleSize.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
const items = [
|
||||
'https://picsum.photos/600/600?random=1',
|
||||
'https://picsum.photos/600/600?random=2',
|
||||
'https://picsum.photos/600/600?random=3',
|
||||
'https://picsum.photos/600/600?random=4',
|
||||
'https://picsum.photos/600/600?random=5',
|
||||
'https://picsum.photos/600/600?random=6'
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UCarousel v-slot="{ item }" :items="items" :ui="{ item: 'basis-full md:basis-1/2 lg:basis-1/3' }">
|
||||
<img :src="item" class="w-full">
|
||||
</UCarousel>
|
||||
</template>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
const items = [
|
||||
'https://picsum.photos/600/800?random=1',
|
||||
'https://picsum.photos/600/800?random=2',
|
||||
'https://picsum.photos/600/800?random=3',
|
||||
'https://picsum.photos/600/800?random=4',
|
||||
'https://picsum.photos/600/800?random=5',
|
||||
'https://picsum.photos/600/800?random=6'
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UCarousel v-slot="{ item }" :items="items" :ui="{ item: 'basis-full' }" class="w-64 mx-auto rounded-lg overflow-hidden">
|
||||
<img :src="item" class="w-full">
|
||||
</UCarousel>
|
||||
</template>
|
||||
16
docs/components/content/examples/CarouselExampleSizeFull.vue
Normal file
16
docs/components/content/examples/CarouselExampleSizeFull.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
const items = [
|
||||
'https://picsum.photos/1920/1080?random=1',
|
||||
'https://picsum.photos/1920/1080?random=2',
|
||||
'https://picsum.photos/1920/1080?random=3',
|
||||
'https://picsum.photos/1920/1080?random=4',
|
||||
'https://picsum.photos/1920/1080?random=5',
|
||||
'https://picsum.photos/1920/1080?random=6'
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UCarousel v-slot="{ item }" :items="items" :ui="{ item: 'basis-full' }" class="rounded-lg overflow-hidden">
|
||||
<img :src="item" class="w-full">
|
||||
</UCarousel>
|
||||
</template>
|
||||
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
const items = [{
|
||||
name: 'Sébastien Chopin',
|
||||
to: 'https://github.com/Atinux',
|
||||
avatar: { src: 'https://ipx.nuxt.com/f_auto,s_192x192/gh_avatar/atinux' }
|
||||
}, {
|
||||
name: 'Pooya Parsa',
|
||||
to: 'https://github.com/pi0',
|
||||
avatar: { src: 'https://ipx.nuxt.com/f_auto,s_192x192/gh_avatar/pi0' }
|
||||
}, {
|
||||
name: 'Daniel Roe',
|
||||
to: 'https://github.com/danielroe',
|
||||
avatar: { src: 'https://ipx.nuxt.com/f_auto,s_192x192/gh_avatar/danielroe' }
|
||||
}, {
|
||||
name: 'Anthony Fu',
|
||||
to: 'https://github.com/antfu',
|
||||
avatar: { src: 'https://ipx.nuxt.com/f_auto,s_192x192/gh_avatar/antfu' }
|
||||
}]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UCarousel :items="items" :ui="{ item: 'w-full' }">
|
||||
<template #default="{ item, index }">
|
||||
<div class="text-center mx-auto">
|
||||
<img :src="item.avatar.src" :alt="item.name" class="rounded-full w-48 h-48 mb-2">
|
||||
|
||||
<p class="font-semibold">
|
||||
{{ index + 1 }}. {{ item.name }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</UCarousel>
|
||||
</template>
|
||||
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
const items = [
|
||||
'https://picsum.photos/600/800?random=1',
|
||||
'https://picsum.photos/600/800?random=2',
|
||||
'https://picsum.photos/600/800?random=3',
|
||||
'https://picsum.photos/600/800?random=4',
|
||||
'https://picsum.photos/600/800?random=5',
|
||||
'https://picsum.photos/600/800?random=6'
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UCarousel
|
||||
:items="items"
|
||||
:ui="{
|
||||
item: 'basis-full',
|
||||
container: 'rounded-lg',
|
||||
indicators: {
|
||||
wrapper: 'relative bottom-0 mt-4'
|
||||
}
|
||||
}"
|
||||
indicators
|
||||
class="w-64 mx-auto"
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<img :src="item" class="w-full">
|
||||
</template>
|
||||
|
||||
<template #indicator="{ onClick, index, active }">
|
||||
<UButton :label="index" :variant="active ? 'solid' : 'outline'" size="2xs" class="rounded-full min-w-6 justify-center" @click="onClick(index)" />
|
||||
</template>
|
||||
</UCarousel>
|
||||
</template>
|
||||
@@ -0,0 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
const items = [
|
||||
'https://picsum.photos/600/800?random=1',
|
||||
'https://picsum.photos/600/800?random=2',
|
||||
'https://picsum.photos/600/800?random=3',
|
||||
'https://picsum.photos/600/800?random=4',
|
||||
'https://picsum.photos/600/800?random=5',
|
||||
'https://picsum.photos/600/800?random=6'
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UCarousel
|
||||
:items="items"
|
||||
:ui="{
|
||||
item: 'basis-full',
|
||||
container: 'rounded-lg'
|
||||
}"
|
||||
arrows
|
||||
class="w-64 mx-auto"
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<img :src="item" class="w-full">
|
||||
</template>
|
||||
|
||||
<template #prev="{ onClick, disabled }">
|
||||
<button :disabled="disabled" @click="onClick">
|
||||
Prev
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<template #next="{ onClick, disabled }">
|
||||
<button :disabled="disabled" class="" @click="onClick">
|
||||
Next
|
||||
</button>
|
||||
</template>
|
||||
</UCarousel>
|
||||
</template>
|
||||
16
docs/components/content/examples/CarouselExampleSnapEnd.vue
Normal file
16
docs/components/content/examples/CarouselExampleSnapEnd.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
const items = [
|
||||
'https://picsum.photos/400/600?random=1',
|
||||
'https://picsum.photos/400/600?random=2',
|
||||
'https://picsum.photos/400/600?random=3',
|
||||
'https://picsum.photos/400/600?random=4',
|
||||
'https://picsum.photos/400/600?random=5',
|
||||
'https://picsum.photos/400/600?random=6'
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UCarousel v-slot="{ item }" :items="items" :ui="{ item: 'snap-end' }">
|
||||
<img :src="item" width="200" height="300">
|
||||
</UCarousel>
|
||||
</template>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
const items = [
|
||||
'https://picsum.photos/400/600?random=1',
|
||||
'https://picsum.photos/400/600?random=2',
|
||||
'https://picsum.photos/400/600?random=3',
|
||||
'https://picsum.photos/400/600?random=4',
|
||||
'https://picsum.photos/400/600?random=5',
|
||||
'https://picsum.photos/400/600?random=6'
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UCarousel v-slot="{ item }" :items="items" :ui="{ item: 'snap-start' }">
|
||||
<img :src="item" width="200" height="300">
|
||||
</UCarousel>
|
||||
</template>
|
||||
@@ -122,7 +122,7 @@ export default defineAppConfig({
|
||||
|
||||
Thanks to [tailwind-merge](https://github.com/dcastil/tailwind-merge), the `app.config.ts` is smartly merged with the default config. This means you don't have to rewrite everything.
|
||||
|
||||
You can change this behaviour by setting `strategy` to `override` in your `app.config.ts`:
|
||||
You can change this behavior by setting `strategy` to `override` in your `app.config.ts`:
|
||||
|
||||
```ts [app.config.ts]
|
||||
export default defineAppConfig({
|
||||
@@ -175,7 +175,7 @@ To change the font of the `label`, you only need to write:
|
||||
|
||||
This will smartly replace the `font-medium` by `font-semibold` and prevent any class duplication and any class priority issue.
|
||||
|
||||
You can change this behaviour by setting `strategy` to `override` inside the `ui` prop:
|
||||
You can change this behavior by setting `strategy` to `override` inside the `ui` prop:
|
||||
|
||||
```vue
|
||||
<UButton
|
||||
|
||||
137
docs/content/2.elements/13.carousel.md
Normal file
137
docs/content/2.elements/13.carousel.md
Normal file
@@ -0,0 +1,137 @@
|
||||
---
|
||||
description: Display images or content in a scrollable area.
|
||||
links:
|
||||
- label: GitHub
|
||||
icon: i-simple-icons-github
|
||||
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/elements/Carousel.vue
|
||||
navigation:
|
||||
badge: New
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
Drag with your mouse, scroll with your mouse wheel or use the navigation arrows to navigate.
|
||||
|
||||
Pass an array to the `items` prop and use the default slot to display the content of each item.
|
||||
|
||||
:component-example{component="carousel-example"}
|
||||
|
||||
### Snap
|
||||
|
||||
The carousel will snap the item to the center (`snap-center`). You can use the `snap` utility classes to change this behavior using the `ui` prop:
|
||||
|
||||
#### Snap to start
|
||||
|
||||
::component-example
|
||||
---
|
||||
component: 'carousel-example-snap-start'
|
||||
---
|
||||
|
||||
#code
|
||||
```vue
|
||||
<template>
|
||||
<UCarousel v-slot="{ item }" :items="items" :ui="{ item: 'snap-start' }">
|
||||
<img :src="item" width="200" height="300">
|
||||
</UCarousel>
|
||||
</template>
|
||||
```
|
||||
::
|
||||
|
||||
#### Snap to end
|
||||
|
||||
::component-example
|
||||
---
|
||||
component: 'carousel-example-snap-end'
|
||||
---
|
||||
|
||||
#code
|
||||
```vue
|
||||
<template>
|
||||
<UCarousel v-slot="{ item }" :items="items" :ui="{ item: 'snap-end' }">
|
||||
<img :src="item" width="200" height="300">
|
||||
</UCarousel>
|
||||
</template>
|
||||
```
|
||||
::
|
||||
|
||||
::callout{icon="i-heroicons-light-bulb" to="https://tailwindcss.com/docs/scroll-snap-align" target="_blank"}
|
||||
Learn more about the `scroll-snap-align` property on the Tailwind CSS documentation.
|
||||
::
|
||||
|
||||
### Size
|
||||
|
||||
Each item will take its own size by default in the carousel. You can use the `basis` / `width` utility classes to change this behavior using the `ui` prop:
|
||||
|
||||
:component-example{component="carousel-example-size"}
|
||||
|
||||
In this example, we used `basis-1/3` to display 3 items at a time but you can also use this to make the carousel full width using `basis-full` and display only one item at a time:
|
||||
|
||||
:component-example{component="carousel-example-size-full"}
|
||||
|
||||
You can also set a width on the container to center the carousel:
|
||||
|
||||
:component-example{component="carousel-example-size-center"}
|
||||
|
||||
::callout{icon="i-heroicons-light-bulb" to="https://tailwindcss.com/docs/flex-basis" target="_blank"}
|
||||
Learn more about the `flex-basis` property on the Tailwind CSS documentation.
|
||||
::
|
||||
|
||||
## Navigation
|
||||
|
||||
### Arrows
|
||||
|
||||
Use the `arrows` prop to enable prev and next buttons, they will be automatically disabled when the carousel reaches the first or last item.
|
||||
|
||||
:component-example{component="carousel-example-arrows"}
|
||||
|
||||
You can also customize the prev and next buttons using the `prev-button` and `next-button` props:
|
||||
|
||||
:component-example{component="carousel-example-arrows-center"}
|
||||
|
||||
In this example, we move the buttons outside of the carousel container. You can also change this globally in `ui.carousel.default.prevButton` and `ui.carousel.default.nextButton`.
|
||||
|
||||
### Indicators
|
||||
|
||||
Use the `indicators` prop to display a list of buttons at the bottom of the carousel to navigate between items.
|
||||
|
||||
:component-example{component="carousel-example-indicators"}
|
||||
|
||||
The number of indicators will be automatically generated based on the number of items:
|
||||
|
||||
:component-example{component="carousel-example-indicators-size"}
|
||||
|
||||
## Slots
|
||||
|
||||
### `default`
|
||||
|
||||
You can put anything inside the default slot, not only images. You will have access to the `item` and `index` properties in the slot scope.
|
||||
|
||||
:component-example{component="carousel-example-slots-default"}
|
||||
|
||||
### `prev` / `next`
|
||||
|
||||
With the `arrows` prop enabled, use the `#prev` and `#next` slots to set the content of the previous and next buttons. You will have access to the `disabled` property and `on-click` method in the slot scope.
|
||||
|
||||
:component-example{component="carousel-example-slots-prev-next"}
|
||||
|
||||
::callout{icon="i-heroicons-light-bulb"}
|
||||
You can customize the position of the buttons through `ui.arrows.wrapper`.
|
||||
::
|
||||
|
||||
### `indicator`
|
||||
|
||||
With the `indicators` prop enabled, use the `#indicator` slot to set the content of the indicators. You will have access to the `active`, `index` properties and `on-click` method in the slot scope.
|
||||
|
||||
:component-example{component="carousel-example-slots-indicator"}
|
||||
|
||||
::callout{icon="i-heroicons-light-bulb"}
|
||||
You can customize the position of the buttons through `ui.indicators.wrapper`.
|
||||
::
|
||||
|
||||
## Props
|
||||
|
||||
:component-props
|
||||
|
||||
## Config
|
||||
|
||||
:component-preset
|
||||
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>
|
||||
@@ -211,7 +211,7 @@ export default defineComponent({
|
||||
updateInput(value)
|
||||
}
|
||||
|
||||
// Update trimmed input so that it has same behaviour as native input https://github.com/vuejs/core/blob/5ea8a8a4fab4e19a71e123e4d27d051f5e927172/packages/runtime-dom/src/directives/vModel.ts#L63
|
||||
// Update trimmed input so that it has same behavior as native input https://github.com/vuejs/core/blob/5ea8a8a4fab4e19a71e123e4d27d051f5e927172/packages/runtime-dom/src/directives/vModel.ts#L63
|
||||
if (modelModifiers.value.trim) {
|
||||
(event.target as HTMLInputElement).value = value.trim()
|
||||
}
|
||||
|
||||
@@ -193,7 +193,7 @@ export default defineComponent({
|
||||
updateInput(value)
|
||||
}
|
||||
|
||||
// Update trimmed input so that it has same behaviour as native input
|
||||
// Update trimmed input so that it has same behavior as native input
|
||||
if (modelModifiers.value.trim) {
|
||||
(event.target as HTMLInputElement).value = value.trim()
|
||||
}
|
||||
|
||||
49
src/runtime/composables/useCarouselScroll.ts
Normal file
49
src/runtime/composables/useCarouselScroll.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { ref, type Ref, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
export const useCarouselScroll = (el: Ref<HTMLElement>) => {
|
||||
const x = ref<number>(0)
|
||||
|
||||
function onMouseDown (e) {
|
||||
el.value.style.scrollSnapType = 'none'
|
||||
el.value.style.scrollBehavior = 'auto'
|
||||
|
||||
x.value = e.pageX
|
||||
|
||||
window.addEventListener('mousemove', onMouseMove)
|
||||
window.addEventListener('mouseup', onMouseUp)
|
||||
}
|
||||
|
||||
function onMouseUp () {
|
||||
el.value.style.removeProperty('scroll-behavior')
|
||||
el.value.style.removeProperty('scroll-snap-type')
|
||||
|
||||
window.removeEventListener('mousemove', onMouseMove)
|
||||
window.removeEventListener('mouseup', onMouseUp)
|
||||
}
|
||||
|
||||
function onMouseMove (e) {
|
||||
e.preventDefault()
|
||||
|
||||
const delta = e.pageX - x.value
|
||||
|
||||
x.value = e.pageX
|
||||
|
||||
el.value.scrollBy(-delta, 0)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!el.value) {
|
||||
return
|
||||
}
|
||||
|
||||
el.value.addEventListener('mousedown', onMouseDown)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (!el.value) {
|
||||
return
|
||||
}
|
||||
|
||||
el.value.removeEventListener('mousedown', onMouseDown)
|
||||
})
|
||||
}
|
||||
26
src/runtime/ui.config/elements/carousel.ts
Normal file
26
src/runtime/ui.config/elements/carousel.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
export default {
|
||||
wrapper: 'relative',
|
||||
container: 'relative w-full flex overflow-x-auto snap-x snap-mandatory scroll-smooth',
|
||||
item: 'flex flex-none snap-center',
|
||||
arrows: {
|
||||
wrapper: 'flex items-center justify-between'
|
||||
},
|
||||
indicators: {
|
||||
wrapper: 'absolute flex items-center justify-center gap-3 bottom-4 inset-x-0',
|
||||
base: 'rounded-full h-3 w-3',
|
||||
active: 'bg-primary-500 dark:bg-primary-400',
|
||||
inactive: 'bg-gray-100 dark:bg-gray-800 mix-blend-overlay'
|
||||
},
|
||||
default: {
|
||||
prevButton: {
|
||||
color: 'black' as const,
|
||||
class: 'rtl:[&_span:first-child]:rotate-180 absolute left-4 top-1/2 transform -translate-y-1/2 rounded-full',
|
||||
icon: 'i-heroicons-chevron-left-20-solid'
|
||||
},
|
||||
nextButton: {
|
||||
color: 'black' as const,
|
||||
class: 'rtl:[&_span:last-child]:rotate-180 absolute right-4 top-1/2 transform -translate-y-1/2 rounded-full',
|
||||
icon: 'i-heroicons-chevron-right-20-solid '
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ export { default as kbd } from './elements/kbd'
|
||||
export { default as progress } from './elements/progress'
|
||||
export { default as meter } from './elements/meter'
|
||||
export { default as meterGroup } from './elements/meterGroup'
|
||||
export { default as carousel } from './elements/carousel'
|
||||
|
||||
// Forms
|
||||
export { default as input } from './forms/input'
|
||||
|
||||
Reference in New Issue
Block a user