Show a parent's name once per calendar week row, not once per band

Ad-hoc days are banded per leave type and adjacent wishes are separate
bands, so a type change inside one parent's row repeated "Name · Type".
Only the parent's leftmost band in the row now carries the name; the
band aria-label still includes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-23 17:38:27 +02:00
parent a14655a831
commit 5faf477266
2 changed files with 119 additions and 1 deletions
@@ -1155,6 +1155,95 @@ describe('CalendarWeek', () => {
})
})
describe('band names: once per parent per week row, not once per segment', () => {
it('shows the name once when ad-hoc days for one parent split into a vacation run then a sjukpenning run', () => {
// Mon/Tue vacation and Wed-Fri sjukpenning are two separate ad-hoc
// bands (bandKeyFor keys on parent+dayType), not one band with a
// mid-period type change — each has its own single-run label. The
// parent's name belongs only on the leftmost (vacation) one.
const plannedDays = [
pd({ id: '1', date: '2026-01-05', parentPersonalDataUuid: 'parent-1', dayType: 'vacation' }),
pd({ id: '2', date: '2026-01-06', parentPersonalDataUuid: 'parent-1', dayType: 'vacation' }),
pd({ id: '3', date: '2026-01-07', parentPersonalDataUuid: 'parent-1', dayType: 'sjukpenning' }),
pd({ id: '4', date: '2026-01-08', parentPersonalDataUuid: 'parent-1', dayType: 'sjukpenning' }),
pd({ id: '5', date: '2026-01-09', parentPersonalDataUuid: 'parent-1', dayType: 'sjukpenning' }),
]
const { container } = render(<CalendarWeek {...defaultProps} plannedDays={plannedDays} parentName="Test Parent" />)
expect(screen.getAllByTestId(/^calendar-band-/)).toHaveLength(2)
const labels = Array.from(container.querySelectorAll('.calendar-band__label'))
expect(labels).toHaveLength(2)
expect(labels[0]?.textContent).toContain('Test Parent')
expect(labels[0]?.textContent).toContain('Semester')
expect(labels[1]?.textContent).not.toContain('Test Parent')
expect(labels[1]?.textContent).toContain('Sjukpenning')
})
it('shows the name once across two adjacent wishes for the same parent', () => {
const plannedDays = [
pd({ id: '1', date: '2026-01-05', wishId: 'wish-1', parentPersonalDataUuid: 'parent-1' }),
pd({ id: '2', date: '2026-01-06', wishId: 'wish-1', parentPersonalDataUuid: 'parent-1' }),
pd({ id: '3', date: '2026-01-07', wishId: 'wish-2', parentPersonalDataUuid: 'parent-1' }),
pd({ id: '4', date: '2026-01-08', wishId: 'wish-2', parentPersonalDataUuid: 'parent-1' }),
]
const { container } = render(<CalendarWeek {...defaultProps} plannedDays={plannedDays} parentName="Test Parent" />)
expect(screen.getAllByTestId(/^calendar-band-/)).toHaveLength(2)
const labels = Array.from(container.querySelectorAll('.calendar-band__label'))
expect(labels).toHaveLength(2)
expect(labels[0]?.textContent).toContain('Test Parent')
expect(labels[1]?.textContent).not.toContain('Test Parent')
})
it('shows each parent\'s name once, independently, when both parents have a mid-week type change', () => {
const plannedDays = [
pd({ id: '1', date: '2026-01-05', parentPersonalDataUuid: 'parent-1', dayType: 'vacation' }),
pd({ id: '2', date: '2026-01-06', parentPersonalDataUuid: 'parent-1', dayType: 'sjukpenning' }),
pd({ id: '3', date: '2026-01-07', parentPersonalDataUuid: 'other-parent', dayType: 'vacation' }),
pd({ id: '4', date: '2026-01-08', parentPersonalDataUuid: 'other-parent', dayType: 'sjukpenning' }),
]
const { container } = render(<CalendarWeek {...defaultProps} plannedDays={plannedDays} parentName="Test Parent" />)
const labels = Array.from(container.querySelectorAll('.calendar-band__label'))
expect(labels).toHaveLength(4)
expect(labels[0]?.textContent).toContain('Test Parent')
expect(labels[1]?.textContent).not.toContain('Test Parent')
expect(labels[1]?.textContent).not.toContain('Partner')
expect(labels[2]?.textContent).toContain('Partner')
expect(labels[3]?.textContent).not.toContain('Partner')
expect(labels[3]?.textContent).not.toContain('Test Parent')
})
it('still shows the name on a band continuing into the next week row (each row recomputes its own leftmost segment)', () => {
const plannedDays = [
pd({ id: '1', date: '2026-01-10', wishId: 'wish-1', parentPersonalDataUuid: 'parent-1' }),
pd({ id: '2', date: '2026-01-13', wishId: 'wish-1', parentPersonalDataUuid: 'parent-1' }),
]
const calendarDaysByDate = new Map<string, CalendarDayEntry>([
['2026-01-11', calDay({ date: '2026-01-11', dayType: 'weekend' })],
['2026-01-12', calDay({ date: '2026-01-12', dayType: 'weekend' })],
])
const { container } = render(
<>
<CalendarWeek
{...defaultProps}
plannedDays={plannedDays}
calendarDaysByDate={calendarDaysByDate}
parentName="Test Parent"
/>
<CalendarWeek
{...defaultProps}
weekDates={['2026-01-12', '2026-01-13', '2026-01-14', '2026-01-15', '2026-01-16', '2026-01-17', '2026-01-18']}
plannedDays={plannedDays}
calendarDaysByDate={calendarDaysByDate}
parentName="Test Parent"
/>
</>,
)
const labels = Array.from(container.querySelectorAll('.calendar-band__label'))
expect(labels).toHaveLength(2)
expect(labels[0]?.textContent).toContain('Test Parent')
expect(labels[1]?.textContent).toContain('Test Parent')
})
})
describe('per-day hover/focus card', () => {
beforeEach(() => {
vi.useFakeTimers()
@@ -452,6 +452,34 @@ export function CalendarWeek({
[weekDates, plannedDays, calendarDaysByDate],
)
// A parent's leave in one week row can be split across several band
// segments — ad-hoc days are keyed by parent+dayType (`bandKeyFor`), so a
// Mon-Tue vacation stretch and a Wed-Fri sjukpenning stretch for the same
// parent are two segments, not one — and separate wishes are always
// separate segments regardless of adjacency. Showing the parent name on
// every segment's first run would repeat "Name · Type" once per segment;
// the spec is ONE name per parent per week row, on that parent's
// leftmost segment (lowest `startCol`, tie-broken by lowest `lane` for
// determinism when two segments happen to start in the same column).
// Every other segment/run for that parent in this row shows the type
// only. This is computed per week (each row calls `computeBandSegments`
// independently), so a band continuing into the next week row still gets
// its own leftmost-segment name there.
const nameSegmentIdByParent = useMemo(() => {
const bestByParent = new Map<string, BandSegment>()
for (const seg of bandSegments) {
const current = bestByParent.get(seg.parentUuid)
if (!current || seg.startCol < current.startCol || (seg.startCol === current.startCol && seg.lane < current.lane)) {
bestByParent.set(seg.parentUuid, seg)
}
}
const map = new Map<string, string>()
for (const [parentUuid, seg] of bestByParent) {
map.set(parentUuid, seg.id)
}
return map
}, [bandSegments])
// Bands overlay the day-cell row itself (grid-row: 1, position: absolute)
// instead of occupying separate rows below it — see the comment on
// `.calendar-week` in calendar.css. `barHeight`/`laneGap`/`bandBottomInset`
@@ -679,6 +707,7 @@ export function CalendarWeek({
const runDubbeldag = segment.dayInfos
.slice(run.startIdx, run.endIdx + 1)
.some((d) => !d.isGap && d.isDubbeldag)
const showName = runIdx === 0 && nameSegmentIdByParent.get(segment.parentUuid) === segment.id
return (
<div
key={`label-${run.startIdx}`}
@@ -693,7 +722,7 @@ export function CalendarWeek({
leaveType={runTypeLabel}
color={getColorForType(runDayType)}
isDubbeldag={runDubbeldag}
parentLabel={runIdx === 0 ? label : undefined}
parentLabel={showName ? label : undefined}
paintBackground={false}
/>
</div>