diff --git a/packages/ui/src/components/TextField.tsx b/packages/ui/src/components/TextField.tsx index 0741a952b..7e97a0aae 100644 --- a/packages/ui/src/components/TextField.tsx +++ b/packages/ui/src/components/TextField.tsx @@ -1,5 +1,6 @@ 'use client'; +import { useState } from 'react'; import { TextField as AriaTextField } from 'react-aria-components'; import type { TextFieldProps as AriaTextFieldProps, @@ -30,6 +31,7 @@ export interface TextFieldProps extends AriaTextFieldProps { labelClassName?: string; errorClassName?: string; useTextArea?: boolean; + maxLength?: number; } export const TextField = ({ @@ -44,6 +46,7 @@ export const TextField = ({ labelClassName, errorClassName, useTextArea, + maxLength, children, isRequired, // we pull this out as it conflicts with other form validation libraries ...props @@ -51,10 +54,41 @@ export const TextField = ({ ref?: React.RefObject; children?: React.ReactNode; }) => { + const isControlled = props.value !== undefined; + const [uncontrolledCount, setUncontrolledCount] = useState( + () => (props.defaultValue ?? '').length, + ); + const charCount = isControlled + ? (props.value?.length ?? 0) + : uncontrolledCount; + + const isInvalid = !!errorMessage && errorMessage.length > 0; + + const handleChange = (value: string) => { + if (!isControlled) { + setUncontrolledCount(value.length); + } + props.onChange?.(value); + }; + + const counterElement = maxLength != null && ( + + {charCount}/{maxLength} + + ); + return ( 0} + onChange={handleChange} + maxLength={maxLength} + isInvalid={isInvalid} className={composeTailwindRenderProps( props.className, 'group flex flex-col gap-1', @@ -94,12 +128,24 @@ export const TextField = ({ {children} - {description && ( - - {description} - + {description ? ( +
+ + {description} + + {counterElement} +
+ ) : ( + counterElement && ( +
+ {errorMessage} + {counterElement} +
+ ) + )} + {(description || !counterElement) && ( + {errorMessage} )} - {errorMessage}
); }; diff --git a/packages/ui/stories/TextField.stories.tsx b/packages/ui/stories/TextField.stories.tsx index 0a7478293..042e921d0 100644 --- a/packages/ui/stories/TextField.stories.tsx +++ b/packages/ui/stories/TextField.stories.tsx @@ -60,3 +60,41 @@ export const Validation = (args: any) => ( Validation.args = { isRequired: true, }; + +export const WithCharacterLimit = () => ( +
+ + + + + +
+);