diff --git a/README.md b/README.md index 01f59467b..dbe625eb7 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 | + ## Column Props | Name | Type | Default | Description | diff --git a/docs/examples/scrollY.tsx b/docs/examples/scrollY.tsx index 4146498a5..39d525dd7 100644 --- a/docs/examples/scrollY.tsx +++ b/docs/examples/scrollY.tsx @@ -88,6 +88,57 @@ const Test = () => { > Scroll To Key 6 + Offset -10 + + + + +
{ + + + + + - 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 } = config; if (validNumberValue(top)) { // In top mode, offset is ignored @@ -361,12 +363,38 @@ const Table = ( ); 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 = { + start: elementTop, + end: elementBottom - containerHeight, + center: elementTop - (containerHeight - elementHeight) / 2, + }; + targetTop = alignMap[align ?? 'start'] + offset; + } + + container.scrollTo({ top: targetTop }); } } } diff --git a/src/VirtualTable/BodyGrid.tsx b/src/VirtualTable/BodyGrid.tsx index a4db84ce4..a360033a1 100644 --- a/src/VirtualTable/BodyGrid.tsx +++ b/src/VirtualTable/BodyGrid.tsx @@ -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'; @@ -79,15 +84,22 @@ 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 alignMap: Record = { + start: 'top', + end: 'bottom', + nearest: 'auto', + }; + + const virtualAlign = alignMap[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..b2d022fe9 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,78 @@ 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(scrollIntoViewElement).toBeNull(); + 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); + }); });