mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 12:22:03 +00:00
fix(a11y): skip-link no longer blocks logo click outside focus
The "Skip to content" link is `position: fixed; top-2 left-2 z-[100]` on every authenticated page. Even with `opacity-0` it still occupied its bounding box, so a mouse click on the desktop logo (also at the top-left corner) sometimes hit the invisible link instead. Fix: keep the visual treatment (opacity, focus styles) but combine `pointer-events-none` + `-translate-y-full` in the unfocused state so the element neither receives clicks nor occupies any visible space. On focus, both modifiers reverse — the link slides into view and becomes interactive again. Existing keyboard flow is unchanged: Tab focuses the link, the focus ring + translucent slide-in is preserved. Tests: extend `e2e/a11y.spec.ts` with a Playwright check that the desktop logo is clickable from its centre on the dashboard and the URL stays at "/". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
85aa15b35b
commit
786b3951f6
2 files changed
+48
-1
No files matched your search
@@ -110,4 +110,43 @@ test.describe("axe-core authenticated surfaces", () => {
|
||||
expect(blocking).toHaveLength(0);
|
||||
});
|
||||
}
|
||||
|
||||
// The keyboard-only "skip to content" link is fixed at the top-left
|
||||
// corner of every authenticated page. Marc reported it intercepting
|
||||
// logo clicks in v1.4.14 — the fix combines `pointer-events-none`
|
||||
// with `-translate-y-full` so the element sits offscreen until
|
||||
// focused. Regression guard: with no keyboard focus, the logo's own
|
||||
// bounding box must accept the click.
|
||||
test("skip-link does not block logo click outside focus", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// Find the desktop sidebar logo link — `<Logo>` is rendered inside
|
||||
// a `<Link href="/">` and the only logo at the absolute top-left
|
||||
// of the layout. We resolve it by role / accessible name to avoid
|
||||
// brittle selectors.
|
||||
const logoLink = page
|
||||
.locator("a[href='/']")
|
||||
.filter({ hasText: "HealthLog" })
|
||||
.first();
|
||||
await expect(logoLink).toBeVisible();
|
||||
|
||||
const logoBox = await logoLink.boundingBox();
|
||||
expect(logoBox).not.toBeNull();
|
||||
if (!logoBox) return;
|
||||
|
||||
// Click the centre of the logo. If the skip-link is intercepting
|
||||
// pointer events, Playwright's auto-waiting click would either time
|
||||
// out or report the wrong element. The defensive `force: false`
|
||||
// (default) ensures we go through the full hit-test pipeline.
|
||||
await logoLink.click({
|
||||
position: { x: logoBox.width / 2, y: logoBox.height / 2 },
|
||||
});
|
||||
|
||||
// Logo points at "/", so we either stay on "/" or get a no-op
|
||||
// reload. Either way the URL must remain on the dashboard.
|
||||
await page.waitForURL(/\/$/);
|
||||
});
|
||||
});
|
||||
@@ -114,9 +114,17 @@ export function AuthShell({ children }: { children: React.ReactNode }) {
|
||||
{showUnlockNotifier && user?.id ? (
|
||||
<AchievementUnlockNotifier userId={user.id} />
|
||||
) : null}
|
||||
{/*
|
||||
Keyboard-only skip link. The non-focused state has both
|
||||
`pointer-events-none` (so a mouse click in the top-left corner
|
||||
passes through to the logo behind it) AND `-translate-y-full`
|
||||
(so even an accidental tap with stylus / touch can't activate
|
||||
it). Focus restores both — the element jumps back into the
|
||||
viewport and becomes clickable.
|
||||
*/}
|
||||
<a
|
||||
href="#main-content"
|
||||
className="bg-primary text-primary-foreground fixed top-2 left-2 z-[100] rounded-md px-4 py-2 text-sm font-medium opacity-0 focus:opacity-100"
|
||||
className="bg-primary text-primary-foreground fixed top-2 left-2 z-[100] -translate-y-full rounded-md px-4 py-2 text-sm font-medium opacity-0 transition-transform pointer-events-none focus:translate-y-0 focus:opacity-100 focus:pointer-events-auto"
|
||||
>
|
||||
{t("nav.skipToContent")}
|
||||
</a>
|
||||
|
||||
Reference in new issue
Block a user