diff --git a/ui/src/ui/views/config.browser.test.ts b/ui/src/ui/views/config.browser.test.ts index 9b8f0d95490..d137617b195 100644 --- a/ui/src/ui/views/config.browser.test.ts +++ b/ui/src/ui/views/config.browser.test.ts @@ -473,6 +473,36 @@ describe("config view", () => { expect(content.scrollLeft).toBe(0); }); + it("resets config content scroll when switching from form to raw mode", async () => { + const container = document.createElement("div"); + document.body.append(container); + + try { + const renderCase = (overrides: Partial) => + render(renderConfig({ ...baseProps(), ...overrides }), container); + + renderCase({ formMode: "form" }); + + const content = queryRequired(container, ".config-content", HTMLElement); + content.scrollTop = 320; + content.scrollLeft = 18; + content.scrollTo = vi.fn(({ top, left }: { top?: number; left?: number }) => { + content.scrollTop = top ?? content.scrollTop; + content.scrollLeft = left ?? content.scrollLeft; + }) as typeof content.scrollTo; + + renderCase({ formMode: "raw" }); + await Promise.resolve(); + + expect(content["scrollTo"]).toHaveBeenCalledOnce(); + expect(content["scrollTo"]).toHaveBeenCalledWith({ top: 0, left: 0, behavior: "auto" }); + expect(content.scrollTop).toBe(0); + expect(content.scrollLeft).toBe(0); + } finally { + container.remove(); + } + }); + it("can hide the root tab for scoped settings surfaces", () => { const { container } = renderConfigView({ activeSection: "channels", diff --git a/ui/src/ui/views/config.ts b/ui/src/ui/views/config.ts index a63477fa90f..11db9b2a539 100644 --- a/ui/src/ui/views/config.ts +++ b/ui/src/ui/views/config.ts @@ -1184,6 +1184,7 @@ function createConfigEphemeralState(): ConfigEphemeralState { const cvs = createConfigEphemeralState(); let lastConfigContextKey: string | null = null; +let lastFormModeForScroll: ConfigProps["formMode"] | null = null; function resetConfigEphemeralState() { Object.assign(cvs, createConfigEphemeralState()); @@ -1222,6 +1223,7 @@ function toggleSensitivePathReveal(path: Array) { export function resetConfigViewStateForTests() { resetConfigEphemeralState(); lastConfigContextKey = null; + lastFormModeForScroll = null; } export function renderConfig(props: ConfigProps) { @@ -1237,6 +1239,31 @@ export function renderConfig(props: ConfigProps) { const rawAvailable = props.rawAvailable ?? true; const formMode = showModeToggle && rawAvailable ? props.formMode : "form"; const requestUpdate = props.onRequestUpdate ?? (() => {}); + // Scroll helper: target-based (nav clicks) with global fallback (form/raw toggle) + const resetContentScroll = (target: EventTarget | null) => { + queueMicrotask(() => { + const origin = target instanceof Element ? target : null; + const content = + origin?.closest(".config-main")?.querySelector(".config-content") ?? + globalThis.document?.querySelector(".config-content"); + if (!content) { + return; + } + if (typeof content.scrollTo === "function") { + content.scrollTo({ top: 0, left: 0, behavior: "auto" }); + return; + } + content.scrollTop = 0; + content.scrollLeft = 0; + }); + }; + + // Reset scroll position when switching between form and raw mode + if (lastFormModeForScroll !== null && lastFormModeForScroll !== formMode) { + resetContentScroll(null); + } + lastFormModeForScroll = formMode; + const currentContextKey = configContextKey(props); if (lastConfigContextKey !== currentContextKey) { resetConfigEphemeralState(); @@ -1301,24 +1328,6 @@ export function renderConfig(props: ConfigProps) { const settingsLayout = props.settingsLayout ?? "tabs"; const allCategories = [...visibleCategories, ...(otherCategory ? [otherCategory] : [])]; - const resetContentScroll = (target: EventTarget | null) => { - queueMicrotask(() => { - const origin = target instanceof Element ? target : null; - const content = origin - ?.closest(".config-main") - ?.querySelector(".config-content"); - if (!content) { - return; - } - if (typeof content.scrollTo === "function") { - content.scrollTo({ top: 0, left: 0, behavior: "auto" }); - return; - } - content.scrollTop = 0; - content.scrollLeft = 0; - }); - }; - function renderAccordionNav() { return html`