mirror of
https://github.com/cozystack/cozystack.git
synced 2026-11-13 08:05:19 +09:00
fix(dashboard): focus the offending field when a submit is blocked
The app form validates on submit with the error list hidden, so a required field left empty made Save look like a no-op: the error rendered somewhere off screen. Pass focusOnFirstError. RJSF's built-in handler resolves the field through form.elements, which the tagName="div" form does not have, so resolve it by generated id instead. Fixes #3135 Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
This commit is contained in:
1 parent
2da6594466
commit
242786f3de
4 files changed
+89
No files matched your search
+10
@@ -198,6 +198,16 @@ describe("SchemaForm validate handle", () => {
|
||||
expect(validate(ref)).toBe(false)
|
||||
})
|
||||
|
||||
it("focuses the first offending field so a blocked submit is not silent", () => {
|
||||
const ref = createRef<SchemaFormHandle>()
|
||||
render(
|
||||
<SchemaForm ref={ref} openAPISchema={requiredSchema} formData={{}} onChange={noop} />,
|
||||
)
|
||||
|
||||
validate(ref)
|
||||
expect(document.activeElement).toBe(document.getElementById("root_name"))
|
||||
})
|
||||
|
||||
it("reports valid through the ref once the required field is populated", () => {
|
||||
const ref = createRef<SchemaFormHandle>()
|
||||
render(
|
||||
|
||||
@@ -4,6 +4,7 @@ import validator from "@rjsf/validator-ajv8"
|
||||
import { getDefaultFormState } from "@rjsf/utils"
|
||||
import type { RJSFSchema, UiSchema, TemplatesType } from "@rjsf/utils"
|
||||
import { keysOrderToUiSchema, sanitizeSchema } from "../lib/keys-order.ts"
|
||||
import { focusFirstError } from "../lib/focus-first-error.ts"
|
||||
import { addSensitiveStringWidgets } from "../lib/sensitive-fields.ts"
|
||||
import {
|
||||
IMMUTABLE_HELP_TEXT,
|
||||
@@ -352,6 +353,7 @@ export const SchemaForm = forwardRef<SchemaFormHandle, SchemaFormProps>(function
|
||||
onChange={(e) => onChange(e.formData)}
|
||||
liveValidate={false}
|
||||
showErrorList={false}
|
||||
focusOnFirstError={focusFirstError}
|
||||
>
|
||||
{children}
|
||||
</Form>
|
||||
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
import { describe, it, expect, beforeEach } from "vitest"
|
||||
import { focusFirstError } from "./focus-first-error.ts"
|
||||
|
||||
describe("focusFirstError", () => {
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = ""
|
||||
})
|
||||
|
||||
it("focuses the field carrying the generated id", () => {
|
||||
document.body.innerHTML = `<input id="root_name" />`
|
||||
const field = document.getElementById("root_name")
|
||||
|
||||
focusFirstError({ property: ".name" })
|
||||
|
||||
expect(document.activeElement).toBe(field)
|
||||
})
|
||||
|
||||
it("resolves a bracketed property path", () => {
|
||||
document.body.innerHTML = `<input id="root_spec_first-name" />`
|
||||
const field = document.getElementById("root_spec_first-name")
|
||||
|
||||
focusFirstError({ property: ".spec['first-name']" })
|
||||
|
||||
expect(document.activeElement).toBe(field)
|
||||
})
|
||||
|
||||
// SourceField and SourceWidget render no element carrying the generated id,
|
||||
// only radios named `<id>-source`, so the exact lookup misses and a blocked
|
||||
// submit used to scroll nowhere.
|
||||
it("falls back to the name attribute when no element carries the id", () => {
|
||||
document.body.innerHTML = `
|
||||
<div class="field">
|
||||
<input type="radio" name="root_source-source" value="http" />
|
||||
<input type="radio" name="root_source-source" value="pvc" />
|
||||
</div>
|
||||
`
|
||||
const first = document.querySelector<HTMLInputElement>('input[value="http"]')
|
||||
|
||||
focusFirstError({ property: ".source" })
|
||||
|
||||
expect(document.getElementById("root_source")).toBeNull()
|
||||
expect(document.activeElement).toBe(first)
|
||||
})
|
||||
|
||||
it("does nothing when the field is nowhere in the document", () => {
|
||||
document.body.innerHTML = `<input id="root_other" />`
|
||||
|
||||
expect(() => focusFirstError({ property: ".missing" })).not.toThrow()
|
||||
expect(document.activeElement).toBe(document.body)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { RJSFValidationError } from "@rjsf/utils"
|
||||
|
||||
/**
|
||||
* Errors render inline with the error list hidden, so a blocked submit is
|
||||
* invisible unless the offending field is brought into view. RJSF's built-in
|
||||
* focus resolves the field through `form.elements`, which the `tagName="div"`
|
||||
* form does not have, so resolve it by generated id instead.
|
||||
*
|
||||
* Not every widget puts that id on an element. `SourceField` and
|
||||
* `SourceWidget` render radios carrying only `name="<id>-source"`, so an exact
|
||||
* lookup finds nothing and the submit scrolls nowhere. Fall back to the first
|
||||
* element whose id or name starts with the generated id.
|
||||
*/
|
||||
export function focusFirstError(error: Pick<RJSFValidationError, "property">) {
|
||||
const segments = (error.property ?? "")
|
||||
.replace(/\['?([^'\]]+)'?\]/g, ".$1")
|
||||
.split(".")
|
||||
.filter(Boolean)
|
||||
const id = ["root", ...segments].join("_")
|
||||
const escaped = id.replace(/["\\]/g, "\\$&")
|
||||
const field =
|
||||
document.getElementById(id) ??
|
||||
document.querySelector<HTMLElement>(`[id^="${escaped}"], [name^="${escaped}"]`)
|
||||
field?.scrollIntoView?.({ block: "center" })
|
||||
field?.focus?.({ preventScroll: true })
|
||||
}
|
||||
Reference in new issue
Block a user