Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
2bd7d49
feat: implement scroll-to-error functionality for @lambdacurry/forms
codegen-sh[bot] Sep 18, 2025
f9fcaf6
feat: add comprehensive Storybook examples for scroll-to-error functi…
codegen-sh[bot] Sep 18, 2025
06f8435
Fix RadioGroupItem import in scroll-to-error story
codegen-sh[bot] Sep 18, 2025
0a24c56
fix: allow passing methods to useScrollToErrorOnSubmit hook
codegen-sh[bot] Sep 18, 2025
693054d
Fix scroll-to-error story context issue
codegen-sh[bot] Sep 18, 2025
6ef7938
Fix scroll-to-error hook null reference error
codegen-sh[bot] Sep 18, 2025
ce180af
Fix ReferenceError: methods is not defined
codegen-sh[bot] Sep 18, 2025
8fc6ada
Fix methods reference in ScrollToErrorHookForm component
codegen-sh[bot] Sep 18, 2025
ddee63e
Refactor: streamline scroll-to-error story and remove unused test cases
jaruesink Sep 18, 2025
87daaa7
Merge pull request #141 from lambda-curry/codegen/lc-322-lets-create-…
jaruesink Sep 18, 2025
43aa58b
Fix: adjust spacing in RadioGroupItemField label for better a11y
lcmohsen Sep 8, 2025
62fd07f
chore: bump @lambdacurry/forms to v0.19.6
codegen-sh[bot] Sep 18, 2025
b5bc11d
Improve select dropdown width matching with ResizeObserver
codegen-sh[bot] Sep 18, 2025
7b9ac09
Add 500px width test story for select dropdown
codegen-sh[bot] Sep 18, 2025
899440b
Fix ResizeObserver compatibility for test environments
codegen-sh[bot] Sep 18, 2025
31ef9b5
Fix TypeScript error in ResizeObserver implementation
codegen-sh[bot] Sep 18, 2025
b16fb55
Fix ResizeObserver timing issues causing test failures
codegen-sh[bot] Sep 18, 2025
23623f1
Fix dropdown width by removing hardcoded w-72 class
codegen-sh[bot] Sep 18, 2025
b8fe440
Add width debugging and ensure inner content takes full width
codegen-sh[bot] Sep 18, 2025
08a6a15
Replace ResizeObserver with Radix CSS custom property
codegen-sh[bot] Sep 18, 2025
c3a2f81
Remove failing test story and cleanup test components
codegen-sh[bot] Sep 18, 2025
c4b7d5f
Remove unnecessary blank lines in select stories
jaruesink Sep 18, 2025
6aebf2f
feat: add keyboard navigation to Select component
codegen-sh[bot] Sep 17, 2025
79f9e62
Fix keyboard navigation timing issue in Select component
codegen-sh[bot] Sep 17, 2025
14eb20f
fix: Improve activeIndex initialization in Select component
codegen-sh[bot] Sep 17, 2025
187a97d
fix: Implement keyboard navigation for Select component
codegen-sh[bot] Sep 17, 2025
caf12ac
chore: remove changeset, update package version, and remove failing t…
codegen-sh[bot] Sep 18, 2025
241c976
chore: bump @lambdacurry/forms to v0.19.7
codegen-sh[bot] Sep 18, 2025
44e4540
chore: bump @lambdacurry/forms to v0.20.0
codegen-sh[bot] Sep 18, 2025
f910d40
Refactor: enhance useScrollToErrorOnSubmit hook with TypeScript types…
jaruesink Sep 18, 2025
869dad8
Merge branch 'codegen/lc-321-complete-implementation-guide-adding-scr…
jaruesink Sep 18, 2025
a62e451
Merge branch 'main' into codegen/lc-321-complete-implementation-guide…
jaruesink Sep 18, 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
6 changes: 1 addition & 5 deletions apps/docs/src/remix-hook-form/phone-input.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,7 @@ const ControlledPhoneInputExample = () => {
<RemixFormProvider {...methods}>
<fetcher.Form onSubmit={methods.handleSubmit}>
<div className="grid gap-8">
<PhoneInput
name="usaPhone"
label="Phone Number"
description="Enter a US phone number"
/>
<PhoneInput name="usaPhone" label="Phone Number" description="Enter a US phone number" />
<PhoneInput
name="internationalPhone"
label="International Phone Number"
Expand Down
29 changes: 14 additions & 15 deletions apps/docs/src/remix-hook-form/select.test.tsx
Original file line number Diff line number Diff line change
@@ -1,80 +1,79 @@
import type { StoryContext } from '@storybook/react';
import { expect } from '@storybook/test';
import { userEvent, within } from '@storybook/testing-library';
import { StoryContext } from '@storybook/react';

// Test selecting a US state
export const testUSStateSelection = async ({ canvasElement }: StoryContext) => {
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');
};

// 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');
};

// 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();
};

// 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();
};

5 changes: 1 addition & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 1 addition & 3 deletions packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,7 @@
"import": "./dist/ui/*.js"
}
},
"files": [
"dist"
],
"files": ["dist"],
"scripts": {
"prepublishOnly": "yarn run build",
"build": "vite build",
Expand Down
4 changes: 4 additions & 0 deletions packages/components/src/index.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down
12 changes: 2 additions & 10 deletions packages/components/src/remix-hook-form/canada-province-select.tsx
Original file line number Diff line number Diff line change
@@ -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<SelectProps, 'options'>;

export function CanadaProvinceSelect(props: CanadaProvinceSelectProps) {
return (
<Select
{...props}
options={CANADA_PROVINCES}
placeholder="Select a province"
/>
);
return <Select {...props} options={CANADA_PROVINCES} placeholder="Select a province" />;
}

Original file line number Diff line number Diff line change
@@ -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 ? <div className={className} aria-hidden="true" /> : null;
};

ScrollToErrorOnSubmit.displayName = 'ScrollToErrorOnSubmit';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './ScrollToErrorOnSubmit';
1 change: 1 addition & 0 deletions packages/components/src/remix-hook-form/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useScrollToErrorOnSubmit';
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { useEffect, useMemo } from 'react';
import { useRemixFormContext } from 'remix-hook-form';
import { type ScrollToErrorOptions, scrollToFirstError } from '../../utils/scrollToError';

export interface UseScrollToErrorOnSubmitOptions extends ScrollToErrorOptions {
delay?: number;
enabled?: boolean;
scrollOnServerErrors?: boolean;
scrollOnMount?: boolean;
}

export const useScrollToErrorOnSubmit = (options: UseScrollToErrorOnSubmitOptions = {}) => {
const { formState } = useRemixFormContext();
const { delay = 100, enabled = true, scrollOnServerErrors = true, scrollOnMount = true, ...scrollOptions } = options;

// Memoize scroll options to prevent unnecessary re-renders
const memoizedScrollOptions = useMemo(() => scrollOptions, [
scrollOptions.behavior,
scrollOptions.block,
scrollOptions.inline,
scrollOptions.offset,
scrollOptions.shouldFocus,
scrollOptions.retryAttempts,
]);

// Handle form submission errors
useEffect(() => {
if (!enabled) return;
const hasErrors = Object.keys(formState.errors).length > 0;

// Scroll after submission attempt when errors exist
if (!formState.isSubmitting && hasErrors) {
const timeoutId = setTimeout(() => {
scrollToFirstError(formState.errors, memoizedScrollOptions);
}, delay);

return () => clearTimeout(timeoutId);
}
}, [formState.errors, formState.isSubmitting, enabled, delay, memoizedScrollOptions]);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Handle server-side validation errors on mount (Remix SSR)
useEffect(() => {
if (!(enabled && scrollOnMount) || !scrollOnServerErrors) return;
const hasErrors = Object.keys(formState.errors).length > 0;

if (hasErrors && !formState.isSubmitting) {
const timeoutId = setTimeout(() => {
scrollToFirstError(formState.errors, memoizedScrollOptions);
}, delay);

return () => clearTimeout(timeoutId);
}
}, [enabled, scrollOnMount, scrollOnServerErrors, formState.errors, formState.isSubmitting, delay, memoizedScrollOptions]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

πŸ› οΈ Refactor suggestion

🧩 Analysis chain

Verification helper: check for other wrappers with prop order pitfalls.

Run to spot any <Select {...props} ... placeholder=...> in wrappers.


🏁 Script executed:

#!/bin/bash
rg -nP --type=ts --type=tsx -C1 '\<Select\s*\{\.\.\.props\}[^>]*\bplaceholder=' packages/components/src | sed -n '1,200p'

Length of output: 27


🏁 Script executed:

#!/bin/bash
rg -n '<Select[^>]*\{\.{3}props\}[^>]*placeholder' -g '*.tsx' -g '*.ts' packages/components/src

Length of output: 302


Don't override consumer-provided placeholder in Select wrappers

Two wrappers spread {...props} before an explicit placeholder, causing the wrapper's placeholder to always win; change to spread {...props} last or use placeholder={props.placeholder ?? '…'}.

  • packages/components/src/remix-hook-form/canada-province-select.tsx:7
  • packages/components/src/remix-hook-form/us-state-select.tsx:7
πŸ€– Prompt for AI Agents
In packages/components/src/remix-hook-form/hooks/useScrollToErrorOnSubmit.ts
around lines 26 to 53, review comment actually targets two Select wrapper files
where the wrapper placeholder is overriding consumer-provided values; update the
wrappers (packages/components/src/remix-hook-form/canada-province-select.tsx and
packages/components/src/remix-hook-form/us-state-select.tsx) so that either
{...props} is spread last (so consumer placeholder wins) or set
placeholder={props.placeholder ?? 'Your default placeholder'} (use the consumer
prop if present, otherwise fall back to the default); apply the change
consistently in both files and ensure TypeScript types still align.

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
};
6 changes: 5 additions & 1 deletion packages/components/src/remix-hook-form/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
// Add scroll-to-error functionality
export * from './hooks/useScrollToErrorOnSubmit';
export * from './components/ScrollToErrorOnSubmit';

// Keep all existing exports
export * from './checkbox';
export * from './form';
export * from './form-error';
Expand All @@ -17,4 +22,3 @@ export * from './use-data-table-url-state';
export * from './select';
export * from './us-state-select';
export * from './canada-province-select';

7 changes: 5 additions & 2 deletions packages/components/src/remix-hook-form/phone-input.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import * as React from 'react';
import { PhoneInputField as BasePhoneInputField, type PhoneInputFieldProps as BasePhoneInputFieldProps } from '../ui/phone-input-field';
import type * as React from 'react';
import {
PhoneInputField as BasePhoneInputField,
type PhoneInputFieldProps as BasePhoneInputFieldProps,
} from '../ui/phone-input-field';
import { FormControl, FormDescription, FormLabel, FormMessage } from './form';

import { useRemixFormContext } from 'remix-hook-form';
Expand Down
19 changes: 5 additions & 14 deletions packages/components/src/remix-hook-form/select.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import * as React from 'react';
import type * as React from 'react';
import { useRemixFormContext } from 'remix-hook-form';
import { FormControl, FormDescription, FormLabel, FormMessage } from './form';
import { FormField, FormItem } from '../ui/form';
import { Select as UISelect, type SelectProps as UISelectProps, type SelectUIComponents } from '../ui/select';
import { type SelectUIComponents, Select as UISelect, type SelectProps as UISelectProps } from '../ui/select';
import { FormControl, FormDescription, FormLabel, FormMessage } from './form';

export interface SelectProps extends Omit<UISelectProps, 'value' | 'onValueChange'> {
name: string;
Expand All @@ -19,14 +19,7 @@ export interface SelectProps extends Omit<UISelectProps, 'value' | 'onValueChang
>;
}

export function Select({
name,
label,
description,
className,
components,
...props
}: SelectProps) {
export function Select({ name, label, description, className, components, ...props }: SelectProps) {
const { control } = useRemixFormContext();

return (
Expand All @@ -50,9 +43,7 @@ export function Select({
}}
/>
</FormControl>
{description && (
<FormDescription Component={components?.FormDescription}>{description}</FormDescription>
)}
{description && <FormDescription Component={components?.FormDescription}>{description}</FormDescription>}
<FormMessage Component={components?.FormMessage} />
</FormItem>
)}
Expand Down
12 changes: 2 additions & 10 deletions packages/components/src/remix-hook-form/us-state-select.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,8 @@
import * as React from 'react';
import { Select, type SelectProps } from './select';
import { US_STATES } from '../ui/data/us-states';
import { Select, type SelectProps } from './select';

export type USStateSelectProps = Omit<SelectProps, 'options'>;

export function USStateSelect(props: USStateSelectProps) {
return (
<Select
{...props}
options={US_STATES}
placeholder="Select a state"
/>
);
return <Select {...props} options={US_STATES} placeholder="Select a state" />;
}

12 changes: 2 additions & 10 deletions packages/components/src/ui/canada-province-select.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,8 @@
import * as React from 'react';
import { Select, type SelectProps } from './select';
import { CANADA_PROVINCES } from './data/canada-provinces';
import { Select, type SelectProps } from './select';

export type CanadaProvinceSelectProps = Omit<SelectProps, 'options'>;

export function CanadaProvinceSelect(props: CanadaProvinceSelectProps) {
return (
<Select
options={CANADA_PROVINCES}
placeholder="Select a province"
{...props}
/>
);
return <Select options={CANADA_PROVINCES} placeholder="Select a province" {...props} />;
}

2 changes: 1 addition & 1 deletion packages/components/src/ui/data/canada-provinces.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SelectOption } from '../select';
import type { SelectOption } from '../select';

export const CANADA_PROVINCES: SelectOption[] = [
{ value: 'AB', label: 'Alberta' },
Expand Down
2 changes: 1 addition & 1 deletion packages/components/src/ui/data/us-states.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SelectOption } from '../select';
import type { SelectOption } from '../select';

export const US_STATES: SelectOption[] = [
{ value: 'AL', label: 'Alabama' },
Expand Down
1 change: 0 additions & 1 deletion packages/components/src/ui/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,3 @@ export * from './us-state-select';
export * from './canada-province-select';
export * from './data/us-states';
export * from './data/canada-provinces';

4 changes: 2 additions & 2 deletions packages/components/src/ui/phone-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,10 @@ export const PhoneNumberInput = ({
const isNumberKey = NUMBER_KEY_REGEX.test(e.key);
const isModifier = e.ctrlKey || e.metaKey || e.altKey;
const allowed = ['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab', 'Home', 'End', 'Enter'];

// Allow typing if we have fewer than 10 digits or if we have 11 digits but the first is '1'
const isComplete = currentDigits.length >= 10 && !(currentDigits.length === 11 && currentDigits.startsWith('1'));

if (!isModifier && isNumberKey && isComplete) {
// Prevent adding more digits once 10-digit US number is complete
e.preventDefault();
Expand Down
12 changes: 2 additions & 10 deletions packages/components/src/ui/us-state-select.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,8 @@
import * as React from 'react';
import { Select, type SelectProps } from './select';
import { US_STATES } from './data/us-states';
import { Select, type SelectProps } from './select';

export type USStateSelectProps = Omit<SelectProps, 'options'>;

export function USStateSelect(props: USStateSelectProps) {
return (
<Select
options={US_STATES}
placeholder="Select a state"
{...props}
/>
);
return <Select options={US_STATES} placeholder="Select a state" {...props} />;
}

1 change: 1 addition & 0 deletions packages/components/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './scrollToError';
Loading