diff --git a/apps/docs/src/remix-hook-form/scroll-to-error.stories.tsx b/apps/docs/src/remix-hook-form/scroll-to-error.stories.tsx new file mode 100644 index 00000000..5cf028ae --- /dev/null +++ b/apps/docs/src/remix-hook-form/scroll-to-error.stories.tsx @@ -0,0 +1,642 @@ +import { zodResolver } from '@hookform/resolvers/zod'; +import { Checkbox } from '@lambdacurry/forms/remix-hook-form/checkbox'; +import { ScrollToErrorOnSubmit } from '@lambdacurry/forms/remix-hook-form/components/ScrollToErrorOnSubmit'; +import { useScrollToErrorOnSubmit } from '@lambdacurry/forms/remix-hook-form/hooks/useScrollToErrorOnSubmit'; +import { RadioGroup } from '@lambdacurry/forms/remix-hook-form/radio-group'; +import { RadioGroupItem } from '@lambdacurry/forms/remix-hook-form/radio-group-item'; +import { Select } from '@lambdacurry/forms/remix-hook-form/select'; +import { TextField } from '@lambdacurry/forms/remix-hook-form/text-field'; +import { Textarea } from '@lambdacurry/forms/remix-hook-form/textarea'; +import { Button } from '@lambdacurry/forms/ui/button'; +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { type ActionFunctionArgs, useFetcher } from 'react-router'; +import { RemixFormProvider, getValidatedFormData, useRemixForm, useRemixFormContext } from 'remix-hook-form'; +import { z } from 'zod'; +import { withReactRouterStubDecorator } from '../lib/storybook/react-router-stub'; + +const formSchema = z.object({ + // Personal Information Section + firstName: z.string().min(2, 'First name must be at least 2 characters'), + lastName: z.string().min(2, 'Last name must be at least 2 characters'), + email: z.string().email('Please enter a valid email address'), + phone: z.string().min(10, 'Phone number must be at least 10 digits'), + + // Address Section + address: z.string().min(5, 'Address must be at least 5 characters'), + city: z.string().min(2, 'City must be at least 2 characters'), + state: z.string().min(1, 'Please select a state'), + zipCode: z.string().min(5, 'ZIP code must be at least 5 characters'), + + // Preferences Section + newsletter: z.boolean(), + notifications: z.enum(['email', 'sms', 'none'], { + errorMap: () => ({ message: 'Please select a notification preference' }), + }), + + // Additional Information + company: z.string().min(2, 'Company name must be at least 2 characters'), + jobTitle: z.string().min(2, 'Job title must be at least 2 characters'), + experience: z.string().min(1, 'Please select your experience level'), + bio: z.string().min(10, 'Bio must be at least 10 characters'), + + // Terms and Conditions + terms: z.boolean().refine((val) => val === true, { + message: 'You must accept the terms and conditions', + }), + privacy: z.boolean().refine((val) => val === true, { + message: 'You must accept the privacy policy', + }), +}); + +type FormData = z.infer; + +// Component that uses the hook inside the form provider +const ScrollToErrorHookForm = () => { + // Use the scroll-to-error hook with custom options + useScrollToErrorOnSubmit({ + offset: 100, // Account for fixed header + behavior: 'smooth', + shouldFocus: true, + scrollOnServerErrors: true, + scrollOnMount: true, + retryAttempts: 3, + delay: 150, + }); + + const methods = useRemixFormContext(); + const { handleSubmit } = methods; + const fetcher = useFetcher<{ message: string; errors?: Record }>(); + + return ( + + {/* Personal Information Section */} +
+

Personal Information

+
+ + +
+
+ + +
+
+ + {/* Address Section */} +
+

Address Information

+
+ +
+ + +