mirror of
https://github.com/cozystack/cozystack.git
synced 2026-11-12 23:54:01 +09:00
fix(dashboard): anchor the focus fallback to a segment boundary
When no element carries the generated field id, the fallback matched any input whose id or name merely started with it, so an error on `data` focused the sibling `database` and scrolled to a field the user was not asked about. Match on a segment boundary instead: the separator for a nested input, the dash for the SourceField radios named `<id>-source`, which are the only names any widget emits. Assisted-by: LLM Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
This commit is contained in:
1 parent
9a59f1d0f7
commit
b4259fee03
2 files changed
+30
-4
No files matched your search
+23
@@ -60,6 +60,29 @@ describe("focusFirstError", () => {
|
||||
expect(document.activeElement).toBe(first)
|
||||
})
|
||||
|
||||
// A bare prefix match would grab any sibling whose id merely starts with the
|
||||
// same string, scrolling to a field the user was not asked about.
|
||||
it("does not fall back to a sibling that merely shares the prefix", () => {
|
||||
document.body.innerHTML = `<input id="root_database" />`
|
||||
|
||||
focusFirstError({ property: ".data" })
|
||||
|
||||
expect(document.getElementById("root_data")).toBeNull()
|
||||
expect(document.activeElement).toBe(document.body)
|
||||
})
|
||||
|
||||
it("falls back to a nested input under the errored object", () => {
|
||||
document.body.innerHTML = `
|
||||
<input id="root_database" />
|
||||
<input id="root_data_host" />
|
||||
`
|
||||
const nested = document.getElementById("root_data_host")
|
||||
|
||||
focusFirstError({ property: ".data" })
|
||||
|
||||
expect(document.activeElement).toBe(nested)
|
||||
})
|
||||
|
||||
it("does nothing when the field is nowhere in the document", () => {
|
||||
document.body.innerHTML = `<input id="root_other" />`
|
||||
|
||||
|
||||
@@ -18,8 +18,9 @@ function propertySegments(property: string): string[] {
|
||||
*
|
||||
* 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
|
||||
* input whose id or name starts with the generated id.
|
||||
* lookup finds nothing and the submit scrolls nowhere. Fall back to a
|
||||
* descendant input, matched on a segment boundary: a bare prefix would let an
|
||||
* error on `data` grab the sibling `database`, scrolling to the wrong field.
|
||||
*/
|
||||
export function focusFirstError(
|
||||
error: Pick<RJSFValidationError, "property">,
|
||||
@@ -27,11 +28,13 @@ export function focusFirstError(
|
||||
idSeparator = "_",
|
||||
) {
|
||||
const id = [idPrefix, ...propertySegments(error.property ?? "")].join(idSeparator)
|
||||
const escaped = id.replace(/["\\]/g, "\\$&")
|
||||
const quote = (v: string) => v.replace(/["\\]/g, "\\$&")
|
||||
const escaped = quote(id)
|
||||
const escapedSeparator = quote(idSeparator)
|
||||
const field =
|
||||
document.getElementById(id) ??
|
||||
document.querySelector<HTMLElement>(
|
||||
`input[id^="${escaped}"], input[name^="${escaped}"]`,
|
||||
`input[id^="${escaped}${escapedSeparator}"], input[name^="${escaped}-"]`,
|
||||
)
|
||||
field?.scrollIntoView?.({ block: "center" })
|
||||
field?.focus?.({ preventScroll: true })
|
||||
|
||||
Reference in new issue
Block a user