fix: defer email validation to after first blur on signup form (#27765)

* fix: defer email validation to after first blur on signup form

Change react-hook-form validation mode from "onChange" to "onTouched"
so that the "Invalid email" error only appears after the user has
interacted with the field and moved away, not on every keystroke.
Password strength hints still update in real-time since onTouched
validates on change after the field has been touched (blurred once).

Fixes #19163

* test: add unit tests for signup email validation mode

Verify the onTouched form validation behavior:
- No error shown while user is typing
- Error appears only after blur with invalid email
- No error for valid email after blur
- Revalidation on each keystroke after first blur

* remove eslint comment

---------

Co-authored-by: Dhairyashil <dhairyashil10101010@gmail.com>
This commit is contained in:
Ahmad Yasser
2026-02-09 06:40:11 +00:00
committed by GitHub
co-authored by Dhairyashil
parent 3052cf5157
commit cdf901fee3
2 changed files with 104 additions and 1 deletions
+103
View File
@@ -0,0 +1,103 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { useForm, FormProvider } from "react-hook-form";
import { describe, expect, it } from "vitest";
import { z } from "zod";
import { signupSchema as apiSignupSchema } from "@calcom/prisma/zod-utils";
const signupSchema = apiSignupSchema.extend({
apiError: z.string().optional(),
cfToken: z.string().optional(),
});
type FormValues = z.infer<typeof signupSchema>;
function TestSignupForm() {
const formMethods = useForm<FormValues>({
resolver: zodResolver(signupSchema),
defaultValues: {
username: "",
email: "",
password: "",
},
mode: "onTouched",
});
const {
register,
formState: { errors },
} = formMethods;
return (
<FormProvider {...formMethods}>
<form>
<label htmlFor="email">Email</label>
<input id="email" type="email" data-testid="email-input" {...register("email")} />
{errors.email && <span data-testid="email-error">{errors.email.message}</span>}
<label htmlFor="password">Password</label>
<input id="password" type="password" data-testid="password-input" {...register("password")} />
{errors.password && <span data-testid="password-error">{errors.password.message}</span>}
</form>
</FormProvider>
);
}
describe("Signup form validation mode", () => {
it("should not show email error while user is still typing", async () => {
const user = userEvent.setup();
render(<TestSignupForm />);
const emailInput = screen.getByTestId("email-input");
await user.type(emailInput, "test");
expect(screen.queryByTestId("email-error")).not.toBeInTheDocument();
});
it("should show email error after the field is blurred with invalid value", async () => {
const user = userEvent.setup();
render(<TestSignupForm />);
const emailInput = screen.getByTestId("email-input");
await user.type(emailInput, "invalid-email");
fireEvent.blur(emailInput);
await waitFor(() => {
expect(screen.getByTestId("email-error")).toBeInTheDocument();
});
});
it("should not show email error if field is blurred with valid email", async () => {
const user = userEvent.setup();
render(<TestSignupForm />);
const emailInput = screen.getByTestId("email-input");
await user.type(emailInput, "test@example.com");
fireEvent.blur(emailInput);
await waitFor(() => {
expect(screen.queryByTestId("email-error")).not.toBeInTheDocument();
});
});
it("should revalidate on each keystroke after the field has been touched and blurred", async () => {
const user = userEvent.setup();
render(<TestSignupForm />);
const emailInput = screen.getByTestId("email-input");
await user.type(emailInput, "bad");
fireEvent.blur(emailInput);
await waitFor(() => {
expect(screen.getByTestId("email-error")).toBeInTheDocument();
});
await user.clear(emailInput);
await user.type(emailInput, "valid@email.com");
await waitFor(() => {
expect(screen.queryByTestId("email-error")).not.toBeInTheDocument();
});
});
});
+1 -1
View File
@@ -200,7 +200,7 @@ export default function Signup({
const formMethods = useForm<FormValues>({
resolver: zodResolver(signupSchema),
defaultValues: prepopulateFormValues satisfies FormValues,
mode: "onChange",
mode: "onTouched",
});
const {
register,