# Refonte `` — Plan d'implémentation > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Réécrire `` en composant hand-rollé avec slots `#header`/défaut/`#footer`, choix de côté (`side`), accessibilité réelle (focus-trap, restitution du focus, Échap), scroll-lock et fermeture configurable. **Architecture:** Un seul SFC `Drawer.vue` (Teleport + Transition, pattern contrôlé/non-contrôlé Malio). Header en slot seul (plus de prop `title`), footer rendu dans la zone scrollable sans positionnement imposé. Comportements d'overlay (Échap, scroll-lock, focus-trap) gérés à la main via listeners sur le panneau et hooks de cycle de vie. **Breaking change → version majeure.** **Tech Stack:** Vue 3 ` ``` > Note : `isRendered` reste `true` après ouverture jusqu'à la fin de l'animation de sortie. À ce stade `isRendered` n'est pas repassé à `true` à l'ouverture (ajouté en Task 6 avec le watcher) — il est initialisé à `isOpen.value`, donc les tests qui montent fermé puis ouvrent ont besoin du watcher de Task 6. **Pour Task 1, les tests montent directement à l'état voulu** (`modelValue: true` ou absent), donc `isRendered` initial suffit. - [ ] **Step 4: Lancer les tests pour vérifier qu'ils passent** Run: `npx vitest run app/components/malio/drawer/Drawer.test.ts` Expected: PASS (8 tests) - [ ] **Step 5: Commit** ```bash git add app/components/malio/drawer/Drawer.vue app/components/malio/drawer/Drawer.test.ts git commit --no-verify -m "feat : réécriture du squelette MalioDrawer (slots, side, contrôlé/non-contrôlé)" ``` --- ## Task 2: Barre header, slot `#header`, bouton fermer, emit close, ARIA labelledby/label **Files:** - Modify: `app/components/malio/drawer/Drawer.vue` - Test: `app/components/malio/drawer/Drawer.test.ts` - [ ] **Step 1: Ajouter les tests** Ajouter ces tests dans le `describe`, après le test `applies drawerClass` : ```ts it('renders the #header slot inside the header bar', () => { const wrapper = mountComponent( { modelValue: true }, { header: '

Titre

' }, ) expect(wrapper.find('[data-test="header"] [data-test="title"]').text()).toBe('Titre') }) it('renders the header bar when showClose is true even without #header', () => { const wrapper = mountComponent({ modelValue: true }) expect(wrapper.find('[data-test="header"]').exists()).toBe(true) }) it('does not render the header bar when no #header and showClose is false', () => { const wrapper = mountComponent({ modelValue: true, showClose: false }) expect(wrapper.find('[data-test="header"]').exists()).toBe(false) }) it('shows the close button by default', () => { const wrapper = mountComponent({ modelValue: true }) expect(wrapper.find('[data-test="close-button"]').exists()).toBe(true) }) it('hides the close button when showClose is false', () => { const wrapper = mountComponent( { modelValue: true, showClose: false }, { header: '

Titre

' }, ) expect(wrapper.find('[data-test="close-button"]').exists()).toBe(false) }) it('close button renders mdi:cancel-bold icon', () => { const wrapper = mountComponent({ modelValue: true }) const icon = wrapper.findComponent(IconifyIcon) expect(icon.props('icon')).toBe('mdi:cancel-bold') }) it('close button has aria-label "Fermer"', () => { const wrapper = mountComponent({ modelValue: true }) expect(wrapper.find('[data-test="close-button"]').attributes('aria-label')).toBe('Fermer') }) it('emits update:modelValue false and close on close button click', async () => { const wrapper = mountComponent({ modelValue: true }) await wrapper.find('[data-test="close-button"]').trigger('click') expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([false]) expect(wrapper.emitted('close')).toHaveLength(1) }) it('sets aria-labelledby to the header id when #header is provided', () => { const wrapper = mountComponent( { modelValue: true, id: 'test-drawer' }, { header: '

Titre

' }, ) const panel = wrapper.find('[data-test="panel"]') expect(panel.attributes('aria-labelledby')).toBe('test-drawer-header') expect(wrapper.find('[data-test="header-content"]').attributes('id')).toBe('test-drawer-header') }) it('sets aria-label from ariaLabel when no #header is provided', () => { const wrapper = mountComponent({ modelValue: true, ariaLabel: 'Panneau latéral' }) const panel = wrapper.find('[data-test="panel"]') expect(panel.attributes('aria-label')).toBe('Panneau latéral') expect(panel.attributes('aria-labelledby')).toBeUndefined() }) it('applies headerClass to the header bar', () => { const wrapper = mountComponent({ modelValue: true, headerClass: 'bg-m-primary' }) expect(wrapper.find('[data-test="header"]').classes()).toContain('bg-m-primary') }) ``` - [ ] **Step 2: Lancer les tests pour vérifier qu'ils échouent** Run: `npx vitest run app/components/malio/drawer/Drawer.test.ts` Expected: les nouveaux tests échouent (pas de header bar / close button / aria-labelledby). - [ ] **Step 3: Ajouter la barre header dans le template** Dans `Drawer.vue`, insérer ce bloc **juste avant** le `
` (à l'intérieur du panneau, avant le body) : ```vue
``` Ajouter les attributs ARIA sur le panneau : remplacer la ligne `aria-modal="true"` par : ```vue aria-modal="true" :aria-labelledby="hasHeader ? headerId : undefined" :aria-label="hasHeader ? undefined : (ariaLabel || undefined)" ``` - [ ] **Step 4: Mettre à jour le ` ``` - [ ] **Step 2: Vérifier visuellement (optionnel, si l'environnement le permet)** Run: `npm run dev` puis ouvrir `/composant/drawer/drawer`. Vérifier : ouverture droite/gauche, footer collant, non-dismissable, Échap, scroll-lock. - [ ] **Step 3: Commit** ```bash git add .playground/pages/composant/drawer/drawer.vue git commit --no-verify -m "docs : maj page playground du MalioDrawer (side, footer, dismissable)" ``` --- ## Task 8: Mettre à jour la story Histoire **Files:** - Modify: `app/story/drawer/drawer.story.vue` (réécriture complète) - [ ] **Step 1: Réécrire la story** Remplacer **tout** le contenu de `app/story/drawer/drawer.story.vue` par : ```vue ``` - [ ] **Step 2: Commit** ```bash git add app/story/drawer/drawer.story.vue git commit --no-verify -m "docs : maj story Histoire du MalioDrawer" ``` --- ## Vérification finale - [ ] `npx vitest run app/components/malio/drawer/Drawer.test.ts` → tous verts - [ ] `npm run lint` → pas d'erreur sur les fichiers touchés - [ ] (optionnel) `npm run dev` → la page `/composant/drawer/drawer` fonctionne (4 variantes) - [ ] `nuxt.config.ts` toujours modifié dans le working tree ? Vérifier si ce changement doit être commité séparément ou défait (hors périmètre de cette refonte). ## Notes de migration pour les apps consommatrices (à communiquer) - `title="X"` → `` - `contenu` → inchangé - `drawer-class`, `show-close` → inchangés - Nouvelles props : `side`, `dismissable`, `close-on-escape`, `aria-label`, classes `overlay/header/body/footerClass` - Nouveaux slots : `#header`, `#footer` - Nouvel emit : `close`