Files
malio-layer-ui/docs/superpowers/specs/2026-03-24-datatable-design.md
tristan 660c3787fd [#MUI-22] Création d'un composant datatable (#27)
| Numéro du ticket | Titre du ticket |
|------------------|-----------------|
|                  |                 |

## Description de la PR

## Modification du .env

## Check list

- [ ] Pas de régression
- [x] TU/TI/TF rédigée
- [ ] TU/TI/TF OK
- [x] CHANGELOG modifié

Reviewed-on: #27
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
2026-04-16 07:00:59 +00:00

7.6 KiB

MalioDataTable — Design Spec

Composant de tableau de données presentational avec pagination, filtres par slots et lignes cliquables.

Ticket : MUI-22 Branche : feature/MUI-22-developper-le-composant-datatable

Architecture

Composant unique MalioDataTable dans app/components/malio/datatable/DataTable.vue. Pas de décomposition — la pagination est intégrée dans le composant.

Le composant est presentational : il ne fait aucun fetch. Le parent fournit les données (items) et le total (totalItems), et réagit aux events de pagination/filtre pour relancer ses propres requêtes API.

Props

Prop Type Défaut Description
id string auto-généré Identifiant HTML du wrapper
columns Column[] requis Définition des colonnes
items Record<string, any>[] requis Données à afficher
totalItems number requis Nombre total d'items (pour calculer le nb de pages)
page number 1 Page courante, 1-based (v-model)
perPage number 10 Nombre de lignes par page (v-model)
perPageOptions number[] [10, 25, 50] Options du sélecteur de lignes
rowClickable boolean true Rend les lignes cliquables (cursor pointer + hover)
tableClass string '' Classes CSS additionnelles sur <table> (twMerge)
emptyMessage string 'Aucune donnée' Message affiché quand items est vide

Type Column

type Column = {
  key: string   // Clé correspondant à item[key]
  label: string // Texte affiché dans le <th> (fallback si pas de slot header)
}

Events

Event Payload Description
update:page number Changement de page (pagination ou Prev/Next)
update:per-page number Changement du nombre de lignes par page
row-click Record<string, any> Clic sur une ligne (l'item de la ligne)

Slots

Slot Scope Description
#header-{key} { column } Contenu du <th> — filtre (input, select…). Si absent, affiche column.label en texte
#cell-{key} { item, column } Contenu du <td>. Si absent, affiche item[column.key] en texte
#empty Contenu affiché quand items est vide. Si absent, affiche emptyMessage

Structure HTML

<div :id="id">                    ← wrapper
  <table>
    <thead>
      <tr>
        <th v-for="col" scope="col">  ← une seule ligne d'en-tête
          slot #header-{key}           ← filtre (placeholder = nom colonne)
          OU label texte               ← si pas de slot
        </th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="item"            ← cliquable si rowClickable
          tabindex="0"            ← (si rowClickable) navigation clavier
          @click="emit row-click"
          @keydown.enter/space="emit row-click">
        <td v-for="col">
          slot #cell-{key}        ← contenu custom
          OU item[col.key]        ← texte brut
        </td>
      </tr>
      <tr v-if="!items.length">   ← état vide
        <td :colspan="columns.length">
          slot #empty OU emptyMessage
        </td>
      </tr>
    </tbody>
  </table>
  <div v-if="totalItems > 0">    ← barre de pagination (masquée si aucune donnée)
    <MalioSelect />               ← sélecteur nb lignes (options mappées depuis perPageOptions)
    <nav aria-label="Pagination"> ← numéros de page + Prev/Next
      <MalioButton variant="tertiary" label="Prev" />  ← disabled si page 1
      <button> pour chaque numéro de page               ← éléments <button>
      <span aria-hidden="true">…</span>                 ← ellipsis
      <MalioButton variant="tertiary" label="Next" />   ← disabled si dernière page
    </nav>
  </div>
</div>

Logique de pagination (troncature)

Règles

  • ≤ 5 pages : afficher toutes les pages, pas d'ellipsis
  • > 5 pages : toujours afficher page 1 et dernière page, 1 voisin de chaque côté de la page active, ellipsis quand écart > 1
  • Prev : MalioButton variant="tertiary", toujours visible, disabled sur page 1
  • Next : MalioButton variant="tertiary", toujours visible, disabled sur dernière page
  • Changement de perPage : émet automatiquement update:page avec 1 (reset à la première page)
  • totalItems = 0 : la barre de pagination est masquée entièrement

Exemples

≤ 5 pages (toutes affichées) :
  Page 1/3 :   Prev(disabled) [1] 2 3 Next
  Page 2/5 :   Prev 1 [2] 3 4 5 Next
  Page 5/5 :   Prev 1 2 3 4 [5] Next(disabled)

> 5 pages (troncature 1 voisin) :
  Page 1/20 :  Prev(disabled) [1] 2 … 20 Next
  Page 2/20 :  Prev 1 [2] 3 … 20 Next
  Page 3/20 :  Prev 1 2 [3] 4 … 20 Next
  Page 4/20 :  Prev 1 … 3 [4] 5 … 20 Next
  Page 7/20 :  Prev 1 … 6 [7] 8 … 20 Next
  Page 18/20 : Prev 1 … 17 [18] 19 20 Next
  Page 19/20 : Prev 1 … 18 [19] 20 Next
  Page 20/20 : Prev 1 … 19 [20] Next(disabled)

En-têtes — logique du <th>

Chaque <th> vérifie si le slot #header-{key} est fourni :

  • Slot fourni → rend le slot (le consommateur y met un MalioInputText, MalioSelect, etc. avec le placeholder qui sert de label de colonne)
  • Slot absent → rend column.label en texte (font-semibold text-m-primary)

Pas de label séparé au-dessus du filtre. Le placeholder de l'input/select fait office de nom de colonne.

Composants Malio utilisés en interne

  • MalioSelect — sélecteur du nombre de lignes par page. Les perPageOptions sont mappés au format { label: string, value: number }[] attendu par MalioSelect (ex: { label: '10', value: 10 })
  • MalioButton variant="tertiary" — boutons Prev / Next

Exemple d'utilisation consommateur

<MalioDataTable
  :columns="[
    { key: 'nom', label: 'Nom' },
    { key: 'ville', label: 'Ville' },
    { key: 'montant', label: 'Montant' },
  ]"
  :items="data"
  :total-items="total"
  v-model:page="page"
  v-model:per-page="perPage"
  @row-click="router.push(`/contact/${$event.id}`)"
>
  <!-- Filtre texte  placeholder sert de label -->
  <template #header-nom>
    <MalioInputText v-model="filtres.nom" placeholder="Nom" />
  </template>

  <!-- Filtre select  placeholder sert de label -->
  <template #header-ville>
    <MalioSelect v-model="filtres.ville" :options="villes"
      empty-option-label="Ville" />
  </template>

  <!-- Pas de slot header pour "montant"  affiche "Montant" en texte -->

  <!-- Cellule custom -->
  <template #cell-montant="{ item }">
    <strong>{{ item.montant }} </strong>
  </template>
</MalioDataTable>

Accessibilité

  • <table> élément natif (sémantique table implicite)
  • <th scope="col"> sur chaque en-tête
  • Pagination dans un <nav aria-label="Pagination">
  • Numéros de page : éléments <button>, page courante avec aria-current="page"
  • Ellipsis : <span aria-hidden="true"> (ignoré par les lecteurs d'écran)
  • Boutons Prev/Next avec aria-label explicites ("Page précédente" / "Page suivante")
  • Lignes cliquables : tabindex="0" + gestion Enter/Space pour navigation clavier (pas de role="link" — on garde la sémantique <tr> native)

Styles

  • En-têtes : bg-m-surface, label en text-m-primary font-semibold
  • Bordures : border-m-border
  • Lignes hover : hover:bg-m-bg (si rowClickable)
  • Ligne cursor : cursor-pointer (si rowClickable)
  • Page active : bg-m-btn-primary text-white rounded
  • Boutons Prev/Next : MalioButton variant="tertiary"
  • Message vide : text-m-muted text-center, <td> avec colspan sur toute la largeur