Skip to content

Commit 6167a4f

Browse files
Add prefix and suffix props to TextField component
1 parent 9573a46 commit 6167a4f

2 files changed

Lines changed: 93 additions & 13 deletions

File tree

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

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import { withRemixStubDecorator } from '../lib/storybook/remix-stub';
1212

1313
const formSchema = z.object({
1414
username: z.string().min(3, 'Username must be at least 3 characters'),
15+
price: z.string().min(1, 'Price is required'),
16+
email: z.string().email('Invalid email address'),
1517
});
1618

1719
type FormData = z.infer<typeof formSchema>;
@@ -26,6 +28,8 @@ const ControlledTextFieldExample = () => {
2628
resolver: zodResolver(formSchema),
2729
defaultValues: {
2830
username: INITIAL_USERNAME,
31+
price: '10.00',
32+
email: 'user@example.com',
2933
},
3034
fetcher,
3135
submitConfig: {
@@ -37,11 +41,28 @@ const ControlledTextFieldExample = () => {
3741
return (
3842
<RemixFormProvider {...methods}>
3943
<fetcher.Form onSubmit={methods.handleSubmit}>
40-
<TextField name="username" label="Username" description="Enter a unique username" />
41-
<Button type="submit" className="mt-4">
42-
Submit
43-
</Button>
44-
{fetcher.data?.message && <p className="mt-2 text-green-600">{fetcher.data.message}</p>}
44+
<div className="space-y-6">
45+
<TextField name="username" label="Username" description="Enter a unique username" />
46+
47+
<TextField
48+
name="price"
49+
label="Price"
50+
description="Enter the price"
51+
prefix="$"
52+
/>
53+
54+
<TextField
55+
name="email"
56+
label="Email"
57+
description="Enter your email address"
58+
suffix="@example.com"
59+
/>
60+
61+
<Button type="submit" className="mt-4">
62+
Submit
63+
</Button>
64+
{fetcher.data?.message && <p className="mt-2 text-green-600">{fetcher.data.message}</p>}
65+
</div>
4566
</fetcher.Form>
4667
</RemixFormProvider>
4768
);
@@ -155,4 +176,33 @@ export const Tests: Story = {
155176
await testUsernameTaken(storyContext);
156177
await testValidSubmission(storyContext);
157178
},
179+
};
180+
181+
// Additional stories to showcase prefix and suffix
182+
export const WithPrefix: Story = {
183+
name: 'With Prefix',
184+
args: {
185+
name: 'price',
186+
label: 'Price',
187+
prefix: '$',
188+
},
189+
};
190+
191+
export const WithSuffix: Story = {
192+
name: 'With Suffix',
193+
args: {
194+
name: 'email',
195+
label: 'Email',
196+
suffix: '@example.com',
197+
},
198+
};
199+
200+
export const WithBoth: Story = {
201+
name: 'With Prefix and Suffix',
202+
args: {
203+
name: 'measurement',
204+
label: 'Measurement',
205+
prefix: '~',
206+
suffix: 'cm',
207+
},
158208
};

packages/components/src/ui/text-field.tsx

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// biome-ignore lint/style/noNamespaceImport: prevents React undefined errors when exporting as a component library
12
import * as React from 'react';
23
import type { Control, FieldPath, FieldValues } from 'react-hook-form';
34
import {
@@ -10,10 +11,23 @@ import {
1011
FormMessage,
1112
} from './form';
1213
import { TextInput } from './text-input';
14+
import { cn } from './utils';
1315

14-
export interface TextFieldComponents extends FieldComponents {
15-
Input?: React.ComponentType<React.InputHTMLAttributes<HTMLInputElement>>;
16-
}
16+
export const FieldPrefix = ({ children, className }: { children: React.ReactNode; className?: string }) => {
17+
return (
18+
<span className={cn("whitespace-nowrap shadow-sm font-bold rounded-md text-base flex items-center px-2.5 pr-5 -mr-2.5 bg-gray-50 text-gray-500", className)}>
19+
{children}
20+
</span>
21+
);
22+
};
23+
24+
export const FieldSuffix = ({ children, className }: { children: React.ReactNode; className?: string }) => {
25+
return (
26+
<span className={cn("whitespace-nowrap shadow-sm font-bold rounded-md text-base flex items-center px-2.5 pl-5 -ml-2.5 bg-gray-50 text-gray-500", className)}>
27+
{children}
28+
</span>
29+
);
30+
};
1731

1832
export interface TextFieldProps<
1933
TFieldValues extends FieldValues = FieldValues,
@@ -23,13 +37,13 @@ export interface TextFieldProps<
2337
name: TName;
2438
label?: string;
2539
description?: string;
26-
components?: Partial<TextFieldComponents>;
40+
components?: Partial<FieldComponents>;
41+
prefix?: React.ReactNode;
42+
suffix?: React.ReactNode;
2743
}
2844

2945
export const TextField = React.forwardRef<HTMLDivElement, TextFieldProps>(
30-
({ control, name, label, description, className, components, ...props }, ref) => {
31-
const InputComponent = components?.Input || TextInput;
32-
46+
({ control, name, label, description, className, components, prefix, suffix, ...props }, ref) => {
3347
return (
3448
<FormField
3549
control={control}
@@ -38,7 +52,23 @@ export const TextField = React.forwardRef<HTMLDivElement, TextFieldProps>(
3852
<FormItem className={className} ref={ref}>
3953
{label && <FormLabel Component={components?.FormLabel}>{label}</FormLabel>}
4054
<FormControl Component={components?.FormControl}>
41-
<InputComponent {...field} {...props} ref={field.ref} />
55+
<div className={cn("flex items-stretch relative", {
56+
"field__input--with-prefix": prefix,
57+
"field__input--with-suffix": suffix,
58+
})}>
59+
{prefix && <FieldPrefix>{prefix}</FieldPrefix>}
60+
<TextInput
61+
{...field}
62+
{...props}
63+
ref={field.ref}
64+
className={cn(props.className, {
65+
"z-10": prefix || suffix,
66+
"rounded-l-none": prefix,
67+
"rounded-r-none": suffix,
68+
})}
69+
/>
70+
{suffix && <FieldSuffix>{suffix}</FieldSuffix>}
71+
</div>
4272
</FormControl>
4373
{description && <FormDescription Component={components?.FormDescription}>{description}</FormDescription>}
4474
{fieldState.error && (

0 commit comments

Comments
 (0)