Skip to content

Commit f910d40

Browse files
committed
Refactor: enhance useScrollToErrorOnSubmit hook with TypeScript types and memoization
- Updated methods parameter in UseScrollToErrorOnSubmitOptions to use FieldValues for better type safety. - Improved memoization of scroll options to prevent unnecessary re-renders. - Adjusted condition for handling server-side validation errors on mount to ensure proper functionality.
1 parent 87daaa7 commit f910d40

File tree

2 files changed

+40
-27
lines changed

2 files changed

+40
-27
lines changed

packages/components/src/remix-hook-form/hooks/useScrollToErrorOnSubmit.ts

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useEffect, useMemo } from 'react';
2+
import type { FieldValues } from 'react-hook-form';
23
import { useRemixFormContext } from 'remix-hook-form';
34
import type { UseRemixFormReturn } from 'remix-hook-form';
45
import { type ScrollToErrorOptions, scrollToFirstError } from '../../utils/scrollToError';
@@ -8,34 +9,39 @@ export interface UseScrollToErrorOnSubmitOptions extends ScrollToErrorOptions {
89
enabled?: boolean;
910
scrollOnServerErrors?: boolean;
1011
scrollOnMount?: boolean;
11-
methods?: UseRemixFormReturn<any>; // Optional methods parameter
12+
methods?: UseRemixFormReturn<FieldValues>; // Optional methods parameter
1213
}
1314

1415
export const useScrollToErrorOnSubmit = (options: UseScrollToErrorOnSubmitOptions = {}) => {
1516
// Use provided methods or fall back to context
1617
const contextMethods = useRemixFormContext();
17-
const { methods, delay = 100, enabled = true, scrollOnServerErrors = true, scrollOnMount = true, ...scrollOptions } = options;
18+
const {
19+
methods,
20+
delay = 100,
21+
enabled = true,
22+
scrollOnServerErrors = true,
23+
scrollOnMount = true,
24+
...scrollOptions
25+
} = options;
1826
const formMethods = methods || contextMethods;
19-
20-
// Early return if no form methods are available
21-
if (!formMethods) {
22-
console.warn('useScrollToErrorOnSubmit: No form methods available. Make sure you are either inside a RemixFormProvider or passing methods explicitly.');
23-
return;
24-
}
25-
27+
2628
const { formState } = formMethods;
2729

2830
// Memoize scroll options to prevent unnecessary re-renders
31+
const { behavior, block, inline, offset, shouldFocus, retryAttempts, selectors } = scrollOptions;
32+
33+
// biome-ignore lint: Compare `selectors` by value via join to avoid unstable array identity.
2934
const memoizedScrollOptions = useMemo(
30-
() => scrollOptions,
31-
[
32-
scrollOptions.behavior,
33-
scrollOptions.block,
34-
scrollOptions.inline,
35-
scrollOptions.offset,
36-
scrollOptions.shouldFocus,
37-
scrollOptions.retryAttempts,
38-
],
35+
() => ({
36+
behavior,
37+
block,
38+
inline,
39+
offset,
40+
shouldFocus,
41+
retryAttempts,
42+
selectors,
43+
}),
44+
[behavior, block, inline, offset, shouldFocus, retryAttempts, selectors?.join(',')],
3945
);
4046

4147
// Handle form submission errors
@@ -55,7 +61,7 @@ export const useScrollToErrorOnSubmit = (options: UseScrollToErrorOnSubmitOption
5561

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

6167
if (hasErrors && !formState.isSubmitting) {

packages/components/src/utils/scrollToError.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,14 @@ export interface ScrollToErrorOptions {
77
offset?: number;
88
shouldFocus?: boolean;
99
retryAttempts?: number;
10+
selectors?: string[];
1011
}
1112

13+
const DEFAULT_ERROR_SELECTORS = [
14+
'[data-slot="form-message"]', // Target error message first (best UX)
15+
'[data-slot="form-control"][aria-invalid="true"]', // Input with error state
16+
];
17+
1218
const findFirstErrorElement = (selectors: string[]): HTMLElement | null => {
1319
for (const selector of selectors) {
1420
const element = document.querySelector(selector) as HTMLElement;
@@ -36,18 +42,19 @@ const focusElement = (element: HTMLElement, shouldFocus: boolean, behavior: Scro
3642
};
3743

3844
export const scrollToFirstError = (errors: FieldErrors, options: ScrollToErrorOptions = {}) => {
39-
const { behavior = 'smooth', offset = 80, shouldFocus = true, retryAttempts = 3 } = options;
45+
const {
46+
behavior = 'smooth',
47+
offset = 80,
48+
shouldFocus = true,
49+
retryAttempts = 3,
50+
selectors = DEFAULT_ERROR_SELECTORS,
51+
} = options;
4052

4153
if (Object.keys(errors).length === 0) return false;
4254

4355
const attemptScroll = (attempt = 0): boolean => {
44-
// Use existing data-slot selectors - no new attributes needed!
45-
const selectors = [
46-
'[data-slot="form-message"]', // Target error message first (best UX)
47-
'[data-slot="form-control"][aria-invalid="true"]', // Input with error state
48-
];
49-
50-
const element = findFirstErrorElement(selectors);
56+
const selectorList = selectors.length > 0 ? selectors : DEFAULT_ERROR_SELECTORS;
57+
const element = findFirstErrorElement(selectorList);
5158
if (element) {
5259
scrollToElement(element, offset, behavior);
5360
focusElement(element, shouldFocus, behavior);

0 commit comments

Comments
 (0)