fix(day): keep Escape in the date calendar from closing the whole day

On a phone the date's calendar is a sheet over the day's own sheet. In
the moment after it opens, before Radix has re-rendered both layers, the
day's sheet still holds the Escape listener, so an Escape aimed at the
calendar closed the day as well; the day-view spec caught it as a flake
on the phone viewport (4 of 10 runs locally). The day's sheet now ignores
an Escape that comes from inside another dialog, and the spec waits for
the layer handover instead of racing it.

The coach open-at-end spec read the thread's height as soon as an element
with the thread's slot was visible, but the empty state carries the same
slot. It now waits for the loaded thread's frame and all of its answers.
This commit is contained in:
Marc-André Bombeck committed 2026-10-11 13:09:12 +02:00
1 parent 118494b91d
commit 7d8ff0f2df
3 files changed
+50 -5

No files matched your search

+19 -5
View File
@@ -185,12 +185,26 @@ test.describe("Coach: a saved conversation opens at its end", () => {
waitUntil: "domcontentloaded",
});
const thread = page.locator('[data-slot="coach-message-thread"]');
await expect(thread).toBeVisible({ timeout: 15_000 });
// The empty state carries the same `coach-message-thread` slot, so the
// thread being visible says nothing about the conversation having
// arrived; the height used to be read from the empty state on a slow
// run. Wait for the frame only a loaded thread renders, and for every
// stored answer in it.
await expect(
page.locator('[data-slot="coach-message-thread-frame"]'),
).toBeVisible({ timeout: 15_000 });
await expect(
page.locator('[data-slot="coach-assistant-turn"]'),
).toHaveCount(8, { timeout: 15_000 });
const thread = page.locator(
'[data-slot="coach-message-thread-frame"] [data-slot="coach-message-thread"]',
);
// The thread scrolls at all: otherwise "at the end" proves nothing.
expect(
await thread.evaluate((el) => el.scrollHeight > el.clientHeight + 200),
).toBe(true);
await expect
.poll(() =>
thread.evaluate((el) => el.scrollHeight > el.clientHeight + 200),
)
.toBe(true);
const chart = page.locator(
'[data-slot="coach-result-chart"][data-ref="r1"]',
+19
View File
@@ -275,6 +275,25 @@ test.describe("the day view", () => {
),
)
.toBe(true);
// On a phone the calendar is a sheet over the day's own sheet. Focus
// arrives before Radix has handed it the Escape key: in between, the day
// underneath still holds it. The handover shows as the day's layer
// turning inert (`pointer-events: none`) once the calendar is the top
// layer; wait for that, not for a fixed time. The docked column is no
// dialog and has no such handover.
if (!desktop)
await expect
.poll(() =>
panel(page).evaluate(
(el) =>
new Promise<string>((resolve) =>
requestAnimationFrame(() =>
setTimeout(() => resolve(getComputedStyle(el).pointerEvents)),
),
),
),
)
.toBe("none");
await page.keyboard.press("Escape");
await expect(picker).toHaveCount(0);
await expect(panel(page)).toBeVisible();
+12
View File
@@ -717,6 +717,18 @@ function DaySheet({
event.preventDefault();
returnFocus();
}}
onEscapeKeyDown={(event) => {
// A dialog opened from inside the day (the date's calendar) is
// portaled out of the panel. For a moment after it opens, until
// Radix has re-rendered both layers, the panel still holds the
// Escape listener: an Escape aimed at the calendar then closed the
// whole day. Escape from inside another dialog is never the day's.
const owner =
event.target instanceof Element
? event.target.closest('[role="dialog"]')
: null;
if (owner && owner !== panelRef.current) event.preventDefault();
}}
className={cn(
// Quicker than the primitive's 500 ms, which read as lag on a
// layer opened from a tap; no slide at all under reduced motion.