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:
Tabdepuis le dernier élément vers l'extérieur) : fermeture automatique du menu
🛠️ Props
| nom | type | défaut | obligatoire | description |
|---|---|---|---|---|
id | string | () => useRandomId(...) | Identifiant unique pour la navigation | |
label | string | 'Menu principal' | Nom associé à la navigation pour l'accessibilité | |
navItems | array | () => [] | ✅ | 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):
| nom | donnée (payload) | description |
|---|---|---|
click | aucune | déclenche l'ouverture ou la fermeture d'un menu |
keydown | aucune | l‘appui sur Échap qui déclenche la fermeture d'un menu ouvert |
focusin | aucune | Au 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.
| nom | description |
|---|---|
default | Slot par défaut pour le contenu personnalisé de la navigation |
📝 Exemples
Exemple simple d'utilisation de DsfrNavigation :
<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
<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>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
)[]
}