From 5cdc2ea233b2c2d4cf07e88f83b50a83bb038e58 Mon Sep 17 00:00:00 2001 From: Valerio Coltre Date: Wed, 30 Sep 2026 08:56:22 +0700 Subject: [PATCH 1/3] fix(gui): keep quota popovers reachable and link account management --- .../src/content/docs/guides/web-dashboard.md | 7 +- gui/README.md | 19 ++ gui/package.json | 3 +- .../quota-summary-bar/QuotaSummaryBar.tsx | 39 +++- .../quota-summary-bar/quota-summary-bar.css | 22 +++ gui/tests/quota-summary-bar.test.tsx | 96 +++++++++- gui/tests/quota-summary-hover-browser.ts | 174 ++++++++++++++++++ structure/dashboard-and-usage.md | 4 +- 8 files changed, 345 insertions(+), 19 deletions(-) create mode 100644 gui/tests/quota-summary-hover-browser.ts diff --git a/docs-site/src/content/docs/guides/web-dashboard.md b/docs-site/src/content/docs/guides/web-dashboard.md index 3645ae7c1d3..7af6c4dc4c9 100644 --- a/docs-site/src/content/docs/guides/web-dashboard.md +++ b/docs-site/src/content/docs/guides/web-dashboard.md @@ -87,9 +87,12 @@ is visible) and never forces an upstream refresh. provider-named window or prepaid credits. - A chip turns amber at 70% used and red at 90% used. - Hover or keyboard-focus a chip to see every reported window with its reset time and the time the - reading was taken. On a touch screen, the first tap shows those details. + reading was taken. The details stay open as the pointer moves from the chip into the popover, + including across the small visual gap. On a touch screen, the first tap shows those details. - Click a chip (or tap it a second time) to open that provider's Accounts tab in Providers, where - its accounts or API keys are managed. + its accounts or API keys are managed. The popover's **Open account management** link opens the + same tab; keyboard users can Tab from the chip to this link and press Enter. Escape closes the + popover and returns focus to the chip when focus was inside the popover. - The bar always stays on one line. When the chips do not fit, scroll it sideways or use the « and » buttons at either end. - Providers that report no quota window are left out. The bar is hidden when no provider reports one. diff --git a/gui/README.md b/gui/README.md index e55b038fb64..d15f4432feb 100644 --- a/gui/README.md +++ b/gui/README.md @@ -53,6 +53,25 @@ bun run setup:hooks # pre-push runs doctor when gui/ changed Fix ESLint errors first. Use `doctor` / `doctor:full` for deeper React triage. +## Quota popover hover browser regression + +```bash +cd gui +bun run build +bun run test:quota-hover +``` + +Rebuild after changing CSS: this opt-in check combines the current React quota component with +the production CSS in `dist`. It uses an installed Chrome/Chromium (`CHROME_BIN` when not on PATH) +and never connects to the proxy or provider accounts. Across 48 cases it moves a real pointer +through the chip-to-popover gap, clicks the account-management link, returns to the chip, and +checks dismissal outside. It also verifies keyboard Tab, Enter, and Escape. Cases cover light/dark themes, mobile and desktop widths, fractional +placement, display scaling, and the last chip after horizontal scrolling. Results and a screenshot +are written to `.tmp/quota-summary-hover-browser/`. This browser check is manual, not part of CI; +happy-dom unit tests do not perform CSS hit-testing. To prove the guard detects the original gap, +run `bun tests/quota-summary-hover-browser.ts .tmp/quota-hover-without-bridge --without-bridge`; +that run must fail while crossing the gap. + ## Sidebar version browser regression ```bash diff --git a/gui/package.json b/gui/package.json index cd9d7fcf917..d7000ade3d2 100644 --- a/gui/package.json +++ b/gui/package.json @@ -12,7 +12,8 @@ "doctor": "npx --yes react-doctor@0.9.11 --verbose --scope changed --base origin/main --no-telemetry", "doctor:full": "npx --yes react-doctor@0.9.11 --verbose --scope full --no-telemetry", "preview": "vite preview", - "test:sidebar-version": "bun tests/sidebar-version-browser.ts" + "test:sidebar-version": "bun tests/sidebar-version-browser.ts", + "test:quota-hover": "bun tests/quota-summary-hover-browser.ts" }, "dependencies": { "@tanstack/react-virtual": "^3.14.9", diff --git a/gui/src/components/quota-summary-bar/QuotaSummaryBar.tsx b/gui/src/components/quota-summary-bar/QuotaSummaryBar.tsx index 957694475f6..eec82276599 100644 --- a/gui/src/components/quota-summary-bar/QuotaSummaryBar.tsx +++ b/gui/src/components/quota-summary-bar/QuotaSummaryBar.tsx @@ -83,6 +83,8 @@ function usePopoverPlacement(open: boolean, anchorRef: RefObject { - if (event.key === "Escape") close(); + if (event.key === "Escape") { + const returnFocus = popoverRef.current?.contains(document.activeElement); + close(); + if (returnFocus) { + chipRef.current?.focus(); + setSuppressed(true); + } + } }; document.addEventListener("pointerdown", onPointer); document.addEventListener("keydown", onKey); @@ -169,19 +178,22 @@ function QuotaSummaryItem({ row, t, locale }: { row: QuotaSummaryRow; t: TFn; lo className={`quota-summary-item quota-summary-item--${row.severity}`} onPointerEnter={onPointerEnter} onPointerLeave={onPointerLeave} + onFocus={event => { + if (!isKeyboardFocus(event.target)) return; + setSuppressed(false); + setFocused(true); + }} + onBlur={event => { + if (!event.currentTarget.contains(event.relatedTarget)) setFocused(false); + }} > { pointerTypeRef.current = event.pointerType; }} - onFocus={event => { - if (!isKeyboardFocus(event.currentTarget)) return; - setSuppressed(false); - setFocused(true); - }} - onBlur={() => setFocused(false)} onClick={onClick} > {row.label} @@ -191,7 +203,7 @@ function QuotaSummaryItem({ row, t, locale }: { row: QuotaSummaryRow; t: TFn; lo {t("quotaSummary.openAccounts")} {open && ( -