From 3affa544a92e407f6552317e29565f44ff7a5bce Mon Sep 17 00:00:00 2001 From: wxp <89768641@qq.com> Date: Sat, 12 Sep 2026 00:17:14 +0800 Subject: [PATCH] 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. --- .../src/components/objects/ObjectBrowser.vue | 204 ++++++++++-------- ...tBrowser.verticalScrollbarViewport.spec.ts | 41 ++++ 2 files changed, 159 insertions(+), 86 deletions(-) create mode 100644 apps/desktop/src/components/objects/__tests__/ObjectBrowser.verticalScrollbarViewport.spec.ts diff --git a/apps/desktop/src/components/objects/ObjectBrowser.vue b/apps/desktop/src/components/objects/ObjectBrowser.vue index fe56730348..64d97aa2e6 100644 --- a/apps/desktop/src/components/objects/ObjectBrowser.vue +++ b/apps/desktop/src/components/objects/ObjectBrowser.vue @@ -433,6 +433,7 @@ type ObjectBrowserScroller = // type loose because vue-virtual-scroller does not ship complete ref typings. const listScrollerRef = ref(null); const gridScrollerRef = ref(null); +const objectListHeaderRef = ref(null); let viewportFrame = 0; let restoreViewportFrame = 0; @@ -463,6 +464,7 @@ function emitViewportChange(scrollTop: number) { } function onObjectsScroll() { + syncObjectListHeaderScroll(); if (viewportFrame) return; viewportFrame = window.requestAnimationFrame(() => { viewportFrame = 0; @@ -472,6 +474,17 @@ function onObjectsScroll() { }); } +// The list header sits outside the row scroller and is clipped (overflow: +// hidden), so keep its programmatic scrollLeft aligned with the scroller's +// horizontal position on every scroll, resize, and (re)attach. +function syncObjectListHeaderScroll() { + if (!isListView) return; + const header = objectListHeaderRef.value; + const el = scrollerElement(listScrollerRef.value); + if (!header || !el) return; + if (header.scrollLeft !== el.scrollLeft) header.scrollLeft = el.scrollLeft; +} + function flushObjectBrowserViewport() { if (viewportFrame) { window.cancelAnimationFrame(viewportFrame); @@ -522,6 +535,7 @@ watch( if (!el) return; el.addEventListener("scroll", onObjectsScroll, { passive: true }); restoreObjectBrowserViewport(); + nextTick(() => syncObjectListHeaderScroll()); onCleanup(() => el.removeEventListener("scroll", onObjectsScroll)); }, { flush: "post" }, @@ -697,6 +711,14 @@ const selectedTableRows = computed(() => { const selectedTableCount = computed(() => selectedTableRows.value.length); const canBatchDropCascade = computed(() => selectedTableCount.value > 0 && supportsDropTableCascade(effectiveDatabaseType.value)); const canBatchTruncateCascade = computed(() => selectedTableCount.value > 0 && supportsTruncateTableCascade(effectiveDatabaseType.value)); + +// Column resizes change the scrollable content width, so the header's clamped +// scrollLeft has to be re-aligned right after the DOM updates. Registered here +// (after every computed it transitively reads) because watch sources are +// evaluated eagerly at registration time. +watch([objectGridMinWidth, objectColumnWidths], () => { + nextTick(() => syncObjectListHeaderScroll()); +}); const allVisibleTablesSelected = computed(() => visibleSelectableRows.value.length > 0 && visibleSelectableRows.value.every((row) => selectedTableIds.value.has(row.id))); const batchDropProgressPercent = computed(() => (batchDropProgress.value.total > 0 ? Math.round((batchDropProgress.value.completed / batchDropProgress.value.total) * 100) : 0)); @@ -3470,102 +3492,104 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
-
-
-
- -
- +
+
+
+
+ +
+ +
-
-
- -
- +
+ +
+ +
-
-
- -
- +
+ +
+ +
-
-
- -
- +
+ +
+ +
-
-
- -
- +
+ +
+ +
-
-
- -
- +
+ +
+ +
-
-
- -
- +
+ +
+ +
-
-
- -
- +
+ +
+ +
- +