Skip to content

Commit d22a20d

Browse files
EmilyyyLiu刘欢gemini-code-assist[bot]claude
authored
feat: scrollTo add align support (#1469)
* feat: scrollTo add align * Update README.md Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Update src/Table.tsx Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Update src/interface.ts Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * fix: virtual table scrollTo align support - Add VirtualScrollConfig type with Exclude<ScrollLogicalPosition, 'center'> - Implement align mapping for virtual table (start->top, end->bottom, nearest->auto) - Add default align 'auto' when neither align nor offset provided - Add backward compatibility: default to 'top' when offset provided but align not - Update tests to cover align parameter scenarios - Fix offset JSDoc comment Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: implement real nearest behavior with offset in Table scrollTo - Fix nearest align to compute position after offset, then determine if scrolling is needed - Add align test buttons to scrollY and virtual demo files - Update center calculation formula Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: improve virtual table align handling Use nullish coalescing for safer align fallback Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: add test for nearest align with element above viewport Adds test coverage for the scrollTo nearest alignment when the target element is above the current viewport after applying offset. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: unify align handling with switch statement Move nearest logic into switch for consistent code style. Also extract static ALIGN_MAP constant in BodyGrid.tsx. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: refine scrollIntoView with offset handling - Simplify scrollY demo buttons - Update README to note virtual table doesn't support center align - Refactor offset logic: call scrollIntoView first, then apply offset via scrollTo 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: simplify offset handling - scrollIntoView first then add offset 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: 刘欢 <lh01217311@antgroup.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 45f97df commit d22a20d

File tree

12 files changed

+311
-153
lines changed

12 files changed

+311
-153
lines changed

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,25 @@ React.render(<Table columns={columns} data={data} />, mountNode);
119119
| summary | (data: readonly RecordType[]) => React.ReactNode | - | `summary` attribute in `table` component is used to define the summary row. |
120120
| rowHoverable | boolean | true | Table hover interaction |
121121

122+
### Methods
123+
124+
#### scrollTo
125+
126+
Table component exposes `scrollTo` method to scroll to a specific position:
127+
128+
```js
129+
const tblRef = useRef();
130+
tblRef.current?.scrollTo({ key: 'rowKey', align: 'start' });
131+
```
132+
133+
| Name | Type | Default | Description |
134+
| --- | --- | --- | --- |
135+
| index | number | - | Row index to scroll to |
136+
| top | number | - | Scroll to specific top position (in px) |
137+
| key | string | - | Scroll to row by row key |
138+
| offset | number | - | Additional offset from target position |
139+
| align | `start` \| `center` \| `end` \| `nearest` | `nearest` | Alignment of the target element within the scroll container. `start` aligns to top, `center` to middle, `end` to bottom, `nearest` automatically chooses the closest alignment. Note: Virtual table does not support `center`. |
140+
122141
## Column Props
123142
124143
| Name | Type | Default | Description |

docs/examples/scrollY.tsx

Lines changed: 25 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -41,52 +41,35 @@ const Test = () => {
4141
return (
4242
<div>
4343
<h2>scroll body table</h2>
44-
<button
45-
onClick={() => {
46-
tblRef.current?.scrollTo({
47-
top: 9999,
48-
});
49-
}}
50-
>
51-
Scroll To End
44+
<button onClick={() => tblRef.current?.scrollTo({ top: 0 })}>Top</button>
45+
<button onClick={() => tblRef.current?.scrollTo({ top: 9999 })}>End</button>
46+
<button onClick={() => tblRef.current?.scrollTo({ key: 9 })}>Key 9</button>
47+
<button onClick={() => tblRef.current?.scrollTo({ key: 9, align: 'start' })}>
48+
Key 9 align: start
5249
</button>
53-
<button
54-
onClick={() => {
55-
tblRef.current?.scrollTo({
56-
key: 9,
57-
});
58-
}}
59-
>
60-
Scroll To key 9
50+
<button onClick={() => tblRef.current?.scrollTo({ key: 9, align: 'center' })}>
51+
Key 9 align: center
6152
</button>
62-
<button
63-
onClick={() => {
64-
tblRef.current?.scrollTo({
65-
top: 0,
66-
});
67-
}}
68-
>
69-
Scroll To top
53+
<button onClick={() => tblRef.current?.scrollTo({ key: 9, align: 'end' })}>
54+
Key 9 align: end
7055
</button>
71-
<button
72-
onClick={() => {
73-
tblRef.current?.scrollTo({
74-
index: 5,
75-
offset: -10,
76-
});
77-
}}
78-
>
79-
Scroll To Index 5 + Offset -10
56+
<button onClick={() => tblRef.current?.scrollTo({ key: 9, align: 'nearest' })}>
57+
Key 9 align: nearest
8058
</button>
81-
<button
82-
onClick={() => {
83-
tblRef.current?.scrollTo({
84-
key: 6,
85-
offset: -10,
86-
});
87-
}}
88-
>
89-
Scroll To Key 6 + Offset -10
59+
<button onClick={() => tblRef.current?.scrollTo({ key: 9, align: 'start', offset: 20 })}>
60+
Key 9 start offset20
61+
</button>
62+
<button onClick={() => tblRef.current?.scrollTo({ key: 9, align: 'center', offset: 20 })}>
63+
Key 9 center offset20
64+
</button>
65+
<button onClick={() => tblRef.current?.scrollTo({ key: 9, align: 'end', offset: 20 })}>
66+
Key 9 end offset20
67+
</button>
68+
<button onClick={() => tblRef.current?.scrollTo({ key: 9, align: 'nearest', offset: 20 })}>
69+
Key 9 nearest offset20
70+
</button>
71+
<button onClick={() => tblRef.current?.scrollTo({ index: 5, offset: 50 })}>
72+
Index 5 + Offset 50
9073
</button>
9174
<Table
9275
ref={tblRef}

docs/examples/virtual.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,21 @@ const Demo: React.FC = () => {
205205
<button onClick={() => tableRef.current?.scrollTo({ key: '50', offset: -10 })}>
206206
Scroll To Key 50 + Offset -10
207207
</button>
208+
<button onClick={() => tableRef.current?.scrollTo({ index: 500, align: 'start' })}>
209+
index 500 + align start
210+
</button>
211+
<button onClick={() => tableRef.current?.scrollTo({ index: 500, align: 'end' })}>
212+
index 500 + align end
213+
</button>
214+
<button onClick={() => tableRef.current?.scrollTo({ index: 500, align: 'nearest' })}>
215+
index 500 + align nearest
216+
</button>
217+
<button onClick={() => tableRef.current?.scrollTo({ index: 500, offset: 50 })}>
218+
index 500 + offset 50
219+
</button>
220+
<button onClick={() => tableRef.current?.scrollTo({ index: 500, offset: 50, align: 'end' })}>
221+
index 500 + offset 50 + align end
222+
</button>
208223
<VirtualTable
209224
style={{ marginTop: 16 }}
210225
ref={tableRef}

src/Table.tsx

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,10 @@ const EMPTY_SCROLL_TARGET = {};
8888
export type SemanticName = 'section' | 'title' | 'footer' | 'content';
8989
export type ComponentsSemantic = 'wrapper' | 'cell' | 'row';
9090

91-
export interface TableProps<RecordType = any>
92-
extends Omit<LegacyExpandableProps<RecordType>, 'showExpandColumn'> {
91+
export interface TableProps<RecordType = any> extends Omit<
92+
LegacyExpandableProps<RecordType>,
93+
'showExpandColumn'
94+
> {
9395
prefixCls?: string;
9496
className?: string;
9597
style?: React.CSSProperties;
@@ -349,7 +351,7 @@ const Table = <RecordType extends DefaultRecordType>(
349351
scrollTo: config => {
350352
if (scrollBodyRef.current instanceof HTMLElement) {
351353
// Native scroll
352-
const { index, top, key, offset } = config;
354+
const { index, top, key, offset, align = 'nearest' } = config;
353355

354356
if (validNumberValue(top)) {
355357
// In top mode, offset is ignored
@@ -360,13 +362,10 @@ const Table = <RecordType extends DefaultRecordType>(
360362
`[data-row-key="${mergedKey}"]`,
361363
);
362364
if (targetElement) {
363-
if (!offset) {
364-
// No offset, use scrollIntoView for default behavior
365-
targetElement.scrollIntoView();
366-
} else {
367-
// With offset, use element's offsetTop + offset
368-
const elementTop = (targetElement as HTMLElement).offsetTop;
369-
scrollBodyRef.current.scrollTo({ top: elementTop + offset });
365+
targetElement.scrollIntoView({ block: align });
366+
if (offset) {
367+
const container = scrollBodyRef.current;
368+
container.scrollTo({ top: container.scrollTop + offset });
370369
}
371370
}
372371
}

src/VirtualTable/BodyGrid.tsx

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,21 @@ import VirtualList, { type ListProps, type ListRef } from '@rc-component/virtual
33
import * as React from 'react';
44
import TableContext, { responseImmutable } from '../context/TableContext';
55
import useFlattenRecords, { type FlattenData } from '../hooks/useFlattenRecords';
6-
import type { ColumnType, OnCustomizeScroll, ScrollConfig } from '../interface';
6+
import type {
7+
ColumnType,
8+
OnCustomizeScroll,
9+
ScrollConfig,
10+
VirtualScrollConfig,
11+
} from '../interface';
712
import BodyLine from './BodyLine';
813
import { GridContext, StaticContext } from './context';
914

15+
const ALIGN_MAP: Record<string, 'top' | 'bottom' | 'auto'> = {
16+
start: 'top',
17+
end: 'bottom',
18+
nearest: 'auto',
19+
};
20+
1021
export interface GridProps<RecordType = any> {
1122
data: RecordType[];
1223
onScroll: OnCustomizeScroll;
@@ -79,15 +90,16 @@ const Grid = React.forwardRef<GridRef, GridProps>((props, ref) => {
7990
// =========================== Ref ============================
8091
React.useImperativeHandle(ref, () => {
8192
const obj = {
82-
scrollTo: (config: ScrollConfig) => {
83-
const { offset, ...restConfig } = config;
84-
85-
// If offset is provided, force align to 'top' for consistent behavior
86-
if (offset) {
87-
listRef.current?.scrollTo({ ...restConfig, offset, align: 'top' });
88-
} else {
89-
listRef.current?.scrollTo(config);
90-
}
93+
scrollTo: (config: VirtualScrollConfig) => {
94+
const { align, offset, ...restConfig } = config;
95+
96+
const virtualAlign = ALIGN_MAP[align] ?? (offset ? 'top' : 'auto');
97+
98+
listRef.current?.scrollTo({
99+
...restConfig,
100+
offset,
101+
align: virtualAlign,
102+
});
91103
},
92104
nativeElement: listRef.current?.nativeElement,
93105
} as unknown as GridRef;

src/interface.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,15 @@ export type ScrollConfig = {
4040
* Additional offset in pixels to apply to the scroll position.
4141
* Only effective when using `key` or `index` mode.
4242
* Ignored when using `top` mode.
43-
* When offset is set, the target element will always be aligned to the top of the container.
43+
* In `key` / `index` mode, `offset` is added to the position resolved by `align`.
4444
*/
4545
offset?: number;
46+
47+
align?: ScrollLogicalPosition;
48+
};
49+
50+
export type VirtualScrollConfig = ScrollConfig & {
51+
align?: Exclude<ScrollLogicalPosition, 'center'>;
4652
};
4753

4854
export type Reference = {

tests/Virtual.spec.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,7 @@ describe('Table.Virtual', () => {
373373

374374
expect(global.scrollToConfig).toEqual({
375375
index: 99,
376+
align: 'auto',
376377
});
377378
});
378379

@@ -423,6 +424,31 @@ describe('Table.Virtual', () => {
423424
});
424425
});
425426

427+
it('scrollTo with align should pass', async () => {
428+
const tblRef = React.createRef<Reference>();
429+
getTable({ ref: tblRef });
430+
431+
// align start -> top
432+
tblRef.current.scrollTo({ index: 50, align: 'start' });
433+
await waitFakeTimer();
434+
expect(global.scrollToConfig).toEqual({ index: 50, align: 'top' });
435+
436+
// align end -> bottom
437+
tblRef.current.scrollTo({ index: 50, align: 'end' });
438+
await waitFakeTimer();
439+
expect(global.scrollToConfig).toEqual({ index: 50, align: 'bottom' });
440+
441+
// align nearest -> auto
442+
tblRef.current.scrollTo({ index: 50, align: 'nearest' });
443+
await waitFakeTimer();
444+
expect(global.scrollToConfig).toEqual({ index: 50, align: 'auto' });
445+
446+
// offset + align
447+
tblRef.current.scrollTo({ index: 50, offset: 20, align: 'end' });
448+
await waitFakeTimer();
449+
expect(global.scrollToConfig).toEqual({ index: 50, offset: 20, align: 'bottom' });
450+
});
451+
426452
describe('auto width', () => {
427453
async function prepareTable(columns: any[]) {
428454
const { container } = getTable({

0 commit comments

Comments
 (0)