diff --git a/src/Header/FixedHeader.tsx b/src/Header/FixedHeader.tsx index 2c2658bb6..3a04172c5 100644 --- a/src/Header/FixedHeader.tsx +++ b/src/Header/FixedHeader.tsx @@ -1,5 +1,7 @@ import * as React from 'react'; import { useMemo } from 'react'; +import classNames from 'classnames'; +import { fillRef } from 'rc-util/lib/ref'; import Header, { HeaderProps } from './Header'; import ColGroup from '../ColGroup'; import { ColumnsType, ColumnType } from '../interface'; @@ -26,74 +28,126 @@ export interface FixedHeaderProps extends HeaderProps { columCount: number; direction: 'ltr' | 'rtl'; fixHeader: boolean; + offsetHeader: number; + stickyClassName?: string; + onScroll: (info: { currentTarget: HTMLDivElement; scrollLeft?: number }) => void; } -function FixedHeader({ - noData, - columns, - flattenColumns, - colWidths, - columCount, - stickyOffsets, - direction, - fixHeader, - ...props -}: FixedHeaderProps) { - const { prefixCls, scrollbarSize, isSticky } = React.useContext(TableContext); - - const combinationScrollBarSize = isSticky && !fixHeader ? 0 : scrollbarSize; - - // Add scrollbar column - const lastColumn = flattenColumns[flattenColumns.length - 1]; - const ScrollBarColumn: ColumnType = { - fixed: lastColumn ? lastColumn.fixed : null, - onHeaderCell: () => ({ - className: `${prefixCls}-cell-scrollbar`, - }), - }; - - const columnsWithScrollbar = useMemo>( - () => (combinationScrollBarSize ? [...columns, ScrollBarColumn] : columns), - [combinationScrollBarSize, columns], - ); - - const flattenColumnsWithScrollbar = useMemo[]>( - () => (combinationScrollBarSize ? [...flattenColumns, ScrollBarColumn] : flattenColumns), - [combinationScrollBarSize, flattenColumns], - ); - - // Calculate the sticky offsets - const headerStickyOffsets = useMemo(() => { - const { right, left } = stickyOffsets; - return { - ...stickyOffsets, - left: - direction === 'rtl' ? [...left.map(width => width + combinationScrollBarSize), 0] : left, - right: - direction === 'rtl' ? right : [...right.map(width => width + combinationScrollBarSize), 0], - isSticky, +const FixedHeader = React.forwardRef>( + ( + { + noData, + columns, + flattenColumns, + colWidths, + columCount, + stickyOffsets, + direction, + fixHeader, + offsetHeader, + stickyClassName, + onScroll, + ...props + }, + ref, + ) => { + const { prefixCls, scrollbarSize, isSticky } = React.useContext(TableContext); + + const combinationScrollBarSize = isSticky && !fixHeader ? 0 : scrollbarSize; + + // Pass wheel to scroll event + const scrollRef = React.useRef(null); + + const setScrollRef = React.useCallback((element: HTMLElement) => { + fillRef(ref, element); + fillRef(scrollRef, element); + }, []); + + React.useEffect(() => { + function onWheel(e: WheelEvent) { + const { currentTarget, deltaX } = (e as unknown) as React.WheelEvent; + if (deltaX) { + onScroll({ currentTarget, scrollLeft: currentTarget.scrollLeft + deltaX }); + e.preventDefault(); + } + } + scrollRef.current?.addEventListener('wheel', onWheel); + + return () => { + scrollRef.current?.removeEventListener('wheel', onWheel); + }; + }, []); + + // Add scrollbar column + const lastColumn = flattenColumns[flattenColumns.length - 1]; + const ScrollBarColumn: ColumnType = { + fixed: lastColumn ? lastColumn.fixed : null, + onHeaderCell: () => ({ + className: `${prefixCls}-cell-scrollbar`, + }), }; - }, [combinationScrollBarSize, stickyOffsets, isSticky]); - - const mergedColumnWidth = useColumnWidth(colWidths, columCount); - - return ( - - -
-
- ); -} + + const columnsWithScrollbar = useMemo>( + () => (combinationScrollBarSize ? [...columns, ScrollBarColumn] : columns), + [combinationScrollBarSize, columns], + ); + + const flattenColumnsWithScrollbar = useMemo[]>( + () => (combinationScrollBarSize ? [...flattenColumns, ScrollBarColumn] : flattenColumns), + [combinationScrollBarSize, flattenColumns], + ); + + // Calculate the sticky offsets + const headerStickyOffsets = useMemo(() => { + const { right, left } = stickyOffsets; + return { + ...stickyOffsets, + left: + direction === 'rtl' ? [...left.map(width => width + combinationScrollBarSize), 0] : left, + right: + direction === 'rtl' + ? right + : [...right.map(width => width + combinationScrollBarSize), 0], + isSticky, + }; + }, [combinationScrollBarSize, stickyOffsets, isSticky]); + + const mergedColumnWidth = useColumnWidth(colWidths, columCount); + + return ( +
+ + +
+
+
+ ); + }, +); + +FixedHeader.displayName = 'FixedHeader'; export default FixedHeader; diff --git a/src/Table.tsx b/src/Table.tsx index 0c99f6055..8ca03b634 100644 --- a/src/Table.tsx +++ b/src/Table.tsx @@ -438,7 +438,10 @@ function Table(props: TableProps & { scrollLeft?: number }) => { + }: { + currentTarget: HTMLElement; + scrollLeft?: number; + }) => { const mergedScrollLeft = typeof scrollLeft === 'number' ? scrollLeft : currentTarget.scrollLeft; const compareTarget = currentTarget || EMPTY_SCROLL_TARGET; @@ -615,24 +618,17 @@ function Table(props: TableProps {/* Header Table */} {showHeader !== false && ( -
- -
+ onScroll={onScroll} + /> )} {/* Body Table */} diff --git a/src/hooks/useSticky.ts b/src/hooks/useSticky.ts index 595decc74..1190f3af0 100644 --- a/src/hooks/useSticky.ts +++ b/src/hooks/useSticky.ts @@ -1,6 +1,7 @@ import * as React from 'react'; import { TableSticky } from '../interface'; +/** Sticky header hooks */ export default function useSticky( sticky: boolean | TableSticky, prefixCls: string, diff --git a/tests/FixedHeader.spec.js b/tests/FixedHeader.spec.js index bcc426213..99a2198d3 100644 --- a/tests/FixedHeader.spec.js +++ b/tests/FixedHeader.spec.js @@ -17,9 +17,21 @@ describe('Table.FixedHeader', () => { />, ); - wrapper.find('ResizeObserver').at(0).props().onResize({ width: 100, offsetWidth: 100 }); - wrapper.find('ResizeObserver').at(1).props().onResize({ width: 200, offsetWidth: 200 }); - wrapper.find('ResizeObserver').at(2).props().onResize({ width: 0, offsetWidth: 0 }); + wrapper + .find('ResizeObserver') + .at(0) + .props() + .onResize({ width: 100, offsetWidth: 100 }); + wrapper + .find('ResizeObserver') + .at(1) + .props() + .onResize({ width: 200, offsetWidth: 200 }); + wrapper + .find('ResizeObserver') + .at(2) + .props() + .onResize({ width: 0, offsetWidth: 0 }); act(() => { jest.runAllTimers(); @@ -29,16 +41,41 @@ describe('Table.FixedHeader', () => { expect(wrapper.find('.rc-table-header table').props().style.visibility).toBeFalsy(); expect(); - expect(wrapper.find('colgroup col').at(0).props().style.width).toEqual(100); - expect(wrapper.find('colgroup col').at(1).props().style.width).toEqual(200); - expect(wrapper.find('colgroup col').at(2).props().style.width).toEqual(0); + expect( + wrapper + .find('colgroup col') + .at(0) + .props().style.width, + ).toEqual(100); + expect( + wrapper + .find('colgroup col') + .at(1) + .props().style.width, + ).toEqual(200); + expect( + wrapper + .find('colgroup col') + .at(2) + .props().style.width, + ).toEqual(0); // Update columns wrapper.setProps({ columns: [col2, col1] }); wrapper.update(); - expect(wrapper.find('colgroup col').at(0).props().style.width).toEqual(200); - expect(wrapper.find('colgroup col').at(1).props().style.width).toEqual(100); + expect( + wrapper + .find('colgroup col') + .at(0) + .props().style.width, + ).toEqual(200); + expect( + wrapper + .find('colgroup col') + .at(1) + .props().style.width, + ).toEqual(100); jest.useRealTimers(); }); @@ -58,12 +95,22 @@ describe('Table.FixedHeader', () => { />, ); - expect(wrapper.find('table').last().find('colgroup col').first().props().className).toEqual( - 'test-internal', - ); - expect(wrapper.find('table').first().find('colgroup col').first().props().className).toEqual( - 'test-internal', - ); + expect( + wrapper + .find('table') + .last() + .find('colgroup col') + .first() + .props().className, + ).toEqual('test-internal'); + expect( + wrapper + .find('table') + .first() + .find('colgroup col') + .first() + .props().className, + ).toEqual('test-internal'); }); it('show header when data is null', () => { @@ -95,4 +142,24 @@ describe('Table.FixedHeader', () => { expect.objectContaining({ visibility: null }), ); }); + + it('rtl', () => { + const wrapper = mount( + , + ); + + expect(wrapper.find('Header').props().stickyOffsets).toEqual( + expect.objectContaining({ + isSticky: false, + left: [expect.anything(), expect.anything()], + }), + ); + }); }); diff --git a/tests/Scroll.spec.js b/tests/Scroll.spec.js index 2ba32d3cf..75c36ff31 100644 --- a/tests/Scroll.spec.js +++ b/tests/Scroll.spec.js @@ -5,10 +5,7 @@ import { spyElementPrototypes } from 'rc-util/lib/test/domHook'; import Table from '../src'; describe('Table.Scroll', () => { - const data = [ - { key: 'key0', name: 'Lucy' }, - { key: 'key1', name: 'Jack' }, - ]; + const data = [{ key: 'key0', name: 'Lucy' }, { key: 'key1', name: 'Jack' }]; const createTable = props => { const columns = [{ title: 'Name', dataIndex: 'name', key: 'name' }]; @@ -91,18 +88,17 @@ describe('Table.Scroll', () => { jest.runAllTimers(); // Use `onScroll` directly since simulate not support `currentTarget` act(() => { - wrapper - .find('.rc-table-header') - .props() - .onScroll({ - currentTarget: { - scrollLeft: 10, - scrollWidth: 200, - clientWidth: 100, - }, - }); + const headerDiv = wrapper.find('div.rc-table-header').instance(); + + const wheelEvent = new WheelEvent('wheel'); + Object.defineProperty(wheelEvent, 'deltaX', { + get: () => 10, + }); + + headerDiv.dispatchEvent(wheelEvent); + jest.runAllTimers(); }); - jest.runAllTimers(); + expect(setScrollLeft).toHaveBeenCalledWith(undefined, 10); setScrollLeft.mockReset();