From 2bd7d49bd5e1d8105d81b95c88e0e2d75597a8fa Mon Sep 17 00:00:00 2001 From: "codegen-sh[bot]" <131295404+codegen-sh[bot]@users.noreply.github.com> Date: Thu, 18 Sep 2025 03:34:28 +0000 Subject: [PATCH 01/29] feat: implement scroll-to-error functionality for @lambdacurry/forms - Add core scrollToFirstError utility with configurable options - Create useScrollToErrorOnSubmit hook for Remix form integration - Add ScrollToErrorOnSubmit component for declarative usage - Leverage existing data-slot selectors for seamless integration - Support both client-side and server-side validation errors - Include retry logic for async rendering scenarios - Export new functionality from main package entry points --- .../remix-hook-form/phone-input.stories.tsx | 6 +- apps/docs/src/remix-hook-form/select.test.tsx | 29 ++++---- package.json | 5 +- packages/components/package.json | 4 +- packages/components/src/index.ts | 4 ++ .../canada-province-select.tsx | 12 +--- .../components/ScrollToErrorOnSubmit.tsx | 14 ++++ .../src/remix-hook-form/components/index.ts | 1 + .../src/remix-hook-form/hooks/index.ts | 1 + .../hooks/useScrollToErrorOnSubmit.ts | 54 ++++++++++++++ .../components/src/remix-hook-form/index.ts | 6 +- .../src/remix-hook-form/phone-input.tsx | 7 +- .../components/src/remix-hook-form/select.tsx | 19 ++--- .../src/remix-hook-form/us-state-select.tsx | 12 +--- .../src/ui/canada-province-select.tsx | 12 +--- .../src/ui/data/canada-provinces.ts | 2 +- packages/components/src/ui/data/us-states.ts | 2 +- packages/components/src/ui/index.ts | 1 - packages/components/src/ui/phone-input.tsx | 4 +- .../components/src/ui/us-state-select.tsx | 12 +--- packages/components/src/utils/index.ts | 1 + .../components/src/utils/scrollToError.ts | 70 +++++++++++++++++++ 22 files changed, 189 insertions(+), 89 deletions(-) create mode 100644 packages/components/src/remix-hook-form/components/ScrollToErrorOnSubmit.tsx create mode 100644 packages/components/src/remix-hook-form/components/index.ts create mode 100644 packages/components/src/remix-hook-form/hooks/index.ts create mode 100644 packages/components/src/remix-hook-form/hooks/useScrollToErrorOnSubmit.ts create mode 100644 packages/components/src/utils/index.ts create mode 100644 packages/components/src/utils/scrollToError.ts diff --git a/apps/docs/src/remix-hook-form/phone-input.stories.tsx b/apps/docs/src/remix-hook-form/phone-input.stories.tsx index 08e77343..82b74fea 100644 --- a/apps/docs/src/remix-hook-form/phone-input.stories.tsx +++ b/apps/docs/src/remix-hook-form/phone-input.stories.tsx @@ -37,11 +37,7 @@ const ControlledPhoneInputExample = () => {
- + { const canvas = within(canvasElement); - + // Find and click the US state dropdown const stateDropdown = canvas.getByLabelText('US State'); await userEvent.click(stateDropdown); - + // Select a state (e.g., California) const californiaOption = await canvas.findByText('California'); await userEvent.click(californiaOption); - + // Verify the selection expect(stateDropdown).toHaveTextContent('California'); }; @@ -21,15 +21,15 @@ export const testUSStateSelection = async ({ canvasElement }: StoryContext) => { // Test selecting a Canadian province export const testCanadaProvinceSelection = async ({ canvasElement }: StoryContext) => { const canvas = within(canvasElement); - + // Find and click the Canada province dropdown const provinceDropdown = canvas.getByLabelText('Canadian Province'); await userEvent.click(provinceDropdown); - + // Select a province (e.g., Ontario) const ontarioOption = await canvas.findByText('Ontario'); await userEvent.click(ontarioOption); - + // Verify the selection expect(provinceDropdown).toHaveTextContent('Ontario'); }; @@ -37,29 +37,29 @@ export const testCanadaProvinceSelection = async ({ canvasElement }: StoryContex // Test form submission export const testFormSubmission = async ({ canvasElement }: StoryContext) => { const canvas = within(canvasElement); - + // Select a state const stateDropdown = canvas.getByLabelText('US State'); await userEvent.click(stateDropdown); const californiaOption = await canvas.findByText('California'); await userEvent.click(californiaOption); - + // Select a province const provinceDropdown = canvas.getByLabelText('Canadian Province'); await userEvent.click(provinceDropdown); const ontarioOption = await canvas.findByText('Ontario'); await userEvent.click(ontarioOption); - + // Select a custom region const regionDropdown = canvas.getByLabelText('Custom Region'); await userEvent.click(regionDropdown); const customOption = await canvas.findByText('New York'); await userEvent.click(customOption); - + // Submit the form const submitButton = canvas.getByRole('button', { name: 'Submit' }); await userEvent.click(submitButton); - + // Verify the submission (mock response would be shown) await expect(canvas.findByText('Selected regions:')).resolves.toBeInTheDocument(); }; @@ -67,14 +67,13 @@ export const testFormSubmission = async ({ canvasElement }: StoryContext) => { // Test validation errors export const testValidationErrors = async ({ canvasElement }: StoryContext) => { const canvas = within(canvasElement); - + // Submit the form without selecting anything const submitButton = canvas.getByRole('button', { name: 'Submit' }); await userEvent.click(submitButton); - + // Verify error messages await expect(canvas.findByText('Please select a state')).resolves.toBeInTheDocument(); await expect(canvas.findByText('Please select a province')).resolves.toBeInTheDocument(); await expect(canvas.findByText('Please select a region')).resolves.toBeInTheDocument(); }; - diff --git a/package.json b/package.json index 609afa67..80343420 100644 --- a/package.json +++ b/package.json @@ -2,10 +2,7 @@ "name": "forms", "version": "0.2.0", "private": true, - "workspaces": [ - "apps/*", - "packages/*" - ], + "workspaces": ["apps/*", "packages/*"], "scripts": { "start": "yarn dev", "dev": "turbo run dev", diff --git a/packages/components/package.json b/packages/components/package.json index 208adedd..d75135c1 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -26,9 +26,7 @@ "import": "./dist/ui/*.js" } }, - "files": [ - "dist" - ], + "files": ["dist"], "scripts": { "prepublishOnly": "yarn run build", "build": "vite build", diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index cdadc33c..7adcf11f 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -1,5 +1,9 @@ // Main exports from both remix-hook-form and ui directories +// Add scroll-to-error utilities +export { scrollToFirstError } from './utils/scrollToError'; +export type { ScrollToErrorOptions } from './utils/scrollToError'; + // Export all components from remix-hook-form export * from './remix-hook-form'; diff --git a/packages/components/src/remix-hook-form/canada-province-select.tsx b/packages/components/src/remix-hook-form/canada-province-select.tsx index fb697b29..072935bd 100644 --- a/packages/components/src/remix-hook-form/canada-province-select.tsx +++ b/packages/components/src/remix-hook-form/canada-province-select.tsx @@ -1,16 +1,8 @@ -import * as React from 'react'; -import { Select, type SelectProps } from './select'; import { CANADA_PROVINCES } from '../ui/data/canada-provinces'; +import { Select, type SelectProps } from './select'; export type CanadaProvinceSelectProps = Omit; export function CanadaProvinceSelect(props: CanadaProvinceSelectProps) { - return ( - ; } - diff --git a/packages/components/src/remix-hook-form/components/ScrollToErrorOnSubmit.tsx b/packages/components/src/remix-hook-form/components/ScrollToErrorOnSubmit.tsx new file mode 100644 index 00000000..2207f433 --- /dev/null +++ b/packages/components/src/remix-hook-form/components/ScrollToErrorOnSubmit.tsx @@ -0,0 +1,14 @@ +import { type UseScrollToErrorOnSubmitOptions, useScrollToErrorOnSubmit } from '../hooks/useScrollToErrorOnSubmit'; + +export interface ScrollToErrorOnSubmitProps extends UseScrollToErrorOnSubmitOptions { + className?: string; +} + +export const ScrollToErrorOnSubmit = ({ className, ...options }: ScrollToErrorOnSubmitProps) => { + useScrollToErrorOnSubmit(options); + + // Return null or hidden div - follows existing patterns + return className ?