| 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>
193 lines
7.6 KiB
Markdown
193 lines
7.6 KiB
Markdown
# 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
|