-
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
Merged
jaruesink
merged 32 commits into
main
from
codegen/lc-321-complete-implementation-guide-adding-scroll-to-error-to
Sep 18, 2025
Merged
Changes from all commits
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] f9fcaf6
feat: add comprehensive Storybook examples for scroll-to-error functiβ¦
codegen-sh[bot] 06f8435
Fix RadioGroupItem import in scroll-to-error story
codegen-sh[bot] 0a24c56
fix: allow passing methods to useScrollToErrorOnSubmit hook
codegen-sh[bot] 693054d
Fix scroll-to-error story context issue
codegen-sh[bot] 6ef7938
Fix scroll-to-error hook null reference error
codegen-sh[bot] ce180af
Fix ReferenceError: methods is not defined
codegen-sh[bot] 8fc6ada
Fix methods reference in ScrollToErrorHookForm component
codegen-sh[bot] ddee63e
Refactor: streamline scroll-to-error story and remove unused test cases
jaruesink 87daaa7
Merge pull request #141 from lambda-curry/codegen/lc-322-lets-create-β¦
jaruesink 43aa58b
Fix: adjust spacing in RadioGroupItemField label for better a11y
lcmohsen 62fd07f
chore: bump @lambdacurry/forms to v0.19.6
codegen-sh[bot] b5bc11d
Improve select dropdown width matching with ResizeObserver
codegen-sh[bot] 7b9ac09
Add 500px width test story for select dropdown
codegen-sh[bot] 899440b
Fix ResizeObserver compatibility for test environments
codegen-sh[bot] 31ef9b5
Fix TypeScript error in ResizeObserver implementation
codegen-sh[bot] b16fb55
Fix ResizeObserver timing issues causing test failures
codegen-sh[bot] 23623f1
Fix dropdown width by removing hardcoded w-72 class
codegen-sh[bot] b8fe440
Add width debugging and ensure inner content takes full width
codegen-sh[bot] 08a6a15
Replace ResizeObserver with Radix CSS custom property
codegen-sh[bot] c3a2f81
Remove failing test story and cleanup test components
codegen-sh[bot] c4b7d5f
Remove unnecessary blank lines in select stories
jaruesink 6aebf2f
feat: add keyboard navigation to Select component
codegen-sh[bot] 79f9e62
Fix keyboard navigation timing issue in Select component
codegen-sh[bot] 14eb20f
fix: Improve activeIndex initialization in Select component
codegen-sh[bot] 187a97d
fix: Implement keyboard navigation for Select component
codegen-sh[bot] caf12ac
chore: remove changeset, update package version, and remove failing tβ¦
codegen-sh[bot] 241c976
chore: bump @lambdacurry/forms to v0.19.7
codegen-sh[bot] 44e4540
chore: bump @lambdacurry/forms to v0.20.0
codegen-sh[bot] f910d40
Refactor: enhance useScrollToErrorOnSubmit hook with TypeScript typesβ¦
jaruesink 869dad8
Merge branch 'codegen/lc-321-complete-implementation-guide-adding-scrβ¦
jaruesink a62e451
Merge branch 'main' into codegen/lc-321-complete-implementation-guideβ¦
jaruesink File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
642 changes: 642 additions & 0 deletions
642
apps/docs/src/remix-hook-form/scroll-to-error.stories.tsx
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
14 changes: 14 additions & 0 deletions
14
packages/components/src/remix-hook-form/components/ScrollToErrorOnSubmit.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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'; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export * from './ScrollToErrorOnSubmit'; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export * from './useScrollToErrorOnSubmit'; |
83 changes: 83 additions & 0 deletions
83
packages/components/src/remix-hook-form/hooks/useScrollToErrorOnSubmit.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| import { useEffect, useMemo } from 'react'; | ||
| import type { FieldValues } from 'react-hook-form'; | ||
| import { useRemixFormContext } from 'remix-hook-form'; | ||
| import type { UseRemixFormReturn } from 'remix-hook-form'; | ||
| import { type ScrollToErrorOptions, scrollToFirstError } from '../../utils/scrollToError'; | ||
|
|
||
| export interface UseScrollToErrorOnSubmitOptions extends ScrollToErrorOptions { | ||
| delay?: number; | ||
| enabled?: boolean; | ||
| scrollOnServerErrors?: boolean; | ||
| scrollOnMount?: boolean; | ||
| methods?: UseRemixFormReturn<FieldValues>; // Optional methods parameter | ||
| } | ||
|
|
||
| export const useScrollToErrorOnSubmit = (options: UseScrollToErrorOnSubmitOptions = {}) => { | ||
| // Use provided methods or fall back to context | ||
| const contextMethods = useRemixFormContext(); | ||
| const { | ||
| methods, | ||
| delay = 100, | ||
| enabled = true, | ||
| scrollOnServerErrors = true, | ||
| scrollOnMount = true, | ||
| ...scrollOptions | ||
| } = options; | ||
| const formMethods = methods || contextMethods; | ||
|
|
||
| const { formState } = formMethods; | ||
|
|
||
| // Memoize scroll options to prevent unnecessary re-renders | ||
| const { behavior, block, inline, offset, shouldFocus, retryAttempts, selectors } = scrollOptions; | ||
|
|
||
| // biome-ignore lint: Compare `selectors` by value via join to avoid unstable array identity. | ||
| const memoizedScrollOptions = useMemo( | ||
| () => ({ | ||
| behavior, | ||
| block, | ||
| inline, | ||
| offset, | ||
| shouldFocus, | ||
| retryAttempts, | ||
| selectors, | ||
| }), | ||
| [behavior, block, inline, offset, shouldFocus, retryAttempts, selectors?.join(',')], | ||
| ); | ||
|
|
||
| // 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, | ||
| ]); | ||
| }; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export * from './scrollToError'; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| import type { FieldErrors } from 'react-hook-form'; | ||
|
|
||
| export interface ScrollToErrorOptions { | ||
| behavior?: ScrollBehavior; | ||
| block?: ScrollLogicalPosition; | ||
| inline?: ScrollLogicalPosition; | ||
| offset?: number; | ||
| shouldFocus?: boolean; | ||
| retryAttempts?: number; | ||
| selectors?: string[]; | ||
| } | ||
|
|
||
| const DEFAULT_ERROR_SELECTORS = [ | ||
| '[data-slot="form-message"]', // Target error message first (best UX) | ||
| '[data-slot="form-control"][aria-invalid="true"]', // Input with error state | ||
| ]; | ||
|
|
||
| const findFirstErrorElement = (selectors: string[]): HTMLElement | null => { | ||
| for (const selector of selectors) { | ||
| const element = document.querySelector(selector) as HTMLElement; | ||
| if (element) { | ||
| return element; | ||
| } | ||
| } | ||
| return null; | ||
| }; | ||
|
|
||
| const scrollToElement = (element: HTMLElement, offset: number, behavior: ScrollBehavior): void => { | ||
| const elementRect = element.getBoundingClientRect(); | ||
| const offsetTop = elementRect.top + window.pageYOffset - offset; | ||
|
|
||
| window.scrollTo({ | ||
| top: Math.max(0, offsetTop), | ||
| behavior, | ||
| }); | ||
| }; | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| const focusElement = (element: HTMLElement, shouldFocus: boolean, behavior: ScrollBehavior): void => { | ||
| if (shouldFocus && element.focus) { | ||
| setTimeout(() => element.focus(), behavior === 'smooth' ? 300 : 0); | ||
| } | ||
| }; | ||
|
|
||
| export const scrollToFirstError = (errors: FieldErrors, options: ScrollToErrorOptions = {}) => { | ||
| const { | ||
| behavior = 'smooth', | ||
| offset = 80, | ||
| shouldFocus = true, | ||
| retryAttempts = 3, | ||
| selectors = DEFAULT_ERROR_SELECTORS, | ||
| } = options; | ||
|
|
||
| if (Object.keys(errors).length === 0) return false; | ||
|
|
||
| const attemptScroll = (attempt = 0): boolean => { | ||
| const selectorList = selectors.length > 0 ? selectors : DEFAULT_ERROR_SELECTORS; | ||
| const element = findFirstErrorElement(selectorList); | ||
| if (element) { | ||
| scrollToElement(element, offset, behavior); | ||
| focusElement(element, shouldFocus, behavior); | ||
| return true; | ||
| } | ||
|
|
||
| // Retry for async rendering (common with Remix) | ||
| if (attempt < retryAttempts) { | ||
| setTimeout(() => attemptScroll(attempt + 1), 100); | ||
| return true; | ||
| } | ||
|
|
||
| console.warn('Could not find any form error elements to scroll to'); | ||
| return false; | ||
| }; | ||
|
|
||
| return attemptScroll(); | ||
| }; | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.