Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/docs/src/remix-hook-form/checkbox-custom.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { FormLabel, FormMessage } from '@lambdacurry/forms/remix-hook-form/
import { Button } from '@lambdacurry/forms/ui/button';
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
import type { Meta, StoryObj } from '@storybook/react-vite';
import { expect, userEvent, within } from 'storybook/test';
import { expect, userEvent, within } from '@storybook/test';
import type * as React from 'react';
import type { ActionFunctionArgs } from 'react-router';
import { useFetcher } from 'react-router';
Expand Down
10 changes: 4 additions & 6 deletions apps/docs/src/remix-hook-form/checkbox-list.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { Checkbox } from '@lambdacurry/forms/remix-hook-form/checkbox';
import { Button } from '@lambdacurry/forms/ui/button';
import { FormMessage } from '@lambdacurry/forms/ui/form';
import type { Meta, StoryContext, StoryObj } from '@storybook/react-vite';
import { expect, userEvent } from 'storybook/test';
import type {} from '@testing-library/dom';
import { type ActionFunctionArgs, Form, useFetcher } from 'react-router';
import { RemixFormProvider, createFormData, getValidatedFormData, useRemixForm } from 'remix-hook-form';
import type { Meta, StoryObj } from '@storybook/react-vite';
import { expect, userEvent, within } from '@storybook/test';
import { type ActionFunctionArgs, useFetcher } from 'react-router';
import { RemixFormProvider, getValidatedFormData, useRemixForm } from 'remix-hook-form';
import { z } from 'zod';
import { withReactRouterStubDecorator } from '../lib/storybook/react-router-stub';

Expand Down
79 changes: 45 additions & 34 deletions apps/docs/src/remix-hook-form/checkbox.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { Checkbox } from '@lambdacurry/forms/remix-hook-form/checkbox';
import { Button } from '@lambdacurry/forms/ui/button';
import type { Meta, StoryContext, StoryObj } from '@storybook/react-vite';
import { expect, userEvent } from 'storybook/test';
import type { Meta, StoryObj } from '@storybook/react-vite';
import { expect, userEvent, within } from '@storybook/test';
import { type ActionFunctionArgs, useFetcher } from 'react-router';
import { RemixFormProvider, getValidatedFormData, useRemixForm } from 'remix-hook-form';
import { z } from 'zod';
Expand Down Expand Up @@ -84,34 +84,6 @@ const meta: Meta<typeof Checkbox> = {
export default meta;
type Story = StoryObj<typeof meta>;

const testDefaultValues = ({ canvas }: StoryContext) => {
const termsCheckbox = canvas.getByLabelText('Accept terms and conditions');
const marketingCheckbox = canvas.getByLabelText('Receive marketing emails');
const requiredCheckbox = canvas.getByLabelText('This is a required checkbox');
expect(termsCheckbox).not.toBeChecked();
expect(marketingCheckbox).not.toBeChecked();
expect(requiredCheckbox).not.toBeChecked();
};

const testInvalidSubmission = async ({ canvas }: StoryContext) => {
const submitButton = canvas.getByRole('button', { name: 'Submit' });
await userEvent.click(submitButton);
await expect(await canvas.findByText('You must accept the terms and conditions')).toBeInTheDocument();
await expect(await canvas.findByText('This field is required')).toBeInTheDocument();
};

const testValidSubmission = async ({ canvas }: StoryContext) => {
const termsCheckbox = canvas.getByLabelText('Accept terms and conditions');
const requiredCheckbox = canvas.getByLabelText('This is a required checkbox');
await userEvent.click(termsCheckbox);
await userEvent.click(requiredCheckbox);

const submitButton = canvas.getByRole('button', { name: 'Submit' });
await userEvent.click(submitButton);

await expect(await canvas.findByText('Form submitted successfully')).toBeInTheDocument();
};

export const Default: Story = {
parameters: {
docs: {
Expand Down Expand Up @@ -161,9 +133,48 @@ const ControlledCheckboxExample = () => {
},
},
},
play: async (storyContext) => {
testDefaultValues(storyContext);
await testInvalidSubmission(storyContext);
await testValidSubmission(storyContext);
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);

await step('Verify initial state', async () => {
// Verify all checkboxes are unchecked initially
const termsCheckbox = canvas.getByLabelText('Accept terms and conditions');
const marketingCheckbox = canvas.getByLabelText('Receive marketing emails');
const requiredCheckbox = canvas.getByLabelText('This is a required checkbox');

expect(termsCheckbox).not.toBeChecked();
expect(marketingCheckbox).not.toBeChecked();
expect(requiredCheckbox).not.toBeChecked();

// Verify submit button is present
const submitButton = canvas.getByRole('button', { name: 'Submit' });
expect(submitButton).toBeInTheDocument();
});

await step('Test validation errors on invalid submission', async () => {
// Submit form without checking required checkboxes
const submitButton = canvas.getByRole('button', { name: 'Submit' });
await userEvent.click(submitButton);

// Verify validation error messages appear
await expect(canvas.findByText('You must accept the terms and conditions')).resolves.toBeInTheDocument();
await expect(canvas.findByText('This field is required')).resolves.toBeInTheDocument();
});

await step('Test successful form submission', async () => {
// Check required checkboxes
const termsCheckbox = canvas.getByLabelText('Accept terms and conditions');
const requiredCheckbox = canvas.getByLabelText('This is a required checkbox');

await userEvent.click(termsCheckbox);
await userEvent.click(requiredCheckbox);

// Submit form
const submitButton = canvas.getByRole('button', { name: 'Submit' });
await userEvent.click(submitButton);

// Verify success message
await expect(canvas.findByText('Form submitted successfully')).resolves.toBeInTheDocument();
});
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { DropdownMenuSelect } from '@lambdacurry/forms/remix-hook-form/dropdown-
import { Button } from '@lambdacurry/forms/ui/button';
import { DropdownMenuSelectItem } from '@lambdacurry/forms/ui/dropdown-menu-select-field';
import type { Meta, StoryObj } from '@storybook/react-vite';
import { expect, screen, userEvent, within } from 'storybook/test';
import { type ActionFunctionArgs, Form, useFetcher } from 'react-router';
import { expect, screen, userEvent, within } from '@storybook/test';
import { type ActionFunctionArgs, useFetcher } from 'react-router';
import { RemixFormProvider, createFormData, getValidatedFormData, useRemixForm } from 'remix-hook-form';
import { z } from 'zod';
import { withReactRouterStubDecorator } from '../lib/storybook/react-router-stub';
Expand Down
4 changes: 2 additions & 2 deletions apps/docs/src/remix-hook-form/otp-input.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { OTPInput } from '@lambdacurry/forms/remix-hook-form/otp-input';
import { Button } from '@lambdacurry/forms/ui/button';
import type { Meta, StoryObj } from '@storybook/react-vite';
import { expect, userEvent, within } from 'storybook/test';
import { type ActionFunctionArgs, Form, useFetcher } from 'react-router';
import { expect, userEvent, within } from '@storybook/test';
import { type ActionFunctionArgs, useFetcher } from 'react-router';
import { RemixFormProvider, createFormData, getValidatedFormData, useRemixForm } from 'remix-hook-form';
import { z } from 'zod';
import { withReactRouterStubDecorator } from '../lib/storybook/react-router-stub';
Expand Down
13 changes: 6 additions & 7 deletions apps/docs/src/remix-hook-form/radio-group-custom.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { RadioGroup } from '@lambdacurry/forms/remix-hook-form/radio-group';
import { RadioGroup, type RadioOption } from '@lambdacurry/forms/remix-hook-form/radio-group';
import { RadioGroupItem } from '@lambdacurry/forms/remix-hook-form/radio-group-item';
import type { FormLabel, FormMessage } from '@lambdacurry/forms/remix-hook-form/form';
import { Button } from '@lambdacurry/forms/ui/button';
import { FormLabel, FormMessage } from '@lambdacurry/forms/ui/form';
import { cn } from '@lambdacurry/forms/ui/utils';
import { Label } from '@lambdacurry/forms/ui/label';
import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
import type { Meta, StoryObj } from '@storybook/react-vite';
import { expect, userEvent, within } from 'storybook/test';
import type * as React from 'react';
import type { ActionFunctionArgs } from 'react-router';
import { Form, useFetcher } from 'react-router';
import { expect, userEvent, within } from '@storybook/test';
import type { ComponentPropsWithoutRef, ComponentType } from 'react';
import { type ActionFunctionArgs, useFetcher } from 'react-router';
import { RemixFormProvider, getValidatedFormData, useRemixForm } from 'remix-hook-form';
import { z } from 'zod';
import { withReactRouterStubDecorator } from '../lib/storybook/react-router-stub';
Expand Down
11 changes: 4 additions & 7 deletions apps/docs/src/remix-hook-form/radio-group.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { RadioGroup, type RadioOption } from '@lambdacurry/forms/remix-hook-form/radio-group';
import { RadioGroupItem } from '@lambdacurry/forms/remix-hook-form/radio-group-item';
import { RadioGroup } from '@lambdacurry/forms/remix-hook-form/radio-group';
import { Button } from '@lambdacurry/forms/ui/button';
import { Label } from '@lambdacurry/forms/ui/label';
import type { Meta, StoryObj } from '@storybook/react-vite';
import { expect, userEvent, within } from 'storybook/test';
import type { ComponentPropsWithoutRef, ComponentType } from 'react';
import { type ActionFunctionArgs, Form, useFetcher } from 'react-router';
import { RemixFormProvider, getValidatedFormData, useRemixForm } from 'remix-hook-form';
import { expect, userEvent, within } from '@storybook/test';
import { type ActionFunctionArgs, useFetcher } from 'react-router';
import { RemixFormProvider, createFormData, getValidatedFormData, useRemixForm } from 'remix-hook-form';
import { z } from 'zod';
import { withReactRouterStubDecorator } from '../lib/storybook/react-router-stub';

Expand Down
48 changes: 24 additions & 24 deletions apps/docs/src/remix-hook-form/switch-custom.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { Switch } from '@lambdacurry/forms/remix-hook-form/switch';
import { FormLabel, FormMessage } from '@lambdacurry/forms/remix-hook-form/form';
import { Button } from '@lambdacurry/forms/ui/button';
import { FormLabel, FormMessage } from '@lambdacurry/forms/ui/form';
import * as SwitchPrimitives from '@radix-ui/react-switch';
import * as SwitchPrimitive from '@radix-ui/react-switch';
import type { Meta, StoryObj } from '@storybook/react-vite';
import { expect, userEvent, within } from 'storybook/test';
import { expect, userEvent, within } from '@storybook/test';
import type * as React from 'react';
import type { ActionFunctionArgs } from 'react-router';
import { useFetcher } from 'react-router';
import { RemixFormProvider, getValidatedFormData, useRemixForm } from 'remix-hook-form';
import { RemixFormProvider, createFormData, getValidatedFormData, useRemixForm } from 'remix-hook-form';
import { z } from 'zod';
import { withReactRouterStubDecorator } from '../lib/storybook/react-router-stub';

Expand All @@ -21,19 +21,19 @@ const formSchema = z.object({
type FormData = z.infer<typeof formSchema>;

// Custom Switch component
const PurpleSwitch = (props: React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>) => (
<SwitchPrimitives.Root
const PurpleSwitch = (props: React.ComponentPropsWithoutRef<typeof SwitchPrimitive.Root>) => (
<SwitchPrimitive.Root
{...props}
className="peer inline-flex h-8 w-16 shrink-0 cursor-pointer items-center rounded-full border-2 border-purple-300 bg-purple-100 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-purple-500 focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-purple-600 data-[state=unchecked]:bg-purple-200"
>
{props.children}
</SwitchPrimitives.Root>
</SwitchPrimitive.Root>
);
PurpleSwitch.displayName = 'PurpleSwitch';

// Custom Switch Thumb component
const PurpleSwitchThumb = (props: React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Thumb>) => (
<SwitchPrimitives.Thumb
const PurpleSwitchThumb = (props: React.ComponentPropsWithoutRef<typeof SwitchPrimitive.Thumb>) => (
<SwitchPrimitive.Thumb
{...props}
className="pointer-events-none flex h-7 w-7 items-center justify-center rounded-full bg-white shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-8 data-[state=unchecked]:translate-x-0 relative overflow-hidden data-[state=checked]:[--on-opacity:1] data-[state=checked]:[--off-opacity:0] data-[state=unchecked]:[--on-opacity:0] data-[state=unchecked]:[--off-opacity:1]"
>
Expand All @@ -43,7 +43,7 @@ const PurpleSwitchThumb = (props: React.ComponentPropsWithoutRef<typeof SwitchPr
<span className="absolute inset-0 opacity-[var(--off-opacity)] flex items-center justify-center text-xs font-bold text-purple-600 transition-opacity duration-200 z-10">
OFF
</span>
</SwitchPrimitives.Thumb>
</SwitchPrimitive.Thumb>
);
PurpleSwitchThumb.displayName = 'PurpleSwitchThumb';

Expand All @@ -60,19 +60,19 @@ const PurpleMessage = (props: React.ComponentPropsWithoutRef<typeof FormMessage>
PurpleMessage.displayName = 'PurpleMessage';

// Green Switch component
const GreenSwitch = (props: React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>) => (
<SwitchPrimitives.Root
const GreenSwitch = (props: React.ComponentPropsWithoutRef<typeof SwitchPrimitive.Root>) => (
<SwitchPrimitive.Root
{...props}
className="peer inline-flex h-7 w-14 shrink-0 cursor-pointer items-center rounded-full border-2 border-green-300 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-green-500 focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-green-600 data-[state=unchecked]:bg-white"
>
{props.children}
</SwitchPrimitives.Root>
</SwitchPrimitive.Root>
);
GreenSwitch.displayName = 'GreenSwitch';

// Green Switch Thumb component
const GreenSwitchThumb = (props: React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Thumb>) => (
<SwitchPrimitives.Thumb
const GreenSwitchThumb = (props: React.ComponentPropsWithoutRef<typeof SwitchPrimitive.Thumb>) => (
<SwitchPrimitive.Thumb
{...props}
className="pointer-events-none flex h-6 w-6 items-center justify-center rounded-full bg-green-100 shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-7 data-[state=unchecked]:translate-x-0 data-[state=checked]:bg-white"
>
Expand All @@ -86,7 +86,7 @@ const GreenSwitchThumb = (props: React.ComponentPropsWithoutRef<typeof SwitchPri
<title>Check mark</title>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</SwitchPrimitives.Thumb>
</SwitchPrimitive.Thumb>
);
GreenSwitchThumb.displayName = 'GreenSwitchThumb';

Expand Down Expand Up @@ -234,17 +234,17 @@ The \`components\` prop allows you to override any of the internal components us

// APPROACH 2: Custom Switch with purple styling and ON/OFF text
const PurpleSwitch = React.forwardRef((props, ref) => (
<SwitchPrimitives.Root
<SwitchPrimitive.Root
ref={ref}
{...props}
className="peer inline-flex h-8 w-16 shrink-0 cursor-pointer items-center rounded-full border-2 border-purple-300 bg-purple-100 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-purple-500 focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-purple-600 data-[state=unchecked]:bg-purple-200"
>
{props.children}
</SwitchPrimitives.Root>
</SwitchPrimitive.Root>
));

const PurpleSwitchThumb = React.forwardRef((props, ref) => (
<SwitchPrimitives.Thumb
<SwitchPrimitive.Thumb
ref={ref}
{...props}
className="pointer-events-none flex h-7 w-7 items-center justify-center rounded-full bg-white shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-8 data-[state=unchecked]:translate-x-0 relative overflow-hidden data-[state=checked]:[--on-opacity:1] data-[state=checked]:[--off-opacity:0] data-[state=unchecked]:[--on-opacity:0] data-[state=unchecked]:[--off-opacity:1]"
Expand All @@ -255,7 +255,7 @@ const PurpleSwitchThumb = React.forwardRef((props, ref) => (
<span className="absolute inset-0 opacity-[var(--off-opacity)] flex items-center justify-center text-xs font-bold text-purple-600 transition-opacity duration-200 z-10">
OFF
</span>
</SwitchPrimitives.Thumb>
</SwitchPrimitive.Thumb>
));

<Switch
Expand All @@ -270,17 +270,17 @@ const PurpleSwitchThumb = React.forwardRef((props, ref) => (

// APPROACH 3: Fully customized switch with green styling, checkmark icon, and custom form components
const GreenSwitch = React.forwardRef((props, ref) => (
<SwitchPrimitives.Root
<SwitchPrimitive.Root
ref={ref}
{...props}
className="peer inline-flex h-7 w-14 shrink-0 cursor-pointer items-center rounded-full border-2 border-green-300 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-green-500 focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-green-600 data-[state=unchecked]:bg-white"
>
{props.children}
</SwitchPrimitives.Root>
</SwitchPrimitive.Root>
));

const GreenSwitchThumb = React.forwardRef((props, ref) => (
<SwitchPrimitives.Thumb
<SwitchPrimitive.Thumb
ref={ref}
{...props}
className="pointer-events-none flex h-6 w-6 items-center justify-center rounded-full bg-green-100 shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-7 data-[state=unchecked]:translate-x-0 data-[state=checked]:bg-white"
Expand All @@ -294,7 +294,7 @@ const GreenSwitchThumb = React.forwardRef((props, ref) => (
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</SwitchPrimitives.Thumb>
</SwitchPrimitive.Thumb>
));

const PurpleLabel = React.forwardRef((props, ref) => (
Expand Down
Loading