@douglasneuroinformatics/libui 3.7.6 → 3.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@douglasneuroinformatics/libui",
3
3
  "type": "module",
4
- "version": "3.7.6",
4
+ "version": "3.8.0",
5
5
  "packageManager": "pnpm@9.14.2",
6
6
  "description": "Generic UI components for DNP projects, built using React and Tailwind CSS",
7
7
  "author": "Joshua Unrau",
@@ -18,20 +18,24 @@ const $ExampleFormData = z.object({
18
18
  booleanCheck: z.boolean().optional(),
19
19
  booleanRadio: z.boolean().optional(),
20
20
  recordArray: z.array(
21
- z.object({
22
- recordArrayStringInput: z.string(),
23
- showRecordArrayDynamicField: z.boolean(),
24
- recordArrayDynamicField: z.string().optional()
25
- })
21
+ z
22
+ .object({
23
+ recordArrayStringInput: z.string(),
24
+ showRecordArrayDynamicField: z.boolean(),
25
+ recordArrayDynamicField: z.string()
26
+ })
27
+ .partial()
26
28
  ),
27
29
  date: z.date().optional(),
28
30
  numberInput: z.number().optional(),
29
31
  numberSlider: z.number().optional(),
30
- numberRecord: z.object({
31
- q1: z.number(),
32
- q2: z.number(),
33
- q3: z.number()
34
- }),
32
+ numberRecord: z
33
+ .object({
34
+ q1: z.number(),
35
+ q2: z.number(),
36
+ q3: z.number()
37
+ })
38
+ .partial(),
35
39
  numberRadio: z.number().min(1).max(5).int().optional(),
36
40
  numberSelect: z.number().min(1).max(5).int().optional(),
37
41
  stringSelect: z.enum(['a', 'b', 'c']).optional(),
@@ -13,6 +13,7 @@ import type { Promisable } from 'type-fest';
13
13
  import { z } from 'zod';
14
14
 
15
15
  import { useTranslation } from '@/hooks';
16
+ import { cn } from '@/utils';
16
17
 
17
18
  import { Button } from '../Button';
18
19
  import { Heading } from '../Heading';
@@ -40,6 +41,7 @@ type FormProps<TSchema extends z.ZodType<FormDataType>, TData extends z.TypeOf<T
40
41
  resetBtn?: boolean;
41
42
  revalidateOnBlur?: boolean;
42
43
  submitBtnLabel?: string;
44
+ suspendWhileSubmitting?: boolean;
43
45
  validationSchema: z.ZodType<TData>;
44
46
  };
45
47
 
@@ -58,6 +60,7 @@ const Form = <TSchema extends z.ZodType<FormDataType>, TData extends z.TypeOf<TS
58
60
  resetBtn,
59
61
  revalidateOnBlur,
60
62
  submitBtnLabel,
63
+ suspendWhileSubmitting,
61
64
  validationSchema,
62
65
  ...props
63
66
  }: FormProps<TSchema, TData>) => {
@@ -67,6 +70,7 @@ const Form = <TSchema extends z.ZodType<FormDataType>, TData extends z.TypeOf<TS
67
70
  const [values, setValues] = useState<PartialFormDataType<TData>>(
68
71
  initialValues ? getInitialValues(initialValues) : {}
69
72
  );
73
+ const [isSubmitting, setIsSubmitting] = useState(false);
70
74
 
71
75
  const handleError = (error: z.ZodError<TData>) => {
72
76
  const fieldErrors: FormErrors<TData> = {};
@@ -97,14 +101,21 @@ const Form = <TSchema extends z.ZodType<FormDataType>, TData extends z.TypeOf<TS
97
101
  };
98
102
 
99
103
  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);
108
119
  }
109
120
  };
110
121
 
@@ -128,6 +139,8 @@ const Form = <TSchema extends z.ZodType<FormDataType>, TData extends z.TypeOf<TS
128
139
  revalidate();
129
140
  }, [resolvedLanguage]);
130
141
 
142
+ const isSuspended = Boolean(suspendWhileSubmitting && isSubmitting);
143
+
131
144
  return (
132
145
  <form
133
146
  autoComplete="off"
@@ -176,8 +189,28 @@ const Form = <TSchema extends z.ZodType<FormDataType>, TData extends z.TypeOf<TS
176
189
  <div className="flex w-full gap-3">
177
190
  {additionalButtons?.left}
178
191
  {/** 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
+ >
180
199
  {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>
181
214
  </Button>
182
215
  {resetBtn && (
183
216
  <Button