| Numéro du ticket | Titre du ticket | |------------------|-----------------| | | | ## Description de la PR ## Modification du .env ## Check list - [x] Pas de régression - [x] TU/TI/TF rédigée - [x] TU/TI/TF OK - [x] CHANGELOG modifié Reviewed-on: #50 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
7.4 KiB
MalioDateTime — Design (version intérimaire)
Date : 2026-05-22 Ticket : #MUI-33 (famille Datepicker) Statut : validé, prêt pour le plan d'implémentation
Objectif
Ajouter un composant MalioDateTime à la famille temporelle de @malio/layer-ui, permettant de saisir une date ET une heure dans un seul champ avec popover.
Cette version est intérimaire : le sélecteur d'heure utilise un <input type="time"> natif, le temps que la maquette du sélecteur d'heure dédié soit fournie. Le bloc heure est volontairement isolé pour pouvoir être remplacé sans toucher au reste.
Valeur du modèle
modelValue: string | null au format ISO naïf sans fuseau : "YYYY-MM-DDTHH:MM:00" (ex. "2026-05-20T14:30:00").
- Heure murale locale : un picker n'a pas de notion de fuseau.
- Symfony (
DateTimeNormalizer, RFC 3339) parse ce format et applique son fuseau configuré → zéro bug TZ/DST côté front. - Les secondes sont toujours
00(le natiftype="time"par défaut n'expose pas les secondes). - Cohérent avec
MalioDate(YYYY-MM-DD) etMalioTime(HH:MM).
Architecture
Fine enveloppe autour du shell partagé internal/CalendarField.vue, comme MalioDate / MalioDateRange / MalioDateWeek.
MalioDateTime (Date/DateTime.vue)
└─ CalendarField (champ + popover + header + navigation mois)
slot #default={ currentMonth, currentYear, close }
├─ MonthGrid (sélection du jour)
└─ <input type="time"> (sélection de l'heure, sous la grille)
Le close du slot n'est pas appelé sur sélection (contrairement à MalioDate) : on a besoin de date ET heure, la fermeture se fait au clic extérieur (déjà gérée par CalendarField via useCalendarPopover).
Props
Identiques à MalioDate :
| Prop | Type | Défaut | Note |
|---|---|---|---|
id |
string |
'' |
|
name |
string |
'' |
|
label |
string |
'' |
|
modelValue |
string | null |
undefined |
"YYYY-MM-DDTHH:MM:00" |
placeholder |
string |
'JJ/MM/AAAA HH:MM' |
|
required |
boolean |
false |
|
disabled |
boolean |
false |
|
readonly |
boolean |
false |
|
hint |
string |
'' |
|
error |
string |
'' |
|
success |
string |
'' |
|
min |
string |
undefined |
datetime ou date ; on borne la grille avec la partie date |
max |
string |
undefined |
idem |
clearable |
boolean |
true |
|
inputClass / labelClass / groupClass |
string |
'' |
Émissions : update:modelValue: string | null.
Affichage du champ
displayValue = formatIsoDateTimeToDisplay(modelValue) → "JJ/MM/AAAA HH:MM" (ex. 20/05/2026 14:30). Chaîne vide si modelValue nul ou invalide.
Flux de sélection
État : modelValue est la source de vérité. Une ref locale pendingTime: string ('HH:MM' ou '') sert de pont quand l'heure est réglée avant qu'un jour soit choisi.
- Partie date courante =
splitDateTime(modelValue).date(ounull). - Valeur de l'
<input type="time">=splitDateTime(modelValue).timesi présent, sinonpendingTime.
Interactions :
- Clic sur un jour
iso:heureEffective= partie heure demodelValue, sinonpendingTime, sinon'00:00'.- émet
composeDateTime(iso, heureEffective)→"iso T heure:00". - le popover reste ouvert.
- Changement de l'
<input type="time">(hhmm) :- si une partie date existe → émet
composeDateTime(datePart, hhmm). - sinon → stocke
hhmmdanspendingTime(pas d'émission tant qu'aucun jour n'est choisi). - si
hhmmest vidé ('') et qu'une date existe → on garde la date avec00:00? Non : on n'émet rien sur vidage, on conserve la dernière valeur émise (le natif renvoie rarement''une fois rempli ; cas négligé pour l'intérim).
- si une partie date existe → émet
- Effacer (croix) : émet
null,pendingTime = ''.
Bornes min/max : passées à MonthGrid après slice de la partie date (min?.slice(0, 10)). Pas de borne sur l'heure dans cette version.
Composable composables/datetimeFormat.ts
Réutilise isValidIso de dateFormat.ts pour valider la partie date.
// "YYYY-MM-DDTHH:MM:00" complet et valide ?
export function isValidIsoDateTime(s: string): boolean
// "YYYY-MM-DDTHH:MM:00" -> "JJ/MM/AAAA HH:MM" (chaîne vide si invalide/nul)
export function formatIsoDateTimeToDisplay(s: string | null): string
// "YYYY-MM-DDTHH:MM:00" -> { date: "YYYY-MM-DD" | null, time: "HH:MM" | '' }
export function splitDateTime(s: string | null): { date: string | null; time: string }
// (date "YYYY-MM-DD", time "HH:MM") -> "YYYY-MM-DDTHH:MM:00"
export function composeDateTime(date: string, time: string): string
Règles :
isValidIsoDateTime: regex^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$, partie date valide viaisValidIso, heure00–23, minutes00–59, secondes00–59.splitDateTime: sisnul ou ne matche pas le motif datetime →{ date: null, time: '' }. Sinon découpe surT, renvoie la date etHH:MM(5 premiers car. de la partie heure).composeDateTime(date, time):${date}T${time}:00. Sitimevide →${date}T00:00:00.formatIsoDateTimeToDisplay: si!isValidIsoDateTime→''. Sinon${dd}/${mm}/${yyyy} ${hh}:${min}.
Tests
Colocalisés, Vitest + @vue/test-utils (jsdom), vi.setSystemTime(new Date(2026, 4, 19)) pour déterminisme.
datetimeFormat.test.ts (helpers purs) :
isValidIsoDateTime: accepte"2026-05-20T14:30:00"; rejette"2026-05-20","2026-13-01T00:00:00","2026-05-20T24:00:00","2026-05-20T14:60:00","", format sans secondes.formatIsoDateTimeToDisplay:"2026-05-20T14:30:00"→"20/05/2026 14:30"; nul/invalide →''.splitDateTime:"2026-05-20T14:30:00"→{ date: "2026-05-20", time: "14:30" };null→{ date: null, time: '' };"2026-05-20"(date seule, non datetime) →{ date: null, time: '' }.composeDateTime:("2026-05-20", "14:30")→"2026-05-20T14:30:00";("2026-05-20", "")→"2026-05-20T00:00:00".
DateTime.test.ts (composant) :
- Rendu : champ présent, placeholder
JJ/MM/AAAA HH:MM,displayValuecorrect quandmodelValuefourni. - Ouverture popover au clic →
MonthGrid+input[type=time]visibles. - Clic sur un jour sans heure → émet
"<iso>T00:00:00", popover reste ouvert. - Clic sur un jour avec
pendingTimeréglé d'abord → émet"<iso>T<pendingTime>:00". - Changement de l'heure avec date déjà choisie → émet
"<date>T<nouvelleHeure>:00". - Changement de l'heure sans date → aucune émission.
clearable: croix émetnull.min/maxdatetime → jours hors bornes désactivés dans la grille.- Accessibilité : label lié,
aria-invalidsur erreur.
Livrables
app/components/malio/date/composables/datetimeFormat.tsapp/components/malio/date/composables/datetimeFormat.test.tsapp/components/malio/date/DateTime.vueapp/components/malio/date/DateTime.test.ts- Page playground
.playground/pages/composant/date/datetime.vue+ entrée nav (playground.nav.ts) - Story
app/story/date/dateTime.story.vue COMPONENTS.md(section MalioDateTime)CHANGELOG.md(ligne sous### Added)
Hors périmètre (intérim)
- Sélecteur d'heure dédié / maquette → itération ultérieure ; on remplacera le bloc
<input type="time">isolé. - Bornes horaires (min/max sur l'heure).
- Pas de minutes configurable (granularité native du navigateur).
- Secondes dans l'UI.