Skip to content
Merged
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,25 @@ React.render(<Table columns={columns} data={data} />, mountNode);
| summary | (data: readonly RecordType[]) => React.ReactNode | - | `summary` attribute in `table` component is used to define the summary row. |
| rowHoverable | boolean | true | Table hover interaction |

### Methods

#### scrollTo

Table component exposes `scrollTo` method to scroll to a specific position:

```js
const tblRef = useRef();
tblRef.current?.scrollTo({ key: 'rowKey', align: 'start' });
```

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| index | number | - | Row index to scroll to |
| top | number | - | Scroll to specific top position (in px) |
| key | string | - | Scroll to row by row key |
| offset | number | - | Additional offset from target position |
| 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`. |

## Column Props

| Name | Type | Default | Description |
Expand Down
67 changes: 25 additions & 42 deletions docs/examples/scrollY.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,52 +41,35 @@ const Test = () => {
return (
<div>
<h2>scroll body table</h2>
<button
onClick={() => {
tblRef.current?.scrollTo({
top: 9999,
});
}}
>
Scroll To End
<button onClick={() => tblRef.current?.scrollTo({ top: 0 })}>Top</button>
<button onClick={() => tblRef.current?.scrollTo({ top: 9999 })}>End</button>
<button onClick={() => tblRef.current?.scrollTo({ key: 9 })}>Key 9</button>
<button onClick={() => tblRef.current?.scrollTo({ key: 9, align: 'start' })}>
Key 9 align: start
</button>
<button
onClick={() => {
tblRef.current?.scrollTo({
key: 9,
});
}}
>
Scroll To key 9
<button onClick={() => tblRef.current?.scrollTo({ key: 9, align: 'center' })}>
Key 9 align: center
</button>
<button
onClick={() => {
tblRef.current?.scrollTo({
top: 0,
});
}}
>
Scroll To top
<button onClick={() => tblRef.current?.scrollTo({ key: 9, align: 'end' })}>
Key 9 align: end
</button>
<button
onClick={() => {
tblRef.current?.scrollTo({
index: 5,
offset: -10,
});
}}
>
Scroll To Index 5 + Offset -10
<button onClick={() => tblRef.current?.scrollTo({ key: 9, align: 'nearest' })}>
Key 9 align: nearest
</button>
<button
onClick={() => {
tblRef.current?.scrollTo({
key: 6,
offset: -10,
});
}}
>
Scroll To Key 6 + Offset -10
<button onClick={() => tblRef.current?.scrollTo({ key: 9, align: 'start', offset: 20 })}>
Key 9 start offset20
</button>
<button onClick={() => tblRef.current?.scrollTo({ key: 9, align: 'center', offset: 20 })}>
Key 9 center offset20
</button>
<button onClick={() => tblRef.current?.scrollTo({ key: 9, align: 'end', offset: 20 })}>
Key 9 end offset20
</button>
<button onClick={() => tblRef.current?.scrollTo({ key: 9, align: 'nearest', offset: 20 })}>
Key 9 nearest offset20
</button>
<button onClick={() => tblRef.current?.scrollTo({ index: 5, offset: 50 })}>
Index 5 + Offset 50
</button>
<Table
ref={tblRef}
Expand Down
15 changes: 15 additions & 0 deletions docs/examples/virtual.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,21 @@ const Demo: React.FC = () => {
<button onClick={() => tableRef.current?.scrollTo({ key: '50', offset: -10 })}>
Scroll To Key 50 + Offset -10
</button>
<button onClick={() => tableRef.current?.scrollTo({ index: 500, align: 'start' })}>
index 500 + align start
</button>
<button onClick={() => tableRef.current?.scrollTo({ index: 500, align: 'end' })}>
index 500 + align end
</button>
<button onClick={() => tableRef.current?.scrollTo({ index: 500, align: 'nearest' })}>
index 500 + align nearest
</button>
<button onClick={() => tableRef.current?.scrollTo({ index: 500, offset: 50 })}>
index 500 + offset 50
</button>
<button onClick={() => tableRef.current?.scrollTo({ index: 500, offset: 50, align: 'end' })}>
index 500 + offset 50 + align end
</button>
<VirtualTable
style={{ marginTop: 16 }}
ref={tableRef}
Expand Down
19 changes: 9 additions & 10 deletions src/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,10 @@ const EMPTY_SCROLL_TARGET = {};
export type SemanticName = 'section' | 'title' | 'footer' | 'content';
export type ComponentsSemantic = 'wrapper' | 'cell' | 'row';

export interface TableProps<RecordType = any>
extends Omit<LegacyExpandableProps<RecordType>, 'showExpandColumn'> {
export interface TableProps<RecordType = any> extends Omit<
LegacyExpandableProps<RecordType>,
'showExpandColumn'
> {
prefixCls?: string;
className?: string;
style?: React.CSSProperties;
Expand Down Expand Up @@ -349,7 +351,7 @@ const Table = <RecordType extends DefaultRecordType>(
scrollTo: config => {
if (scrollBodyRef.current instanceof HTMLElement) {
// Native scroll
const { index, top, key, offset } = config;
const { index, top, key, offset, align = 'nearest' } = config;

if (validNumberValue(top)) {
// In top mode, offset is ignored
Expand All @@ -360,13 +362,10 @@ const Table = <RecordType extends DefaultRecordType>(
`[data-row-key="${mergedKey}"]`,
);
if (targetElement) {
if (!offset) {
// No offset, use scrollIntoView for default behavior
targetElement.scrollIntoView();
} else {
// With offset, use element's offsetTop + offset
const elementTop = (targetElement as HTMLElement).offsetTop;
scrollBodyRef.current.scrollTo({ top: elementTop + offset });
targetElement.scrollIntoView({ block: align });
if (offset) {
const container = scrollBodyRef.current;
container.scrollTo({ top: container.scrollTop + offset });
}
}
}
Expand Down
32 changes: 22 additions & 10 deletions src/VirtualTable/BodyGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,21 @@ import VirtualList, { type ListProps, type ListRef } from '@rc-component/virtual
import * as React from 'react';
import TableContext, { responseImmutable } from '../context/TableContext';
import useFlattenRecords, { type FlattenData } from '../hooks/useFlattenRecords';
import type { ColumnType, OnCustomizeScroll, ScrollConfig } from '../interface';
import type {
ColumnType,
OnCustomizeScroll,
ScrollConfig,
VirtualScrollConfig,
} from '../interface';
import BodyLine from './BodyLine';
import { GridContext, StaticContext } from './context';

const ALIGN_MAP: Record<string, 'top' | 'bottom' | 'auto'> = {
start: 'top',
end: 'bottom',
nearest: 'auto',
};

export interface GridProps<RecordType = any> {
data: RecordType[];
onScroll: OnCustomizeScroll;
Expand Down Expand Up @@ -79,15 +90,16 @@ const Grid = React.forwardRef<GridRef, GridProps>((props, ref) => {
// =========================== Ref ============================
React.useImperativeHandle(ref, () => {
const obj = {
scrollTo: (config: ScrollConfig) => {
const { offset, ...restConfig } = config;

// If offset is provided, force align to 'top' for consistent behavior
if (offset) {
listRef.current?.scrollTo({ ...restConfig, offset, align: 'top' });
} else {
listRef.current?.scrollTo(config);
}
scrollTo: (config: VirtualScrollConfig) => {
const { align, offset, ...restConfig } = config;

const virtualAlign = ALIGN_MAP[align] ?? (offset ? 'top' : 'auto');

listRef.current?.scrollTo({
...restConfig,
offset,
align: virtualAlign,
});
},
nativeElement: listRef.current?.nativeElement,
} as unknown as GridRef;
Expand Down
8 changes: 7 additions & 1 deletion src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,15 @@ export type ScrollConfig = {
* 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`.
*/
offset?: number;

align?: ScrollLogicalPosition;
};

export type VirtualScrollConfig = ScrollConfig & {
align?: Exclude<ScrollLogicalPosition, 'center'>;
};

export type Reference = {
Expand Down
26 changes: 26 additions & 0 deletions tests/Virtual.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,7 @@ describe('Table.Virtual', () => {

expect(global.scrollToConfig).toEqual({
index: 99,
align: 'auto',
});
});

Expand Down Expand Up @@ -423,6 +424,31 @@ describe('Table.Virtual', () => {
});
});

it('scrollTo with align should pass', async () => {
const tblRef = React.createRef<Reference>();
getTable({ ref: tblRef });

// align start -> top
tblRef.current.scrollTo({ index: 50, align: 'start' });
await waitFakeTimer();
expect(global.scrollToConfig).toEqual({ index: 50, align: 'top' });

// align end -> bottom
tblRef.current.scrollTo({ index: 50, align: 'end' });
await waitFakeTimer();
expect(global.scrollToConfig).toEqual({ index: 50, align: 'bottom' });

// align nearest -> auto
tblRef.current.scrollTo({ index: 50, align: 'nearest' });
await waitFakeTimer();
expect(global.scrollToConfig).toEqual({ index: 50, align: 'auto' });

// offset + align
tblRef.current.scrollTo({ index: 50, offset: 20, align: 'end' });
await waitFakeTimer();
expect(global.scrollToConfig).toEqual({ index: 50, offset: 20, align: 'bottom' });
});

describe('auto width', () => {
async function prepareTable(columns: any[]) {
const { container } = getTable({
Expand Down
Loading
Loading