From ac5e90b1d526da4a497194fed0338e1a50a9f818 Mon Sep 17 00:00:00 2001 From: Italo Israel Baeza Cabrera Date: Sun, 19 Nov 2023 02:12:40 -0300 Subject: [PATCH] feat(List): new component --- .../content/examples/ListExample.vue | 20 ++++ .../content/examples/ListExampleGap.vue | 16 +++ .../examples/ListExampleItemOrientation.vue | 25 ++++ .../content/examples/ListExampleOrdered.vue | 20 ++++ .../examples/ListExampleOrientation.vue | 22 ++++ .../examples/ListExamplePaddingClass.vue | 16 +++ .../ListExampleSlotSeparatorAfter.vue | 20 ++++ .../ListExampleSlotSeparatorBefore.vue | 20 ++++ .../content/examples/ListExampleWrap.vue | 22 ++++ docs/content/7.layout/5.list.md | 87 ++++++++++++++ src/runtime/components/layout/List.ts | 107 ++++++++++++++++++ src/runtime/ui.config.ts | 14 +++ 12 files changed, 389 insertions(+) create mode 100644 docs/components/content/examples/ListExample.vue create mode 100644 docs/components/content/examples/ListExampleGap.vue create mode 100644 docs/components/content/examples/ListExampleItemOrientation.vue create mode 100644 docs/components/content/examples/ListExampleOrdered.vue create mode 100644 docs/components/content/examples/ListExampleOrientation.vue create mode 100644 docs/components/content/examples/ListExamplePaddingClass.vue create mode 100644 docs/components/content/examples/ListExampleSlotSeparatorAfter.vue create mode 100644 docs/components/content/examples/ListExampleSlotSeparatorBefore.vue create mode 100644 docs/components/content/examples/ListExampleWrap.vue create mode 100644 docs/content/7.layout/5.list.md create mode 100644 src/runtime/components/layout/List.ts diff --git a/docs/components/content/examples/ListExample.vue b/docs/components/content/examples/ListExample.vue new file mode 100644 index 0000000000..4c49e26752 --- /dev/null +++ b/docs/components/content/examples/ListExample.vue @@ -0,0 +1,20 @@ + + + diff --git a/docs/components/content/examples/ListExampleGap.vue b/docs/components/content/examples/ListExampleGap.vue new file mode 100644 index 0000000000..0ff2e1e7e5 --- /dev/null +++ b/docs/components/content/examples/ListExampleGap.vue @@ -0,0 +1,16 @@ + + + diff --git a/docs/components/content/examples/ListExampleItemOrientation.vue b/docs/components/content/examples/ListExampleItemOrientation.vue new file mode 100644 index 0000000000..7b52ce5bf7 --- /dev/null +++ b/docs/components/content/examples/ListExampleItemOrientation.vue @@ -0,0 +1,25 @@ + + + diff --git a/docs/components/content/examples/ListExampleOrdered.vue b/docs/components/content/examples/ListExampleOrdered.vue new file mode 100644 index 0000000000..9e444db5e1 --- /dev/null +++ b/docs/components/content/examples/ListExampleOrdered.vue @@ -0,0 +1,20 @@ + + + diff --git a/docs/components/content/examples/ListExampleOrientation.vue b/docs/components/content/examples/ListExampleOrientation.vue new file mode 100644 index 0000000000..7501104625 --- /dev/null +++ b/docs/components/content/examples/ListExampleOrientation.vue @@ -0,0 +1,22 @@ + + + diff --git a/docs/components/content/examples/ListExamplePaddingClass.vue b/docs/components/content/examples/ListExamplePaddingClass.vue new file mode 100644 index 0000000000..8947f6cbf4 --- /dev/null +++ b/docs/components/content/examples/ListExamplePaddingClass.vue @@ -0,0 +1,16 @@ + + + diff --git a/docs/components/content/examples/ListExampleSlotSeparatorAfter.vue b/docs/components/content/examples/ListExampleSlotSeparatorAfter.vue new file mode 100644 index 0000000000..edfb873e2f --- /dev/null +++ b/docs/components/content/examples/ListExampleSlotSeparatorAfter.vue @@ -0,0 +1,20 @@ + + + diff --git a/docs/components/content/examples/ListExampleSlotSeparatorBefore.vue b/docs/components/content/examples/ListExampleSlotSeparatorBefore.vue new file mode 100644 index 0000000000..70eb50396e --- /dev/null +++ b/docs/components/content/examples/ListExampleSlotSeparatorBefore.vue @@ -0,0 +1,20 @@ + + + diff --git a/docs/components/content/examples/ListExampleWrap.vue b/docs/components/content/examples/ListExampleWrap.vue new file mode 100644 index 0000000000..2a99ec5c2a --- /dev/null +++ b/docs/components/content/examples/ListExampleWrap.vue @@ -0,0 +1,22 @@ + + + diff --git a/docs/content/7.layout/5.list.md b/docs/content/7.layout/5.list.md new file mode 100644 index 0000000000..d7e95cf32c --- /dev/null +++ b/docs/content/7.layout/5.list.md @@ -0,0 +1,87 @@ +--- +description: Create horizontal o vertical lists with separators. +links: + - label: GitHub + icon: i-simple-icons-github + to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/layout/List.vue +--- + +## Usage + +Add list items to the `UList` component, and a separator using the [`separator-before`](#separator-before) or [`separator-after`](#separator-after) slots. + +List items can be from simple HTML elements, to a `v-for` loop of components. + +:component-example{component="list-example"} + +Lists are unordered by default, created using the `ul` tag. You may change the HTML element to `ol`, which semantically makes the list _ordered_, by setting the `ordered` prop to `true`. + +:component-example{component="list-example-ordered"} + +::callout{icon="i-heroicons-light-bulb"} +Changing `ordered` to `true` makes only a semantic change, not a visual change. +:: + +### Orientation + +By default, items are stacked vertically. To stack the items horizontally, set the `orientation` prop to `horizontal`. + +:component-example{component="list-example-orientation"} + +### Item Orientation + +Both list and separators have the same stacking orientation. By default, as the list is vertical, and each item (contents and separators) are also stacked vertically. + +The `itemOrientation` allows to change the stacking orientation of the items regardless of the list orientation. + +:component-example{component="list-example-item-orientation"} + +### Gap + +To add a default space in between each item, use the `gap` prop as `true`. + +:component-example{component="list-example-gap"} + +Alternatively, you may add a gap manually using the `class` attribute like any other HTML Element. + +:component-example{component="list-example-padding-class"} + +::callout{icon="i-heroicons-light-bulb"} +Items are listed using Tailwind CSS [`flex`](https://tailwindcss.com/docs/flex). You can change this through the [UI configuration](#config). +:: + +### Wrapping + +When the items exceed the container height or width, these will not be wrapped into another line. To enable this behaviour, set the `wrap` prop to `true`. + +:component-example{component="list-example-wrap"} + +## Slots + +### `separator-before` + +Use this slot to set a separator **before** the item **contents**. It receives the current `index` of the item where is located, and both `isFirst` and `isLast` boolean if is the first or last item of the list, respectively. + +:component-example{component="list-example-slot-separator-before"} + +::callout{icon="i-heroicons-exclamation-triangle"} +Both `isFirst` and `isLast` booleans always returns `true` if there is only one item. +:: + +### `separator-after` + +Use this slot to set a separator **after** the item **contents**. It receives the current `index` of the item where is located, and both `isFirst` and `isLast` boolean if is the first or last item of the list, respectively. + +:component-example{component="list-example-slot-separator-after"} + +::callout{icon="i-heroicons-exclamation-triangle"} +Both `isFirst` and `isLast` booleans always returns `true` if there is only one item. +:: + +## Props + +:component-props + +## Config + +:component-preset diff --git a/src/runtime/components/layout/List.ts b/src/runtime/components/layout/List.ts new file mode 100644 index 0000000000..132f15b4eb --- /dev/null +++ b/src/runtime/components/layout/List.ts @@ -0,0 +1,107 @@ +import { h, computed, toRef, defineComponent, onMounted } from 'vue' +import type { PropType, SlotsType } from 'vue' +import type { RequireAtLeastOne } from 'type-fest' +import { twMerge, twJoin } from 'tailwind-merge' +import { useUI } from '../../composables/useUI' +import type { Strategy } from '../../types' +// @ts-expect-error +import appConfig from '#build/app.config' +import { mergeConfig, getSlotsChildren } from '#ui/utils' +import { list } from '#ui/ui.config' + +const config = mergeConfig(appConfig.ui.strategy, appConfig.ui.list, list) + +export default defineComponent({ + inheritAttrs: false, + props: { + ordered: { + type: Boolean, + default: false + }, + orientation: { + type: String as PropType<'horizontal' | 'vertical'>, + default: 'vertical', + validator (value: string) { + return Object.keys(config).includes(value) + } + }, + gap: { + type: Boolean, + default: false + }, + wrap: { + type: Boolean, + default: () => config.wrapItems + }, + itemOrientation: { + type: String as PropType<'horizontal' | 'vertical'>, + default: undefined, + validator (value: string|undefined) { + return typeof value === 'string' ? Object.keys(config).includes(value) : true + } + }, + class: { + type: [String, Object, Array] as PropType, + default: undefined + }, + ui: { + type: Object as PropType>, + default: undefined + } + }, + slots: Object as SlotsType< + RequireAtLeastOne<{ + default: undefined, + 'separator-before'?: { index: number, isFirst: boolean, isLast: boolean }, + 'separator-after'?: { index: number, isFirst: boolean, isLast: boolean }, + }, 'separator-before' | 'separator-after'> + >, + setup (props, { slots }) { + const { ui, attrs } = useUI('list', toRef(props, 'ui'), config) + + const listClass = computed(() => { + return twMerge(twJoin( + ui.value[props.orientation].base, + props.wrap ? ui.value.wrap : ui.value.nowrap, + props.gap ? ui.value[props.orientation].gap : '' + ), props.class) + }) + + const itemClass = computed(() => { + return twJoin( + ui.value[props.itemOrientation ?? props.orientation].base, + ui.value.nowrap + ) + }) + + function addSeparator (array) { + return array.map((item, index) => { + const children = [] + + const isFirst = array.length === 1 + || ((slots['separator-before'] && index === 1) || (slots['separator-after'] && index === 0)) + + const isLast = array.length === 1 + || ((slots['separator-before'] && index === (array.length - 1)) || (slots['separator-after'] && index === (array.length - 2))) + + if (slots['separator-before'] && (index > 0)) { + children.push(slots['separator-before']({ index, isFirst, isLast })) + } + + children.push(item) + + if (slots['separator-after'] && (index < (array.length - 1))) { + children.push(slots['separator-after']({ index, isFirst, isLast })) + } + + return h('li', { class: itemClass.value }, children) + }) + } + + const children = computed(() => addSeparator(getSlotsChildren(slots))) + + const orderedElement = computed(() => props.ordered ? 'ol' : 'ul') + + return () => h(orderedElement.value, { ...attrs, class: listClass.value, ref: 'list' }, children.value) + } +}) diff --git a/src/runtime/ui.config.ts b/src/runtime/ui.config.ts index 6dc2781428..748ce7150c 100644 --- a/src/runtime/ui.config.ts +++ b/src/runtime/ui.config.ts @@ -996,6 +996,20 @@ export const divider = { label: 'text-sm' } +export const list = { + vertical: { + base: 'flex flex-col', + gap: 'gap-4' + }, + horizontal: { + base: 'flex flex-row', + gap: 'gap-4' + }, + wrapItems: false, + wrap: 'flex-wrap', + nowrap: 'flex-nowrap' +} + // Navigation export const verticalNavigation = {