You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(form): scroll to first invalid field on validation error
useForm scrolls the first [aria-invalid="true"] into view after a 422, matching the per-field scroll the app layer previously wired by hand. The primitive useValidationErrors stays DOM-free; scrolling lives in the opinionated useForm and is opt-out via {scrollToError: false}.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The two source territories diverged on exactly one axis: one camelCased the error keys, the other used them raw. `keyMapper` (default identity) is the single injection point that absorbs that divergence, so the package fits both without forking.
72
72
:::
73
73
74
+
## Scroll to the First Error
75
+
76
+
On a 422, `useForm` scrolls the first invalid field into view so the user lands on the first thing to fix — it targets the first `[aria-invalid="true"]` element (the marker the presentation layer sets from the error bag) and calls `scrollIntoView({behavior: 'smooth', block: 'center'})` after the mark is painted. `useForm` derives no ids and marks no fields itself.
77
+
78
+
```typescript
79
+
useForm<Field>(http); // scrolls on error (default)
80
+
useForm<Field>(http, {scrollToError: false}); // opt out
81
+
```
82
+
83
+
By default the query is **document-wide** — the first `[aria-invalid="true"]` in document order — which is right for a single form. When several forms share a page, pass each form's root as `scrollRoot` so a 422 in one never scrolls to another's field:
84
+
85
+
```typescript
86
+
const formEl =ref<HTMLElement|null>(null);
87
+
useForm<Field>(http, {scrollRoot: formEl}); // scopes the scroll to formEl's subtree
88
+
```
89
+
90
+
`scrollRoot` keeps a form's scroll within its own subtree, but it does **not** isolate forms that share one `HttpService`: a 422 fills every such form's error bag (see [Scoping & Backend Contract](#scoping--backend-contract) below), so a co-mounted form still scrolls to its _own_ matching field on an unrelated submit. Give concurrently-mounted forms separate `HttpService` instances to avoid that.
91
+
92
+
`useValidationErrors` never scrolls (the DOM-free primitive). A consumer that already scrolls on error should opt out with `scrollToError: false` to avoid a double scroll.
93
+
74
94
## Composing the Primitives
75
95
76
96
`useForm` is `useValidationErrors` + `useFormSubmit` wired together. Reach for the primitives directly when you want one half without the other — e.g. a validation-less confirm action needs the submit guard but no 422 middleware:
|`httpService`|`HttpService`| The `fs-http` service whose 422 responses to observe |
132
+
|`options.keyMapper`|`(key: string) => string`| Remaps raw backend field keys (default: identity) |
133
+
|`options.scrollToError`|`boolean`| Scroll the first invalid field into view on a 422 (default: `true`; see [Scroll to the First Error](#scroll-to-the-first-error)) |
134
+
|`options.scrollRoot`|`Ref<HTMLElement \| null>`| Scope the `scrollToError` query to a form's subtree; omit for document-wide (see [Scroll to the First Error](#scroll-to-the-first-error)) |
0 commit comments