Skip to content

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

NomTypeDéfautDescription
pagesPage[]requisListe des pages, où chaque page est un objet contenant des informations comme href, label et title.
truncLimitnumber5Nombre maximum de pages affichées simultanément.
currentPagenumber0Index de la page actuellement sélectionnée (commence à 0).
firstPageTitlestring'Première page'Texte d'info-bulle pour le lien de la première page.
lastPageTitlestring'Dernière page'Texte d'info-bulle pour le lien de la dernière page.
nextPageTitlestring'Page suivante'Texte d'info-bulle pour le lien de la page suivante.
prevPageTitlestring'Page précédente'Texte d'info-bulle pour le lien de la page précédente.
currentPageTitleSuffixstringundefinedTexte aditionnel d'info-bulle de la page courante.

📡Événements

NomPayloadDescription
update:current-pagenumberÉ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

vue
<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

vue
<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>
ts
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
}