feat(DropdownMenu): add #item slot for consistency

This commit is contained in:
Benjamin Canac
2024-04-17 12:18:37 +02:00
parent ea19a3061f
commit 1dcc1f5074
6 changed files with 184 additions and 36 deletions

View File

@@ -19,9 +19,9 @@ export interface DropdownMenuItem extends Omit<LinkProps, 'type'> {
shortcuts?: string[] | KbdProps[]
/**
* The item type.
* @defaultValue "item"
* @defaultValue "link"
*/
type?: 'label' | 'item'
type?: 'label' | 'link'
slot?: string
open?: boolean
defaultOpen?: boolean
@@ -48,6 +48,8 @@ export interface DropdownMenuSlots<T> {
leading: SlotProps<T>
label: SlotProps<T>
trailing: SlotProps<T>
item: SlotProps<T>
[key: string]: SlotProps<T>
}
</script>

View File

@@ -1,13 +1,17 @@
<script lang="ts">
import { tv } from 'tailwind-variants'
import type { DropdownMenuContentProps as RadixDropdownMenuContentProps, DropdownMenuContentEmits as RadixDropdownMenuContentEmits } from 'radix-vue'
import theme from '#build/ui/dropdown-menu'
import type { DropdownMenuItem, DropdownMenuSlots } from '#ui/types'
const dropdownMenu = tv(theme)()
interface DropdownMenuContentProps<T> extends Omit<RadixDropdownMenuContentProps, 'asChild' | 'forceMount'> {
items?: T[] | T[][]
portal?: boolean
sub?: boolean
class?: any
ui: any
ui: typeof dropdownMenu
}
interface DropdownMenuContentEmits extends RadixDropdownMenuContentEmits {}
@@ -40,20 +44,20 @@ const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0
<template>
<DefineItemTemplate v-slot="{ item, active, index }">
<slot name="leading" :item="item" :active="active" :index="index">
<UAvatar v-if="item.avatar" size="2xs" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ active })" />
<UIcon v-else-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ active })" />
<UAvatar v-if="item.avatar" size="2xs" v-bind="item.avatar" :class="ui.linkLeadingAvatar({ active })" />
<UIcon v-else-if="item.icon" :name="item.icon" :class="ui.linkLeadingIcon({ active })" />
</slot>
<span v-if="item.label || $slots.default" :class="ui.itemLabel()">
<span v-if="item.label || $slots.label" :class="ui.linkLabel()">
<slot name="label" :item="item" :active="active" :index="index">
{{ item.label }}
</slot>
</span>
<span v-if="$slots.trailing || item.children?.length || item.shortcuts?.length" :class="ui.itemTrailing()">
<span v-if="$slots.trailing || item.children?.length || item.shortcuts?.length" :class="ui.linkTrailing()">
<slot name="trailing" :item="item" :active="active" :index="index">
<UIcon v-if="item.children?.length" :name="appConfig.ui.icons.chevronRight" :class="ui.itemTrailingIcon()" />
<span v-else-if="item.shortcuts?.length" :class="ui.itemTrailingShortcuts()">
<UIcon v-if="item.children?.length" :name="appConfig.ui.icons.chevronRight" :class="ui.linkTrailingIcon()" />
<span v-else-if="item.shortcuts?.length" :class="ui.linkTrailingShortcuts()">
<UKbd v-for="(shortcut, shortcutIndex) in item.shortcuts" :key="shortcutIndex" size="md" v-bind="typeof shortcut === 'string' ? { value: shortcut } : shortcut" />
</span>
</slot>
@@ -65,7 +69,9 @@ const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0
<DropdownMenu.Group v-for="(group, groupIndex) in groups" :key="`group-${groupIndex}`" :class="ui.group()">
<template v-for="(item, index) in group" :key="`group-${groupIndex}-${index}`">
<DropdownMenu.Label v-if="item.type === 'label'" :class="ui.label()">
<ReuseItemTemplate :item="item" :index="index" />
<slot :name="item.slot || 'item'" :item="item" :index="index">
<ReuseItemTemplate :item="item" :index="index" />
</slot>
</DropdownMenu.Label>
<DropdownMenu.Sub v-else-if="item?.children?.length">
<DropdownMenu.SubTrigger
@@ -75,9 +81,11 @@ const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0
:open="item.open"
:default-open="item.defaultOpen"
:text-value="item.label"
:class="ui.item()"
:class="ui.link()"
>
<ReuseItemTemplate :item="item" :index="index" />
<slot :name="item.slot || 'item'" :item="item" :index="index">
<ReuseItemTemplate :item="item" :index="index" />
</slot>
</DropdownMenu.SubTrigger>
<UDropdownMenuContent
@@ -98,11 +106,13 @@ const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0
</UDropdownMenuContent>
</DropdownMenu.Sub>
<DropdownMenu.Item v-else as-child :disabled="item.disabled" :text-value="item.label" @select="item.select">
<ULink v-slot="{ active, ...slotProps }" v-bind="omit((item as DropdownMenuItem), ['label', 'icon', 'avatar', 'shortcuts', 'slot', 'open', 'defaultOpen', 'select', 'children', 'type'])" custom>
<ULinkBase v-bind="slotProps" :class="ui.item({ active })">
<ReuseItemTemplate :item="item" :active="active" :index="index" />
</ULinkBase>
</ULink>
<slot :name="item.slot || 'item'" :item="item" :index="index">
<ULink v-slot="{ active, ...slotProps }" v-bind="omit((item as DropdownMenuItem), ['label', 'icon', 'avatar', 'shortcuts', 'slot', 'open', 'defaultOpen', 'select', 'children', 'type'])" custom>
<ULinkBase v-bind="slotProps" :class="ui.link({ active })">
<ReuseItemTemplate :item="item" :active="active" :index="index" />
</ULinkBase>
</ULink>
</slot>
</DropdownMenu.Item>
</template>
</DropdownMenu.Group>

View File

@@ -1,26 +1,26 @@
export default {
slots: {
content: 'min-w-48 bg-white dark:bg-gray-900 shadow-lg rounded-md ring ring-gray-200 dark:ring-gray-800 divide-y divide-gray-200 dark:divide-gray-800 overflow-y-auto scroll-py-1 data-[state=open]:animate-[scale-in_100ms_ease-out] data-[state=closed]:animate-[scale-out_100ms_ease-in]',
content: 'min-w-32 bg-white dark:bg-gray-900 shadow-lg rounded-md ring ring-gray-200 dark:ring-gray-800 divide-y divide-gray-200 dark:divide-gray-800 overflow-y-auto scroll-py-1 data-[state=open]:animate-[scale-in_100ms_ease-out] data-[state=closed]:animate-[scale-out_100ms_ease-in]',
arrow: 'fill-gray-200 dark:fill-gray-800',
group: 'p-1 isolate',
label: 'w-full flex items-center gap-1.5 p-1.5 text-sm font-medium select-none',
item: 'group relative w-full flex items-center gap-1.5 p-1.5 text-sm select-none outline-none before:absolute before:z-[-1] before:inset-px before:rounded-md data-disabled:cursor-not-allowed data-disabled:opacity-75',
itemLeadingIcon: 'shrink-0 size-5',
itemLeadingAvatar: 'shrink-0',
itemTrailing: 'ms-auto inline-flex',
itemTrailingIcon: 'shrink-0 size-5',
itemTrailingShortcuts: 'hidden lg:inline-flex items-center shrink-0 gap-0.5',
itemLabel: 'truncate'
link: 'group relative w-full flex items-center gap-1.5 p-1.5 text-sm select-none outline-none before:absolute before:z-[-1] before:inset-px before:rounded-md data-disabled:cursor-not-allowed data-disabled:opacity-75',
linkLeadingIcon: 'shrink-0 size-5',
linkLeadingAvatar: 'shrink-0',
linkTrailing: 'ms-auto inline-flex',
linkTrailingIcon: 'shrink-0 size-5',
linkTrailingShortcuts: 'hidden lg:inline-flex items-center shrink-0 gap-0.5',
linkLabel: 'truncate'
},
variants: {
active: {
true: {
item: 'text-gray-900 dark:text-white before:bg-gray-100 dark:before:bg-gray-800',
itemLeadingIcon: 'text-gray-700 dark:text-gray-200'
link: 'text-gray-900 dark:text-white before:bg-gray-100 dark:before:bg-gray-800',
linkLeadingIcon: 'text-gray-700 dark:text-gray-200'
},
false: {
item: 'text-gray-700 dark:text-gray-200 data-highlighted:text-gray-900 dark:data-highlighted:text-white data-[state=open]:text-gray-900 dark:data-[state=open]:text-white data-highlighted:before:bg-gray-50 dark:data-highlighted:before:bg-gray-800/50 data-[state=open]:before:bg-gray-50 dark:data-[state=open]:before:bg-gray-800/50',
itemLeadingIcon: 'text-gray-400 dark:text-gray-500 group-data-highlighted:text-gray-700 dark:group-data-highlighted:text-gray-200 group-data-[state=open]:text-gray-700 dark:group-data-[state=open]:text-gray-200'
link: 'text-gray-700 dark:text-gray-200 data-highlighted:text-gray-900 dark:data-highlighted:text-white data-[state=open]:text-gray-900 dark:data-[state=open]:text-white data-highlighted:before:bg-gray-50 dark:data-highlighted:before:bg-gray-800/50 data-[state=open]:before:bg-gray-50 dark:data-[state=open]:before:bg-gray-800/50',
linkLeadingIcon: 'text-gray-400 dark:text-gray-500 group-data-highlighted:text-gray-700 dark:group-data-highlighted:text-gray-200 group-data-[state=open]:text-gray-700 dark:group-data-[state=open]:text-gray-200'
}
}
}