FormLayout.tsx434 lines · main
1import { cva } from 'class-variance-authority'
2import React from 'react'
3import { cn, FormDescription, FormLabel, FormMessage, Label } from 'ui'
4import { SIZE } from 'ui/src/lib/constants'
5
6type Props = {
7 align?: 'left' | 'right'
8 description?: string | React.ReactNode
9 error?: string | React.ReactNode
10 label?: string | React.ReactNode
11 labelOptional?: string | React.ReactNode
12 layout?: 'horizontal' | 'vertical' | 'flex' | 'flex-row-reverse'
13 size?: 'tiny' | 'small' | 'medium' | 'large' | 'xlarge'
14 beforeLabel?: string | React.ReactNode
15 afterLabel?: string | React.ReactNode
16 nonBoxInput?: boolean
17 labelLayout?: 'horizontal' | 'vertical'
18 isReactForm?: boolean
19 hideMessage?: boolean
20 name?: string
21}
22
23const ContainerVariants = cva('relative grid gap-10', {
24 variants: {
25 size: {
26 tiny: 'text-xs',
27 small: 'text-base md:text-sm leading-4',
28 medium: 'text-base md:text-sm',
29 large: 'text-base',
30 xlarge: 'text-base',
31 },
32 align: { left: '', right: '' },
33 responsive: {
34 true: '',
35 false: '',
36 },
37 layout: {
38 horizontal: 'flex flex-col gap-2 md:grid md:grid-cols-12',
39 vertical: 'flex flex-col gap-2',
40 flex: 'flex flex-row gap-3',
41 'flex-row-reverse':
42 'flex flex-col-reverse gap-2 md:gap-6 md:flex-row-reverse md:justify-between',
43 },
44 flex: {
45 true: '',
46 false: '',
47 },
48 },
49 compoundVariants: [
50 {
51 layout: 'flex',
52 align: 'right',
53 className: 'justify-between',
54 },
55 {
56 layout: 'flex-row-reverse',
57 align: 'right',
58 className: 'justify-between',
59 },
60 ],
61 defaultVariants: {},
62})
63
64const LabelContainerVariants = cva('transition-all duration-500 ease-in-out', {
65 variants: {
66 flex: {
67 true: '',
68 false: '',
69 },
70 align: {
71 left: '',
72 right: '',
73 },
74 layout: {
75 horizontal: 'flex flex-col gap-2 col-span-4',
76 vertical: 'flex flex-row gap-2 justify-between',
77 flex: 'flex flex-col gap-0 min-w-0',
78 'flex-row-reverse': 'flex flex-col min-w-0 grow',
79 },
80 labelLayout: {
81 horizontal: '',
82 vertical: '',
83 '': '',
84 },
85 },
86 compoundVariants: [
87 {
88 flex: true,
89 align: 'left',
90 className: 'order-2',
91 },
92 {
93 flex: true,
94 align: 'right',
95 className: 'order-1',
96 },
97 {
98 layout: ['vertical', 'flex'],
99 labelLayout: undefined,
100 flex: false,
101 className: 'flex flex-row gap-2 justify-between',
102 },
103 {
104 layout: 'horizontal',
105 className: 'flex flex-col gap-2',
106 },
107 ],
108 defaultVariants: {},
109})
110
111const DataContainerVariants = cva('transition-all duration-500 ease-in-out', {
112 variants: {
113 flex: {
114 true: '',
115 false: '',
116 },
117 align: {
118 left: 'order-1',
119 right: 'order-2',
120 },
121 layout: {
122 horizontal: '',
123 vertical: '',
124 flex: '',
125 'flex-row-reverse': '',
126 },
127 },
128 compoundVariants: [
129 {
130 flex: true,
131 align: 'left',
132 className: 'order-1',
133 },
134 {
135 flex: true,
136 align: 'right',
137 className: 'order-2',
138 },
139 {
140 layout: ['vertical', 'flex'],
141 className: 'col-span-12',
142 },
143 {
144 layout: 'horizontal',
145 align: 'left',
146 className: 'col-span-8',
147 },
148 {
149 layout: 'horizontal',
150 align: 'right',
151 className: 'text-right',
152 },
153 ],
154 defaultVariants: {},
155})
156
157const DescriptionVariants = cva('text-foreground-lighter leading-normal', {
158 variants: {
159 size: {
160 ...SIZE.text,
161 },
162 layout: {
163 vertical: 'mt-2',
164 horizontal: 'mt-2',
165 flex: '',
166 'flex-row-reverse': '',
167 },
168 },
169 defaultVariants: {},
170})
171
172const LabelBeforeVariants = cva('text-foreground-muted', {
173 variants: {
174 size: {
175 ...SIZE.text,
176 },
177 },
178 defaultVariants: {},
179})
180
181const LabelAfterVariants = cva('text-foreground-muted', {
182 variants: {
183 size: {
184 ...SIZE.text,
185 },
186 },
187 defaultVariants: {},
188})
189
190const LabelOptionalVariants = cva('text-foreground-muted', {
191 variants: {
192 size: {
193 ...SIZE.text,
194 },
195 },
196 defaultVariants: {},
197})
198
199const FlexContainer = cva('', {
200 variants: {
201 flex: {
202 true: '',
203 false: '',
204 },
205 align: {
206 left: '',
207 right: '',
208 },
209 layout: {
210 horizontal: '',
211 vertical: '',
212 flex: '',
213 'flex-row-reverse': '',
214 },
215 },
216 compoundVariants: [
217 {
218 flex: true,
219 align: 'left',
220 className: '',
221 },
222 {
223 flex: true,
224 align: 'right',
225 className: 'order-last',
226 },
227 {
228 layout: 'flex-row-reverse',
229 className:
230 'flex flex-col justify-center items-start md:items-end shrink-0 md:w-1/2 xl:w-2/5 [&>div]:md:w-full',
231 },
232 ],
233})
234
235const NonBoxInputContainer = cva('', {
236 variants: {
237 nonBoxInput: {
238 true: '',
239 false: '',
240 },
241 label: {
242 true: '',
243 false: '',
244 },
245 layout: {
246 vertical: '',
247 horizontal: '',
248 'flex-row-reverse': '',
249 },
250 },
251 compoundVariants: [
252 {
253 nonBoxInput: true,
254 label: true,
255 layout: 'vertical',
256 className: 'my-3',
257 },
258 {
259 nonBoxInput: true,
260 label: true,
261 layout: 'horizontal',
262 className: 'my-3 md:mt-0 mb-3',
263 },
264 ],
265 defaultVariants: {},
266})
267
268export const FormLayout = React.forwardRef<
269 HTMLDivElement,
270 React.ComponentPropsWithoutRef<'div'> & Props
271>(
272 (
273 {
274 align = 'left',
275 className,
276 description,
277 id,
278 label,
279 labelOptional,
280 layout = 'vertical',
281 style,
282 labelLayout,
283 size = 'medium',
284 beforeLabel,
285 afterLabel,
286 nonBoxInput = !label,
287 hideMessage = false,
288 isReactForm,
289 error,
290 ...props
291 },
292 ref
293 ) => {
294 const flex = layout === 'flex' || layout === 'flex-row-reverse'
295 const hasLabel = Boolean(label || beforeLabel || afterLabel)
296 const renderError =
297 isReactForm && !hideMessage ? (
298 <FormMessage
299 className={cn(
300 'mt-2 transition-all duration-300 ease-in-out',
301 layout === 'flex-row-reverse' && 'mt-0'
302 )}
303 data-formlayout-id="message"
304 />
305 ) : error && !hideMessage ? (
306 <p className={cn('mt-2 text-sm text-destructive', layout === 'flex-row-reverse' && 'mt-0')}>
307 {error}
308 </p>
309 ) : null
310
311 const renderDescription =
312 description && isReactForm ? (
313 <FormDescription
314 className={cn(DescriptionVariants({ size, layout }))}
315 data-formlayout-id={'description'}
316 id={`${id}-description`}
317 >
318 {description}
319 </FormDescription>
320 ) : description ? (
321 <p
322 className={cn(DescriptionVariants({ size, layout }), 'text-sm text-foreground-light')}
323 data-formlayout-id={'description'}
324 >
325 {description}
326 </p>
327 ) : null
328
329 const LabelContents = () => (
330 <>
331 {beforeLabel && (
332 <span
333 className={cn(LabelBeforeVariants({ size }))}
334 id={id + '-before'}
335 data-formlayout-id={'beforeLabel'}
336 >
337 <span>{beforeLabel}</span>
338 </span>
339 )}
340 <span>{label}</span>
341 {afterLabel && (
342 <span
343 className={cn(LabelAfterVariants({ size }))}
344 id={id + '-after'}
345 data-formlayout-id={'afterLabel'}
346 >
347 {afterLabel}
348 </span>
349 )}
350 </>
351 )
352
353 return (
354 <div
355 ref={ref}
356 {...props}
357 className={cn(ContainerVariants({ size, flex, align, layout }), className)}
358 >
359 {flex && (
360 <div className={cn(FlexContainer({ flex, align, layout }))}>
361 {props.children}
362 {layout === 'flex-row-reverse' && renderError}
363 </div>
364 )}
365 {hasLabel || labelOptional || layout === 'horizontal' ? (
366 <>
367 <div
368 className={cn(LabelContainerVariants({ align, labelLayout, flex, layout }))}
369 data-formlayout-id={'labelContainer'}
370 >
371 {hasLabel && isReactForm ? (
372 <FormLabel
373 className="text-foreground flex gap-2 items-center wrap-break-word"
374 data-formlayout-id="formLabel"
375 htmlFor={props.name || id}
376 >
377 <LabelContents />
378 </FormLabel>
379 ) : (
380 <Label
381 className="text-foreground flex gap-2 items-center wrap-break-word leading-normal"
382 data-formlayout-id="label"
383 htmlFor={props.name || id}
384 >
385 <LabelContents />
386 </Label>
387 )}
388 {labelOptional && (
389 <span
390 className={cn(LabelOptionalVariants({ size }))}
391 id={id + '-optional'}
392 data-formlayout-id={'labelOptional'}
393 >
394 {labelOptional}
395 </span>
396 )}
397 {flex && (
398 <>
399 {renderDescription}
400 {layout !== 'flex-row-reverse' && renderError}
401 </>
402 )}
403 </div>
404 </>
405 ) : null}
406 {!flex && (
407 <div
408 className={cn(DataContainerVariants({ align, layout }))}
409 style={style}
410 data-formlayout-id={'dataContainer'}
411 >
412 <>
413 <div
414 className={cn(
415 NonBoxInputContainer({
416 nonBoxInput,
417 // @ts-expect-error
418 label,
419 layout,
420 })
421 )}
422 data-formlayout-id={'nonBoxInputContainer'}
423 >
424 {props.children}
425 </div>
426 {renderError}
427 {renderDescription}
428 </>
429 </div>
430 )}
431 </div>
432 )
433 }
434)