Skip to content
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 |

## Column Props

| Name | Type | Default | Description |
Expand Down
30 changes: 30 additions & 0 deletions docs/examples/scrollY.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,36 @@ const Test = () => {
>
Scroll To Key 6 + Offset -10
</button>
<button
onClick={() => {
tblRef.current?.scrollTo({
key: 9,
align: 'start',
});
}}
>
Scroll To key 9 (align: start)
</button>
<button
onClick={() => {
tblRef.current?.scrollTo({
key: 9,
align: 'center',
});
}}
>
Scroll To key 9 (align: center)
</button>
<button
onClick={() => {
tblRef.current?.scrollTo({
key: 9,
align: 'end',
});
}}
>
Scroll To key 9 (align: end)
</button>
<Table
ref={tblRef}
columns={columns}
Expand Down
25 changes: 18 additions & 7 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 @@ -361,12 +363,21 @@ const Table = <RecordType extends DefaultRecordType>(
);
if (targetElement) {
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 });
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}
}
}
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,7 +3,12 @@ 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';

Expand Down Expand Up @@ -79,15 +84,22 @@ 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 alignMap: Record<string, 'top' | 'bottom' | 'auto'> = {
start: 'top',
end: 'bottom',
nearest: 'auto',
};
Comment thread
EmilyyyLiu marked this conversation as resolved.
Outdated

const virtualAlign = align ? alignMap[align] : offset ? 'top' : 'auto';

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
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
42 changes: 21 additions & 21 deletions tests/__snapshots__/ExpandRow.spec.jsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ exports[`Table.Expand > does not crash if scroll is not set 1`] = `
<tr>
<th
class="rc-table-cell rc-table-row-expand-icon-cell rc-table-cell-fix rc-table-cell-fix-start rc-table-cell-fix-start-shadow"
style="inset-inline-start: 0; --z-offset: 8; --z-offset-reverse: 4;"
style="inset-inline-start: 0px; --z-offset: 8; --z-offset-reverse: 4;"
/>
<th
class="rc-table-cell"
Expand Down Expand Up @@ -202,7 +202,7 @@ exports[`Table.Expand > does not crash if scroll is not set 1`] = `
>
<td
class="rc-table-cell rc-table-row-expand-icon-cell rc-table-cell-fix rc-table-cell-fix-start rc-table-cell-fix-start-shadow"
style="inset-inline-start: 0; --z-offset: 8; --z-offset-reverse: 4;"
style="inset-inline-start: 0px; --z-offset: 8; --z-offset-reverse: 4;"
>
<span
class="rc-table-row-expand-icon rc-table-row-collapsed"
Expand Down Expand Up @@ -230,7 +230,7 @@ exports[`Table.Expand > does not crash if scroll is not set 1`] = `
>
<td
class="rc-table-cell rc-table-row-expand-icon-cell rc-table-cell-fix rc-table-cell-fix-start rc-table-cell-fix-start-shadow"
style="inset-inline-start: 0; --z-offset: 8; --z-offset-reverse: 4;"
style="inset-inline-start: 0px; --z-offset: 8; --z-offset-reverse: 4;"
>
<span
class="rc-table-row-expand-icon rc-table-row-collapsed"
Expand Down Expand Up @@ -284,7 +284,7 @@ exports[`Table.Expand > does not crash if scroll is not set 2`] = `
<tr>
<th
class="rc-table-cell rc-table-row-expand-icon-cell rc-table-cell-fix rc-table-cell-fix-start rc-table-cell-fix-start-shadow"
style="inset-inline-start: 0; --z-offset: 8; --z-offset-reverse: 4;"
style="inset-inline-start: 0px; --z-offset: 8; --z-offset-reverse: 4;"
/>
<th
class="rc-table-cell"
Expand Down Expand Up @@ -357,7 +357,7 @@ exports[`Table.Expand > does not crash if scroll is not set 2`] = `
>
<td
class="rc-table-cell rc-table-row-expand-icon-cell rc-table-cell-fix rc-table-cell-fix-start rc-table-cell-fix-start-shadow"
style="inset-inline-start: 0; --z-offset: 8; --z-offset-reverse: 4;"
style="inset-inline-start: 0px; --z-offset: 8; --z-offset-reverse: 4;"
>
<span
class="rc-table-row-expand-icon rc-table-row-collapsed"
Expand Down Expand Up @@ -385,7 +385,7 @@ exports[`Table.Expand > does not crash if scroll is not set 2`] = `
>
<td
class="rc-table-cell rc-table-row-expand-icon-cell rc-table-cell-fix rc-table-cell-fix-start rc-table-cell-fix-start-shadow"
style="inset-inline-start: 0; --z-offset: 8; --z-offset-reverse: 4;"
style="inset-inline-start: 0px; --z-offset: 8; --z-offset-reverse: 4;"
>
<span
class="rc-table-row-expand-icon rc-table-row-collapsed"
Expand Down Expand Up @@ -525,12 +525,12 @@ exports[`Table.Expand > renders fixed column correctly > work 1`] = `
<tr>
<th
class="rc-table-cell rc-table-row-expand-icon-cell rc-table-cell-fix rc-table-cell-fix-start"
style="inset-inline-start: 0; --z-offset: 8; --z-offset-reverse: 4;"
style="inset-inline-start: 0px; --z-offset: 8; --z-offset-reverse: 4;"
/>
<th
class="rc-table-cell rc-table-cell-fix rc-table-cell-fix-start rc-table-cell-fix-start-shadow"
scope="col"
style="inset-inline-start: 0; --z-offset: 7; --z-offset-reverse: 5;"
style="inset-inline-start: 0px; --z-offset: 7; --z-offset-reverse: 5;"
>
Name
</th>
Expand All @@ -543,7 +543,7 @@ exports[`Table.Expand > renders fixed column correctly > work 1`] = `
<th
class="rc-table-cell rc-table-cell-fix rc-table-cell-fix-end rc-table-cell-fix-end-shadow"
scope="col"
style="inset-inline-end: 0; --z-offset: 3; --z-offset-reverse: 1;"
style="inset-inline-end: 0px; --z-offset: 3; --z-offset-reverse: 1;"
>
Gender
</th>
Expand Down Expand Up @@ -600,15 +600,15 @@ exports[`Table.Expand > renders fixed column correctly > work 1`] = `
>
<td
class="rc-table-cell rc-table-row-expand-icon-cell rc-table-cell-fix rc-table-cell-fix-start"
style="inset-inline-start: 0; --z-offset: 8; --z-offset-reverse: 4;"
style="inset-inline-start: 0px; --z-offset: 8; --z-offset-reverse: 4;"
>
<span
class="rc-table-row-expand-icon rc-table-row-expanded"
/>
</td>
<td
class="rc-table-cell rc-table-cell-fix rc-table-cell-fix-start rc-table-cell-fix-start-shadow"
style="inset-inline-start: 0; --z-offset: 7; --z-offset-reverse: 5;"
style="inset-inline-start: 0px; --z-offset: 7; --z-offset-reverse: 5;"
>
Lucy
</td>
Expand All @@ -619,7 +619,7 @@ exports[`Table.Expand > renders fixed column correctly > work 1`] = `
</td>
<td
class="rc-table-cell rc-table-cell-fix rc-table-cell-fix-end rc-table-cell-fix-end-shadow"
style="inset-inline-end: 0; --z-offset: 3; --z-offset-reverse: 1;"
style="inset-inline-end: 0px; --z-offset: 3; --z-offset-reverse: 1;"
>
F
</td>
Expand Down Expand Up @@ -647,15 +647,15 @@ exports[`Table.Expand > renders fixed column correctly > work 1`] = `
>
<td
class="rc-table-cell rc-table-row-expand-icon-cell rc-table-cell-fix rc-table-cell-fix-start"
style="inset-inline-start: 0; --z-offset: 8; --z-offset-reverse: 4;"
style="inset-inline-start: 0px; --z-offset: 8; --z-offset-reverse: 4;"
>
<span
class="rc-table-row-expand-icon rc-table-row-expanded"
/>
</td>
<td
class="rc-table-cell rc-table-cell-fix rc-table-cell-fix-start rc-table-cell-fix-start-shadow"
style="inset-inline-start: 0; --z-offset: 7; --z-offset-reverse: 5;"
style="inset-inline-start: 0px; --z-offset: 7; --z-offset-reverse: 5;"
>
Jack
</td>
Expand All @@ -666,7 +666,7 @@ exports[`Table.Expand > renders fixed column correctly > work 1`] = `
</td>
<td
class="rc-table-cell rc-table-cell-fix rc-table-cell-fix-end rc-table-cell-fix-end-shadow"
style="inset-inline-end: 0; --z-offset: 3; --z-offset-reverse: 1;"
style="inset-inline-end: 0px; --z-offset: 3; --z-offset-reverse: 1;"
>
M
</td>
Expand Down Expand Up @@ -991,7 +991,7 @@ exports[`Table.Expand > work in expandable fix 1`] = `
<tr>
<th
class="rc-table-cell rc-table-row-expand-icon-cell rc-table-cell-fix rc-table-cell-fix-start rc-table-cell-fix-start-shadow"
style="inset-inline-start: 0; --z-offset: 8; --z-offset-reverse: 4;"
style="inset-inline-start: 0px; --z-offset: 8; --z-offset-reverse: 4;"
/>
<th
class="rc-table-cell"
Expand Down Expand Up @@ -1064,7 +1064,7 @@ exports[`Table.Expand > work in expandable fix 1`] = `
>
<td
class="rc-table-cell rc-table-row-expand-icon-cell rc-table-cell-fix rc-table-cell-fix-start rc-table-cell-fix-start-shadow"
style="inset-inline-start: 0; --z-offset: 8; --z-offset-reverse: 4;"
style="inset-inline-start: 0px; --z-offset: 8; --z-offset-reverse: 4;"
>
<span
class="rc-table-row-expand-icon rc-table-row-collapsed"
Expand Down Expand Up @@ -1092,7 +1092,7 @@ exports[`Table.Expand > work in expandable fix 1`] = `
>
<td
class="rc-table-cell rc-table-row-expand-icon-cell rc-table-cell-fix rc-table-cell-fix-start rc-table-cell-fix-start-shadow"
style="inset-inline-start: 0; --z-offset: 8; --z-offset-reverse: 4;"
style="inset-inline-start: 0px; --z-offset: 8; --z-offset-reverse: 4;"
>
<span
class="rc-table-row-expand-icon rc-table-row-collapsed"
Expand Down Expand Up @@ -1167,7 +1167,7 @@ exports[`Table.Expand > work in expandable fix 2`] = `
</th>
<th
class="rc-table-cell rc-table-row-expand-icon-cell rc-table-cell-fix rc-table-cell-fix-start rc-table-cell-fix-start-shadow"
style="inset-inline-start: 0; --z-offset: 5; --z-offset-reverse: 7;"
style="inset-inline-start: 0px; --z-offset: 5; --z-offset-reverse: 7;"
/>
</tr>
</thead>
Expand Down Expand Up @@ -1237,7 +1237,7 @@ exports[`Table.Expand > work in expandable fix 2`] = `
</td>
<td
class="rc-table-cell rc-table-row-expand-icon-cell rc-table-cell-fix rc-table-cell-fix-start rc-table-cell-fix-start-shadow"
style="inset-inline-start: 0; --z-offset: 5; --z-offset-reverse: 7;"
style="inset-inline-start: 0px; --z-offset: 5; --z-offset-reverse: 7;"
>
<span
class="rc-table-row-expand-icon rc-table-row-collapsed"
Expand Down Expand Up @@ -1265,7 +1265,7 @@ exports[`Table.Expand > work in expandable fix 2`] = `
</td>
<td
class="rc-table-cell rc-table-row-expand-icon-cell rc-table-cell-fix rc-table-cell-fix-start rc-table-cell-fix-start-shadow"
style="inset-inline-start: 0; --z-offset: 5; --z-offset-reverse: 7;"
style="inset-inline-start: 0px; --z-offset: 5; --z-offset-reverse: 7;"
>
<span
class="rc-table-row-expand-icon rc-table-row-collapsed"
Expand Down
Loading