Skip to content

feat: scrollTo add align support#1469

Open
EmilyyyLiu wants to merge 4 commits intoreact-component:masterfrom
EmilyyyLiu:liuh-scroll
Open

feat: scrollTo add align support#1469
EmilyyyLiu wants to merge 4 commits intoreact-component:masterfrom
EmilyyyLiu:liuh-scroll

Conversation

@EmilyyyLiu
Copy link
Copy Markdown

@EmilyyyLiu EmilyyyLiu commented Mar 31, 2026

Summary

为 Table 组件的 scrollTo 方法增加 align 参数支持,类似于虚拟列表中的对齐方式。

Feature

  • align: 'start' - 滚动到顶部
  • align: 'center' - 滚动到中间
  • align: 'end' - 滚动到底部
  • align: 'nearest' - 智能滚动(默认)

Related Issue

ant-design/ant-design#45945

Usage

tableRef.current?.scrollTo({
  index: 9,
  align: 'start', // 'center' | 'end' | 'nearest'
});

也可以结合 offset 使用:

tableRef.current?.scrollTo({
  index: 9,
  align: 'center',
  offset: 10,
});

Summary by CodeRabbit

发版说明

  • 新功能

    • Table 组件 scrollTo 支持可选 alignstart/center/end/nearest),用于更精确的滚动对齐行为。
  • 文档

    • README 增加 scrollTo 方法说明与用法示例,更新了 ScrollConfig 参数说明以包含 align
  • 示例

    • 新示例按钮演示以不同 align 值滚动表格(start/center/end)。
  • 测试

    • scrollToalign 行为与带 offset 的滚动计算添加了测试覆盖。

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 31, 2026

Someone is attempting to deploy a commit to the React Component Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 31, 2026

Walkthrough

为 Table 组件的公开方法 scrollTo 添加了可选参数 alignstart | center | end | nearest,默认 nearest),并在文档、示例、类型定义、实现和测试中同步更新滚动对齐与带 offset 时的目标位置计算逻辑。

Changes

Cohort / File(s) Summary
文档与示例
README.md, docs/examples/scrollY.tsx
在 README 中记录 scrollTo 新增 align 参数及允许值;在示例中新增按钮调用 `tblRef.current?.scrollTo({ key: 9, align: 'start'
类型定义
src/interface.ts
ScrollConfig 类型中新增可选属性 align?: ScrollLogicalPosition,并调整 offset 的说明以反映与 align 的组合行为。
核心实现
src/Table.tsx
扩展 scrollTo Imperative Handle,新增 align 参数(默认 'nearest');无 offset 时使用 element.scrollIntoView({ block: align });有 offset 时根据 alignstart/center/end/nearest)计算目标 top 并调用 container.scrollTo({ top })。同时对 TableProps 类型格式化改动(无签名变更)。
测试覆盖
tests/refs.spec.tsx
新增测试覆盖 scrollTo 在不同 align 值下的行为:验证默认走 scrollIntoViewalign 为 `start

Sequence Diagram(s)

sequenceDiagram
    participant Button
    participant TableRef
    participant Container as ScrollContainer
    participant Element as TargetElement

    Button->>TableRef: invoke scrollTo({ key/index/top, offset?, align? })
    TableRef->>Container: resolve target element (by key/index or DOM query)
    alt offset not provided
        TableRef->>Element: Element.scrollIntoView({ block: align })
    else offset provided
        TableRef->>Container: compute targetTop based on element.offsetTop, offsetHeight, clientHeight and align
        TableRef->>Container: Container.scrollTo({ top: targetTop + offset })
    end
Loading

Estimated code review effort

🎯 3 (中等) | ⏱️ ~20 分钟

Possibly related PRs

Suggested reviewers

  • zombieJ
  • afc163

功能概览

向 Table 组件的 scrollTo 方法添加了 align 参数支持,允许控制滚动对齐方式(startcenterendnearest),并更新了相应的类型定义、实现、文档、示例和测试用例。

变更明细

内聚组件 / 文件 摘要
文档与示例
README.md, docs/examples/scrollY.tsx
记录 scrollTo 方法新增的 align 参数及其允许值;示例新增按钮演示使用 align: 'start' | 'center' | 'end' 的滚动行为。
类型定义
src/interface.ts
ScrollConfig 类型中新增可选属性 align?: ScrollLogicalPosition,并调整 offset 注释以反映与 align 的组合行为。
核心实现
src/Table.tsx
扩展 scrollTo 的逻辑处理:当不提供 offset 时根据 align 值调用 scrollIntoView({ block: align });当提供 offset 时基于 align 值(startendcenternearest)计算目标 top 后执行 scrollTo。此外对类型声明格式做了小幅重排。
测试覆盖
tests/refs.spec.tsx
新增测试用例验证 scrollTo 在不同 align 值下的行为,包括单独使用 align 时调用 scrollIntoView,以及 align 结合 offset 时的 top 计算逻辑。

预估代码审查工作量

🎯 3 (中等) | ⏱️ ~20 分钟

可能相关的 PR

  • feat: Table scrollTo support offset #1301:同样修改了 Table 组件的 scrollTo 实现和 ScrollConfig 处理,该 PR 添加了 offset 支持,本 PR 在其基础上扩展了 align 参数并基于 align 计算目标位置。

建议审查人员

  • afc163
  • zombieJ

我是小兔去瞧瞧,表格滚动有新法,
start 抵顶,end 抵底,center 居中不慌张,
nearest 就近最机灵,offset 搭配算位置,
鼠标一按跳一跳,表格行列都有光! 🐰✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title "feat: scrollTo add align support" clearly and accurately summarizes the main change: adding align parameter support to the scrollTo method.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

Copy link
Copy Markdown
Contributor

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

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 the align parameter to the scrollTo method of the Table component, allowing users to specify vertical alignment (start, center, end, nearest) when scrolling to a specific row. The changes include updates to the ScrollConfig interface, implementation logic in src/Table.tsx to handle alignment both with and without offsets, new documentation in the README, and comprehensive unit tests. Review feedback suggests translating README descriptions to English for consistency, updating outdated comments in the interface definition, and refining the alignMap type for better type safety and behavioral consistency with the documentation.

EmilyyyLiu and others added 3 commits March 31, 2026 10:29
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/interface.ts (1)

39-47: ⚠️ Potential issue | 🟡 Minor

offset 的类型注释已经和新行为冲突了。

这里还写着“设置 offset 后总是顶对齐”,但本次改动已经允许 align 参与定位。这个类型是公开 API,IDE 悬浮提示会直接误导调用方;建议把说明改成“offset 会叠加到按 align 计算出的目标位置”,或者如果只打算保留顶对齐,就把运行时逻辑收回到同一约束。

✏️ 建议同步注释
   /**
    * Additional offset in pixels to apply to the scroll position.
    * Only effective when using `key` or `index` mode.
    * Ignored when using `top` mode.
-   * When offset is set, the target element will always be aligned to the top of the container.
+   * In `key` / `index` mode, `offset` is added to the position resolved by `align`.
    */
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/interface.ts` around lines 39 - 47, The JSDoc for the offset property is
out-of-date: update the comment for offset (the offset?: number; declaration) to
state that offset is added to the target position computed from align
(ScrollLogicalPosition) rather than forcing top alignment; mention that offset
is effective when using key/index modes and will be applied on top of the align
calculation (or alternatively revert runtime behavior to force top alignment if
you prefer keeping the old doc), and ensure the phrase "always aligned to the
top" is removed so IDE hover shows the correct public API behavior for offset
and align.
src/Table.tsx (1)

354-380: ⚠️ Potential issue | 🟠 Major

align: 'nearest' 在 offset 分支里实际上没有被支持。

Line 354 先把 align 默认成了 'nearest',Line 377 又把 'nearest' 直接映射成 elementTop。这样 scrollTo({ index, offset, align: 'nearest' }) 和旧的 scrollTo({ index, offset }) 完全等价,都会退化成顶对齐,而不是“最小滚动”。如果这里是想兼容老行为,建议保留 align 的原始 undefined 状态:未显式传入时继续走旧逻辑;显式传入 'nearest' 时再按当前 scrollTop / elementBottom 计算真正的 nearest。

💡 一种兼容旧行为的修正方式
-          const { index, top, key, offset, align = 'nearest' } = config;
+          const { index, top, key, offset, align } = config;

           if (validNumberValue(top)) {
             // In top mode, offset is ignored
             scrollBodyRef.current?.scrollTo({ top });
           } else {
             const mergedKey = key ?? getRowKey(mergedData[index]);
             const targetElement = scrollBodyRef.current.querySelector(
               `[data-row-key="${mergedKey}"]`,
             );
             if (targetElement) {
               if (!offset) {
-                targetElement.scrollIntoView({ block: align });
+                targetElement.scrollIntoView({ block: align ?? 'nearest' });
               } else {
                 const container = scrollBodyRef.current;
+                const currentTop = container.scrollTop;
                 const elementTop = (targetElement as HTMLElement).offsetTop;
                 const elementHeight = (targetElement as HTMLElement).offsetHeight;
+                const elementBottom = elementTop + elementHeight;
                 const containerHeight = container.clientHeight;
+                const containerBottom = currentTop + containerHeight;
+                const mergedAlign = align ?? 'start';

-                const alignMap: Record<string, number> = {
-                  start: elementTop,
-                  end: elementTop + elementHeight - containerHeight,
-                  center: elementTop + (elementHeight - containerHeight) / 2,
-                  nearest: elementTop,
-                };
-                const targetTop = alignMap[align] ?? elementTop;
+                let targetTop = elementTop;
+                switch (mergedAlign) {
+                  case 'end':
+                    targetTop = elementBottom - containerHeight;
+                    break;
+                  case 'center':
+                    targetTop = elementTop - (containerHeight - elementHeight) / 2;
+                    break;
+                  case 'nearest':
+                    if (elementTop < currentTop) {
+                      targetTop = elementTop;
+                    } else if (elementBottom > containerBottom) {
+                      targetTop = elementBottom - containerHeight;
+                    } else {
+                      targetTop = currentTop;
+                    }
+                    break;
+                  default:
+                    targetTop = elementTop;
+                }
                 container.scrollTo({ top: targetTop + offset });
               }
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Table.tsx` around lines 354 - 380, The current logic defaults align to
'nearest' and then treats 'nearest' as elementTop, which breaks true nearest
behavior and conflates explicit nearest with the old undefined case; change the
destructuring so align defaults to undefined (const { index, top, key, offset,
align } = config), preserve legacy behavior when align is undefined (keep
existing top-based calculation used by scrollTo({index, offset}) ), and
implement real "nearest" handling inside the offset branch: compute
container.scrollTop, elementTop and elementBottom, then choose the smallest
scroll delta (top, center, or end) to produce the nearest targetTop when align
=== 'nearest'; update the alignMap/selection in the code that uses
scrollBodyRef, mergedData, getRowKey and targetElement accordingly.
🧹 Nitpick comments (1)
tests/refs.spec.tsx (1)

114-186: 这组测试还区分不出 align 是否真的生效。

现在的 mock 只记录了 scrollIntoView 的目标元素,没有记录 { block } 参数;而且 JSDOM 下 offsetTopoffsetHeightclientHeight 默认都是 0,所以 startcenterendnearest 在这里基本会退化成同一种结果。实现就算把 align 忽略掉,这些用例大概率也还能通过。

🧪 可参考的补强方向
 let scrollParam: any = null;
 let scrollIntoViewElement: HTMLElement = null;
+let scrollIntoViewParam: ScrollIntoViewOptions | undefined;

   beforeAll(() => {
     spyElementPrototypes(HTMLElement, {
       scrollTo: (_: any, param: any) => {
         scrollParam = param;
       },
-      scrollIntoView() {
+      scrollIntoView(param?: ScrollIntoViewOptions) {
         // eslint-disable-next-line `@typescript-eslint/no-this-alias`
         scrollIntoViewElement = this;
+        scrollIntoViewParam = param;
       },
     });
   });

   beforeEach(() => {
     scrollParam = null;
     scrollIntoViewElement = null;
+    scrollIntoViewParam = undefined;
   });

   // Align start - should use scrollIntoView
   scrollIntoViewElement = null;
   ref.current.scrollTo({ index: 0, align: 'start' });
   expect(scrollIntoViewElement.textContent).toEqual('light');
+  expect(scrollIntoViewParam).toMatchObject({ block: 'start' });

align + offset 这组再给 row / container 手动补一组不同的几何值后,才能真正把 startcenterendnearest 的计算分开。

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/interface.ts`:
- Around line 39-47: The JSDoc for the offset property is out-of-date: update
the comment for offset (the offset?: number; declaration) to state that offset
is added to the target position computed from align (ScrollLogicalPosition)
rather than forcing top alignment; mention that offset is effective when using
key/index modes and will be applied on top of the align calculation (or
alternatively revert runtime behavior to force top alignment if you prefer
keeping the old doc), and ensure the phrase "always aligned to the top" is
removed so IDE hover shows the correct public API behavior for offset and align.

In `@src/Table.tsx`:
- Around line 354-380: The current logic defaults align to 'nearest' and then
treats 'nearest' as elementTop, which breaks true nearest behavior and conflates
explicit nearest with the old undefined case; change the destructuring so align
defaults to undefined (const { index, top, key, offset, align } = config),
preserve legacy behavior when align is undefined (keep existing top-based
calculation used by scrollTo({index, offset}) ), and implement real "nearest"
handling inside the offset branch: compute container.scrollTop, elementTop and
elementBottom, then choose the smallest scroll delta (top, center, or end) to
produce the nearest targetTop when align === 'nearest'; update the
alignMap/selection in the code that uses scrollBodyRef, mergedData, getRowKey
and targetElement accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ae9a085b-6c5c-49f6-88fe-5afb49c27bb0

📥 Commits

Reviewing files that changed from the base of the PR and between 45f97df and b4bc3f7.

⛔ Files ignored due to path filters (4)
  • tests/__snapshots__/ExpandRow.spec.jsx.snap is excluded by !**/*.snap
  • tests/__snapshots__/FixedColumn.spec.tsx.snap is excluded by !**/*.snap
  • tests/__snapshots__/Summary.spec.tsx.snap is excluded by !**/*.snap
  • tests/__snapshots__/Table.spec.jsx.snap is excluded by !**/*.snap
📒 Files selected for processing (5)
  • README.md
  • docs/examples/scrollY.tsx
  • src/Table.tsx
  • src/interface.ts
  • tests/refs.spec.tsx

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/interface.ts`:
- Around line 43-47: The public ScrollConfig.align option is being ignored in
the virtual path: update the virtual scroller code (the BodyGrid virtual branch
that handles offset/align in the scrollTo logic) to respect the
ScrollConfig.align value instead of forcibly setting align = 'top'; ensure the
scrollTo call (and any helper that computes target offset) reads the external
align and offset fields from ScrollConfig and computes the final scroll position
accordingly so scrollTo({ index, align: 'center', offset: 10 }) behaves as
documented. Reference symbols: ScrollConfig, align, offset, scrollTo, BodyGrid
(virtual branch).

In `@src/Table.tsx`:
- Around line 365-380: The current offset-branch mapping in Table.tsx maps align
'nearest' to elementTop causing unnecessary jumps; change the 'nearest' case in
the alignMap (or replace alignMap with conditional logic) to compute the minimal
scroll needed: read container.scrollTop and containerHeight, elementTop and
elementBottom (= elementTop + elementHeight), and if elementTop < scrollTop set
targetTop = elementTop, else if elementBottom > scrollTop + containerHeight set
targetTop = elementBottom - containerHeight, else set targetTop = scrollTop (no
change); then use container.scrollTo({ top: targetTop + offset }) so 'nearest'
only scrolls when the row is outside the viewport.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f7624530-43b1-4628-a4fc-e07079cf3fde

📥 Commits

Reviewing files that changed from the base of the PR and between b4bc3f7 and 1027f17.

📒 Files selected for processing (3)
  • README.md
  • src/Table.tsx
  • src/interface.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • README.md

Comment on lines +43 to +47
* When offset is set, the target element will be aligned according to the `align` parameter.
*/
offset?: number;

align?: ScrollLogicalPosition;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

align 的公共契约还没有覆盖到 virtual 路径。

这里已经把 align 暴露成 ScrollConfig 的公共字段,但 src/VirtualTable/BodyGrid.tsx:82-91offset 分支仍然会把它覆盖成 'top'。这样在 virtual 表格里,scrollTo({ index, align: 'center', offset: 10 }) 依然会按顶部对齐,和这次新增 API 的语义不一致。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/interface.ts` around lines 43 - 47, The public ScrollConfig.align option
is being ignored in the virtual path: update the virtual scroller code (the
BodyGrid virtual branch that handles offset/align in the scrollTo logic) to
respect the ScrollConfig.align value instead of forcibly setting align = 'top';
ensure the scrollTo call (and any helper that computes target offset) reads the
external align and offset fields from ScrollConfig and computes the final scroll
position accordingly so scrollTo({ index, align: 'center', offset: 10 }) behaves
as documented. Reference symbols: ScrollConfig, align, offset, scrollTo,
BodyGrid (virtual branch).

Comment on lines 365 to +380
if (!offset) {
// No offset, use scrollIntoView for default behavior
targetElement.scrollIntoView();
targetElement.scrollIntoView({ block: align });
} else {
// With offset, use element's offsetTop + offset
const container = scrollBodyRef.current;
const elementTop = (targetElement as HTMLElement).offsetTop;
scrollBodyRef.current.scrollTo({ top: elementTop + offset });
const elementHeight = (targetElement as HTMLElement).offsetHeight;
const containerHeight = container.clientHeight;

const alignMap: Record<ScrollLogicalPosition, number> = {
start: elementTop,
end: elementTop + elementHeight - containerHeight,
center: elementTop + (elementHeight - containerHeight) / 2,
nearest: elementTop,
};
const targetTop = alignMap[align] ?? elementTop;
container.scrollTo({ top: targetTop + offset });
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

nearestoffset 分支里退化成了强制顶对齐。

Line 377 把 nearest 映射成了 elementTop,所以 scrollTo({ index, offset }) 或显式传 align: 'nearest' 时,会无条件跳到目标行顶部附近,而不是“仅在需要时滚到最近可见位置”。这和这次新增的默认语义不一致,也会让本来已经可见的行发生意外跳动。

💡 可行修正
               } else {
                 const container = scrollBodyRef.current;
                 const elementTop = (targetElement as HTMLElement).offsetTop;
                 const elementHeight = (targetElement as HTMLElement).offsetHeight;
                 const containerHeight = container.clientHeight;
+                const currentTop = container.scrollTop;
+                const elementBottom = elementTop + elementHeight;
+                const viewportBottom = currentTop + containerHeight;
 
-                const alignMap: Record<ScrollLogicalPosition, number> = {
+                const alignMap: Record<Exclude<ScrollLogicalPosition, 'nearest'>, number> = {
                   start: elementTop,
                   end: elementTop + elementHeight - containerHeight,
                   center: elementTop + (elementHeight - containerHeight) / 2,
-                  nearest: elementTop,
                 };
-                const targetTop = alignMap[align] ?? elementTop;
+                const targetTop =
+                  align === 'nearest'
+                    ? elementTop < currentTop
+                      ? elementTop
+                      : elementBottom > viewportBottom
+                        ? elementBottom - containerHeight
+                        : currentTop
+                    : alignMap[align];
                 container.scrollTo({ top: targetTop + offset });
               }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Table.tsx` around lines 365 - 380, The current offset-branch mapping in
Table.tsx maps align 'nearest' to elementTop causing unnecessary jumps; change
the 'nearest' case in the alignMap (or replace alignMap with conditional logic)
to compute the minimal scroll needed: read container.scrollTop and
containerHeight, elementTop and elementBottom (= elementTop + elementHeight),
and if elementTop < scrollTop set targetTop = elementTop, else if elementBottom
> scrollTop + containerHeight set targetTop = elementBottom - containerHeight,
else set targetTop = scrollTop (no change); then use container.scrollTo({ top:
targetTop + offset }) so 'nearest' only scrolls when the row is outside the
viewport.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant