| Numéro du ticket | Titre du ticket | |------------------|-----------------| | | | ## Description de la PR ## Modification du .env ## Check list - [ ] Pas de régression - [ ] TU/TI/TF rédigée - [ ] TU/TI/TF OK - [ ] CHANGELOG modifié --------- Co-authored-by: admin malio <malio@yuno.malio.fr> Co-authored-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr> Co-authored-by: matthieu <matthieu@yuno.malio.fr> Reviewed-on: #84 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
7.5 KiB
Sélecteur d'année dans le calendrier (3ᵉ niveau de navigation)
Date : 2026-06-22
Statut : Design validé
Composants concernés : famille date/ (Date, DateRange, DateTime, DateWeek) via le shell partagé internal/CalendarField.vue
Contexte & objectif
Aujourd'hui le calendrier a deux vues : la grille de jours (days) et le sélecteur de
mois (months). En cliquant sur le libellé « Mai 2026 » du header, on bascule entre les
deux (toggleView).
On ajoute un 3ᵉ niveau : depuis la vue mois, recliquer sur le header ouvre un sélecteur d'année, visuellement calqué sur le sélecteur de mois. Le tout doit fonctionner pour les 4 composants de la famille sans casser leur API publique.
Flux cible
| Vue courante | Header affiche | Clic header → | Clic sur une cellule → |
|---|---|---|---|
days |
« Mai 2026 » | months |
(jour) sélection + fermeture |
months |
« 2026 » | years |
days (mois choisi) |
years |
« 2020 – 2031 » | rien (niveau le plus haut) | months (année choisie) |
Décisions de design (validées)
- Grille d'années : 12 ans en
grid-cols-3(4 lignes), calquée surMonthPicker, avec chevrons de pagination±12 ans. - Header contextuel : libellé adapté à la vue (voir tableau). Chevron-bas masqué en
vue
years(clic neutralisé). - min/max respectés : le sélecteur d'année grise les années hors
[min, max], ET on corrige leMonthPickerexistant pour qu'il grise aussi les mois hors plage (asymétrie actuelle : aujourd'hui tous les mois sont cliquables). - Cadrage de la fenêtre d'années : centrée,
yearPageStart = currentYear − 5(fenêtre[courante−5 … courante+6]), l'année courante apparaît ~au milieu.
Architecture
Le shell CalendarField orchestre l'input, le popover, le header et la commutation de
vues. MonthPicker et le nouveau YearPicker sont rendus dans CalendarField
(contrairement à la grille de jours, fournie par chaque consommateur via slot scoped).
1. Machine à états des vues — composables/useCalendarPopover.ts
viewMode:'days' | 'months'→'days' | 'months' | 'years'.- Remplacer
toggleView()pargoToHigherView()(zoom arrière) :days → months,months → years,years → years(no-op). open()/close()repartent endays(inchangé).
2. Navigation & fenêtre d'années — composables/useCalendarView.ts
- Élargir le type
viewModeà'days' | 'months' | 'years'. - Nouveau ref
yearPageStart. watch(viewMode): à l'entrée enyears, recentreryearPageStart = currentYear − 5.goToPrev/goToNext: brancheyears→yearPageStart ∓ 12(months→ année ±1,days→ mois ±1 : inchangés).- Nouveau
selectYear(y)→currentYear = y.
3. Header — internal/CalendarHeader.vue
- Props : ajouter
yearPageStart: number, élargirviewMode. labelcalculé :days→ « Mai 2026 »months→ « 2026 »years→`${yearPageStart} – ${yearPageStart + 11}`
- Chevron-bas (
mdi:chevron-down) masqué en vueyears; le bouton n'émettoggle-viewque siviewMode !== 'years'. - aria-labels prev/next adaptés : « Période précédente/suivante » en vue
years.
4. Nouveau composant — internal/YearPicker.vue
Calque de MonthPicker :
- Props :
pageStart: number,selectedYear?: number,min?: string,max?: string. years = Array.from({length: 12}, (_, i) => pageStart + i).- Pour chaque année :
disabled = !isYearInRange(year, min, max)→disabled,aria-disabled, style muet (text-m-muted/30,cursor-not-allowed), pas d'émission. - Année sélectionnée :
bg-m-primary text-white(comme le mois sélectionné). - Émet
select(year: number). defineOptions({name: 'MalioDateYearPicker'}).- Attributs de test :
data-test="year-picker",data-test="year",data-year.
5. Correction MonthPicker — internal/MonthPicker.vue
- Props : ajouter
currentYear: number,min?: string,max?: string. - Pour chaque mois
index:disabled = !isMonthInRange(currentYear, index, min, max)→ même traitement disabled queYearPicker.
6. Helpers purs — composables/dateFormat.ts
Comparaison par préfixe ISO (les chaînes ISO se comparent lexicographiquement) :
export function isMonthInRange(year: number, month: number, min?: string, max?: string): boolean {
const ym = `${year}-${String(month + 1).padStart(2, '0')}`
if (min && ym < min.slice(0, 7)) return false
if (max && ym > max.slice(0, 7)) return false
return true
}
export function isYearInRange(year: number, min?: string, max?: string): boolean {
if (min && year < Number(min.slice(0, 4))) return false
if (max && year > Number(max.slice(0, 4))) return false
return true
}
7. Câblage — internal/CalendarField.vue + consommateurs
CalendarField: ajouter propsmin?: string,max?: string.- Récupérer
yearPageStart,selectYear,goToHigherViewdes composables. - Template du popover :
<CalendarHeader … :year-page-start="yearPageStart" @toggle-view="goToHigherView" />- slot jours
v-if="viewMode === 'days'"(inchangé) <MonthPicker v-else-if="viewMode === 'months'" :selected-month :current-year :min :max @select="onSelectMonth" /><YearPicker v-else :page-start="yearPageStart" :selected-year="currentYear" :min :max @select="onSelectYear" />
- Handlers :
onSelectMonth(m)→selectMonth(m)puis vuedays.onSelectYear(y)→selectYear(y)puis vuemonths.
- Les 4 consommateurs bindent
:min/:maxsur<CalendarField>(déjà disponibles comme props ISO ;DateTimetronque via.slice(0, 10)). Aucune API publique ne change.
Effets de bord & cohérence
month-change(émis sur[isOpen, currentMonth, currentYear]) : la pagination d'années ne touche pascurrentYear→ pas d'émission parasite. Sélectionner une année changecurrentYear→ unmonth-changeest émis (comportement attendu : le consommateur peut recharger les données).- Pas de navigation clavier dans les grilles (le
MonthPickeractuel n'en a pas non plus) — hors scope. Escapeferme le popover quelle que soit la vue (inchangé).
Plan de tests (TDD)
dateFormat.test.ts:isMonthInRange,isYearInRange(bornes, sans min/max, hors plage par année et par mois).useCalendarPopover.test.ts: nouveau cyclegoToHigherView(days→months→years→years),close()réinitialise àdays.useCalendarView.test.ts: nav annéesyearPageStart ±12,selectYear, recentrage à l'entrée en vueyears.MonthPicker.test.ts: mois hors plage grisés / non émis.YearPicker.test.ts(nouveau) : rend 12 années depuispageStart, année sélectionnée, années hors plage grisées, émetselect.- Non-régression :
Date.test.ts,DateRange.test.ts,DateTime.test.ts,DateWeek.test.tsdoivent rester verts ; ajouter au moins un test de bout en bout du fluxdays → months → years → months → daysdansDate.test.ts. - Déterminisme :
vi.setSystemTime(new Date(2026, 4, 19))comme l'existant.
Livrables doc (convention projet)
- Maj manuelle de
COMPONENTS.md(documenter le 3ᵉ niveau du calendrier). - Entrée
CHANGELOG.md.
Non-objectifs (YAGNI)
- Pas de sélecteur de décennie/siècle (4ᵉ niveau).
- Pas de saisie clavier directe de l'année dans la grille.
- Pas de navigation au clavier (flèches) dans les grilles mois/années.