From c69066281cff0f7bf608c4c4c0322155a3abb470 Mon Sep 17 00:00:00 2001 From: tristan Date: Thu, 21 May 2026 16:22:33 +0200 Subject: [PATCH] docs : design refonte du composant Drawer (MUI-35) --- .../2026-05-21-drawer-redesign-design.md | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-21-drawer-redesign-design.md diff --git a/docs/superpowers/specs/2026-05-21-drawer-redesign-design.md b/docs/superpowers/specs/2026-05-21-drawer-redesign-design.md new file mode 100644 index 0000000..80c97b3 --- /dev/null +++ b/docs/superpowers/specs/2026-05-21-drawer-redesign-design.md @@ -0,0 +1,144 @@ +# Refonte du composant `` — Design + +> Ticket : MUI-35 — Revoir le design du composant Drawer +> Date : 2026-05-21 +> Statut : design validé, à implémenter + +## Contexte & problème + +Le `` actuel fait le strict minimum et ne tient pas la comparaison +avec les drawers des libs modernes (shadcn/Sheet, PrimeVue, Element Plus, Nuxt UI) : + +- glisse **uniquement depuis la droite**, pas de choix de côté ; +- **un seul slot** (le contenu), pas de header/footer structurés ; +- **aucune accessibilité réelle** : pas de focus-trap, pas de restitution du focus, + pas de fermeture au clavier (Échap) ; +- **pas de scroll-lock** du body quand le drawer est ouvert. + +Objectif : refondre le composant en gardant l'esprit du layer Malio +(hand-rollé, 1 composant `.vue`, props communes, `twMerge`), sans introduire de +dépendance ni refondre les autres composants. + +## Décisions structurantes + +- **Hand-rollé**, pas de dépendance type Reka UI. Cohérence avec le reste du layer. +- **Un seul composant** `` (props + slots). Pas de primitives composables. +- **Breaking change assumé** → bump de version **majeure** via semantic-release. + Les apps consommatrices migreront (cf. section Migration). +- Périmètre : **le drawer uniquement**. Les autres composants ne bougent pas. + +## API + +### Slots + +| Slot | Rôle | +|------|------| +| `#header` | Contenu d'en-tête (titre + ce que veut le consommateur). **Aucune prop `title`.** | +| _défaut_ | Le body, dans la zone scrollable. | +| `#footer` | Rendu **dans la zone scrollable**, juste après le body. **Aucune classe de positionnement imposée.** | + +### Props + +| Prop | Type | Défaut | Rôle | +|------|------|--------|------| +| `modelValue` | `boolean` | `undefined` | v-model d'ouverture (pattern contrôlé/non-contrôlé Malio) | +| `id` | `string` | `''` (auto) | id du composant | +| `side` | `'right' \| 'left'` | `'right'` | côté d'apparition | +| `showClose` | `boolean` | `true` | affiche le bouton de fermeture (croix) | +| `dismissable` | `boolean` | `true` | clic sur le backdrop ferme le drawer | +| `closeOnEscape` | `boolean` | `true` | touche Échap ferme le drawer | +| `ariaLabel` | `string` | `''` | nom accessible de secours quand `#header` est absent | +| `drawerClass` | `string` | `''` | override du panneau (largeur réglée ici, ex. `max-w-2xl`) | +| `overlayClass` | `string` | `''` | override du backdrop | +| `headerClass` | `string` | `''` | override de la barre header | +| `bodyClass` | `string` | `''` | override de la zone scrollable | +| `footerClass` | `string` | `''` | override du wrapper du `#footer` | + +> **Largeur/hauteur** : pas de prop `size`. Tout se règle via `drawerClass` +> (comme aujourd'hui). + +### Emits + +| Event | Payload | Quand | +|-------|---------|-------| +| `update:modelValue` | `boolean` | ouverture/fermeture | +| `close` | — | à la fermeture (pratique pour la logique appelante) | + +## Layout + +``` +┌─────────────────────────────┐ +│ [slot #header] [ ✕ ] │ ← barre header (rendue si #header OU showClose) +├─────────────────────────────┤ +│ │ +│ slot par défaut (body) │ ← zone scrollable (flex-1, overflow-y-auto) +│ [slot #footer] │ ← rendu juste après le body, dans le même scroll, +│ │ SANS classe de position par défaut +└─────────────────────────────┘ +``` + +- La **barre header** n'est rendue que si le slot `#header` est fourni **ou** si + `showClose` est vrai. Le bouton croix vit dans cette barre, à droite. +- La **zone scrollable** (`flex-1 overflow-y-auto`) contient le slot par défaut + puis, si fourni, le wrapper `#footer`. +- Le **`#footer`** n'a **aucune** classe `sticky`/`flex-shrink-0`/position. Par défaut + il scrolle avec le contenu. Pour le coller en bas, le consommateur passe + `footer-class="sticky bottom-0 bg-white"`. + +## Comportements (les manques actuels corrigés) + +1. **Échap** ferme le drawer si `closeOnEscape` (listener `keydown` global, ajouté à + l'ouverture, retiré à la fermeture). +2. **Scroll-lock du body** : `overflow: hidden` sur `document.body` à l'ouverture, + restauré à la fermeture. +3. **Focus-trap** : à l'ouverture, focus sur le premier élément focusable du panneau + (ou le panneau lui-même) ; `Tab`/`Shift+Tab` bouclent à l'intérieur du panneau. +4. **Restitution du focus** : mémoriser `document.activeElement` à l'ouverture, le + restaurer à la fermeture. +5. **ARIA** : + - `role="dialog"`, `aria-modal="true"` sur le panneau ; + - `aria-labelledby` pointant sur l'id du wrapper `#header` **si** le slot est fourni ; + - sinon `aria-label` = prop `ariaLabel` (fallback accessible). + +## Transition + +- Backdrop : fondu (`opacity`). +- Panneau : translation selon `side` — + - `right` : `translateX(100%)` → `0` ; + - `left` : `translateX(-100%)` → `0`. +- Conserver le pattern actuel `` + `` + + `isRendered` (démontage après l'animation de sortie). + +## Migration (breaking) + +| Avant | Après | +|-------|-------| +| `title="Titre"` | `` (ou composant de titre Malio) | +| `contenu` | inchangé (slot par défaut = body) | +| `drawer-class` | inchangé | +| `show-close` | inchangé | +| _(nouveau)_ | `side`, `dismissable`, `closeOnEscape`, `ariaLabel`, slots `#header`/`#footer` | + +Les défauts des nouvelles props reproduisent au plus près le comportement actuel +(`side="right"`, `showClose=true`, `dismissable=true`). + +## Tests (Vitest + @vue/test-utils, jsdom) + +À couvrir, en plus des tests de rendu/props/emits existants : + +- rendu des 3 slots (`#header`, défaut, `#footer`) ; +- `side` left/right → classes/transition attendues ; +- `showClose` toggle la croix ; clic croix → ferme + emit ; +- `dismissable` : clic backdrop ferme / ne ferme pas ; +- `closeOnEscape` : Échap ferme / ne ferme pas ; +- scroll-lock : `body` `overflow:hidden` à l'ouverture, restauré à la fermeture ; +- focus-trap : focus initial dans le panneau ; restitution au déclencheur ; +- ARIA : `aria-labelledby` quand `#header`, `aria-label` sinon ; +- pattern contrôlé/non-contrôlé. + +## Hors périmètre (YAGNI) + +- côtés `top`/`bottom` (sheets) — extensible plus tard via `side` ; +- prop `size` sémantique — `drawerClass` suffit ; +- hook `before-close` ; +- empilement de plusieurs drawers (un seul scroll-lock géré simplement).