From b73ce44718ab8f4d45b3583ee11ef2fdc69b8563 Mon Sep 17 00:00:00 2001 From: Thomas Cazade Date: Fri, 27 Mar 2026 16:32:48 +0100 Subject: [PATCH] fix(NavigationMenu): correct `item-content` and `item-trailing` slot behavior --- .../docs/2.components/navigation-menu.md | 8 ++++ src/runtime/components/NavigationMenu.vue | 15 ++++--- test/components/NavigationMenu.spec.ts | 41 +++++++++++++++++++ 3 files changed, 58 insertions(+), 6 deletions(-) diff --git a/docs/content/docs/2.components/navigation-menu.md b/docs/content/docs/2.components/navigation-menu.md index 8e7de5c6fd..9e07aad6ee 100644 --- a/docs/content/docs/2.components/navigation-menu.md +++ b/docs/content/docs/2.components/navigation-menu.md @@ -1340,6 +1340,10 @@ You can also use the `#item`, `#item-leading`, `#item-label`, `#item-trailing` a Use the `#item-trailing` slot or the `slot` property (`#{{ item.slot }}-trailing`) to add a [DropdownMenu](/docs/components/dropdown-menu) that appears on hover, similar to Notion or Linear. +::note +When you pass `#item-trailing` (or `#{{ item.slot }}-trailing`), it replaces the default trailing UI for every item (badge and chevron). Use the slot props (`item`, `active`, etc.) to render different content per row. +:: + ::component-example --- collapse: true @@ -1351,6 +1355,10 @@ name: 'navigation-menu-trailing-slot-example' Use the `#item-content` slot or the `slot` property (`#{{ item.slot }}-content`) to customize the content of a specific item. +::note +In **horizontal** orientation, the menu trigger and panel are only rendered for items that define a `children` array. The `#item-content` slot customizes what appears inside that panel, it does not create a dropdown for items without `children`. +:: + ::component-example --- collapse: true diff --git a/src/runtime/components/NavigationMenu.vue b/src/runtime/components/NavigationMenu.vue index 877fbd2e79..f238bc05b6 100644 --- a/src/runtime/components/NavigationMenu.vue +++ b/src/runtime/components/NavigationMenu.vue @@ -366,13 +366,16 @@ function onLinkTrailingClick(e: Event, item: NavigationMenuItem) { - + + @@ -402,7 +405,7 @@ function onLinkTrailingClick(e: Event, item: NavigationMenuItem) { - +
  • diff --git a/test/components/NavigationMenu.spec.ts b/test/components/NavigationMenu.spec.ts index 99351ba9a0..907053598d 100644 --- a/test/components/NavigationMenu.spec.ts +++ b/test/components/NavigationMenu.spec.ts @@ -1,4 +1,5 @@ import { describe, it, expect, test } from 'vitest' +import { h } from 'vue' import { axe } from 'vitest-axe' import { mountSuspended } from '@nuxt/test-utils/runtime' import { renderEach } from '../component-render' @@ -130,6 +131,46 @@ describe('NavigationMenu', () => { expect(await axe(wrapper.element)).toHaveNoViolations() }) + it('does not treat global item-content slot as a trigger for items without children', async () => { + const wrapper = await mountSuspended(NavigationMenu, { + props: { + orientation: 'horizontal', + variant: 'link', + items: [[ + { label: 'NoChildren', to: '/' }, + { label: 'WithChildren', to: '/', children: [{ label: 'Child', to: '/child' }] } + ]] + }, + slots: { + 'item-content': ({ item }: { item: { children?: unknown[] } }) => h('div', item.children?.length ? 'custom' : '') + } + }) + + // Only the item with `children` uses NavigationMenuTrigger; the slot must not force a trigger on plain links. + expect(wrapper.findAll('[data-navigation-menu-trigger]')).toHaveLength(1) + }) + + it('item-trailing slot fully overrides default trailing icons', async () => { + const wrapper = await mountSuspended(NavigationMenu, { + props: { + orientation: 'horizontal', + variant: 'link', + items: [[ + { label: 'NoChildren', to: '/' }, + { label: 'WithChildren', to: '/', children: [{ label: 'Child', to: '/child' }] } + ]] + }, + slots: { + 'item-trailing': ({ item }: { item: { children?: unknown[] } }) => item.children?.length + ? h('span', { 'data-testid': 'custom-trailing' }, 'V') + : undefined + } + }) + + expect(wrapper.findAll('[data-slot="linkTrailingIcon"]')).toHaveLength(0) + expect(wrapper.findAll('[data-testid="custom-trailing"]')).toHaveLength(1) + }) + test('should have the correct types', () => { // normal expectSlotProps('item', () => NavigationMenu({