Skip to content
This repository was archived by the owner on Mar 7, 2026. It is now read-only.
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 .cursor/rules/coding.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Always follow the following recipe for implementing tasks:
12. After PR ist submitted, report to the user and wait for manual instructions.
13. The user will then review and merge the PR or ask for updates.
14. Pull the repo again to check if the PR has been merged.
15. After task is completed (PR merged), the task is marked with a checkmark in docs/project_plan.md. This change does not warrant a separate PR, it will be included in the next PR. Just do git add for the file.
15. After task is completed (PR merged), the task is marked with a checkmark in docs/project_plan.md. This change does not warrant a separate PR, it will be included in the next PR. Just do git add for the file. Delete the feature branch locally and remotely.

# Instructions to handle error situations:
- CI Pipeline fails:
Expand Down
4 changes: 2 additions & 2 deletions docs/project_plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ A pragmatic breakdown into **four one‑week sprints** plus a preparatory **Spri

| # | Task | DoD | Status |
| --- | --------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | ------ |
| 2.1 | Add NumberInput, Select, Checkbox, RadioGroup components. | Unit & a11y tests pass; form story displays all. | PR |
| 2.2 | Integrate **React Hook Form + Zod**; create `FormGrid` + `FormGroup`. | Story "Form Example" submits & reports validation errors in Storybook interaction test. | |
| 2.1 | Add NumberInput, Select, Checkbox, RadioGroup components. | Unit & a11y tests pass; form story displays all. | |
| 2.2 | Integrate **React Hook Form + Zod**; create `FormGrid` + `FormGroup`. | Story "Form Example" submits & reports validation errors in Storybook interaction test. | PR |
| 2.3 | Implement Zustand session store skeleton with dark‑mode flag. | Vitest verifies default state + setter actions. | |
| 2.4 | ESLint rule enforcing named `useEffect` & cleanup. | Failing example in test repo triggers lint error; real code passes. | |
| 2.5 | Extend CI to run axe‑core on all stories. | Pipeline fails if any new a11y violations introduced. | |
Expand Down
49 changes: 49 additions & 0 deletions docs/task-planning/task-2.2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Task 2.2 Planning: React Hook Form + Zod Integration

## Overview

This task involves integrating React Hook Form (RHF) and Zod validation into the UI Kit, along with creating layout components for forms: FormGrid and FormGroup. These components will provide a foundation for building complex forms with validation in the UI Kit.

## Task Breakdown

| Task Description | Definition of Done (DoD) | Status |
| -------------------------------------------------- | -------------------------------------------------------------------------------------------- | -------- |
| Set up React Hook Form and Zod dependencies | Dependencies installed and configured | complete |
| Create utility hooks for form integration | Hooks created and unit tested | complete |
| Implement FormGrid component for layout | Component renders correctly and accepts all required props; Unit tests pass; a11y tests pass | complete |
| Implement FormGroup component for field grouping | Component renders correctly and accepts all required props; Unit tests pass; a11y tests pass | complete |
| Create form field wrappers for existing components | Field wrappers for TextInput, NumberInput, Select, Checkbox, and RadioGroup created | complete |
| Implement example form with validation | Story "Form Example" demonstrates form submission and validation errors | complete |
| Add unit tests for validation and form submission | Tests verify form validation and submission behavior | complete |
| Update barrel exports in index.ts | All components are properly exported and accessible | complete |
| Document form integration patterns | Documentation added to Storybook about form usage patterns | complete |

## Implementation Strategy

### Component Architecture

- Create a `form` directory under `src/components` for form-specific components
- Implement `useForm` wrapper hook around React Hook Form
- Create Field wrapper components for each form input component
- Use Zod schemas for validation
- Implement layout components (FormGrid, FormGroup) for structuring forms

### Testing Approach

- Unit tests with Vitest for functionality
- Test validation error reporting
- Test form submission behavior
- Create interactive Storybook example with form submission and validation

## Integration with Existing Components

- Existing form components (TextInput, NumberInput, etc.) will be wrapped with RHF field components
- These field wrappers will handle the connection between RHF and the UI components
- Layout components will provide consistent spacing and alignment

## Form Validation Strategy

- Use Zod for schema definition and validation
- Display validation errors inline with form fields
- Support both synchronous and asynchronous validation
- Support field-level and form-level validation
5 changes: 4 additions & 1 deletion packages/ui-kit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"tokens-check": "node scripts/tokens-check.js"
},
"dependencies": {
"@hookform/resolvers": "^5.0.1",
"@radix-ui/react-checkbox": "^1.3.1",
"@radix-ui/react-radio-group": "^1.3.6",
"@radix-ui/react-select": "^2.2.4",
Expand All @@ -45,8 +46,10 @@
"lucide-react": "^0.511.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-hook-form": "^7.56.4",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7"
"tailwindcss-animate": "^1.0.7",
"zod": "^3.25.7"
},
"devDependencies": {
"@chromatic-com/storybook": "^1.9.0",
Expand Down
36 changes: 36 additions & 0 deletions packages/ui-kit/src/components/form/CheckboxField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { FieldValues, Path } from 'react-hook-form';
import { Checkbox, CheckboxProps } from '../primitives/Checkbox/Checkbox';
import { FieldWrapper, FieldWrapperProps } from './FieldWrapper';

export type CheckboxFieldProps<TFieldValues extends FieldValues> =
Omit<CheckboxProps, 'name' | 'checked'> &
Omit<FieldWrapperProps<TFieldValues>, 'render'> & {
name: Path<TFieldValues>;
};

/**
* CheckboxField component that integrates Checkbox with React Hook Form
*/
export function CheckboxField<TFieldValues extends FieldValues>({
name,
label,
required,
...props
}: CheckboxFieldProps<TFieldValues>) {
return (
<FieldWrapper
name={name}
label={label}
required={required}
render={({ field, fieldState }) => (
<Checkbox
{...props}
error={fieldState.error?.message || ''}
checked={!!field.value}
onCheckedChange={field.onChange}
name={field.name}
/>
)}
/>
);
}
114 changes: 114 additions & 0 deletions packages/ui-kit/src/components/form/FieldWrapper.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { render, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { FieldWrapper } from './FieldWrapper';
import { FormProvider, useForm } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';

const TestForm = ({ children }: { children: React.ReactNode }) => {
const form = useForm({
defaultValues: {
testField: ''
}
});

return <FormProvider {...form}>{children}</FormProvider>;
};

describe('FieldWrapper', () => {
it('renders the provided field component', () => {
render(
<TestForm>
<FieldWrapper
name="testField"
label="Test Field"
render={({ field }) => (
<input
data-testid="test-input"
value={field.value as string}
onChange={field.onChange}
onBlur={field.onBlur}
name={field.name}
/>
)}
/>
</TestForm>
);

expect(screen.getByTestId('test-input')).toBeInTheDocument();
expect(screen.getByText('Test Field')).toBeInTheDocument();
});

it('renders error message when field has error', () => {
const schema = z.object({
testField: z.string().min(1, 'This field is required')
});

const TestFormWithValidation = () => {
const form = useForm({
resolver: zodResolver(schema),
defaultValues: {
testField: ''
},
mode: 'onChange'
});

// Set error manually for testing
form.setError('testField', {
type: 'manual',
message: 'This field is required'
});

return (
<FormProvider {...form}>
<FieldWrapper
name="testField"
label="Test Field"
render={({ field }) => (
<input
data-testid="test-input"
value={field.value as string}
onChange={field.onChange}
onBlur={field.onBlur}
name={field.name}
/>
)}
/>
</FormProvider>
);
};

render(<TestFormWithValidation />);

// Use queryByTestId to verify error is displayed
expect(screen.getByTestId('test-input')).toBeInTheDocument();
// Use getAllByText to handle multiple elements with the same text
const errorElements = screen.getAllByText('This field is required');
expect(errorElements.length).toBeGreaterThan(0);
});

it('passes required prop to FormGroup', () => {
render(
<TestForm>
<FieldWrapper
name="testField"
label="Required Field"
required={true}
render={({ field }) => (
<input
data-testid="test-input"
value={field.value as string}
onChange={field.onChange}
onBlur={field.onBlur}
name={field.name}
/>
)}
/>
</TestForm>
);

const requiredIndicator = screen.getByText('*');
expect(requiredIndicator).toBeInTheDocument();
expect(requiredIndicator).toHaveClass('text-error');
});
});
59 changes: 59 additions & 0 deletions packages/ui-kit/src/components/form/FieldWrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React from 'react';
import { useFormContext, Controller, FieldValues, Path } from 'react-hook-form';
import { FormGroup, FormGroupProps } from './FormGroup';

export type FieldWrapperProps<TFieldValues extends FieldValues> = {
name: Path<TFieldValues>;
render: (props: {
field: {
onChange: (...event: unknown[]) => void;
onBlur: () => void;
value: unknown;
name: string;
ref: React.Ref<unknown>;
};
fieldState: {
invalid: boolean;
isTouched: boolean;
isDirty: boolean;
error?: {
type: string;
message?: string;
};
};
}) => React.ReactElement;
} & Omit<FormGroupProps, 'children'>;

/**
* A wrapper component for form fields to be used with React Hook Form
*/
export function FieldWrapper<
TFieldValues extends FieldValues = FieldValues
>({
name,
label,
htmlFor = name as string,
render,
required,
...formGroupProps
}: FieldWrapperProps<TFieldValues>) {
const { control } = useFormContext<TFieldValues>();

return (
<Controller
control={control}
name={name}
render={({ field, fieldState }) => (
<FormGroup
label={label}
htmlFor={htmlFor}
error={fieldState.error?.message}
required={required}
{...formGroupProps}
>
{render({ field, fieldState })}
</FormGroup>
)}
/>
);
}
Loading
Loading