Skip to content

fix: sync table scroll by hover source#1495

Draft
zombieJ wants to merge 1 commit into
masterfrom
codex/virtual-scroll-hover-source
Draft

fix: sync table scroll by hover source#1495
zombieJ wants to merge 1 commit into
masterfrom
codex/virtual-scroll-hover-source

Conversation

@zombieJ

@zombieJ zombieJ commented Jul 2, 2026

Copy link
Copy Markdown
Member

Fixes ant-design/ant-design#57850

Summary

  • track the hovered scroll source across table body, fixed header, fixed summary, and sticky scrollbar
  • pass scroll source metadata through internal scroll callbacks
  • ignore scroll callbacks from non-hovered sources so synced scroll events do not steal the 100ms scroll lock
  • allow the hovered source to drive sync even when a previous source is still locked

Test Plan

  • npm test -- tests/Virtual.spec.tsx
  • npm run tsc
  • npm run lint
  • npm test

@vercel

vercel Bot commented Jul 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
table Ready Ready Preview, Comment Jul 2, 2026 7:35am

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 04214502-9163-4d78-bf68-0aae5648b265

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/virtual-scroll-hover-source

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

React Doctor found 27 issues in 4 files · 27 warnings · score 68 / 100 (Needs work) · vs master

27 warnings

src/FixedHolder/index.tsx

  • ⚠️ L31 Missing effect dependencies exhaustive-deps
  • ⚠️ L31 Missing effect dependencies exhaustive-deps
  • ⚠️ L102 Missing effect dependencies exhaustive-deps
  • ⚠️ L130 Non-passive scroll listener client-passive-event-listeners
  • ⚠️ L149 Missing effect dependencies exhaustive-deps
  • ⚠️ L154 Missing effect dependencies exhaustive-deps

src/Table.tsx

  • ⚠️ L215 Large component is hard to read and change no-giant-component
  • ⚠️ L218 Many related useState calls prefer-useReducer
  • ⚠️ L223 Event logic handled in an effect no-event-handler
  • ⚠️ L415 Missing effect dependencies exhaustive-deps
  • ⚠️ L415 Missing effect dependencies exhaustive-deps
  • ⚠️ L477 Pure function rebuilt every render prefer-module-scope-pure-function
  • ⚠️ L599 Missing effect dependencies exhaustive-deps
  • ⚠️ L607 Derived value copied into state no-derived-state
  • ⚠️ L609 Missing effect dependencies exhaustive-deps
  • ⚠️ L625 Missing effect dependencies exhaustive-deps
  • ⚠️ L671 Missing effect dependencies exhaustive-deps
  • ⚠️ L703 Duplicate data in server props server-dedup-props
  • ⚠️ L795 JSX element passed as a prop jsx-no-jsx-as-prop
  • ⚠️ L813 JSX element passed as a prop jsx-no-jsx-as-prop
  • ⚠️ L1051 Non-component export in component file only-export-components

src/VirtualTable/BodyGrid.tsx

  • ⚠️ L89 Missing effect dependencies exhaustive-deps
  • ⚠️ L230 Array index used as a key no-array-index-as-key

src/stickyScrollBar.tsx

  • ⚠️ L144 Missing effect dependencies exhaustive-deps
  • ⚠️ L170 Missing effect dependencies exhaustive-deps
  • ⚠️ L186 Missing effect dependencies exhaustive-deps
  • ⚠️ L198 Interaction on static element no-static-element-interactions

Reviewed by React Doctor for commit 142ef68. See inline comments for fixes.

Comment thread src/Table.tsx
@@ -747,6 +793,8 @@ const Table = <RecordType extends DefaultRecordType>(
className={`${prefixCls}-header`}
ref={scrollHeaderRef}
colGroup={bodyColGroup}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React Doctor · react-doctor/jsx-no-jsx-as-prop (warning)

This child redraws every render because the prop gets brand new JSX each time.

Fix → Move the JSX outside the component or wrap it in useMemo so memoized children do not redraw every render.

Docs

Comment thread src/Table.tsx
@@ -763,6 +811,8 @@ const Table = <RecordType extends DefaultRecordType>(
className={`${prefixCls}-summary`}
ref={scrollSummaryRef}
colGroup={bodyColGroup}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React Doctor · react-doctor/jsx-no-jsx-as-prop (warning)

This child redraws every render because the prop gets brand new JSX each time.

Fix → Move the JSX outside the component or wrap it in useMemo so memoized children do not redraw every render.

Docs

Comment thread src/stickyScrollBar.tsx
className={`${prefixCls}-sticky-scroll`}
onMouseEnter={onMouseEnter}
>
<div

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React Doctor · react-doctor/no-static-element-interactions (warning)

Screen reader users can't tell this click handler is interactive because it has no role, so add a role or use a button or link.

Fix → Give clickable static elements a role, or use a button or link.

Docs

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

✅ Preview is ready!

PR preview ✅ Ready ✅ Ready
🔗 Preview https://react-component-table-preview-pr-1495.surge.sh
📝 Commit142ef68
⏱️ Build time28.407s
📦 Size2.7 MB · 227 files
🪵 LogsView logs
📱 MobileScan to open preview on mobile

↩️ Previous: ⚡️ 142ef68 · react-component-table-preview-pr-1495.surge.sh (open ↗) · 2026-07-02 07:35:56 UTC

🤖 Powered by surge-preview

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a mechanism to track the active scroll source (header, body, summary, or sticky scrollbar) using hover states (onMouseEnter) to improve scroll synchronization across different table sections. It also includes corresponding unit tests. The review feedback highlights two critical issues: first, a potential bug in getScrollSource where undefined or null values could incorrectly match and resolve the scroll source; second, the lack of a reset mechanism for hoverEle when the mouse leaves the table, which could break scroll synchronization during keyboard navigation.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/Table.tsx
Comment on lines +95 to +105
const getScrollSource = (
source: ScrollSource | undefined,
currentTarget: HTMLElement | undefined,
refs: Record<ScrollSource, HTMLElement | null | undefined>,
): ScrollSource | undefined => {
if (source) {
return source;
}

return (Object.keys(refs) as ScrollSource[]).find(key => refs[key] === currentTarget);
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

In getScrollSource, if currentTarget is undefined or null, and any reference in refs is also undefined or null (e.g., refs.sticky is explicitly undefined, or refs.summary is null when no summary is rendered), the comparison refs[key] === currentTarget will evaluate to true.

This will incorrectly resolve the scroll source to that key (e.g., 'sticky' or 'summary'). To prevent this, we should ensure currentTarget is truthy before performing the comparison.

Suggested change
const getScrollSource = (
source: ScrollSource | undefined,
currentTarget: HTMLElement | undefined,
refs: Record<ScrollSource, HTMLElement | null | undefined>,
): ScrollSource | undefined => {
if (source) {
return source;
}
return (Object.keys(refs) as ScrollSource[]).find(key => refs[key] === currentTarget);
};
const getScrollSource = (
source: ScrollSource | undefined,
currentTarget: HTMLElement | undefined,
refs: Record<ScrollSource, HTMLElement | null | undefined>,
): ScrollSource | undefined => {
if (source) {
return source;
}
return (Object.keys(refs) as ScrollSource[]).find(
key => currentTarget && refs[key] === currentTarget,
);
};

Comment thread src/Table.tsx
Comment on lines +366 to +372
const [hoverEle, setHoverEle] = React.useState<ScrollSource | null>(null);
const onHoverScrollSource = React.useCallback(
(source: ScrollSource) => () => {
setHoverEle(source);
},
[],
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

hoverEle is set to the hovered scroll source on onMouseEnter, but it is never reset to null when the mouse leaves the table container.

If a user hovers the header (setting hoverEle to 'header'), moves the mouse out of the table, and then uses keyboard navigation (e.g., Arrow keys) to scroll the table body, the scroll event will be triggered with source: 'body'. Since hoverEle is still locked to 'header', the condition hoverEle !== scrollSource will be true, causing the scroll sync to be ignored and breaking synchronization.

To fix this, we should reset hoverEle to null when the mouse leaves the table container by listening to the mouseleave event on fullTableRef.current.

  const [hoverEle, setHoverEle] = React.useState<ScrollSource | null>(null);
  const onHoverScrollSource = React.useCallback(
    (source: ScrollSource) => () => {
      setHoverEle(source);
    },
    [],
  );

  React.useEffect(() => {
    const tableEl = fullTableRef.current;
    const handleMouseLeave = () => {
      setHoverEle(null);
    };
    tableEl?.addEventListener('mouseleave', handleMouseLeave);
    return () => {
      tableEl?.removeEventListener('mouseleave', handleMouseLeave);
    };
  }, []);

@codecov

codecov Bot commented Jul 2, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.20%. Comparing base (14b74e6) to head (142ef68).

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1495      +/-   ##
==========================================
+ Coverage   99.04%   99.20%   +0.15%     
==========================================
  Files          45       45              
  Lines        1362     1377      +15     
  Branches      409      412       +3     
==========================================
+ Hits         1349     1366      +17     
+ Misses         13       11       -2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant