Skip to content

Commit

Permalink
feat: sticky header and scroll (#505)
Browse files Browse the repository at this point in the history
* feat: sticky header and scroll

* improve style

* refactor, complete test suite

* add mouseup test

* test if left <= 0

* remove useless px

* remove useless code

* improve ref value if undefined

* fix NaN

* rename isShow -> show

* Update FixedHeader.tsx

* fix example refactor

* imporve sticky prop

* fix: trackpad scroll sync sticky scroll

* fix test fail

* imporve sticky

* add test

* remove useless code

* imporve useSticky

Co-authored-by: 07akioni <[email protected]>
  • Loading branch information
shaodahong and 07akioni authored Jul 31, 2020
1 parent 659158f commit 4acc581
Show file tree
Hide file tree
Showing 10 changed files with 554 additions and 11 deletions.
3 changes: 2 additions & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
"singleQuote": true,
"trailingComma": "all",
"proseWrap": "never",
"printWidth": 100
"printWidth": 100,
"arrowParens": "avoid"
}
27 changes: 27 additions & 0 deletions assets/index.less
Original file line number Diff line number Diff line change
Expand Up @@ -287,4 +287,31 @@
background: #fff;
}
}
&-sticky {
&-header {
position: sticky;
z-index: 10;
}
&-scroll {
position: fixed;
bottom: 0;
display: flex;
align-items: center;
border-top: 1px solid #f3f3f3;
opacity: 0.6;
transition: transform 0.1s ease-in 0s;
&:hover {
transform: scaleY(1.2);
transform-origin: center bottom;
}
&-bar {
height: 8px;
border-radius: 4px;
background-color: #bbb;
&:hover {
background-color: #999;
}
}
}
}
}
114 changes: 114 additions & 0 deletions examples/stickyHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/* eslint-disable no-console,func-names,react/no-multi-comp */
import React from 'react';
import Table from '../src';
import '../assets/index.less';
import { ColumnType } from '../src/interface';

interface RecordType {
a?: string;
b?: string;
c?: string;
}

const columns: ColumnType<{ a: string; b: string; c: string }>[] = [
{ title: 'title1', dataIndex: 'a', key: 'a', width: 100 },
{ title: 'title2', dataIndex: 'b', key: 'b', width: 100, align: 'right' },
{ title: 'title3', dataIndex: 'c', key: 'c', width: 200 },
{
title: 'Operations',
dataIndex: '',
key: 'd',
render(_, record) {
return (
<a
onClick={e => {
e.preventDefault();
console.log('Operate on:', record);
}}
href="#"
>
Operations
</a>
);
},
},
];

const data = [
{ a: '123', key: '1' },
{ a: 'cdd', b: 'edd', key: '2' },
{ a: '1333', c: 'eee', d: 2, key: '3' },
{ a: '1333', c: 'eee', d: 2, key: '4' },
{ a: '1333', c: 'eee', d: 2, key: '5' },
{ a: '1333', c: 'eee', d: 2, key: '6' },
{ a: '1333', c: 'eee', d: 2, key: '7' },
{ a: '1333', c: 'eee', d: 2, key: '8' },
{ a: '1333', c: 'eee', d: 2, key: '9' },
{ a: '1333', c: 'eee', d: 2, key: '10' },
{ a: '1333', c: 'eee', d: 2, key: '11' },
{ a: '1333', c: 'eee', d: 2, key: '12' },
{ a: '1333', c: 'eee', d: 2, key: '13' },
{ a: '1333', c: 'eee', d: 2, key: '14' },
{ a: '1333', c: 'eee', d: 2, key: '15' },
{ a: '1333', c: 'eee', d: 2, key: '16' },
{ a: '1333', c: 'eee', d: 2, key: '17' },
{ a: '1333', c: 'eee', d: 2, key: '18' },
{ a: '1333', c: 'eee', d: 2, key: '19' },
{ a: '1333', c: 'eee', d: 2, key: '20' },
];

const Demo = () => (
<div
style={{
height: 10000,
}}
>
<h2>Sticky</h2>
<Table<RecordType>
columns={columns}
data={data}
tableLayout="auto"
sticky
scroll={{
x: 10000,
}}
style={{
marginBottom: 100,
}}
/>

<h2>Show offset Header</h2>
<Table<RecordType>
columns={columns}
data={data}
tableLayout="auto"
sticky={{
offsetHeader: 100,
}}
scroll={{
x: 10000,
}}
style={{
marginBottom: 100,
}}
/>

<h2>Show offset scroll</h2>
<Table<RecordType>
columns={columns}
data={data}
tableLayout="auto"
sticky={{
offsetScroll: 100,
}}
scroll={{
x: 10000,
}}
style={{
marginBottom: 100,
}}
/>
</div>
);

export default Demo;
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
"classnames": "^2.2.5",
"raf": "^3.4.1",
"rc-resize-observer": "^0.2.0",
"rc-util": "^5.0.0",
"rc-util": "^5.0.4",
"shallowequal": "^1.1.0"
},
"devDependencies": {
Expand Down
6 changes: 4 additions & 2 deletions src/Header/FixedHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export interface FixedHeaderProps<RecordType> extends HeaderProps<RecordType> {
colWidths: number[];
columCount: number;
direction: 'ltr' | 'rtl';
fixHeader: boolean;
}

function FixedHeader<RecordType>({
Expand All @@ -33,6 +34,7 @@ function FixedHeader<RecordType>({
columCount,
stickyOffsets,
direction,
fixHeader,
...props
}: FixedHeaderProps<RecordType>) {
const { prefixCls, scrollbarSize } = React.useContext(TableContext);
Expand All @@ -47,8 +49,8 @@ function FixedHeader<RecordType>({
};

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

const flattenColumnsWithScrollbar = useMemo<ColumnType<RecordType>[]>(
Expand Down
42 changes: 36 additions & 6 deletions src/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import {
CustomizeComponent,
ColumnType,
CustomizeScrollBody,
TableSticky,
} from './interface';
import TableContext from './context/TableContext';
import BodyContext from './context/BodyContext';
Expand All @@ -67,6 +68,8 @@ import Panel from './Panel';
import Footer, { FooterComponents } from './Footer';
import { findAllChildrenKeys, renderExpandIcon } from './utils/expandUtil';
import { getCellFixedInfo } from './utils/fixUtil';
import StickyScrollBar from './stickyScrollBar';
import useSticky from './hooks/useSticky';

// Used for conditions cache
const EMPTY_DATA = [];
Expand Down Expand Up @@ -155,6 +158,8 @@ export interface TableProps<RecordType = unknown> extends LegacyExpandableProps<
internalRefs?: {
body: React.MutableRefObject<HTMLDivElement>;
};

sticky?: boolean | TableSticky;
}

function Table<RecordType extends DefaultRecordType>(props: TableProps<RecordType>) {
Expand Down Expand Up @@ -186,6 +191,8 @@ function Table<RecordType extends DefaultRecordType>(props: TableProps<RecordTyp
internalHooks,
transformColumns,
internalRefs,

sticky,
} = props;

const mergedData = data || EMPTY_DATA;
Expand Down Expand Up @@ -377,6 +384,10 @@ function Table<RecordType extends DefaultRecordType>(props: TableProps<RecordTyp
const horizonScroll = scroll && validateValue(scroll.x);
const fixColumn = horizonScroll && flattenColumns.some(({ fixed }) => fixed);

// Sticky
const stickyRef = React.useRef<{ setScrollLeft: (left: number) => void }>();
const { isSticky, offsetHeader, offsetScroll, stickyClassName } = useSticky(sticky, prefixCls);

let scrollXStyle: React.CSSProperties;
let scrollYStyle: React.CSSProperties;
let scrollTableStyle: React.CSSProperties;
Expand Down Expand Up @@ -412,11 +423,16 @@ function Table<RecordType extends DefaultRecordType>(props: TableProps<RecordTyp

const [setScrollTarget, getScrollTarget] = useTimeoutLock(null);

function forceScroll(scrollLeft: number, target: HTMLDivElement) {
function forceScroll(scrollLeft: number, target: HTMLDivElement | ((left: number) => void)) {
/* eslint-disable no-param-reassign */
if (target && target.scrollLeft !== scrollLeft) {
target.scrollLeft = scrollLeft;
if (target) {
if (typeof target === 'function') {
target(scrollLeft);
} else if (target.scrollLeft !== scrollLeft) {
target.scrollLeft = scrollLeft;
}
}

/* eslint-enable */
}

Expand All @@ -432,6 +448,7 @@ function Table<RecordType extends DefaultRecordType>(props: TableProps<RecordTyp

forceScroll(mergedScrollLeft, scrollHeaderRef.current);
forceScroll(mergedScrollLeft, scrollBodyRef.current);
forceScroll(mergedScrollLeft, stickyRef.current?.setScrollLeft);
}

if (currentTarget) {
Expand Down Expand Up @@ -495,6 +512,7 @@ function Table<RecordType extends DefaultRecordType>(props: TableProps<RecordTyp
columCount: flattenColumns.length,
stickyOffsets,
onHeaderRow,
fixHeader,
};

// Empty
Expand All @@ -513,7 +531,7 @@ function Table<RecordType extends DefaultRecordType>(props: TableProps<RecordTyp
const bodyTable = (
<Body
data={mergedData}
measureColumnWidth={fixHeader || horizonScroll}
measureColumnWidth={fixHeader || horizonScroll || isSticky}
expandedKeys={mergedExpandedKeys}
rowExpandable={rowExpandable}
getRowKey={getRowKey}
Expand All @@ -539,7 +557,7 @@ function Table<RecordType extends DefaultRecordType>(props: TableProps<RecordTyp
warning(false, '`components.body` with render props is only work on `scroll.y`.');
}

if (fixHeader) {
if (fixHeader || isSticky) {
let bodyContent: React.ReactNode;

if (typeof customizeScrollBody === 'function') {
Expand Down Expand Up @@ -581,6 +599,15 @@ function Table<RecordType extends DefaultRecordType>(props: TableProps<RecordTyp
{bodyTable}
{footerTable}
</TableComponent>

{isSticky && (
<StickyScrollBar
ref={stickyRef}
offsetScroll={offsetScroll}
scrollBodyRef={scrollBodyRef}
onScroll={onScroll}
/>
)}
</div>
);
}
Expand All @@ -592,10 +619,13 @@ function Table<RecordType extends DefaultRecordType>(props: TableProps<RecordTyp
<div
style={{
overflow: 'hidden',
...(isSticky ? { top: offsetHeader } : {}),
}}
onScroll={onScroll}
ref={scrollHeaderRef}
className={classNames(`${prefixCls}-header`)}
className={classNames(`${prefixCls}-header`, {
[stickyClassName]: !!stickyClassName,
})}
>
<FixedHeader {...headerProps} {...columnContext} direction={direction} />
</div>
Expand Down
22 changes: 22 additions & 0 deletions src/hooks/useSticky.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as React from 'react';
import { TableSticky } from '../interface';

export default function useSticky(
sticky: boolean | TableSticky,
prefixCls: string,
): {
isSticky: boolean;
offsetHeader: number;
offsetScroll: number;
stickyClassName: string;
} {
return React.useMemo(() => {
const isSticky = !!sticky;
return {
isSticky,
stickyClassName: isSticky ? `${prefixCls}-sticky-header` : '',
offsetHeader: typeof sticky === 'object' ? sticky.offsetHeader || 0 : 0,
offsetScroll: typeof sticky === 'object' ? sticky.offsetScroll || 0 : 0,
};
}, [sticky, prefixCls]);
}
9 changes: 8 additions & 1 deletion src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@ export interface ColumnType<RecordType> extends ColumnSharedType<RecordType> {

export type ColumnsType<RecordType = unknown> = (
| ColumnGroupType<RecordType>
| ColumnType<RecordType>)[];
| ColumnType<RecordType>
)[];

export type GetRowKey<RecordType> = (record: RecordType, index?: number) => Key;

Expand Down Expand Up @@ -206,3 +207,9 @@ export type TriggerEventHandler<RecordType> = (
record: RecordType,
event: React.MouseEvent<HTMLElement>,
) => void;

// =================== Sticky ===================
export interface TableSticky {
offsetHeader?: number;
offsetScroll?: number;
}
Loading

0 comments on commit 4acc581

Please sign in to comment.