Contenu du drawer. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+Ce drawer n'a pas de bouton fermer. Cliquez sur le backdrop pour fermer.
+Ce drawer utilise une largeur personnalisée via drawerClass.
+
```
+
+---
+
+## MalioDrawer
+
+Panneau latéral (drawer) qui s'ouvre depuis la droite avec backdrop semi-transparent.
+
+| Prop | Type | Défaut | Description |
+|------|------|--------|-------------|
+| `id` | `string` | auto | Identifiant HTML |
+| `modelValue` | `boolean` | `undefined` | État ouvert/fermé (v-model) |
+| `title` | `string` | `''` | Titre affiché dans le header |
+| `showClose` | `boolean` | `true` | Afficher le bouton de fermeture (croix) |
+| `drawerClass` | `string` | `''` | Classes CSS panneau (twMerge) |
+
+**Events :** `update:modelValue(value: boolean)`
+**Slots :** `default` (contenu du drawer)
+
+```vue
+Contenu du drawer
+Fermeture uniquement via backdrop
+Drawer plus large
+Contenu du drawer
' }, + ) + expect(wrapper.find('[data-test="content"]').text()).toBe('Contenu du drawer') + }) + + it('emits update:modelValue false on backdrop click', async () => { + const wrapper = mountComponent({ modelValue: true }) + await wrapper.find('[data-test="backdrop"]').trigger('click') + expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([false]) + }) + + it('emits update:modelValue false 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]) + }) + + it('shows close button by default', () => { + const wrapper = mountComponent({ modelValue: true }) + expect(wrapper.find('[data-test="close-button"]').exists()).toBe(true) + }) + + it('hides close button when showClose is false', () => { + const wrapper = mountComponent({ modelValue: true, showClose: false }) + expect(wrapper.find('[data-test="close-button"]').exists()).toBe(false) + }) + + it('close button renders mdi:close icon', () => { + const wrapper = mountComponent({ modelValue: true }) + const icon = wrapper.findComponent(IconifyIcon) + expect(icon.props('icon')).toBe('mdi:close') + }) + + it('uses custom id when provided', () => { + const wrapper = mountComponent({ modelValue: true, id: 'my-drawer' }) + expect(wrapper.find('.fixed').attributes('id')).toBe('my-drawer') + }) + + it('generates an id when not provided', () => { + const wrapper = mountComponent({ modelValue: true }) + const id = wrapper.find('.fixed').attributes('id') + expect(id).toMatch(/^malio-drawer-/) + }) + + it('has role="dialog" and aria-modal on panel', () => { + const wrapper = mountComponent({ modelValue: true }) + const panel = wrapper.find('[data-test="panel"]') + expect(panel.attributes('role')).toBe('dialog') + expect(panel.attributes('aria-modal')).toBe('true') + }) + + it('aria-labelledby links to title id', () => { + const wrapper = mountComponent({ modelValue: true, id: 'test-drawer' }) + const panel = wrapper.find('[data-test="panel"]') + expect(panel.attributes('aria-labelledby')).toBe('test-drawer-title') + expect(wrapper.find('h2').attributes('id')).toBe('test-drawer-title') + }) + + it('applies drawerClass to the panel', () => { + const wrapper = mountComponent({ modelValue: true, drawerClass: 'max-w-lg' }) + const panel = wrapper.find('[data-test="panel"]') + expect(panel.classes()).toContain('max-w-lg') + }) + + it('works in uncontrolled mode', () => { + const wrapper = mountComponent() + // Without modelValue, defaults to closed + expect(wrapper.find('[data-test="panel"]').exists()).toBe(false) + }) + + it('close button has aria-label "Fermer"', () => { + const wrapper = mountComponent({ modelValue: true }) + expect(wrapper.find('[data-test="close-button"]').attributes('aria-label')).toBe('Fermer') + }) +}) diff --git a/app/components/malio/drawer/Drawer.vue b/app/components/malio/drawer/Drawer.vue new file mode 100644 index 0000000..3ad8eb7 --- /dev/null +++ b/app/components/malio/drawer/Drawer.vue @@ -0,0 +1,135 @@ + +Contenu simple du drawer.
+Ce drawer n'a pas de bouton fermer. Cliquez sur le backdrop pour fermer.
+Ce drawer utilise une largeur personnalisée via la prop drawerClass.
+