mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-04 16:14:07 +00:00
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:
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user