Skip to content

Navigation principale - DsfrNavigation

🌟 Introduction

Le système de navigation principal permet d'orienter les utilisateurs à travers l'application. Il constitue l'épine dorsale de la navigation d'un site, offrant une structure claire et accessible pour explorer les différentes sections et fonctionnalités.

Le composant DsfrNavigation est le système central de navigation au sein d'un site. Il permet d'orienter aisément l'usager à travers l'application avec une structure hiérarchique claire et des menus déroulants.

🏅 La documentation sur la navigation sur le DSFR

La story sur la navigation sur le storybook de VueDsfr

📐 Structure

La navigation principale est composée des éléments suivants :

  • un conteneur principal avec un identifiant unique (prop id)
  • un label d'accessibilité (prop label)
  • une liste de liens et sous-menus (prop navItems) organisée hiérarchiquement
  • des menus déroulants qui s'ouvrent/ferment au clic
  • une gestion des interactions clavier et focus pour l'accessibilité
    • touche Échap : fermeture du menu ouvert
    • sortie de focus du menu ouvert (ex: Tab depuis le dernier élément vers l'extérieur) : fermeture automatique du menu

🛠️ Props

nomtypedéfautobligatoiredescription
idstring() => useRandomId(...)Identifiant unique pour la navigation
labelstring'Menu principal'Nom associé à la navigation pour l'accessibilité
navItemsarray() => []Tableau contenant les liens ou sous-menus de la navigation

📡 Événements écoutés (internes)

DsfrNavigation n’émet pas d’événements spécifiques. Le composant écoute les événements DOM globaux suivants (pour gérer l’ouverture/fermeture des menus):

nomdonnée (payload)description
clickaucunedéclenche l'ouverture ou la fermeture d'un menu
keydownaucunel‘appui sur Échap qui déclenche la fermeture d'un menu ouvert
focusinaucuneAu changement de focus : si le focus sort du menu ouvert, celui-ci est refermé automatiquement

🧩 Slots

DsfrNavigation possède un slot par défaut pour le contenu personnalisé de la navigation.

nomdescription
defaultSlot par défaut pour le contenu personnalisé de la navigation

📝 Exemples

Exemple simple d'utilisation de DsfrNavigation :

vue
<script lang="ts" setup>
import DsfrNavigation from '../DsfrNavigation.vue'

const navItemsDemo = [
  {
    to: '/accueil',
    text: 'Accueil',
  },
  {
    to: '/tableau-de-bord',
    text: 'Tableau de bord',
  },
  {
    to: '/historique',
    text: 'Historique',
  },
]
</script>

<template>
  <DsfrNavigation
    label="Menu principal démo"
    :nav-items="navItemsDemo"
  />
</template>

⚙️ Code source du composant

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

import { onMounted, onUnmounted, ref } from 'vue'

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

import DsfrNavigationItem from './DsfrNavigationItem.vue'
import DsfrNavigationMegaMenu from './DsfrNavigationMegaMenu.vue'
import DsfrNavigationMenu from './DsfrNavigationMenu.vue'
import DsfrNavigationMenuLink from './DsfrNavigationMenuLink.vue'

export type { DsfrNavigationMenuLinks, DsfrNavigationProps }

const props = withDefaults(defineProps<DsfrNavigationProps>(), {
  id: () => useRandomId('nav'),
  label: 'Menu principal',
  navItems: () => [],
})

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

const expandedMenuId = ref<string | undefined>(undefined)

const toggle = (id: string | undefined) => {
  if (id === expandedMenuId.value) {
    expandedMenuId.value = undefined
    return
  }
  expandedMenuId.value = id
}

const handleElementClick = (el: HTMLElement) => {
  if (el === document.getElementById(props.id)) {
    return
  }

  if (!el?.parentNode) {
    toggle(expandedMenuId.value)
    return
  }

  handleElementClick(el.parentNode as HTMLElement)
}

const onDocumentClick = (e: MouseEvent) => {
  handleElementClick(e.target as HTMLElement)
}

const onKeyDown = (e: KeyboardEvent) => {
  if (e.key === 'Escape') {
    toggle(expandedMenuId.value)
  }
}

const onDocumentFocusIn = (e: FocusEvent) => {
  if (!expandedMenuId.value) {
    return
  }

  const expandedMenu = document.getElementById(expandedMenuId.value)
  const target = e.target as HTMLElement | null

  if (!expandedMenu || !target) {
    return
  }

  // Keep menu state when focusing its controlling button.
  if (target.getAttribute('aria-controls') === expandedMenuId.value) {
    return
  }

  if (!expandedMenu.contains(target)) {
    toggle(expandedMenuId.value)
  }
}

onMounted(() => {
  document.addEventListener('click', onDocumentClick)
  document.addEventListener('keydown', onKeyDown)
  document.addEventListener('focusin', onDocumentFocusIn)
})
onUnmounted(() => {
  document.removeEventListener('click', onDocumentClick)
  document.removeEventListener('keydown', onKeyDown)
  document.removeEventListener('focusin', onDocumentFocusIn)
})
</script>

<template>
  <nav
    :id="id"
    class="fr-nav"
    role="navigation"
    :aria-label="label"
  >
    <ul class="fr-nav__list">
      <slot />
      <DsfrNavigationItem
        v-for="(navItem, idx) of navItems"
        :id="navItem.id"
        :key="idx"
      >
        <DsfrNavigationMenuLink
          v-if="(navItem as DsfrNavigationMenuLinkProps).to && (navItem as DsfrNavigationMenuLinkProps).text"
          v-bind="navItem"
          :expanded-id="expandedMenuId"
          @toggle-id="toggle($event)"
        />
        <DsfrNavigationMenu
          v-else-if="(navItem as DsfrNavigationMenuProps).title && (navItem as DsfrNavigationMenuProps).links"
          v-bind="(navItem as DsfrNavigationMenuProps)"
          :expanded-id="expandedMenuId"
          @toggle-id="toggle($event)"
        />
        <DsfrNavigationMegaMenu
          v-else-if="(navItem as DsfrNavigationMegaMenuProps).title && (navItem as DsfrNavigationMegaMenuProps).menus"
          v-bind="(navItem as DsfrNavigationMegaMenuProps)"
          :expanded-id="expandedMenuId"
          @toggle-id="toggle($event)"
        />
      </DsfrNavigationItem>
    </ul>
  </nav>
</template>

<style>
.fr-nav__list {
  position: relative;
}
</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
  )[]
}