feat(NavigationMenu): add collapsible field in items

Resolves #3353, resolves #3911

Reverts 07e1b4f1f4
This commit is contained in:
Benjamin Canac
2025-05-10 17:51:46 +02:00
parent 09b4699aea
commit 2be60cddfe
4 changed files with 33 additions and 10 deletions

View File

@@ -21,8 +21,12 @@ Use the `items` prop as an array of objects with the following properties:
- `icon?: string`{lang="ts-type"}
- `avatar?: AvatarProps`{lang="ts-type"}
- `badge?: string | number | BadgeProps`{lang="ts-type"}
- `tooltip?: TooltipProps`{lang="ts-type"}
- `trailingIcon?: string`{lang="ts-type"}
- `type?: 'label' | 'link'`{lang="ts-type"}
- `collapsible?: boolean`{lang="ts-type"}
- `defaultOpen?: boolean`{lang="ts-type"}
- `open?: boolean`{lang="ts-type"}
- `value?: string`{lang="ts-type"}
- `disabled?: boolean`{lang="ts-type"}
- `class?: any`{lang="ts-type"}
@@ -140,7 +144,7 @@ Each item can take a `children` array of objects with the following properties t
Use the `orientation` prop to change the orientation of the NavigationMenu.
::note
When orientation is `vertical`, a [Collapsible](/components/collapsible) component is used to display children. You can control the open state of each item using the `open` and `defaultOpen` properties.
When orientation is `vertical`, a [Collapsible](/components/collapsible) component is used to display children. You can control the open state of each item using the `open` and `defaultOpen` properties. You can also use the `collapsible` property to control if the item is collapsible.
::
::component-code

View File

@@ -44,6 +44,12 @@ export interface NavigationMenuItem extends Omit<LinkProps, 'type' | 'raw' | 'cu
type?: 'label' | 'link'
slot?: string
value?: string
/**
* Make the item collapsible.
* Only works when `orientation` is `vertical`.
* @defaultValue true
*/
collapsible?: boolean
children?: NavigationMenuChildItem[]
onSelect?(e: Event): void
[key: string]: any
@@ -236,7 +242,7 @@ const lists = computed<NavigationMenuItem[][]>(() =>
:class="ui.linkTrailingBadge({ class: props.ui?.linkTrailingBadge })"
/>
<UIcon v-if="(orientation === 'horizontal' && (item.children?.length || !!slots[(item.slot ? `${item.slot}-content` : 'item-content') as keyof NavigationMenuSlots<T>])) || (orientation === 'vertical' && 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') as keyof NavigationMenuSlots<T>])) || (orientation === 'vertical' && item.children?.length && item.collapsible !== false)" :name="item.trailingIcon || trailingIcon || appConfig.ui.icons.chevronDown" :class="ui.linkTrailingIcon({ class: props.ui?.linkTrailingIcon, active })" />
<UIcon v-else-if="item.trailingIcon" :name="item.trailingIcon" :class="ui.linkTrailingIcon({ class: props.ui?.linkTrailingIcon, active })" />
</slot>
</span>
@@ -245,17 +251,18 @@ const lists = computed<NavigationMenuItem[][]>(() =>
<DefineItemTemplate v-slot="{ item, index, level = 0 }">
<component
:is="(orientation === 'vertical' && item.children?.length && !collapsed) ? UCollapsible : NavigationMenuItem"
:is="(orientation === 'vertical' && item.children?.length) ? UCollapsible : NavigationMenuItem"
as="li"
:value="item.value || String(index)"
:default-open="item.defaultOpen"
:unmount-on-hide="(orientation === 'vertical' && item.children?.length && !collapsed) ? unmountOnHide : undefined"
:disabled="(orientation === 'vertical' && item.children?.length) ? item.collapsible === false : undefined"
:unmount-on-hide="(orientation === 'vertical' && item.children?.length) ? unmountOnHide : undefined"
:open="item.open"
>
<div v-if="orientation === 'vertical' && item.type === 'label'" :class="ui.label({ class: props.ui?.label })">
<ReuseLinkTemplate :item="item" :index="index" />
</div>
<ULink v-else-if="item.type !== 'label'" v-slot="{ active, ...slotProps }" v-bind="(orientation === 'vertical' && item.children?.length && !collapsed) ? {} : pickLinkProps(item as Omit<NavigationMenuItem, 'type'>)" custom>
<ULink v-else-if="item.type !== 'label'" v-slot="{ active, ...slotProps }" v-bind="(orientation === 'vertical' && item.children?.length && item.collapsible !== false) ? {} : pickLinkProps(item as Omit<NavigationMenuItem, 'type'>)" custom>
<component
:is="(orientation === 'horizontal' && (item.children?.length || !!slots[(item.slot ? `${item.slot}-content` : 'item-content') as keyof NavigationMenuSlots<T>])) ? NavigationMenuTrigger : NavigationMenuLink"
as-child
@@ -301,7 +308,7 @@ const lists = computed<NavigationMenuItem[][]>(() =>
</NavigationMenuContent>
</ULink>
<template v-if="orientation === 'vertical' && item.children?.length && !collapsed" #content>
<template v-if="orientation === 'vertical' && item.children?.length " #content>
<ul :class="ui.childList({ class: props.ui?.childList })">
<ReuseItemTemplate
v-for="(childItem, childIndex) in item.children"

View File

@@ -842,17 +842,23 @@ exports[`NavigationMenu > renders with orientation vertical and collapsed correc
<!--v-if-->
</div>
</li>
<li data-menu-item="" class="min-w-0"><button type="button" class="group relative w-full flex items-center gap-1.5 font-medium text-sm before:absolute before:z-[-1] before:rounded-md focus:outline-none focus-visible:outline-none dark:focus-visible:outline-none focus-visible:before:ring-inset focus-visible:before:ring-2 focus-visible:before:ring-primary flex-row py-1.5 before:inset-y-px before:inset-x-0 text-muted hover:text-highlighted hover:before:bg-elevated/50 transition-colors before:transition-colors px-1.5" data-reka-collection-item=""><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="shrink-0 size-5 text-dimmed group-hover:text-default transition-colors" width="1em" height="1em" viewBox="0 0 16 16"></svg>
<li data-state="closed" class="min-w-0" value="1"><button type="button" aria-controls="" aria-expanded="false" data-state="closed" class="group relative w-full flex items-center gap-1.5 font-medium text-sm before:absolute before:z-[-1] before:rounded-md focus:outline-none focus-visible:outline-none dark:focus-visible:outline-none focus-visible:before:ring-inset focus-visible:before:ring-2 focus-visible:before:ring-primary flex-row py-1.5 before:inset-y-px before:inset-x-0 text-muted hover:text-highlighted hover:before:bg-elevated/50 transition-colors before:transition-colors px-1.5" data-reka-collection-item=""><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="shrink-0 size-5 text-dimmed group-hover:text-default transition-colors" width="1em" height="1em" viewBox="0 0 16 16"></svg>
<!--v-if-->
<!--v-if-->
</button>
<!--v-if-->
<div class="data-[state=open]:animate-[collapsible-down_200ms_ease-out] data-[state=closed]:animate-[collapsible-up_200ms_ease-out] overflow-hidden" id="reka-collapsible-content-v-1" hidden="" data-state="closed" style="--reka-collapsible-content-height: 0px; --reka-collapsible-content-width: 0px;">
<!---->
</div>
</li>
<li data-menu-item="" class="min-w-0"><button type="button" class="group relative w-full flex items-center gap-1.5 font-medium text-sm before:absolute before:z-[-1] before:rounded-md focus:outline-none focus-visible:outline-none dark:focus-visible:outline-none focus-visible:before:ring-inset focus-visible:before:ring-2 focus-visible:before:ring-primary flex-row py-1.5 before:inset-y-px before:inset-x-0 text-primary before:bg-elevated px-1.5" data-active="" aria-current="page" data-reka-collection-item=""><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="shrink-0 size-5 text-primary group-data-[state=open]:text-primary" width="1em" height="1em" viewBox="0 0 16 16"></svg>
<li data-state="closed" class="min-w-0" value="2"><button type="button" aria-controls="" aria-expanded="false" data-state="closed" class="group relative w-full flex items-center gap-1.5 font-medium text-sm before:absolute before:z-[-1] before:rounded-md focus:outline-none focus-visible:outline-none dark:focus-visible:outline-none focus-visible:before:ring-inset focus-visible:before:ring-2 focus-visible:before:ring-primary flex-row py-1.5 before:inset-y-px before:inset-x-0 text-primary before:bg-elevated px-1.5" data-active="" aria-current="page" data-reka-collection-item=""><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="shrink-0 size-5 text-primary group-data-[state=open]:text-primary" width="1em" height="1em" viewBox="0 0 16 16"></svg>
<!--v-if-->
<!--v-if-->
</button>
<!--v-if-->
<div class="data-[state=open]:animate-[collapsible-down_200ms_ease-out] data-[state=closed]:animate-[collapsible-up_200ms_ease-out] overflow-hidden" id="reka-collapsible-content-v-2" hidden="" data-state="closed" style="--reka-collapsible-content-height: 0px; --reka-collapsible-content-width: 0px;">
<!---->
</div>
</li>
</ul>
</div>

View File

@@ -842,17 +842,23 @@ exports[`NavigationMenu > renders with orientation vertical and collapsed correc
<!--v-if-->
</div>
</li>
<li data-menu-item="" class="min-w-0"><button type="button" class="group relative w-full flex items-center gap-1.5 font-medium text-sm before:absolute before:z-[-1] before:rounded-md focus:outline-none focus-visible:outline-none dark:focus-visible:outline-none focus-visible:before:ring-inset focus-visible:before:ring-2 focus-visible:before:ring-primary flex-row py-1.5 before:inset-y-px before:inset-x-0 text-muted hover:text-highlighted hover:before:bg-elevated/50 transition-colors before:transition-colors px-1.5" data-reka-collection-item=""><span class="iconify i-lucide:book-open shrink-0 size-5 text-dimmed group-hover:text-default transition-colors" aria-hidden="true"></span>
<li data-state="closed" class="min-w-0" value="1"><button type="button" aria-controls="" aria-expanded="false" data-state="closed" class="group relative w-full flex items-center gap-1.5 font-medium text-sm before:absolute before:z-[-1] before:rounded-md focus:outline-none focus-visible:outline-none dark:focus-visible:outline-none focus-visible:before:ring-inset focus-visible:before:ring-2 focus-visible:before:ring-primary flex-row py-1.5 before:inset-y-px before:inset-x-0 text-muted hover:text-highlighted hover:before:bg-elevated/50 transition-colors before:transition-colors px-1.5" data-reka-collection-item=""><span class="iconify i-lucide:book-open shrink-0 size-5 text-dimmed group-hover:text-default transition-colors" aria-hidden="true"></span>
<!--v-if-->
<!--v-if-->
</button>
<!--v-if-->
<div class="data-[state=open]:animate-[collapsible-down_200ms_ease-out] data-[state=closed]:animate-[collapsible-up_200ms_ease-out] overflow-hidden" id="reka-collapsible-content-v-0-0-1" hidden="" data-state="closed" style="--reka-collapsible-content-height: 0px; --reka-collapsible-content-width: 0px;">
<!---->
</div>
</li>
<li data-menu-item="" class="min-w-0"><button type="button" class="group relative w-full flex items-center gap-1.5 font-medium text-sm before:absolute before:z-[-1] before:rounded-md focus:outline-none focus-visible:outline-none dark:focus-visible:outline-none focus-visible:before:ring-inset focus-visible:before:ring-2 focus-visible:before:ring-primary flex-row py-1.5 before:inset-y-px before:inset-x-0 text-primary before:bg-elevated px-1.5" data-active="" aria-current="page" data-reka-collection-item=""><span class="iconify i-lucide:box shrink-0 size-5 text-primary group-data-[state=open]:text-primary" aria-hidden="true"></span>
<li data-state="closed" class="min-w-0" value="2"><button type="button" aria-controls="" aria-expanded="false" data-state="closed" class="group relative w-full flex items-center gap-1.5 font-medium text-sm before:absolute before:z-[-1] before:rounded-md focus:outline-none focus-visible:outline-none dark:focus-visible:outline-none focus-visible:before:ring-inset focus-visible:before:ring-2 focus-visible:before:ring-primary flex-row py-1.5 before:inset-y-px before:inset-x-0 text-primary before:bg-elevated px-1.5" data-active="" aria-current="page" data-reka-collection-item=""><span class="iconify i-lucide:box shrink-0 size-5 text-primary group-data-[state=open]:text-primary" aria-hidden="true"></span>
<!--v-if-->
<!--v-if-->
</button>
<!--v-if-->
<div class="data-[state=open]:animate-[collapsible-down_200ms_ease-out] data-[state=closed]:animate-[collapsible-up_200ms_ease-out] overflow-hidden" id="reka-collapsible-content-v-0-0-2" hidden="" data-state="closed" style="--reka-collapsible-content-height: 0px; --reka-collapsible-content-width: 0px;">
<!---->
</div>
</li>
</ul>
</div>