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 (sicollapsable)
- Une liste <ul>avec la classefr-sidemenu__list
- Des éléments DsfrSideMenuListItempour chaque élément de menu
- Support automatique des liens externes (<a>) et internes (<RouterLink>)
- Gestion des sous-menus avec DsfrSideMenuButtonpour les éléments parents
🛠️ Props 
| nom | type | défaut | obligatoire | description | 
|---|---|---|---|---|
| id | string | ✅ | Identifiant unique de la liste | |
| collapsable | boolean | false | Si la liste peut être réduite/expandue | |
| expanded | boolean | false | État d'expansion de la liste | |
| menuItems | MenuItem[] | [] | Éléments du menu avec structure imbriquée | |
| focusOnExpanding | boolean | false | Focus automatique lors de l'expansion | 
📡 Événements 
DsfrSideMenuList déclenche l'événement suivant :
| nom | donnée (payload) | description | 
|---|---|---|
| toggleExpand | string | Émis lors du toggle d'expansion d'un élément | 
🧩 Slots 
| nom | description | 
|---|---|
| default | Contenu 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="menuItem.expanded = !menuItem.expanded"
          >
            {{ 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
}