Skip to content

Commit 07f423c

Browse files
committed
feat: add measurement field to text field stories and enhance TextField component
- Introduced a new measurement field in the ControlledTextFieldExample with validation. - Updated TextField component to support prefix and suffix props with improved structure. - Refactored FieldPrefix and FieldSuffix components for better styling and accessibility. - Consolidated story examples into a single export for clarity and maintainability.
1 parent 6167a4f commit 07f423c

2 files changed

Lines changed: 100 additions & 103 deletions

File tree

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

Lines changed: 32 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const formSchema = z.object({
1414
username: z.string().min(3, 'Username must be at least 3 characters'),
1515
price: z.string().min(1, 'Price is required'),
1616
email: z.string().email('Invalid email address'),
17+
measurement: z.string().min(1, 'Measurement is required'),
1718
});
1819

1920
type FormData = z.infer<typeof formSchema>;
@@ -30,6 +31,7 @@ const ControlledTextFieldExample = () => {
3031
username: INITIAL_USERNAME,
3132
price: '10.00',
3233
email: 'user@example.com',
34+
measurement: '10',
3335
},
3436
fetcher,
3537
submitConfig: {
@@ -43,21 +45,21 @@ const ControlledTextFieldExample = () => {
4345
<fetcher.Form onSubmit={methods.handleSubmit}>
4446
<div className="space-y-6">
4547
<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"
48+
49+
<TextField name="price" label="Price" description="Enter the price" prefix="$" />
50+
51+
<TextField name="email" label="Email" description="Enter your email address" suffix="@example.com" />
52+
53+
<TextField
54+
type="number"
55+
name="measurement"
56+
step={0.1}
57+
label="Measurement"
58+
description="Enter a measurement"
59+
prefix="~"
60+
suffix="cm"
5961
/>
60-
62+
6163
<Button type="submit" className="mt-4">
6264
Submit
6365
</Button>
@@ -101,20 +103,7 @@ const meta: Meta<typeof TextField> = {
101103
component: TextField,
102104
parameters: { layout: 'centered' },
103105
tags: ['autodocs'],
104-
decorators: [
105-
withRemixStubDecorator({
106-
root: {
107-
Component: ControlledTextFieldExample,
108-
},
109-
routes: [
110-
{
111-
path: '/username',
112-
action: async ({ request }: ActionFunctionArgs) => handleFormSubmission(request),
113-
},
114-
],
115-
}),
116-
],
117-
} satisfies Meta<typeof TextField>;
106+
};
118107

119108
export default meta;
120109
type Story = StoryObj<typeof meta>;
@@ -168,41 +157,25 @@ const testValidSubmission = async ({ canvas }: StoryContext) => {
168157
expect(successMessage).toBeInTheDocument();
169158
};
170159

171-
// Stories
172-
export const Tests: Story = {
160+
// Single story that contains all variants
161+
export const Examples: Story = {
173162
play: async (storyContext) => {
174163
testDefaultValues(storyContext);
175164
await testInvalidSubmission(storyContext);
176165
await testUsernameTaken(storyContext);
177166
await testValidSubmission(storyContext);
178167
},
168+
decorators: [
169+
withRemixStubDecorator({
170+
root: {
171+
Component: ControlledTextFieldExample,
172+
},
173+
routes: [
174+
{
175+
path: '/username',
176+
action: async ({ request }: ActionFunctionArgs) => handleFormSubmission(request),
177+
},
178+
],
179+
}),
180+
],
179181
};
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-
},
208-
};

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

Lines changed: 68 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
// biome-ignore lint/style/noNamespaceImport: prevents React undefined errors when exporting as a component library
21
import * as React from 'react';
32
import type { Control, FieldPath, FieldValues } from 'react-hook-form';
43
import {
@@ -13,69 +12,94 @@ import {
1312
import { TextInput } from './text-input';
1413
import { cn } from './utils';
1514

16-
export const FieldPrefix = ({ children, className }: { children: React.ReactNode; className?: string }) => {
15+
export const FieldPrefix = ({
16+
children,
17+
className,
18+
}: {
19+
children: React.ReactNode;
20+
className?: string;
21+
}) => {
1722
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>
23+
<div
24+
className={cn(
25+
'flex h-full text-base items-center pl-3 pr-0 text-gray-500 group-focus-within:text-gray-700 transition-colors duration-200 border-y border-l border-input rounded-l-md bg-background',
26+
className,
27+
)}
28+
>
29+
<span className="whitespace-nowrap">{children}</span>
30+
</div>
2131
);
2232
};
2333

24-
export const FieldSuffix = ({ children, className }: { children: React.ReactNode; className?: string }) => {
34+
export const FieldSuffix = ({
35+
children,
36+
className,
37+
}: {
38+
children: React.ReactNode;
39+
className?: string;
40+
}) => {
2541
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>
42+
<div
43+
className={cn(
44+
'flex h-full text-base items-center pr-3 pl-0 text-gray-500 group-focus-within:text-gray-700 transition-colors duration-200 border-y border-r border-input rounded-r-md bg-background',
45+
className,
46+
)}
47+
>
48+
<span className="whitespace-nowrap">{children}</span>
49+
</div>
2950
);
3051
};
3152

32-
export interface TextFieldProps<
33-
TFieldValues extends FieldValues = FieldValues,
34-
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
35-
> extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'name'> {
36-
control?: Control<TFieldValues>;
37-
name: TName;
53+
// Create a specific interface for the input props that includes className explicitly
54+
type TextInputProps = React.ComponentPropsWithRef<typeof TextInput> & {
55+
control?: Control<FieldValues>;
56+
name: FieldPath<FieldValues>;
3857
label?: string;
3958
description?: string;
4059
components?: Partial<FieldComponents>;
4160
prefix?: React.ReactNode;
4261
suffix?: React.ReactNode;
43-
}
62+
className?: string;
63+
};
4464

45-
export const TextField = React.forwardRef<HTMLDivElement, TextFieldProps>(
65+
export const TextField = React.forwardRef<HTMLDivElement, TextInputProps>(
4666
({ control, name, label, description, className, components, prefix, suffix, ...props }, ref) => {
4767
return (
4868
<FormField
4969
control={control}
5070
name={name}
51-
render={({ field, fieldState }) => (
52-
<FormItem className={className} ref={ref}>
53-
{label && <FormLabel Component={components?.FormLabel}>{label}</FormLabel>}
54-
<FormControl Component={components?.FormControl}>
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,
71+
render={({ field, fieldState }) => {
72+
return (
73+
<FormItem className={className} ref={ref}>
74+
{label && <FormLabel Component={components?.FormLabel}>{label}</FormLabel>}
75+
<FormControl Component={components?.FormControl}>
76+
<div
77+
className={cn('flex group transition-all duration-200 rounded-md', {
78+
'field__input--with-prefix': prefix,
79+
'field__input--with-suffix': suffix,
80+
'focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 focus-within:ring-offset-background': true,
6881
})}
69-
/>
70-
{suffix && <FieldSuffix>{suffix}</FieldSuffix>}
71-
</div>
72-
</FormControl>
73-
{description && <FormDescription Component={components?.FormDescription}>{description}</FormDescription>}
74-
{fieldState.error && (
75-
<FormMessage Component={components?.FormMessage}>{fieldState.error.message}</FormMessage>
76-
)}
77-
</FormItem>
78-
)}
82+
>
83+
{prefix && <FieldPrefix>{prefix}</FieldPrefix>}
84+
<TextInput
85+
{...field}
86+
{...props}
87+
ref={field.ref}
88+
className={cn('focus-visible:ring-0 focus-visible:ring-offset-0', {
89+
'rounded-l-none border-l-0': prefix,
90+
'rounded-r-none border-r-0': suffix,
91+
})}
92+
/>
93+
{suffix && <FieldSuffix>{suffix}</FieldSuffix>}
94+
</div>
95+
</FormControl>
96+
{description && <FormDescription Component={components?.FormDescription}>{description}</FormDescription>}
97+
{fieldState.error && (
98+
<FormMessage Component={components?.FormMessage}>{fieldState.error.message}</FormMessage>
99+
)}
100+
</FormItem>
101+
);
102+
}}
79103
/>
80104
);
81105
},

0 commit comments

Comments
 (0)