diff --git a/packages/system/dashboard/images/console/apps/console/src/lib/focus-first-error.test.ts b/packages/system/dashboard/images/console/apps/console/src/lib/focus-first-error.test.ts index a12dabb0e..8ce315cbe 100644 --- a/packages/system/dashboard/images/console/apps/console/src/lib/focus-first-error.test.ts +++ b/packages/system/dashboard/images/console/apps/console/src/lib/focus-first-error.test.ts @@ -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 = `` + + 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 = ` + + + ` + 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 = `` diff --git a/packages/system/dashboard/images/console/apps/console/src/lib/focus-first-error.ts b/packages/system/dashboard/images/console/apps/console/src/lib/focus-first-error.ts index 794024a0f..91b137515 100644 --- a/packages/system/dashboard/images/console/apps/console/src/lib/focus-first-error.ts +++ b/packages/system/dashboard/images/console/apps/console/src/lib/focus-first-error.ts @@ -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="-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, @@ -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( - `input[id^="${escaped}"], input[name^="${escaped}"]`, + `input[id^="${escaped}${escapedSeparator}"], input[name^="${escaped}-"]`, ) field?.scrollIntoView?.({ block: "center" }) field?.focus?.({ preventScroll: true })