Skip to content

Liste de menu latéral - DsfrSideMenuList

🌟 Introduction

La liste de menu latéral est un composant qui gère une liste d'éléments de navigation dans un menu latéral. Elle supporte les éléments imbriqués et le collapse/expand automatique.

Le composant DsfrSideMenuList crée une liste <ul> avec la classe fr-sidemenu__list et gère automatiquement les liens externes/internes, les états actifs, et les sous-menus collapsibles.

Important

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

📐 Structure

La liste de menu latéral crée :

  • Un conteneur <div> avec support du collapse (si collapsable)
  • Une liste <ul> avec la classe fr-sidemenu__list
  • Des éléments DsfrSideMenuListItem pour chaque élément de menu
  • Support automatique des liens externes (<a>) et internes (<RouterLink>)
  • Gestion des sous-menus avec DsfrSideMenuButton pour les éléments parents

🛠️ Props

nomtypedéfautobligatoiredescription
idstringIdentifiant unique de la liste
collapsablebooleanfalseSi la liste peut être réduite/expandue
expandedbooleanfalseÉtat d'expansion de la liste
menuItemsMenuItem[][]Éléments du menu avec structure imbriquée
focusOnExpandingbooleanfalseFocus automatique lors de l'expansion

📡 Événements

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

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

🧩 Slots

nomdescription
defaultContenu personnalisé de la liste (remplace les éléments par défaut)

📝 Exemples

Exemple d'utilisation de DsfrSideMenuList dans un menu latéral :

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

const menuItems = ref([
  { text: 'Accueil', to: '/', active: true },
  { 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">
    <DsfrSideMenuList
      id="main-menu"
      :menu-items="menuItems"
      @toggle-expand="onToggleExpand"
    />
  </DsfrSideMenu>
</template>

⚙️ Code source du composant

vue
<script lang="ts" setup>
import type { DsfrSideMenuListProps } from './DsfrSideMenu.types'
import type { RouteLocationRaw } from 'vue-router'

import { onMounted, watch } from 'vue'

import { useCollapsable } from '../../composables'

import DsfrSideMenuButton from './DsfrSideMenuButton.vue'
import DsfrSideMenuListItem from './DsfrSideMenuListItem.vue'

export type { DsfrSideMenuListProps }

const props = withDefaults(defineProps<DsfrSideMenuListProps>(), {
  menuItems: () => [],
})

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

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

watch(() => props.expanded, (newValue, oldValue) => {
  if (newValue !== oldValue) {
    doExpand(newValue)
  }
})

onMounted(() => {
  if (props.expanded) {
    doExpand(true)
  }
})

const isExternalLink = (to: string | RouteLocationRaw | undefined) => {
  return typeof to === 'string' && to.startsWith('http')
}
const is = (to: string | RouteLocationRaw | undefined) => {
  return isExternalLink(to) ? 'a' : 'RouterLink'
}
const linkProps = (to: string | RouteLocationRaw | undefined) => {
  return { [isExternalLink(to) ? 'href' : 'to']: to }
}
</script>

<template>
  <div
    :id="id"
    ref="collapse"
    :class="{
      'fr-collapse': collapsable,
      'fr-collapsing': collapsing,
      'fr-collapse--expanded': cssExpanded,
    }"
    @transitionend="onTransitionEnd(!!expanded, focusOnExpanding)"
  >
    <ul
      class="fr-sidemenu__list"
    >
      <!-- @slot Slot par défaut pour le contenu d’une liste du menu latéral -->
      <slot />

      <DsfrSideMenuListItem
        v-for="(menuItem, i) of menuItems"
        :key="i"
        :active="menuItem.active"
      >
        <component
          :is="is(menuItem.to)"
          v-if="!menuItem.menuItems"
          class="fr-sidemenu__link"
          :aria-current="menuItem.active ? 'page' : undefined"
          v-bind="linkProps(menuItem.to)"
        >
          {{ menuItem.text }}
        </component>

        <template v-if="menuItem.menuItems">
          <DsfrSideMenuButton
            :active="!!menuItem.active"
            :expanded="!!menuItem.expanded"
            :control-id="(menuItem.id as string)"
            @toggle-expand="$emit('toggleExpand', $event)"
          >
            {{ menuItem.text }}
          </DsfrSideMenuButton>
          <DsfrSideMenuList
            v-if="menuItem.menuItems"
            :id="(menuItem.id as string)"
            collapsable
            :expanded="menuItem.expanded"
            :menu-items="menuItem.menuItems"
            @toggle-expand="$emit('toggleExpand', $event)"
          />
        </template>
      </DsfrSideMenuListItem>
    </ul>
  </div>
</template>

<style lang="css">
/* Missing in DSFR */
.fr-sidemenu .fr-accordion .fr-collapse {
  padding: 0 1rem 0 1rem;
}
.fr-sidemenu .fr-accordion .fr-collapse--expanded {
  padding-bottom: 0;
  padding-top: 0;
}
</style>
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
}