docs : design du composant Modal
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,109 @@
|
|||||||
|
# Design — `MalioModal`
|
||||||
|
|
||||||
|
Date : 2026-05-26
|
||||||
|
Statut : validé
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
|
Ajouter un composant `MalioModal` à `@malio/layer-ui` : un dialogue modal centré sur fond
|
||||||
|
assombri. Périmètre initial = **shell générique seul** (pas de variante confirmation/alerte).
|
||||||
|
Le consommateur place ce qu'il veut dans les slots.
|
||||||
|
|
||||||
|
## Décisions clés
|
||||||
|
|
||||||
|
- **Composant autonome** : la Modal réimplémente sa propre logique en s'inspirant du Drawer,
|
||||||
|
**sans modifier le Drawer existant** (zéro risque de régression). Un éventuel refactor vers
|
||||||
|
un composable partagé Drawer/Modal pourra se faire plus tard.
|
||||||
|
- **Largeur unique + override** : une largeur par défaut (`max-w-md`), ajustable par le
|
||||||
|
consommateur via la prop `modalClass` (pas de prop `size`).
|
||||||
|
- **Footer fixe en bas** : header fixe en haut, body scrollable au milieu (`max-h-[85vh]`),
|
||||||
|
footer fixe en bas séparé par une bordure — structure modale classique (≠ Drawer où le footer
|
||||||
|
est dans la zone scrollable).
|
||||||
|
- **Front volontairement simple** pour cette première version.
|
||||||
|
|
||||||
|
## Emplacement & livrables
|
||||||
|
|
||||||
|
- `app/components/malio/modal/Modal.vue`
|
||||||
|
- `app/components/malio/modal/Modal.test.ts` (colocalisé, jsdom)
|
||||||
|
- Page playground `.playground/pages/composant/modal/modal.vue`
|
||||||
|
- Entrée nav : `{label: 'Modal', to: '/composant/modal/modal'}` ajoutée dans la section
|
||||||
|
**NAVIGATION** de `.playground/playground.nav.ts`, juste après le Drawer
|
||||||
|
- Story Histoire `app/story/modal.story.vue`
|
||||||
|
- Mise à jour `COMPONENTS.md` (API) et `CHANGELOG.md` (`### Added`)
|
||||||
|
|
||||||
|
## Structure (template)
|
||||||
|
|
||||||
|
```
|
||||||
|
Teleport to body
|
||||||
|
└ Transition (fade overlay + fade/scale du panneau)
|
||||||
|
└ div fixed inset-0 z-50 flex items-center justify-center p-4 ← centre la modal
|
||||||
|
├ div backdrop (bg-black/40, @click → si dismissable)
|
||||||
|
└ div panel (role=dialog, aria-modal, w-full max-w-md max-h-[85vh], flex flex-col)
|
||||||
|
├ header (slot #header + bouton fermer) ← shrink-0, rendu si slot OU showClose
|
||||||
|
├ body (slot par défaut) ← flex-1 overflow-y-auto
|
||||||
|
└ footer (slot #footer, bordure haute) ← shrink-0, rendu si slot #footer présent
|
||||||
|
```
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
### Props
|
||||||
|
|
||||||
|
| Prop | Type | Défaut | Rôle |
|
||||||
|
|------|------|--------|------|
|
||||||
|
| `id` | `string` | `''` | id du composant (sinon généré via `useId`) |
|
||||||
|
| `modelValue` | `boolean` | `undefined` | ouverture (contrôlé) ; fallback `localValue` interne si non fourni |
|
||||||
|
| `showClose` | `boolean` | `true` | affiche le bouton de fermeture (croix) dans le header |
|
||||||
|
| `dismissable` | `boolean` | `true` | clic sur le backdrop ferme la modal |
|
||||||
|
| `closeOnEscape` | `boolean` | `true` | touche Échap ferme la modal |
|
||||||
|
| `ariaLabel` | `string` | `''` | label ARIA si pas de slot header |
|
||||||
|
| `modalClass` | `string` | `''` | override du panneau (ex. largeur `max-w-lg`) |
|
||||||
|
| `overlayClass` | `string` | `''` | override du backdrop |
|
||||||
|
| `headerClass` | `string` | `''` | override du header |
|
||||||
|
| `bodyClass` | `string` | `''` | override du body |
|
||||||
|
| `footerClass` | `string` | `''` | override du footer |
|
||||||
|
|
||||||
|
Mêmes props que le Drawer, **sans `side`** ; `drawerClass` → `modalClass`.
|
||||||
|
|
||||||
|
### Events
|
||||||
|
|
||||||
|
- `update:modelValue(value: boolean)` — pour le `v-model`
|
||||||
|
- `close()` — émis à chaque fermeture (croix, backdrop, Échap, fermeture programmatique)
|
||||||
|
|
||||||
|
### Slots
|
||||||
|
|
||||||
|
- `#header` — contenu du header (titre…). Si absent **et** `showClose=false`, header non rendu.
|
||||||
|
- *(défaut)* — corps de la modal (zone scrollable)
|
||||||
|
- `#footer` — pied (boutons d'action). Rendu uniquement si le slot est fourni.
|
||||||
|
|
||||||
|
## Comportements repris du Drawer
|
||||||
|
|
||||||
|
- **Teleport** vers `<body>`.
|
||||||
|
- **Focus-trap** : focus initial sur le 1er élément focusable (sinon le panneau) ; boucle
|
||||||
|
Tab / Shift+Tab ; restauration du focus précédent à la fermeture.
|
||||||
|
- **Scroll-lock partagé** : compteur module-level (`openModalCount`) — `overflow:hidden` sur
|
||||||
|
`<body>` tant qu'au moins une instance est ouverte, libéré par la dernière (gère aussi
|
||||||
|
`onBeforeUnmount`).
|
||||||
|
- **Pattern contrôlé / non-contrôlé** : `isControlled = computed(() => modelValue !== undefined)`
|
||||||
|
avec `localValue` en fallback ; `isRendered` pour démonter après la transition de sortie.
|
||||||
|
- **`defineOptions({ name: 'MalioModal', inheritAttrs: false })`** + `v-bind="attrs"` sur le
|
||||||
|
conteneur.
|
||||||
|
- **Transition** : fade de l'overlay + fade léger **et scale** (`scale-95 → scale-100`) du
|
||||||
|
panneau (remplace le translate latéral du Drawer).
|
||||||
|
|
||||||
|
## Accessibilité
|
||||||
|
|
||||||
|
`role="dialog"`, `aria-modal="true"`, `aria-labelledby` vers l'id du header si présent (sinon
|
||||||
|
`aria-label`), bouton fermer avec `aria-label="Fermer"`.
|
||||||
|
|
||||||
|
## Tests (Vitest + @vue/test-utils, jsdom, colocalisés)
|
||||||
|
|
||||||
|
- Rendu conditionnel (fermé → non rendu ; ouvert → panneau + backdrop).
|
||||||
|
- `v-model` / contrôlé vs non-contrôlé.
|
||||||
|
- Fermeture : croix, backdrop (selon `dismissable`), Échap (selon `closeOnEscape`) ; events
|
||||||
|
`update:modelValue(false)` + `close`.
|
||||||
|
- Slots header / défaut / footer (footer rendu seulement si fourni ; header rendu si slot OU
|
||||||
|
`showClose`).
|
||||||
|
- Accessibilité : `role`, `aria-modal`, `aria-labelledby`/`aria-label`.
|
||||||
|
- Focus-trap : focus initial, boucle Tab/Shift+Tab.
|
||||||
|
- Scroll-lock : `overflow:hidden` à l'ouverture, libéré à la fermeture (et avec instances
|
||||||
|
multiples).
|
||||||
Reference in New Issue
Block a user