Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 244d62dc71 | |||
| 29bd6abcfe |
@@ -78,11 +78,29 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-start gap-10">
|
||||||
|
<div class="w-[396px] space-y-3">
|
||||||
|
<h2 class="font-semibold">markedDates + @month-change</h2>
|
||||||
|
<MalioDate
|
||||||
|
v-model="markedValue"
|
||||||
|
label="Calendrier avec statuts par jour"
|
||||||
|
hint="Jours verts = validés, rouges = à corriger"
|
||||||
|
:marked-dates="markedDates"
|
||||||
|
@month-change="onMonthChange"
|
||||||
|
/>
|
||||||
|
<div class="rounded border p-3 text-sm">
|
||||||
|
<p>Mois affiché : <code>{{ shownMonth }}</code></p>
|
||||||
|
<p class="mt-1 text-m-success">● success : {{ successDays.join(', ') }}</p>
|
||||||
|
<p class="text-m-danger">● danger : {{ dangerDays.join(', ') }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {ref} from 'vue'
|
import {computed, ref} from 'vue'
|
||||||
|
|
||||||
const pad = (n: number) => String(n).padStart(2, '0')
|
const pad = (n: number) => String(n).padStart(2, '0')
|
||||||
const toIso = (d: Date) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
|
const toIso = (d: Date) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
|
||||||
@@ -95,4 +113,20 @@ const value = ref<string | null>(null)
|
|||||||
const erpValue = ref<string | null>(null)
|
const erpValue = ref<string | null>(null)
|
||||||
const bounded = ref<string | null>(null)
|
const bounded = ref<string | null>(null)
|
||||||
const editableValue = ref<string | null>(null)
|
const editableValue = ref<string | null>(null)
|
||||||
|
|
||||||
|
// Démo markedDates : quelques jours du mois courant marqués success / danger.
|
||||||
|
const ym = `${now.getFullYear()}-${pad(now.getMonth() + 1)}`
|
||||||
|
const successDays = [`${ym}-05`, `${ym}-06`, `${ym}-12`]
|
||||||
|
const dangerDays = [`${ym}-09`, `${ym}-20`]
|
||||||
|
const markedDates = computed<Record<string, 'success' | 'danger'>>(() => ({
|
||||||
|
...Object.fromEntries(successDays.map(d => [d, 'success' as const])),
|
||||||
|
...Object.fromEntries(dangerDays.map(d => [d, 'danger' as const])),
|
||||||
|
}))
|
||||||
|
const markedValue = ref<string | null>(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} (month=${month})`
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ Liste des évolutions de la librairie Malio layer UI
|
|||||||
* [#MUI-43] MalioDateTime : saisie clavier `JJ/MM/AAAA HH:MM` optionnelle (prop `editable`, masque maska, `invalidMessage`) + même event `update:valid` que MalioDate (mêmes règles, émis dès le montage). Nouveau parseur `parseDisplayToIsoDateTime`.
|
* [#MUI-43] MalioDateTime : saisie clavier `JJ/MM/AAAA HH:MM` optionnelle (prop `editable`, masque maska, `invalidMessage`) + même event `update:valid` que MalioDate (mêmes règles, émis dès le montage). Nouveau parseur `parseDisplayToIsoDateTime`.
|
||||||
* [#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] 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-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
|
### Changed
|
||||||
* DataTable : libellés de pagination en français — `Préc.` / `Suiv.` (étaient `Prev` / `Next`) ; aria-labels déjà en français inchangés.
|
* DataTable : libellés de pagination en français — `Préc.` / `Suiv.` (étaient `Prev` / `Next`) ; aria-labels déjà en français inchangés.
|
||||||
|
|||||||
+21
-3
@@ -509,6 +509,12 @@ Avec `editable`, l'utilisateur peut aussi taper la date au clavier. La valeur n'
|
|||||||
|
|
||||||
L'event `update:valid` remonte l'état de validité de la saisie au parent (`true` = vide ou date valide dans les bornes ; `false` = saisie malformée ou hors `min`/`max`). Il est émis **dès le montage** (état d'un champ pré-rempli connu sans interaction) puis à chaque transition. Il permet d'agréger la validité des champs date dans la gate de submit d'un formulaire — une saisie invalide n'émettant pas `modelValue`, c'est le seul signal disponible côté parent. La validité ne couvre **pas** l'obligation `required` (un champ vide reste valide), qui reste à la charge du parent.
|
L'event `update:valid` remonte l'état de validité de la saisie au parent (`true` = vide ou date valide dans les bornes ; `false` = saisie malformée ou hors `min`/`max`). Il est émis **dès le montage** (état d'un champ pré-rempli connu sans interaction) puis à chaque transition. Il permet d'agréger la validité des champs date dans la gate de submit d'un formulaire — une saisie invalide n'émettant pas `modelValue`, c'est le seul signal disponible côté parent. La validité ne couvre **pas** l'obligation `required` (un champ vide reste valide), qui reste à la charge du parent.
|
||||||
|
|
||||||
|
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 |
|
| Prop | Type | Défaut | Description |
|
||||||
|------|------|--------|-------------|
|
|------|------|--------|-------------|
|
||||||
| `modelValue` | `string \| null` | `undefined` | Date ISO `"YYYY-MM-DD"` (v-model) |
|
| `modelValue` | `string \| null` | `undefined` | Date ISO `"YYYY-MM-DD"` (v-model) |
|
||||||
@@ -524,13 +530,14 @@ L'event `update:valid` remonte l'état de validité de la saisie au parent (`tru
|
|||||||
| `success` | `string` | `''` | Message de succès |
|
| `success` | `string` | `''` | Message de succès |
|
||||||
| `min` | `string` | `undefined` | Date min `"YYYY-MM-DD"` (jours antérieurs désactivé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) |
|
| `max` | `string` | `undefined` | Date max `"YYYY-MM-DD"` (jours postérieurs désactivés) |
|
||||||
|
| `markedDates` | `Record<string, 'success' \| 'danger'>` | `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 |
|
| `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 |
|
| `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` |
|
| `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. |
|
| `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 |
|
| `inputClass` / `labelClass` / `groupClass` | `string` | `''` | Override des classes |
|
||||||
|
|
||||||
**Events :** `update:modelValue(value: string | null)`, `update:valid(value: boolean)`
|
**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.)_
|
**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.)_
|
||||||
|
|
||||||
@@ -540,6 +547,16 @@ L'event `update:valid` remonte l'état de validité de la saisie au parent (`tru
|
|||||||
<MalioDate v-model="rdv" label="Rendez-vous" :min="todayIso" :max="maxIso" />
|
<MalioDate v-model="rdv" label="Rendez-vous" :min="todayIso" :max="maxIso" />
|
||||||
<MalioDate v-model="date" label="Date de naissance" editable />
|
<MalioDate v-model="date" label="Date de naissance" editable />
|
||||||
<MalioDate v-model="date" label="Date de naissance" editable @update:valid="dateValide = $event" />
|
<MalioDate v-model="date" label="Date de naissance" editable @update:valid="dateValide = $event" />
|
||||||
|
<!-- Validation back-autoritative : on envoie la saisie brute si invalide -->
|
||||||
|
<MalioDate v-model="date" editable @update:valid="valide = $event" @update:rawValue="brut = $event" />
|
||||||
|
<!-- payload : valide ? date : brut -->
|
||||||
|
<!-- Statut par jour + chargement du mois visible (ex. SIRH « Heures ») -->
|
||||||
|
<MalioDate
|
||||||
|
v-model="date"
|
||||||
|
:marked-dates="statutsDuMois"
|
||||||
|
@month-change="({ month, year }) => chargerStatuts(month, year)"
|
||||||
|
/>
|
||||||
|
<!-- statutsDuMois === { "2026-05-05": "success", "2026-05-20": "danger" } -->
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -694,16 +711,17 @@ La valeur est une chaîne **ISO naïve sans fuseau** au format `"YYYY-MM-DDTHH:M
|
|||||||
| `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. |
|
| `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 |
|
| `inputClass` / `labelClass` / `groupClass` | `string` | `''` | Override des classes |
|
||||||
|
|
||||||
**Events :** `update:modelValue(value: string | null)`, `update:valid(value: boolean)`
|
**Events :** `update:modelValue(value: string | null)`, `update:valid(value: boolean)`, `update:rawValue(value: string)`
|
||||||
|
|
||||||
Flux : cliquer un jour fixe la date (heure par défaut `00:00`), régler l'heure met à jour l'heure ; le popover se ferme au clic extérieur. La valeur est émise en direct à chaque interaction.
|
Flux : cliquer un jour fixe la date (heure par défaut `00:00`), régler l'heure met à jour l'heure ; le popover se ferme au clic extérieur. La valeur est émise en direct à chaque interaction.
|
||||||
|
|
||||||
Avec `editable`, l'utilisateur peut aussi taper `JJ/MM/AAAA HH:MM` au clavier. La valeur n'est émise qu'au blur (ou sur Entrée) si elle est valide et dans les bornes ; sinon le texte est conservé et le champ passe en erreur (`invalidMessage`). Un **gabarit fantôme** affiche le format en gris et se remplit au fil de la saisie (cf. MalioDate). L'event `update:valid` (booléen) — émis **dès le montage** puis à chaque transition — remonte l'état de validité au parent (`false` = saisie malformée ou hors `min`/`max`, qui n'émet pas `modelValue`), pour bloquer un submit. La validité ne couvre **pas** `required` (champ vide = valide), comme sur `MalioDate`.
|
Avec `editable`, l'utilisateur peut aussi taper `JJ/MM/AAAA HH:MM` au clavier. La valeur n'est émise qu'au blur (ou sur Entrée) si elle est valide et dans les bornes ; sinon le texte est conservé et le champ passe en erreur (`invalidMessage`). Un **gabarit fantôme** affiche le format en gris et se remplit au fil de la saisie (cf. MalioDate). L'event `update:valid` (booléen) — émis **dès le montage** puis à chaque transition — remonte l'état de validité au parent (`false` = saisie malformée ou hors `min`/`max`, qui n'émet pas `modelValue`), pour bloquer un submit. La validité ne couvre **pas** `required` (champ vide = valide), comme sur `MalioDate`. L'event `update:rawValue` expose la saisie brute pour la validation back-autoritative (mêmes règles que `MalioDate` : texte trimmé sur saisie invalide, `''` sinon — clear et sélection au calendrier compris).
|
||||||
|
|
||||||
```vue
|
```vue
|
||||||
<MalioDateTime v-model="rdv" label="Date et heure du rendez-vous" />
|
<MalioDateTime v-model="rdv" label="Date et heure du rendez-vous" />
|
||||||
<!-- rdv === "2026-05-20T14:30:00" -->
|
<!-- rdv === "2026-05-20T14:30:00" -->
|
||||||
<MalioDateTime v-model="rdv" label="Rendez-vous" editable @update:valid="rdvValide = $event" />
|
<MalioDateTime v-model="rdv" label="Rendez-vous" editable @update:valid="rdvValide = $event" />
|
||||||
|
<MalioDateTime v-model="rdv" editable @update:valid="valide = $event" @update:rawValue="brut = $event" />
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ type DateProps = {
|
|||||||
success?: string
|
success?: string
|
||||||
min?: string
|
min?: string
|
||||||
max?: string
|
max?: string
|
||||||
|
markedDates?: Record<string, 'success' | 'danger'>
|
||||||
clearable?: boolean
|
clearable?: boolean
|
||||||
editable?: boolean
|
editable?: boolean
|
||||||
invalidMessage?: string
|
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', () => {
|
describe('sélection', () => {
|
||||||
it('emits the ISO date and closes on day click', async () => {
|
it('emits the ISO date and closes on day click', async () => {
|
||||||
const wrapper = mountDate()
|
const wrapper = mountDate()
|
||||||
@@ -471,4 +514,58 @@ describe('MalioDate', () => {
|
|||||||
expect(wrapper.emitted('update:valid')?.at(-1)).toEqual([true])
|
expect(wrapper.emitted('update:valid')?.at(-1)).toEqual([true])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('saisie brute (update:rawValue)', () => {
|
||||||
|
it('émet le texte brut trimmé sur saisie malformée, sans émettre modelValue', async () => {
|
||||||
|
const wrapper = mountDate({editable: true})
|
||||||
|
const input = wrapper.get('[data-test="date-input"]')
|
||||||
|
await input.setValue('32/13/2026')
|
||||||
|
await input.trigger('blur')
|
||||||
|
expect(wrapper.emitted('update:rawValue')?.at(-1)).toEqual(['32/13/2026'])
|
||||||
|
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('émet le texte brut trimmé sur saisie hors min/max', async () => {
|
||||||
|
const wrapper = mountDate({editable: true, min: '2026-05-10', max: '2026-05-20'})
|
||||||
|
const input = wrapper.get('[data-test="date-input"]')
|
||||||
|
await input.setValue('25/12/2026')
|
||||||
|
await input.trigger('blur')
|
||||||
|
expect(wrapper.emitted('update:rawValue')?.at(-1)).toEqual(['25/12/2026'])
|
||||||
|
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('émet rawValue vide et l\'ISO sur saisie clavier valide', async () => {
|
||||||
|
const wrapper = mountDate({editable: true})
|
||||||
|
const input = wrapper.get('[data-test="date-input"]')
|
||||||
|
await input.setValue('19/05/2026')
|
||||||
|
await input.trigger('blur')
|
||||||
|
expect(wrapper.emitted('update:rawValue')?.at(-1)).toEqual([''])
|
||||||
|
expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual(['2026-05-19'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('émet rawValue vide sur saisie vidée au blur', async () => {
|
||||||
|
const wrapper = mountDate({editable: true, modelValue: '2026-05-19'})
|
||||||
|
const input = wrapper.get('[data-test="date-input"]')
|
||||||
|
await input.setValue('')
|
||||||
|
await input.trigger('blur')
|
||||||
|
expect(wrapper.emitted('update:rawValue')?.at(-1)).toEqual([''])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('émet rawValue vide sur clear', async () => {
|
||||||
|
const wrapper = mountDate({modelValue: '2026-05-19'})
|
||||||
|
await wrapper.get('[data-test="clear"]').trigger('click')
|
||||||
|
expect(wrapper.emitted('update:rawValue')?.at(-1)).toEqual([''])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('émet rawValue vide quand on sélectionne une date au calendrier', async () => {
|
||||||
|
const wrapper = mountDate({editable: true})
|
||||||
|
const input = wrapper.get('[data-test="date-input"]')
|
||||||
|
await input.setValue('32/13/2026')
|
||||||
|
await input.trigger('blur')
|
||||||
|
expect(wrapper.emitted('update:rawValue')?.at(-1)).toEqual(['32/13/2026'])
|
||||||
|
await input.trigger('focus')
|
||||||
|
await wrapper.get('[data-test="day"][data-iso="2026-05-19"]').trigger('click')
|
||||||
|
expect(wrapper.emitted('update:rawValue')?.at(-1)).toEqual([''])
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -20,12 +20,14 @@
|
|||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
@clear="onClear"
|
@clear="onClear"
|
||||||
@commit="onCommit"
|
@commit="onCommit"
|
||||||
|
@month-change="(payload) => emit('month-change', payload)"
|
||||||
>
|
>
|
||||||
<template #default="{ currentMonth, currentYear, close }">
|
<template #default="{ currentMonth, currentYear, close }">
|
||||||
<MonthGrid
|
<MonthGrid
|
||||||
:month="currentMonth"
|
:month="currentMonth"
|
||||||
:year="currentYear"
|
:year="currentYear"
|
||||||
:selected-date="modelValue ?? null"
|
:selected-date="modelValue ?? null"
|
||||||
|
:marked-dates="markedDates"
|
||||||
:min="min"
|
:min="min"
|
||||||
:max="max"
|
:max="max"
|
||||||
@select="(iso) => onSelect(iso, close)"
|
@select="(iso) => onSelect(iso, close)"
|
||||||
@@ -57,6 +59,9 @@ const props = withDefaults(
|
|||||||
success?: string
|
success?: string
|
||||||
min?: string
|
min?: string
|
||||||
max?: string
|
max?: string
|
||||||
|
// Statut générique par jour, ISO yyyy-mm-dd → variante de fond. Aucune
|
||||||
|
// logique métier dans le layer : le consommateur fournit la liste.
|
||||||
|
markedDates?: Record<string, 'success' | 'danger'>
|
||||||
clearable?: boolean
|
clearable?: boolean
|
||||||
editable?: boolean
|
editable?: boolean
|
||||||
invalidMessage?: string
|
invalidMessage?: string
|
||||||
@@ -78,6 +83,7 @@ const props = withDefaults(
|
|||||||
success: '',
|
success: '',
|
||||||
min: undefined,
|
min: undefined,
|
||||||
max: undefined,
|
max: undefined,
|
||||||
|
markedDates: undefined,
|
||||||
clearable: true,
|
clearable: true,
|
||||||
editable: false,
|
editable: false,
|
||||||
invalidMessage: 'Date invalide',
|
invalidMessage: 'Date invalide',
|
||||||
@@ -90,6 +96,12 @@ const props = withDefaults(
|
|||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'update:modelValue', value: string | null): void
|
(e: 'update:modelValue', value: string | null): void
|
||||||
(e: 'update:valid', value: boolean): void
|
(e: 'update:valid', value: boolean): void
|
||||||
|
// Canal séparé pour la saisie invalide (validation back-autoritative) : texte brut
|
||||||
|
// tel que tapé sur saisie non parsable/hors plage, '' sinon. Ne JAMAIS transiter
|
||||||
|
// par modelValue, qui doit rester ISO|null pour l'affichage et le round-trip.
|
||||||
|
(e: 'update:rawValue', value: string): void
|
||||||
|
// Mois affiché dans le popover (month 0-11) : à l'ouverture et à chaque nav.
|
||||||
|
(e: 'month-change', value: {month: number, year: number}): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const displayValue = computed(() => formatIsoToDisplay(props.modelValue ?? null))
|
const displayValue = computed(() => formatIsoToDisplay(props.modelValue ?? null))
|
||||||
@@ -108,25 +120,30 @@ const onCommit = (text: string) => {
|
|||||||
const trimmed = text.trim()
|
const trimmed = text.trim()
|
||||||
if (trimmed === '') {
|
if (trimmed === '') {
|
||||||
setError('')
|
setError('')
|
||||||
|
emit('update:rawValue', '')
|
||||||
emit('update:modelValue', null)
|
emit('update:modelValue', null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const iso = parseDisplayToIso(trimmed)
|
const iso = parseDisplayToIso(trimmed)
|
||||||
if (iso && isDateInRange(iso, props.min, props.max)) {
|
if (iso && isDateInRange(iso, props.min, props.max)) {
|
||||||
setError('')
|
setError('')
|
||||||
|
emit('update:rawValue', '')
|
||||||
emit('update:modelValue', iso)
|
emit('update:modelValue', iso)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setError(props.invalidMessage)
|
setError(props.invalidMessage)
|
||||||
|
emit('update:rawValue', trimmed)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onClear = () => {
|
const onClear = () => {
|
||||||
setError('')
|
setError('')
|
||||||
|
emit('update:rawValue', '')
|
||||||
emit('update:modelValue', null)
|
emit('update:modelValue', null)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onSelect = (iso: string, close: () => void) => {
|
const onSelect = (iso: string, close: () => void) => {
|
||||||
setError('')
|
setError('')
|
||||||
|
emit('update:rawValue', '')
|
||||||
emit('update:modelValue', iso)
|
emit('update:modelValue', iso)
|
||||||
close()
|
close()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -283,4 +283,58 @@ describe('MalioDateTime', () => {
|
|||||||
expect(wrapper.emitted('update:valid')?.at(-1)).toEqual([true])
|
expect(wrapper.emitted('update:valid')?.at(-1)).toEqual([true])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('saisie brute (update:rawValue)', () => {
|
||||||
|
it('émet le texte brut trimmé sur saisie malformée, sans émettre modelValue', async () => {
|
||||||
|
const wrapper = mountDateTime({editable: true})
|
||||||
|
const input = wrapper.get('[data-test="date-input"]')
|
||||||
|
await input.setValue('32/13/2026 14:30')
|
||||||
|
await input.trigger('blur')
|
||||||
|
expect(wrapper.emitted('update:rawValue')?.at(-1)).toEqual(['32/13/2026 14:30'])
|
||||||
|
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('émet le texte brut trimmé sur saisie hors min/max', async () => {
|
||||||
|
const wrapper = mountDateTime({editable: true, min: '2026-05-10T00:00:00', max: '2026-05-20T00:00:00'})
|
||||||
|
const input = wrapper.get('[data-test="date-input"]')
|
||||||
|
await input.setValue('25/12/2026 10:00')
|
||||||
|
await input.trigger('blur')
|
||||||
|
expect(wrapper.emitted('update:rawValue')?.at(-1)).toEqual(['25/12/2026 10:00'])
|
||||||
|
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('émet rawValue vide et l\'ISO sur saisie clavier valide', async () => {
|
||||||
|
const wrapper = mountDateTime({editable: true})
|
||||||
|
const input = wrapper.get('[data-test="date-input"]')
|
||||||
|
await input.setValue('20/05/2026 14:30')
|
||||||
|
await input.trigger('blur')
|
||||||
|
expect(wrapper.emitted('update:rawValue')?.at(-1)).toEqual([''])
|
||||||
|
expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual(['2026-05-20T14:30:00'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('émet rawValue vide sur saisie vidée au blur', async () => {
|
||||||
|
const wrapper = mountDateTime({editable: true, modelValue: '2026-05-20T14:30:00'})
|
||||||
|
const input = wrapper.get('[data-test="date-input"]')
|
||||||
|
await input.setValue('')
|
||||||
|
await input.trigger('blur')
|
||||||
|
expect(wrapper.emitted('update:rawValue')?.at(-1)).toEqual([''])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('émet rawValue vide sur clear', async () => {
|
||||||
|
const wrapper = mountDateTime({modelValue: '2026-05-20T14:30:00'})
|
||||||
|
await wrapper.get('[data-test="clear"]').trigger('click')
|
||||||
|
expect(wrapper.emitted('update:rawValue')?.at(-1)).toEqual([''])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('émet rawValue vide quand on sélectionne une date au calendrier', async () => {
|
||||||
|
const wrapper = mountDateTime({editable: true})
|
||||||
|
const input = wrapper.get('[data-test="date-input"]')
|
||||||
|
await input.setValue('32/13/2026 14:30')
|
||||||
|
await input.trigger('blur')
|
||||||
|
expect(wrapper.emitted('update:rawValue')?.at(-1)).toEqual(['32/13/2026 14:30'])
|
||||||
|
await input.trigger('focus')
|
||||||
|
await wrapper.get('[data-test="day"][data-iso="2026-05-19"]').trigger('click')
|
||||||
|
expect(wrapper.emitted('update:rawValue')?.at(-1)).toEqual([''])
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -103,6 +103,10 @@ const props = withDefaults(
|
|||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'update:modelValue', value: string | null): void
|
(e: 'update:modelValue', value: string | null): void
|
||||||
(e: 'update:valid', value: boolean): void
|
(e: 'update:valid', value: boolean): void
|
||||||
|
// Canal séparé pour la saisie invalide (validation back-autoritative) : texte brut
|
||||||
|
// tel que tapé sur saisie non parsable/hors plage, '' sinon. Ne JAMAIS transiter
|
||||||
|
// par modelValue, qui doit rester ISO|null pour l'affichage et le round-trip.
|
||||||
|
(e: 'update:rawValue', value: string): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
// pendingTime : heure réglée avant qu'un jour ne soit choisi (sinon on ne peut pas émettre).
|
// pendingTime : heure réglée avant qu'un jour ne soit choisi (sinon on ne peut pas émettre).
|
||||||
@@ -129,6 +133,7 @@ function onSelectDay(iso: string) {
|
|||||||
const now = new Date()
|
const now = new Date()
|
||||||
const time = parts.value.time || pendingTime.value || formatTime(now.getHours(), now.getMinutes())
|
const time = parts.value.time || pendingTime.value || formatTime(now.getHours(), now.getMinutes())
|
||||||
setError('')
|
setError('')
|
||||||
|
emit('update:rawValue', '')
|
||||||
emit('update:modelValue', composeDateTime(iso, time))
|
emit('update:modelValue', composeDateTime(iso, time))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,6 +141,7 @@ function onTimeChange(value: string | null) {
|
|||||||
if (!value) return
|
if (!value) return
|
||||||
if (datePart.value) {
|
if (datePart.value) {
|
||||||
setError('')
|
setError('')
|
||||||
|
emit('update:rawValue', '')
|
||||||
emit('update:modelValue', composeDateTime(datePart.value, value))
|
emit('update:modelValue', composeDateTime(datePart.value, value))
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -147,21 +153,25 @@ function onCommit(text: string) {
|
|||||||
const trimmed = text.trim()
|
const trimmed = text.trim()
|
||||||
if (trimmed === '') {
|
if (trimmed === '') {
|
||||||
setError('')
|
setError('')
|
||||||
|
emit('update:rawValue', '')
|
||||||
emit('update:modelValue', null)
|
emit('update:modelValue', null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const iso = parseDisplayToIsoDateTime(trimmed)
|
const iso = parseDisplayToIsoDateTime(trimmed)
|
||||||
if (iso && isDateInRange(iso, props.min, props.max)) {
|
if (iso && isDateInRange(iso, props.min, props.max)) {
|
||||||
setError('')
|
setError('')
|
||||||
|
emit('update:rawValue', '')
|
||||||
emit('update:modelValue', iso)
|
emit('update:modelValue', iso)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setError(props.invalidMessage)
|
setError(props.invalidMessage)
|
||||||
|
emit('update:rawValue', trimmed)
|
||||||
}
|
}
|
||||||
|
|
||||||
function onClear() {
|
function onClear() {
|
||||||
setError('')
|
setError('')
|
||||||
pendingTime.value = ''
|
pendingTime.value = ''
|
||||||
|
emit('update:rawValue', '')
|
||||||
emit('update:modelValue', null)
|
emit('update:modelValue', null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -180,6 +180,9 @@ const props = withDefaults(
|
|||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'clear' | 'close'): void
|
(e: 'clear' | 'close'): void
|
||||||
(e: 'commit', value: string): void
|
(e: 'commit', value: string): void
|
||||||
|
// Mois affiché (month 0-11) : émis à l'ouverture du popover et à chaque
|
||||||
|
// navigation, pour qu'un consommateur (ex. SIRH) charge les données du mois.
|
||||||
|
(e: 'month-change', value: {month: number, year: number}): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const attrs = useAttrs()
|
const attrs = useAttrs()
|
||||||
@@ -229,6 +232,12 @@ watch(isOpen, (value) => {
|
|||||||
if (!value) emit('close')
|
if (!value) emit('close')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Émet le mois affiché tant que le popover est ouvert : une fois à l'ouverture
|
||||||
|
// (isOpen → true, après syncToIso), puis à chaque changement de mois/année.
|
||||||
|
watch([isOpen, currentMonth, currentYear], () => {
|
||||||
|
if (isOpen.value) emit('month-change', {month: currentMonth.value, year: currentYear.value})
|
||||||
|
})
|
||||||
|
|
||||||
const onFieldClick = () => {
|
const onFieldClick = () => {
|
||||||
if (props.disabled || props.readonly) return
|
if (props.disabled || props.readonly) return
|
||||||
if (props.editable) {
|
if (props.editable) {
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
|
||||||
|
import {mount} from '@vue/test-utils'
|
||||||
|
import type {DefineComponent} from 'vue'
|
||||||
|
import MonthGrid from './MonthGrid.vue'
|
||||||
|
|
||||||
|
type MonthGridProps = {
|
||||||
|
month: number
|
||||||
|
year: number
|
||||||
|
selectedDate?: string | null
|
||||||
|
markedDates?: Record<string, 'success' | 'danger'>
|
||||||
|
min?: string
|
||||||
|
max?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const Grid = MonthGrid as DefineComponent<MonthGridProps>
|
||||||
|
const mountGrid = (props: MonthGridProps) => mount(Grid, {props, attachTo: document.body})
|
||||||
|
|
||||||
|
// Récupère la pastille (span rond) qui porte les classes de `cellClass` pour un jour donné.
|
||||||
|
const pill = (wrapper: ReturnType<typeof mountGrid>, iso: string) =>
|
||||||
|
wrapper.get(`[data-iso="${iso}"]`).get('span.rounded-full')
|
||||||
|
|
||||||
|
describe('MalioDateMonthGrid — markedDates', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.useFakeTimers()
|
||||||
|
vi.setSystemTime(new Date(2026, 4, 19)) // 19 mai 2026
|
||||||
|
})
|
||||||
|
afterEach(() => vi.useRealTimers())
|
||||||
|
|
||||||
|
it('applique un fond success sur un jour marqué', () => {
|
||||||
|
const wrapper = mountGrid({month: 4, year: 2026, markedDates: {'2026-05-20': 'success'}})
|
||||||
|
expect(pill(wrapper, '2026-05-20').classes()).toContain('bg-m-success/15')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('applique un fond danger sur un jour marqué', () => {
|
||||||
|
const wrapper = mountGrid({month: 4, year: 2026, markedDates: {'2026-05-21': 'danger'}})
|
||||||
|
expect(pill(wrapper, '2026-05-21').classes()).toContain('bg-m-danger/15')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ne marque pas les jours absents de markedDates', () => {
|
||||||
|
const wrapper = mountGrid({month: 4, year: 2026, markedDates: {'2026-05-20': 'success'}})
|
||||||
|
const classes = pill(wrapper, '2026-05-22').classes()
|
||||||
|
expect(classes).not.toContain('bg-m-success/15')
|
||||||
|
expect(classes).not.toContain('bg-m-danger/15')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('précédence : la sélection (primary) prime sur la variante marquée', () => {
|
||||||
|
const wrapper = mountGrid({
|
||||||
|
month: 4,
|
||||||
|
year: 2026,
|
||||||
|
selectedDate: '2026-05-22',
|
||||||
|
markedDates: {'2026-05-22': 'success'},
|
||||||
|
})
|
||||||
|
const classes = pill(wrapper, '2026-05-22').classes()
|
||||||
|
expect(classes).toContain('bg-m-primary')
|
||||||
|
expect(classes).toContain('text-white')
|
||||||
|
expect(classes).not.toContain('bg-m-success/15')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('today marqué : garde sa bordure ET reçoit le fond marqué', () => {
|
||||||
|
const wrapper = mountGrid({month: 4, year: 2026, markedDates: {'2026-05-19': 'success'}})
|
||||||
|
const classes = pill(wrapper, '2026-05-19').classes()
|
||||||
|
expect(classes).toContain('border-m-primary')
|
||||||
|
expect(classes).toContain('bg-m-success/15')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('today non marqué : bordure sans fond marqué', () => {
|
||||||
|
const wrapper = mountGrid({month: 4, year: 2026, markedDates: {'2026-05-20': 'success'}})
|
||||||
|
const classes = pill(wrapper, '2026-05-19').classes()
|
||||||
|
expect(classes).toContain('border-m-primary')
|
||||||
|
expect(classes).not.toContain('bg-m-success/15')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -84,6 +84,14 @@ import {dayRangeRole, resolveRangeBounds, type DayRangeRole} from '../composable
|
|||||||
|
|
||||||
defineOptions({name: 'MalioDateMonthGrid'})
|
defineOptions({name: 'MalioDateMonthGrid'})
|
||||||
|
|
||||||
|
// Statut générique par jour : aucune sémantique métier dans le layer, juste un
|
||||||
|
// fond tokenisé. `success` et `danger` suffisent pour l'instant (MUI-45).
|
||||||
|
type MarkedVariant = 'success' | 'danger'
|
||||||
|
const markedBg: Record<MarkedVariant, string> = {
|
||||||
|
success: 'bg-m-success/15',
|
||||||
|
danger: 'bg-m-danger/15',
|
||||||
|
}
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
month: number
|
month: number
|
||||||
@@ -94,6 +102,7 @@ const props = withDefaults(
|
|||||||
previewDate?: string | null
|
previewDate?: string | null
|
||||||
interactiveWeekNumber?: boolean
|
interactiveWeekNumber?: boolean
|
||||||
markedWeekStart?: string | null
|
markedWeekStart?: string | null
|
||||||
|
markedDates?: Record<string, MarkedVariant>
|
||||||
min?: string
|
min?: string
|
||||||
max?: string
|
max?: string
|
||||||
}>(),
|
}>(),
|
||||||
@@ -104,6 +113,7 @@ const props = withDefaults(
|
|||||||
previewDate: undefined,
|
previewDate: undefined,
|
||||||
interactiveWeekNumber: false,
|
interactiveWeekNumber: false,
|
||||||
markedWeekStart: null,
|
markedWeekStart: null,
|
||||||
|
markedDates: undefined,
|
||||||
min: undefined,
|
min: undefined,
|
||||||
max: undefined,
|
max: undefined,
|
||||||
},
|
},
|
||||||
@@ -165,6 +175,10 @@ const cellClass = (cell: DayCell) => {
|
|||||||
if (role === 'start' || role === 'end' || role === 'single') return 'bg-m-primary text-white'
|
if (role === 'start' || role === 'end' || role === 'single') return 'bg-m-primary text-white'
|
||||||
if (role === 'in-range') return 'text-black'
|
if (role === 'in-range') return 'text-black'
|
||||||
const parts = ['hover:bg-m-primary/10']
|
const parts = ['hover:bg-m-primary/10']
|
||||||
|
// Précédence : sélection/range (primary, return ci-dessus) > variante marquée > défaut.
|
||||||
|
// `today` n'est pas exclusif : il garde sa bordure ET peut recevoir le fond marqué.
|
||||||
|
const marked = props.markedDates?.[cell.isoDate]
|
||||||
|
if (marked) parts.push(markedBg[marked])
|
||||||
if (cell.isToday) parts.push('border border-m-primary text-m-primary')
|
if (cell.isToday) parts.push('border border-m-primary text-m-primary')
|
||||||
else if (cell.isCurrentMonth) parts.push('text-black')
|
else if (cell.isCurrentMonth) parts.push('text-black')
|
||||||
else parts.push('opacity-[60%]')
|
else parts.push('opacity-[60%]')
|
||||||
|
|||||||
@@ -82,6 +82,20 @@
|
|||||||
success="Enregistrée"
|
success="Enregistrée"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-lg border p-4">
|
||||||
|
<h2 class="mb-4 text-xl font-bold">Statuts par jour (markedDates) + @month-change</h2>
|
||||||
|
<MalioDate
|
||||||
|
v-model="markedValue"
|
||||||
|
label="Jours validés / à corriger"
|
||||||
|
hint="Ouvre le calendrier : jours verts (success) et rouges (danger)"
|
||||||
|
:marked-dates="markedDates"
|
||||||
|
@month-change="onMonthChange"
|
||||||
|
/>
|
||||||
|
<p class="mt-2 text-sm text-m-muted">
|
||||||
|
Mois affiché : <code>{{ shownMonth }}</code>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Story>
|
</Story>
|
||||||
</template>
|
</template>
|
||||||
@@ -102,4 +116,20 @@ const initialValue = ref<string | null>(todayIso)
|
|||||||
const boundedValue = ref<string | null>(null)
|
const boundedValue = ref<string | null>(null)
|
||||||
const errorValue = ref<string | null>(null)
|
const errorValue = ref<string | null>(null)
|
||||||
const editableValue = ref<string | null>(null)
|
const editableValue = ref<string | null>(null)
|
||||||
|
|
||||||
|
const ym = `${now.getFullYear()}-${pad(now.getMonth() + 1)}`
|
||||||
|
const markedDates = ref<Record<string, 'success' | 'danger'>>({
|
||||||
|
[`${ym}-05`]: 'success',
|
||||||
|
[`${ym}-06`]: 'success',
|
||||||
|
[`${ym}-12`]: 'success',
|
||||||
|
[`${ym}-09`]: 'danger',
|
||||||
|
[`${ym}-20`]: 'danger',
|
||||||
|
})
|
||||||
|
const markedValue = ref<string | null>(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}`
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user