fix: rendre le footer du Drawer hors zone scrollable (épinglé en bas)
Le slot #footer était rendu à l'intérieur du body overflow-y-auto, ce qui faisait courir la scrollbar sur toute la hauteur, derrière le footer. Il est désormais frère du body (comme MalioModal) : seul le body défile et le footer reste fixé en bas. Tests, story, pages playground et doc alignés. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -33,7 +33,7 @@ const drawerNoDismiss = ref(false)
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-lg border p-6">
|
<div class="rounded-lg border p-6">
|
||||||
<h2 class="mb-6 text-xl font-bold">Avec footer collant</h2>
|
<h2 class="mb-6 text-xl font-bold">Avec footer d'actions</h2>
|
||||||
<MalioButton label="Ouvrir le formulaire" variant="tertiary" @click="drawerForm = true" />
|
<MalioButton label="Ouvrir le formulaire" variant="tertiary" @click="drawerForm = true" />
|
||||||
<MalioDrawer v-model="drawerForm" drawer-class="max-w-lg">
|
<MalioDrawer v-model="drawerForm" drawer-class="max-w-lg">
|
||||||
<template #header>
|
<template #header>
|
||||||
@@ -45,32 +45,27 @@ const drawerNoDismiss = ref(false)
|
|||||||
<MalioInputText label="Email" />
|
<MalioInputText label="Email" />
|
||||||
</div>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="sticky bottom-0 flex gap-3 bg-white py-4">
|
<MalioButton label="Annuler" variant="secondary" button-class="flex-1" @click="drawerForm = false" />
|
||||||
<MalioButton label="Annuler" variant="secondary" button-class="flex-1" @click="drawerForm = false" />
|
<MalioButton label="Enregistrer" button-class="flex-1" @click="drawerForm = false" />
|
||||||
<MalioButton label="Enregistrer" button-class="flex-1" @click="drawerForm = false" />
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</MalioDrawer>
|
</MalioDrawer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-lg border p-6">
|
<div class="rounded-lg border p-6">
|
||||||
<h2 class="mb-6 text-xl font-bold">Avec footer fixed bottom</h2>
|
<h2 class="mb-6 text-xl font-bold">Footer fixe avec contenu long</h2>
|
||||||
<MalioButton label="Ouvrir (footer fixe)" variant="tertiary" @click="drawerFixedFooter = true" />
|
<MalioButton label="Ouvrir (contenu long)" variant="tertiary" @click="drawerFixedFooter = true" />
|
||||||
<MalioDrawer v-model="drawerFixedFooter">
|
<MalioDrawer v-model="drawerFixedFooter">
|
||||||
<template #header>
|
<template #header>
|
||||||
<h2 class="text-[24px] font-bold text-black">Conditions</h2>
|
<h2 class="text-[24px] font-bold text-black">Conditions</h2>
|
||||||
</template>
|
</template>
|
||||||
<!-- pb-24 : laisse la place au footer fixe qui sort du flux et recouvrirait le bas du contenu -->
|
<!-- Pas de hack : le footer est hors zone scrollable, seul le body défile -->
|
||||||
<div class="flex flex-col gap-4 pb-24">
|
<div class="flex flex-col gap-4">
|
||||||
<p v-for="n in 12" :key="n" class="text-m-text">
|
<p v-for="n in 12" :key="n" class="text-m-text">
|
||||||
Paragraphe {{ n }} — contenu long pour forcer le scroll et montrer que le footer reste fixé en bas du viewport.
|
Paragraphe {{ n }} — contenu long pour forcer le scroll et montrer que seul le body défile, le footer restant fixé en bas.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<!-- fixed : positionné par rapport au viewport ; w-full max-w-md cale la largeur sur le drawer droite par défaut -->
|
<MalioButton label="Accepter" button-class="w-full" @click="drawerFixedFooter = false" />
|
||||||
<div class="fixed bottom-0 right-0 w-full max-w-md border-t border-m-border bg-white px-5 py-4">
|
|
||||||
<MalioButton label="Accepter" button-class="w-full" @click="drawerFixedFooter = false" />
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</MalioDrawer>
|
</MalioDrawer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
side="right"
|
side="right"
|
||||||
drawer-class="max-w-[450px]"
|
drawer-class="max-w-[450px]"
|
||||||
body-class="p-0"
|
body-class="p-0"
|
||||||
footer-class="sticky bottom-0 flex justify-between gap-4 bg-white px-5 py-7"
|
footer-class="justify-between gap-4 py-7"
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<h2 class="text-[24px] font-bold uppercase">Filtres</h2>
|
<h2 class="text-[24px] font-bold uppercase">Filtres</h2>
|
||||||
|
|||||||
@@ -41,5 +41,6 @@ Liste des évolutions de la librairie Malio layer UI
|
|||||||
* [#MUI-35] Refonte du composant drawer : slots `#header`/`#footer`, prop `side` (droite/gauche), `dismissable`, `closeOnEscape`, classes d'override, focus-trap, scroll-lock et fermeture au clavier. **Breaking** : la prop `title` est remplacée par le slot `#header`.
|
* [#MUI-35] Refonte du composant drawer : slots `#header`/`#footer`, prop `side` (droite/gauche), `dismissable`, `closeOnEscape`, classes d'override, focus-trap, scroll-lock et fermeture au clavier. **Breaking** : la prop `title` est remplacée par le slot `#header`.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
* Drawer : le slot `#footer` est désormais rendu hors de la zone scrollable (épinglé en bas, comme la modal) ; seul le body défile et la scrollbar ne s'étend plus derrière le footer
|
||||||
* Hauteur des boutons de pagination du datatable alignée sur le select (40px)
|
* Hauteur des boutons de pagination du datatable alignée sur le select (40px)
|
||||||
* Distribution de `tailwind.config.ts` aux projets consommateurs avec paths `content` absolus
|
* Distribution de `tailwind.config.ts` aux projets consommateurs avec paths `content` absolus
|
||||||
|
|||||||
+6
-8
@@ -813,14 +813,14 @@ Panneau latéral (drawer) qui s'ouvre depuis la droite ou la gauche avec backdro
|
|||||||
| `overlayClass` | `string` | `''` | Classes CSS backdrop (twMerge) |
|
| `overlayClass` | `string` | `''` | Classes CSS backdrop (twMerge) |
|
||||||
| `headerClass` | `string` | `''` | Classes CSS barre header (twMerge) |
|
| `headerClass` | `string` | `''` | Classes CSS barre header (twMerge) |
|
||||||
| `bodyClass` | `string` | `''` | Classes CSS zone scrollable (twMerge) |
|
| `bodyClass` | `string` | `''` | Classes CSS zone scrollable (twMerge) |
|
||||||
| `footerClass` | `string` | `''` | Classes CSS wrapper du footer (aucune position imposée) |
|
| `footerClass` | `string` | `''` | Classes CSS du footer fixe (twMerge) |
|
||||||
|
|
||||||
**Events :** `update:modelValue(value: boolean)`, `close()`
|
**Events :** `update:modelValue(value: boolean)`, `close()`
|
||||||
|
|
||||||
**Slots :**
|
**Slots :**
|
||||||
- `header` — en-tête (titre, etc.). S'il est absent et que `showClose` est `true`, seule la croix est affichée.
|
- `header` — en-tête (titre, etc.), fixe en haut. S'il est absent et que `showClose` est `true`, seule la croix est affichée.
|
||||||
- `default` — contenu (zone scrollable).
|
- `default` — contenu (zone scrollable : seul le body défile).
|
||||||
- `footer` — rendu dans la zone scrollable, sans positionnement imposé : le consommateur choisit (`sticky bottom-0`, `fixed`, ou rien).
|
- `footer` — actions (boutons). Rendu en bas du panneau, fixe, hors de la zone scrollable. N'apparaît que si le slot est fourni.
|
||||||
|
|
||||||
```vue
|
```vue
|
||||||
<MalioDrawer v-model="isOpen">
|
<MalioDrawer v-model="isOpen">
|
||||||
@@ -836,14 +836,12 @@ Panneau latéral (drawer) qui s'ouvre depuis la droite ou la gauche avec backdro
|
|||||||
<p>Drawer large depuis la gauche</p>
|
<p>Drawer large depuis la gauche</p>
|
||||||
</MalioDrawer>
|
</MalioDrawer>
|
||||||
|
|
||||||
<!-- Footer collé en bas (le consommateur applique le positionnement) -->
|
<!-- Footer d'actions (fixe en bas, hors zone scrollable) -->
|
||||||
<MalioDrawer v-model="isOpen">
|
<MalioDrawer v-model="isOpen">
|
||||||
<template #header><h2>Formulaire</h2></template>
|
<template #header><h2>Formulaire</h2></template>
|
||||||
<MalioInputText label="Nom" />
|
<MalioInputText label="Nom" />
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="sticky bottom-0 bg-white py-4">
|
<MalioButton label="Enregistrer" button-class="w-full" @click="isOpen = false" />
|
||||||
<MalioButton label="Enregistrer" button-class="w-full" @click="isOpen = false" />
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</MalioDrawer>
|
</MalioDrawer>
|
||||||
|
|
||||||
|
|||||||
@@ -152,12 +152,13 @@ describe('MalioDrawer', () => {
|
|||||||
expect(wrapper.find('[data-test="header"]').classes()).toContain('bg-m-primary')
|
expect(wrapper.find('[data-test="header"]').classes()).toContain('bg-m-primary')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders the #footer slot inside the body (scrollable zone)', () => {
|
it('renders the #footer slot in a footer pinned below the body', () => {
|
||||||
const wrapper = mountComponent(
|
const wrapper = mountComponent(
|
||||||
{ modelValue: true },
|
{ modelValue: true },
|
||||||
{ footer: '<button data-test="save">Enregistrer</button>' },
|
{ footer: '<button data-test="save">Enregistrer</button>' },
|
||||||
)
|
)
|
||||||
expect(wrapper.find('[data-test="body"] [data-test="footer"] [data-test="save"]').exists()).toBe(true)
|
expect(wrapper.find('[data-test="body"] [data-test="footer"]').exists()).toBe(false)
|
||||||
|
expect(wrapper.find('[data-test="footer"] [data-test="save"]').exists()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('does not render the footer wrapper when no #footer slot', () => {
|
it('does not render the footer wrapper when no #footer slot', () => {
|
||||||
@@ -170,14 +171,12 @@ describe('MalioDrawer', () => {
|
|||||||
expect(wrapper.find('[data-test="body"]').classes()).toContain('px-10')
|
expect(wrapper.find('[data-test="body"]').classes()).toContain('px-10')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('applies footerClass to the footer wrapper', () => {
|
it('applies footerClass to the footer', () => {
|
||||||
const wrapper = mountComponent(
|
const wrapper = mountComponent(
|
||||||
{ modelValue: true, footerClass: 'sticky bottom-0' },
|
{ modelValue: true, footerClass: 'justify-end' },
|
||||||
{ footer: '<span>pied</span>' },
|
{ footer: '<span>pied</span>' },
|
||||||
)
|
)
|
||||||
const footer = wrapper.find('[data-test="footer"]')
|
expect(wrapper.find('[data-test="footer"]').classes()).toContain('justify-end')
|
||||||
expect(footer.classes()).toContain('sticky')
|
|
||||||
expect(footer.classes()).toContain('bottom-0')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('aligns to the right by default', () => {
|
it('aligns to the right by default', () => {
|
||||||
|
|||||||
@@ -64,13 +64,13 @@
|
|||||||
data-test="body"
|
data-test="body"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
<div
|
</div>
|
||||||
v-if="$slots.footer"
|
<div
|
||||||
:class="footerClass"
|
v-if="$slots.footer"
|
||||||
data-test="footer"
|
:class="twMerge('flex shrink-0 items-center gap-3 px-5 py-4', footerClass)"
|
||||||
>
|
data-test="footer"
|
||||||
<slot name="footer" />
|
>
|
||||||
</div>
|
<slot name="footer" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ const showNoDismiss = ref(false)
|
|||||||
</div>
|
</div>
|
||||||
</Variant>
|
</Variant>
|
||||||
|
|
||||||
<Variant title="Avec footer collant">
|
<Variant title="Avec footer d'actions">
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
<button
|
<button
|
||||||
class="rounded bg-m-btn-primary px-4 py-2 text-white"
|
class="rounded bg-m-btn-primary px-4 py-2 text-white"
|
||||||
@@ -62,9 +62,7 @@ const showNoDismiss = ref(false)
|
|||||||
<MalioInputText label="Prénom" />
|
<MalioInputText label="Prénom" />
|
||||||
</div>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="sticky bottom-0 flex gap-3 bg-white py-4">
|
<MalioButton label="Enregistrer" button-class="flex-1" @click="showForm = false" />
|
||||||
<MalioButton label="Enregistrer" button-class="flex-1" @click="showForm = false" />
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</MalioDrawer>
|
</MalioDrawer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user