Pagination - DsfrPagination
🌟 Introduction
Le composant DsfrPagination est un système de pagination conforme aux bonnes pratiques ergonomiques et accessible (ARIA). Il permet de naviguer facilement à travers plusieurs pages avec des fonctionnalités avancées comme la limitation de pages affichées et la gestion des événements.
🏅 La documentation sur le pagination sur le DSFR
La story sur le tag sur le storybook de VueDsfr📐 Structure
Ce composant affiche des liens vers les pages avoisinant la page courante (mise en avant). Il affiche aussi la dernière page de la liste comme dernier élément de la pagination afin que l’usager connaisse le nombre total de pages. Il présente un accès rapide vers la première page, la précédente, la suivante, et la dernière, avec des contrôles adaptatifs selon l'état de la pagination. Des troncatures sont affichées (éventuellement masquées pour de petits écrans) pour matérialiser les pages ommises. Le composant propose aussi l'ajout d'un suffixe au texte du titre (title qui sert nottament à l'affichage d'une bulle d'aide) de la page courante pour la mettre en valeur.
🛠️ Props
| Nom | Type | Défaut | Description |
|---|---|---|---|
pages | Page[] | requis | Liste des pages, où chaque page est un objet contenant des informations comme href, label et title. |
truncLimit | number | 5 | Nombre maximum de pages affichées simultanément. |
currentPage | number | 0 | Index de la page actuellement sélectionnée (commence à 0). |
firstPageTitle | string | 'Première page' | Texte d'info-bulle pour le lien de la première page. |
lastPageTitle | string | 'Dernière page' | Texte d'info-bulle pour le lien de la dernière page. |
nextPageTitle | string | 'Page suivante' | Texte d'info-bulle pour le lien de la page suivante. |
prevPageTitle | string | 'Page précédente' | Texte d'info-bulle pour le lien de la page précédente. |
currentPageTitleSuffix | string | undefined | Texte aditionnel d'info-bulle de la page courante. |
📡Événements
| Nom | Payload | Description |
|---|---|---|
update:current-page | number | Émis lorsque l'utilisateur change de page. |
Il faut donc utiliser v-model:current-page sur le composant (cf. l’exemple ci-dessous).
🧩 Slots
Ce composant n'utilise pas de slots, tout est configuré via les props et les données des pages. 🚀
📝 Exemple d'utilisation
<script setup lang="ts">
import type { Page } from '../DsfrPagination.vue'
import { ref } from 'vue'
import DsfrPagination from '../DsfrPagination.vue'
const currentPage = ref(5)
const pages = ref<Page[]>([
{ title: 'Lien vers la page 1', href: '#1', label: '1' },
{ title: 'Lien vers la page 2', href: '#2', label: '2' },
{ title: 'Lien vers la page 3', href: '#3', label: '3' },
{ title: 'Lien vers la page 4', href: '#4', label: '4' },
{ title: 'Lien vers la page 5', href: '#5', label: '5' },
{ title: 'Lien vers la page 6', href: '#6', label: '6' },
{ title: 'Lien vers la page 7', href: '#7', label: '7' },
{ title: 'Lien vers la page 8', href: '#8', label: '8' },
{ title: 'Lien vers la page 9', href: '#9', label: '9' },
{ title: 'Lien vers la page 10', href: '#10', label: '10' },
])
</script>
<template>
<DsfrPagination
v-model:current-page="currentPage"
:pages="pages"
:trunc-limit="2"
current-page-title-suffix=" - page courante"
/>
</template>⚙️ Code source du composant
<script lang="ts" setup>
import type { DsfrPaginationProps, Page } from './DsfrPagination.types'
import { computed } from 'vue'
export type { DsfrPaginationProps, Page }
const props = withDefaults(defineProps<DsfrPaginationProps>(), {
truncLimit: 5,
currentPage: 0,
firstPageTitle: 'Première page',
lastPageTitle: 'Dernière page',
nextPageTitle: 'Page suivante',
prevPageTitle: 'Page précédente',
ariaLabel: 'Pagination',
})
const emit = defineEmits<{
/** Émis lors de la mise à jour de la page courante */
'update:current-page': [payload: number]
}>()
const startIndex = computed(() => {
return Math.min(props.pages.length - 1 - props.truncLimit, Math.max(props.currentPage - (props.truncLimit - props.truncLimit % 2) / 2, 0))
})
const endIndex = computed(() => {
return Math.min(props.pages.length - 1, startIndex.value + props.truncLimit)
})
const displayedPages = computed(() => {
return props.pages.length > props.truncLimit ? props.pages.slice(startIndex.value, endIndex.value + 1) : props.pages
})
const lastPage = props.pages[props.pages.length - 1]
const updatePage = (index: number) => emit('update:current-page', index)
const toPage = (index: number) => updatePage(index)
const tofirstPage = () => toPage(0)
const toPreviousPage = () => toPage(Math.max(0, props.currentPage - 1))
const toNextPage = () => toPage(Math.min(props.pages.length - 1, props.currentPage + 1))
const toLastPage = () => toPage(props.pages.length - 1)
const isCurrentPage = (page: Page) => props.pages.indexOf(page) === props.currentPage
</script>
<template>
<nav
role="navigation"
class="fr-pagination"
:aria-label="ariaLabel"
>
<ul class="fr-pagination__list">
<li>
<a
:href="pages[0]?.href"
class="fr-pagination__link fr-pagination__link--first"
:class="{ 'fr-pagination__link--disabled': currentPage === 0 }"
:title="firstPageTitle"
:aria-disabled="currentPage === 0 ? true : undefined"
@click.prevent="currentPage === 0 ? null : tofirstPage()"
>
<span class="fr-sr-only">{{ firstPageTitle }}</span>
</a>
</li>
<li>
<a
:href="pages[Math.max(currentPage - 1, 0)]?.href"
class="fr-pagination__link fr-pagination__link--prev fr-pagination__link--lg-label"
:class="{ 'fr-pagination__link--disabled': currentPage === 0 }"
:title="prevPageTitle"
:aria-disabled="currentPage === 0 ? true : undefined"
@click.prevent="currentPage === 0 ? null : toPreviousPage()"
>{{ prevPageTitle }}</a>
</li>
<li v-if="startIndex > 0 ">
<span class="fr-pagination__link fr-unhidden-lg">...</span>
</li>
<li
v-for="(page, idx) in displayedPages"
:key="idx"
>
<a
:href="page?.href"
class="fr-pagination__link fr-unhidden-lg"
:title="(isCurrentPage(page) && page.title) ? (currentPageTitleSuffix) ? page.title + currentPageTitleSuffix : page.title : (page.title !== page.label) ? page.title : undefined"
:aria-current="isCurrentPage(page) ? 'page' : undefined"
@click.prevent="toPage(pages.indexOf(page))"
>
{{ page.label }}
</a>
</li>
<li v-if="endIndex < pages.length - 2">
<span class="fr-pagination__link fr-unhidden-lg">...</span>
</li>
<li v-if="endIndex < pages.length - 1">
<a
:href="lastPage.href"
class="fr-pagination__link fr-unhidden-lg"
:title="(lastPage.title !== lastPage.label) ? lastPage.title : undefined"
:aria-current="isCurrentPage(lastPage) ? 'page' : undefined"
@click.prevent="toPage(props.pages.indexOf(lastPage))"
>{{ lastPage.label }}</a>
</li>
<li>
<a
:href="pages[Math.min(currentPage + 1, pages.length - 1)]?.href"
class="fr-pagination__link fr-pagination__link--next fr-pagination__link--lg-label"
:class="{ 'fr-pagination__link--disabled': currentPage === pages.length - 1 }"
:title="nextPageTitle"
:disabled="currentPage === pages.length - 1 ? true : undefined"
:aria-disabled="currentPage === pages.length - 1 ? true : undefined"
@click.prevent="currentPage === pages.length - 1 ? null : toNextPage()"
>{{ nextPageTitle }}</a>
</li>
<li>
<a
:href="pages.at(-1)?.href"
class="fr-pagination__link fr-pagination__link--last"
:class="{ 'fr-pagination__link--disabled': currentPage === pages.length - 1 }"
:title="lastPageTitle"
:disabled="currentPage === pages.length - 1 ? true : undefined"
:aria-disabled="currentPage === pages.length - 1 ? true : undefined"
@click.prevent="currentPage === pages.length - 1 ? null : toLastPage()"
>
<span class="fr-sr-only">{{ lastPageTitle }}</span>
</a>
</li>
</ul>
</nav>
</template>
<style scoped>
.fr-pagination__link:hover {
background-image: linear-gradient(
deg, rgba(224,224,224,0.5), rgba(224,224,224,0.5));
}
.fr-pagination__link--disabled {
color: currentColor;
cursor: not-allowed;
opacity: 0.5;
text-decoration: none;
}
</style>export type Page = {
href?: string
label: string
title: string
}
export type DsfrPaginationProps = {
pages: Page[]
currentPage?: number
firstPageTitle?: string
lastPageTitle?: string
nextPageTitle?: string
prevPageTitle?: string
currentPageTitleSuffix?: string
truncLimit?: number
ariaLabel?: string
}