Skip to content

Menu de navigation - DsfrNavigationMenu ​

🌟 Introduction ​

Le menu de navigation est un composant qui affiche un menu déroulant simple avec une liste de liens. Il offre une navigation hiérarchique avec animations d'ouverture/fermeture.

Le composant DsfrNavigationMenu crée un menu déroulant traditionnel avec un bouton déclencheur et une liste de liens organisés verticalement.

Important

Ce composant NE devrait PAS être utilisé directement, il est utilisé en interne par son parent DsfrNavigation

📐 Structure ​

Le menu de navigation est composé des éléments suivants :

  • un bouton déclencheur avec le titre (prop title)
  • un conteneur expansible avec animations de collapse
  • une liste non-ordonnée (<ul>) de liens de navigation
  • chaque lien utilise les composants DsfrNavigationMenuItem et DsfrNavigationMenuLink
  • un slot par défaut pour contenu personnalisé

🛠️ Props ​

nomtypedéfautobligatoiredescription
titlestring✅Titre du menu affiché dans le bouton
idstring() => useRandomId(...)Identifiant unique pour le menu
linksDsfrNavigationMenuLinkProps[][]Tableau des liens à afficher dans ce menu
expandedIdstring''ID du menu actuellement expansé
activebooleanfalseIndique si ce menu est actuellement actif

📡 Événements ​

DsfrNavigationMenu déclenche l'événement suivant :

nomdonnée (payload)description
toggleIdstringÉmis lors du clic sur le bouton pour ouvrir/fermer le menu

🧩 Slots ​

DsfrNavigationMenu possède un slot par défaut pour personnaliser le contenu.

nomdescription
defaultSlot par défaut pour le contenu personnalisé du menu

📝 Exemples ​

Exemple d'utilisation de DsfrNavigationMenu dans une navigation :

vue
<template>
  <DsfrNavigation :nav-items="navItems">
    <DsfrNavigationMenu
      title="Administration"
      :links="[
        { to: '/impots', text: 'Impôts' },
        { to: '/securite-sociale', text: 'Sécurité sociale' },
        { to: '/permis', text: 'Permis de conduire' },
      ]"
      :expanded-id="expandedMenuId"
      @toggle-id="handleToggle"
    />
  </DsfrNavigation>
</template>

⚙️ Code source du composant ​

vue
<script lang="ts" setup>
import type { DsfrNavigationMenuProps } from './DsfrNavigation.types'

import { computed, onMounted, watch } from 'vue'

import { useCollapsable } from '../../composables'
import { useRandomId } from '../../utils/random-utils'

import DsfrNavigationMenuItem from './DsfrNavigationMenuItem.vue'
import DsfrNavigationMenuLink from './DsfrNavigationMenuLink.vue'

export type { DsfrNavigationMenuProps }

const props = withDefaults(defineProps<DsfrNavigationMenuProps>(), {
  id: () => useRandomId('menu'),
  links: () => [],
  expandedId: '',
})

defineEmits<{ (event: 'toggleId', id: string): void }>()

defineSlots<{
  /**
   * Slot par défaut pour le contenu de l’item de liste.
   * Sera dans `<ul class="fr-menu__list">`
   */
  default: () => any
}>()

const {
  collapse,
  collapsing,
  cssExpanded,
  doExpand,
  onTransitionEnd,
} = useCollapsable()

const expanded = computed(() => props.id === props.expandedId)

watch(expanded, (newValue, oldValue) => {
  // @see https://github.com/GouvernementFR/dsfr/blob/main/src/core/script/collapse/collapse.js
  if (newValue !== oldValue) {
    doExpand(newValue)
  }
})

onMounted(() => {
  // NavigationMenu can be expanded by default
  // We need to trigger the expand animation at mounted
  if (expanded.value) {
    doExpand(true)
  }
})
</script>

<template>
  <button
    class="fr-nav__btn"
    :aria-expanded="expanded"
    :aria-current="active || undefined"
    :aria-controls="id"
    @click="$emit('toggleId', id)"
  >
    <span>{{ title }}</span>
  </button>
  <div
    :id="id"
    ref="collapse"
    class="fr-collapse fr-menu"
    data-testid="navigation-menu"
    :class="{ 'fr-collapse--expanded': cssExpanded, 'fr-collapsing': collapsing }"
    @transitionend="onTransitionEnd(expanded)"
  >
    <ul
      class="fr-menu__list"
    >
      <slot />
      <DsfrNavigationMenuItem
        v-for="(link, idx) of links"
        :key="idx"
      >
        <DsfrNavigationMenuLink
          v-bind="link"
          @toggle-id="$emit('toggleId', expandedId)"
        />
      </DsfrNavigationMenuItem>
    </ul>
  </div>
</template>
ts
import type { RouteLocationRaw } from 'vue-router'

export type DsfrNavigationMenuLinkProps = {
  id?: string
  to?: string | RouteLocationRaw
  text?: string
  icon?: string
  onClick?: ($event: MouseEvent) => void
}

export type DsfrNavigationMenuItemProps = {
  id?: string
  active?: boolean
}

export type DsfrNavigationMenuProps = {
  id?: string
  title: string
  links?: DsfrNavigationMenuLinkProps[]
  expandedId?: string
  active?: boolean
}

export type DsfrNavigationItemProps = {
  id?: string
  active?: boolean
}

export type DsfrNavigationMegaMenuCategoryProps = {
  title: string
  active?: boolean
  links: DsfrNavigationMenuLinkProps[]
}

export type DsfrNavigationMegaMenuProps = {
  id?: string
  title: string
  description?: string
  link?: { to: RouteLocationRaw, text: string }
  menus?: DsfrNavigationMegaMenuCategoryProps[]
  expandedId?: string
  active?: boolean
}

export type DsfrNavigationMenuLinks = (DsfrNavigationMenuLinkProps | DsfrNavigationMegaMenuProps | DsfrNavigationMenuProps)[]

export type DsfrNavigationProps = {
  id?: string
  label?: string
  navItems?: (
    DsfrNavigationMenuLinkProps
    | DsfrNavigationMenuProps
    | DsfrNavigationMegaMenuProps
  )[]
}