Skip to content

Commit

Permalink
feat(List): new component
Browse files Browse the repository at this point in the history
  • Loading branch information
DarkGhostHunter committed Nov 20, 2023
1 parent 9b976a0 commit c3db82e
Show file tree
Hide file tree
Showing 11 changed files with 376 additions and 0 deletions.
20 changes: 20 additions & 0 deletions docs/components/content/examples/ListExample.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<template>
<UList>
<div v-for="(item, key) in items" :key="key" class="flex items-center gap-2">
<UIcon :name="item.icon" /> {{ item.label }}
</div>

<template #separator-after>
<UDivider class="my-2" />
</template>
</UList>
</template>

<script setup>
const items = [
{ label: 'Home', icon: 'i-heroicons-home' },
{ label: 'Profile', icon: 'i-heroicons-user-circle' },
{ label: 'Security', icon: 'i-heroicons-shield-check' },
{ label: 'Password Reset', icon: 'i-heroicons-key' }
]
</script>
20 changes: 20 additions & 0 deletions docs/components/content/examples/ListExampleGap.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<template>
<UList gap>
<div v-for="(item, key) in items" :key="key" class="rounded bg-gray-200 dark:bg-gray-800 px-2 flex items-center gap-2">
<UIcon :name="item.icon" /> {{ item.label }}
</div>

<template #separator-after>
<UDivider class="mt-4" />
</template>
</UList>
</template>

<script setup>
const items = [
{ label: 'Home', icon: 'i-heroicons-home' },
{ label: 'Users', icon: 'i-heroicons-users' },
{ label: 'Carts', icon: 'i-heroicons-shopping-bag' },
{ label: 'Shipments', icon: 'i-heroicons-truck' }
]
</script>
25 changes: 25 additions & 0 deletions docs/components/content/examples/ListExampleItemOrientation.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<template>
<UList item-orientation="horizontal">
<template #separator-before="{ index }">
<div
class="flex items-center text-right text-green-500 pr-1"
:style="`padding-left: ${Math.max(0, index - 1)}em`"
>
<UIcon name="i-heroicons-arrow-uturn-right" class="-scale-y-100" />
</div>
</template>

<div v-for="(item, key) in items" :key="key" class="flex items-center gap-2">
{{ item.label }}
</div>
</UList>
</template>

<script setup>
const items = [
{ label: 'Home' },
{ label: 'Profile' },
{ label: 'Security' },
{ label: 'Password Reset' }
]
</script>
22 changes: 22 additions & 0 deletions docs/components/content/examples/ListExampleOrientation.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<template>
<UList orientation="horizontal">
<div v-for="(item, key) in items" :key="key" class="flex items-center gap-2">
{{ item.label }}
</div>

<template #separator-after>
<div class="flex items-center px-1 text-gray-400 dark:text-gray-600">
<UIcon name="i-heroicons-chevron-right" />
</div>
</template>
</UList>
</template>

<script setup>
const items = [
{ label: 'Home' },
{ label: 'Profile' },
{ label: 'Security' },
{ label: 'Password Reset' }
]
</script>
20 changes: 20 additions & 0 deletions docs/components/content/examples/ListExamplePaddingClass.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<template>
<UList class="gap-y-1">
<div v-for="(item, key) in items" :key="key" class="rounded bg-gray-200 dark:bg-gray-800 px-2 flex items-center gap-2">
<UIcon :name="item.icon" /> {{ item.label }}
</div>

<template #separator-after>
<UDivider class="mt-1" />
</template>
</UList>
</template>

<script setup>
const items = [
{ label: 'Home', icon: 'i-heroicons-home' },
{ label: 'Users', icon: 'i-heroicons-users' },
{ label: 'Carts', icon: 'i-heroicons-shopping-bag' },
{ label: 'Shipments', icon: 'i-heroicons-truck' }
]
</script>
20 changes: 20 additions & 0 deletions docs/components/content/examples/ListExampleSlotSeparatorAfter.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<template>
<UList>
<div v-for="(item, key) in items" :key="key" class="flex items-center gap-2">
<UIcon :name="item.icon" /> {{ item.label }}
</div>

<template #separator-after>
<hr class="border-gray-200 dark:border-gray-800 my-2">
</template>
</UList>
</template>

<script setup>
const items = [
{ label: 'Home', icon: 'i-heroicons-home' },
{ label: 'Profile', icon: 'i-heroicons-user-circle' },
{ label: 'Security', icon: 'i-heroicons-shield-check' },
{ label: 'Password Reset', icon: 'i-heroicons-key' }
]
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<template>
<UList>
<template #separator-before>
<hr class="border-gray-200 dark:border-gray-800 my-2">
</template>

<div v-for="(item, key) in items" :key="key" class="flex items-center gap-2">
<UIcon :name="item.icon" /> {{ item.label }}
</div>
</UList>
</template>

<script setup>
const items = [
{ label: 'Home', icon: 'i-heroicons-home' },
{ label: 'Profile', icon: 'i-heroicons-user-circle' },
{ label: 'Security', icon: 'i-heroicons-shield-check' },
{ label: 'Password Reset', icon: 'i-heroicons-key' }
]
</script>
20 changes: 20 additions & 0 deletions docs/components/content/examples/ListExampleWrap.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<template>
<UList class="w-64" orientation="horizontal" wrap gap>
<div
v-for="(item, key) in items"
:key="key"
class="w-30 rounded bg-gray-200 dark:bg-gray-800 px-2 flex items-center gap-2"
>
<UIcon :name="item.icon" /> {{ item.label }}
</div>
</UList>
</template>

<script setup>
const items = [
{ label: 'Home', icon: 'i-heroicons-home' },
{ label: 'Users', icon: 'i-heroicons-users' },
{ label: 'Carts', icon: 'i-heroicons-shopping-bag' },
{ label: 'Shipments', icon: 'i-heroicons-truck' }
]
</script>
87 changes: 87 additions & 0 deletions docs/content/7.layout/5.list.md
Original file line number Diff line number Diff line change
@@ -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 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"}

::callout{icon="i-heroicons-light-bulb"}
Lists are unordered by default, and are created using the `ul` tag. You can change the tag to `ol` for semantically ordered list by setting the `ordered` prop to `true`. This does not make any visual change.
::

### Orientation

By default, list items are stacked vertically. To stack the list items horizontally, set the `orientation` prop to `horizontal`.

:component-example{component="list-example-orientation"}

::callout{icon="i-heroicons-light-bulb"}
You can also change the default orientation for all list through the [global UI config](#/getting-started/theming#appconfigts).
::

### Item Orientation

Both content and separators have the same stacking orientation than the list. For example, if a list is stacked vertically, the container holding the content and the separators will also be stacked vertically.

The `itemOrientation` allows to change the stacking orientation of the item regardless of the list orientation.

:component-example{component="list-example-item-orientation"}

### Gap

To add a default space in between each list item, set the `gap` prop.

:component-example{component="list-example-gap"}

Alternatively, you may set a custom gap 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.

: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
107 changes: 107 additions & 0 deletions src/runtime/components/layout/List.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { h, computed, toRef, defineComponent } 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<typeof list>(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: () => config.orientation,
validator (value: string) {
return ['horizontal', 'vertical'].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 value === undefined ? true : ['horizontal', 'vertical'].includes(value)
}
},
class: {
type: [String, Object, Array] as PropType<any>,
default: undefined
},
ui: {
type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
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.value, class: listClass.value, ref: 'list' }, children.value)
}
})
15 changes: 15 additions & 0 deletions src/runtime/ui.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -996,6 +996,21 @@ export const divider = {
label: 'text-sm'
}

export const list = {
orientation: 'vertical',
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 = {
Expand Down

0 comments on commit c3db82e

Please sign in to comment.