From 9161764993172a04c5fa07ff60246019e41258f9 Mon Sep 17 00:00:00 2001 From: Nour Malaeb Date: Wed, 4 Mar 2026 09:57:16 -0500 Subject: [PATCH 1/3] Add character limit and stories --- packages/ui/src/components/TextField.tsx | 55 ++++++++++++++++++++--- packages/ui/stories/TextField.stories.tsx | 38 ++++++++++++++++ 2 files changed, 87 insertions(+), 6 deletions(-) diff --git a/packages/ui/src/components/TextField.tsx b/packages/ui/src/components/TextField.tsx index 0741a952b..e40f5a722 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,38 @@ export const TextField = ({ ref?: React.RefObject; children?: React.ReactNode; }) => { + const [charCount, setCharCount] = useState( + () => (props.value ?? props.defaultValue ?? '').length, + ); + + const isInvalid = !!errorMessage && errorMessage.length > 0; + + const handleChange = (value: string) => { + setCharCount(value.length); + props.onChange?.(value); + }; + + const nearLimit = maxLength != null && maxLength - charCount <= 5; + + 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 +125,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 = () => ( +
+ + + + + +
+); From e64121612d7ff494688227dcfe874a4d917933d0 Mon Sep 17 00:00:00 2001 From: Nour Malaeb Date: Wed, 4 Mar 2026 10:13:25 -0500 Subject: [PATCH 2/3] Handle character count sync for controlled components --- packages/ui/src/components/TextField.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/components/TextField.tsx b/packages/ui/src/components/TextField.tsx index e40f5a722..a1fdddad9 100644 --- a/packages/ui/src/components/TextField.tsx +++ b/packages/ui/src/components/TextField.tsx @@ -54,14 +54,18 @@ export const TextField = ({ ref?: React.RefObject; children?: React.ReactNode; }) => { - const [charCount, setCharCount] = useState( - () => (props.value ?? props.defaultValue ?? '').length, + 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) => { - setCharCount(value.length); + if (!isControlled) { + setUncontrolledCount(value.length); + } props.onChange?.(value); }; From 7c82b6acc1f2961f17e1a365016f169a68644312 Mon Sep 17 00:00:00 2001 From: Nour Malaeb Date: Wed, 4 Mar 2026 10:39:57 -0500 Subject: [PATCH 3/3] Remove near limit effect --- packages/ui/src/components/TextField.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/ui/src/components/TextField.tsx b/packages/ui/src/components/TextField.tsx index a1fdddad9..7e97a0aae 100644 --- a/packages/ui/src/components/TextField.tsx +++ b/packages/ui/src/components/TextField.tsx @@ -58,7 +58,9 @@ export const TextField = ({ const [uncontrolledCount, setUncontrolledCount] = useState( () => (props.defaultValue ?? '').length, ); - const charCount = isControlled ? (props.value?.length ?? 0) : uncontrolledCount; + const charCount = isControlled + ? (props.value?.length ?? 0) + : uncontrolledCount; const isInvalid = !!errorMessage && errorMessage.length > 0; @@ -69,14 +71,11 @@ export const TextField = ({ props.onChange?.(value); }; - const nearLimit = maxLength != null && maxLength - charCount <= 5; - const counterElement = maxLength != null && (