Skip to content

Commit 529dbaf

Browse files
committed
fix: sync table header scroll with body on horizontal scroll
Closes #4633 AntD's virtual Table (rc-table with virtual prop) syncs the header's horizontal scroll position with the body internally. However, the sync can drift after: - column visibility changes (columns added/removed) - resizable-column width updates that change scroll.x - other re-renders that reset the header's scrollLeft When drift occurs, the header columns appear misaligned with the body columns whenever the user scrolls horizontally. Fix: add a passive 'scroll' listener on .ant-table-body in InfiniteVirtualTableInner that writes body.scrollLeft → header.scrollLeft on every scroll tick. The effect re-attaches whenever finalColumns or scrollConfig.x changes (the conditions under which drift can occur). In the normal (no-drift) path AntD has already run its own sync handler, so setting the same value is a no-op. When drift has occurred, this listener corrects it on the very next scroll event without any visible glitch.
1 parent 399602a commit 529dbaf

1 file changed

Lines changed: 30 additions & 0 deletions

File tree

web/oss/src/components/InfiniteVirtualTable/components/InfiniteVirtualTableInner.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,36 @@ const InfiniteVirtualTableInnerBase = <RecordType extends object>({
446446
tableHeaderHeight,
447447
])
448448

449+
// Sync .ant-table-header scroll position with .ant-table-body on every horizontal scroll.
450+
//
451+
// AntD's virtual Table syncs header/body scroll internally, but it can lose sync after
452+
// column visibility changes, resizes, or scroll-config updates that trigger a re-render.
453+
// We attach our own passive scroll listener as a safety net: when it fires, the header is
454+
// already correct (AntD's handler ran first), so this is a no-op in the happy path.
455+
// When AntD's sync breaks, our listener corrects the header on the very next scroll tick.
456+
useEffect(() => {
457+
const container = containerRef.current
458+
if (!container) return
459+
460+
const body = container.querySelector<HTMLElement>(".ant-table-body")
461+
const header = container.querySelector<HTMLElement>(".ant-table-header")
462+
if (!body || !header) return
463+
464+
const sync = () => {
465+
if (header.scrollLeft !== body.scrollLeft) {
466+
header.scrollLeft = body.scrollLeft
467+
}
468+
}
469+
470+
body.addEventListener("scroll", sync, {passive: true})
471+
// Correct any drift that happened during the re-render that triggered this effect
472+
sync()
473+
474+
return () => {
475+
body.removeEventListener("scroll", sync)
476+
}
477+
}, [finalColumns, scrollConfig.x])
478+
449479
// Memoize dependencies object to prevent unnecessary useEffect runs in useScrollContainer
450480
// Without memoization, a new object is created every render, causing infinite loops during scroll
451481
const scrollContainerDeps = useMemo(

0 commit comments

Comments
 (0)