Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
51 changes: 51 additions & 0 deletions docs/examples/scrollY.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,57 @@ 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>
<button
onClick={() => {
tblRef.current?.scrollTo({
key: 9,
align: 'nearest',
});
}}
>
Scroll To key 9 (align: nearest)
</button>
<button
onClick={() => {
tblRef.current?.scrollTo({
index: 9,
offset: 50,
align: 'nearest',
});
}}
>
Scroll To index 9 + offset 50 (align: nearest)
</button>
<Table
ref={tblRef}
columns={columns}
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
42 changes: 35 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 } = config;

if (validNumberValue(top)) {
// In top mode, offset is ignored
Expand All @@ -361,12 +363,38 @@ const Table = <RecordType extends DefaultRecordType>(
);
if (targetElement) {
if (!offset) {
// No offset, use scrollIntoView for default behavior
targetElement.scrollIntoView();
targetElement.scrollIntoView({ block: align ?? 'nearest' });
} 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 currentTop = container.scrollTop;
const elementBottom = elementTop + elementHeight;
const viewportBottom = currentTop + containerHeight;
let targetTop: number;

if (align === 'nearest') {
const targetWithOffset = elementTop + offset;
const targetBottomWithOffset = elementBottom + offset;

if (targetWithOffset < currentTop) {
targetTop = targetWithOffset;
} else if (targetBottomWithOffset > viewportBottom) {
targetTop = targetBottomWithOffset - containerHeight;
} else {
targetTop = currentTop;
}
} else {
const alignMap: Record<string, number> = {
start: elementTop,
end: elementBottom - containerHeight,
center: elementTop - (containerHeight - elementHeight) / 2,
};
targetTop = alignMap[align ?? 'start'] + offset;
Comment on lines +390 to +394
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.

medium

The alignMap object is recreated on every call to scrollTo. For better performance, consider moving this mapping outside the function or using a constant.

}

container.scrollTo({ top: targetTop });
}
}
}
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 on lines +90 to +94
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.

medium

The alignMap object is recreated on every call to scrollTo. For better performance, consider moving this mapping outside the function or using a constant.


const virtualAlign = alignMap[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