Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 120 additions & 66 deletions src/Header/FixedHeader.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -26,74 +28,126 @@ export interface FixedHeaderProps<RecordType> extends HeaderProps<RecordType> {
columCount: number;
direction: 'ltr' | 'rtl';
fixHeader: boolean;
offsetHeader: number;
stickyClassName?: string;
onScroll: (info: { currentTarget: HTMLDivElement; scrollLeft?: number }) => void;
}

function FixedHeader<RecordType>({
noData,
columns,
flattenColumns,
colWidths,
columCount,
stickyOffsets,
direction,
fixHeader,
...props
}: FixedHeaderProps<RecordType>) {
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<RecordType> = {
fixed: lastColumn ? lastColumn.fixed : null,
onHeaderCell: () => ({
className: `${prefixCls}-cell-scrollbar`,
}),
};

const columnsWithScrollbar = useMemo<ColumnsType<RecordType>>(
() => (combinationScrollBarSize ? [...columns, ScrollBarColumn] : columns),
[combinationScrollBarSize, columns],
);

const flattenColumnsWithScrollbar = useMemo<ColumnType<RecordType>[]>(
() => (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<HTMLDivElement, FixedHeaderProps<unknown>>(
(
{
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<HTMLDivElement>(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<HTMLDivElement>;
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<unknown> = {
fixed: lastColumn ? lastColumn.fixed : null,
onHeaderCell: () => ({
className: `${prefixCls}-cell-scrollbar`,
}),
};
}, [combinationScrollBarSize, stickyOffsets, isSticky]);

const mergedColumnWidth = useColumnWidth(colWidths, columCount);

return (
<table
style={{ tableLayout: 'fixed', visibility: noData || mergedColumnWidth ? null : 'hidden' }}
>
<ColGroup
colWidths={mergedColumnWidth ? [...mergedColumnWidth, combinationScrollBarSize] : []}
columCount={columCount + 1}
columns={flattenColumnsWithScrollbar}
/>
<Header
{...props}
stickyOffsets={headerStickyOffsets}
columns={columnsWithScrollbar}
flattenColumns={flattenColumnsWithScrollbar}
/>
</table>
);
}

const columnsWithScrollbar = useMemo<ColumnsType<unknown>>(
() => (combinationScrollBarSize ? [...columns, ScrollBarColumn] : columns),
[combinationScrollBarSize, columns],
);

const flattenColumnsWithScrollbar = useMemo<ColumnType<unknown>[]>(
() => (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 (
<div
style={{
overflow: 'hidden',
...(isSticky ? { top: offsetHeader } : {}),
}}
ref={setScrollRef}
className={classNames(`${prefixCls}-header`, {
[stickyClassName]: !!stickyClassName,
})}
>
<table
style={{
tableLayout: 'fixed',
visibility: noData || mergedColumnWidth ? null : 'hidden',
}}
>
<ColGroup
colWidths={mergedColumnWidth ? [...mergedColumnWidth, combinationScrollBarSize] : []}
columCount={columCount + 1}
columns={flattenColumnsWithScrollbar}
/>
<Header
{...props}
stickyOffsets={headerStickyOffsets}
columns={columnsWithScrollbar}
flattenColumns={flattenColumnsWithScrollbar}
/>
</table>
</div>
);
},
);

FixedHeader.displayName = 'FixedHeader';

export default FixedHeader;
32 changes: 14 additions & 18 deletions src/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,10 @@ function Table<RecordType extends DefaultRecordType>(props: TableProps<RecordTyp
const onScroll = ({
currentTarget,
scrollLeft,
}: React.UIEvent<HTMLDivElement> & { scrollLeft?: number }) => {
}: {
currentTarget: HTMLElement;
scrollLeft?: number;
}) => {
const mergedScrollLeft = typeof scrollLeft === 'number' ? scrollLeft : currentTarget.scrollLeft;

const compareTarget = currentTarget || EMPTY_SCROLL_TARGET;
Expand Down Expand Up @@ -615,24 +618,17 @@ function Table<RecordType extends DefaultRecordType>(props: TableProps<RecordTyp
<>
{/* Header Table */}
{showHeader !== false && (
<div
style={{
overflow: 'hidden',
...(isSticky ? { top: offsetHeader } : {}),
}}
onScroll={onScroll}
<FixedHeader
noData={!mergedData.length}
{...headerProps}
{...columnContext}
direction={direction}
// Fixed Props
offsetHeader={offsetHeader}
stickyClassName={stickyClassName}
ref={scrollHeaderRef}
className={classNames(`${prefixCls}-header`, {
[stickyClassName]: !!stickyClassName,
})}
>
<FixedHeader
noData={!mergedData.length}
{...headerProps}
{...columnContext}
direction={direction}
/>
</div>
onScroll={onScroll}
/>
)}

{/* Body Table */}
Expand Down
1 change: 1 addition & 0 deletions src/hooks/useSticky.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
95 changes: 81 additions & 14 deletions tests/FixedHeader.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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();
});
Expand All @@ -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', () => {
Expand Down Expand Up @@ -95,4 +142,24 @@ describe('Table.FixedHeader', () => {
expect.objectContaining({ visibility: null }),
);
});

it('rtl', () => {
const wrapper = mount(
<Table
columns={[{ dataIndex: 'light', width: 100 }]}
data={[{ key: 0, light: 'bamboo' }]}
direction="rtl"
scroll={{
y: 100,
}}
/>,
);

expect(wrapper.find('Header').props().stickyOffsets).toEqual(
expect.objectContaining({
isSticky: false,
left: [expect.anything(), expect.anything()],
}),
);
});
});
Loading