Hold every calendar cell to the same size

A calendar whose boxes resize is not a calendar. Each week row is its own
grid, and a bare 1fr track takes its minimum from its content, so the one
week containing "Alla helgons dag" had its seven cells squeezed while
every other week's stayed wide. The tracks are now minmax(0, 1fr) and the
cell, the day and the holiday name each refuse to contribute an intrinsic
width; the name clips to an ellipsis and keeps its full text in the title.

Row height follows the same rule: the band lanes are reserved from the
month's tallest week rather than each row's own, so a week with no leave
in it stands as tall as the week beside it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ei7gCZzdd751uZtPzb1Pep
This commit is contained in:
2026-09-19 14:08:04 +02:00
parent 224fe7ae3e
commit f0f537c95f
6 changed files with 281 additions and 6 deletions
+83
View File
@@ -1,4 +1,6 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import fs from 'node:fs'
import path from 'node:path'
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { useSearchParams, type SetURLSearchParams } from 'react-router-dom'
import { Calendar } from './Calendar'
@@ -1471,4 +1473,85 @@ describe('Calendar', () => {
await waitFor(() => expect(screen.queryByTestId('modal')).not.toBeInTheDocument())
})
})
describe('grid layout: uniform cell size and row height regardless of content', () => {
it('keeps every week row on the same shared grid definition even when one week contains a long holiday name', () => {
// Regression guard for the "31 October" bug: a long holiday name
// ("Alla helgons dag") used to widen only the week row containing it,
// because each week is its own CSS Grid container and a bare `1fr`
// track's minimum is its content's own width. jsdom does no layout,
// so this can't assert pixel widths directly — instead it asserts the
// structural invariant (every week row uses the exact same
// `.calendar-week` class, with no per-row width/grid override) and
// that the shared stylesheet rule those rows resolve against is
// actually content-safe (`minmax(0, 1fr)`, not a bare `1fr`).
vi.mocked(useCalendarDays).mockReturnValue(
makeCalendarDaysResult({
data: makeCalendarWindow({
days: [
makeCalendarDayEntry({
date: '2026-10-31',
name: 'Alla helgons dag',
dayType: 'red_day',
isFree: true,
}),
],
}),
}),
)
vi.mocked(useSearchParams).mockReturnValue([new URLSearchParams('date=2026-10-15'), mockSetSearchParams])
const { container } = render(<Calendar />)
// The long holiday name actually rendered somewhere in the grid.
expect(screen.getByText('Alla helgons dag')).toBeInTheDocument()
// Every week row shares the exact same class — none carries its own
// width/grid-template-columns override that could diverge per row.
const weekRows = Array.from(container.querySelectorAll('.calendar-week'))
expect(weekRows.length).toBeGreaterThan(3)
weekRows.forEach((row) => {
expect(row.className).toBe('calendar-week')
expect((row as HTMLElement).style.width).toBe('')
expect((row as HTMLElement).style.gridTemplateColumns).toBe('')
})
// The stylesheet rule those rows all resolve against must be immune
// to content width: `minmax(0, 1fr)` tracks, and every element
// inside a cell that could carry a long string sets `min-width: 0`.
const css = fs.readFileSync(path.resolve(__dirname, 'calendar.css'), 'utf-8')
expect(css).toMatch(/\.calendar-week\s*{[^}]*grid-template-columns:\s*repeat\(7,\s*minmax\(0,\s*1fr\)\)/)
expect(css).toMatch(
/\.calendar-week--with-week-numbers\s*{[^}]*grid-template-columns:\s*50px repeat\(7,\s*minmax\(0,\s*1fr\)\)/,
)
expect(css).toMatch(/\.calendar-day\s*{[^}]*min-width:\s*0/)
expect(css).toMatch(/\.calendar-week__day-cell\s*{[^}]*min-width:\s*0/)
expect(css).toMatch(/\.calendar-day__holiday-name\s*{[^}]*min-width:\s*0/)
})
it('reserves the same --band-reserved-height on every week row, even though only one week has a band', () => {
// Regression guard: `--band-reserved-height` used to be computed per
// week from that week's OWN max concurrent lane count, so a week with
// a band was taller than a quiet week in the same month. Every row of
// the displayed month must reserve the same height now.
vi.mocked(usePlannedDays).mockReturnValue(
makePlannedDaysResult({
data: [makePlannedLeaveDay({ id: 'pd-1', date: '2026-10-05', parentPersonalDataUuid: 'person-1' })],
}),
)
vi.mocked(useSearchParams).mockReturnValue([new URLSearchParams('date=2026-10-15'), mockSetSearchParams])
const { container } = render(<Calendar />)
const weekRows = Array.from(container.querySelectorAll('.calendar-week')) as HTMLElement[]
expect(weekRows.length).toBeGreaterThan(3)
const reservedHeights = weekRows.map((row) => row.style.getPropertyValue('--band-reserved-height'))
// Every row reserves the SAME height, and it's non-zero (the month
// has at least one band) — a week with no band of its own must still
// match the tallest week's reservation, or its row would render
// shorter than the others.
expect(new Set(reservedHeights).size).toBe(1)
expect(reservedHeights[0]).not.toBe('0px')
})
})
})