diff --git a/packages/shadcn/registry-spec.json b/packages/shadcn/registry-spec.json
index 2acebab60..cdf8e44ba 100644
--- a/packages/shadcn/registry-spec.json
+++ b/packages/shadcn/registry-spec.json
@@ -61,8 +61,8 @@
"type": "registry:block",
"title": "Email Link Auth Form",
"description": "A form allowing users to sign in via email link.",
- "dependencies": ["{{ DEP | @firebase-oss/ui-react@beta }}"],
- "registryDependencies": ["input", "button", "form", "alert", "{{ DOMAIN }}/r/policies.json"],
+ "dependencies": ["{{ DEP | @firebase-oss/ui-react@beta }}", "react-hook-form", "@hookform/resolvers"],
+ "registryDependencies": ["input", "button", "field", "alert", "{{ DOMAIN }}/r/policies.json"],
"files": [
{
"path": "src/components/email-link-auth-form.tsx",
@@ -126,8 +126,8 @@
"type": "registry:block",
"title": "Forgot Password Auth Form",
"description": "A form allowing users to reset their password via email.",
- "dependencies": ["{{ DEP | @firebase-oss/ui-react@beta }}"],
- "registryDependencies": ["input", "button", "form", "{{ DOMAIN }}/r/policies.json"],
+ "dependencies": ["{{ DEP | @firebase-oss/ui-react@beta }}", "react-hook-form", "@hookform/resolvers"],
+ "registryDependencies": ["input", "button", "field", "{{ DOMAIN }}/r/policies.json"],
"files": [
{
"path": "src/components/forgot-password-auth-form.tsx",
@@ -379,9 +379,9 @@
"type": "registry:block",
"title": "Phone Auth Form",
"description": "A form allowing users to authenticate using their phone number with SMS verification.",
- "dependencies": ["{{ DEP | @firebase-oss/ui-react@beta }}"],
+ "dependencies": ["{{ DEP | @firebase-oss/ui-react@beta }}", "react-hook-form", "@hookform/resolvers"],
"registryDependencies": [
- "form",
+ "field",
"input",
"button",
"{{ DOMAIN }}/r/country-selector.json",
@@ -457,8 +457,8 @@
"type": "registry:block",
"title": "Sign In Auth Form",
"description": "A form allowing users to sign in with email and password.",
- "dependencies": ["{{ DEP | @firebase-oss/ui-react@beta }}"],
- "registryDependencies": ["input", "button", "form", "{{ DOMAIN }}/r/policies.json"],
+ "dependencies": ["{{ DEP | @firebase-oss/ui-react@beta }}", "react-hook-form", "@hookform/resolvers"],
+ "registryDependencies": ["input", "button", "field", "{{ DOMAIN }}/r/policies.json"],
"files": [
{
"path": "src/components/sign-in-auth-form.tsx",
@@ -496,8 +496,8 @@
"type": "registry:block",
"title": "Sign Up Auth Form",
"description": "A form allowing users to sign up with email and password.",
- "dependencies": ["{{ DEP | @firebase-oss/ui-react@beta }}"],
- "registryDependencies": ["input", "button", "form", "{{ DOMAIN }}/r/policies.json"],
+ "dependencies": ["{{ DEP | @firebase-oss/ui-react@beta }}", "react-hook-form", "@hookform/resolvers"],
+ "registryDependencies": ["input", "button", "field", "{{ DOMAIN }}/r/policies.json"],
"files": [
{
"path": "src/components/sign-up-auth-form.tsx",
@@ -535,8 +535,8 @@
"type": "registry:block",
"title": "SMS Multi-Factor Assertion Form",
"description": "A form allowing users to complete SMS-based multi-factor authentication during sign-in.",
- "dependencies": ["{{ DEP | @firebase-oss/ui-react@beta }}"],
- "registryDependencies": ["form", "input", "button", "input-otp"],
+ "dependencies": ["{{ DEP | @firebase-oss/ui-react@beta }}", "react-hook-form", "@hookform/resolvers"],
+ "registryDependencies": ["field", "input", "button", "input-otp"],
"files": [
{
"path": "src/components/sms-multi-factor-assertion-form.tsx",
@@ -552,8 +552,8 @@
"type": "registry:block",
"title": "SMS Multi-Factor Enrollment Form",
"description": "A form allowing users to enroll SMS-based multi-factor authentication.",
- "dependencies": ["{{ DEP | @firebase-oss/ui-react@beta }}"],
- "registryDependencies": ["form", "input", "button", "input-otp", "{{ DOMAIN }}/r/country-selector.json"],
+ "dependencies": ["{{ DEP | @firebase-oss/ui-react@beta }}", "react-hook-form", "@hookform/resolvers"],
+ "registryDependencies": ["field", "input", "button", "input-otp", "{{ DOMAIN }}/r/country-selector.json"],
"files": [
{
"path": "src/components/sms-multi-factor-enrollment-form.tsx",
@@ -569,8 +569,8 @@
"type": "registry:block",
"title": "TOTP Multi-Factor Assertion Form",
"description": "A form allowing users to complete TOTP-based multi-factor authentication during sign-in.",
- "dependencies": ["{{ DEP | @firebase-oss/ui-react@beta }}"],
- "registryDependencies": ["form", "button", "input-otp"],
+ "dependencies": ["{{ DEP | @firebase-oss/ui-react@beta }}", "react-hook-form", "@hookform/resolvers"],
+ "registryDependencies": ["field", "button", "input-otp"],
"files": [
{
"path": "src/components/totp-multi-factor-assertion-form.tsx",
@@ -586,8 +586,8 @@
"type": "registry:block",
"title": "TOTP Multi-Factor Enrollment Form",
"description": "A form allowing users to enroll TOTP-based multi-factor authentication with QR code generation.",
- "dependencies": ["{{ DEP | @firebase-oss/ui-react@beta }}"],
- "registryDependencies": ["form", "input", "button", "input-otp"],
+ "dependencies": ["{{ DEP | @firebase-oss/ui-react@beta }}", "react-hook-form", "@hookform/resolvers"],
+ "registryDependencies": ["field", "input", "button", "input-otp"],
"files": [
{
"path": "src/components/totp-multi-factor-enrollment-form.tsx",
diff --git a/packages/shadcn/src/components/email-link-auth-form.test.tsx b/packages/shadcn/src/components/email-link-auth-form.test.tsx
index 32110c633..6ee79d8e9 100644
--- a/packages/shadcn/src/components/email-link-auth-form.test.tsx
+++ b/packages/shadcn/src/components/email-link-auth-form.test.tsx
@@ -68,6 +68,21 @@ describe("", () => {
expect(container.querySelector("button[type='submit']")).toBeInTheDocument();
});
+ it("should associate the email label with input via htmlFor/id", () => {
+ const mockUI = createMockUI();
+
+ const { container } = render(
+
+
+
+ );
+
+ expect(container.querySelector('[data-slot="field-label"][for="email"]')).toBeInTheDocument();
+ expect(container.querySelector("input#email")).toBeInTheDocument();
+ expect(container.querySelector("input#email")?.getAttribute("aria-invalid")).toBe("false");
+ expect(container.querySelectorAll('[data-slot="field-error"]').length).toBe(0);
+ });
+
it("should call the onEmailSent callback when the form is submitted successfully", async () => {
const mockAction = vi.fn().mockResolvedValue(undefined);
vi.mocked(useEmailLinkAuthFormAction).mockReturnValue(mockAction);
diff --git a/packages/shadcn/src/components/email-link-auth-form.tsx b/packages/shadcn/src/components/email-link-auth-form.tsx
index 4c4cdf4e4..7a098f6be 100644
--- a/packages/shadcn/src/components/email-link-auth-form.tsx
+++ b/packages/shadcn/src/components/email-link-auth-form.tsx
@@ -27,12 +27,12 @@ import {
type EmailLinkAuthFormProps,
} from "@firebase-oss/ui-react";
import { useState } from "react";
-import { useForm } from "react-hook-form";
+import { useForm, FormProvider, Controller } from "react-hook-form";
import { Policies } from "@/components/policies";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
-import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
+import { Field, FieldLabel, FieldError } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
export type { EmailLinkAuthFormProps };
@@ -74,27 +74,25 @@ export function EmailLinkAuthForm(props: EmailLinkAuthFormProps) {
}
return (
-
+
);
}
diff --git a/packages/shadcn/src/components/forgot-password-auth-form.test.tsx b/packages/shadcn/src/components/forgot-password-auth-form.test.tsx
index b363e14e1..802d89b92 100644
--- a/packages/shadcn/src/components/forgot-password-auth-form.test.tsx
+++ b/packages/shadcn/src/components/forgot-password-auth-form.test.tsx
@@ -65,6 +65,21 @@ describe("", () => {
expect(container.querySelector("button[type='submit']")).toBeInTheDocument();
});
+ it("should associate the email label with input via htmlFor/id", () => {
+ const mockUI = createMockUI();
+
+ const { container } = render(
+
+
+
+ );
+
+ expect(container.querySelector('[data-slot="field-label"][for="email"]')).toBeInTheDocument();
+ expect(container.querySelector("input#email")).toBeInTheDocument();
+ expect(container.querySelector("input#email")?.getAttribute("aria-invalid")).toBe("false");
+ expect(container.querySelectorAll('[data-slot="field-error"]').length).toBe(0);
+ });
+
it("should render with back to sign in callback", () => {
const onBackToSignInClickMock = vi.fn();
const mockUI = createMockUI({
diff --git a/packages/shadcn/src/components/forgot-password-auth-form.tsx b/packages/shadcn/src/components/forgot-password-auth-form.tsx
index 0d4f106f1..ada00ea3b 100644
--- a/packages/shadcn/src/components/forgot-password-auth-form.tsx
+++ b/packages/shadcn/src/components/forgot-password-auth-form.tsx
@@ -23,12 +23,12 @@ import {
useUI,
type ForgotPasswordAuthFormProps,
} from "@firebase-oss/ui-react";
-import { useForm } from "react-hook-form";
+import { useForm, FormProvider, Controller } from "react-hook-form";
import { standardSchemaResolver } from "@hookform/resolvers/standard-schema";
import { FirebaseUIError, getTranslation } from "@firebase-oss/ui-core";
import { useState } from "react";
-import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
+import { Field, FieldLabel, FieldError } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Policies } from "./policies";
@@ -69,32 +69,30 @@ export function ForgotPasswordAuthForm(props: ForgotPasswordAuthFormProps) {
}
return (
-
+
);
}
diff --git a/packages/shadcn/src/components/phone-auth-form.test.tsx b/packages/shadcn/src/components/phone-auth-form.test.tsx
index 36fd8e8b9..5ec926dbd 100644
--- a/packages/shadcn/src/components/phone-auth-form.test.tsx
+++ b/packages/shadcn/src/components/phone-auth-form.test.tsx
@@ -225,7 +225,7 @@ describe("", () => {
expect(container.querySelector("input[name='verificationCode']")).toBeInTheDocument();
});
- const description = container.querySelector('[data-slot="form-description"]');
+ const description = container.querySelector('[data-slot="field-description"]');
expect(description).toBeInTheDocument();
expect(description).toHaveTextContent("Enter the verification code sent to your phone number");
});
diff --git a/packages/shadcn/src/components/phone-auth-form.tsx b/packages/shadcn/src/components/phone-auth-form.tsx
index a1a67ff82..6a51188e4 100644
--- a/packages/shadcn/src/components/phone-auth-form.tsx
+++ b/packages/shadcn/src/components/phone-auth-form.tsx
@@ -28,7 +28,7 @@ import {
import { useState } from "react";
import type { UserCredential } from "firebase/auth";
import { useRef } from "react";
-import { useForm } from "react-hook-form";
+import { useForm, FormProvider, Controller } from "react-hook-form";
import { standardSchemaResolver } from "@hookform/resolvers/standard-schema";
import {
FirebaseUIError,
@@ -38,7 +38,7 @@ import {
type PhoneAuthVerifyFormSchema,
} from "@firebase-oss/ui-core";
-import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
+import { Field, FieldLabel, FieldDescription, FieldError } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Policies } from "@/components/policies";
@@ -75,37 +75,35 @@ function VerifyPhoneNumberForm(props: VerifyPhoneNumberFormProps) {
}
return (
-
+
- (
-
- {getTranslation(ui, "labels", "verificationCode")}
- {getTranslation(ui, "prompts", "smsVerificationPrompt")}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ render={({ field, fieldState }) => (
+
+ {getTranslation(ui, "labels", "verificationCode")}
+ {getTranslation(ui, "prompts", "smsVerificationPrompt")}
+
+
+
+
+
+
+
+
+
+
+ {fieldState.error && {fieldState.error.message}}
+
)}
/>
- {form.formState.errors.root && {form.formState.errors.root.message}}
+ {form.formState.errors.root && {form.formState.errors.root.message}}
-
+
);
}
@@ -141,22 +139,20 @@ function PhoneNumberForm(props: PhoneNumberFormProps) {
}
return (
-
+
- (
-
- {getTranslation(ui, "labels", "phoneNumber")}
-
-
-
-
-
-
-
-
+ render={({ field, fieldState }) => (
+
+ {getTranslation(ui, "labels", "phoneNumber")}
+
+
+
+
+ {fieldState.error && {fieldState.error.message}}
+
)}
/>
@@ -164,9 +160,9 @@ function PhoneNumberForm(props: PhoneNumberFormProps) {
- {form.formState.errors.root && {form.formState.errors.root.message}}
+ {form.formState.errors.root && {form.formState.errors.root.message}}
-
+
);
}
diff --git a/packages/shadcn/src/components/sign-in-auth-form.test.tsx b/packages/shadcn/src/components/sign-in-auth-form.test.tsx
index c8d2710cb..b4b7a5672 100644
--- a/packages/shadcn/src/components/sign-in-auth-form.test.tsx
+++ b/packages/shadcn/src/components/sign-in-auth-form.test.tsx
@@ -208,6 +208,84 @@ describe("", () => {
expect(await screen.findByText("Error: foo")).toBeInTheDocument();
});
+ it("should associate labels with inputs via htmlFor/id", () => {
+ const mockUI = createMockUI();
+
+ const { container } = render(
+
+
+
+ );
+
+ const emailLabel = container.querySelector('[data-slot="field-label"][for="email"]');
+ const emailInput = container.querySelector("input#email");
+ expect(emailLabel).toBeInTheDocument();
+ expect(emailInput).toBeInTheDocument();
+
+ const passwordLabel = container.querySelector('[data-slot="field-label"][for="password"]');
+ const passwordInput = container.querySelector("input#password");
+ expect(passwordLabel).toBeInTheDocument();
+ expect(passwordInput).toBeInTheDocument();
+ });
+
+ it("should set aria-invalid and data-invalid on validation error", async () => {
+ const mockUI = createMockUI({
+ locale: registerLocale("test", {
+ labels: {
+ emailAddress: "Email Address",
+ password: "Password",
+ signIn: "Sign In",
+ },
+ errors: {
+ invalidEmail: "Please enter a valid email address",
+ weakPassword: "Password should be at least 6 characters",
+ },
+ }),
+ });
+
+ const { container } = render(
+
+
+
+ );
+
+ const emailInput = container.querySelector("input[name='email']") as HTMLInputElement;
+ const passwordInput = container.querySelector("input[name='password']") as HTMLInputElement;
+ const form = container.querySelector("form") as HTMLFormElement;
+
+ expect(emailInput.getAttribute("aria-invalid")).toBe("false");
+ expect(passwordInput.getAttribute("aria-invalid")).toBe("false");
+
+ await act(async () => {
+ fireEvent.submit(form);
+ });
+
+ await waitFor(() => {
+ expect(emailInput.getAttribute("aria-invalid")).toBe("true");
+ expect(passwordInput.getAttribute("aria-invalid")).toBe("true");
+ });
+
+ const fields = container.querySelectorAll('[data-slot="field"]');
+ const invalidFields = Array.from(fields).filter((f) => f.getAttribute("data-invalid") === "true");
+ expect(invalidFields.length).toBe(2);
+
+ const errors = container.querySelectorAll('[role="alert"]');
+ expect(errors.length).toBeGreaterThanOrEqual(2);
+ });
+
+ it("should not render field errors when fields are valid", () => {
+ const mockUI = createMockUI();
+
+ const { container } = render(
+
+
+
+ );
+
+ const fieldErrors = container.querySelectorAll('[data-slot="field-error"]');
+ expect(fieldErrors.length).toBe(0);
+ });
+
it("should show validation errors only after submit and clear after typing valid values", async () => {
const mockUI = createMockUI({
locale: registerLocale("test", {
diff --git a/packages/shadcn/src/components/sign-in-auth-form.tsx b/packages/shadcn/src/components/sign-in-auth-form.tsx
index 92e623365..39e28cf8f 100644
--- a/packages/shadcn/src/components/sign-in-auth-form.tsx
+++ b/packages/shadcn/src/components/sign-in-auth-form.tsx
@@ -23,11 +23,11 @@ import {
useUI,
type SignInAuthFormProps,
} from "@firebase-oss/ui-react";
-import { useForm } from "react-hook-form";
+import { useForm, FormProvider, Controller } from "react-hook-form";
import { standardSchemaResolver } from "@hookform/resolvers/standard-schema";
import { FirebaseUIError, getTranslation } from "@firebase-oss/ui-core";
-import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
+import { Field, FieldLabel, FieldError } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Policies } from "./policies";
@@ -59,46 +59,42 @@ export function SignInAuthForm(props: SignInAuthFormProps) {
}
return (
-
+
- (
-
- {getTranslation(ui, "labels", "emailAddress")}
-
-
-
-
-
+ render={({ field, fieldState }) => (
+
+ {getTranslation(ui, "labels", "emailAddress")}
+
+ {fieldState.error && {fieldState.error.message}}
+
)}
/>
- (
-
-
+ render={({ field, fieldState }) => (
+
+
{getTranslation(ui, "labels", "password")}
{props.onForgotPasswordClick ? (
) : null}
-
-
-
-
-
-
+
+
+ {fieldState.error && {fieldState.error.message}}
+
)}
/>
- {form.formState.errors.root && {form.formState.errors.root.message}}
+ {form.formState.errors.root && {form.formState.errors.root.message}}
{props.onSignUpClick ? (
<>
-
+
);
}
diff --git a/packages/shadcn/src/components/sign-up-auth-form.test.tsx b/packages/shadcn/src/components/sign-up-auth-form.test.tsx
index 7fce9445b..3c6ad7ccd 100644
--- a/packages/shadcn/src/components/sign-up-auth-form.test.tsx
+++ b/packages/shadcn/src/components/sign-up-auth-form.test.tsx
@@ -296,6 +296,79 @@ describe("", () => {
expect(onSignUpMock).toHaveBeenCalled();
});
+ it("should associate labels with inputs via htmlFor/id", () => {
+ vi.mocked(useRequireDisplayName).mockReturnValue(true);
+ const mockUI = createMockUI({
+ locale: registerLocale("test", {
+ labels: {
+ displayName: "Display Name",
+ },
+ }),
+ behaviors: [
+ {
+ requireDisplayName: { type: "callable" as const, handler: vi.fn() },
+ },
+ ],
+ });
+
+ const { container } = render(
+
+
+
+ );
+
+ expect(container.querySelector('[data-slot="field-label"][for="displayName"]')).toBeInTheDocument();
+ expect(container.querySelector("input#displayName")).toBeInTheDocument();
+ expect(container.querySelector('[data-slot="field-label"][for="email"]')).toBeInTheDocument();
+ expect(container.querySelector("input#email")).toBeInTheDocument();
+ expect(container.querySelector('[data-slot="field-label"][for="password"]')).toBeInTheDocument();
+ expect(container.querySelector("input#password")).toBeInTheDocument();
+ });
+
+ it("should set aria-invalid and data-invalid on validation error", async () => {
+ vi.mocked(useRequireDisplayName).mockReturnValue(false);
+ const mockUI = createMockUI({
+ locale: registerLocale("test", {
+ labels: {
+ emailAddress: "Email Address",
+ password: "Password",
+ createAccount: "Create Account",
+ },
+ errors: {
+ invalidEmail: "Please enter a valid email address",
+ weakPassword: "Password should be at least 6 characters",
+ },
+ }),
+ behaviors: [],
+ });
+
+ const { container } = render(
+
+
+
+ );
+
+ const emailInput = container.querySelector("input[name='email']") as HTMLInputElement;
+ const form = container.querySelector("form") as HTMLFormElement;
+
+ expect(emailInput.getAttribute("aria-invalid")).toBe("false");
+ expect(container.querySelectorAll('[data-slot="field-error"]').length).toBe(0);
+
+ await act(async () => {
+ fireEvent.submit(form);
+ });
+
+ await waitFor(() => {
+ expect(emailInput.getAttribute("aria-invalid")).toBe("true");
+ });
+
+ const invalidFields = container.querySelectorAll('[data-slot="field"][data-invalid="true"]');
+ expect(invalidFields.length).toBeGreaterThanOrEqual(1);
+
+ const errors = container.querySelectorAll('[role="alert"]');
+ expect(errors.length).toBeGreaterThanOrEqual(1);
+ });
+
it("should show validation errors after submit and clear when invalid values become valid", async () => {
vi.mocked(useRequireDisplayName).mockReturnValue(false);
const mockUI = createMockUI({
diff --git a/packages/shadcn/src/components/sign-up-auth-form.tsx b/packages/shadcn/src/components/sign-up-auth-form.tsx
index 032988feb..f95aa10bb 100644
--- a/packages/shadcn/src/components/sign-up-auth-form.tsx
+++ b/packages/shadcn/src/components/sign-up-auth-form.tsx
@@ -24,11 +24,11 @@ import {
type SignUpAuthFormProps,
useRequireDisplayName,
} from "@firebase-oss/ui-react";
-import { useForm } from "react-hook-form";
+import { useForm, FormProvider, Controller } from "react-hook-form";
import { standardSchemaResolver } from "@hookform/resolvers/standard-schema";
import { FirebaseUIError, getTranslation } from "@firebase-oss/ui-core";
-import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
+import { Field, FieldLabel, FieldError } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Policies } from "./policies";
@@ -62,54 +62,48 @@ export function SignUpAuthForm(props: SignUpAuthFormProps) {
}
return (
-
+
{requireDisplayName ? (
- (
-
- {getTranslation(ui, "labels", "displayName")}
-
-
-
-
-
+ render={({ field, fieldState }) => (
+
+ {getTranslation(ui, "labels", "displayName")}
+
+ {fieldState.error && {fieldState.error.message}}
+
)}
/>
) : null}
- (
-
- {getTranslation(ui, "labels", "emailAddress")}
-
-
-
-
-
+ render={({ field, fieldState }) => (
+
+ {getTranslation(ui, "labels", "emailAddress")}
+
+ {fieldState.error && {fieldState.error.message}}
+
)}
/>
- (
-
- {getTranslation(ui, "labels", "password")}
-
-
-
-
-
+ render={({ field, fieldState }) => (
+
+ {getTranslation(ui, "labels", "password")}
+
+ {fieldState.error && {fieldState.error.message}}
+
)}
/>
- {form.formState.errors.root && {form.formState.errors.root.message}}
+ {form.formState.errors.root && {form.formState.errors.root.message}}
{props.onSignInClick ? (
) : null}
-
+
);
}
diff --git a/packages/shadcn/src/components/sms-multi-factor-assertion-form.test.tsx b/packages/shadcn/src/components/sms-multi-factor-assertion-form.test.tsx
index ac8494ac1..88c042779 100644
--- a/packages/shadcn/src/components/sms-multi-factor-assertion-form.test.tsx
+++ b/packages/shadcn/src/components/sms-multi-factor-assertion-form.test.tsx
@@ -39,13 +39,13 @@ vi.mock("@/components/ui/input-otp", () => ({
}),
}));
-vi.mock("@/components/ui/form", async (importOriginal) => {
- const mod = await importOriginal();
+vi.mock("@/components/ui/field", async (importOriginal) => {
+ const mod = await importOriginal();
return {
...mod,
- FormItem: ({ children, ...props }: any) => React.createElement("div", { ...props }, children),
- FormLabel: ({ children, ...props }: any) => React.createElement("label", { ...props }, children),
- FormDescription: ({ children, ...props }: any) => React.createElement("p", { ...props }, children),
+ Field: ({ children, ...props }: any) => React.createElement("div", { ...props }, children),
+ FieldLabel: ({ children, ...props }: any) => React.createElement("label", { ...props }, children),
+ FieldDescription: ({ children, ...props }: any) => React.createElement("p", { ...props }, children),
};
});
diff --git a/packages/shadcn/src/components/sms-multi-factor-assertion-form.tsx b/packages/shadcn/src/components/sms-multi-factor-assertion-form.tsx
index 9a2b7ccfa..7f1651ad2 100644
--- a/packages/shadcn/src/components/sms-multi-factor-assertion-form.tsx
+++ b/packages/shadcn/src/components/sms-multi-factor-assertion-form.tsx
@@ -27,10 +27,10 @@ import {
useSmsMultiFactorAssertionPhoneFormAction,
useSmsMultiFactorAssertionVerifyFormAction,
} from "@firebase-oss/ui-react";
-import { useForm } from "react-hook-form";
+import { useForm, FormProvider, Controller } from "react-hook-form";
import { standardSchemaResolver } from "@hookform/resolvers/standard-schema";
-import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
+import { Field, FieldLabel, FieldDescription, FieldError } from "@/components/ui/field";
import { Button } from "@/components/ui/button";
import { InputOTP, InputOTPGroup, InputOTPSlot } from "@/components/ui/input-otp";
@@ -63,14 +63,14 @@ function SmsMultiFactorAssertionPhoneForm(props: SmsMultiFactorAssertionPhoneFor
return (
-
- {getTranslation(ui, "labels", "phoneNumber")}
-
+
+ {getTranslation(ui, "labels", "phoneNumber")}
+
{getTranslation(ui, "messages", "mfaSmsAssertionPrompt", {
phoneNumber: (props.hint as PhoneMultiFactorInfo).phoneNumber || "",
})}
-
-
+
+
-
+
- (
-
- {getTranslation(ui, "labels", "verificationCode")}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ render={({ field, fieldState }) => (
+
+ {getTranslation(ui, "labels", "verificationCode")}
+
+
+
+
+
+
+
+
+
+
+ {fieldState.error && {fieldState.error.message}}
+
)}
/>
{getTranslation(ui, "labels", "verifyCode")}
- {form.formState.errors.root && {form.formState.errors.root.message}}
+ {form.formState.errors.root && {form.formState.errors.root.message}}
-
+
);
}
diff --git a/packages/shadcn/src/components/ui/form.tsx b/packages/shadcn/src/components/ui/form.tsx
deleted file mode 100644
index a21641e10..000000000
--- a/packages/shadcn/src/components/ui/form.tsx
+++ /dev/null
@@ -1,138 +0,0 @@
-"use client";
-
-import * as React from "react";
-import type * as LabelPrimitive from "@radix-ui/react-label";
-import { Slot } from "@radix-ui/react-slot";
-import {
- Controller,
- FormProvider,
- useFormContext,
- useFormState,
- type ControllerProps,
- type FieldPath,
- type FieldValues,
-} from "react-hook-form";
-
-import { cn } from "@/lib/utils";
-import { Label } from "@/components/ui/label";
-
-const Form = FormProvider;
-
-type FormFieldContextValue<
- TFieldValues extends FieldValues = FieldValues,
- TName extends FieldPath = FieldPath,
-> = {
- name: TName;
-};
-
-const FormFieldContext = React.createContext({} as FormFieldContextValue);
-
-const FormField = <
- TFieldValues extends FieldValues = FieldValues,
- TName extends FieldPath = FieldPath,
->({
- ...props
-}: ControllerProps) => {
- return (
-
-
-
- );
-};
-
-const useFormField = () => {
- const fieldContext = React.useContext(FormFieldContext);
- const itemContext = React.useContext(FormItemContext);
- const { getFieldState } = useFormContext();
- const formState = useFormState({ name: fieldContext.name });
- const fieldState = getFieldState(fieldContext.name, formState);
-
- if (!fieldContext) {
- throw new Error("useFormField should be used within ");
- }
-
- const { id } = itemContext;
-
- return {
- id,
- name: fieldContext.name,
- formItemId: `${id}-form-item`,
- formDescriptionId: `${id}-form-item-description`,
- formMessageId: `${id}-form-item-message`,
- ...fieldState,
- };
-};
-
-type FormItemContextValue = {
- id: string;
-};
-
-const FormItemContext = React.createContext({} as FormItemContextValue);
-
-function FormItem({ className, ...props }: React.ComponentProps<"div">) {
- const id = React.useId();
-
- return (
-
-
-
- );
-}
-
-function FormLabel({ className, ...props }: React.ComponentProps) {
- const { error, formItemId } = useFormField();
-
- return (
-
- );
-}
-
-function FormControl({ ...props }: React.ComponentProps) {
- const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
-
- return (
-
- );
-}
-
-function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
- const { formDescriptionId } = useFormField();
-
- return (
-
- );
-}
-
-function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
- const { error, formMessageId } = useFormField();
- const body = error ? String(error?.message ?? "") : props.children;
-
- if (!body) {
- return null;
- }
-
- return (
-
- {body}
-
- );
-}
-
-export { useFormField, Form, FormItem, FormLabel, FormControl, FormDescription, FormMessage, FormField };