Skip to content

Commit 4acc581

Browse files
shaodahong07akioni
andauthored
feat: sticky header and scroll (#505)
* 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]>
1 parent 659158f commit 4acc581

File tree

10 files changed

+554
-11
lines changed

10 files changed

+554
-11
lines changed

.prettierrc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22
"singleQuote": true,
33
"trailingComma": "all",
44
"proseWrap": "never",
5-
"printWidth": 100
5+
"printWidth": 100,
6+
"arrowParens": "avoid"
67
}

assets/index.less

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,4 +287,31 @@
287287
background: #fff;
288288
}
289289
}
290+
&-sticky {
291+
&-header {
292+
position: sticky;
293+
z-index: 10;
294+
}
295+
&-scroll {
296+
position: fixed;
297+
bottom: 0;
298+
display: flex;
299+
align-items: center;
300+
border-top: 1px solid #f3f3f3;
301+
opacity: 0.6;
302+
transition: transform 0.1s ease-in 0s;
303+
&:hover {
304+
transform: scaleY(1.2);
305+
transform-origin: center bottom;
306+
}
307+
&-bar {
308+
height: 8px;
309+
border-radius: 4px;
310+
background-color: #bbb;
311+
&:hover {
312+
background-color: #999;
313+
}
314+
}
315+
}
316+
}
290317
}

examples/stickyHeader.tsx

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/* eslint-disable no-console,func-names,react/no-multi-comp */
2+
import React from 'react';
3+
import Table from '../src';
4+
import '../assets/index.less';
5+
import { ColumnType } from '../src/interface';
6+
7+
interface RecordType {
8+
a?: string;
9+
b?: string;
10+
c?: string;
11+
}
12+
13+
const columns: ColumnType<{ a: string; b: string; c: string }>[] = [
14+
{ title: 'title1', dataIndex: 'a', key: 'a', width: 100 },
15+
{ title: 'title2', dataIndex: 'b', key: 'b', width: 100, align: 'right' },
16+
{ title: 'title3', dataIndex: 'c', key: 'c', width: 200 },
17+
{
18+
title: 'Operations',
19+
dataIndex: '',
20+
key: 'd',
21+
render(_, record) {
22+
return (
23+
<a
24+
onClick={e => {
25+
e.preventDefault();
26+
console.log('Operate on:', record);
27+
}}
28+
href="#"
29+
>
30+
Operations
31+
</a>
32+
);
33+
},
34+
},
35+
];
36+
37+
const data = [
38+
{ a: '123', key: '1' },
39+
{ a: 'cdd', b: 'edd', key: '2' },
40+
{ a: '1333', c: 'eee', d: 2, key: '3' },
41+
{ a: '1333', c: 'eee', d: 2, key: '4' },
42+
{ a: '1333', c: 'eee', d: 2, key: '5' },
43+
{ a: '1333', c: 'eee', d: 2, key: '6' },
44+
{ a: '1333', c: 'eee', d: 2, key: '7' },
45+
{ a: '1333', c: 'eee', d: 2, key: '8' },
46+
{ a: '1333', c: 'eee', d: 2, key: '9' },
47+
{ a: '1333', c: 'eee', d: 2, key: '10' },
48+
{ a: '1333', c: 'eee', d: 2, key: '11' },
49+
{ a: '1333', c: 'eee', d: 2, key: '12' },
50+
{ a: '1333', c: 'eee', d: 2, key: '13' },
51+
{ a: '1333', c: 'eee', d: 2, key: '14' },
52+
{ a: '1333', c: 'eee', d: 2, key: '15' },
53+
{ a: '1333', c: 'eee', d: 2, key: '16' },
54+
{ a: '1333', c: 'eee', d: 2, key: '17' },
55+
{ a: '1333', c: 'eee', d: 2, key: '18' },
56+
{ a: '1333', c: 'eee', d: 2, key: '19' },
57+
{ a: '1333', c: 'eee', d: 2, key: '20' },
58+
];
59+
60+
const Demo = () => (
61+
<div
62+
style={{
63+
height: 10000,
64+
}}
65+
>
66+
<h2>Sticky</h2>
67+
<Table<RecordType>
68+
columns={columns}
69+
data={data}
70+
tableLayout="auto"
71+
sticky
72+
scroll={{
73+
x: 10000,
74+
}}
75+
style={{
76+
marginBottom: 100,
77+
}}
78+
/>
79+
80+
<h2>Show offset Header</h2>
81+
<Table<RecordType>
82+
columns={columns}
83+
data={data}
84+
tableLayout="auto"
85+
sticky={{
86+
offsetHeader: 100,
87+
}}
88+
scroll={{
89+
x: 10000,
90+
}}
91+
style={{
92+
marginBottom: 100,
93+
}}
94+
/>
95+
96+
<h2>Show offset scroll</h2>
97+
<Table<RecordType>
98+
columns={columns}
99+
data={data}
100+
tableLayout="auto"
101+
sticky={{
102+
offsetScroll: 100,
103+
}}
104+
scroll={{
105+
x: 10000,
106+
}}
107+
style={{
108+
marginBottom: 100,
109+
}}
110+
/>
111+
</div>
112+
);
113+
114+
export default Demo;

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
"classnames": "^2.2.5",
5656
"raf": "^3.4.1",
5757
"rc-resize-observer": "^0.2.0",
58-
"rc-util": "^5.0.0",
58+
"rc-util": "^5.0.4",
5959
"shallowequal": "^1.1.0"
6060
},
6161
"devDependencies": {

src/Header/FixedHeader.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export interface FixedHeaderProps<RecordType> extends HeaderProps<RecordType> {
2424
colWidths: number[];
2525
columCount: number;
2626
direction: 'ltr' | 'rtl';
27+
fixHeader: boolean;
2728
}
2829

2930
function FixedHeader<RecordType>({
@@ -33,6 +34,7 @@ function FixedHeader<RecordType>({
3334
columCount,
3435
stickyOffsets,
3536
direction,
37+
fixHeader,
3638
...props
3739
}: FixedHeaderProps<RecordType>) {
3840
const { prefixCls, scrollbarSize } = React.useContext(TableContext);
@@ -47,8 +49,8 @@ function FixedHeader<RecordType>({
4749
};
4850

4951
const columnsWithScrollbar = useMemo<ColumnsType<RecordType>>(
50-
() => (scrollbarSize ? [...columns, ScrollBarColumn] : columns),
51-
[scrollbarSize, columns],
52+
() => (scrollbarSize && fixHeader ? [...columns, ScrollBarColumn] : columns),
53+
[scrollbarSize, columns, fixHeader],
5254
);
5355

5456
const flattenColumnsWithScrollbar = useMemo<ColumnType<RecordType>[]>(

src/Table.tsx

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import {
5252
CustomizeComponent,
5353
ColumnType,
5454
CustomizeScrollBody,
55+
TableSticky,
5556
} from './interface';
5657
import TableContext from './context/TableContext';
5758
import BodyContext from './context/BodyContext';
@@ -67,6 +68,8 @@ import Panel from './Panel';
6768
import Footer, { FooterComponents } from './Footer';
6869
import { findAllChildrenKeys, renderExpandIcon } from './utils/expandUtil';
6970
import { getCellFixedInfo } from './utils/fixUtil';
71+
import StickyScrollBar from './stickyScrollBar';
72+
import useSticky from './hooks/useSticky';
7073

7174
// Used for conditions cache
7275
const EMPTY_DATA = [];
@@ -155,6 +158,8 @@ export interface TableProps<RecordType = unknown> extends LegacyExpandableProps<
155158
internalRefs?: {
156159
body: React.MutableRefObject<HTMLDivElement>;
157160
};
161+
162+
sticky?: boolean | TableSticky;
158163
}
159164

160165
function Table<RecordType extends DefaultRecordType>(props: TableProps<RecordType>) {
@@ -186,6 +191,8 @@ function Table<RecordType extends DefaultRecordType>(props: TableProps<RecordTyp
186191
internalHooks,
187192
transformColumns,
188193
internalRefs,
194+
195+
sticky,
189196
} = props;
190197

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

387+
// Sticky
388+
const stickyRef = React.useRef<{ setScrollLeft: (left: number) => void }>();
389+
const { isSticky, offsetHeader, offsetScroll, stickyClassName } = useSticky(sticky, prefixCls);
390+
380391
let scrollXStyle: React.CSSProperties;
381392
let scrollYStyle: React.CSSProperties;
382393
let scrollTableStyle: React.CSSProperties;
@@ -412,11 +423,16 @@ function Table<RecordType extends DefaultRecordType>(props: TableProps<RecordTyp
412423

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

415-
function forceScroll(scrollLeft: number, target: HTMLDivElement) {
426+
function forceScroll(scrollLeft: number, target: HTMLDivElement | ((left: number) => void)) {
416427
/* eslint-disable no-param-reassign */
417-
if (target && target.scrollLeft !== scrollLeft) {
418-
target.scrollLeft = scrollLeft;
428+
if (target) {
429+
if (typeof target === 'function') {
430+
target(scrollLeft);
431+
} else if (target.scrollLeft !== scrollLeft) {
432+
target.scrollLeft = scrollLeft;
433+
}
419434
}
435+
420436
/* eslint-enable */
421437
}
422438

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

433449
forceScroll(mergedScrollLeft, scrollHeaderRef.current);
434450
forceScroll(mergedScrollLeft, scrollBodyRef.current);
451+
forceScroll(mergedScrollLeft, stickyRef.current?.setScrollLeft);
435452
}
436453

437454
if (currentTarget) {
@@ -495,6 +512,7 @@ function Table<RecordType extends DefaultRecordType>(props: TableProps<RecordTyp
495512
columCount: flattenColumns.length,
496513
stickyOffsets,
497514
onHeaderRow,
515+
fixHeader,
498516
};
499517

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

542-
if (fixHeader) {
560+
if (fixHeader || isSticky) {
543561
let bodyContent: React.ReactNode;
544562

545563
if (typeof customizeScrollBody === 'function') {
@@ -581,6 +599,15 @@ function Table<RecordType extends DefaultRecordType>(props: TableProps<RecordTyp
581599
{bodyTable}
582600
{footerTable}
583601
</TableComponent>
602+
603+
{isSticky && (
604+
<StickyScrollBar
605+
ref={stickyRef}
606+
offsetScroll={offsetScroll}
607+
scrollBodyRef={scrollBodyRef}
608+
onScroll={onScroll}
609+
/>
610+
)}
584611
</div>
585612
);
586613
}
@@ -592,10 +619,13 @@ function Table<RecordType extends DefaultRecordType>(props: TableProps<RecordTyp
592619
<div
593620
style={{
594621
overflow: 'hidden',
622+
...(isSticky ? { top: offsetHeader } : {}),
595623
}}
596624
onScroll={onScroll}
597625
ref={scrollHeaderRef}
598-
className={classNames(`${prefixCls}-header`)}
626+
className={classNames(`${prefixCls}-header`, {
627+
[stickyClassName]: !!stickyClassName,
628+
})}
599629
>
600630
<FixedHeader {...headerProps} {...columnContext} direction={direction} />
601631
</div>

src/hooks/useSticky.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import * as React from 'react';
2+
import { TableSticky } from '../interface';
3+
4+
export default function useSticky(
5+
sticky: boolean | TableSticky,
6+
prefixCls: string,
7+
): {
8+
isSticky: boolean;
9+
offsetHeader: number;
10+
offsetScroll: number;
11+
stickyClassName: string;
12+
} {
13+
return React.useMemo(() => {
14+
const isSticky = !!sticky;
15+
return {
16+
isSticky,
17+
stickyClassName: isSticky ? `${prefixCls}-sticky-header` : '',
18+
offsetHeader: typeof sticky === 'object' ? sticky.offsetHeader || 0 : 0,
19+
offsetScroll: typeof sticky === 'object' ? sticky.offsetScroll || 0 : 0,
20+
};
21+
}, [sticky, prefixCls]);
22+
}

src/interface.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,8 @@ export interface ColumnType<RecordType> extends ColumnSharedType<RecordType> {
9191

9292
export type ColumnsType<RecordType = unknown> = (
9393
| ColumnGroupType<RecordType>
94-
| ColumnType<RecordType>)[];
94+
| ColumnType<RecordType>
95+
)[];
9596

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

@@ -206,3 +207,9 @@ export type TriggerEventHandler<RecordType> = (
206207
record: RecordType,
207208
event: React.MouseEvent<HTMLElement>,
208209
) => void;
210+
211+
// =================== Sticky ===================
212+
export interface TableSticky {
213+
offsetHeader?: number;
214+
offsetScroll?: number;
215+
}

0 commit comments

Comments
 (0)