From 5c7a644a71dc6f40d9eb7a557cb57c429faef994 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Tue, 14 Jul 2026 14:55:07 -0700 Subject: [PATCH] fix table columns and scroll breaking on resize/reorder/pin --- .../hooks/use-table-pane-sync.ts | 77 ++++++++++++++----- 1 file changed, 59 insertions(+), 18 deletions(-) diff --git a/src/renderer/components/item-list/item-table-list/hooks/use-table-pane-sync.ts b/src/renderer/components/item-list/item-table-list/hooks/use-table-pane-sync.ts index 79103ed88..b00e75b3c 100644 --- a/src/renderer/components/item-list/item-table-list/hooks/use-table-pane-sync.ts +++ b/src/renderer/components/item-list/item-table-list/hooks/use-table-pane-sync.ts @@ -3,7 +3,7 @@ import type { TableScrollShadowStore } from '/@/renderer/components/item-list/it import { autoScrollForElements } from '@atlaskit/pragmatic-drag-and-drop-auto-scroll/element'; import throttle from 'lodash/throttle'; import { useOverlayScrollbars } from 'overlayscrollbars-react'; -import { useEffect } from 'react'; +import { useEffect, useRef } from 'react'; import { ItemListStateActions } from '/@/renderer/components/item-list/helpers/item-list-state'; @@ -60,19 +60,36 @@ export const useTablePaneSync = ({ scrollShadowStore: TableScrollShadowStore; scrollSyncKey: string; }) => { + const pinnedRightColumnCountRef = useRef(pinnedRightColumnCount); + pinnedRightColumnCountRef.current = pinnedRightColumnCount; + + // When right-pinned columns exist, OverlayScrollbars is configured with y:'hidden' + // so only the right pane shows a vertical scrollbar. OS may later apply + // overflowYVisible + overflowImportant, which makes the main viewport + // overflow-y:visible and breaks scrollTop sync. Force auto with !important + // on init/update so the main pane remains vertically scroll-syncable. + const applyMainViewportOverflow = (viewport: HTMLElement) => { + viewport.style.overflowX = `var(--os-viewport-overflow-x)`; + + if (pinnedRightColumnCountRef.current > 0) { + viewport.style.setProperty('overflow-y', 'auto', 'important'); + } else { + viewport.style.removeProperty('overflow-y'); + viewport.style.overflowY = `var(--os-viewport-overflow-y)`; + } + }; + // Main grid overlayscrollbars - only handle X-axis if right-pinned columns exist const [initialize, osInstance] = useOverlayScrollbars({ defer: false, events: { initialized(osInstance) { const { viewport } = osInstance.elements(); - viewport.style.overflowX = `var(--os-viewport-overflow-x)`; - - if (pinnedRightColumnCount > 0) { - viewport.style.overflowY = 'auto'; - } else { - viewport.style.overflowY = `var(--os-viewport-overflow-y)`; - } + applyMainViewportOverflow(viewport); + }, + updated(osInstance) { + const { viewport } = osInstance.elements(); + applyMainViewportOverflow(viewport); }, }, options: { @@ -166,7 +183,6 @@ export const useTablePaneSync = ({ osInstance, pinnedRightColumnCount, scrollContainerRef, - scrollSyncKey, ]); useEffect(() => { @@ -258,7 +274,6 @@ export const useTablePaneSync = ({ osInstanceRightPinned, pinnedRightColumnCount, pinnedRightColumnRef, - scrollSyncKey, ]); useEffect(() => { @@ -274,21 +289,37 @@ export const useTablePaneSync = ({ rowRef, }); + const ensureMainRowAcceptsScrollTop = (row: HTMLDivElement) => { + if (pinnedRightColumnCount <= 0) { + return; + } + + // Keep the main pane syncable even if OverlayScrollbars measuring flips + // overflow-y back to visible after a column layout change. + row.style.setProperty('overflow-y', 'auto', 'important'); + applyMainViewportOverflow(row); + }; + const isVerticalScrollHostReady = ( row: HTMLDivElement, pinnedRight: HTMLDivElement | undefined, ) => { - const verticalScrollHost = pinnedRightColumnCount > 0 ? pinnedRight : row; - - if (!verticalScrollHost) { - return false; - } - if (pinnedRightColumnCount > 0) { - return verticalScrollHost.scrollHeight > verticalScrollHost.clientHeight; + if (!pinnedRight) { + return false; + } + + // Right pane is the visible scrollbar host, but the main pane must also + // accept programmatic scrollTop sync before listeners are attached. + const rowOverflowY = getComputedStyle(row).overflowY; + return ( + pinnedRight.scrollHeight > pinnedRight.clientHeight && + row.scrollHeight > row.clientHeight && + rowOverflowY !== 'visible' + ); } - return verticalScrollHost.scrollHeight > 0; + return row.scrollHeight > 0; }; const setupScrollSync = () => { @@ -300,6 +331,10 @@ export const useTablePaneSync = ({ const { header, pinnedLeft, pinnedRight, row } = resolvePaneRefs(); + if (row) { + ensureMainRowAcceptsScrollTop(row); + } + if (!row || !isVerticalScrollHostReady(row, pinnedRight)) { setupFrameId = requestAnimationFrame(setupScrollSync); return; @@ -315,6 +350,8 @@ export const useTablePaneSync = ({ return; } + ensureMainRowAcceptsScrollTop(syncRow); + const rowHeight = syncRow.scrollHeight; let targetHeight = rowHeight; @@ -463,8 +500,12 @@ export const useTablePaneSync = ({ } if (syncPinnedRight && currentElement === syncPinnedRight && !isScrolling.row) { + ensureMainRowAcceptsScrollTop(syncRow); isScrolling.row = true; syncRow.scrollTo({ behavior: 'instant', top: scrollTop }); + if (syncRow.scrollTop !== scrollTop) { + syncRow.scrollTop = scrollTop; + } isScrolling.row = false; if (syncPinnedLeft) { isScrolling.pinnedLeft = true;