From d65884dc44bf06925b3ffe17c98990ec079db581 Mon Sep 17 00:00:00 2001 From: tristan Date: Wed, 20 May 2026 08:21:36 +0200 Subject: [PATCH] =?UTF-8?q?feat=20:=20composable=20d'=C3=A9tat=20du=20popo?= =?UTF-8?q?ver=20calendrier=20(#MUI-33)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 (1M context) --- .../composables/useCalendarPopover.test.ts | 64 +++++++++++++++++++ .../date/composables/useCalendarPopover.ts | 28 ++++++++ 2 files changed, 92 insertions(+) create mode 100644 app/components/malio/date/composables/useCalendarPopover.test.ts create mode 100644 app/components/malio/date/composables/useCalendarPopover.ts diff --git a/app/components/malio/date/composables/useCalendarPopover.test.ts b/app/components/malio/date/composables/useCalendarPopover.test.ts new file mode 100644 index 0000000..206b5f4 --- /dev/null +++ b/app/components/malio/date/composables/useCalendarPopover.test.ts @@ -0,0 +1,64 @@ +import {describe, expect, it} from 'vitest' +import {defineComponent, h, ref} from 'vue' +import {mount} from '@vue/test-utils' +import {useCalendarPopover} from './useCalendarPopover' + +const mountHost = () => { + const api: ReturnType = {} as never + const Host = defineComponent({ + setup() { + const root = ref(null) + Object.assign(api, useCalendarPopover(root)) + return () => h('div', {ref: root}, 'host') + }, + }) + const wrapper = mount(Host, {attachTo: document.body}) + return {wrapper, api} +} + +describe('useCalendarPopover', () => { + it('starts closed in days view', () => { + const {api} = mountHost() + expect(api.isOpen.value).toBe(false) + expect(api.viewMode.value).toBe('days') + }) + + it('open() opens in days view', () => { + const {api} = mountHost() + api.open() + expect(api.isOpen.value).toBe(true) + expect(api.viewMode.value).toBe('days') + }) + + it('toggleView() switches between days and months', () => { + const {api} = mountHost() + api.open() + api.toggleView() + expect(api.viewMode.value).toBe('months') + api.toggleView() + expect(api.viewMode.value).toBe('days') + }) + + it('close() resets isOpen and viewMode', () => { + const {api} = mountHost() + api.open() + api.toggleView() + api.close() + expect(api.isOpen.value).toBe(false) + expect(api.viewMode.value).toBe('days') + }) + + it('closes on outside mousedown', () => { + const {api} = mountHost() + api.open() + document.body.dispatchEvent(new MouseEvent('mousedown', {bubbles: true})) + expect(api.isOpen.value).toBe(false) + }) + + it('stays open on inside mousedown', () => { + const {wrapper, api} = mountHost() + api.open() + wrapper.element.dispatchEvent(new MouseEvent('mousedown', {bubbles: true})) + expect(api.isOpen.value).toBe(true) + }) +}) diff --git a/app/components/malio/date/composables/useCalendarPopover.ts b/app/components/malio/date/composables/useCalendarPopover.ts new file mode 100644 index 0000000..db179cc --- /dev/null +++ b/app/components/malio/date/composables/useCalendarPopover.ts @@ -0,0 +1,28 @@ +import {onBeforeUnmount, onMounted, ref, type Ref} from 'vue' + +export function useCalendarPopover(rootRef: Ref) { + const isOpen = ref(false) + const viewMode = ref<'days' | 'months'>('days') + + const open = () => { + isOpen.value = true + viewMode.value = 'days' + } + const close = () => { + isOpen.value = false + viewMode.value = 'days' + } + const toggleView = () => { + viewMode.value = viewMode.value === 'days' ? 'months' : 'days' + } + + const onMouseDown = (event: MouseEvent) => { + if (!isOpen.value || !rootRef.value) return + if (!rootRef.value.contains(event.target as Node)) close() + } + + onMounted(() => document.addEventListener('mousedown', onMouseDown)) + onBeforeUnmount(() => document.removeEventListener('mousedown', onMouseDown)) + + return {isOpen, viewMode, open, close, toggleView} +}