Accordéon - DsfrAccordion
🌟 Introduction
Les accordéons permettent aux utilisateurs d'afficher et de masquer des sections de contenu présentés dans une page.
🏅 La documentation sur l’accordéon sur le DSFR
La story sur l’accordéon sur le storybook de VueDsfr📐 Structure
Un accordéon est constitué des éléments suivants :
- un en-tête (prop
title
, de typestring
), correspondant au titre de la section - obligatoire. - une icône, indique quand le panneau est fermé, quand le panneau est ouvert.
- un séparateur
- une zone de contenu, masqué par défaut pouvant contenir tout type d'élément, le
slot
par défaut est fait pour ça
Autres props :
id
: identifiant du contenu de l’accordéon, qui est utilisé aussi pour l’attributaria-controls
du bouton qui permet de plier et déplier l’accordéonexpandedId
: identifiant de l’accordéon actuellement déplié (pour savoir si l’accordéon doit être déplié)
🛠️ Props
Nom | Type | Défaut | Obligatoire |
---|---|---|---|
title | string | ✅ | |
titleTag | TitleTag | 'h3' | |
id | string | random string | |
expandedId | string | undefined |
📡 Évenements
DsfrAccordion
déclenche l’événement 'expand'
au clic sur le titre de l’accordéon.
nom | donnée (payload) | détail de la donnée |
---|---|---|
'expand' | string | Il s’agit de l’id de l’accordéon (sa prop id ) |
Ceci permet de récupérer l’id et de passer la prop expandedId
à tous les accordéons du groupe (voir le code de la démo ci-dessous).
🧩 Slots
DsfrAccordion
possède un slot par défaut pour le contenu de l’accordéon.
🏗️ Les groupes d’accordéons - DsfrAccordionGroup
Un accordéon prend plus de sens lorsqu’il fait partie d’un groupe (comme nous tous, non ?).
Les accordéons sont tous fermés et comprennent l’en tête et l’icône si la prop expandedId
est undefined
.
La totalité de la barre de titre est cliquable. L’événement clic sur le titre de l’accordéon peut être intercepté en ajoutant un écouteur sur l’événement expand
qui envoie l’id de l’accordéon :
<script lang="ts" setup>
// (...)
const title1 = ref('Un titre d’accordéon 1')
const title2 = ref('Un titre d’accordéon 2')
// (...)
const expandedId = ref('')
</script>
<template>
<DsfrAccordionsGroup>
<li>
<DsfrAccordion
id="accordion-1"
:title="title1"
:expanded-id="expandedId"
@expand="expandedId = $event"
>
Contenu de l’accordéon 1
</DsfrAccordion>
</li>
<li>
<DsfrAccordion
id="accordion-2"
:title="title2"
:expanded-id="expandedId"
@expand="id => expandedId = id"
>
Contenu de l’accordéon 2
</DsfrAccordion>
</li>
<!-- (...) -->
</template>
TIP
Comme l’événement expand
a comme donnée (payload) l’id
de l’accordéon, les deux lignes suivantes sont équivalentes :
@expand="id => expandedId = id"
et
@expand="expandedId = $event"
Cf. la documentation Vue pour plus de détails.
📝 Exemple complet
<script lang="ts" setup>
import { ref } from 'vue'
import DsfrAccordion from '../DsfrAccordion.vue'
import DsfrAccordionsGroup from '../DsfrAccordionsGroup.vue'
const title1 = ref('Un titre d’accordéon 1')
const title2 = ref('Un titre d’accordéon 2')
const title3 = ref('Un titre d’accordéon 3')
const expandedId = ref<string>()
</script>
<template>
<DsfrAccordionsGroup>
<li>
<DsfrAccordion
id="accordion-1"
:title="title1"
:expanded-id="expandedId"
@expand="expandedId = $event"
>
Contenu de l’accordéon 1
</DsfrAccordion>
</li>
<li>
<DsfrAccordion
id="accordion-2"
:title="title2"
:expanded-id="expandedId"
@expand="id => expandedId = id"
>
Contenu de l’accordéon 2
</DsfrAccordion>
</li>
<li>
<DsfrAccordion
id="accordion-3"
:title="title3"
:expanded-id="expandedId"
@expand="id => expandedId = id"
>
Contenu de l’accordéon 3
</DsfrAccordion>
</li>
</DsfrAccordionsGroup>
</template>
<script lang="ts" setup>
import { computed, onMounted, watch } from 'vue'
import { getRandomId } from '../../utils/random-utils'
import { useCollapsable } from '../../composables'
import type { DsfrAccordionProps } from './DsfrAccordion.types'
export type { DsfrAccordionProps }
const props = withDefaults(
defineProps<DsfrAccordionProps>(),
{
id: () => getRandomId('accordion'),
expandedId: undefined,
title: 'Sans intitulé',
titleTag: 'h3',
},
)
const emit = defineEmits<{ (event: 'expand', id: string | undefined): void }>()
const {
collapse,
collapsing,
cssExpanded,
doExpand,
onTransitionEnd,
} = useCollapsable()
const expanded = computed(() => props.expandedId === props.id)
onMounted(() => {
// Accordion can be expanded by default
// We need to trigger the expand animation on mounted
if (expanded.value) {
doExpand(true)
}
})
watch(expanded, (newValue, oldValue) => {
/*
* @see https://github.com/GouvernementFR/dsfr/blob/main/src/core/script/collapse/collapse.js
*/
if (newValue !== oldValue) {
doExpand(newValue)
}
})
const toggleExpanded = () => {
/*
* Close current accordion if expanded
*/
emit('expand', expanded.value ? undefined : props.id)
}
</script>
<template>
<section class="fr-accordion">
<component
:is="titleTag"
class="fr-accordion__title"
>
<button
class="fr-accordion__btn"
:aria-expanded="expanded"
:aria-controls="id"
type="button"
@click="toggleExpanded()"
>
<!-- @slot Slot pour le contenu personnalisé du titre de l’accordéon. Une **props du même nom est utilisable pour du texte simple** sans mise en forme. -->
<slot name="title">
<span>{{ title }}</span>
</slot>
</button>
</component>
<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)"
>
<!-- @slot Slot par défaut pour le contenu de l’accordéon: sera dans `<div class="fr-collapse">` -->
<slot />
</div>
</section>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const expandedId = ref<string>()
const expand = (id: string): void => { expandedId.value = id }
</script>
<template>
<ul
class="fr-accordions-group"
@expand="expand($event)"
>
<!-- @slot Slot par défaut pour le contenu de la liste. Sera dans `<ul class="fr-accordions-group">` -->
<slot :expanded-id="expandedId" />
</ul>
</template>