shoc-frontend-new/src/components/auth/password-checklist.tsx
2026-09-25 11:52:10 -03:00

63 lines
2.2 KiB
TypeScript

import { useState } from "react";
import { Circle, CircleCheck } from "lucide-react";
import { Text } from "@/components/ui/text";
import { evaluatePasswordRules, type PasswordRuleResult } from "@/domain/auth/password-policy";
import { cn } from "@/lib/utils";
export type PasswordChecklistProps = {
password: string;
id?: string;
className?: string;
};
function describeChanges(previous: PasswordRuleResult[], next: PasswordRuleResult[]): string {
return next
.filter((rule, index) => rule.met !== previous[index]?.met)
.map((rule) => `${rule.label}: ${rule.met ? "requirement met" : "requirement not met"}`)
.join(". ");
}
/**
* Live, per-rule feedback for the shared password policy. A polite status region
* announces only the rules whose state changed, so screen-reader users hear
* progress while typing without the whole list being re-read on each keystroke.
*/
export function PasswordChecklist({ password, id, className }: PasswordChecklistProps) {
const results = evaluatePasswordRules(password);
const signature = results.map((rule) => (rule.met ? "1" : "0")).join("");
const [previous, setPrevious] = useState({ signature, results });
const [announcement, setAnnouncement] = useState("");
if (previous.signature !== signature) {
setPrevious({ signature, results });
setAnnouncement(describeChanges(previous.results, results));
}
return (
<>
<ul
id={id}
aria-label="Password requirements"
className={cn("m-0 mt-2 grid list-none gap-1 p-0", className)}
>
{results.map(({ id: ruleId, label, met }) => (
<li key={ruleId} data-rule={ruleId} data-met={met} className="flex items-center gap-1.5">
{met ? (
<CircleCheck size={13} aria-hidden="true" className="text-(--color-success)" />
) : (
<Circle size={13} aria-hidden="true" className="text-(--color-text-muted)" />
)}
<Text variant="caption" tone={met ? "success" : "muted"}>
{label}
</Text>
<span className="sr-only">{met ? "requirement met" : "requirement not met"}</span>
</li>
))}
</ul>
<Text variant="feedback" className="sr-only">
{announcement}
</Text>
</>
);
}