diff --git a/README.md b/README.md index 01f59467b..ed9515839 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,25 @@ React.render(, 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 | diff --git a/docs/examples/scrollY.tsx b/docs/examples/scrollY.tsx index 4146498a5..fa7908698 100644 --- a/docs/examples/scrollY.tsx +++ b/docs/examples/scrollY.tsx @@ -41,52 +41,35 @@ const Test = () => { return (

scroll body table

- + + + - - - - + + + +
{ + + + + + - extends Omit, 'showExpandColumn'> { +export interface TableProps extends Omit< + LegacyExpandableProps, + 'showExpandColumn' +> { prefixCls?: string; className?: string; style?: React.CSSProperties; @@ -349,7 +351,7 @@ const Table = ( 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 @@ -360,13 +362,10 @@ const Table = ( `[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 }); } } } diff --git a/src/VirtualTable/BodyGrid.tsx b/src/VirtualTable/BodyGrid.tsx index a4db84ce4..11bf129ab 100644 --- a/src/VirtualTable/BodyGrid.tsx +++ b/src/VirtualTable/BodyGrid.tsx @@ -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 = { + start: 'top', + end: 'bottom', + nearest: 'auto', +}; + export interface GridProps { data: RecordType[]; onScroll: OnCustomizeScroll; @@ -79,15 +90,16 @@ const Grid = React.forwardRef((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; diff --git a/src/interface.ts b/src/interface.ts index 2c2d67801..d5a8aee03 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -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; }; export type Reference = { diff --git a/tests/Virtual.spec.tsx b/tests/Virtual.spec.tsx index 81aff6aba..dad854b21 100644 --- a/tests/Virtual.spec.tsx +++ b/tests/Virtual.spec.tsx @@ -373,6 +373,7 @@ describe('Table.Virtual', () => { expect(global.scrollToConfig).toEqual({ index: 99, + align: 'auto', }); }); @@ -423,6 +424,31 @@ describe('Table.Virtual', () => { }); }); + it('scrollTo with align should pass', async () => { + const tblRef = React.createRef(); + 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({ diff --git a/tests/__snapshots__/ExpandRow.spec.jsx.snap b/tests/__snapshots__/ExpandRow.spec.jsx.snap index 9531f6414..e589629f2 100644 --- a/tests/__snapshots__/ExpandRow.spec.jsx.snap +++ b/tests/__snapshots__/ExpandRow.spec.jsx.snap @@ -129,7 +129,7 @@ exports[`Table.Expand > does not crash if scroll is not set 1`] = ` @@ -543,7 +543,7 @@ exports[`Table.Expand > renders fixed column correctly > work 1`] = ` @@ -600,7 +600,7 @@ exports[`Table.Expand > renders fixed column correctly > work 1`] = ` > @@ -619,7 +619,7 @@ exports[`Table.Expand > renders fixed column correctly > work 1`] = ` @@ -647,7 +647,7 @@ exports[`Table.Expand > renders fixed column correctly > work 1`] = ` > @@ -666,7 +666,7 @@ exports[`Table.Expand > renders fixed column correctly > work 1`] = ` @@ -991,7 +991,7 @@ exports[`Table.Expand > work in expandable fix 1`] = ` @@ -1237,7 +1237,7 @@ exports[`Table.Expand > work in expandable fix 2`] = ` @@ -114,7 +114,7 @@ exports[`Table.FixedColumn > fixed column renders correctly RTL 1`] = ` @@ -243,7 +243,7 @@ exports[`Table.FixedColumn > fixed column renders correctly RTL 1`] = ` > @@ -305,7 +305,7 @@ exports[`Table.FixedColumn > fixed column renders correctly RTL 1`] = ` @@ -316,7 +316,7 @@ exports[`Table.FixedColumn > fixed column renders correctly RTL 1`] = ` > @@ -378,7 +378,7 @@ exports[`Table.FixedColumn > fixed column renders correctly RTL 1`] = ` @@ -389,7 +389,7 @@ exports[`Table.FixedColumn > fixed column renders correctly RTL 1`] = ` > @@ -437,7 +437,7 @@ exports[`Table.FixedColumn > fixed column renders correctly RTL 1`] = ` /> fixed column renders correctly RTL 1`] = ` > @@ -494,7 +494,7 @@ exports[`Table.FixedColumn > fixed column renders correctly RTL 1`] = ` /> fixed column renders correctly RTL 1`] = ` > @@ -551,7 +551,7 @@ exports[`Table.FixedColumn > fixed column renders correctly RTL 1`] = ` /> fixed column renders correctly RTL 1`] = ` > @@ -608,7 +608,7 @@ exports[`Table.FixedColumn > fixed column renders correctly RTL 1`] = ` /> fixed column renders correctly RTL 1`] = ` > @@ -665,7 +665,7 @@ exports[`Table.FixedColumn > fixed column renders correctly RTL 1`] = ` /> fixed column renders correctly RTL 1`] = ` > @@ -722,7 +722,7 @@ exports[`Table.FixedColumn > fixed column renders correctly RTL 1`] = ` /> fixed column renders correctly RTL 1`] = ` > @@ -779,7 +779,7 @@ exports[`Table.FixedColumn > fixed column renders correctly RTL 1`] = ` /> @@ -832,7 +832,7 @@ exports[`Table.FixedColumn > renders correctly > scrollX - with data 1`] = ` @@ -905,7 +905,7 @@ exports[`Table.FixedColumn > renders correctly > scrollX - with data 1`] = ` @@ -1034,7 +1034,7 @@ exports[`Table.FixedColumn > renders correctly > scrollX - with data 1`] = ` > @@ -1096,7 +1096,7 @@ exports[`Table.FixedColumn > renders correctly > scrollX - with data 1`] = ` @@ -1107,7 +1107,7 @@ exports[`Table.FixedColumn > renders correctly > scrollX - with data 1`] = ` > @@ -1169,7 +1169,7 @@ exports[`Table.FixedColumn > renders correctly > scrollX - with data 1`] = ` @@ -1180,7 +1180,7 @@ exports[`Table.FixedColumn > renders correctly > scrollX - with data 1`] = ` > @@ -1228,7 +1228,7 @@ exports[`Table.FixedColumn > renders correctly > scrollX - with data 1`] = ` /> renders correctly > scrollX - with data 1`] = ` > @@ -1285,7 +1285,7 @@ exports[`Table.FixedColumn > renders correctly > scrollX - with data 1`] = ` /> renders correctly > scrollX - with data 1`] = ` > @@ -1342,7 +1342,7 @@ exports[`Table.FixedColumn > renders correctly > scrollX - with data 1`] = ` /> renders correctly > scrollX - with data 1`] = ` > @@ -1399,7 +1399,7 @@ exports[`Table.FixedColumn > renders correctly > scrollX - with data 1`] = ` /> renders correctly > scrollX - with data 1`] = ` > @@ -1456,7 +1456,7 @@ exports[`Table.FixedColumn > renders correctly > scrollX - with data 1`] = ` /> renders correctly > scrollX - with data 1`] = ` > @@ -1513,7 +1513,7 @@ exports[`Table.FixedColumn > renders correctly > scrollX - with data 1`] = ` /> renders correctly > scrollX - with data 1`] = ` > @@ -1570,7 +1570,7 @@ exports[`Table.FixedColumn > renders correctly > scrollX - with data 1`] = ` /> @@ -1621,7 +1621,7 @@ exports[`Table.FixedColumn > renders correctly > scrollX - without data 1`] = ` @@ -1694,7 +1694,7 @@ exports[`Table.FixedColumn > renders correctly > scrollX - without data 1`] = ` @@ -1901,7 +1901,7 @@ exports[`Table.FixedColumn > renders correctly > scrollXY - with data 1`] = ` @@ -1980,7 +1980,7 @@ exports[`Table.FixedColumn > renders correctly > scrollXY - with data 1`] = ` @@ -2136,7 +2136,7 @@ exports[`Table.FixedColumn > renders correctly > scrollXY - with data 1`] = ` > @@ -2198,7 +2198,7 @@ exports[`Table.FixedColumn > renders correctly > scrollXY - with data 1`] = ` @@ -2209,7 +2209,7 @@ exports[`Table.FixedColumn > renders correctly > scrollXY - with data 1`] = ` > @@ -2271,7 +2271,7 @@ exports[`Table.FixedColumn > renders correctly > scrollXY - with data 1`] = ` @@ -2282,7 +2282,7 @@ exports[`Table.FixedColumn > renders correctly > scrollXY - with data 1`] = ` > @@ -2330,7 +2330,7 @@ exports[`Table.FixedColumn > renders correctly > scrollXY - with data 1`] = ` /> renders correctly > scrollXY - with data 1`] = ` > @@ -2387,7 +2387,7 @@ exports[`Table.FixedColumn > renders correctly > scrollXY - with data 1`] = ` /> renders correctly > scrollXY - with data 1`] = ` > @@ -2444,7 +2444,7 @@ exports[`Table.FixedColumn > renders correctly > scrollXY - with data 1`] = ` /> renders correctly > scrollXY - with data 1`] = ` > @@ -2501,7 +2501,7 @@ exports[`Table.FixedColumn > renders correctly > scrollXY - with data 1`] = ` /> renders correctly > scrollXY - with data 1`] = ` > @@ -2558,7 +2558,7 @@ exports[`Table.FixedColumn > renders correctly > scrollXY - with data 1`] = ` /> renders correctly > scrollXY - with data 1`] = ` > @@ -2615,7 +2615,7 @@ exports[`Table.FixedColumn > renders correctly > scrollXY - with data 1`] = ` /> renders correctly > scrollXY - with data 1`] = ` > @@ -2672,7 +2672,7 @@ exports[`Table.FixedColumn > renders correctly > scrollXY - with data 1`] = ` /> @@ -2723,7 +2723,7 @@ exports[`Table.FixedColumn > renders correctly > scrollXY - without data 1`] = ` @@ -2802,7 +2802,7 @@ exports[`Table.FixedColumn > renders correctly > scrollXY - without data 1`] = ` diff --git a/tests/__snapshots__/Summary.spec.tsx.snap b/tests/__snapshots__/Summary.spec.tsx.snap index c71599c83..6904cccea 100644 --- a/tests/__snapshots__/Summary.spec.tsx.snap +++ b/tests/__snapshots__/Summary.spec.tsx.snap @@ -8,7 +8,7 @@ exports[`Table.Summary > support data type 1`] = ` diff --git a/tests/__snapshots__/Table.spec.jsx.snap b/tests/__snapshots__/Table.spec.jsx.snap index 90936f847..dca0d4471 100644 --- a/tests/__snapshots__/Table.spec.jsx.snap +++ b/tests/__snapshots__/Table.spec.jsx.snap @@ -98,7 +98,7 @@ exports[`Table.Basic > custom components > renders fixed column and header corre class="rc-table-cell rc-table-cell-fix rc-table-cell-fix-start rc-table-cell-fix-start-shadow" name="my-header-cell" scope="col" - style="inset-inline-start: 0; --z-offset: 8; --z-offset-reverse: 4;" + style="inset-inline-start: 0px; --z-offset: 8; --z-offset-reverse: 4;" > Name @@ -120,7 +120,7 @@ exports[`Table.Basic > custom components > renders fixed column and header corre @@ -179,7 +179,7 @@ exports[`Table.Basic > custom components > renders fixed column and header corre @@ -192,7 +192,7 @@ exports[`Table.Basic > custom components > renders fixed column and header corre diff --git a/tests/refs.spec.tsx b/tests/refs.spec.tsx index a53b03cb7..7d7edc773 100644 --- a/tests/refs.spec.tsx +++ b/tests/refs.spec.tsx @@ -21,6 +21,7 @@ describe('Table.Ref', () => { beforeEach(() => { scrollParam = null; + scrollIntoViewElement = null; }); it('support reference', () => { @@ -109,4 +110,101 @@ describe('Table.Ref', () => { }); expect(scrollIntoViewElement.textContent).toEqual('light'); }); + + it('support scrollTo with align', () => { + const ref = React.createRef(); + + render( +
does not crash if scroll is not set 1`] = ` > does not crash if scroll is not set 1`] = ` > does not crash if scroll is not set 2`] = `
does not crash if scroll is not set 2`] = ` > does not crash if scroll is not set 2`] = ` > renders fixed column correctly > work 1`] = `
Name Gender renders fixed column correctly > work 1`] = ` Lucy F renders fixed column correctly > work 1`] = ` Jack M
work in expandable fix 1`] = ` > work in expandable fix 1`] = ` > work in expandable fix 2`] = `
work in expandable fix 2`] = ` fixed column renders correctly RTL 1`] = ` title1 title12 123 xxxxxxxx cdd edd12221 133
133
133
133
133
133
133
title1 title12 123 xxxxxxxx cdd edd12221 133
133
133
133
133
133
133
title1 title12 title1
123 xxxxxxxx cdd edd12221 133
133
133
133
133
133
133
title1
Light
Lucy F
, + ); + + // Default behavior: uses scrollIntoView (not scrollTo) + ref.current.scrollTo({ index: 0 }); + expect(scrollIntoViewElement).not.toBeNull(); + expect(scrollIntoViewElement.textContent).toEqual('light'); + + // Align start - should use scrollIntoView + scrollIntoViewElement = null; + ref.current.scrollTo({ index: 0, align: 'start' }); + expect(scrollIntoViewElement.textContent).toEqual('light'); + + // Align center - should use scrollIntoView + ref.current.scrollTo({ index: 1, align: 'center' }); + expect(scrollIntoViewElement.textContent).toEqual('bamboo'); + + // Align end - should use scrollIntoView + scrollIntoViewElement = null; + ref.current.scrollTo({ key: 'bamboo', align: 'end' }); + expect(scrollIntoViewElement.textContent).toEqual('bamboo'); + }); + + it('support scrollTo with align and offset', () => { + const ref = React.createRef(); + + render( +
, + ); + + // align start + offset 20 = 0 + 20 = 20 + ref.current.scrollTo({ index: 0, align: 'start', offset: 20 }); + expect(scrollParam.top).toEqual(20); + + // align center + offset 30 = 0 + 30 = 30 + ref.current.scrollTo({ index: 1, align: 'center', offset: 30 }); + expect(scrollParam.top).toEqual(30); + + // align end + offset 10 = 0 + 10 = 10 + ref.current.scrollTo({ key: 'bamboo', align: 'end', offset: 10 }); + expect(scrollParam.top).toEqual(10); + + // align nearest + offset 50 = 0 + 50 = 50 + ref.current.scrollTo({ index: 0, align: 'nearest', offset: 50 }); + expect(scrollParam.top).toEqual(50); + }); + + it('support scrollTo with align nearest and element above viewport', () => { + const ref = React.createRef(); + + render( +
, + ); + + ref.current.scrollTo({ top: 50 }); + + ref.current.scrollTo({ index: 0, align: 'nearest', offset: -10 }); + expect(scrollParam.top).toEqual(-10); + }); });