Skip to content

Méga-menu de navigation - DsfrNavigationMegaMenu

🌟 Introduction

Le méga-menu de navigation est un composant complexe qui affiche un menu déroulant étendu avec plusieurs catégories organisées en grille. Il offre une navigation riche avec descriptions et liens structurés.

Le composant DsfrNavigationMegaMenu crée un menu déroulant expansif avec animations de collapse/expand, idéal pour présenter une navigation hiérarchique complexe avec descriptions et liens organisés.

Important

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

📐 Structure

Le méga-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 zone de leadership avec titre, description et lien principal
  • une grille de catégories de menu (prop menus)
  • un bouton de fermeture intégré
  • des slots pour contenu personnalisé

🛠️ Props

nomtypedéfautobligatoiredescription
titlestringTitre du méga-menu affiché dans le bouton et l'en-tête
idstring() => useRandomId(...)Identifiant unique pour le méga-menu
descriptionstring''Description affichée dans la zone de leadership
link{ to: RouteLocationRaw, text: string }{ to: '#', text: 'Voir toute la rubrique' }Lien principal vers la page complète de la rubrique
menusDsfrNavigationMegaMenuCategoryProps[][]Tableau des catégories de menu à afficher
expandedIdstring''ID du menu actuellement expansé
activebooleanfalseIndique si ce méga-menu est actuellement actif

📡 Événements

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

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

🧩 Slots

DsfrNavigationMegaMenu possède deux slots pour personnaliser le contenu.

nomdescription
descriptionSlot nommé pour le contenu personnalisé de la description
defaultSlot par défaut pour le contenu personnalisé du méga-menu

📝 Exemples

Exemple d'utilisation de DsfrNavigationMegaMenu dans une navigation :

vue
<template>
  <DsfrNavigation :nav-items="navItems">
    <DsfrNavigationMegaMenu
      title="Services"
      description="Découvrez tous nos services publics"
      :link="{ to: '/services', text: 'Voir tous les services' }"
      :menus="serviceMenus"
      :expanded-id="expandedMenuId"
      @toggle-id="handleToggle"
    />
  </DsfrNavigation>
</template>

⚙️ Code source du composant

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

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

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

import DsfrNavigationMegaMenuCategory from './DsfrNavigationMegaMenuCategory.vue'

export type { DsfrNavigationMegaMenuProps }

const props = withDefaults(defineProps<DsfrNavigationMegaMenuProps>(), {
  id: () => useRandomId('mega-menu'),
  description: '',
  link: () => ({ to: '#', text: 'Voir toute la rubrique' }),
  menus: () => [],
  expandedId: '',
})

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

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

const expanded = computed(() => {
  return props.id === props.expandedId
})

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

onMounted(() => {
  // NavigationMegaMenu 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)"
  >
    {{ title }}
  </button>
  <div
    :id="id"
    ref="collapse"
    data-testid="mega-menu-wrapper"
    class="fr-collapse fr-mega-menu"
    tabindex="-1"
    :class="{
      'fr-collapse--expanded': cssExpanded, // Need to use a separate data to add/remove the class after a RAF
      'fr-collapsing': collapsing,
    }"
    @transitionend="onTransitionEnd(expanded)"
  >
    <div class="fr-container  fr-container--fluid  fr-container-lg">
      <button
        class="fr-link--close fr-link"
        aria-controls="mega-menu-695"
        @click="$emit('toggleId', id)"
      >
        Fermer
      </button>
      <div
        class="fr-grid-row fr-grid-row-lg--gutters"
      >
        <div class="fr-col-12 fr-col-lg-8 fr-col-offset-lg-4--right fr-mb-4v">
          <div class="fr-mega-menu__leader">
            <h4 class="fr-h4 fr-mb-2v">
              {{ title }}
            </h4>
            <p class="fr-hidden fr-displayed-lg">
              {{ description }}
              <!-- @slot Slot par défaut pour le contenu de la description du mega-menu. Sera dans `<p class="fr-text--sm">` -->
              <slot name="description" />
            </p>
            <RouterLink
              class="fr-link fr-icon-arrow-right-line fr-link--icon-right fr-link--align-on-content"
              :to="link.to"
            >
              {{ link.text }}
            </RouterLink>
          </div>
        </div>
        <!-- @slot Slot par défaut pour le contenu du mega-menu. Sera dans `<div class="fr-grid-row fr-grid-row--gutters">` -->
        <slot />
        <DsfrNavigationMegaMenuCategory
          v-for="(menu, idx) of menus"
          :key="idx"
          v-bind="menu"
        />
      </div>
    </div>
  </div>
</template>

<style scoped>
.fr-collapse--expanded {
  max-height: none !important;
}
</style>
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
  )[]
}