fix(objects): pin object list vertical scrollbar to the viewport

When the object-browser columns exceeded the panel width, the vertical
scrollbar travelled with the content: the row scroller was stretched to
the content min-width inside the horizontally scrolling table container,
so its scrollbar sat at the far-right edge of the (off-screen) content
and users had to scroll all the way right to reach it (#8885).

The list scroller now stays viewport-width and owns the horizontal
scrolling itself (content min-width moves to the virtual-scroller item
wrapper), and the header becomes a clipped wrapper whose scrollLeft is
kept in sync with the scroller on scroll, resize, and re-attach.
This commit is contained in:
wxp committed 2026-09-12 00:17:14 +08:00
1 parent 48d6c94aad
commit 3affa544a9
2 files changed
+159 -86

No files matched your search

@@ -0,0 +1,41 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const source = readFileSync(new URL("../ObjectBrowser.vue", import.meta.url), "utf8");
// Regression test for #8885: when the object-browser columns are wider than the
// panel, the vertical scrollbar must stay pinned to the visible right edge
// instead of travelling with the content. The list scroller therefore owns the
// horizontal scrolling (with the content width applied to its inner item
// wrapper), while the header is clipped and aligned programmatically.
describe("ObjectBrowser vertical scrollbar viewport lock", () => {
it("keeps the outer table container from scrolling horizontally", () => {
const tableMatch = /class="object-browser-table[^"]*"/.exec(source);
expect(tableMatch).not.toBeNull();
expect(tableMatch![0]).toContain("overflow-hidden");
expect(tableMatch![0]).not.toContain("overflow-x-auto");
});
it("moves the content min-width onto the scroller's inner wrapper", () => {
const scrollerMatch = /<RecycleScroller[^>]*listScrollerRef[^>]*>/.exec(source);
expect(scrollerMatch).not.toBeNull();
expect(scrollerMatch![0]).toContain("--dbx-object-grid-min-width");
expect(scrollerMatch![0]).not.toContain("minWidth:");
expect(source).toContain(".object-browser-scroller {");
expect(source).toContain("overflow-x: auto;");
expect(source).toContain("min-width: var(--dbx-object-grid-min-width, 0px);");
});
it("clips the header and syncs its scroll position with the scroller", () => {
const headerMatch = /ref="objectListHeaderRef"[^>]*/.exec(source);
expect(headerMatch).not.toBeNull();
expect(headerMatch![0]).toContain("overflow-hidden");
expect(source).toContain("function syncObjectListHeaderScroll(");
expect(source).toContain("header.scrollLeft = el.scrollLeft");
// The sync runs on every scroller scroll event, when the scroller (re)attaches,
// and whenever the column widths change the content width.
const scrollHandler = source.indexOf("function onObjectsScroll(");
expect(source.slice(scrollHandler, scrollHandler + 300)).toContain("syncObjectListHeaderScroll()");
expect(source).toContain("nextTick(() => syncObjectListHeaderScroll())");
});
});