diff --git a/frontend/src/i18n/en/calendar.ts b/frontend/src/i18n/en/calendar.ts
index cb91605..569453d 100644
--- a/frontend/src/i18n/en/calendar.ts
+++ b/frontend/src/i18n/en/calendar.ts
@@ -1,6 +1,8 @@
export const calendar = {
'calendar.advanceMonthAria': '{month} - click to advance month',
'calendar.advanceYearAria': '{year} - click to advance year',
+ 'calendar.selectMonthAria': 'Select month',
+ 'calendar.selectYearAria': 'Select year',
'calendar.deleteError': 'Failed to delete the planned day',
'calendar.legendDoubleDay': 'Double day',
'calendar.legendIncomeBased': 'Income-based rate (sjukpenningnivå)',
diff --git a/frontend/src/i18n/sv/calendar.ts b/frontend/src/i18n/sv/calendar.ts
index cfff1ba..42786ae 100644
--- a/frontend/src/i18n/sv/calendar.ts
+++ b/frontend/src/i18n/sv/calendar.ts
@@ -1,6 +1,8 @@
export const calendar = {
'calendar.advanceMonthAria': '{month} - klicka för att byta månad',
'calendar.advanceYearAria': '{year} - klicka för att byta år',
+ 'calendar.selectMonthAria': 'Välj månad',
+ 'calendar.selectYearAria': 'Välj år',
'calendar.deleteError': 'Det gick inte att ta bort den planerade dagen',
'calendar.legendDoubleDay': 'Dubbeldag',
'calendar.legendIncomeBased': 'Sjukpenningnivå',
diff --git a/frontend/src/pages/Calendar.test.tsx b/frontend/src/pages/Calendar.test.tsx
index b9a0a0b..7dcfe38 100644
--- a/frontend/src/pages/Calendar.test.tsx
+++ b/frontend/src/pages/Calendar.test.tsx
@@ -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()
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()
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()
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()
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()
+
+ 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()
+
+ 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', () => {
,
)
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()
diff --git a/frontend/src/pages/Calendar.tsx b/frontend/src/pages/Calendar.tsx
index 9111073..235dc60 100644
--- a/frontend/src/pages/Calendar.tsx
+++ b/frontend/src/pages/Calendar.tsx
@@ -137,12 +137,14 @@ export function Calendar(): ReactNode {
setCurrentMonth(new Date(year, month + 1, 1))
}
- const handleMonthClick = () => {
- setCurrentMonth(new Date(year, month + 1, 1))
+ const handleMonthChange = (e: React.ChangeEvent) => {
+ const newMonth = Number(e.target.value)
+ setCurrentMonth(new Date(year, newMonth, 1))
}
- const handleYearClick = () => {
- setCurrentMonth(new Date(year + 1, month, 1))
+ const handleYearChange = (e: React.ChangeEvent) => {
+ const newYear = Number(e.target.value)
+ setCurrentMonth(new Date(newYear, month, 1))
}
// §3.6 — an explicit mode toggle (rather than a modifier-click or context
@@ -299,26 +301,34 @@ export function Calendar(): ReactNode {
←
-
-
+ {Array.from({ length: 2055 - 2026 + 1 }, (_, i) => 2026 + i).map((y) => (
+
+ ))}
+