diff --git a/.playground/pages/composant/date/date.vue b/.playground/pages/composant/date/date.vue index 7d50df5..ead8c8b 100644 --- a/.playground/pages/composant/date/date.vue +++ b/.playground/pages/composant/date/date.vue @@ -78,11 +78,29 @@ /> + +
+
+

markedDates + @month-change

+ +
+

Mois affiché : {{ shownMonth }}

+

● success : {{ successDays.join(', ') }}

+

● danger : {{ dangerDays.join(', ') }}

+
+
+
diff --git a/CHANGELOG.md b/CHANGELOG.md index 81c7a2d..e5f999c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,7 @@ Liste des évolutions de la librairie Malio layer UI * [#MUI-43] Famille Date editable (MalioDate, MalioDateTime) : gabarit fantôme progressif — le format (`JJ/MM/AAAA` / `JJ/MM/AAAA HH:MM`) s'affiche en gris et se remplit au fil de la saisie (tapé en noir, reste en gris) ; séparateurs (`/`, espace, `:`) posés automatiquement dès qu'un groupe est complet (maska `eager`). CalendarField : prop `placeholderTemplate` (le masque maska en est dérivé), remplace l'ancienne mécanique de masque codé en dur. * [#MUI-43] CalendarField : la croix d'effacement réinitialise désormais la saisie clavier même après une date invalide (le `v-model` restant `null`, le champ se vidait pas). * [#MUI-44] MalioDate / MalioDateTime : event `update:rawValue` (string) exposant la saisie brute sur un canal séparé pour la validation back-autoritative — saisie invalide (non parsable ou hors `min`/`max`) → texte trimmé tel que tapé, saisie valide/vide + clear + sélection au calendrier → `''`. `modelValue` reste `string` ISO `| null` (la saisie invalide n'y transite jamais) ; le parent construit son payload via `valid ? modelValue : rawValue`. +* [#MUI-45] MalioDate : prop `markedDates` (`Record<"YYYY-MM-DD", 'success' | 'danger'>`) appliquant un fond tokenisé par jour dans la grille (générique, fourni par le consommateur ; précédence sélection/`today` > variante marquée > défaut) + event `month-change` (`{ month: 0-11, year }`) émis à l'ouverture du popover et à chaque navigation de mois. Sert l'écran *Heures* de SIRH (jours validés en vert, chargement du mois visible à la volée). ### Changed * DataTable : libellés de pagination en français — `Préc.` / `Suiv.` (étaient `Prev` / `Next`) ; aria-labels déjà en français inchangés. diff --git a/COMPONENTS.md b/COMPONENTS.md index cb02580..cb40887 100644 --- a/COMPONENTS.md +++ b/COMPONENTS.md @@ -511,6 +511,10 @@ L'event `update:valid` remonte l'état de validité de la saisie au parent (`tru L'event `update:rawValue` expose la **saisie brute** sur un canal séparé, pour les formulaires en validation back-autoritative (le serveur tranche le format et renvoie un `422`). Il est émis à chaque commit : saisie invalide (non parsable ou hors `min`/`max`) → la chaîne trimmée telle que tapée (ex. `"32/13/2026"`) ; saisie valide ou vide, clear, sélection au calendrier → `''`. Le parent construit alors son payload via `valid ? modelValue : rawValue`. La saisie invalide **ne transite jamais** par `modelValue` (qui reste `string` ISO `| null` pour l'affichage et le round-trip) ; `valid` dit *qu'il y a* une erreur, `rawValue` dit *quoi* envoyer. +La prop `markedDates` permet d'afficher un **statut par jour** dans la grille : un objet `{ "YYYY-MM-DD": "success" | "danger" }` applique un fond tokenisé (`success` → vert clair, `danger` → rouge clair). C'est **purement générique** — aucune logique métier dans le layer : le consommateur fournit la liste des jours à marquer. **Précédence** : un jour sélectionné garde son style primary (fond plein, prime sur la variante marquée) ; le jour courant (`today`) **garde sa bordure** et reçoit **en plus** le fond marqué s'il est dans `markedDates` (vert/rouge bordé) ; sinon, fond marqué simple. + +L'event `month-change` remonte le **mois affiché** dans le popover (`{ month: number /* 0-11 */, year: number }`). Il est émis **à l'ouverture** du popover (sur le mois de la valeur, ou le mois courant) **et à chaque navigation** (chevrons, sélection dans la vue mois). Couplé à `markedDates`, il permet à un consommateur (ex. l'écran *Heures* de SIRH) de charger les statuts du mois visible à la volée : on écoute `@month-change` pour fetch, puis on réinjecte le résultat dans `:marked-dates`. + | Prop | Type | Défaut | Description | |------|------|--------|-------------| | `modelValue` | `string \| null` | `undefined` | Date ISO `"YYYY-MM-DD"` (v-model) | @@ -526,13 +530,14 @@ L'event `update:rawValue` expose la **saisie brute** sur un canal séparé, pour | `success` | `string` | `''` | Message de succès | | `min` | `string` | `undefined` | Date min `"YYYY-MM-DD"` (jours antérieurs désactivés) | | `max` | `string` | `undefined` | Date max `"YYYY-MM-DD"` (jours postérieurs désactivés) | +| `markedDates` | `Record` | `undefined` | Statut par jour : ISO `"YYYY-MM-DD"` → fond tokenisé. Générique (fourni par le consommateur). | | `clearable` | `boolean` | `true` | Affiche la croix d'effacement | | `editable` | `boolean` | `false` | Autorise la saisie clavier `JJ/MM/AAAA` (masque maska, validation au blur) en plus du calendrier | | `invalidMessage` | `string` | `'Date invalide'` | Message affiché quand la saisie clavier est invalide ou hors `min`/`max` | | `reserveMessageSpace` | `boolean` | `true` | Réserve l'espace de la ligne message sous le champ (min-h) même sans message. `false` = la ligne ne prend de place que s'il y a un hint/error/success. | | `inputClass` / `labelClass` / `groupClass` | `string` | `''` | Override des classes | -**Events :** `update:modelValue(value: string | null)`, `update:valid(value: boolean)`, `update:rawValue(value: string)` +**Events :** `update:modelValue(value: string | null)`, `update:valid(value: boolean)`, `update:rawValue(value: string)`, `month-change(value: { month: number /* 0-11 */, year: number })` **Clavier :** `Entrée` / `Espace` ouvrent le calendrier, `Échap` ferme. Anneau de focus clavier (combo champ + calendrier à l'ouverture). La croix d'effacement est focusable. _(Comportement partagé par DateRange, DateTime, DateWeek via le shell CalendarField.)_ @@ -545,6 +550,13 @@ L'event `update:rawValue` expose la **saisie brute** sur un canal séparé, pour + + + ``` --- diff --git a/app/components/malio/date/Date.test.ts b/app/components/malio/date/Date.test.ts index 382cda0..7762404 100644 --- a/app/components/malio/date/Date.test.ts +++ b/app/components/malio/date/Date.test.ts @@ -17,6 +17,7 @@ type DateProps = { success?: string min?: string max?: string + markedDates?: Record clearable?: boolean editable?: boolean invalidMessage?: string @@ -109,6 +110,48 @@ describe('MalioDate', () => { }) }) + describe('month-change', () => { + it('émet month-change à l\'ouverture avec le mois courant', async () => { + const wrapper = mountDate() + await wrapper.get('[data-test="date-input"]').trigger('click') + expect(wrapper.emitted('month-change')?.at(-1)).toEqual([{month: 4, year: 2026}]) + }) + + it('émet month-change sur le mois de la valeur à l\'ouverture', async () => { + const wrapper = mountDate({modelValue: '2025-12-25'}) + await wrapper.get('[data-test="date-input"]').trigger('click') + expect(wrapper.emitted('month-change')?.at(-1)).toEqual([{month: 11, year: 2025}]) + }) + + it('émet month-change à chaque navigation de mois', async () => { + const wrapper = mountDate() + await wrapper.get('[data-test="date-input"]').trigger('click') + await wrapper.get('[data-test="header-next"]').trigger('click') + expect(wrapper.emitted('month-change')?.at(-1)).toEqual([{month: 5, year: 2026}]) + await wrapper.get('[data-test="header-prev"]').trigger('click') + await wrapper.get('[data-test="header-prev"]').trigger('click') + expect(wrapper.emitted('month-change')?.at(-1)).toEqual([{month: 3, year: 2026}]) + }) + + it('ne ré-émet pas month-change après fermeture', async () => { + const wrapper = mountDate() + await wrapper.get('[data-test="date-input"]').trigger('click') + const countOpen = wrapper.emitted('month-change')?.length ?? 0 + document.body.dispatchEvent(new MouseEvent('mousedown', {bubbles: true})) + await wrapper.vm.$nextTick() + expect(wrapper.emitted('month-change')?.length ?? 0).toBe(countOpen) + }) + }) + + describe('markedDates', () => { + it('transmet markedDates à la grille (fond tokenisé)', async () => { + const wrapper = mountDate({markedDates: {'2026-05-20': 'success'}}) + await wrapper.get('[data-test="date-input"]').trigger('click') + const pill = wrapper.get('[data-iso="2026-05-20"]').get('span.rounded-full') + expect(pill.classes()).toContain('bg-m-success/15') + }) + }) + describe('sélection', () => { it('emits the ISO date and closes on day click', async () => { const wrapper = mountDate() diff --git a/app/components/malio/date/Date.vue b/app/components/malio/date/Date.vue index 34aeed0..d131b04 100644 --- a/app/components/malio/date/Date.vue +++ b/app/components/malio/date/Date.vue @@ -20,12 +20,14 @@ v-bind="$attrs" @clear="onClear" @commit="onCommit" + @month-change="(payload) => emit('month-change', payload)" > @@ -102,4 +116,20 @@ const initialValue = ref(todayIso) const boundedValue = ref(null) const errorValue = ref(null) const editableValue = ref(null) + +const ym = `${now.getFullYear()}-${pad(now.getMonth() + 1)}` +const markedDates = ref>({ + [`${ym}-05`]: 'success', + [`${ym}-06`]: 'success', + [`${ym}-12`]: 'success', + [`${ym}-09`]: 'danger', + [`${ym}-20`]: 'danger', +}) +const markedValue = ref(null) +const monthsLong = ['janvier', 'février', 'mars', 'avril', 'mai', 'juin', + 'juillet', 'août', 'septembre', 'octobre', 'novembre', 'décembre'] +const shownMonth = ref('—') +const onMonthChange = ({month, year}: {month: number, year: number}) => { + shownMonth.value = `${monthsLong[month]} ${year}` +}