Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions web-ui/src/components/runtime-settings-dialog.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(
<RuntimeSettingsDialog
open={true}
workspaceId={"workspace-1"}
initialConfig={savedClineOauthConfig}
onOpenChange={() => {}}
/>,
);
});

const projectMarker = document.querySelector<HTMLElement>('[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<HTMLElement>("[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 () => {
Expand Down
9 changes: 9 additions & 0 deletions web-ui/src/components/runtime-settings-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ const SETTINGS_NAV_ITEMS: ReadonlyArray<{
{ id: "project", label: "Project", icon: <FolderOpen size={16} /> },
];

const SETTINGS_SCROLL_BOTTOM_TOLERANCE_PX = 2;

function getShortcutIconOption(icon: string | undefined): RuntimeShortcutIconOption {
return getRuntimeShortcutPickerOption(icon);
}
Expand Down Expand Up @@ -600,6 +602,7 @@ export function RuntimeSettingsDialog({
if (!body) return;
const headings = body.querySelectorAll<HTMLElement>("[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) {
Expand All @@ -610,6 +613,12 @@ export function RuntimeSettingsDialog({
}
}

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;
}

setActiveSection(current);
}, []);

Expand Down
Loading