Files
malio-layer-ui/docs/superpowers/specs/2026-05-21-drawer-redesign-design.md
tristan f3e298e03b [#MUI-35] Refonte du composant drawer (#49)
| Numéro du ticket | Titre du ticket |
|------------------|-----------------|
|                  |                 |

## Description de la PR

## Modification du .env

## Check list

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

Reviewed-on: #49
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
2026-05-21 15:17:58 +00:00

6.6 KiB

Refonte du composant <MalioDrawer> — Design

Ticket : MUI-35 — Revoir le design du composant Drawer Date : 2026-05-21 Statut : design validé, à implémenter

Contexte & problème

Le <MalioDrawer> 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 <MalioDrawer> (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. L'icône de fermeture est mdi:cancel-bold (on conserve l'icône actuelle ; c'est le test qui sera adapté).
  • 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 <Teleport to="body"> + <Transition> + isRendered (démontage après l'animation de sortie).

Migration (breaking)

Avant Après
title="Titre" <template #header><h2>Titre</h2></template> (ou composant de titre Malio)
<MalioDrawer>contenu</MalioDrawer> 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).