From f37b0431382867c24a0eff511ae151115cdfa2a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Hanusek?= Date: Mon, 22 Jan 2024 17:47:14 +0100 Subject: [PATCH] feat(Carousel): new component (#927) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Michał Hanusek Co-authored-by: Inesh Bose Co-authored-by: Benjamin Canac --- .../content/examples/CarouselExample.vue | 16 ++ .../examples/CarouselExampleArrows.vue | 16 ++ .../examples/CarouselExampleArrowsCenter.vue | 35 ++++ .../examples/CarouselExampleIndicators.vue | 16 ++ .../CarouselExampleIndicatorsSize.vue | 16 ++ .../content/examples/CarouselExampleSize.vue | 16 ++ .../examples/CarouselExampleSizeCenter.vue | 16 ++ .../examples/CarouselExampleSizeFull.vue | 16 ++ .../examples/CarouselExampleSlotsDefault.vue | 33 ++++ .../CarouselExampleSlotsIndicator.vue | 33 ++++ .../examples/CarouselExampleSlotsPrevNext.vue | 38 ++++ .../examples/CarouselExampleSnapEnd.vue | 16 ++ .../examples/CarouselExampleSnapStart.vue | 16 ++ docs/content/1.getting-started/3.theming.md | 4 +- docs/content/2.elements/13.carousel.md | 137 ++++++++++++++ src/runtime/components/elements/Carousel.vue | 176 ++++++++++++++++++ src/runtime/components/forms/Input.vue | 2 +- src/runtime/components/forms/Textarea.vue | 2 +- src/runtime/composables/useCarouselScroll.ts | 49 +++++ src/runtime/ui.config/elements/carousel.ts | 26 +++ src/runtime/ui.config/index.ts | 1 + 21 files changed, 676 insertions(+), 4 deletions(-) create mode 100644 docs/components/content/examples/CarouselExample.vue create mode 100644 docs/components/content/examples/CarouselExampleArrows.vue create mode 100644 docs/components/content/examples/CarouselExampleArrowsCenter.vue create mode 100644 docs/components/content/examples/CarouselExampleIndicators.vue create mode 100644 docs/components/content/examples/CarouselExampleIndicatorsSize.vue create mode 100644 docs/components/content/examples/CarouselExampleSize.vue create mode 100644 docs/components/content/examples/CarouselExampleSizeCenter.vue create mode 100644 docs/components/content/examples/CarouselExampleSizeFull.vue create mode 100644 docs/components/content/examples/CarouselExampleSlotsDefault.vue create mode 100644 docs/components/content/examples/CarouselExampleSlotsIndicator.vue create mode 100644 docs/components/content/examples/CarouselExampleSlotsPrevNext.vue create mode 100644 docs/components/content/examples/CarouselExampleSnapEnd.vue create mode 100644 docs/components/content/examples/CarouselExampleSnapStart.vue create mode 100644 docs/content/2.elements/13.carousel.md create mode 100644 src/runtime/components/elements/Carousel.vue create mode 100644 src/runtime/composables/useCarouselScroll.ts create mode 100644 src/runtime/ui.config/elements/carousel.ts diff --git a/docs/components/content/examples/CarouselExample.vue b/docs/components/content/examples/CarouselExample.vue new file mode 100644 index 00000000..031c8107 --- /dev/null +++ b/docs/components/content/examples/CarouselExample.vue @@ -0,0 +1,16 @@ + + + diff --git a/docs/components/content/examples/CarouselExampleArrows.vue b/docs/components/content/examples/CarouselExampleArrows.vue new file mode 100644 index 00000000..f064fe1b --- /dev/null +++ b/docs/components/content/examples/CarouselExampleArrows.vue @@ -0,0 +1,16 @@ + + + diff --git a/docs/components/content/examples/CarouselExampleArrowsCenter.vue b/docs/components/content/examples/CarouselExampleArrowsCenter.vue new file mode 100644 index 00000000..1ad2cac3 --- /dev/null +++ b/docs/components/content/examples/CarouselExampleArrowsCenter.vue @@ -0,0 +1,35 @@ + + + diff --git a/docs/components/content/examples/CarouselExampleIndicators.vue b/docs/components/content/examples/CarouselExampleIndicators.vue new file mode 100644 index 00000000..cd86bf8e --- /dev/null +++ b/docs/components/content/examples/CarouselExampleIndicators.vue @@ -0,0 +1,16 @@ + + + diff --git a/docs/components/content/examples/CarouselExampleIndicatorsSize.vue b/docs/components/content/examples/CarouselExampleIndicatorsSize.vue new file mode 100644 index 00000000..64a583c9 --- /dev/null +++ b/docs/components/content/examples/CarouselExampleIndicatorsSize.vue @@ -0,0 +1,16 @@ + + + diff --git a/docs/components/content/examples/CarouselExampleSize.vue b/docs/components/content/examples/CarouselExampleSize.vue new file mode 100644 index 00000000..82981b66 --- /dev/null +++ b/docs/components/content/examples/CarouselExampleSize.vue @@ -0,0 +1,16 @@ + + + diff --git a/docs/components/content/examples/CarouselExampleSizeCenter.vue b/docs/components/content/examples/CarouselExampleSizeCenter.vue new file mode 100644 index 00000000..cd74b265 --- /dev/null +++ b/docs/components/content/examples/CarouselExampleSizeCenter.vue @@ -0,0 +1,16 @@ + + + diff --git a/docs/components/content/examples/CarouselExampleSizeFull.vue b/docs/components/content/examples/CarouselExampleSizeFull.vue new file mode 100644 index 00000000..1e09b45c --- /dev/null +++ b/docs/components/content/examples/CarouselExampleSizeFull.vue @@ -0,0 +1,16 @@ + + + diff --git a/docs/components/content/examples/CarouselExampleSlotsDefault.vue b/docs/components/content/examples/CarouselExampleSlotsDefault.vue new file mode 100644 index 00000000..47bf6b5a --- /dev/null +++ b/docs/components/content/examples/CarouselExampleSlotsDefault.vue @@ -0,0 +1,33 @@ + + + diff --git a/docs/components/content/examples/CarouselExampleSlotsIndicator.vue b/docs/components/content/examples/CarouselExampleSlotsIndicator.vue new file mode 100644 index 00000000..52e0768d --- /dev/null +++ b/docs/components/content/examples/CarouselExampleSlotsIndicator.vue @@ -0,0 +1,33 @@ + + + diff --git a/docs/components/content/examples/CarouselExampleSlotsPrevNext.vue b/docs/components/content/examples/CarouselExampleSlotsPrevNext.vue new file mode 100644 index 00000000..6461c34f --- /dev/null +++ b/docs/components/content/examples/CarouselExampleSlotsPrevNext.vue @@ -0,0 +1,38 @@ + + + diff --git a/docs/components/content/examples/CarouselExampleSnapEnd.vue b/docs/components/content/examples/CarouselExampleSnapEnd.vue new file mode 100644 index 00000000..5eda03f5 --- /dev/null +++ b/docs/components/content/examples/CarouselExampleSnapEnd.vue @@ -0,0 +1,16 @@ + + + diff --git a/docs/components/content/examples/CarouselExampleSnapStart.vue b/docs/components/content/examples/CarouselExampleSnapStart.vue new file mode 100644 index 00000000..5fb27e52 --- /dev/null +++ b/docs/components/content/examples/CarouselExampleSnapStart.vue @@ -0,0 +1,16 @@ + + + diff --git a/docs/content/1.getting-started/3.theming.md b/docs/content/1.getting-started/3.theming.md index a77e0dc6..4f7f52e7 100644 --- a/docs/content/1.getting-started/3.theming.md +++ b/docs/content/1.getting-started/3.theming.md @@ -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 + + + + +``` +:: + +#### Snap to end + +::component-example +--- +component: 'carousel-example-snap-end' +--- + +#code +```vue + +``` +:: + +::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 diff --git a/src/runtime/components/elements/Carousel.vue b/src/runtime/components/elements/Carousel.vue new file mode 100644 index 00000000..905039e5 --- /dev/null +++ b/src/runtime/components/elements/Carousel.vue @@ -0,0 +1,176 @@ + + + + + diff --git a/src/runtime/components/forms/Input.vue b/src/runtime/components/forms/Input.vue index f9d4a444..333b2ad4 100644 --- a/src/runtime/components/forms/Input.vue +++ b/src/runtime/components/forms/Input.vue @@ -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() } diff --git a/src/runtime/components/forms/Textarea.vue b/src/runtime/components/forms/Textarea.vue index 0d30b357..3e51f80f 100644 --- a/src/runtime/components/forms/Textarea.vue +++ b/src/runtime/components/forms/Textarea.vue @@ -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() } diff --git a/src/runtime/composables/useCarouselScroll.ts b/src/runtime/composables/useCarouselScroll.ts new file mode 100644 index 00000000..69290f22 --- /dev/null +++ b/src/runtime/composables/useCarouselScroll.ts @@ -0,0 +1,49 @@ +import { ref, type Ref, onMounted, onUnmounted } from 'vue' + +export const useCarouselScroll = (el: Ref) => { + const x = ref(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) + }) +} diff --git a/src/runtime/ui.config/elements/carousel.ts b/src/runtime/ui.config/elements/carousel.ts new file mode 100644 index 00000000..297af98c --- /dev/null +++ b/src/runtime/ui.config/elements/carousel.ts @@ -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 ' + } + } +} diff --git a/src/runtime/ui.config/index.ts b/src/runtime/ui.config/index.ts index cf4203be..4255a096 100644 --- a/src/runtime/ui.config/index.ts +++ b/src/runtime/ui.config/index.ts @@ -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'