mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
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:
1 parent
ca3c599e7c
commit
7af1f59bb9
6 files changed
+28
-5
No files matched your search
+3
-1
@@ -41,7 +41,9 @@
|
||||
"copy": "Kopieren",
|
||||
"copyDetails": "Details kopieren",
|
||||
"retry": "Erneut versuchen",
|
||||
"reportIssue": "Fehler melden"
|
||||
"reportIssue": "Fehler melden",
|
||||
"showPassword": "Passwort anzeigen",
|
||||
"hidePassword": "Passwort verbergen"
|
||||
},
|
||||
"doctorReport": {
|
||||
"title": "Gesundheitsbericht",
|
||||
|
||||
+3
-1
@@ -41,7 +41,9 @@
|
||||
"copy": "Copy",
|
||||
"copyDetails": "Copy details",
|
||||
"retry": "Retry",
|
||||
"reportIssue": "Report issue"
|
||||
"reportIssue": "Report issue",
|
||||
"showPassword": "Show password",
|
||||
"hidePassword": "Hide password"
|
||||
},
|
||||
"doctorReport": {
|
||||
"title": "Health Report",
|
||||
|
||||
@@ -217,7 +217,12 @@ export default function LoginPage() {
|
||||
{t("auth.noAccount")}{" "}
|
||||
<Link
|
||||
href="/auth/register"
|
||||
className="text-primary hover:underline"
|
||||
// axe-core `link-in-text-block` requires a visible
|
||||
// distinction beyond colour alone (WCAG 1.4.1). Forcing
|
||||
// the underline always-on instead of `hover:underline`
|
||||
// satisfies the rule without changing the visual after
|
||||
// hover.
|
||||
className="text-primary underline"
|
||||
>
|
||||
{t("auth.register")}
|
||||
</Link>
|
||||
|
||||
+5
-1
@@ -363,7 +363,11 @@ export default function DashboardPage() {
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button size="sm">
|
||||
{/* WCAG 2.5.5 — touch targets must be ≥44×44 CSS px on mobile.
|
||||
`size="sm"` (h-8 = 32px) was below threshold; the explicit
|
||||
min-h-11 ensures we hit 44px on the Pixel 5 viewport while
|
||||
keeping the desktop visual unchanged. */}
|
||||
<Button size="sm" className="min-h-11">
|
||||
<Plus className="mr-1 h-4 w-4" />
|
||||
{t("common.add")}
|
||||
</Button>
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
Reference in new issue
Block a user