feat : sélecteur d'année dans le calendrier (3ᵉ niveau) (#83)
## Sélecteur d'année dans le calendrier (3ᵉ niveau de navigation) Ajoute un 3ᵉ niveau de navigation à la famille de composants date, et corrige le bornage min/max du sélecteur de mois. ### Comportement - Clic sur le champ → calendrier (vue **jours**) - Clic sur l'en-tête → **sélecteur de mois** - **Re-clic sur l'en-tête → sélecteur d'année** (grille de 12 ans, chevrons paginant par pas de 12 ans, fenêtre centrée sur l'année courante − 5) - Clic sur une année → retour au sélecteur de mois ; clic sur un mois → retour à la grille de jours - Les props `min`/`max` **grisent les mois ET les années** hors plage (corrige l'asymétrie : le `MonthPicker` affichait jusqu'ici tous les mois) En-tête contextuel : « Mai 2026 » (jours) / « 2026 » (mois) / « 2020 – 2031 » (années). ### Périmètre - Shell partagé `internal/CalendarField.vue` → bénéficie aux 4 composants publics `Date`, `DateRange`, `DateTime`, `DateWeek` - **Aucune API publique modifiée** - Nouveau composant `internal/YearPicker.vue` (calqué sur `MonthPicker`) - Helpers purs `isMonthInRange` / `isYearInRange` (comparaison par préfixe ISO, bornes inclusives) - State machine `viewMode` à 3 niveaux (`useCalendarPopover` / `useCalendarView`) ### Tests - Suite date **246/246 verte**, ESLint propre - Unitaires : helpers, `YearPicker`, `MonthPicker` (grisage), composables (pagination ±12, recentrage, `selectYear`) - e2e `Date.test.ts` : flux complet jours→mois→années→mois→jours + grisage min/max ### Process Développé en brainstorming → spec → plan → exécution TDD (un commit par étape). Spec et plan inclus sous `docs/superpowers/`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Reviewed-on: #83 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
This commit was merged in pull request #83.
This commit is contained in:
@@ -185,6 +185,67 @@ describe('MalioDate', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('vue années', () => {
|
||||
it('opens the year picker on second header toggle, current year centered (2nd row/2nd col)', async () => {
|
||||
const wrapper = mountDate()
|
||||
await wrapper.get('[data-test="date-input"]').trigger('click')
|
||||
await wrapper.get('[data-test="header-toggle"]').trigger('click') // -> mois
|
||||
await wrapper.get('[data-test="header-toggle"]').trigger('click') // -> années
|
||||
expect(wrapper.find('[data-test="year-picker"]').exists()).toBe(true)
|
||||
// Le libellé reste « Mois Année » dans toutes les vues.
|
||||
expect(wrapper.get('[data-test="header-toggle"]').text()).toContain('Mai 2026')
|
||||
// Année courante (2026) en 2e ligne / 2e colonne d'une grille 3 colonnes = index 4.
|
||||
const years = wrapper.findAll('[data-test="year"]')
|
||||
expect(years[4].attributes('data-year')).toBe('2026')
|
||||
expect(years[0].attributes('data-year')).toBe('2022')
|
||||
})
|
||||
|
||||
it('cycles back to day view on third header toggle', async () => {
|
||||
const wrapper = mountDate()
|
||||
await wrapper.get('[data-test="date-input"]').trigger('click')
|
||||
await wrapper.get('[data-test="header-toggle"]').trigger('click') // -> mois
|
||||
await wrapper.get('[data-test="header-toggle"]').trigger('click') // -> années
|
||||
await wrapper.get('[data-test="header-toggle"]').trigger('click') // -> jours
|
||||
expect(wrapper.find('[data-test="year-picker"]').exists()).toBe(false)
|
||||
expect(wrapper.find('[data-test="month-picker"]').exists()).toBe(false)
|
||||
expect(wrapper.get('[data-test="header-toggle"]').text()).toContain('Mai 2026')
|
||||
})
|
||||
|
||||
it('navigates days -> months -> years -> months -> days', async () => {
|
||||
const wrapper = mountDate()
|
||||
await wrapper.get('[data-test="date-input"]').trigger('click')
|
||||
await wrapper.get('[data-test="header-toggle"]').trigger('click')
|
||||
await wrapper.get('[data-test="header-toggle"]').trigger('click')
|
||||
await wrapper.get('[data-test="year"][data-year="2024"]').trigger('click') // -> mois 2024
|
||||
expect(wrapper.find('[data-test="month-picker"]').exists()).toBe(true)
|
||||
expect(wrapper.get('[data-test="header-toggle"]').text()).toContain('2024')
|
||||
await wrapper.get('[data-test="month"][data-month="0"]').trigger('click') // -> jours
|
||||
expect(wrapper.find('[data-test="month-picker"]').exists()).toBe(false)
|
||||
expect(wrapper.get('[data-test="header-toggle"]').text()).toContain('Janvier 2024')
|
||||
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('paginates the year window by 12 with chevrons', async () => {
|
||||
const wrapper = mountDate()
|
||||
await wrapper.get('[data-test="date-input"]').trigger('click')
|
||||
await wrapper.get('[data-test="header-toggle"]').trigger('click')
|
||||
await wrapper.get('[data-test="header-toggle"]').trigger('click') // années : fenêtre 2022–2033
|
||||
await wrapper.get('[data-test="header-next"]').trigger('click') // +12 -> 2034–2045
|
||||
const years = wrapper.findAll('[data-test="year"]')
|
||||
expect(years[0].attributes('data-year')).toBe('2034')
|
||||
expect(years[11].attributes('data-year')).toBe('2045')
|
||||
})
|
||||
|
||||
it('greys out years outside [min, max]', async () => {
|
||||
const wrapper = mountDate({min: '2025-01-01', max: '2027-12-31'})
|
||||
await wrapper.get('[data-test="date-input"]').trigger('click')
|
||||
await wrapper.get('[data-test="header-toggle"]').trigger('click')
|
||||
await wrapper.get('[data-test="header-toggle"]').trigger('click')
|
||||
expect(wrapper.get('[data-test="year"][data-year="2024"]').attributes('disabled')).toBeDefined()
|
||||
expect(wrapper.get('[data-test="year"][data-year="2026"]').attributes('disabled')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('vue mois', () => {
|
||||
it('switches to month view on header toggle', async () => {
|
||||
const wrapper = mountDate()
|
||||
|
||||
Reference in New Issue
Block a user