diff --git a/assets/index.less b/assets/index.less index bfa245b03..f6c23c50a 100644 --- a/assets/index.less +++ b/assets/index.less @@ -56,12 +56,6 @@ } // ================== Cell ================== - &-fixed-column-gapped { - .@{tablePrefixCls}-cell-fix-left-last::after, - .@{tablePrefixCls}-cell-fix-right-first::after { - display: none !important; - } - } &-cell { background: #f4f4f4; diff --git a/src/Body/BodyRow.tsx b/src/Body/BodyRow.tsx index d33cf38dc..51ce174e2 100644 --- a/src/Body/BodyRow.tsx +++ b/src/Body/BodyRow.tsx @@ -167,6 +167,7 @@ function BodyRow( key={key} record={record} index={index} + colIndex={colIndex} renderIndex={renderIndex} dataIndex={dataIndex} render={render} diff --git a/src/Cell/index.tsx b/src/Cell/index.tsx index 90bb04a48..da21a2f91 100644 --- a/src/Cell/index.tsx +++ b/src/Cell/index.tsx @@ -22,6 +22,8 @@ export interface CellProps { record?: RecordType; /** `column` index is the real show rowIndex */ index?: number; + /** the column index which cell in */ + colIndex?: number; /** the index of the record. For the render(value, record, renderIndex) */ renderIndex?: number; dataIndex?: DataIndex; @@ -72,7 +74,10 @@ const getTitleFromCellRenderChildren = ({ return title; }; -function Cell(props: CellProps) { +function Cell( + props: CellProps, + ref: React.ForwardedRef, +) { if (process.env.NODE_ENV !== 'production') { devRenderTimes(props); } @@ -99,6 +104,9 @@ function Cell(props: CellProps) { index, rowType, + // Col, + colIndex, + // Span colSpan, rowSpan, @@ -118,11 +126,14 @@ function Cell(props: CellProps) { } = props; const cellPrefixCls = `${prefixCls}-cell`; - const { supportSticky, allColumnsFixedLeft, rowHoverable } = useContext(TableContext, [ - 'supportSticky', - 'allColumnsFixedLeft', - 'rowHoverable', - ]); + const { supportSticky, allColumnsFixedLeft, rowHoverable, bodyScrollLeft, headerCellRefs } = + useContext(TableContext, [ + 'supportSticky', + 'allColumnsFixedLeft', + 'rowHoverable', + 'bodyScrollLeft', + 'headerCellRefs', + ]); // ====================== Value ======================= const [childNode, legacyCellProps] = useCellRender( @@ -171,6 +182,19 @@ function Cell(props: CellProps) { additionalProps?.onMouseLeave?.(event); }); + const mergedLastFixLeft = React.useMemo(() => { + const { current } = headerCellRefs; + const dom = current[colIndex]; + + if (lastFixLeft && dom && typeof fixLeft === 'number') { + const offsetLeft = + dom.getBoundingClientRect().x - dom.parentElement.getBoundingClientRect().x || 0; + + // should not be tagged as lastFixLeft if cell is not stickying; + return offsetLeft === fixLeft + bodyScrollLeft; + } + return lastFixLeft; + }, [bodyScrollLeft, colIndex, fixLeft, headerCellRefs, lastFixLeft]); // ====================== Render ====================== if (mergedColSpan === 0 || mergedRowSpan === 0) { return null; @@ -192,8 +216,8 @@ function Cell(props: CellProps) { { [`${cellPrefixCls}-fix-left`]: isFixLeft && supportSticky, [`${cellPrefixCls}-fix-left-first`]: firstFixLeft && supportSticky, - [`${cellPrefixCls}-fix-left-last`]: lastFixLeft && supportSticky, - [`${cellPrefixCls}-fix-left-all`]: lastFixLeft && allColumnsFixedLeft && supportSticky, + [`${cellPrefixCls}-fix-left-last`]: mergedLastFixLeft && supportSticky, + [`${cellPrefixCls}-fix-left-all`]: mergedLastFixLeft && allColumnsFixedLeft && supportSticky, [`${cellPrefixCls}-fix-right`]: isFixRight && supportSticky, [`${cellPrefixCls}-fix-right-first`]: firstFixRight && supportSticky, [`${cellPrefixCls}-fix-right-last`]: lastFixRight && supportSticky, @@ -237,6 +261,7 @@ function Cell(props: CellProps) { return ( (props: CellProps) { ); } -export default React.memo(Cell) as typeof Cell; +export default React.memo(React.forwardRef(Cell)) as ( + props: CellProps & { ref?: React.ForwardedRef }, +) => React.JSX.Element; diff --git a/src/Header/HeaderRow.tsx b/src/Header/HeaderRow.tsx index fe7629c64..45e4dec78 100644 --- a/src/Header/HeaderRow.tsx +++ b/src/Header/HeaderRow.tsx @@ -32,7 +32,11 @@ const HeaderRow = (props: RowProps) => { onHeaderRow, index, } = props; - const { prefixCls, direction } = useContext(TableContext, ['prefixCls', 'direction']); + const { prefixCls, direction, headerCellRefs } = useContext(TableContext, [ + 'prefixCls', + 'direction', + 'headerCellRefs', + ]); let rowProps: React.HTMLAttributes; if (onHeaderRow) { rowProps = onHeaderRow( @@ -43,6 +47,11 @@ const HeaderRow = (props: RowProps) => { const columnsKey = getColumnsKey(cells.map(cell => cell.column)); + const handleHeaderCellRef = + (cellIndex: number) => (headerCellRef: HTMLTableCellElement | null) => { + headerCellRefs.current[cellIndex] = headerCellRef; + }; + return ( {cells.map((cell: CellType, cellIndex) => { @@ -62,6 +71,8 @@ const HeaderRow = (props: RowProps) => { return ( 1 ? 'colgroup' : 'col') : null} ellipsis={column.ellipsis} diff --git a/src/Table.tsx b/src/Table.tsx index a1db3de5b..ce1df4675 100644 --- a/src/Table.tsx +++ b/src/Table.tsx @@ -435,12 +435,14 @@ function Table( } } } + const [bodyScrollLeft, setBodyScrollLeft] = React.useState(0); const onInternalScroll = useEvent( ({ currentTarget, scrollLeft }: { currentTarget: HTMLElement; scrollLeft?: number }) => { const isRTL = direction === 'rtl'; const mergedScrollLeft = typeof scrollLeft === 'number' ? scrollLeft : currentTarget.scrollLeft; + setBodyScrollLeft(mergedScrollLeft); const compareTarget = currentTarget || EMPTY_SCROLL_TARGET; if (!getScrollTarget() || getScrollTarget() === compareTarget) { @@ -796,6 +798,7 @@ function Table( const fixedInfoList = useFixedInfo(flattenColumns, stickyOffsets, direction); + const headerCellRefs = React.useRef([]); const TableContextValue = React.useMemo( () => ({ // Scroll @@ -846,6 +849,8 @@ function Table( childrenColumnName: mergedChildrenColumnName, rowHoverable, + bodyScrollLeft, + headerCellRefs, }), [ // Scroll @@ -895,6 +900,8 @@ function Table( mergedChildrenColumnName, rowHoverable, + bodyScrollLeft, + headerCellRefs, ], ); diff --git a/src/context/TableContext.tsx b/src/context/TableContext.tsx index f566c84f0..70b079266 100644 --- a/src/context/TableContext.tsx +++ b/src/context/TableContext.tsx @@ -66,8 +66,9 @@ export interface TableContextProps { expandedKeys: Set; getRowKey: GetRowKey; childrenColumnName: string; - + headerCellRefs: React.MutableRefObject; rowHoverable?: boolean; + bodyScrollLeft?: number; } const TableContext = createContext(); diff --git a/tests/FixedColumn.spec.tsx b/tests/FixedColumn.spec.tsx index 50a21baf9..c4a3b9fa8 100644 --- a/tests/FixedColumn.spec.tsx +++ b/tests/FixedColumn.spec.tsx @@ -25,6 +25,31 @@ describe('Table.FixedColumn', () => { offsetWidth: { get: () => 1000, }, + getBoundingClientRect() { + if (this.tagName === 'TR') { + return { + x: 0, + y: 0, + width: 1000, + height: 105, + }; + } + if (this.textContent === 'title2') { + return { + x: 93, + y: 0, + width: 93, + height: 105, + }; + } + + return { + x: 0, + y: 0, + width: 100, + height: 100, + }; + }, }); }); @@ -249,6 +274,20 @@ describe('Table.FixedColumn', () => { it('when all columns fixed left,cell should has classname rc-table-cell-fix-left-all', async () => { const wrapper = mount(); + + // make `mergedLastFixLeft`'s calculation work. + act(() => { + wrapper + .find(RcResizeObserver.Collection) + .first() + .props() + .onBatchResize([ + { + data: wrapper.find('table ResizeObserver').first().props().data, + size: { width: 93, offsetWidth: 93 }, + } as any, + ]); + }); await safeAct(wrapper); expect(wrapper.find('.rc-table-cell-fix-left-all')).toHaveLength(10); }); diff --git a/tests/Scroll.spec.jsx b/tests/Scroll.spec.jsx index 387e6cd5f..8e726972c 100644 --- a/tests/Scroll.spec.jsx +++ b/tests/Scroll.spec.jsx @@ -3,6 +3,7 @@ import { spyElementPrototypes } from 'rc-util/lib/test/domHook'; import React from 'react'; import { act } from 'react-dom/test-utils'; import Table from '../src'; +import { safeAct } from './utils'; describe('Table.Scroll', () => { const data = [ @@ -49,10 +50,10 @@ describe('Table.Scroll', () => { expect(wrapper.find('.rc-table-body').props().style.overflowY).toEqual('scroll'); }); - it('fire scroll event', () => { - vi.useFakeTimers(); + describe('scroll', () => { let scrollLeft = 0; let scrollTop = 0; + let domSpy; const setScrollLeft = vi.fn((_, val) => { scrollLeft = val; @@ -60,80 +61,186 @@ describe('Table.Scroll', () => { const setScrollTop = vi.fn((_, val) => { scrollTop = val; }); + beforeEach(() => { + vi.useFakeTimers(); + domSpy = spyElementPrototypes(HTMLElement, { + scrollLeft: { + get: () => scrollLeft, + set: setScrollLeft, + }, + scrollTop: { + get: () => scrollTop, + set: setScrollTop, + }, + scrollWidth: { + get: () => 200, + }, + clientWidth: { + get: () => 100, + }, + offsetWidth: { + get: () => 200, + }, + getBoundingClientRect() { + if (this.tagName === 'TR') { + return { + x: 0, + y: 0, + width: 1000, + height: 105, + }; + } + if (this.textContent === 'title3') { + return { + x: 400, + y: 0, + width: 100, + height: 105, + }; + } - const domSpy = spyElementPrototypes(HTMLDivElement, { - scrollLeft: { - get: () => scrollLeft, - set: setScrollLeft, - }, - scrollTop: { - get: () => scrollTop, - set: setScrollTop, - }, - scrollWidth: { - get: () => 200, - }, - clientWidth: { - get: () => 100, - }, + return { + x: 0, + y: 0, + width: 100, + height: 100, + }; + }, + }); }); - const newColumns = [ - { title: 'title1', dataIndex: 'a', key: 'a', width: 100, fixed: 'left' }, - { title: 'title2', dataIndex: 'b', key: 'b' }, - { title: 'title3', dataIndex: 'c', key: 'c' }, - { title: 'title4', dataIndex: 'd', key: 'd', width: 100, fixed: 'right' }, - ]; - const newData = [ - { a: '123', b: 'xxxxxxxx', c: 3, d: 'hehe', key: '1' }, - { a: 'cdd', b: 'edd12221', c: 3, d: 'haha', key: '2' }, - ]; - const wrapper = mount( -
, - ); - - vi.runAllTimers(); - // Use `onScroll` directly since simulate not support `currentTarget` - act(() => { - const headerDiv = wrapper.find('div.rc-table-header').instance(); - - const wheelEvent = new WheelEvent('wheel'); - Object.defineProperty(wheelEvent, 'deltaX', { - get: () => 10, - }); + afterEach(() => { + setScrollLeft.mockReset(); - headerDiv.dispatchEvent(wheelEvent); - vi.runAllTimers(); + domSpy.mockRestore(); + vi.useRealTimers(); }); - expect(setScrollLeft).toHaveBeenCalledWith(undefined, 10); - setScrollLeft.mockReset(); - - act(() => { - wrapper - .find('.rc-table-body') - .props() - .onScroll({ - currentTarget: { - scrollLeft: 33, - scrollWidth: 200, - clientWidth: 100, - }, + it('scroll event', () => { + const newColumns = [ + { title: 'title1', dataIndex: 'a', key: 'a', width: 100, fixed: 'left' }, + { title: 'title2', dataIndex: 'b', key: 'b' }, + { title: 'title3', dataIndex: 'c', key: 'c' }, + { title: 'title4', dataIndex: 'd', key: 'd', width: 100, fixed: 'right' }, + ]; + const newData = [ + { a: '123', b: 'xxxxxxxx', c: 3, d: 'hehe', key: '1' }, + { a: 'cdd', b: 'edd12221', c: 3, d: 'haha', key: '2' }, + ]; + const wrapper = mount( +
, + ); + + vi.runAllTimers(); + // Use `onScroll` directly since simulate not support `currentTarget` + act(() => { + const headerDiv = wrapper.find('div.rc-table-header').instance(); + + const wheelEvent = new WheelEvent('wheel'); + Object.defineProperty(wheelEvent, 'deltaX', { + get: () => 10, }); + + headerDiv.dispatchEvent(wheelEvent); + vi.runAllTimers(); + }); + + expect(setScrollLeft).toHaveBeenCalledWith(undefined, 10); + setScrollLeft.mockReset(); + + act(() => { + wrapper + .find('.rc-table-body') + .props() + .onScroll({ + currentTarget: { + scrollLeft: 33, + scrollWidth: 200, + clientWidth: 100, + }, + }); + }); + vi.runAllTimers(); + expect(setScrollLeft).toHaveBeenCalledWith(undefined, 33); }); - vi.runAllTimers(); - expect(setScrollLeft).toHaveBeenCalledWith(undefined, 33); - setScrollLeft.mockReset(); - domSpy.mockRestore(); - vi.useRealTimers(); + it('cell should have not have classname `rc-table-cell-fix-left-last` before being sticky', async () => { + // const cellSpy = spyElementPrototypes(HTMLElement, { + // offsetParent: { + // get: () => ({}), + // }, + + // }); + + const newColumns = [ + { title: 'title1', dataIndex: 'a', key: 'a', width: 100, fixed: 'left' }, + { + title: 'title2', + dataIndex: 'b', + key: 'b', + width: 100, + render: () => 1111, + }, + { title: 'title3', dataIndex: 'c', key: 'c', width: 100, fixed: 'left' }, + { title: 'title4', dataIndex: 'b', key: 'd' }, + { title: 'title5', dataIndex: 'b', key: 'e' }, + { title: 'title6', dataIndex: 'b', key: 'f' }, + { title: 'title7', dataIndex: 'b', key: 'g' }, + { title: 'title8', dataIndex: 'b', key: 'h' }, + { title: 'title9', dataIndex: 'b', key: 'i' }, + { title: 'title10', dataIndex: 'b', key: 'j' }, + { title: 'title11', dataIndex: 'b', key: 'k' }, + { title: 'title12', dataIndex: 'b', key: 'l', width: 100, fixed: 'right' }, + ]; + const data = [ + { a: '123', b: 'xxxxxxxx', d: 3, key: '1' }, + { a: 'cdd', b: 'edd12221', d: 3, key: '2' }, + { a: '133', c: 'edd12221', d: 2, key: '3' }, + { a: '133', c: 'edd12221', d: 2, key: '4' }, + { a: '133', c: 'edd12221', d: 2, key: '5' }, + { a: '133', c: 'edd12221', d: 2, key: '6' }, + { a: '133', c: 'edd12221', d: 2, key: '7' }, + { a: '133', c: 'edd12221', d: 2, key: '8' }, + { a: '133', c: 'edd12221', d: 2, key: '9' }, + ]; + const wrapper = mount( +
, + ); + + vi.runAllTimers(); + await safeAct(wrapper); + console.log(wrapper.find('th').first(), wrapper.find('th').at(2)); + + expect(wrapper.find('th').first().props().className).toContain('rc-table-cell-fix-left-last'); + expect(wrapper.find('th').at(2).props().className).not.toContain( + 'rc-table-cell-fix-left-last', + ); + + act(() => { + wrapper + .find('.rc-table-body') + .props() + .onScroll({ + currentTarget: { + scrollLeft: 200, + scrollWidth: 200, + clientWidth: 100, + }, + }); + }); + vi.runAllTimers(); + setScrollLeft.mockReset(); + await safeAct(wrapper); + + expect(wrapper.find('th').at(2).props().className).toContain('rc-table-cell-fix-left-last'); + }); }); it('trigger inner scrollTo when set `top` 0 after render', () => {