Skip to content

Menu latéral - DsfrSideMenu

🌟 Introduction

Le menu latéral est un composant de navigation verticale qui peut être réduit/expandé. Il suit les spécifications du Système de Design Français (DSFR) pour les menus de navigation latérale.

Le composant DsfrSideMenu fournit une navigation latérale avec support du collapse/expand, gestion des éléments de menu imbriqués, et intégration avec le routeur Vue.

📐 Structure

Le menu latéral crée :

  • Un élément <nav> avec la classe fr-sidemenu
  • Un bouton de toggle pour réduire/expandre le menu
  • Un conteneur collapsible avec les éléments de menu
  • Support des liens externes et internes avec le routeur Vue
  • Gestion automatique des états actifs et expandés

🛠️ Props

nomtypedéfautobligatoiredescription
buttonLabelstring'Dans cette rubrique'Texte du bouton de toggle
idstring() => useRandomId(...)Identifiant unique du menu
sideMenuListIdstring() => useRandomId(...)Identifiant de la liste de menu
collapseValuestring'-492px'Valeur de collapse CSS
menuItemsDsfrSideMenuListItemProps[]undefinedÉléments du menu (structure imbriquée)
headingTitlestring''Titre du menu
titleTagstring'h3'Balise HTML pour le titre
focusOnExpandingbooleantrueFocus automatique lors de l'expansion

📡 Événements

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

nomdonnée (payload)description
toggleExpandstringÉmis lors du toggle d'expansion d'un élément

🧩 Slots

nomdescription
defaultContenu du menu latéral (remplace la liste par défaut)

📝 Exemples

Exemple d'utilisation basique du menu latéral :

vue
<script setup lang="ts">
import { ref } from 'vue'

const menuItems = ref([
  { text: 'Accueil', to: '/' },
  { text: 'À propos', to: '/about' },
  {
    text: 'Services',
    menuItems: [
      { text: 'Service 1', to: '/service1' },
      { text: 'Service 2', to: '/service2' },
    ],
  },
])

const onToggleExpand = (id: string) => {
  console.log('Toggle expand:', id)
}
</script>

<template>
  <DsfrSideMenu
    heading-title="Navigation"
    :menu-items="menuItems"
    @toggle-expand="onToggleExpand"
  />
</template>

⚙️ Code source du composant

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

import { ref, watch } from 'vue'

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

import DsfrSideMenuList from './DsfrSideMenuList.vue'

export type { DsfrSideMenuProps }

withDefaults(defineProps<DsfrSideMenuProps>(), {
  buttonLabel: 'Dans cette rubrique',
  id: () => useRandomId('sidemenu'),
  sideMenuListId: () => useRandomId('sidemenu', 'list'),
  collapseValue: '-492px',
  // @ts-expect-error this is really undefined
  menuItems: () => undefined,
  headingTitle: '',
  titleTag: 'h3',
  focusOnExpanding: true,
})

defineEmits<{ (e: 'toggleExpand', payload: string): void }>()

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

const expanded = ref(false)

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

<template>
  <nav
    class="fr-sidemenu"
    :aria-labelledby="id"
  >
    <div class="fr-sidemenu__inner">
      <button
        class="fr-sidemenu__btn"
        :aria-controls="id"
        :aria-expanded="expanded"
        @click.prevent.stop="expanded = !expanded"
      >
        {{ buttonLabel }}
      </button>
      <div
        :id="id"
        ref="collapse"
        class="fr-collapse"
        :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, focusOnExpanding)"
      >
        <component
          :is="titleTag"
          class="fr-sidemenu__title"
        >
          {{ headingTitle }}
        </component>
        <!-- @slot Slot par défaut du contenu du menu latéral -->
        <slot>
          <DsfrSideMenuList
            :id="sideMenuListId"
            :menu-items="menuItems"
            @toggle-expand="$emit('toggleExpand', $event)"
          />
        </slot>
      </div>
    </div>
  </nav>
</template>
ts
import type { RouteLocationRaw } from 'vue-router'

export type DsfrSideMenuListItemProps = { active?: boolean }

export type DsfrSideMenuProps = {
  buttonLabel?: string
  id?: string
  sideMenuListId?: string
  collapseValue?: string
  menuItems?: DsfrSideMenuListItemProps[]
  headingTitle?: string
  titleTag?: string
  focusOnExpanding?: boolean
}

export type DsfrSideMenuButtonProps = {
  active?: boolean
  expanded?: boolean
  controlId: string
}

export type DsfrSideMenuListProps = {
  id: string
  collapsable?: boolean
  expanded?: boolean
  menuItems?: (
    DsfrSideMenuListItemProps & Partial<DsfrSideMenuListProps & { to?: RouteLocationRaw, text?: string }>
    & { menuItems?: (DsfrSideMenuListItemProps & Partial<DsfrSideMenuListProps & { to?: RouteLocationRaw, text?: string }>)[] }
  )[]
  focusOnExpanding?: boolean
}