Skip to content

Commit f15c578

Browse files
authored
Fix Someday hover times on week grid (#1806)
* fix(web): show someday hover times * fix(web): match someday preview layout * fix(web): keep someday hover time visible * fix(web): stack someday preview time * fix(web): show past drag time previews * fix(web): keep someday preview text stacked * refactor(web): share timed event text styles
1 parent ca13f72 commit f15c578

13 files changed

Lines changed: 472 additions & 94 deletions

File tree

docs/acceptance/events.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,12 @@ Someday events can be dragged from the sidebar onto a specific day and time on t
245245

246246
### Expected Results
247247

248+
- While the Someday event is over a timed grid slot, the preview uses the same
249+
layout as a Timed Event: title at the top, time underneath.
250+
- The preview shows the tentative time while hovering, even if the target time
251+
has already passed.
252+
- If the target time has already passed, the time disappears after drop the same
253+
way it does on saved past Timed Events.
248254
- The event disappears from the sidebar.
249255
- The event appears on the grid at the dropped time.
250256
- The event is now a regular scheduled event and persists after a page reload.
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { expect, type Page, test } from "@playwright/test";
2+
import {
3+
createEventTitle,
4+
expectSomedayEventVisible,
5+
fillTitleAndSaveEventForm,
6+
getMainGridPoint,
7+
openSomedayEventFormWithMouse,
8+
prepareCalendarPage,
9+
} from "../utils/event-test-utils";
10+
11+
test.skip(
12+
({ isMobile }) => isMobile,
13+
"Mouse flows are desktop-only in week view.",
14+
);
15+
16+
const dragSomedayEventToTimedGrid = async (page: Page, titlePrefix: string) => {
17+
const title = createEventTitle(titlePrefix);
18+
await openSomedayEventFormWithMouse(page, "week");
19+
await fillTitleAndSaveEventForm(page, title);
20+
await expectSomedayEventVisible(page, title);
21+
22+
const somedayEvent = page.locator("#sidebar").getByRole("button", {
23+
name: title,
24+
});
25+
const eventBox = await somedayEvent.boundingBox();
26+
27+
if (!eventBox) {
28+
throw new Error("Expected the Someday event to be visible.");
29+
}
30+
31+
const start = {
32+
x: eventBox.x + eventBox.width / 2,
33+
y: eventBox.y + eventBox.height / 2,
34+
};
35+
const target = await getMainGridPoint(page, { xRatio: 0.35, yRatio: 0.35 });
36+
37+
await page.mouse.move(start.x, start.y);
38+
await page.mouse.down();
39+
await page.mouse.move(target.x, target.y, { steps: 12 });
40+
41+
return title;
42+
};
43+
44+
const expectTimedPreviewTextStack = async (page: Page, title: string) => {
45+
const overlay = page.locator("[data-calendar-interaction-overlay]");
46+
const titleLabel = overlay.getByText(title);
47+
const timeLabel = overlay.locator("[data-someday-interaction-time-label]");
48+
49+
await expect(overlay).toBeVisible();
50+
await expect(timeLabel).toHaveText(
51+
/\d{1,2}(?::\d{2})?\s+-\s+\d{1,2}(?::\d{2})?\s*(AM|PM)/i,
52+
);
53+
54+
const titleBox = await titleLabel.boundingBox();
55+
const timeBox = await timeLabel.boundingBox();
56+
57+
if (!titleBox || !timeBox) {
58+
throw new Error("Expected the Someday timed preview text to be visible.");
59+
}
60+
61+
expect(timeBox.y + timeBox.height / 2).toBeGreaterThan(
62+
titleBox.y + titleBox.height / 2,
63+
);
64+
expect(timeBox.y - titleBox.y).toBeLessThan(titleBox.height + 8);
65+
expect(timeBox.x).toBeLessThan(titleBox.x + 8);
66+
};
67+
68+
test("shows a timed-grid preview while dragging a someday event", async ({
69+
page,
70+
}) => {
71+
await prepareCalendarPage(page);
72+
await page.getByRole("button", { name: "Next week" }).click();
73+
await page.locator("#mainGrid").waitFor({ state: "visible" });
74+
75+
const title = await dragSomedayEventToTimedGrid(page, "Someday Drag Preview");
76+
await expectTimedPreviewTextStack(page, title);
77+
78+
await page.mouse.up();
79+
});
80+
81+
test("shows a timed-grid preview while dragging over a past slot", async ({
82+
page,
83+
}) => {
84+
await prepareCalendarPage(page);
85+
await page.getByRole("button", { name: "Previous week" }).click();
86+
await page.locator("#mainGrid").waitFor({ state: "visible" });
87+
88+
const title = await dragSomedayEventToTimedGrid(
89+
page,
90+
"Someday Past Drag Preview",
91+
);
92+
await expectTimedPreviewTextStack(page, title);
93+
94+
await page.mouse.up();
95+
});

e2e/timed/create-event-mouse.spec.ts

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
ensureSidebarOpen,
55
expectTimedEventVisible,
66
fillTitleAndSaveEventForm,
7+
getMainGridPoint,
78
openTimedEventFormWithMouse,
89
prepareCalendarPage,
910
} from "../utils/event-test-utils";
@@ -14,24 +15,6 @@ interface StoredTimedEvent {
1415
title?: string;
1516
}
1617

17-
const getMainGridPoint = async (
18-
page: Page,
19-
{ xRatio = 0.3, yRatio = 0.3 } = {},
20-
) => {
21-
const mainGrid = page.locator("#mainGrid");
22-
await mainGrid.scrollIntoViewIfNeeded();
23-
const box = await mainGrid.boundingBox();
24-
25-
if (!box) {
26-
throw new Error("Expected the week grid to be visible.");
27-
}
28-
29-
return {
30-
x: box.x + box.width * xRatio,
31-
y: box.y + box.height * yRatio,
32-
};
33-
};
34-
3518
const getSavedEventsByTitle = (page: Page, title: string) =>
3619
page.evaluate(async (eventTitle) => {
3720
const db = await new Promise<IDBDatabase>((resolve, reject) => {

e2e/utils/event-test-utils.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,24 @@ export const clickGridCenter = async (page: Page, locator: Locator) => {
227227
await page.mouse.up();
228228
};
229229

230+
export const getMainGridPoint = async (
231+
page: Page,
232+
{ xRatio = 0.3, yRatio = 0.3 } = {},
233+
) => {
234+
const mainGrid = page.locator("#mainGrid");
235+
await mainGrid.scrollIntoViewIfNeeded();
236+
const box = await mainGrid.boundingBox();
237+
238+
if (!box) {
239+
throw new Error("Expected the week grid to be visible.");
240+
}
241+
242+
return {
243+
x: box.x + box.width * xRatio,
244+
y: box.y + box.height * yRatio,
245+
};
246+
};
247+
230248
/**
231249
* Fills the event form title and submits via the Save control (role=button, name Save).
232250
* Keyboard shortcuts for submit (Enter / Mod+Enter) are not driven here: Playwright’s

packages/web/src/components/PlannerSidebar/SomedayEventSections/SomedayEvents/SomedayEventContainer/SomedayEventRectangle.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,10 @@ export const SomedayEventRectangle = ({
5050
direction={FlexDirections.ROW}
5151
justifyContent={JustifyContent.SPACE_BETWEEN}
5252
>
53-
<div className="flex min-w-0 flex-1 items-center gap-1.5">
53+
<div
54+
className="flex min-w-0 flex-1 items-center gap-1.5"
55+
data-someday-event-title-row="true"
56+
>
5457
<DotsSixVertical
5558
aria-hidden="true"
5659
className="shrink-0 text-text-light"

packages/web/src/components/PlannerSidebar/SomedayEventSections/interaction/adapter/SomedayInteractionAdapter.test.ts

Lines changed: 95 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,11 @@ const setReducedMotionPreference = (matches: boolean) => {
101101
};
102102
};
103103

104-
const createHarness = () => {
104+
const createHarness = ({
105+
viewStart = dayjs("2026-05-17"),
106+
}: {
107+
viewStart?: ReturnType<typeof dayjs>;
108+
} = {}) => {
105109
document.body.innerHTML = "";
106110
somedayDropTargetRegistry.clear();
107111
somedayEventRegistry.clear();
@@ -112,6 +116,8 @@ const createHarness = () => {
112116
const timerCallbacks = new Map<unknown, () => void>();
113117
const event = createSomedayEvent();
114118
const source = document.createElement("div");
119+
const sourceContent = document.createElement("div");
120+
const sourceTitleRow = document.createElement("div");
115121
const sourceChild = document.createElement("span");
116122
const sourceButton = document.createElement("button");
117123
const weekDropTarget = document.createElement("div");
@@ -126,7 +132,12 @@ const createHarness = () => {
126132
const onRequestWeekNavigation = mock();
127133

128134
sourceButton.type = "button";
129-
source.append(sourceChild, sourceButton);
135+
sourceButton.setAttribute("data-someday-drag-affordance", "true");
136+
sourceContent.className = "h-full";
137+
sourceTitleRow.setAttribute("data-someday-event-title-row", "true");
138+
sourceTitleRow.append(sourceChild);
139+
sourceContent.append(sourceTitleRow, sourceButton);
140+
source.append(sourceContent);
130141
weekDropTarget.append(source);
131142
mainGrid.id = ID_GRID_MAIN;
132143
timedColumns.id = ID_GRID_COLUMNS_TIMED;
@@ -205,7 +216,7 @@ const createHarness = () => {
205216
mainGridElement: mainGrid,
206217
timedColumnsElement: timedColumns,
207218
}),
208-
getViewStart: () => dayjs("2026-05-17"),
219+
getViewStart: () => viewStart,
209220
runtime: () => ({
210221
getSomedayEventById: (eventId) => (eventId === event._id ? event : null),
211222
onCancelInteraction,
@@ -362,6 +373,87 @@ describe("SomedayInteractionAdapter", () => {
362373
expect(overlay?.querySelector("[data-someday-drag-affordance]")).toBeNull();
363374
});
364375

376+
it("shows the tentative timed-grid range inside the visible Someday preview", () => {
377+
const { adapter, flushFrame, sourceChild, timedColumns } = createHarness({
378+
viewStart: dayjs().add(1, "week").startOf("week"),
379+
});
380+
381+
adapter.handlePointerDown(
382+
makePointerEvent("pointerdown", { target: sourceChild, x: 20, y: 12 }),
383+
);
384+
adapter.handlePointerMove(
385+
makePointerEvent("pointermove", {
386+
target: timedColumns,
387+
x: 250,
388+
y: 220,
389+
}),
390+
);
391+
flushFrame();
392+
393+
const overlay = document.body.querySelector<HTMLElement>(
394+
"[data-calendar-interaction-overlay]",
395+
);
396+
const timeLabel = overlay?.querySelector<HTMLElement>(
397+
"[data-someday-interaction-time-label]",
398+
);
399+
const titleRow = overlay?.querySelector<HTMLElement>(
400+
"[data-someday-event-title-row]",
401+
);
402+
const textStack = titleRow?.parentElement;
403+
404+
expect(timeLabel).toBeTruthy();
405+
expect(timeLabel?.textContent).toMatch(/2\s+-\s+3 AM/);
406+
expect(timeLabel?.style.display).toBe("block");
407+
expect(timeLabel?.parentElement).toBe(textStack);
408+
expect(timeLabel?.previousElementSibling).toBe(titleRow);
409+
expect(textStack?.style.alignItems).toBe("flex-start");
410+
expect(textStack?.style.display).toBe("flex");
411+
expect(textStack?.style.flexDirection).toBe("column");
412+
expect(titleRow?.style.alignSelf).toBe("stretch");
413+
expect(titleRow?.style.alignItems).toBe("flex-start");
414+
expect(titleRow?.style.flex).toBe("0 1 auto");
415+
expect(titleRow?.style.flexDirection).toBe("row");
416+
});
417+
418+
it("keeps the tentative timed-grid range visible for past targets", () => {
419+
const { adapter, flushFrame, sourceChild, timedColumns } = createHarness({
420+
viewStart: dayjs().subtract(1, "week").startOf("week"),
421+
});
422+
423+
adapter.handlePointerDown(
424+
makePointerEvent("pointerdown", { target: sourceChild, x: 20, y: 12 }),
425+
);
426+
adapter.handlePointerMove(
427+
makePointerEvent("pointermove", {
428+
target: timedColumns,
429+
x: 250,
430+
y: 220,
431+
}),
432+
);
433+
flushFrame();
434+
435+
const overlay = document.body.querySelector<HTMLElement>(
436+
"[data-calendar-interaction-overlay]",
437+
);
438+
const titleRow = overlay?.querySelector<HTMLElement>(
439+
"[data-someday-event-title-row]",
440+
);
441+
442+
expect(
443+
overlay?.querySelector("[data-someday-interaction-time-label]")
444+
?.textContent,
445+
).toMatch(/2\s+-\s+3 AM/);
446+
expect(
447+
overlay?.querySelector("[data-someday-interaction-time-label]")
448+
?.parentElement,
449+
).toBe(titleRow?.parentElement);
450+
expect(titleRow?.style.alignSelf).toBe("stretch");
451+
expect(titleRow?.style.alignItems).toBe("flex-start");
452+
expect(titleRow?.style.flex).toBe("0 1 auto");
453+
expect(titleRow?.style.flexDirection).toBe("row");
454+
expect(titleRow?.parentElement?.style.flexDirection).toBe("column");
455+
});
456+
365457
it("disables Someday overlay motion when reduced motion is preferred", () => {
366458
const restoreMatchMedia = setReducedMotionPreference(true);
367459
const { adapter, flushFrame, sourceChild, timedColumns } = createHarness();

0 commit comments

Comments
 (0)