fix: UI glitch: config is not visible (#96145)

Summary:
- The branch tracks effective Settings Config Form/Raw mode, resets `.config-content` scroll when that mode changes, and adds a browser regression test for the retained-scroll transition.
- PR surface: Source +9, Tests +30. Total +39 across 2 files.
- Reproducibility: yes. at source level: current main resets `.config-content` for section navigation but not  ... ro in this read-only pass, but the source PR includes after-fix browser proof for the same branch behavior.

Automerge notes:
- No ClawSweeper repair was needed after automerge opt-in.

Validation:
- ClawSweeper review passed for head a6ea91e6ed.
- Required merge gates passed before the squash merge.

Prepared head SHA: a6ea91e6ed
Review: https://github.com/openclaw/openclaw/pull/96145#issuecomment-4784983447

Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: sunlit-deng <253064511+sunlit-deng@users.noreply.github.com>
Approved-by: takhoffman
This commit is contained in:
clawsweeper[bot]
2026-06-24 01:18:03 +00:00
committed by GitHub
parent 4d034639ad
commit 63874fa0d1
2 changed files with 57 additions and 18 deletions

View File

@@ -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<ConfigProps>) =>
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",

View File

@@ -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<string | number>) {
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<HTMLElement>(".config-content") ??
globalThis.document?.querySelector<HTMLElement>(".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<HTMLElement>(".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`
<div class="config-accordion-nav">