Skip to content

Commit 8d57a6c

Browse files
committed
feat: refine IconInput component in text field stories for improved accessibility and structure
- Simplified IconInput implementation by directly integrating the icon within the component. - Enhanced CustomTextFieldExample to utilize the updated IconInput for better clarity and maintainability. - Streamlined Storybook tests by removing unnecessary waitFor calls, ensuring efficient input handling. - Updated documentation to reflect changes in IconInput structure and usage.
1 parent b184404 commit 8d57a6c

1 file changed

Lines changed: 103 additions & 60 deletions

File tree

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

Lines changed: 103 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { FormLabel, FormMessage } from '@lambdacurry/forms/ui/form';
55
import type { ActionFunctionArgs } from '@remix-run/node';
66
import { useFetcher } from '@remix-run/react';
77
import type { Meta, StoryObj } from '@storybook/react';
8-
import { expect, userEvent, waitFor, within } from '@storybook/test';
8+
import { expect, userEvent, within } from '@storybook/test';
99
import * as React from 'react';
1010
import { RemixFormProvider, getValidatedFormData, useRemixForm } from 'remix-hook-form';
1111
import { z } from 'zod';
@@ -44,19 +44,27 @@ const PurpleMessage = React.forwardRef<HTMLParagraphElement, React.ComponentProp
4444
PurpleMessage.displayName = 'PurpleMessage';
4545

4646
// Custom Input with icon
47-
const IconInput = React.forwardRef<
48-
HTMLInputElement,
49-
React.InputHTMLAttributes<HTMLInputElement> & { icon?: React.ReactNode }
50-
>(({ icon, ...props }, ref) => (
51-
<div className="relative">
52-
{icon && <div className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500">{icon}</div>}
53-
<input
54-
ref={ref}
55-
{...props}
56-
className="w-full rounded-md border border-gray-300 py-2 pl-10 pr-3 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
57-
/>
58-
</div>
59-
));
47+
const IconInput = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>(
48+
({ ...props }, ref) => (
49+
<div className="relative">
50+
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500">
51+
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
52+
<title>Lock</title>
53+
<path
54+
fillRule="evenodd"
55+
d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z"
56+
clipRule="evenodd"
57+
/>
58+
</svg>
59+
</div>
60+
<input
61+
ref={ref}
62+
{...props}
63+
className="w-full rounded-md border border-gray-300 py-2 pl-10 pr-3 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
64+
/>
65+
</div>
66+
),
67+
);
6068
IconInput.displayName = 'IconInput';
6169

6270
const CustomTextFieldExample = () => {
@@ -101,25 +109,7 @@ const CustomTextFieldExample = () => {
101109
type="password"
102110
placeholder="Enter your password"
103111
components={{
104-
Input: React.forwardRef<
105-
HTMLInputElement,
106-
React.InputHTMLAttributes<HTMLInputElement> & { icon?: React.ReactNode }
107-
>((props, ref) => (
108-
<IconInput
109-
{...props}
110-
ref={ref}
111-
icon={
112-
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
113-
<title>Lock</title>
114-
<path
115-
fillRule="evenodd"
116-
d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z"
117-
clipRule="evenodd"
118-
/>
119-
</svg>
120-
}
121-
/>
122-
)),
112+
Input: IconInput,
123113
}}
124114
/>
125115
</div>
@@ -167,11 +157,24 @@ export const CustomComponents: Story = {
167157
story: `
168158
### TextField Component Customization
169159
170-
This example demonstrates three different approaches to customizing the TextField component:
160+
This example demonstrates three different approaches to customizing the TextField component with complete control over styling and behavior.
161+
162+
#### 1. Default Styling
171163
172-
1. **Default Styling**: The first text field uses the default styling with no customization needed.
164+
The first text field uses the default styling with no customization needed:
165+
166+
\`\`\`tsx
167+
<TextField
168+
name="username"
169+
label="Username"
170+
placeholder="Enter your username"
171+
/>
172+
\`\`\`
173+
174+
#### 2. Custom Styling with Purple Theme
175+
176+
The second text field customizes the Input, FormLabel, and FormMessage components with purple styling:
173177
174-
2. **Custom Styling**: The second text field customizes the Input, FormLabel, and FormMessage components with purple styling.
175178
\`\`\`tsx
176179
<TextField
177180
name="email"
@@ -185,26 +188,80 @@ This example demonstrates three different approaches to customizing the TextFiel
185188
/>
186189
\`\`\`
187190
188-
3. **Icon Input**: The third text field demonstrates how to create a custom input with an icon. When using inline components, you must use React.forwardRef.
191+
Where the custom components are defined as:
192+
193+
\`\`\`tsx
194+
// Custom Input component
195+
const PurpleInput = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>((props, ref) => (
196+
<input
197+
ref={ref}
198+
{...props}
199+
className="w-full rounded-lg border-2 border-purple-300 bg-purple-50 px-4 py-2 text-purple-900 placeholder:text-purple-400 focus:border-purple-500 focus:outline-none focus:ring-2 focus:ring-purple-500/50"
200+
/>
201+
));
202+
203+
// Custom Form Label component
204+
const PurpleLabel = React.forwardRef<HTMLLabelElement, React.ComponentPropsWithoutRef<typeof FormLabel>>(
205+
({ className, ...props }, ref) => <FormLabel ref={ref} className="text-lg font-bold text-purple-700" {...props} />,
206+
);
207+
208+
// Custom Form Message component
209+
const PurpleMessage = React.forwardRef<HTMLParagraphElement, React.ComponentPropsWithoutRef<typeof FormMessage>>(
210+
({ className, ...props }, ref) => (
211+
<FormMessage ref={ref} className="text-purple-500 bg-purple-50 p-2 rounded-md mt-1" {...props} />
212+
),
213+
);
214+
\`\`\`
215+
216+
#### 3. Icon Input
217+
218+
The third text field demonstrates how to create a custom input with an icon:
219+
189220
\`\`\`tsx
190221
<TextField
191222
name="password"
192223
label="Password"
193224
type="password"
194225
placeholder="Enter your password"
195226
components={{
196-
Input: React.forwardRef((props, ref) => (
197-
<IconInput
198-
{...props}
199-
ref={ref}
200-
icon={<LockIcon />}
201-
/>
202-
)),
227+
Input: IconInput,
203228
}}
204229
/>
205230
\`\`\`
206231
207-
The \`components\` prop allows you to override any of the internal components used by the TextField component, giving you complete control over the styling and behavior.
232+
With the IconInput component defined as:
233+
234+
\`\`\`tsx
235+
const IconInput = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>(
236+
({ ...props }, ref) => (
237+
<div className="relative">
238+
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500">
239+
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
240+
<title>Lock</title>
241+
<path
242+
fillRule="evenodd"
243+
d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z"
244+
clipRule="evenodd"
245+
/>
246+
</svg>
247+
</div>
248+
<input
249+
ref={ref}
250+
{...props}
251+
className="w-full rounded-md border border-gray-300 py-2 pl-10 pr-3 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
252+
/>
253+
</div>
254+
),
255+
);
256+
\`\`\`
257+
258+
### Key Points
259+
260+
- Always use React.forwardRef when creating custom components
261+
- Make sure to spread the props to pass all necessary attributes
262+
- Include the ref to maintain form functionality
263+
- Add a displayName to your component for better debugging
264+
- The components prop accepts replacements for Input, FormLabel, FormMessage, and FormDescription
208265
`,
209266
},
210267
},
@@ -220,21 +277,7 @@ The \`components\` prop allows you to override any of the internal components us
220277
// Type values
221278
await userEvent.type(usernameInput, 'johndoe');
222279
await userEvent.type(emailInput, 'john@example.com');
223-
emailInput.blur();
224-
await waitFor(
225-
() => {
226-
passwordInput.focus();
227-
},
228-
{ timeout: 1000 },
229-
);
230-
231-
userEvent.type(passwordInput, 'password123');
232-
await waitFor(
233-
() => {
234-
passwordInput.blur();
235-
},
236-
{ timeout: 1000 },
237-
);
280+
await userEvent.type(passwordInput, 'password123');
238281

239282
// Submit the form
240283
const submitButton = canvas.getByRole('button', { name: 'Submit' });

0 commit comments

Comments
 (0)