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
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
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="$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
}