mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 14:32:03 +00:00
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>
37 lines
1.3 KiB
TypeScript
37 lines
1.3 KiB
TypeScript
"use client";
|
|
|
|
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
|
|
* settings monolith so every section that needs a masked secret (Withings,
|
|
* moodLog, ntfy, Telegram, AI provider keys, account password) can share the
|
|
* same UX without duplicating the visibility-toggle plumbing.
|
|
*/
|
|
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"
|
|
>
|
|
{visible ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|