# 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. 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 `` + `` + `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).