8.8 KiB
MalioDateWeek — Design Spec
Composant de sélection d'une semaine ISO complète (lundi→dimanche) via le shell calendrier partagé. Troisième brique de la famille temporelle.
Ticket : MUI-33 (suite)
Branche : feature/MUI-33-developper-le-composant-datepicker
Specs liées : 2026-05-19-datepicker-design.md, 2026-05-20-daterange-design.md
Périmètre
Sélection d'une semaine entière en un clic (sur n'importe quel jour OU le numéro de semaine). Visuellement : la ligne de la semaine se surligne en pilule bg-m-primary-light (lundi arrondi gauche, dimanche arrondi droit), exactement comme une plage DateRange figée lun→dim. Survol = aperçu de la semaine. La cellule n° de la semaine sélectionnée passe en bg-m-primary (repère).
Inclus : clic jour/n° → semaine, hover de semaine, surlignage pilule, repère n° semaine, bornes min/max (semaine sélectionnable si elle chevauche), effacement, vue mois conservée.
Reporté : deux mois, saisie/navigation clavier.
Donnée retournée
modelValue: string | null au format ISO 8601 semaine YYYY-Www (ex. "2026-W21"), comme l'<input type="week"> natif. L'année est l'année ISO de numérotation (peut différer de l'année calendaire aux bords d'année). Affichage humain dans le champ : "Semaine 21 (18/05 → 24/05/2026)" (le modelValue reste 2026-W21).
Architecture (Approche 1 — réutilisation du rendu plage)
Une semaine sélectionnée est une plage lundi→dimanche : on réutilise le rendu pilule de MonthGrid (mode plage) en passant les bornes de la semaine active. Les events select/hover (jour) sont réutilisés ; l'enveloppe DateWeek mappe jour → semaine.
app/components/malio/date/
DateWeek.vue # NOUVEAU — enveloppe
DateWeek.test.ts # nouveau
internal/
MonthGrid.vue # étendu : interactiveWeekNumber + markedWeekStart (additifs)
CalendarField.vue # inchangé (shell réutilisé)
CalendarHeader.vue # inchangé
MonthPicker.vue # inchangé
composables/
dateWeek.ts # NOUVEAU — helpers semaine ISO (purs)
dateWeek.test.ts # nouveau
dateRange.ts # inchangé (rendu pilule réutilisé)
dateFormat.ts # inchangé
useCalendarView.ts # inchangé
useCalendarPopover.ts # inchangé
useMonthMatrix.ts # inchangé
app/story/date/dateWeek.story.vue
.playground/pages/composant/date/dateWeek.vue
Flux :
DateWeek.vue (enveloppe)
├─ état : hoverWeekStart (lundi de la semaine survolée)
├─ validWeek = isValidIsoWeek(modelValue) ? { monday: isoWeekToMonday(modelValue) } : null
├─ activeMonday = hoverWeekStart ?? validWeek.monday → activeSunday = sundayOf(activeMonday)
├─ displayValue = formatWeekDisplay(modelValue)
└─ <CalendarField :display-value :sync-to=validWeek.monday @clear @close>
└─ <MonthGrid
:range-start=activeMonday :range-end=activeSunday ← pilule lun→dim réutilisée
:marked-week-start=validWeek.monday ← repère n° semaine
interactive-week-number ← n° semaine cliquable/hoverable
:min :max
@select="(iso)=>onSelect(iso, close)" @hover="onHover" />
dateWeek.ts (helpers purs)
function mondayOf(iso: string): string // "2026-05-20" → "2026-05-18"
function sundayOf(iso: string): string // "2026-05-20" → "2026-05-24"
function toIsoWeek(iso: string): string // "2026-05-20" → "2026-W21" (année ISO + n° semaine)
function isoWeekToMonday(week: string): string | null // "2026-W21" → "2026-05-18" ; invalide → null
function isValidIsoWeek(week: string): boolean // "2026-W21" → true ; "2026-W54"/"2026-21" → false
function formatWeekDisplay(week: string): string // "2026-W21" → "Semaine 21 (18/05 → 24/05/2026)" ; invalide → ""
- Algo ISO 8601 : jeudi de la semaine pour l'année de numérotation ; lundi de la semaine contenant le 4 janvier = semaine 1.
formatWeekDisplay:Semaine {n° sans zéro} ({JJ/MM lundi} → {JJ/MM/AAAA dimanche}), réutiliseformatIsoToDisplay.- Cas pièges testés :
2025-12-31→2026-W01,2027-01-01→2026-W53,2026-01-01→2026-W01.
MonthGrid.vue — ajouts additifs
Nouvelles props optionnelles (n'altèrent pas les modes simple/plage) :
interactiveWeekNumber?: boolean // défaut false
markedWeekStart?: string | null // défaut null — lundi de la semaine repère
Quand interactiveWeekNumber === true :
- La cellule n° de semaine devient un
<button>:@clickémetselect(week.days[0].isoDate),@mouseenterémethover(week.days[0].isoDate).:disabledsi!weekSelectable, oùweekSelectable = week.days.some(d => inRange(d.isoDate)).cursor-pointersi sélectionnable. - Repère : si
week.days[0].isoDate === markedWeekStart, la cellule n° passe enbg-m-primary text-white(au lieu debg-m-primary-light).
Toujours (inoffensif hors mode semaine) : la cellule n° porte :data-week-start="week.days[0].isoDate" et :data-marked="week.days[0].isoDate === markedWeekStart".
Inchangé : rendu pilule des jours piloté par rangeStart/rangeEnd ; events select/hover jour ; quand interactiveWeekNumber est false, la cellule n° reste un <div> non cliquable (aucune régression Date/DateRange).
DateWeek.vue (enveloppe)
Props : identiques à Date sauf modelValue?: string | null (YYYY-Www).
Emit : update:modelValue → string | null.
État :
hoverWeekStart = ref<string | null>(null)
const validWeek = computed(() =>
(props.modelValue && isValidIsoWeek(props.modelValue))
? {monday: isoWeekToMonday(props.modelValue) as string}
: null)
const activeMonday = computed(() => hoverWeekStart.value ?? validWeek.value?.monday ?? null)
const activeSunday = computed(() => activeMonday.value ? sundayOf(activeMonday.value) : null)
Passé à MonthGrid : range-start=activeMonday, range-end=activeSunday, marked-week-start=validWeek?.monday ?? null, interactive-week-number, min, max, month/year du slot.
displayValue = validWeek ? formatWeekDisplay(modelValue) : ''. syncTo = validWeek?.monday ?? null.
Comportement (1 clic) :
onSelect(iso, close): # jour OU n° de semaine (= lundi)
emit('update:modelValue', toIsoWeek(iso))
hoverWeekStart = null
close()
onHover(iso): # jour/n° survolé ; null au mouseleave
hoverWeekStart = iso ? mondayOf(iso) : null
onClose(): hoverWeekStart = null
onClear(): emit('update:modelValue', null) ; hoverWeekStart = null
- Survol → toute la ligne en pilule via
activeMonday. - Sélection en un clic →
YYYY-Www+ fermeture. - Repère de la semaine committée conservé pendant le survol d'une autre.
modelValueinvalide → traité commenull+ warning dev.
Tests
dateWeek.test.ts (~14)
mondayOf/sundayOf: mercredi, lundi (idempotent), dimanchetoIsoWeek: nominal + bords d'année (2025-12-31→2026-W01,2027-01-01→2026-W53,2026-01-01→2026-W01)isoWeekToMonday:2026-W21→2026-05-18; round-trip ; invalide → nullisValidIsoWeek: valide /W00/W54/ format fauxformatWeekDisplay:2026-W21→"Semaine 21 (18/05 → 24/05/2026)"; invalide →""
DateWeek.test.ts (~14, setSystemTime(2026-05-19))
- Rendu label/icône, affichage
"Semaine ..."si modelValue, champ vide sinon - Ouverture sur le mois de la semaine du modelValue
- Clic d'un jour → émet le
YYYY-Wwwde sa semaine + ferme - Clic du n° de semaine (
[data-test="week-number"][data-week-start="..."]) → émet + ferme - Hover d'un jour →
data-range-rolestart/in-range/end sur les 7 jours de la ligne ; autre lignenone - Hover du n° de semaine → même surlignage
- Semaine committée → roles corrects +
data-marked="true"sur la cellule n° clear→ émetnull- min/max : semaine hors bornes n° désactivé + jours non cliquables ; semaine qui chevauche reste sélectionnable
disabled/readonly→ pas d'ouverture- a11y :
aria-invalidsur error
Date.test.ts / DateRange.test.ts restent verts (props additives).
Story dateWeek.story.vue
Default vide, semaine initiale, min/max, états (disabled/readonly/error/success), non-clearable.
Playground .playground/pages/composant/date/dateWeek.vue
Comparatif Large (480px) / ERP (396px), affichage modelValue + bornes du champ, boutons set/reset, cas borné.
Découpage d'implémentation
dateWeek.ts(purs) + tests- Extension
MonthGrid.vue(interactiveWeekNumber + markedWeekStart + data attrs) DateWeek.vue+DateWeek.test.ts- Story + playground