mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +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);
|
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 ? (
|
{showUnlockNotifier && user?.id ? (
|
||||||
<AchievementUnlockNotifier userId={user.id} />
|
<AchievementUnlockNotifier userId={user.id} />
|
||||||
) : null}
|
) : 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
|
<a
|
||||||
href="#main-content"
|
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")}
|
{t("nav.skipToContent")}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
Reference in new issue
Block a user