From cdf901fee31248856b0c4856eaa51b6904a72284 Mon Sep 17 00:00:00 2001 From: Ahmad Yasser Date: Mon, 9 Feb 2026 01:40:11 -0500 Subject: [PATCH] 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 --- apps/web/modules/signup-view.test.tsx | 103 ++++++++++++++++++++++++++ apps/web/modules/signup-view.tsx | 2 +- 2 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 apps/web/modules/signup-view.test.tsx diff --git a/apps/web/modules/signup-view.test.tsx b/apps/web/modules/signup-view.test.tsx new file mode 100644 index 0000000000..9888b88b99 --- /dev/null +++ b/apps/web/modules/signup-view.test.tsx @@ -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; + +function TestSignupForm() { + const formMethods = useForm({ + resolver: zodResolver(signupSchema), + defaultValues: { + username: "", + email: "", + password: "", + }, + mode: "onTouched", + }); + + const { + register, + formState: { errors }, + } = formMethods; + + return ( + +
+ + + {errors.email && {errors.email.message}} + + + + {errors.password && {errors.password.message}} +
+
+ ); +} + +describe("Signup form validation mode", () => { + it("should not show email error while user is still typing", async () => { + const user = userEvent.setup(); + render(); + + 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(); + + 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(); + + 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(); + + 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(); + }); + }); +}); diff --git a/apps/web/modules/signup-view.tsx b/apps/web/modules/signup-view.tsx index 061b75e02d..f287ff9e33 100644 --- a/apps/web/modules/signup-view.tsx +++ b/apps/web/modules/signup-view.tsx @@ -200,7 +200,7 @@ export default function Signup({ const formMethods = useForm({ resolver: zodResolver(signupSchema), defaultValues: prepopulateFormValues satisfies FormValues, - mode: "onChange", + mode: "onTouched", }); const { register,