@@ -13,6 +13,7 @@ import type { Promisable } from 'type-fest';
1313import { z } from 'zod' ;
1414
1515import { useTranslation } from '@/hooks' ;
16+ import { cn } from '@/utils' ;
1617
1718import { Button } from '../Button' ;
1819import { Heading } from '../Heading' ;
@@ -40,6 +41,7 @@ type FormProps<TSchema extends z.ZodType<FormDataType>, TData extends z.TypeOf<T
4041 resetBtn ?: boolean ;
4142 revalidateOnBlur ?: boolean ;
4243 submitBtnLabel ?: string ;
44+ suspendWhileSubmitting ?: boolean ;
4345 validationSchema : z . ZodType < TData > ;
4446} ;
4547
@@ -58,6 +60,7 @@ const Form = <TSchema extends z.ZodType<FormDataType>, TData extends z.TypeOf<TS
5860 resetBtn,
5961 revalidateOnBlur,
6062 submitBtnLabel,
63+ suspendWhileSubmitting,
6164 validationSchema,
6265 ...props
6366} : FormProps < TSchema , TData > ) => {
@@ -67,6 +70,7 @@ const Form = <TSchema extends z.ZodType<FormDataType>, TData extends z.TypeOf<TS
6770 const [ values , setValues ] = useState < PartialFormDataType < TData > > (
6871 initialValues ? getInitialValues ( initialValues ) : { }
6972 ) ;
73+ const [ isSubmitting , setIsSubmitting ] = useState ( false ) ;
7074
7175 const handleError = ( error : z . ZodError < TData > ) => {
7276 const fieldErrors : FormErrors < TData > = { } ;
@@ -97,14 +101,21 @@ const Form = <TSchema extends z.ZodType<FormDataType>, TData extends z.TypeOf<TS
97101 } ;
98102
99103 const handleSubmit = async ( event : React . FormEvent < HTMLFormElement > ) => {
100- event . preventDefault ( ) ;
101- const result = await validationSchema . safeParseAsync ( values ) ;
102- if ( result . success ) {
103- reset ( ) ;
104- await onSubmit ( result . data ) ;
105- } else {
106- console . error ( result . error . issues ) ;
107- handleError ( result . error ) ;
104+ const minSubmitTime = new Promise ( ( resolve ) => setTimeout ( resolve , 500 ) ) ;
105+ try {
106+ setIsSubmitting ( true ) ;
107+ event . preventDefault ( ) ;
108+ const result = await validationSchema . safeParseAsync ( values ) ;
109+ if ( result . success ) {
110+ reset ( ) ;
111+ await onSubmit ( result . data ) ;
112+ } else {
113+ console . error ( result . error . issues ) ;
114+ handleError ( result . error ) ;
115+ }
116+ } finally {
117+ await minSubmitTime ;
118+ setIsSubmitting ( false ) ;
108119 }
109120 } ;
110121
@@ -128,6 +139,8 @@ const Form = <TSchema extends z.ZodType<FormDataType>, TData extends z.TypeOf<TS
128139 revalidate ( ) ;
129140 } , [ resolvedLanguage ] ) ;
130141
142+ const isSuspended = Boolean ( suspendWhileSubmitting && isSubmitting ) ;
143+
131144 return (
132145 < form
133146 autoComplete = "off"
@@ -176,8 +189,28 @@ const Form = <TSchema extends z.ZodType<FormDataType>, TData extends z.TypeOf<TS
176189 < div className = "flex w-full gap-3" >
177190 { additionalButtons ?. left }
178191 { /** Note - aria-label is used for testing in downstream packages */ }
179- < Button aria-label = "Submit" className = "block w-full" disabled = { readOnly } type = "submit" variant = "primary" >
192+ < Button
193+ aria-label = "Submit"
194+ className = "flex w-32 items-center justify-center gap-2"
195+ disabled = { readOnly || isSuspended }
196+ type = "submit"
197+ variant = "primary"
198+ >
180199 { submitBtnLabel ?? t ( 'form.submit' ) }
200+ < svg
201+ className = { cn ( 'hidden h-4 w-4 animate-spin' , isSuspended && 'block' ) }
202+ fill = "none"
203+ height = "24"
204+ stroke = "currentColor"
205+ strokeLinecap = "round"
206+ strokeLinejoin = "round"
207+ strokeWidth = "2"
208+ viewBox = "0 0 24 24"
209+ width = "24"
210+ xmlns = "http://www.w3.org/2000/svg"
211+ >
212+ < path d = "M21 12a9 9 0 1 1-6.219-8.56" />
213+ </ svg >
181214 </ Button >
182215 { resetBtn && (
183216 < Button
0 commit comments