fix: keep an event's title in sight while its block runs above the visible hours, as an overnight event's morning part

This commit is contained in:
Linus Rath
2026-11-04 13:51:08 +01:00
parent f80f2ec612
commit cf844ff4d7
3 changed files with 52 additions and 3 deletions
@@ -0,0 +1,37 @@
import { render, renderHook, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { EventCard } from '../event-card';
import { useDisplayHours } from '@/hooks/use-display-hours';
import type { CalendarEvent } from '@/lib/jmap/types';
// pim-p2-19: the part of "Night train to Vienna" after midnight starts at the
// top of the day, scrolled out of view, so the week showed a block with no
// title. The title line now sticks under the grid's header while the block is
// in sight. A block that hid its overflow was a scroller of its own, and a
// sticky line inside one sticks to the block, not to the grid.
const train = {
id: 'n1', '@type': 'Event', uid: 'n1', title: 'Night train to Vienna', start: '2026-11-07T00:00:00',
duration: 'PT7H40M', showWithoutTime: false, status: 'confirmed', calendarIds: { cal: true }, participants: null,
} as unknown as CalendarEvent;
describe('the title of a long block stays in sight', () => {
it('sticks the title line under the header and clips the block without making it a scroller', () => {
render(<EventCard event={train} variant="block" continuesBefore />);
const card = screen.getByRole('button', { name: /Night train to Vienna/ });
expect(card.className).toContain('overflow-clip');
expect(card.className).not.toContain('overflow-hidden');
const line = card.querySelector('[data-event-title-line]') as HTMLElement;
expect(line.className).toContain('sticky');
expect(line.className).toContain('top-[var(--grid-header-height,0px)]');
expect(line.textContent).toContain('Night train to Vienna');
});
it('tells the grid how tall its header is, for the sticky title', () => {
const pane = document.createElement('div');
const header = document.createElement('div');
Object.defineProperty(header, 'offsetHeight', { value: 96 });
renderHook(() => useDisplayHours({ scrollRef: { current: pane }, headerRef: { current: header }, minHourHeight: 48 }));
expect(pane.style.getPropertyValue('--grid-header-height')).toBe('96px');
});
});
+8 -2
View File
@@ -450,8 +450,9 @@ export function EventCard({ event, calendar, variant, onClick, onMouseEnter, onM
data-continues-after={continuesAfter || undefined}
className={cn(
// A block box: an inline one hangs its line's descent below the
// grid's last hour and makes the filled pane scroll.
"relative block w-full h-full text-start rounded-xs px-1.5 py-0.5 text-xs overflow-hidden",
// grid's last hour and makes the filled pane scroll. Clipped, not
// hidden: hidden makes the block a scroller and its title can't stick.
"relative block w-full h-full text-start rounded-xs px-1.5 py-0.5 text-xs overflow-clip",
"hover:opacity-90 transition-opacity cursor-pointer",
// Cut at the edge of the visible hours: a square edge and an arrow
// say the event runs on past it.
@@ -470,6 +471,10 @@ export function EventCard({ event, calendar, variant, onClick, onMouseEnter, onM
{/* Shorter blocks have no room for a second line at the 60px hour, so
their start time follows the title on the same line, when the
block is wide enough to keep the title readable. */}
{/* The title stays in sight under the grid's header while the block
is: the morning part of a night train started at midnight, scrolled
out of view, and showed as a block with no name (pim-p2-19). */}
<div data-event-title-line className="sticky top-[var(--grid-header-height,0px)]">
<div className="@container">
<div className="flex items-baseline gap-1 min-w-0">
<span dir="auto" className={cn("font-medium truncate", isInactive && "line-through")}>{event.title || t("events.no_title")}</span>
@@ -487,6 +492,7 @@ export function EventCard({ event, calendar, variant, onClick, onMouseEnter, onM
</div>
</div>
)}
</div>
{visibleCalendarName && (
<div data-calendar-name dir="auto" className="opacity-85 text-[0.65625rem] truncate">{visibleCalendarName}</div>
)}
+7 -1
View File
@@ -58,7 +58,13 @@ export function useDisplayHours({ scrollRef, minHourHeight, headerRef, revealAt
useLayoutEffect(() => {
const root = scrollRef.current;
if (!root) return;
const measure = () => setAvailable(Math.max(0, root.clientHeight - (headerRef?.current?.offsetHeight ?? 0)));
const measure = () => {
const headerHeight = headerRef?.current?.offsetHeight ?? 0;
setAvailable(Math.max(0, root.clientHeight - headerHeight));
// Where an event's title sticks while its block runs above the visible
// hours: an overnight event's morning part (pim-p2-19).
root.style?.setProperty("--grid-header-height", `${headerHeight}px`);
};
measure();
if (typeof ResizeObserver === "undefined") return;
const observer = new ResizeObserver(measure);