From 48f28fd59aa06cdf1be52e903a5467a37bd4f1ae Mon Sep 17 00:00:00 2001 From: VernSG Date: Thu, 21 May 2026 03:13:10 +0800 Subject: [PATCH 1/2] fix(web-ui): update settings sidebar active state at scroll bottom --- .../runtime-settings-dialog.test.tsx | 65 +++++++++++++++++++ .../components/runtime-settings-dialog.tsx | 7 ++ 2 files changed, 72 insertions(+) diff --git a/web-ui/src/components/runtime-settings-dialog.test.tsx b/web-ui/src/components/runtime-settings-dialog.test.tsx index 9b9d6b471..13e1b4166 100644 --- a/web-ui/src/components/runtime-settings-dialog.test.tsx +++ b/web-ui/src/components/runtime-settings-dialog.test.tsx @@ -166,6 +166,13 @@ function findButtonByAriaLabel(container: ParentNode, ariaLabel: string): HTMLBu ) ?? null) as HTMLButtonElement | null; } +function setScrollMetric(element: HTMLElement, property: "clientHeight" | "scrollHeight" | "scrollTop", value: number) { + Object.defineProperty(element, property, { + value, + configurable: true, + }); +} + const savedClineOauthConfig = { selectedAgentId: "cline", selectedShortcutLabel: null, @@ -282,6 +289,64 @@ describe("RuntimeSettingsDialog", () => { expect(resetLayoutCustomizationsMock).toHaveBeenCalledTimes(1); }); + it("activates the final settings nav item when the dialog is scrolled to the bottom", async () => { + await act(async () => { + root.render( + {}} + />, + ); + }); + + const projectMarker = document.querySelector('[data-settings-section="project"]'); + const scrollBody = projectMarker?.parentElement; + if (!projectMarker || !(scrollBody instanceof HTMLElement)) { + throw new Error("Expected the Project settings section to render inside a scrollable body."); + } + + setScrollMetric(scrollBody, "scrollHeight", 1000); + setScrollMetric(scrollBody, "clientHeight", 400); + setScrollMetric(scrollBody, "scrollTop", 600); + scrollBody.getBoundingClientRect = () => + ({ + top: 100, + bottom: 500, + left: 0, + right: 600, + width: 600, + height: 400, + x: 0, + y: 100, + toJSON: () => ({}), + }) satisfies DOMRect; + + for (const marker of Array.from(scrollBody.querySelectorAll("[data-settings-section]"))) { + const markerTop = marker === projectMarker ? 170 : 120; + marker.getBoundingClientRect = () => + ({ + top: markerTop, + bottom: markerTop, + left: 0, + right: 0, + width: 0, + height: 0, + x: 0, + y: markerTop, + toJSON: () => ({}), + }) satisfies DOMRect; + } + + await act(async () => { + scrollBody.dispatchEvent(new Event("scroll", { bubbles: true })); + }); + + const projectNavButton = findButtonByText(document.body, "Project"); + expect(projectNavButton?.className).toContain("bg-surface-3"); + }); + it("enables save on theme change and reverts preview on cancel", async () => { const handleOpenChange = vi.fn(); await act(async () => { diff --git a/web-ui/src/components/runtime-settings-dialog.tsx b/web-ui/src/components/runtime-settings-dialog.tsx index 314e80c68..006cec7a8 100644 --- a/web-ui/src/components/runtime-settings-dialog.tsx +++ b/web-ui/src/components/runtime-settings-dialog.tsx @@ -600,6 +600,7 @@ export function RuntimeSettingsDialog({ if (!body) return; const headings = body.querySelectorAll("[data-settings-section]"); const bodyRect = body.getBoundingClientRect(); + const maxScrollTop = Math.max(0, body.scrollHeight - body.clientHeight); let current: SettingsNavId = "general"; for (const heading of headings) { @@ -610,6 +611,12 @@ export function RuntimeSettingsDialog({ } } + if (maxScrollTop > 0 && body.scrollTop >= maxScrollTop - 2) { + const lastHeading = headings.item(headings.length - 1); + const id = lastHeading?.getAttribute("data-settings-section"); + if (id) current = id as SettingsNavId; + } + setActiveSection(current); }, []); From a808eadd1f2abf3d244bf8e88c17c72b9844365f Mon Sep 17 00:00:00 2001 From: VernSG Date: Thu, 21 May 2026 03:23:29 +0800 Subject: [PATCH 2/2] fix(web-ui): name settings scroll bottom tolerance --- web-ui/src/components/runtime-settings-dialog.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web-ui/src/components/runtime-settings-dialog.tsx b/web-ui/src/components/runtime-settings-dialog.tsx index 006cec7a8..e393a4cb0 100644 --- a/web-ui/src/components/runtime-settings-dialog.tsx +++ b/web-ui/src/components/runtime-settings-dialog.tsx @@ -110,6 +110,8 @@ const SETTINGS_NAV_ITEMS: ReadonlyArray<{ { id: "project", label: "Project", icon: }, ]; +const SETTINGS_SCROLL_BOTTOM_TOLERANCE_PX = 2; + function getShortcutIconOption(icon: string | undefined): RuntimeShortcutIconOption { return getRuntimeShortcutPickerOption(icon); } @@ -611,7 +613,7 @@ export function RuntimeSettingsDialog({ } } - if (maxScrollTop > 0 && body.scrollTop >= maxScrollTop - 2) { + if (maxScrollTop > 0 && body.scrollTop >= maxScrollTop - SETTINGS_SCROLL_BOTTOM_TOLERANCE_PX) { const lastHeading = headings.item(headings.length - 1); const id = lastHeading?.getAttribute("data-settings-section"); if (id) current = id as SettingsNavId;