fix(a11y): close violations the new e2e axe-core sweep surfaced

Five real WCAG 2.1 AA violations the extended axe-core spec caught:

- top-bar.tsx: mobile user-menu DropdownMenuTrigger had no
  accessible name on viewports < sm (the username text is
  `hidden sm:inline`, leaving an icon-only button). Added
  aria-label using the existing nav.userMenu key.
- password-input.tsx: show/hide eye toggle was icon-only, no
  aria-label. Added common.showPassword / common.hidePassword
  in en + de and bound to the visible state.
- auth/login/page.tsx: "Sign up" link relied on colour alone
  (link-in-text-block). Switched hover:underline → underline so
  the link is distinguishable without colour.
- page.tsx (dashboard): "Add" dropdown trigger was 32px tall
  (size="sm"); on Pixel 5 below WCAG 2.5.5 44px threshold.
  Added min-h-11 — desktop visual unchanged because the explicit
  height only kicks in if it exceeds the size variant's h-8.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc-André BombeckandClaude Opus 4.7 committed 2026-05-09 16:49:34 +02:00
1 parent ca3c599e7c
commit 7af1f59bb9
6 files changed
+28 -5

No files matched your search

+4 -1
View File
@@ -59,7 +59,10 @@ export function TopBar() {
<div className="bg-muted h-4 w-20 animate-pulse rounded" />
) : user ? (
<DropdownMenu>
<DropdownMenuTrigger className="text-muted-foreground hover:text-foreground flex items-center gap-1.5 rounded-md px-2 py-1.5 text-sm transition-colors focus:outline-none">
<DropdownMenuTrigger
aria-label={t("nav.userMenu")}
className="text-muted-foreground hover:text-foreground flex items-center gap-1.5 rounded-md px-2 py-1.5 text-sm transition-colors focus:outline-none"
>
<User className="h-4 w-4" />
<span className="hidden sm:inline">{user.username}</span>
<ChevronDown className="h-3 w-3 opacity-60" />
@@ -4,6 +4,7 @@ import { useState } from "react";
import { Eye, EyeOff } from "lucide-react";
import { Input } from "@/components/ui/input";
import { useTranslations } from "@/lib/i18n/context";
/**
* Password input with a built-in show/hide toggle. Extracted from the legacy
@@ -13,12 +14,18 @@ import { Input } from "@/components/ui/input";
*/
export function PasswordInput(props: React.ComponentProps<typeof Input>) {
const [visible, setVisible] = useState(false);
const { t } = useTranslations();
return (
<div className="relative">
<Input {...props} type={visible ? "text" : "password"} />
<button
type="button"
tabIndex={-1}
// WCAG 2.1.2 button-name — the icon-only toggle has no visible text
// and screen readers fall back to the icon's filename otherwise.
aria-label={
visible ? t("common.hidePassword") : t("common.showPassword")
}
onClick={() => setVisible((v) => !v)}
className="text-muted-foreground hover:text-foreground absolute top-1/2 right-3 -translate-y-1/2"
>