feat(NavigationMenu): add item-content slot

This commit is contained in:
Benjamin Canac
2024-10-31 11:27:52 +01:00
parent d980115408
commit b5ca0d96f1
5 changed files with 156 additions and 50 deletions

View File

@@ -78,6 +78,7 @@ export type NavigationMenuSlots<T extends { slot?: string }> = {
'item-leading': SlotProps<T>
'item-label': SlotProps<T>
'item-trailing': SlotProps<T>
'item-content': SlotProps<T>
} & DynamicSlots<T, SlotProps<T>>
</script>
@@ -153,7 +154,8 @@ const lists = computed(() => props.items?.length ? (Array.isArray(props.items[0]
v-bind="(typeof item.badge === 'string' || typeof item.badge === 'number') ? { label: item.badge } : item.badge"
:class="ui.linkTrailingBadge({ class: props.ui?.linkTrailingBadge })"
/>
<UIcon v-if="item.children?.length" :name="item.trailingIcon || trailingIcon || appConfig.ui.icons.chevronDown" :class="ui.linkTrailingIcon({ class: props.ui?.linkTrailingIcon, active })" />
<UIcon v-if="(orientation === 'horizontal' && (item.children?.length || !!slots[item.slot ? `${item.slot}-content` : 'item-content'])) || (orientation === 'vertical' && item.children?.length)" :name="item.trailingIcon || trailingIcon || appConfig.ui.icons.chevronDown" :class="ui.linkTrailingIcon({ class: props.ui?.linkTrailingIcon, active })" />
</slot>
</span>
</slot>
@@ -172,10 +174,10 @@ const lists = computed(() => props.items?.length ? (Array.isArray(props.items[0]
>
<ULink v-slot="{ active, ...slotProps }" v-bind="pickLinkProps(item)" custom>
<component
:is="(item.children?.length && orientation === 'horizontal') ? NavigationMenuTrigger : NavigationMenuLink"
v-bind="item.children?.length ? { disabled: item.disabled } : { active }"
:is="(orientation === 'horizontal' && (item.children?.length || !!slots[item.slot ? `${item.slot}-content` : 'item-content'])) ? NavigationMenuTrigger : NavigationMenuLink"
as-child
:active="active"
:disabled="item.disabled"
@select="item.onSelect"
>
<ULinkBase v-bind="slotProps" :class="ui.link({ class: [props.ui?.link, item.class], active, disabled: !!item.disabled })">
@@ -183,33 +185,35 @@ const lists = computed(() => props.items?.length ? (Array.isArray(props.items[0]
</ULinkBase>
</component>
<NavigationMenuContent v-if="item.children?.length && orientation === 'horizontal'" v-bind="contentProps" :class="ui.content({ class: props.ui?.content })">
<ul :class="ui.childList({ class: props.ui?.childList })">
<li v-for="(childItem, childIndex) in item.children" :key="childIndex" :class="ui.childItem({ class: props.ui?.childItem })">
<ULink v-slot="{ active: childActive, ...childSlotProps }" v-bind="pickLinkProps(childItem)" custom>
<NavigationMenuLink as-child :active="childActive" @select="childItem.onSelect">
<ULinkBase v-bind="childSlotProps" :class="ui.childLink({ class: [props.ui?.childLink, childItem.class], active: childActive })">
<UIcon v-if="childItem.icon" :name="childItem.icon" :class="ui.childLinkIcon({ class: props.ui?.childLinkIcon, active: childActive })" />
<NavigationMenuContent v-if="orientation === 'horizontal' && (item.children?.length || !!slots[item.slot ? `${item.slot}-content` : 'item-content'])" v-bind="contentProps" :class="ui.content({ class: props.ui?.content })">
<slot :name="item.slot ? `${item.slot}-content` : 'item-content'" :item="(item as T)" :active="active" :index="index">
<ul :class="ui.childList({ class: props.ui?.childList })">
<li v-for="(childItem, childIndex) in item.children" :key="childIndex" :class="ui.childItem({ class: props.ui?.childItem })">
<ULink v-slot="{ active: childActive, ...childSlotProps }" v-bind="pickLinkProps(childItem)" custom>
<NavigationMenuLink as-child :active="childActive" @select="childItem.onSelect">
<ULinkBase v-bind="childSlotProps" :class="ui.childLink({ class: [props.ui?.childLink, childItem.class], active: childActive })">
<UIcon v-if="childItem.icon" :name="childItem.icon" :class="ui.childLinkIcon({ class: props.ui?.childLinkIcon, active: childActive })" />
<div :class="ui.childLinkWrapper({ class: props.ui?.childLinkWrapper })">
<p :class="ui.childLinkLabel({ class: props.ui?.childLinkLabel, active: childActive })">
{{ get(childItem, props.labelKey as string) }}
<div :class="ui.childLinkWrapper({ class: props.ui?.childLinkWrapper })">
<p :class="ui.childLinkLabel({ class: props.ui?.childLinkLabel, active: childActive })">
{{ get(childItem, props.labelKey as string) }}
<UIcon v-if="childItem.target === '_blank'" :name="appConfig.ui.icons.external" :class="ui.childLinkLabelExternalIcon({ class: props.ui?.childLinkLabelExternalIcon, active: childActive })" />
</p>
<p v-if="childItem.description" :class="ui.childLinkDescription({ class: props.ui?.childLinkDescription, active: childActive })">
{{ childItem.description }}
</p>
</div>
</ULinkBase>
</NavigationMenuLink>
</ULink>
</li>
</ul>
<UIcon v-if="childItem.target === '_blank'" :name="appConfig.ui.icons.external" :class="ui.childLinkLabelExternalIcon({ class: props.ui?.childLinkLabelExternalIcon, active: childActive })" />
</p>
<p v-if="childItem.description" :class="ui.childLinkDescription({ class: props.ui?.childLinkDescription, active: childActive })">
{{ childItem.description }}
</p>
</div>
</ULinkBase>
</NavigationMenuLink>
</ULink>
</li>
</ul>
</slot>
</NavigationMenuContent>
</ULink>
<template v-if="item.children?.length && orientation === 'vertical'" #content>
<template v-if="orientation === 'vertical' && item.children?.length" #content>
<ul :class="ui.childList({ class: props.ui?.childList })">
<li v-for="(childItem, childIndex) in item.children" :key="childIndex" :class="ui.childItem({ class: props.ui?.childItem })">
<ULink v-slot="{ active: childActive, ...childSlotProps }" v-bind="pickLinkProps(childItem)" custom>

View File

@@ -24,9 +24,8 @@ export default (options: Required<ModuleOptions>) => ({
childLinkLabelExternalIcon: 'inline-block size-3 align-top text-[var(--ui-text-dimmed)]',
childLinkDescription: 'text-sm text-[var(--ui-text-muted)]',
separator: 'px-2 h-px bg-[var(--ui-border)]',
viewportWrapper: 'absolute top-full inset-x-0 flex w-full',
// FIXME: add `sm:w-[var(--radix-navigation-menu-viewport-width)]` / `transition-[width,height]` / `origin-[top_center]` once position is based on trigger
viewport: 'relative overflow-hidden bg-[var(--ui-bg)] shadow-lg rounded-[calc(var(--ui-radius)*1.5)] ring ring-[var(--ui-border)] h-[var(--radix-navigation-menu-viewport-height)] w-full data-[state=open]:animate-[scale-in_100ms_ease-out] data-[state=closed]:animate-[scale-out_100ms_ease-in]',
viewportWrapper: 'absolute top-full left-0 flex w-full justify-center',
viewport: 'relative overflow-hidden bg-[var(--ui-bg)] shadow-lg rounded-[calc(var(--ui-radius)*1.5)] ring ring-[var(--ui-border)] h-[var(--radix-navigation-menu-viewport-height)] w-full transition-[width,height] origin-[top_center] data-[state=open]:animate-[scale-in_100ms_ease-out] data-[state=closed]:animate-[scale-out_100ms_ease-in]',
content: 'absolute top-0 left-0 w-full data-[motion=from-start]:animate-[enter-from-left_200ms_ease] data-[motion=from-end]:animate-[enter-from-right_200ms_ease] data-[motion=to-start]:animate-[exit-to-left_200ms_ease] data-[motion=to-end]:animate-[exit-to-right_200ms_ease]',
indicator: 'data-[state=visible]:animate-[fade-in_100ms_ease-out] data-[state=hidden]:animate-[fade-out_100ms_ease-in] bottom-0 z-[1] flex h-2.5 items-end justify-center overflow-hidden transition-transform duration-200 ease-out',
arrow: 'relative top-[50%] size-2.5 rotate-45 border border-[var(--ui-border)] bg-[var(--ui-bg)] z-[1] rounded-[calc(var(--ui-radius)/2)]'