From 906320080defdd7e5e7b362cc7075f74753064f8 Mon Sep 17 00:00:00 2001 From: tristan Date: Thu, 21 May 2026 16:30:21 +0200 Subject: [PATCH] =?UTF-8?q?docs=20:=20plan=20d'impl=C3=A9mentation=20refon?= =?UTF-8?q?te=20du=20Drawer=20(MUI-35)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plans/2026-05-21-drawer-redesign.md | 1117 +++++++++++++++++ 1 file changed, 1117 insertions(+) create mode 100644 docs/superpowers/plans/2026-05-21-drawer-redesign.md diff --git a/docs/superpowers/plans/2026-05-21-drawer-redesign.md b/docs/superpowers/plans/2026-05-21-drawer-redesign.md new file mode 100644 index 0000000..3f7c29a --- /dev/null +++ b/docs/superpowers/plans/2026-05-21-drawer-redesign.md @@ -0,0 +1,1117 @@ +# 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`