Files
malio-layer-ui/docs/superpowers/specs/2026-06-22-date-year-picker-design.md
T
tristan 28705c8285
Release / release (push) Successful in 1m18s
fix: date year picker (#84)
| 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>
2026-06-22 09:29:59 +00:00

7.5 KiB
Raw Blame History

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)

  1. Grille d'années : 12 ans en grid-cols-3 (4 lignes), calquée sur MonthPicker, avec chevrons de pagination ±12 ans.
  2. Header contextuel : libellé adapté à la vue (voir tableau). Chevron-bas masqué en vue years (clic neutralisé).
  3. min/max respectés : le sélecteur d'année grise les années hors [min, max], ET on corrige le MonthPicker existant pour qu'il grise aussi les mois hors plage (asymétrie actuelle : aujourd'hui tous les mois sont cliquables).
  4. Cadrage de la fenêtre d'années : centrée, yearPageStart = currentYear 5 (fenêtre [courante5 … 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() par goToHigherView() (zoom arrière) : days → months, months → years, years → years (no-op).
  • open() / close() repartent en days (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 en years, recentrer yearPageStart = currentYear 5.
  • goToPrev / goToNext : branche yearsyearPageStart ∓ 12 (months → année ±1, days → mois ±1 : inchangés).
  • Nouveau selectYear(y)currentYear = y.

3. Header — internal/CalendarHeader.vue

  • Props : ajouter yearPageStart: number, élargir viewMode.
  • label calculé :
    • days → « Mai 2026 »
    • months → « 2026 »
    • years`${yearPageStart} ${yearPageStart + 11}`
  • Chevron-bas (mdi:chevron-down) masqué en vue years ; le bouton n'émet toggle-view que si viewMode !== '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 MonthPickerinternal/MonthPicker.vue

  • Props : ajouter currentYear: number, min?: string, max?: string.
  • Pour chaque mois index : disabled = !isMonthInRange(currentYear, index, min, max) → même traitement disabled que YearPicker.

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 props min?: string, max?: string.
  • Récupérer yearPageStart, selectYear, goToHigherView des 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 vue days.
    • onSelectYear(y)selectYear(y) puis vue months.
  • Les 4 consommateurs bindent :min / :max sur <CalendarField> (déjà disponibles comme props ISO ; DateTime tronque 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 pas currentYear → pas d'émission parasite. Sélectionner une année change currentYear → un month-change est émis (comportement attendu : le consommateur peut recharger les données).
  • Pas de navigation clavier dans les grilles (le MonthPicker actuel n'en a pas non plus) — hors scope.
  • Escape ferme 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 cycle goToHigherView (days→months→years→years), close() réinitialise à days.
  • useCalendarView.test.ts : nav années yearPageStart ±12, selectYear, recentrage à l'entrée en vue years.
  • MonthPicker.test.ts : mois hors plage grisés / non émis.
  • YearPicker.test.ts (nouveau) : rend 12 années depuis pageStart, année sélectionnée, années hors plage grisées, émet select.
  • Non-régression : Date.test.ts, DateRange.test.ts, DateTime.test.ts, DateWeek.test.ts doivent rester verts ; ajouter au moins un test de bout en bout du flux days → months → years → months → days dans Date.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.