feat(ContextMenu/DropdownMenu): handle loading field in items

This commit is contained in:
Benjamin Canac
2024-10-17 22:03:32 +02:00
parent 81a59969f6
commit b975235c8b
11 changed files with 76 additions and 20 deletions

View File

@@ -14,8 +14,8 @@ const items = computed(() => [{
onUpdateChecked(checked: boolean) { onUpdateChecked(checked: boolean) {
showSidebar.value = checked showSidebar.value = checked
}, },
onSelect(e?: Event) { onSelect(e: Event) {
e?.preventDefault() e.preventDefault()
} }
}, { }, {
label: 'Show Toolbar', label: 'Show Toolbar',

View File

@@ -17,8 +17,8 @@ const items = computed(() => [{
onUpdateChecked(checked: boolean) { onUpdateChecked(checked: boolean) {
showBookmarks.value = checked showBookmarks.value = checked
}, },
onSelect(e?: Event) { onSelect(e: Event) {
e?.preventDefault() e.preventDefault()
} }
}, { }, {
label: 'Show History', label: 'Show History',

View File

@@ -33,7 +33,8 @@ const groups = computed(() => [{
icon: 'i-heroicons-document-plus', icon: 'i-heroicons-document-plus',
loading: loading.value, loading: loading.value,
onSelect(e: Event) { onSelect(e: Event) {
e?.preventDefault() e.preventDefault()
toast.add({ title: 'New file added!' }) toast.add({ title: 'New file added!' })
loading.value = true loading.value = true
@@ -48,7 +49,8 @@ const groups = computed(() => [{
suffix: 'Create a new folder in the current directory or workspace.', suffix: 'Create a new folder in the current directory or workspace.',
icon: 'i-heroicons-folder-plus', icon: 'i-heroicons-folder-plus',
onSelect(e: Event) { onSelect(e: Event) {
e?.preventDefault() e.preventDefault()
toast.add({ title: 'New folder added!' }) toast.add({ title: 'New folder added!' })
}, },
kbds: ['meta', 'F'] kbds: ['meta', 'F']
@@ -57,7 +59,8 @@ const groups = computed(() => [{
suffix: 'Add a hashtag to the current item.', suffix: 'Add a hashtag to the current item.',
icon: 'i-heroicons-hashtag', icon: 'i-heroicons-hashtag',
onSelect(e: Event) { onSelect(e: Event) {
e?.preventDefault() e.preventDefault()
toast.add({ title: 'Hashtag added!' }) toast.add({ title: 'Hashtag added!' })
}, },
kbds: ['meta', 'H'] kbds: ['meta', 'H']
@@ -66,7 +69,8 @@ const groups = computed(() => [{
suffix: 'Add a label to the current item.', suffix: 'Add a label to the current item.',
icon: 'i-heroicons-tag', icon: 'i-heroicons-tag',
onSelect(e: Event) { onSelect(e: Event) {
e?.preventDefault() e.preventDefault()
toast.add({ title: 'Label added!' }) toast.add({ title: 'Label added!' })
}, },
kbds: ['meta', 'L'] kbds: ['meta', 'L']

View File

@@ -1,9 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import theme from '#build/ui/context-menu' import theme from '#build/ui/context-menu'
const items = [ const loading = ref(false)
const items = computed(() => [
[{ [{
label: 'My account', label: 'My account',
type: 'label' as const,
avatar: { avatar: {
src: 'https://github.com/benjamincanac.png' src: 'https://github.com/benjamincanac.png'
} }
@@ -37,7 +40,16 @@ const items = [
label: 'Collapse Pinned Tabs', label: 'Collapse Pinned Tabs',
disabled: true disabled: true
}], [{ }], [{
label: 'Refresh the Page' label: 'Refresh the Page',
loading: loading.value,
onSelect(e: Event) {
e.preventDefault()
loading.value = true
setTimeout(() => {
loading.value = false
}, 2000)
}
}, { }, {
label: 'Clear Cookies and Refresh' label: 'Clear Cookies and Refresh'
}, { }, {
@@ -72,13 +84,13 @@ const items = [
} }
}]] }]]
}] }]
] ])
const sizes = Object.keys(theme.variants.size) const sizes = Object.keys(theme.variants.size)
const size = ref('md' as const) const size = ref('md' as const)
defineShortcuts(extractShortcuts(items)) defineShortcuts(extractShortcuts(items.value))
</script> </script>
<template> <template>

View File

@@ -1,7 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import theme from '#build/ui/dropdown-menu' import theme from '#build/ui/dropdown-menu'
const items = [ const loading = ref(false)
const items = computed(() => [
[{ [{
label: 'My account', label: 'My account',
avatar: { avatar: {
@@ -45,7 +47,7 @@ const items = [
icon: 'i-heroicons-link', icon: 'i-heroicons-link',
kbds: ['meta', 'i'], kbds: ['meta', 'i'],
onSelect(e: Event) { onSelect(e: Event) {
e?.preventDefault() e.preventDefault()
console.log('Invite by link clicked') console.log('Invite by link clicked')
} }
}], [{ }], [{
@@ -80,8 +82,14 @@ const items = [
label: 'New team', label: 'New team',
icon: 'i-heroicons-plus', icon: 'i-heroicons-plus',
kbds: ['meta', 'n'], kbds: ['meta', 'n'],
onSelect() { loading: loading.value,
console.log('New team clicked') onSelect(e: Event) {
e.preventDefault()
loading.value = true
setTimeout(() => {
loading.value = false
}, 2000)
} }
}], [{ }], [{
label: 'GitHub', label: 'GitHub',
@@ -112,13 +120,13 @@ const items = [
console.log('Logout clicked') console.log('Logout clicked')
} }
}] }]
] ])
const sizes = Object.keys(theme.variants.size) const sizes = Object.keys(theme.variants.size)
const size = ref('md' as const) const size = ref('md' as const)
defineShortcuts(extractShortcuts(items)) defineShortcuts(extractShortcuts(items.value))
</script> </script>
<template> <template>

View File

@@ -24,6 +24,7 @@ export interface ContextMenuItem extends Omit<LinkProps, 'type' | 'raw' | 'custo
*/ */
type?: 'label' | 'separator' | 'link' | 'checkbox' type?: 'label' | 'separator' | 'link' | 'checkbox'
slot?: string slot?: string
loading?: boolean
disabled?: boolean disabled?: boolean
checked?: boolean checked?: boolean
open?: boolean open?: boolean
@@ -43,6 +44,11 @@ export interface ContextMenuProps<T> extends Omit<ContextMenuRootProps, 'dir'> {
* @defaultValue appConfig.ui.icons.check * @defaultValue appConfig.ui.icons.check
*/ */
checkedIcon?: string checkedIcon?: string
/**
* The icon displayed when an item is loading.
* @defaultValue appConfig.ui.icons.loading
*/
loadingIcon?: string
/** The content of the menu. */ /** The content of the menu. */
content?: Omit<ContextMenuContentProps, 'as' | 'asChild' | 'forceMount'> content?: Omit<ContextMenuContentProps, 'as' | 'asChild' | 'forceMount'>
/** /**
@@ -113,6 +119,7 @@ const ui = computed(() => contextMenu({
:portal="portal" :portal="portal"
:label-key="labelKey" :label-key="labelKey"
:checked-icon="checkedIcon" :checked-icon="checkedIcon"
:loading-icon="loadingIcon"
> >
<template v-for="(_, name) in proxySlots" #[name]="slotData: any"> <template v-for="(_, name) in proxySlots" #[name]="slotData: any">
<slot :name="name" v-bind="slotData" /> <slot :name="name" v-bind="slotData" />

View File

@@ -12,6 +12,7 @@ interface ContextMenuContentProps<T> extends Omit<RadixContextMenuContentProps,
sub?: boolean sub?: boolean
labelKey: string labelKey: string
checkedIcon?: string checkedIcon?: string
loadingIcon?: string
class?: any class?: any
ui: typeof _contextMenu ui: typeof _contextMenu
uiOverride?: any uiOverride?: any
@@ -51,7 +52,8 @@ const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0
<DefineItemTemplate v-slot="{ item, active, index }"> <DefineItemTemplate v-slot="{ item, active, index }">
<slot :name="item.slot || 'item'" :item="(item as T)" :index="index"> <slot :name="item.slot || 'item'" :item="(item as T)" :index="index">
<slot :name="item.slot ? `${item.slot}-leading`: 'item-leading'" :item="(item as T)" :active="active" :index="index"> <slot :name="item.slot ? `${item.slot}-leading`: 'item-leading'" :item="(item as T)" :active="active" :index="index">
<UIcon v-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: uiOverride?.itemLeadingIcon, active })" /> <UIcon v-if="item.loading" :name="loadingIcon || appConfig.ui.icons.loading" :class="ui.itemLeadingIcon({ class: uiOverride?.itemLeadingIcon, loading: true })" />
<UIcon v-else-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: uiOverride?.itemLeadingIcon, active })" />
<UAvatar v-else-if="item.avatar" :size="((props.uiOverride?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: uiOverride?.itemLeadingAvatar, active })" /> <UAvatar v-else-if="item.avatar" :size="((props.uiOverride?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: uiOverride?.itemLeadingAvatar, active })" />
</slot> </slot>
@@ -106,6 +108,8 @@ const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0
:items="item.children" :items="item.children"
:align-offset="-4" :align-offset="-4"
:label-key="labelKey" :label-key="labelKey"
:checked-icon="checkedIcon"
:loading-icon="loadingIcon"
v-bind="item.content" v-bind="item.content"
> >
<template v-for="(_, name) in proxySlots" #[name]="slotData: any"> <template v-for="(_, name) in proxySlots" #[name]="slotData: any">

View File

@@ -24,6 +24,7 @@ export interface DropdownMenuItem extends Omit<LinkProps, 'type' | 'raw' | 'cust
*/ */
type?: 'label' | 'separator' | 'link' | 'checkbox' type?: 'label' | 'separator' | 'link' | 'checkbox'
slot?: string slot?: string
loading?: boolean
disabled?: boolean disabled?: boolean
checked?: boolean checked?: boolean
open?: boolean open?: boolean
@@ -43,6 +44,11 @@ export interface DropdownMenuProps<T> extends Omit<DropdownMenuRootProps, 'dir'>
* @defaultValue appConfig.ui.icons.check * @defaultValue appConfig.ui.icons.check
*/ */
checkedIcon?: string checkedIcon?: string
/**
* The icon displayed when an item is loading.
* @defaultValue appConfig.ui.icons.loading
*/
loadingIcon?: string
/** /**
* The content of the menu. * The content of the menu.
* @defaultValue { side: 'bottom', sideOffset: 8 } * @defaultValue { side: 'bottom', sideOffset: 8 }
@@ -123,6 +129,7 @@ const ui = computed(() => dropdownMenu({
:portal="portal" :portal="portal"
:label-key="labelKey" :label-key="labelKey"
:checked-icon="checkedIcon" :checked-icon="checkedIcon"
:loading-icon="loadingIcon"
> >
<template v-for="(_, name) in proxySlots" #[name]="slotData: any"> <template v-for="(_, name) in proxySlots" #[name]="slotData: any">
<slot :name="name" v-bind="slotData" /> <slot :name="name" v-bind="slotData" />

View File

@@ -13,6 +13,7 @@ interface DropdownMenuContentProps<T> extends Omit<RadixDropdownMenuContentProps
sub?: boolean sub?: boolean
labelKey: string labelKey: string
checkedIcon?: string checkedIcon?: string
loadingIcon?: string
class?: any class?: any
ui: typeof _dropdownMenu ui: typeof _dropdownMenu
uiOverride?: any uiOverride?: any
@@ -57,7 +58,8 @@ const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0
<DefineItemTemplate v-slot="{ item, active, index }"> <DefineItemTemplate v-slot="{ item, active, index }">
<slot :name="item.slot || 'item'" :item="(item as T)" :index="index"> <slot :name="item.slot || 'item'" :item="(item as T)" :index="index">
<slot :name="item.slot ? `${item.slot}-leading`: 'item-leading'" :item="(item as T)" :active="active" :index="index"> <slot :name="item.slot ? `${item.slot}-leading`: 'item-leading'" :item="(item as T)" :active="active" :index="index">
<UIcon v-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: uiOverride?.itemLeadingIcon, active })" /> <UIcon v-if="item.loading" :name="loadingIcon || appConfig.ui.icons.loading" :class="ui.itemLeadingIcon({ class: uiOverride?.itemLeadingIcon, loading: true })" />
<UIcon v-else-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: uiOverride?.itemLeadingIcon, active })" />
<UAvatar v-else-if="item.avatar" :size="((props.uiOverride?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: uiOverride?.itemLeadingAvatar, active })" /> <UAvatar v-else-if="item.avatar" :size="((props.uiOverride?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: uiOverride?.itemLeadingAvatar, active })" />
</slot> </slot>
@@ -115,6 +117,8 @@ const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0
:align-offset="-4" :align-offset="-4"
:side-offset="3" :side-offset="3"
:label-key="labelKey" :label-key="labelKey"
:checked-icon="checkedIcon"
:loading-icon="loadingIcon"
v-bind="item.content" v-bind="item.content"
> >
<template v-for="(_, name) in proxySlots" #[name]="slotData: any"> <template v-for="(_, name) in proxySlots" #[name]="slotData: any">

View File

@@ -28,6 +28,11 @@ export default (options: Required<ModuleOptions>) => ({
itemLeadingIcon: ['text-[var(--ui-text-dimmed)] group-data-highlighted:text-[var(--ui-text)] group-data-[state=open]:text-[var(--ui-text)]', options.theme.transitions && 'transition-colors'] itemLeadingIcon: ['text-[var(--ui-text-dimmed)] group-data-highlighted:text-[var(--ui-text)] group-data-[state=open]:text-[var(--ui-text)]', options.theme.transitions && 'transition-colors']
} }
}, },
loading: {
true: {
itemLeadingIcon: 'animate-spin'
}
},
size: { size: {
xs: { xs: {
label: 'p-1 text-xs gap-1', label: 'p-1 text-xs gap-1',

View File

@@ -29,6 +29,11 @@ export default (options: Required<ModuleOptions>) => ({
itemLeadingIcon: ['text-[var(--ui-text-dimmed)] group-data-highlighted:text-[var(--ui-text)] group-data-[state=open]:text-[var(--ui-text)]', options.theme.transitions && 'transition-colors'] itemLeadingIcon: ['text-[var(--ui-text-dimmed)] group-data-highlighted:text-[var(--ui-text)] group-data-[state=open]:text-[var(--ui-text)]', options.theme.transitions && 'transition-colors']
} }
}, },
loading: {
true: {
itemLeadingIcon: 'animate-spin'
}
},
size: { size: {
xs: { xs: {
label: 'p-1 text-xs gap-1', label: 'p-1 text-xs gap-1',