diff --git a/components/motion/input.tsx b/components/motion/input.tsx index 11058ca..38c30fe 100644 --- a/components/motion/input.tsx +++ b/components/motion/input.tsx @@ -38,6 +38,8 @@ export interface InputProps extends Omit< onChange?: (value: string) => void; /** Truthy error triggers a shake, red border and (if a string) a message. */ error?: string | boolean; + /** Reserve one message line so validation does not shift nearby content. */ + reserveErrorLine?: boolean; success?: boolean; leftIcon?: ReactNode; rightIcon?: ReactNode; @@ -54,6 +56,7 @@ export const Input = forwardRef(function Input( onFocus, onBlur, error, + reserveErrorLine = false, success, leftIcon, rightIcon, @@ -206,32 +209,34 @@ export const Input = forwardRef(function Input( ) : null} - - {errorMessage ? ( - - {errorMessage} - - ) : null} - +
+ + {errorMessage ? ( + + {errorMessage} + + ) : null} + +
); }); diff --git a/components/motion/signup-form.tsx b/components/motion/signup-form.tsx index 2a10ae8..3bdf406 100644 --- a/components/motion/signup-form.tsx +++ b/components/motion/signup-form.tsx @@ -289,6 +289,7 @@ export function SignUpForm({ onChange={(next) => setValue("name", next)} onBlur={() => touch("name")} error={shownError("name")} + reserveErrorLine success={isValid("name")} /> @@ -304,6 +305,7 @@ export function SignUpForm({ onChange={(next) => setValue("email", next)} onBlur={() => touch("email")} error={shownError("email")} + reserveErrorLine success={isValid("email")} /> @@ -330,6 +332,7 @@ export function SignUpForm({ onChange={(next) => setValue("password", next)} onBlur={() => touch("password")} error={shownError("password")} + reserveErrorLine /> @@ -383,6 +386,7 @@ export function SignUpForm({ onChange={(next) => setValue("confirmPassword", next)} onBlur={() => touch("confirmPassword")} error={shownError("confirmPassword")} + reserveErrorLine success={isValid("confirmPassword")} /> diff --git a/components/previews/motion/input.preview.tsx b/components/previews/motion/input.preview.tsx index 2def1a2..4920a3b 100644 --- a/components/previews/motion/input.preview.tsx +++ b/components/previews/motion/input.preview.tsx @@ -23,6 +23,7 @@ export function InputPreview() { value={email} onChange={setEmail} error={emailError} + reserveErrorLine /> { expect(onChange).toHaveBeenCalledWith("beUI"); }); + + test("can reserve a stable validation message row", () => { + const { container, getByRole, rerender } = render( + , + ); + const root = container.firstElementChild; + const messageRow = root?.lastElementChild; + + expect(messageRow?.classList).toContain("min-h-4"); + + rerender( + , + ); + + expect(root?.lastElementChild).toBe(messageRow); + expect(getByRole("alert").textContent).toBe("Enter a valid email address."); + }); });