502460c6ab
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5.1 KiB
5.1 KiB
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 propmodalClass(pas de propsize). - 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.vueapp/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) etCHANGELOG.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 lev-modelclose()— émis à chaque fermeture (croix, backdrop, Échap, fermeture programmatique)
Slots
#header— contenu du header (titre…). Si absent etshowClose=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:hiddensur<body>tant qu'au moins une instance est ouverte, libéré par la dernière (gère aussionBeforeUnmount). - Pattern contrôlé / non-contrôlé :
isControlled = computed(() => modelValue !== undefined)aveclocalValueen fallback ;isRenderedpour 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 (seloncloseOnEscape) ; eventsupdate: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).