Persist values, a11y errors, and aria-invalid on /register
- Server action now echoes firstName, lastName, email, and the privacy checkbox state on every error path so failed submits don't wipe the form. - New `attempt` counter on RegisterState drives a remount key per field, bypassing React's default <form action> reset semantics. - Error <p> elements gain role="alert" + aria-live="polite" so screen readers announce validation failures live. - Privacy checkbox now flags aria-invalid when not accepted and points to the error message via aria-describedby. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -32,6 +32,16 @@ const registerSchema = z.object({
|
||||
const PRIVACY_POLICY_VERSION = "2026-05-01";
|
||||
|
||||
export type RegisterState = {
|
||||
// Bumped after every action call so the client can key uncontrolled inputs
|
||||
// and force a remount with the echoed values from a failed submit.
|
||||
attempt: number;
|
||||
// Echoed inputs (sans password) so failed submits don't wipe the form.
|
||||
values?: {
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
email?: string;
|
||||
acceptPrivacyPolicy?: boolean;
|
||||
};
|
||||
errors?: {
|
||||
email?: string[];
|
||||
password?: string[];
|
||||
@@ -42,13 +52,29 @@ export type RegisterState = {
|
||||
};
|
||||
};
|
||||
|
||||
export const initialRegisterState: RegisterState = { attempt: 0 };
|
||||
|
||||
function echoValues(formData: FormData): RegisterState["values"] {
|
||||
return {
|
||||
firstName: String(formData.get("firstName") ?? ""),
|
||||
lastName: String(formData.get("lastName") ?? ""),
|
||||
email: String(formData.get("email") ?? ""),
|
||||
acceptPrivacyPolicy: formData.get("acceptPrivacyPolicy") === "on",
|
||||
};
|
||||
}
|
||||
|
||||
export async function registerAction(
|
||||
_prevState: RegisterState,
|
||||
prevState: RegisterState,
|
||||
formData: FormData,
|
||||
): Promise<RegisterState> {
|
||||
const attempt = prevState.attempt + 1;
|
||||
const parsed = registerSchema.safeParse(Object.fromEntries(formData));
|
||||
if (!parsed.success) {
|
||||
return { errors: parsed.error.flatten().fieldErrors };
|
||||
return {
|
||||
attempt,
|
||||
values: echoValues(formData),
|
||||
errors: parsed.error.flatten().fieldErrors,
|
||||
};
|
||||
}
|
||||
const { email, password, firstName, lastName } = parsed.data;
|
||||
|
||||
@@ -77,6 +103,8 @@ export async function registerAction(
|
||||
err.code === "P2002"
|
||||
) {
|
||||
return {
|
||||
attempt,
|
||||
values: echoValues(formData),
|
||||
errors: {
|
||||
email: ["Mit dieser E-Mail-Adresse existiert bereits ein Account."],
|
||||
},
|
||||
@@ -93,5 +121,5 @@ export async function registerAction(
|
||||
});
|
||||
|
||||
// Unreachable – signIn redirected.
|
||||
return {};
|
||||
return { attempt };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user