Skip to content

Commit 45b628c

Browse files
author
Mohsen Ghaemaghami
committed
Enhance TextField component with ref support and update stories
- Refactored TextField to use React.forwardRef for better ref handling. - Added a new example in the storybook to demonstrate the ref functionality with a focus button. - Updated TextInput to also support refs, ensuring consistent behavior across components.
1 parent 879e7e4 commit 45b628c

4 files changed

Lines changed: 67 additions & 60 deletions

File tree

apps/docs/src/remix-hook-form/text-field.stories.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
22
import { TextField } from '@lambdacurry/forms/remix-hook-form/text-field';
33
import { Button } from '@lambdacurry/forms/ui/button';
44
import type { Meta, StoryContext, StoryObj } from '@storybook/react-vite';
5+
import { useRef } from 'react';
56
import { type ActionFunctionArgs, useFetcher } from 'react-router';
67
import { RemixFormProvider, getValidatedFormData, useRemixForm } from 'remix-hook-form';
78
import { expect, userEvent } from 'storybook/test';
@@ -38,6 +39,8 @@ const ControlledTextFieldExample = () => {
3839
},
3940
});
4041

42+
const ref = useRef<HTMLInputElement>(null);
43+
4144
return (
4245
<RemixFormProvider {...methods}>
4346
<fetcher.Form onSubmit={methods.handleSubmit}>
@@ -58,6 +61,12 @@ const ControlledTextFieldExample = () => {
5861
suffix="cm"
5962
/>
6063

64+
<div className="flex gap-2 items-end">
65+
<TextField name="refExample" label="Ref Example" placeholder="Enter something, this has a ref" ref={ref} />
66+
67+
<Button onClick={() => ref.current?.focus()}>Focus the input</Button>
68+
</div>
69+
6170
<Button type="submit" className="mt-4">
6271
Submit
6372
</Button>
Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1+
import * as React from 'react';
12
import { TextField as BaseTextField, type TextInputProps as BaseTextFieldProps } from '../ui/text-field';
23
import { FormControl, FormDescription, FormLabel, FormMessage } from './form';
34

45
import { useRemixFormContext } from 'remix-hook-form';
56

67
export type TextFieldProps = Omit<BaseTextFieldProps, 'control'>;
78

8-
export function TextField(props: TextFieldProps) {
9+
export const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>((props, ref) => {
910
const { control } = useRemixFormContext();
1011

1112
// Merge the provided components with the default form components
@@ -15,11 +16,13 @@ export function TextField(props: TextFieldProps) {
1516
FormDescription,
1617
FormMessage,
1718
};
18-
19+
1920
const components = {
2021
...defaultComponents,
2122
...props.components,
2223
};
2324

24-
return <BaseTextField control={control} components={components} {...props} />;
25-
}
25+
return <BaseTextField ref={ref} control={control} components={components} {...props} />;
26+
});
27+
28+
TextField.displayName = 'RemixTextField';
Lines changed: 46 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type * as React from 'react';
1+
import * as React from 'react';
22
import type { Control, FieldPath, FieldValues } from 'react-hook-form';
33
import {
44
type FieldComponents,
@@ -57,64 +57,57 @@ export interface TextInputProps extends Omit<InputProps, 'prefix' | 'suffix'> {
5757
label?: string;
5858
description?: string;
5959
components?: Partial<FieldComponents> & {
60-
Input?: React.ComponentType<InputProps>;
60+
Input?: React.ComponentType<InputProps & React.RefAttributes<HTMLInputElement>>;
6161
};
6262
prefix?: React.ReactNode;
6363
suffix?: React.ReactNode;
6464
className?: string;
6565
}
6666

67-
export const TextField = ({
68-
control,
69-
name,
70-
label,
71-
description,
72-
className,
73-
components,
74-
prefix,
75-
suffix,
76-
...props
77-
}: TextInputProps) => {
78-
// Use the custom Input component if provided, otherwise use the default TextInput
79-
const InputComponent = components?.Input || TextInput;
67+
export const TextField = React.forwardRef<HTMLInputElement, TextInputProps>(
68+
({ control, name, label, description, className, components, prefix, suffix, ...props }, ref) => {
69+
// Use the custom Input component if provided, otherwise use the default TextInput
70+
const InputComponent = components?.Input || TextInput;
8071

81-
return (
82-
<FormField
83-
control={control}
84-
name={name}
85-
render={({ field, fieldState }) => {
86-
return (
87-
<FormItem className={className}>
88-
{label && <FormLabel Component={components?.FormLabel}>{label}</FormLabel>}
89-
<div
90-
className={cn('flex group transition-all duration-200 rounded-md', {
91-
'field__input--with-prefix': prefix,
92-
'field__input--with-suffix': suffix,
93-
'focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 focus-within:ring-offset-background': true,
94-
})}
95-
>
96-
{prefix && <FieldPrefix>{prefix}</FieldPrefix>}
97-
<FormControl Component={components?.FormControl}>
98-
<InputComponent
99-
{...field}
100-
{...props}
101-
className={cn('focus-visible:ring-0 focus-visible:ring-offset-0 border-input', {
102-
'rounded-l-none border-l-0': prefix,
103-
'rounded-r-none border-r-0': suffix,
104-
})}
105-
/>
106-
</FormControl>
107-
{suffix && <FieldSuffix>{suffix}</FieldSuffix>}
108-
</div>
109-
{description && <FormDescription Component={components?.FormDescription}>{description}</FormDescription>}
110-
{fieldState.error && (
111-
<FormMessage Component={components?.FormMessage}>{fieldState.error.message}</FormMessage>
112-
)}
113-
</FormItem>
114-
);
115-
}}
116-
/>
117-
);
118-
};
72+
return (
73+
<FormField
74+
control={control}
75+
name={name}
76+
render={({ field, fieldState }) => {
77+
return (
78+
<FormItem className={className}>
79+
{label && <FormLabel Component={components?.FormLabel}>{label}</FormLabel>}
80+
<div
81+
className={cn('flex group transition-all duration-200 rounded-md', {
82+
'field__input--with-prefix': prefix,
83+
'field__input--with-suffix': suffix,
84+
'focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 focus-within:ring-offset-background': true,
85+
})}
86+
>
87+
{prefix && <FieldPrefix>{prefix}</FieldPrefix>}
88+
<FormControl Component={components?.FormControl}>
89+
<InputComponent
90+
{...field}
91+
{...props}
92+
ref={ref}
93+
className={cn('focus-visible:ring-0 focus-visible:ring-offset-0 border-input', {
94+
'rounded-l-none border-l-0': prefix,
95+
'rounded-r-none border-r-0': suffix,
96+
})}
97+
/>
98+
</FormControl>
99+
{suffix && <FieldSuffix>{suffix}</FieldSuffix>}
100+
</div>
101+
{description && <FormDescription Component={components?.FormDescription}>{description}</FormDescription>}
102+
{fieldState.error && (
103+
<FormMessage Component={components?.FormMessage}>{fieldState.error.message}</FormMessage>
104+
)}
105+
</FormItem>
106+
);
107+
}}
108+
/>
109+
);
110+
},
111+
);
119112

120113
TextField.displayName = 'TextField';
Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import type * as React from 'react';
1+
import * as React from 'react';
22
import { cn } from './utils';
33

44
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
55

6-
function TextInput({ className, type, ...props }: InputProps) {
6+
const TextInput = React.forwardRef<HTMLInputElement, InputProps>(({ className, type, ...props }, ref) => {
77
return (
88
<input
9+
ref={ref}
910
type={type}
1011
className={cn(
1112
'flex h-10 w-full text-base sm:text-sm rounded-md border border-input bg-background px-3 py-2 ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
@@ -15,6 +16,7 @@ function TextInput({ className, type, ...props }: InputProps) {
1516
{...props}
1617
/>
1718
);
18-
}
19+
});
20+
TextInput.displayName = 'TextInput';
1921

2022
export { TextInput };

0 commit comments

Comments
 (0)