diff --git a/docs/examples/scrollY.tsx b/docs/examples/scrollY.tsx index 25b913614..f82cf846c 100644 --- a/docs/examples/scrollY.tsx +++ b/docs/examples/scrollY.tsx @@ -1,5 +1,5 @@ +import Table, { type Reference } from 'rc-table'; import React from 'react'; -import Table from 'rc-table'; import '../../assets/index.less'; const data = []; @@ -12,51 +12,63 @@ for (let i = 0; i < 10; i += 1) { }); } -class Demo extends React.Component { - state = { - showBody: true, - }; +const Test = () => { + const tblRef = React.useRef(); + const [showBody, setShowBody] = React.useState(true); - toggleBody = () => { - this.setState(({ showBody }) => ({ showBody: !showBody })); + const toggleBody = () => { + setShowBody(!showBody); }; - render() { - const { showBody } = this.state; - const columns = [ - { title: 'title1', key: 'a', dataIndex: 'a', width: 100 }, - { id: '123', title: 'title2', dataIndex: 'b', key: 'b', width: 100 }, - { title: 'title3', key: 'c', dataIndex: 'c', width: 200 }, - { - title: ( - - {showBody ? '隐藏' : '显示'}体 - - ), - key: 'x', - width: 200, - render() { - return Operations; - }, + const columns = [ + { title: 'title1', key: 'a', dataIndex: 'a', width: 100 }, + { id: '123', title: 'title2', dataIndex: 'b', key: 'b', width: 100 }, + { title: 'title3', key: 'c', dataIndex: 'c', width: 200 }, + { + title: ( + + {showBody ? '隐藏' : '显示'}体 + + ), + key: 'x', + width: 200, + render() { + return Operations; }, - ]; - return ( + }, + ]; + + return ( +
+

scroll body table

+ + record.key} - onRow={(record, index) => ({ style: { backgroundColor: "red" } })} + onRow={(record, index) => ({ style: { backgroundColor: 'red' } })} /> - ); - } -} - -const Test = () => ( -
-

scroll body table

- -
-); + + ); +}; export default Test; diff --git a/docs/examples/virtual.tsx b/docs/examples/virtual.tsx index 5aadf61ad..25a64a742 100644 --- a/docs/examples/virtual.tsx +++ b/docs/examples/virtual.tsx @@ -1,7 +1,7 @@ import React from 'react'; import '../../assets/index.less'; import { VirtualTable } from '../../src'; -import type { ColumnsType } from '../../src/interface'; +import type { ColumnsType, Reference } from '../../src/interface'; interface RecordType { a: string; @@ -189,18 +189,34 @@ const data: RecordType[] = new Array(4 * 10000).fill(null).map((_, index) => ({ })); const Demo = () => { - const [scrollY, setScrollY] = React.useState(true); + const tblRef = React.useRef(); return (
- + + + + b || c} - scroll={{ x: 1300, y: scrollY ? 200 : null }} + scroll={{ x: 1300, y: 200 }} data={data} // data={[]} rowKey="indexKey" @@ -220,6 +236,7 @@ const Demo = () => { return mergedWidth; }} + reference={tblRef} />
); diff --git a/package.json b/package.json index 7be16fb27..65df01809 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "@rc-component/context": "^1.4.0", "classnames": "^2.2.5", "rc-resize-observer": "^1.1.0", - "rc-util": "^5.36.0", + "rc-util": "^5.37.0", "rc-virtual-list": "^3.11.1" }, "devDependencies": { diff --git a/src/Table.tsx b/src/Table.tsx index a52901bd0..9c3c899a5 100644 --- a/src/Table.tsx +++ b/src/Table.tsx @@ -64,6 +64,7 @@ import type { GetRowKey, LegacyExpandableProps, PanelRender, + Reference, RowClassName, TableComponents, TableLayout, @@ -120,6 +121,8 @@ export interface TableProps sticky?: boolean | TableSticky; + reference?: React.Ref; + // =================================== Internal =================================== /** * @private Internal usage, may remove by refactor. Should always use `columns` instead. @@ -185,6 +188,7 @@ function Table(tableProps: TableProps(tableProps: TableProps(); const scrollHeaderRef = React.useRef(); const scrollBodyRef = React.useRef(); const scrollBodyContainerRef = React.useRef(); + + React.useImperativeHandle(reference, () => { + return { + nativeElement: fullTableRef.current, + scrollTo: config => { + if (scrollBodyRef.current instanceof HTMLElement) { + // Native scroll + const { index, top, key } = config; + + if (top) { + scrollBodyRef.current?.scrollTo({ + top, + }); + } else { + const mergedKey = key ?? getRowKey(mergedData[index]); + scrollBodyRef.current.querySelector(`[data-row-key="${mergedKey}"]`)?.scrollIntoView(); + } + } else if ((scrollBodyRef.current as any)?.scrollTo) { + // Pass to proxy + (scrollBodyRef.current as any).scrollTo(config); + } + }, + }; + }); + + // ====================== Scroll ====================== const scrollSummaryRef = React.useRef(); const [pingedLeft, setPingedLeft] = React.useState(false); const [pingedRight, setPingedRight] = React.useState(false); diff --git a/src/VirtualTable/BodyGrid.tsx b/src/VirtualTable/BodyGrid.tsx index a7efe6f13..c7840159c 100644 --- a/src/VirtualTable/BodyGrid.tsx +++ b/src/VirtualTable/BodyGrid.tsx @@ -5,7 +5,7 @@ import * as React from 'react'; import Cell from '../Cell'; import TableContext, { responseImmutable } from '../context/TableContext'; import useFlattenRecords, { type FlattenData } from '../hooks/useFlattenRecords'; -import type { ColumnType, OnCustomizeScroll } from '../interface'; +import type { ColumnType, OnCustomizeScroll, ScrollConfig } from '../interface'; import BodyLine from './BodyLine'; import { GridContext, StaticContext } from './context'; @@ -16,6 +16,7 @@ export interface GridProps { export interface GridRef { scrollLeft: number; + scrollTo?: (scrollConfig: ScrollConfig) => void; } const Grid = React.forwardRef((props, ref) => { @@ -70,7 +71,12 @@ const Grid = React.forwardRef((props, ref) => { // =========================== Ref ============================ React.useImperativeHandle(ref, () => { - const obj = {} as GridRef; + const obj = { + scrollTo: (config: ScrollConfig) => { + console.log('!!!!', config); + listRef.current?.scrollTo(config); + }, + } as unknown as GridRef; Object.defineProperty(obj, 'scrollLeft', { get: () => listRef.current?.getScrollInfo().x || 0, diff --git a/src/index.ts b/src/index.ts index 81dc8b5a7..dd67c0309 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,12 +1,13 @@ import { EXPAND_COLUMN, INTERNAL_HOOKS } from './constant'; import { FooterComponents as Summary } from './Footer'; -import VirtualTable, { genVirtualTable } from './VirtualTable'; -import type { VirtualTableProps } from './VirtualTable'; +import type { Reference } from './interface'; import Column from './sugar/Column'; import ColumnGroup from './sugar/ColumnGroup'; import type { TableProps } from './Table'; import Table, { genTable } from './Table'; import { INTERNAL_COL_DEFINE } from './utils/legacyUtil'; +import type { VirtualTableProps } from './VirtualTable'; +import VirtualTable, { genVirtualTable } from './VirtualTable'; export { genTable, @@ -20,6 +21,7 @@ export { VirtualTable, genVirtualTable, type VirtualTableProps, + type Reference, }; export default Table; diff --git a/src/interface.ts b/src/interface.ts index b9eb10433..f589862fb 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -25,6 +25,17 @@ export type DefaultRecordType = Record; export type TableLayout = 'auto' | 'fixed'; +export type ScrollConfig = { + index?: number; + key?: Key; + top?: number; +}; + +export type Reference = { + nativeElement: HTMLDivElement; + scrollTo: (config: ScrollConfig) => void; +}; + // ==================== Row ===================== export type RowClassName = ( record: RecordType, @@ -135,7 +146,7 @@ export type CustomizeScrollBody = ( data: readonly RecordType[], info: { scrollbarSize: number; - ref: React.Ref<{ scrollLeft: number }>; + ref: React.Ref<{ scrollLeft: number; scrollTo?: (scrollConfig: ScrollConfig) => void }>; onScroll: OnCustomizeScroll; }, ) => React.ReactNode; diff --git a/tests/Virtual.spec.tsx b/tests/Virtual.spec.tsx index 129a745c4..35f332b38 100644 --- a/tests/Virtual.spec.tsx +++ b/tests/Virtual.spec.tsx @@ -4,14 +4,26 @@ import { _rs as onLibResize } from 'rc-resize-observer/lib/utils/observerUtil'; import { spyElementPrototypes } from 'rc-util/lib/test/domHook'; import { resetWarned } from 'rc-util/lib/warning'; import React from 'react'; -import { VirtualTable, type VirtualTableProps } from '../src'; +import { VirtualTable, type Reference, type VirtualTableProps } from '../src'; + +global.scrollToConfig = null; vi.mock('rc-virtual-list', async () => { const RealVirtualList = ((await vi.importActual('rc-virtual-list')) as any).default; - const WrapperVirtualList = React.forwardRef((props: any, ref) => ( - - )); + const WrapperVirtualList = React.forwardRef((props: any, ref) => { + const myRef = React.useRef(null); + + React.useImperativeHandle(ref, () => ({ + ...myRef.current, + scrollTo: (config: any) => { + global.scrollToConfig = config; + return myRef.current.scrollTo(config); + }, + })); + + return ; + }); return { default: WrapperVirtualList, @@ -38,6 +50,7 @@ describe('Table.Virtual', () => { beforeEach(() => { scrollLeftCalled = false; + global.scrollToConfig = null; vi.useFakeTimers(); resetWarned(); }); @@ -295,4 +308,19 @@ describe('Table.Virtual', () => { bottom: '10px', }); }); + + it('scrollTo should pass', async () => { + const tblRef = React.createRef(); + getTable({ reference: tblRef }); + + tblRef.current.scrollTo({ + index: 99, + }); + + await waitFakeTimer(); + + expect(global.scrollToConfig).toEqual({ + index: 99, + }); + }); }); diff --git a/tests/refs.spec.tsx b/tests/refs.spec.tsx new file mode 100644 index 000000000..883c7c5fc --- /dev/null +++ b/tests/refs.spec.tsx @@ -0,0 +1,65 @@ +import { render } from '@testing-library/react'; +import { spyElementPrototypes } from 'rc-util/lib/test/domHook'; +import React from 'react'; +import Table, { type Reference } from '../src'; + +describe('Table.Ref', () => { + let scrollParam: any = null; + let scrollIntoViewElement: HTMLElement = null; + + beforeAll(() => { + spyElementPrototypes(HTMLElement, { + scrollTo: (_: any, param: any) => { + scrollParam = param; + }, + scrollIntoView() { + // eslint-disable-next-line @typescript-eslint/no-this-alias + scrollIntoViewElement = this; + }, + }); + }); + + beforeEach(() => { + scrollParam = null; + }); + + it('support reference', () => { + const ref = React.createRef(); + + const { container } = render( +
, + ); + + expect(ref.current.nativeElement).toBe(container.querySelector('.rc-table')); + + // Scroll To number + ref.current.scrollTo({ + top: 903, + }); + + expect(scrollParam.top).toEqual(903); + + // Scroll index + ref.current.scrollTo({ + index: 0, + }); + expect(scrollIntoViewElement.textContent).toEqual('light'); + + // Scroll key + ref.current.scrollTo({ + key: 'bamboo', + }); + expect(scrollIntoViewElement.textContent).toEqual('bamboo'); + }); +});