feat: ajout du composant datatable (#28)
All checks were successful
Release / release (push) Successful in 1m5s
All checks were successful
Release / release (push) Successful in 1m5s
| 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é Co-authored-by: kevin <kevin@yuno.malio.fr> Co-authored-by: Kevin Boudet <kevin@yuno.malio.fr> Reviewed-on: #28 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
This commit was merged in pull request #28.
This commit is contained in:
192
docs/superpowers/specs/2026-03-24-datatable-design.md
Normal file
192
docs/superpowers/specs/2026-03-24-datatable-design.md
Normal file
@@ -0,0 +1,192 @@
|
||||
# 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
|
||||
|
||||
```ts
|
||||
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
|
||||
|
||||
```vue
|
||||
<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
|
||||
Reference in New Issue
Block a user