Skip to content

Commit 1d4ce32

Browse files
committed
scroll forms on invalid submit
1 parent 2dc564c commit 1d4ce32

File tree

2 files changed

+61
-1
lines changed

2 files changed

+61
-1
lines changed

web/src/app/admin/assistants/AssistantEditor.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ import { SEARCH_TOOL_ID } from "@/app/chat/components/tools/constants";
9191
import TextView from "@/components/chat/TextView";
9292
import { MinimalOnyxDocument } from "@/lib/search/interfaces";
9393
import { MAX_CHARACTERS_PERSONA_DESCRIPTION } from "@/lib/constants";
94+
import { FormErrorFocus } from "@/components/FormErrorHelpers";
9495

9596
function findSearchTool(tools: ToolSnapshot[]) {
9697
return tools.find((tool) => tool.in_code_tool_id === SEARCH_TOOL_ID);
@@ -772,6 +773,7 @@ export function AssistantEditor({
772773
/>
773774
)}
774775
<Form className="w-full text-text-950 assistant-editor">
776+
<FormErrorFocus />
775777
{/* Refresh starter messages when name or description changes */}
776778
<p className="text-base font-normal text-2xl">
777779
{existingPersona ? (
@@ -1795,7 +1797,7 @@ export function AssistantEditor({
17951797
</Button>
17961798
)}
17971799
</div>
1798-
<div className="flex gap-x-2">
1800+
<div className="flex gap-x-4 items-center">
17991801
<Button
18001802
type="submit"
18011803
disabled={isSubmitting || isRequestSuccessful}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
"use client";
2+
3+
import { useEffect, useRef } from "react";
4+
import { useFormikContext } from "formik";
5+
6+
// After a submit with errors, scroll + focus the first invalid field
7+
export function FormErrorFocus() {
8+
const { submitCount, errors, isSubmitting } = useFormikContext<any>();
9+
const lastHandled = useRef(0);
10+
11+
useEffect(() => {
12+
if (isSubmitting) return;
13+
if (submitCount <= 0 || submitCount === lastHandled.current) return;
14+
15+
const keys = Object.keys(errors || {});
16+
if (keys.length === 0) return;
17+
18+
const timer = setTimeout(() => {
19+
try {
20+
let target: HTMLElement | null = null;
21+
22+
// 1) Try by id, then data-testid
23+
for (const key of keys) {
24+
target =
25+
(document.getElementById(key) as HTMLElement | null) ||
26+
(document.querySelector(
27+
`[data-testid="${key}"]`
28+
) as HTMLElement | null);
29+
if (target) break;
30+
}
31+
32+
// 2) Fallback: first element with matching name
33+
if (!target) {
34+
for (const key of keys) {
35+
const byName = document.getElementsByName(key);
36+
if (byName && byName.length > 0) {
37+
target = byName[0] as HTMLElement;
38+
break;
39+
}
40+
}
41+
}
42+
43+
if (target) {
44+
target.scrollIntoView({ behavior: "smooth", block: "center" });
45+
if (typeof (target as any).focus === "function") {
46+
(target as any).focus({ preventScroll: true });
47+
}
48+
}
49+
} finally {
50+
lastHandled.current = submitCount;
51+
}
52+
}, 0);
53+
54+
return () => clearTimeout(timer);
55+
}, [submitCount, errors, isSubmitting]);
56+
57+
return null;
58+
}

0 commit comments

Comments
 (0)