diff --git a/apps/docs/src/remix-hook-form/text-field.stories.tsx b/apps/docs/src/remix-hook-form/text-field.stories.tsx index 52cfa183..512d1f25 100644 --- a/apps/docs/src/remix-hook-form/text-field.stories.tsx +++ b/apps/docs/src/remix-hook-form/text-field.stories.tsx @@ -12,6 +12,9 @@ import { withRemixStubDecorator } from '../lib/storybook/remix-stub'; const formSchema = z.object({ username: z.string().min(3, 'Username must be at least 3 characters'), + price: z.string().min(1, 'Price is required'), + email: z.string().email('Invalid email address'), + measurement: z.string().min(1, 'Measurement is required'), }); type FormData = z.infer; @@ -26,6 +29,9 @@ const ControlledTextFieldExample = () => { resolver: zodResolver(formSchema), defaultValues: { username: INITIAL_USERNAME, + price: '10.00', + email: 'user@example.com', + measurement: '10', }, fetcher, submitConfig: { @@ -37,11 +43,28 @@ const ControlledTextFieldExample = () => { return ( - - - {fetcher.data?.message &&

{fetcher.data.message}

} +
+ + + + + + + + + + {fetcher.data?.message &&

{fetcher.data.message}

} +
); @@ -80,20 +103,7 @@ const meta: Meta = { component: TextField, parameters: { layout: 'centered' }, tags: ['autodocs'], - decorators: [ - withRemixStubDecorator({ - root: { - Component: ControlledTextFieldExample, - }, - routes: [ - { - path: '/username', - action: async ({ request }: ActionFunctionArgs) => handleFormSubmission(request), - }, - ], - }), - ], -} satisfies Meta; +}; export default meta; type Story = StoryObj; @@ -147,12 +157,25 @@ const testValidSubmission = async ({ canvas }: StoryContext) => { expect(successMessage).toBeInTheDocument(); }; -// Stories -export const Tests: Story = { +// Single story that contains all variants +export const Examples: Story = { play: async (storyContext) => { testDefaultValues(storyContext); await testInvalidSubmission(storyContext); await testUsernameTaken(storyContext); await testValidSubmission(storyContext); }, -}; \ No newline at end of file + decorators: [ + withRemixStubDecorator({ + root: { + Component: ControlledTextFieldExample, + }, + routes: [ + { + path: '/username', + action: async ({ request }: ActionFunctionArgs) => handleFormSubmission(request), + }, + ], + }), + ], +}; diff --git a/packages/components/src/ui/text-field.tsx b/packages/components/src/ui/text-field.tsx index cdab1df2..73a42d08 100644 --- a/packages/components/src/ui/text-field.tsx +++ b/packages/components/src/ui/text-field.tsx @@ -10,42 +10,96 @@ import { FormMessage, } from './form'; import { TextInput } from './text-input'; +import { cn } from './utils'; -export interface TextFieldComponents extends FieldComponents { - Input?: React.ComponentType>; -} +export const FieldPrefix = ({ + children, + className, +}: { + children: React.ReactNode; + className?: string; +}) => { + return ( +
+ {children} +
+ ); +}; -export interface TextFieldProps< - TFieldValues extends FieldValues = FieldValues, - TName extends FieldPath = FieldPath, -> extends Omit, 'name'> { - control?: Control; - name: TName; +export const FieldSuffix = ({ + children, + className, +}: { + children: React.ReactNode; + className?: string; +}) => { + return ( +
+ {children} +
+ ); +}; + +// Create a specific interface for the input props that includes className explicitly +type TextInputProps = React.ComponentPropsWithRef & { + control?: Control; + name: FieldPath; label?: string; description?: string; - components?: Partial; -} - -export const TextField = React.forwardRef( - ({ control, name, label, description, className, components, ...props }, ref) => { - const InputComponent = components?.Input || TextInput; + components?: Partial; + prefix?: React.ReactNode; + suffix?: React.ReactNode; + className?: string; +}; +export const TextField = React.forwardRef( + ({ control, name, label, description, className, components, prefix, suffix, ...props }, ref) => { return ( ( - - {label && {label}} - - - - {description && {description}} - {fieldState.error && ( - {fieldState.error.message} - )} - - )} + render={({ field, fieldState }) => { + return ( + + {label && {label}} + +
+ {prefix && {prefix}} + + {suffix && {suffix}} +
+
+ {description && {description}} + {fieldState.error && ( + {fieldState.error.message} + )} +
+ ); + }} /> ); },