11import type { HTMLAttributes , ReactNode } from 'react'
2+ import { useEffect } from 'react'
3+ import { useState } from 'react'
24import { useId } from 'react'
35import { clsx } from 'clsx'
46import type { BagFunction } from '@/src/utils/bagFunctions'
57import type { LabelProps } from '@/src/components/user-action/Label'
68import { Label } from '@/src/components/user-action/Label'
79
10+ type ErrorShowBehaviour = 'always' | 'whenTouched'
11+
812type FormElementWrapperBag = {
13+ 'touched' : boolean ,
14+ 'onTouched' : ( isTouched ?: boolean ) => void ,
915 'invalid' : boolean ,
1016 'required' : boolean ,
1117 'disabled' : boolean ,
@@ -17,10 +23,12 @@ type FormElementWrapperBag = {
1723export type FormElementWrapperProps = {
1824 children : BagFunction < FormElementWrapperBag > ,
1925 id ?: string ,
20- required : boolean ,
21- disabled : boolean ,
26+ required ?: boolean ,
27+ disabled ?: boolean ,
28+ isTouched ?: boolean ,
2229 label ?: ReactNode ,
2330 labelProps ?: Omit < LabelProps , 'children' > ,
31+ errorShowBehaviour ?: ErrorShowBehaviour ,
2432 error ?: ReactNode ,
2533 errorProps ?: Omit < HTMLAttributes < HTMLParagraphElement > , 'children' > ,
2634 description ?: ReactNode ,
@@ -33,49 +41,60 @@ export const FormElementWrapper = ({
3341 id,
3442 required = false ,
3543 disabled = false ,
44+ isTouched : initialIsTouched = false ,
3645 label,
3746 labelProps,
47+ errorShowBehaviour = 'whenTouched' ,
3848 error,
3949 errorProps,
4050 description,
4151 descriptionProps,
4252 containerClassName,
4353 } : FormElementWrapperProps ) => {
54+ const [ touched , setTouched ] = useState ( initialIsTouched )
4455 const generatedId = useId ( )
4556 const usedId = id ?? generatedId
4657
58+ useEffect ( ( ) => {
59+ setTouched ( initialIsTouched )
60+ } , [ initialIsTouched ] )
61+
4762 const describedBy : string = [
4863 description ? `${ usedId } -description` : undefined ,
4964 error ? `${ usedId } -error` : undefined ,
5065 ] . filter ( Boolean ) . join ( ' ' )
5166
5267 const labeledBy = label ? `${ usedId } -label` : undefined
5368
69+ const isShowingError = errorShowBehaviour === 'always' || ( touched )
70+
5471 const bag : FormElementWrapperBag = {
72+ 'touched' : touched ,
73+ 'onTouched' : ( isTouched ) => setTouched ( isTouched ?? true ) ,
5574 'disabled' : disabled ,
56- 'invalid' : ! ! error ,
75+ 'invalid' : ! ! error && isShowingError ,
5776 'required' : required ,
5877 'id' : usedId ,
5978 'aria-describedby' : describedBy ,
6079 'aria-labelby' : labeledBy
6180 }
6281
6382 return (
64- < div className = { clsx ( 'flex flex-col gap-y-1' , containerClassName ) } >
83+ < div className = { clsx ( 'relative flex flex-col gap-y-1' , containerClassName ) } >
6584 { label && (
66- < Label htmlFor = { usedId } className = { clsx ( 'typography-label-md ' , labelProps ?. className ) } >
85+ < Label htmlFor = { usedId } size = "lg" className = { clsx ( 'flex-row-1 items-start ' , labelProps ?. className ) } >
6786 { label }
68- { required && < span role = "none" className = "text -primary font-bold" > * </ span > }
87+ { required && < div role = "none" className = "bg -primary w-2 h-2 rounded-full" / >}
6988 </ Label >
7089 ) }
7190 { description && (
72- < p { ...descriptionProps } className = { clsx ( 'text-description text-xs ' , descriptionProps ?. className ) } >
91+ < p { ...descriptionProps } className = { clsx ( 'text-description text-sm ' , descriptionProps ?. className ) } >
7392 { description }
7493 </ p >
7594 ) }
7695 { children ( bag ) }
77- { error && (
78- < p { ...errorProps } role = "alert" className = { clsx ( 'text-negative text-sm font-medium' , errorProps ) } >
96+ { error && isShowingError && (
97+ < p { ...errorProps } role = "alert" className = { clsx ( 'absolute top-[calc(100%_+_0.25rem)] left-0 text-negative text-sm font-medium' , errorProps ) } >
7998 { error }
8099 </ p >
81100 ) }
0 commit comments