-
Notifications
You must be signed in to change notification settings - Fork 0
feat: implement scroll-to-error functionality for @lambdacurry/forms #140
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
2bd7d49
f9fcaf6
06f8435
0a24c56
693054d
6ef7938
ce180af
8fc6ada
ddee63e
87daaa7
43aa58b
62fd07f
b5bc11d
7b9ac09
899440b
31ef9b5
b16fb55
23623f1
b8fe440
08a6a15
c3a2f81
c4b7d5f
6aebf2f
79f9e62
14eb20f
187a97d
caf12ac
241c976
44e4540
f910d40
869dad8
a62e451
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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(); | ||
| }; | ||
|
|
| 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'; |
| 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]); | ||
|
|
||
| // 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]); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π οΈ Refactor suggestion π§© Analysis chainVerification helper: check for other wrappers with prop order pitfalls. Run to spot any π 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/srcLength 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 ?? 'β¦'}.
π€ Prompt for AI Agents
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
| }; | ||
| 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" />; | ||
| } | ||
|
|
| 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} />; | ||
| } | ||
|
|
| 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} />; | ||
| } | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export * from './scrollToError'; |
Uh oh!
There was an error while loading. Please reload this page.