@@ -51,7 +51,7 @@ const SIZE_CLASSES = {
5151} ;
5252
5353/**
54- * Input base component with core functionality and shared styles.
54+ * Input component with core functionality (submitting, clearing, validating) and shared styles.
5555 */
5656export const Input = React . forwardRef < HTMLInputElement | HTMLTextAreaElement , InputProps > (
5757 ( props , passedRef ) => {
@@ -81,40 +81,47 @@ export const Input = React.forwardRef<HTMLInputElement | HTMLTextAreaElement, In
8181 ...rest
8282 } = props ;
8383
84- const [ value , setValue ] = React . useState ( initialValue ?? '' ) ;
84+ const [ internalValue , setInternalValue ] = React . useState ( initialValue ?? '' ) ;
8585 const [ submitted , setSubmitted ] = React . useState ( false ) ;
8686 const inputRef = React . useRef < HTMLInputElement | HTMLTextAreaElement > ( null ) ;
8787 const ref = passedRef ?? inputRef ;
8888
8989 const language = useLanguage ( ) ;
90+ const isControlled = 'value' in props ;
91+ const value = isControlled ? ( initialValue ?? '' ) : internalValue ;
9092 const hasValue = value . toString ( ) . trim ( ) ;
9193 const hasValidValue =
9294 hasValue &&
9395 ( maxLength ? value . toString ( ) . length <= maxLength : true ) &&
9496 ( minLength ? value . toString ( ) . length >= minLength : true ) ;
9597
96- React . useEffect ( ( ) => {
97- setValue ( initialValue ?? '' ) ;
98- } , [ initialValue ] ) ;
99-
10098 const handleChange = ( event : React . ChangeEvent < HybridInputElement > ) => {
101- setValue ( event . target . value ) ;
99+ const newValue = event . target . value ;
100+ if ( ! isControlled ) {
101+ setInternalValue ( newValue ) ;
102+ }
102103 onChange ?.( event ) ;
103104
104- // Auto-resize
105105 if ( multiline && resize && 'current' in ref && ref . current ) {
106106 ref . current . style . height = 'auto' ;
107107 ref . current . style . height = `${ ref . current . scrollHeight } px` ;
108108 }
109109 } ;
110110
111+ const handleClear = ( ) => {
112+ if ( ! ( 'current' in ref ) || ! ref . current ) return ;
113+
114+ const syntheticEvent = {
115+ target : { value : '' } ,
116+ currentTarget : ref . current ,
117+ } as React . ChangeEvent < HybridInputElement > ;
118+
119+ handleChange ( syntheticEvent ) ;
120+ } ;
121+
111122 const handleClick = ( ) => {
112123 if ( ! ( 'current' in ref ) ) return ;
113-
114- const element = ref . current ;
115- if ( element ) {
116- element . focus ( ) ;
117- }
124+ ref . current ?. focus ( ) ;
118125 } ;
119126
120127 const handleSubmit = ( ) => {
@@ -124,38 +131,37 @@ export const Input = React.forwardRef<HTMLInputElement | HTMLTextAreaElement, In
124131 }
125132 } ;
126133
127- const input = (
128- < input
129- className = { tcls (
130- 'peer -m-2 max-h-64 grow resize-none text-left outline-none placeholder:text-tint/8 aria-busy:cursor-progress' ,
131- SIZE_CLASSES [ sizing ] . input
132- ) }
133- type = "text"
134- ref = { ref as React . Ref < HTMLInputElement > }
135- value = { value }
136- size = { 1 } // This will make the input have the smallest possible width (1 character) so we can grow it with flexbox
137- onKeyDown = { ( event ) => {
138- if ( event . key === 'Enter' && ! event . shiftKey && value . toString ( ) . trim ( ) ) {
139- event . preventDefault ( ) ;
140- handleSubmit ( ) ;
141- }
142- if ( event . key === 'Escape' ) {
143- event . preventDefault ( ) ;
144- event . currentTarget . blur ( ) ;
145- }
146- onKeyDown ?.( event as React . KeyboardEvent < HybridInputElement > ) ;
147- } }
148- aria-busy = { ariaBusy }
149- onChange = { handleChange }
150- aria-label = { ariaLabel ?? label }
151- placeholder = { placeholder ? placeholder : label }
152- disabled = { disabled }
153- maxLength = { maxLength }
154- minLength = { minLength }
155- { ...( rest as React . InputHTMLAttributes < HTMLInputElement > ) }
156- />
134+ const handleKeyDown = ( event : React . KeyboardEvent < HybridInputElement > ) => {
135+ if ( event . key === 'Enter' && ! event . shiftKey && hasValue ) {
136+ event . preventDefault ( ) ;
137+ handleSubmit ( ) ;
138+ } else if ( event . key === 'Escape' ) {
139+ event . preventDefault ( ) ;
140+ event . currentTarget . blur ( ) ;
141+ }
142+ onKeyDown ?.( event ) ;
143+ } ;
144+
145+ const inputClassName = tcls (
146+ 'peer -m-2 max-h-64 grow resize-none text-left outline-none placeholder:text-tint/8 aria-busy:cursor-progress' ,
147+ SIZE_CLASSES [ sizing ] . input
157148 ) ;
158149
150+ const inputProps = {
151+ className : inputClassName ,
152+ ref : ref as React . Ref < HTMLInputElement | HTMLTextAreaElement > ,
153+ value : value ,
154+ onKeyDown : handleKeyDown ,
155+ 'aria-busy' : ariaBusy ,
156+ onChange : handleChange ,
157+ 'aria-label' : ariaLabel ?? label ,
158+ placeholder : placeholder ?? label ,
159+ disabled : disabled ,
160+ maxLength : maxLength ,
161+ minLength : minLength ,
162+ ...rest ,
163+ } ;
164+
159165 return (
160166 < div
161167 className = { tcls (
@@ -206,33 +212,37 @@ export const Input = React.forwardRef<HTMLInputElement | HTMLTextAreaElement, In
206212 multiline ? 'mt-0.5' : '' ,
207213 hasValue ? 'group-focus-within/input:flex' : ''
208214 ) }
209- onClick = { ( ) => {
210- handleChange ( {
211- target : {
212- value : '' ,
213- } ,
214- } as React . ChangeEvent < HybridInputElement > ) ;
215- } }
215+ onClick = { handleClear }
216216 />
217217 ) : null }
218- { multiline ? < textarea { ...input . props } /> : input }
218+ { multiline ? (
219+ < textarea
220+ { ...( inputProps as React . TextareaHTMLAttributes < HTMLTextAreaElement > ) }
221+ />
222+ ) : (
223+ < input
224+ { ...( inputProps as React . InputHTMLAttributes < HTMLInputElement > ) }
225+ type = "text"
226+ size = { 1 }
227+ />
228+ ) }
219229
220- < div className = { multiline ? 'absolute top-2.5 right-2.5' : '' } >
221- { keyboardShortcut !== false ? (
222- typeof keyboardShortcut === 'object' ? (
230+ { keyboardShortcut !== false ? (
231+ < div className = { multiline ? 'absolute top-2.5 right-2.5' : '' } >
232+ { typeof keyboardShortcut === 'object' ? (
223233 < KeyboardShortcut { ...keyboardShortcut } />
224234 ) : onSubmit && ! submitted && hasValue ? (
225235 < KeyboardShortcut
226236 keys = { [ 'enter' ] }
227237 className = "hidden bg-tint-base group-focus-within/input:flex"
228238 />
229- ) : null
230- ) : null }
231- </ div >
239+ ) : null }
240+ </ div >
241+ ) : null }
232242 </ div >
233243 { trailing || submitButton || maxLength ? (
234244 < div className = "flex items-center gap-2 empty:hidden" >
235- { trailing ? trailing : null }
245+ { trailing }
236246 { maxLength && ! submitted && value . toString ( ) . length > maxLength * 0.8 ? (
237247 < span
238248 className = { tcls (
@@ -266,9 +276,7 @@ export const Input = React.forwardRef<HTMLInputElement | HTMLTextAreaElement, In
266276 disabled = { disabled || ! hasValidValue }
267277 iconOnly = { ! multiline }
268278 className = "ml-auto"
269- { ...( typeof submitButton === 'object'
270- ? { ...submitButton }
271- : undefined ) }
279+ { ...( typeof submitButton === 'object' ? submitButton : { } ) }
272280 />
273281 ) : null }
274282 </ div >
0 commit comments