Let the calendar jump to any month or year, in both directions

Clicking the month or year label only ever stepped forward
(`month + 1`, `year + 1`), so the only way back was the prev-month arrow,
one month at a time. Jumping back a year was impossible.

Both labels are now selects, so any month or year is one click away in
either direction. Selects rather than +/- steppers because the app
already uses them elsewhere, and because stepping in a second direction
would still not give the "quickly jump" part of the original intent.

Year options are bounded to 2026-2055, the calendar cache range: outside
it the backend raises UncachedYearError and returns 422, so offering
those years would hand the user an error instead of a calendar.

The prev/next month arrows are unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RXMhc9nT6sNz1SL4j8dznL
This commit is contained in:
2026-09-15 03:31:33 +02:00
parent 20f45d6c7b
commit 31a60a296f
5 changed files with 116 additions and 76 deletions
+62 -44
View File
@@ -206,86 +206,104 @@ describe('Calendar', () => {
expect(selectedDaysInRange.length).toBeGreaterThan(0)
})
it('advances one month when clicking the month button', () => {
it('jumps forward to any month via month dropdown', () => {
;(useWorkspace as any).mockReturnValue(mockWorkspace)
;(usePlannedDays as any).mockReturnValue(mockPlannedDays)
render(<Calendar />)
const title = screen.getByRole('heading', { level: 2 })
const monthButton = title.querySelector('.calendar__month-button') as HTMLButtonElement
const monthSelect = title.querySelector('.calendar__month-select') as HTMLSelectElement
expect(monthButton).toBeInTheDocument()
expect(monthSelect).toBeInTheDocument()
// Get initial month/year from title
const initialText = title.textContent
// Change to a different month (e.g., month 5 = June, 0-indexed)
fireEvent.change(monthSelect, { target: { value: '5' } })
// Click the month button to advance one month
fireEvent.click(monthButton)
// The month should have changed (we can't predict the exact month without knowing current date,
// but we can verify the text changed)
const updatedText = title.textContent
expect(updatedText).not.toBe(initialText)
// Verify the select value changed
expect(monthSelect.value).toBe('5')
})
it('advances one year when clicking the year button', () => {
it('jumps backward to any month via month dropdown', () => {
;(useWorkspace as any).mockReturnValue(mockWorkspace)
;(usePlannedDays as any).mockReturnValue(mockPlannedDays)
render(<Calendar />)
const title = screen.getByRole('heading', { level: 2 })
const yearButton = title.querySelector('.calendar__year-button') as HTMLButtonElement
const monthSelect = title.querySelector('.calendar__month-select') as HTMLSelectElement
expect(yearButton).toBeInTheDocument()
expect(monthSelect).toBeInTheDocument()
// Get initial year
const initialYear = parseInt(yearButton.textContent || '0')
// Jump backward to January (month 0)
fireEvent.change(monthSelect, { target: { value: '0' } })
// Click the year button to advance one year
fireEvent.click(yearButton)
// The year should have increased by 1
const updatedYearButton = screen.getByRole('heading', { level: 2 }).querySelector('.calendar__year-button') as HTMLButtonElement
const updatedYear = parseInt(updatedYearButton.textContent || '0')
expect(updatedYear).toBe(initialYear + 1)
expect(monthSelect.value).toBe('0')
})
it('handles December to January rollover when clicking month button in December', () => {
it('jumps forward to any year via year dropdown', () => {
;(useWorkspace as any).mockReturnValue(mockWorkspace)
;(usePlannedDays as any).mockReturnValue(mockPlannedDays)
// Mock a December render - we need to test the logic, not the actual calendar state
render(<Calendar />)
const title = screen.getByRole('heading', { level: 2 })
const monthButton = title.querySelector('.calendar__month-button') as HTMLButtonElement
const yearSelect = title.querySelector('.calendar__year-select') as HTMLSelectElement
// Click month button 12 times (a full year cycle)
for (let i = 0; i < 12; i++) {
fireEvent.click(monthButton)
}
expect(yearSelect).toBeInTheDocument()
// The year might have incremented depending on when we started, but the calendar should still work
const finalTitle = screen.getByRole('heading', { level: 2 })
expect(finalTitle).toBeInTheDocument()
// Jump to year 2035
fireEvent.change(yearSelect, { target: { value: '2035' } })
expect(yearSelect.value).toBe('2035')
})
it('month and year buttons have proper accessibility labels', () => {
it('jumps backward to any year via year dropdown', () => {
;(useWorkspace as any).mockReturnValue(mockWorkspace)
;(usePlannedDays as any).mockReturnValue(mockPlannedDays)
render(<Calendar />)
const title = screen.getByRole('heading', { level: 2 })
const monthButton = title.querySelector('.calendar__month-button') as HTMLButtonElement
const yearButton = title.querySelector('.calendar__year-button') as HTMLButtonElement
const yearSelect = title.querySelector('.calendar__year-select') as HTMLSelectElement
expect(monthButton).toHaveAttribute('aria-label')
expect(yearButton).toHaveAttribute('aria-label')
expect(monthButton.getAttribute('aria-label')).toContain('klicka för att byta månad')
expect(yearButton.getAttribute('aria-label')).toContain('klicka för att byta år')
expect(yearSelect).toBeInTheDocument()
// Jump backward to year 2026 (the minimum supported year)
fireEvent.change(yearSelect, { target: { value: '2026' } })
expect(yearSelect.value).toBe('2026')
})
it('year dropdown includes all supported years from 2026 to 2055', () => {
;(useWorkspace as any).mockReturnValue(mockWorkspace)
;(usePlannedDays as any).mockReturnValue(mockPlannedDays)
render(<Calendar />)
const title = screen.getByRole('heading', { level: 2 })
const yearSelect = title.querySelector('.calendar__year-select') as HTMLSelectElement
const options = Array.from(yearSelect.options).map((opt) => parseInt(opt.value))
// Verify the range is correct
expect(Math.min(...options)).toBe(2026)
expect(Math.max(...options)).toBe(2055)
expect(options).toHaveLength(30) // 2026 to 2055 inclusive is 30 years
})
it('month and year selects have proper accessibility labels', () => {
;(useWorkspace as any).mockReturnValue(mockWorkspace)
;(usePlannedDays as any).mockReturnValue(mockPlannedDays)
render(<Calendar />)
const title = screen.getByRole('heading', { level: 2 })
const monthSelect = title.querySelector('.calendar__month-select') as HTMLSelectElement
const yearSelect = title.querySelector('.calendar__year-select') as HTMLSelectElement
expect(monthSelect).toHaveAttribute('aria-label', 'Välj månad')
expect(yearSelect).toHaveAttribute('aria-label', 'Välj år')
})
// §3.4 shading + §3.6 click-to-override
@@ -588,9 +606,9 @@ describe('Calendar', () => {
</LanguageProvider>,
)
const title = screen.getByRole('heading', { level: 2 })
const monthButton = title.querySelector('.calendar__month-button') as HTMLButtonElement
// English month names differ from the Swedish ones (e.g. "January" vs "januari").
expect(monthButton.getAttribute('aria-label')).toContain('click to advance month')
const monthSelect = title.querySelector('.calendar__month-select') as HTMLSelectElement
// Month select has a simple aria label
expect(monthSelect.getAttribute('aria-label')).toBe('Select month')
} finally {
Object.defineProperty(window.navigator, 'language', { value: 'sv-SE', configurable: true })
vi.unstubAllGlobals()