diff --git a/packages/app/e2e/browser/context-window-meter.spec.ts b/packages/app/e2e/browser/context-window-meter.spec.ts index 80a43d6aa2..5d024de761 100644 --- a/packages/app/e2e/browser/context-window-meter.spec.ts +++ b/packages/app/e2e/browser/context-window-meter.spec.ts @@ -268,10 +268,16 @@ for (const theme of ["light", "dark"] as const) { await expect(card).toBeVisible(); }); - await test.step("Refresh replaces the card with the source's new report", async () => { - usage.answerNext([expiredLogin(onWorkLogin(claude!))]); + await test.step("Refresh keeps the card open and replaces it with the new report", async () => { + const refreshed = gate(); + usage.answerNext({ stream: [refreshed.promise, expiredLogin(onWorkLogin(claude!))] }); const card = contextWindowDetails(page); await refreshUsageCard(card, "Claude"); + await expect(card.getByRole("button", { name: "Refresh Claude" })).toBeDisabled(); + // Past the card's close grace, with the pointer still on the card. + await page.waitForTimeout(500); + await expect(card).toBeVisible(); + refreshed.open(); await expect(card.getByText(LOGIN_EXPIRED)).toBeVisible(); expect(usage.refreshedAgents()).toEqual([agent.agentId]); }); diff --git a/packages/app/e2e/browser/sidebar-workspace.spec.ts b/packages/app/e2e/browser/sidebar-workspace.spec.ts index 55782a2155..8071b3b4f3 100644 --- a/packages/app/e2e/browser/sidebar-workspace.spec.ts +++ b/packages/app/e2e/browser/sidebar-workspace.spec.ts @@ -429,7 +429,7 @@ test.describe("Sidebar workspace list", () => { } }); - test("workspace hover card stays open for a keyboard-focused row", async ({ page }) => { + test("workspace hover card follows keyboard focus on a row", async ({ page }) => { const workspace = await seedWorkspace({ repoPrefix: "sidebar-hover-keyboard-" }); try { @@ -445,6 +445,11 @@ test.describe("Sidebar workspace list", () => { await expectWorkspaceHoverCardStaysOpen(page); await page.keyboard.press("Escape"); await expectWorkspaceHoverCardClosed(page); + + await focusWorkspaceRowWithKeyboard(page, workspace.workspaceId); + await expectWorkspaceHoverCardOpen(page); + await page.keyboard.press("Tab"); + await expectWorkspaceHoverCardClosed(page); } finally { await workspace.cleanup(); } diff --git a/packages/app/src/components/ui/hover-card.tsx b/packages/app/src/components/ui/hover-card.tsx index e186b39cd0..3d438505d3 100644 --- a/packages/app/src/components/ui/hover-card.tsx +++ b/packages/app/src/components/ui/hover-card.tsx @@ -82,6 +82,8 @@ function WebHoverCard({ const contentRef = useRef(null); const [open, setOpen] = useState(false); const graceTimerRef = useRef | null>(null); + // Read when focus arrives: by focusout the element losing focus no longer matches :focus-visible. + const keyboardFocusedRef = useRef(false); const clearGraceTimer = useCallback(() => { if (graceTimerRef.current) { @@ -176,16 +178,20 @@ function WebHoverCard({ setOpen(false); }; const focusEntered = (event: FocusEvent) => { - const target = event.target as Node; + const target = event.target as Element; + keyboardFocusedRef.current = target.matches(":focus-visible"); if (trigger.contains(target)) { openNow(); } else if ((contentRef.current as unknown as HTMLElement | null)?.contains(target)) { clearGraceTimer(); } }; + // Only keyboard focus keeps the card open, so only losing keyboard focus closes it. A button + // that disables itself on press drops pointer focus while the pointer is still on the card. const focusLeft = (event: FocusEvent) => { const next = event.relatedTarget as Node | null; if ( + keyboardFocusedRef.current && !trigger.contains(next) && !(contentRef.current as unknown as HTMLElement | null)?.contains(next) ) {