| 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>
6.6 KiB
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 viadrawerClass(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
#headerest fourni ou sishowCloseest vrai. Le bouton croix vit dans cette barre, à droite. L'icône de fermeture estmdi: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
#footern'a aucune classesticky/flex-shrink-0/position. Par défaut il scrolle avec le contenu. Pour le coller en bas, le consommateur passefooter-class="sticky bottom-0 bg-white".
Comportements (les manques actuels corrigés)
- Échap ferme le drawer si
closeOnEscape(listenerkeydownglobal, ajouté à l'ouverture, retiré à la fermeture). - Scroll-lock du body :
overflow: hiddensurdocument.bodyà l'ouverture, restauré à la fermeture. - Focus-trap : à l'ouverture, focus sur le premier élément focusable du panneau
(ou le panneau lui-même) ;
Tab/Shift+Tabbouclent à l'intérieur du panneau. - Restitution du focus : mémoriser
document.activeElementà l'ouverture, le restaurer à la fermeture. - ARIA :
role="dialog",aria-modal="true"sur le panneau ;aria-labelledbypointant sur l'id du wrapper#headersi le slot est fourni ;- sinon
aria-label= propariaLabel(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) ; sideleft/right → classes/transition attendues ;showClosetoggle la croix ; clic croix → ferme + emit ;dismissable: clic backdrop ferme / ne ferme pas ;closeOnEscape: Échap ferme / ne ferme pas ;- scroll-lock :
bodyoverflow:hiddenà l'ouverture, restauré à la fermeture ; - focus-trap : focus initial dans le panneau ; restitution au déclencheur ;
- ARIA :
aria-labelledbyquand#header,aria-labelsinon ; - pattern contrôlé/non-contrôlé.
Hors périmètre (YAGNI)
- côtés
top/bottom(sheets) — extensible plus tard viaside; - prop
sizesémantique —drawerClasssuffit ; - hook
before-close; - empilement de plusieurs drawers (un seul scroll-lock géré simplement).