Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
9a309be
feat(select): switch dropdown to shadcn Command (cmdk) primitives and…
codegen-sh[bot] Sep 21, 2025
4f25a08
feat: integrate cmdk for Command components and enhance Select with f…
jaruesink Sep 22, 2025
55716fd
feat(select): enhance Select component with search and creatable options
jaruesink Sep 22, 2025
6c45f77
chore: add type-check scripts and update Turbo configuration
jaruesink Sep 22, 2025
69c6859
ci: add PR quality checks for lint and typecheck (modeled after repor…
codegen-sh[bot] Sep 22, 2025
af19345
Fix typecheck command in PR quality checks workflow
codegen-sh[bot] Sep 22, 2025
0593b32
Fix flaky select test by adding timing delay
codegen-sh[bot] Sep 22, 2025
22ee174
Fix lint issues: add parentheses around arrow function parameters
codegen-sh[bot] Sep 22, 2025
45e2acf
test(storybook): make Select tests portal-aware and less flaky
codegen-sh[bot] Sep 22, 2025
0e0e960
Fix CreatableOption test: wait for component render before interaction
codegen-sh[bot] Sep 22, 2025
591cb12
chore: update typecheck command and dependencies in package.json
jaruesink Sep 22, 2025
b6818e6
Merge branch 'codegen-bot/add-lint-typecheck-ci-1758507' of github.co…
jaruesink Sep 22, 2025
c6975d5
Merge pull request #149 from lambda-curry/codegen-bot/add-lint-typech…
jaruesink Sep 22, 2025
9df601e
Merge branch 'codegen/lc-324-researcher-test' into codegen-bot/fix-po…
jaruesink Sep 22, 2025
1d37c02
Fix Select component ARIA roles for Storybook tests
codegen-sh[bot] Sep 22, 2025
e60441c
Fix CreatableOption test: improve async handling for portaled dropdown
codegen-sh[bot] Sep 22, 2025
3a4ae4c
Fix: improve listbox retrieval in CreatableOption test
jaruesink Sep 22, 2025
c3a00c5
Fix: enhance listbox retrieval and validation in select stories
jaruesink Sep 22, 2025
d3321f9
Fix: enhance listbox interaction and error handling in select stories
jaruesink Sep 22, 2025
e173cef
Fix: improve loading waits in select stories for better test reliability
jaruesink Sep 22, 2025
d24d4e6
Fix: streamline listbox interactions in select stories for improved t…
jaruesink Sep 22, 2025
86707c5
Merge pull request #150 from lambda-curry/codegen-bot/fix-portaled-se…
jaruesink Sep 22, 2025
a0a9256
chore(release): bump @lambdacurry/forms to 0.22.0
codegen-sh[bot] Sep 22, 2025
ac82256
Merge pull request #152 from lambda-curry/codegen-bot/bump-components…
jaruesink Sep 22, 2025
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
66 changes: 66 additions & 0 deletions .github/workflows/pr-quality-checks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
name: PR Quality Checks

on:
pull_request:
paths:
- 'apps/**'
- 'packages/**'
- '.github/workflows/**'
- '*.json'
- '*.js'
- '*.ts'
- '*.tsx'
- 'yarn.lock'
- 'turbo.json'
- 'biome.json'
- '!**/*.md'
- '!**/*.txt'
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
quality-checks:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '22.9.0'
Comment on lines +35 to +37
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Upgrade setup-node to v4 (v3 is deprecated on current runners).

Prevents CI breakage and silences actionlint.

-      - name: Setup Node.js
-        uses: actions/setup-node@v3
+      - name: Setup Node.js
+        uses: actions/setup-node@v4
         with:
           node-version: '22.9.0'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
uses: actions/setup-node@v3
with:
node-version: '22.9.0'
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22.9.0'
🧰 Tools
🪛 actionlint (1.7.7)

35-35: the runner of "actions/setup-node@v3" action is too old to run on GitHub Actions. update the action's version to fix this issue

(action)

🤖 Prompt for AI Agents
.github/workflows/pr-quality-checks.yml around lines 35 to 37: the workflow
currently uses actions/setup-node@v3 which is deprecated on current runners;
update the action reference to actions/setup-node@v4 (keeping the same
node-version value) so the workflow uses the supported major version and
actionlint warnings are resolved; simply change the uses line to
actions/setup-node@v4 and verify the existing with: node-version: '22.9.0'
remains unchanged.


- name: Setup Yarn Corepack
run: corepack enable

- name: Install dependencies
run: yarn install

- name: Cache Turbo
uses: actions/cache@v4
with:
path: .turbo
key: ${{ runner.os }}-turbo-${{ github.ref_name }}-${{ hashFiles('**/yarn.lock') }}-${{ github.sha }}
restore-keys: |
${{ runner.os }}-turbo-${{ github.ref_name }}-${{ hashFiles('**/yarn.lock') }}-
${{ runner.os }}-turbo-${{ github.ref_name }}-
${{ runner.os }}-turbo-

- name: Run Turbo lint
run: yarn turbo run lint

- name: Run Turbo typecheck
run: yarn turbo run type-check --filter=@lambdacurry/forms

- name: Summary
run: |
echo "## PR Quality Checks Summary" >> $GITHUB_STEP_SUMMARY
echo "✅ Linting passed (Biome)" >> $GITHUB_STEP_SUMMARY
echo "✅ TypeScript compilation passed" >> $GITHUB_STEP_SUMMARY
echo "✅ All checks completed with Turbo caching" >> $GITHUB_STEP_SUMMARY
6 changes: 5 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"Bazza",
"biomejs",
"cleanbuild",
"cmdk",
"Filenaming",
"hookform",
"isbot",
Expand All @@ -23,5 +24,8 @@
"source.fixAll.biome": "explicit",
"source.organizeImports.biome": "explicit"
},
"tailwindCSS.classAttributes": ["class", "className", "ngClass", "class:list", "wrapperClassName"]
"tailwindCSS.classAttributes": ["class", "className", "ngClass", "class:list", "wrapperClassName"],
"[jsonc]": {
"editor.defaultFormatter": "biomejs.biome"
}
}
6 changes: 5 additions & 1 deletion apps/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"storybook": "storybook dev -p 6006",
"serve": "http-server ./storybook-static -p 6006 -s",
"test": "start-server-and-test serve http://127.0.0.1:6006 'test-storybook --url http://127.0.0.1:6006'",
"test:local": "test-storybook"
"test:local": "test-storybook",
"type-check": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@hookform/error-message": "^2.0.0",
Expand All @@ -30,6 +31,9 @@
"@storybook/testing-library": "^0.2.2",
"@tailwindcss/postcss": "^4.1.8",
"@tailwindcss/vite": "^4.0.0",
"@testing-library/jest-dom": "^6.8.0",
"@testing-library/react": "^16.3.0",
"@types/jest": "^30.0.0",
"@types/react": "^19.0.0",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
Expand Down
14 changes: 5 additions & 9 deletions apps/docs/src/examples/middleware-example.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,7 @@ export const action = async ({ context }: ActionFunctionArgs) => {

// Component
export default function MiddlewareExample() {
const {
handleSubmit,
formState: { errors },
register,
} = useRemixForm<FormData>({
const methods = useRemixForm<FormData>({
mode: 'onSubmit',
resolver,
});
Expand All @@ -46,12 +42,12 @@ export default function MiddlewareExample() {
<div className="p-4">
<h1 className="text-2xl font-bold mb-4">Remix Hook Form v7 Middleware Example</h1>

<RemixFormProvider>
<Form method="POST" onSubmit={handleSubmit}>
<RemixFormProvider {...methods}>
<Form method="POST" onSubmit={methods.handleSubmit}>
<div className="space-y-4">
<TextField label="Name" {...register('name')} error={errors.name?.message} />
<TextField name="name" label="Name" />

<TextField label="Email" type="email" {...register('email')} error={errors.email?.message} />
<TextField name="email" type="email" label="Email" />

<button type="submit" className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
Submit
Expand Down
15 changes: 8 additions & 7 deletions apps/docs/src/remix-hook-form/checkbox-list.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Checkbox } from '@lambdacurry/forms/remix-hook-form/checkbox';
import { FormMessage } from '@lambdacurry/forms/remix-hook-form/form';
import { Button } from '@lambdacurry/forms/ui/button';
import type { Meta, StoryObj } from '@storybook/react-vite';
import { expect, userEvent, type within } from '@storybook/test';
import { expect, userEvent, within } from '@storybook/test';
import { type ActionFunctionArgs, Form, useFetcher } from 'react-router';
import { createFormData, getValidatedFormData, RemixFormProvider, useRemixForm } from 'remix-hook-form';
import { z } from 'zod';
Expand Down Expand Up @@ -137,18 +137,18 @@ const meta: Meta<typeof Checkbox> = {
export default meta;
type Story = StoryObj<typeof meta>;

interface StoryContext {
canvas: ReturnType<typeof within>;
}
type StoryContext = { canvasElement: HTMLElement };

const testDefaultValues = ({ canvas }: StoryContext) => {
const testDefaultValues = ({ canvasElement }: StoryContext) => {
const canvas = within(canvasElement);
AVAILABLE_COLORS.forEach(({ label }) => {
const checkbox = canvas.getByLabelText(label);
expect(checkbox).not.toBeChecked();
});
};

const testErrorState = async ({ canvas }: StoryContext) => {
const testErrorState = async ({ canvasElement }: StoryContext) => {
const canvas = within(canvasElement);
// Submit form without selecting any colors
const submitButton = canvas.getByRole('button', { name: 'Submit' });
await userEvent.click(submitButton);
Expand All @@ -157,7 +157,8 @@ const testErrorState = async ({ canvas }: StoryContext) => {
await expect(await canvas.findByText('Please select at least one color')).toBeInTheDocument();
};

const testColorSelection = async ({ canvas }: StoryContext) => {
const testColorSelection = async ({ canvasElement }: StoryContext) => {
const canvas = within(canvasElement);
// Select two colors
const redCheckbox = canvas.getByLabelText('Red');
const blueCheckbox = canvas.getByLabelText('Blue');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useDataTableFilters } from '@lambdacurry/forms/ui/data-table-filter/hoo
import { useFilterSync } from '@lambdacurry/forms/ui/utils/use-filter-sync';
import { CheckCircledIcon, PersonIcon, StarIcon, TextIcon } from '@radix-ui/react-icons';
import type { Meta, StoryContext, StoryObj } from '@storybook/react';
import { expect } from '@storybook/test';
import { expect, within } from '@storybook/test';
import { withReactRouterStubDecorator } from '../../lib/storybook/react-router-stub';

/**
Expand Down Expand Up @@ -228,15 +228,17 @@ type Story = StoryObj<typeof meta>;
/**
* Test functions for accessibility testing
*/
const testBasicRendering = ({ canvas }: StoryContext) => {
const testBasicRendering = ({ canvasElement }: StoryContext) => {
const canvas = within(canvasElement);
const title = canvas.getByText('Data Table Filter Accessibility Test');
expect(title).toBeInTheDocument();

const filterInterface = canvas.getByText('Filter Interface');
expect(filterInterface).toBeInTheDocument();
};

const testKeyboardNavigation = async ({ canvas }: StoryContext) => {
const testKeyboardNavigation = async ({ canvasElement }: StoryContext) => {
const canvas = within(canvasElement);
// Look for filter-related buttons or elements
const buttons = canvas.getAllByRole('button');
await expect(buttons.length).toBeGreaterThan(0);
Expand All @@ -248,7 +250,8 @@ const testKeyboardNavigation = async ({ canvas }: StoryContext) => {
}
};

const testAriaAttributes = async ({ canvas }: StoryContext) => {
const testAriaAttributes = async ({ canvasElement }: StoryContext) => {
const canvas = within(canvasElement);
// Test that interactive elements have proper roles
const buttons = canvas.getAllByRole('button');
expect(buttons.length).toBeGreaterThan(0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,8 @@ export default meta;
type Story = StoryObj<typeof meta>;

export const Default: Story = {
args: {} satisfies Record<string, unknown>, // Args for DataTableRouterForm if needed, handled by Example component
// biome-ignore lint/suspicious/noExplicitAny: any for flexibility
args: {} as any,
render: () => <DataTableRouterFormExample />,
parameters: {
docs: {
Expand Down
20 changes: 11 additions & 9 deletions apps/docs/src/remix-hook-form/form-error.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import { render, screen } from '@testing-library/react';
import { useFetcher } from 'react-router';
import { RemixFormProvider, useRemixForm } from 'remix-hook-form';
import { z } from 'zod';
import type { ElementType, PropsWithChildren } from 'react';
import type { ElementType } from 'react';
import type { FetcherWithComponents } from 'react-router';
import type { FormMessageProps } from '@lambdacurry/forms/ui/form';

// Mock useFetcher
jest.mock('react-router', () => ({
Expand All @@ -31,15 +33,15 @@ const TestFormWithError = ({
}: {
initialErrors?: Record<string, { message: string }>;
formErrorName?: string;
customComponents?: { FormMessage?: React.ComponentType<PropsWithChildren<Record<string, unknown>>> };
customComponents?: { FormMessage?: React.ComponentType<FormMessageProps> };
className?: string;
}) => {
const mockFetcher = {
data: { errors: initialErrors },
state: 'idle' as const,
submit: jest.fn(),
Form: 'form' as ElementType,
};
} as unknown as FetcherWithComponents<unknown>;

mockUseFetcher.mockReturnValue(mockFetcher);

Expand Down Expand Up @@ -143,9 +145,9 @@ describe('FormError Component', () => {

describe('Component Customization', () => {
it('uses custom FormMessage component when provided', () => {
const CustomFormMessage = ({ children, ...props }: PropsWithChildren<Record<string, unknown>>) => (
const CustomFormMessage = (props: FormMessageProps) => (
<div data-testid="custom-form-message" className="custom-message" {...props}>
Custom: {children}
Custom: {props.children}
</div>
);

Expand Down Expand Up @@ -218,7 +220,7 @@ describe('FormError Component', () => {
state: 'idle' as const,
submit: jest.fn(),
Form: 'form' as ElementType,
};
} as unknown as FetcherWithComponents<unknown>;

mockUseFetcher.mockReturnValue(mockFetcher);

Expand Down Expand Up @@ -315,9 +317,9 @@ describe('FormError Component', () => {
it('does not re-render unnecessarily when unrelated form state changes', () => {
const renderSpy = jest.fn();

const CustomFormMessage = ({ children, ...props }: PropsWithChildren<Record<string, unknown>>) => {
const CustomFormMessage = (props: FormMessageProps) => {
renderSpy();
return <div {...props}>{children}</div>;
return <div {...props}>{props.children}</div>;
};

const errors = {
Expand Down Expand Up @@ -346,7 +348,7 @@ describe('FormError Integration Tests', () => {
state: 'idle' as const,
submit: jest.fn(),
Form: 'form' as ElementType,
};
} as unknown as FetcherWithComponents<unknown>;

mockUseFetcher.mockReturnValue(mockFetcher);

Expand Down
20 changes: 13 additions & 7 deletions apps/docs/src/remix-hook-form/password-field.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { PasswordField } from '@lambdacurry/forms/remix-hook-form/password-field';
import { Button } from '@lambdacurry/forms/ui/button';
import type { Meta, StoryContext, StoryObj } from '@storybook/react-vite';
import { expect, userEvent } from '@storybook/test';
import { expect, userEvent, within } from '@storybook/test';
import { useRef } from 'react';
import { type ActionFunctionArgs, useFetcher } from 'react-router';
import { getValidatedFormData, RemixFormProvider, useRemixForm } from 'remix-hook-form';
Expand Down Expand Up @@ -114,14 +114,16 @@ export default meta;
type Story = StoryObj<typeof meta>;

// Test scenarios
const testDefaultValues = ({ canvas }: StoryContext) => {
const testDefaultValues = ({ canvasElement }: StoryContext) => {
const canvas = within(canvasElement);
const passwordInput = canvas.getByLabelText('Password');
const confirmInput = canvas.getByLabelText('Confirm Password');
expect(passwordInput).toHaveValue(INITIAL_PASSWORD);
expect(confirmInput).toHaveValue(INITIAL_PASSWORD);
};

const testPasswordVisibilityToggle = async ({ canvas }: StoryContext) => {
const testPasswordVisibilityToggle = async ({ canvasElement }: StoryContext) => {
const canvas = within(canvasElement);
const passwordInput = canvas.getByLabelText('Password');

// Find the toggle button within the same form item as the password input
Expand Down Expand Up @@ -150,7 +152,8 @@ const testPasswordVisibilityToggle = async ({ canvas }: StoryContext) => {
expect(showButtonAgain).toBeInTheDocument();
};

const testWeakPasswordValidation = async ({ canvas }: StoryContext) => {
const testWeakPasswordValidation = async ({ canvasElement }: StoryContext) => {
const canvas = within(canvasElement);
const passwordInput = canvas.getByLabelText('Password');
const submitButton = canvas.getByRole('button', { name: 'Create Account' });

Expand All @@ -162,7 +165,8 @@ const testWeakPasswordValidation = async ({ canvas }: StoryContext) => {
await expect(await canvas.findByText(WEAK_PASSWORD_ERROR)).toBeInTheDocument();
};

const testPasswordMismatchValidation = async ({ canvas }: StoryContext) => {
const testPasswordMismatchValidation = async ({ canvasElement }: StoryContext) => {
const canvas = within(canvasElement);
const passwordInput = canvas.getByLabelText('Password');
const confirmInput = canvas.getByLabelText('Confirm Password');
const submitButton = canvas.getByRole('button', { name: 'Create Account' });
Expand All @@ -180,7 +184,8 @@ const testPasswordMismatchValidation = async ({ canvas }: StoryContext) => {
await expect(await canvas.findByText(MISMATCH_PASSWORD_ERROR)).toBeInTheDocument();
};

const testValidSubmission = async ({ canvas }: StoryContext) => {
const testValidSubmission = async ({ canvasElement }: StoryContext) => {
const canvas = within(canvasElement);
const passwordInput = canvas.getByLabelText('Password');
const confirmInput = canvas.getByLabelText('Confirm Password');
const submitButton = canvas.getByRole('button', { name: 'Create Account' });
Expand All @@ -199,7 +204,8 @@ const testValidSubmission = async ({ canvas }: StoryContext) => {
expect(successMessage).toBeInTheDocument();
};

const testRefFunctionality = async ({ canvas }: StoryContext) => {
const testRefFunctionality = async ({ canvasElement }: StoryContext) => {
const canvas = within(canvasElement);
const refInput = canvas.getByLabelText('Ref Example');
const focusButton = canvas.getByRole('button', { name: 'Focus' });

Expand Down
Loading