Skip to content

Commit 4cb0966

Browse files
feat: Add comprehensive ControlledTextArea Storybook stories
- Add BasicUsage story with simple textarea and react-hook-form integration - Add CharacterLimits story with character count validation and display - Add RequiredFieldValidation story with error states and custom messages - Add AutoResizeFunctionality story with dynamic height adjustment - Add ValidationErrorStates story with various error scenarios - Add ComprehensiveExample story showing multiple textareas in a form - Follow established pattern from ControlledInput.stories.tsx - Use react-hook-form directly with FormProvider and useForm - Include proper validation with zod schemas - Demonstrate real-world usage patterns Addresses LC-245: Create ControlledTextArea Storybook Stories
1 parent b446142 commit 4cb0966

1 file changed

Lines changed: 374 additions & 0 deletions

File tree

Lines changed: 374 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,374 @@
1+
import { ControlledTextArea } from '@lambdacurry/medusa-forms/controlled/ControlledTextArea';
2+
import type { Meta, StoryObj } from '@storybook/react-vite';
3+
import { FormProvider, useForm } from 'react-hook-form';
4+
import { z } from 'zod';
5+
import { zodResolver } from '@hookform/resolvers/zod';
6+
7+
const meta = {
8+
title: 'Medusa Forms/Controlled Text Area',
9+
component: ControlledTextArea,
10+
parameters: {
11+
layout: 'centered',
12+
},
13+
tags: ['autodocs'],
14+
} satisfies Meta<typeof ControlledTextArea>;
15+
16+
export default meta;
17+
type Story = StoryObj<typeof meta>;
18+
19+
// Basic Usage Story
20+
const BasicUsageForm = () => {
21+
const form = useForm({
22+
defaultValues: {
23+
description: '',
24+
},
25+
});
26+
27+
return (
28+
<FormProvider {...form}>
29+
<div className="w-[400px]">
30+
<ControlledTextArea
31+
name="description"
32+
label="Description"
33+
placeholder="Enter your description here..."
34+
rows={4}
35+
/>
36+
</div>
37+
</FormProvider>
38+
);
39+
};
40+
41+
export const BasicUsage: Story = {
42+
render: () => <BasicUsageForm />,
43+
parameters: {
44+
docs: {
45+
description: {
46+
story: 'A basic textarea with react-hook-form integration for multi-line text input.',
47+
},
48+
},
49+
},
50+
};
51+
52+
// Character Limits Story
53+
const CharacterLimitsSchema = z.object({
54+
bio: z.string().max(150, 'Bio must be 150 characters or less'),
55+
});
56+
57+
const CharacterLimitsForm = () => {
58+
const form = useForm({
59+
resolver: zodResolver(CharacterLimitsSchema),
60+
defaultValues: {
61+
bio: '',
62+
},
63+
});
64+
65+
const bioValue = form.watch('bio');
66+
const characterCount = bioValue?.length || 0;
67+
const maxLength = 150;
68+
69+
return (
70+
<FormProvider {...form}>
71+
<div className="w-[400px] space-y-2">
72+
<ControlledTextArea
73+
name="bio"
74+
label="Bio"
75+
placeholder="Tell us about yourself..."
76+
rows={4}
77+
maxLength={maxLength}
78+
/>
79+
<div className="text-sm text-gray-500 text-right">
80+
{characterCount}/{maxLength} characters
81+
</div>
82+
{form.formState.errors.bio && (
83+
<p className="text-sm text-red-600">{form.formState.errors.bio.message}</p>
84+
)}
85+
</div>
86+
</FormProvider>
87+
);
88+
};
89+
90+
export const CharacterLimits: Story = {
91+
render: () => <CharacterLimitsForm />,
92+
parameters: {
93+
docs: {
94+
description: {
95+
story: 'Textarea with character count validation, counter display, and limit enforcement.',
96+
},
97+
},
98+
},
99+
};
100+
101+
// Required Field Validation Story
102+
const RequiredFieldSchema = z.object({
103+
feedback: z.string().min(1, 'Feedback is required').min(10, 'Feedback must be at least 10 characters'),
104+
});
105+
106+
const RequiredFieldForm = () => {
107+
const form = useForm({
108+
resolver: zodResolver(RequiredFieldSchema),
109+
defaultValues: {
110+
feedback: '',
111+
},
112+
});
113+
114+
const onSubmit = (data: any) => {
115+
console.log('Form submitted:', data);
116+
};
117+
118+
return (
119+
<FormProvider {...form}>
120+
<form onSubmit={form.handleSubmit(onSubmit)} className="w-[400px] space-y-4">
121+
<ControlledTextArea
122+
name="feedback"
123+
label="Feedback *"
124+
placeholder="Please provide your feedback..."
125+
rows={5}
126+
/>
127+
{form.formState.errors.feedback && (
128+
<p className="text-sm text-red-600">{form.formState.errors.feedback.message}</p>
129+
)}
130+
<button
131+
type="submit"
132+
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
133+
>
134+
Submit Feedback
135+
</button>
136+
</form>
137+
</FormProvider>
138+
);
139+
};
140+
141+
export const RequiredFieldValidation: Story = {
142+
render: () => <RequiredFieldForm />,
143+
parameters: {
144+
docs: {
145+
description: {
146+
story: 'Required field validation with error state display and custom validation messages.',
147+
},
148+
},
149+
},
150+
};
151+
152+
// Auto-resize Functionality Story
153+
const AutoResizeForm = () => {
154+
const form = useForm({
155+
defaultValues: {
156+
content: '',
157+
},
158+
});
159+
160+
return (
161+
<FormProvider {...form}>
162+
<div className="w-[400px] space-y-4">
163+
<ControlledTextArea
164+
name="content"
165+
label="Auto-resize Content"
166+
placeholder="Start typing and watch the textarea grow..."
167+
rows={2}
168+
style={{
169+
minHeight: '60px',
170+
maxHeight: '200px',
171+
resize: 'none',
172+
overflow: 'hidden'
173+
}}
174+
onInput={(e) => {
175+
const target = e.target as HTMLTextAreaElement;
176+
target.style.height = 'auto';
177+
target.style.height = Math.min(target.scrollHeight, 200) + 'px';
178+
}}
179+
/>
180+
<div className="text-sm text-gray-500">
181+
This textarea automatically adjusts its height based on content (min: 60px, max: 200px)
182+
</div>
183+
</div>
184+
</FormProvider>
185+
);
186+
};
187+
188+
export const AutoResizeFunctionality: Story = {
189+
render: () => <AutoResizeForm />,
190+
parameters: {
191+
docs: {
192+
description: {
193+
story: 'Dynamic height adjustment with content-based resizing and min/max height constraints.',
194+
},
195+
},
196+
},
197+
};
198+
199+
// Validation Error States Story
200+
const ValidationErrorSchema = z.object({
201+
message: z.string()
202+
.min(1, 'Message is required')
203+
.min(20, 'Message must be at least 20 characters')
204+
.max(500, 'Message must be less than 500 characters')
205+
.refine((val) => !val.includes('spam'), 'Message cannot contain spam'),
206+
});
207+
208+
const ValidationErrorForm = () => {
209+
const form = useForm({
210+
resolver: zodResolver(ValidationErrorSchema),
211+
defaultValues: {
212+
message: '',
213+
},
214+
mode: 'onChange', // Validate on change for immediate feedback
215+
});
216+
217+
const onSubmit = (data: any) => {
218+
console.log('Form submitted:', data);
219+
};
220+
221+
const hasError = !!form.formState.errors.message;
222+
const messageValue = form.watch('message');
223+
224+
return (
225+
<FormProvider {...form}>
226+
<form onSubmit={form.handleSubmit(onSubmit)} className="w-[400px] space-y-4">
227+
<div className="space-y-2">
228+
<ControlledTextArea
229+
name="message"
230+
label="Message"
231+
placeholder="Enter your message (20-500 characters, no spam)..."
232+
rows={6}
233+
className={hasError ? 'border-red-500 focus:border-red-500' : ''}
234+
/>
235+
<div className="flex justify-between text-sm">
236+
<div>
237+
{form.formState.errors.message && (
238+
<span className="text-red-600">{form.formState.errors.message.message}</span>
239+
)}
240+
</div>
241+
<div className="text-gray-500">
242+
{messageValue?.length || 0}/500
243+
</div>
244+
</div>
245+
</div>
246+
247+
<div className="space-y-2 text-sm text-gray-600">
248+
<p><strong>Validation Rules:</strong></p>
249+
<ul className="list-disc list-inside space-y-1">
250+
<li>Required field</li>
251+
<li>Minimum 20 characters</li>
252+
<li>Maximum 500 characters</li>
253+
<li>Cannot contain the word "spam"</li>
254+
</ul>
255+
</div>
256+
257+
<button
258+
type="submit"
259+
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:bg-gray-400"
260+
disabled={!form.formState.isValid}
261+
>
262+
Submit Message
263+
</button>
264+
</form>
265+
</FormProvider>
266+
);
267+
};
268+
269+
export const ValidationErrorStates: Story = {
270+
render: () => <ValidationErrorForm />,
271+
parameters: {
272+
docs: {
273+
description: {
274+
story: 'Various error scenarios with error message display and field highlighting.',
275+
},
276+
},
277+
},
278+
};
279+
280+
// Comprehensive Form Example
281+
const ComprehensiveSchema = z.object({
282+
title: z.string().min(1, 'Title is required').max(100, 'Title must be less than 100 characters'),
283+
description: z.string().min(1, 'Description is required').min(50, 'Description must be at least 50 characters'),
284+
notes: z.string().optional(),
285+
});
286+
287+
const ComprehensiveForm = () => {
288+
const form = useForm({
289+
resolver: zodResolver(ComprehensiveSchema),
290+
defaultValues: {
291+
title: '',
292+
description: '',
293+
notes: '',
294+
},
295+
});
296+
297+
const onSubmit = (data: any) => {
298+
console.log('Comprehensive form submitted:', data);
299+
};
300+
301+
return (
302+
<FormProvider {...form}>
303+
<form onSubmit={form.handleSubmit(onSubmit)} className="w-[500px] space-y-6">
304+
<div>
305+
<label className="block text-sm font-medium mb-2">Title *</label>
306+
<input
307+
{...form.register('title')}
308+
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
309+
placeholder="Enter a title..."
310+
/>
311+
{form.formState.errors.title && (
312+
<p className="text-sm text-red-600 mt-1">{form.formState.errors.title.message}</p>
313+
)}
314+
</div>
315+
316+
<div>
317+
<ControlledTextArea
318+
name="description"
319+
label="Description *"
320+
placeholder="Provide a detailed description (minimum 50 characters)..."
321+
rows={4}
322+
/>
323+
{form.formState.errors.description && (
324+
<p className="text-sm text-red-600 mt-1">{form.formState.errors.description.message}</p>
325+
)}
326+
</div>
327+
328+
<div>
329+
<ControlledTextArea
330+
name="notes"
331+
label="Additional Notes (Optional)"
332+
placeholder="Any additional notes or comments..."
333+
rows={3}
334+
/>
335+
</div>
336+
337+
<div className="flex gap-4">
338+
<button
339+
type="submit"
340+
className="px-6 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:bg-gray-400"
341+
disabled={!form.formState.isValid}
342+
>
343+
Submit
344+
</button>
345+
<button
346+
type="button"
347+
onClick={() => form.reset()}
348+
className="px-6 py-2 bg-gray-600 text-white rounded hover:bg-gray-700"
349+
>
350+
Reset
351+
</button>
352+
</div>
353+
354+
{form.formState.isSubmitted && form.formState.isValid && (
355+
<div className="p-4 bg-green-100 border border-green-400 text-green-700 rounded">
356+
Form submitted successfully! Check the console for the data.
357+
</div>
358+
)}
359+
</form>
360+
</FormProvider>
361+
);
362+
};
363+
364+
export const ComprehensiveExample: Story = {
365+
render: () => <ComprehensiveForm />,
366+
parameters: {
367+
docs: {
368+
description: {
369+
story: 'A comprehensive form example showing multiple ControlledTextArea components with different validation rules and states.',
370+
},
371+
},
372+
},
373+
};
374+

0 commit comments

Comments
 (0)