Skip to content

Commit 0fd991f

Browse files
authored
feat: Add API for customize body list content (#381)
* feat: Add virtual list demo * add grid demo * update ts define * update demo * add more test case * add warning info * more test case * improve coverage
1 parent 2764deb commit 0fd991f

File tree

8 files changed

+372
-36
lines changed

8 files changed

+372
-36
lines changed

examples/virtual-list-grid.tsx

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import React from 'react';
2+
import classNames from 'classnames';
3+
import { VariableSizeGrid as Grid } from 'react-window';
4+
import Table from '../src';
5+
import '../assets/index.less';
6+
import './virtual-list.less';
7+
8+
const columns = [
9+
{ title: 'A', dataIndex: 'a', width: 150 },
10+
{ title: 'B', dataIndex: 'b', width: 300 },
11+
{ title: 'C', dataIndex: 'c', width: 200 },
12+
{ title: 'D', dataIndex: 'd', width: 100 },
13+
];
14+
15+
const data = [];
16+
for (let i = 0; i < 100000; i += 1) {
17+
data.push({
18+
key: i,
19+
a: `a${i}`,
20+
b: `b${i}`,
21+
c: `c${i}`,
22+
d: `d${i}`,
23+
});
24+
}
25+
const Demo = () => {
26+
const gridRef = React.useRef<any>();
27+
const [connectObject] = React.useState<any>(() => {
28+
const obj = {};
29+
Object.defineProperty(obj, 'scrollLeft', {
30+
get: () => null,
31+
set: (scrollLeft: number) => {
32+
if (gridRef.current) {
33+
gridRef.current.scrollTo({ scrollLeft });
34+
}
35+
},
36+
});
37+
38+
return obj;
39+
});
40+
41+
React.useEffect(() => {
42+
gridRef.current.resetAfterIndices({
43+
columnIndex: 0,
44+
shouldForceUpdate: false,
45+
});
46+
}, []);
47+
48+
const renderVirtualList = (rawData: object[], { scrollbarSize, ref, onScroll }: any) => {
49+
ref.current = connectObject;
50+
51+
return (
52+
<Grid
53+
ref={gridRef}
54+
className="virtual-grid"
55+
columnCount={columns.length}
56+
columnWidth={index => {
57+
const { width } = columns[index];
58+
return index === columns.length - 1 ? width - scrollbarSize - 1 : width;
59+
}}
60+
height={300}
61+
rowCount={rawData.length}
62+
rowHeight={() => 50}
63+
width={301}
64+
onScroll={({ scrollLeft }) => {
65+
onScroll({ scrollLeft });
66+
}}
67+
>
68+
{({ columnIndex, rowIndex, style }) => (
69+
<div
70+
className={classNames('virtual-cell', {
71+
'virtual-cell-last': columnIndex === columns.length - 1,
72+
})}
73+
style={style}
74+
>
75+
r{rowIndex}, c{columnIndex}
76+
</div>
77+
)}
78+
</Grid>
79+
);
80+
};
81+
82+
return (
83+
<Table
84+
style={{ width: 301 }}
85+
tableLayout="fixed"
86+
columns={columns}
87+
data={data}
88+
scroll={{ y: 300, x: 300 }}
89+
components={{
90+
body: renderVirtualList,
91+
}}
92+
/>
93+
);
94+
};
95+
96+
export default Demo;

examples/virtual-list.less

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
.virtual-grid {
2+
border: 1px solid green;
3+
box-sizing: border-box;
4+
}
5+
6+
.virtual-cell {
7+
box-sizing: border-box;
8+
border: 1px solid green;
9+
border-left: 0;
10+
border-top: 0;
11+
background: #fff;
12+
13+
&-last {
14+
border-right: 0;
15+
}
16+
}

examples/virtual-list.tsx

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import React from 'react';
2+
import classNames from 'classnames';
3+
import { VariableSizeGrid as Grid } from 'react-window';
4+
import Table from '../src';
5+
import '../assets/index.less';
6+
import './virtual-list.less';
7+
8+
const columns = [
9+
{ title: 'A', dataIndex: 'a', width: 100 },
10+
{ title: 'B', dataIndex: 'b', width: 100 },
11+
{ title: 'C', dataIndex: 'c', width: 100 },
12+
];
13+
14+
const data = [];
15+
for (let i = 0; i < 100000; i += 1) {
16+
data.push({
17+
key: i,
18+
a: `a${i}`,
19+
b: `b${i}`,
20+
c: `c${i}`,
21+
});
22+
}
23+
24+
const Cell = ({ columnIndex, rowIndex, style }) => (
25+
<div
26+
className={classNames('virtual-cell', {
27+
'virtual-cell-last': columnIndex === columns.length - 1,
28+
})}
29+
style={style}
30+
>
31+
r{rowIndex}, c{columnIndex}
32+
</div>
33+
);
34+
35+
const Demo = () => {
36+
const gridRef = React.useRef<any>();
37+
38+
React.useEffect(() => {
39+
gridRef.current.resetAfterIndices({
40+
columnIndex: 0,
41+
shouldForceUpdate: false,
42+
});
43+
}, []);
44+
45+
const renderVirtualList = (rawData: object[], { scrollbarSize }: any) => (
46+
<Grid
47+
ref={gridRef}
48+
className="virtual-grid"
49+
columnCount={columns.length}
50+
columnWidth={index => {
51+
const { width } = columns[index];
52+
return index === columns.length - 1 ? width - scrollbarSize - 1 : width;
53+
}}
54+
height={300}
55+
rowCount={rawData.length}
56+
rowHeight={() => 50}
57+
width={301}
58+
>
59+
{Cell}
60+
</Grid>
61+
);
62+
63+
return (
64+
<Table
65+
style={{ width: 301 }}
66+
tableLayout="fixed"
67+
columns={columns}
68+
data={data}
69+
scroll={{ y: 300 }}
70+
components={{
71+
body: renderVirtualList,
72+
}}
73+
/>
74+
);
75+
};
76+
77+
export default Demo;

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
"react-resizable": "^1.7.5",
8383
"react-test-renderer": "^16.0.0",
8484
"react-virtualized": "^9.12.0",
85+
"react-window": "^1.8.5",
8586
"styled-components": "^4.3.2",
8687
"typescript": "^3.6.4"
8788
}

src/Table.tsx

Lines changed: 79 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import {
5050
RowClassName,
5151
CustomizeComponent,
5252
ColumnType,
53+
CustomizeScrollBody,
5354
} from './interface';
5455
import TableContext from './context/TableContext';
5556
import BodyContext from './context/BodyContext';
@@ -68,6 +69,9 @@ import { findAllChildrenKeys, renderExpandIcon } from './utils/expandUtil';
6869
// Used for conditions cache
6970
const EMPTY_DATA = [];
7071

72+
// Used for customize scroll
73+
const EMPTY_SCROLL_TARGET = {};
74+
7175
export const INTERNAL_HOOKS = 'rc-table-internal-hook';
7276

7377
export interface TableProps<RecordType extends DefaultRecordType>
@@ -98,7 +102,7 @@ export interface TableProps<RecordType extends DefaultRecordType>
98102
// Customize
99103
id?: string;
100104
showHeader?: boolean;
101-
components?: TableComponents;
105+
components?: TableComponents<RecordType>;
102106
onRow?: GetComponentProps<RecordType>;
103107
onHeaderRow?: GetComponentProps<ColumnType<RecordType>[]>;
104108
emptyText?: React.ReactNode | (() => React.ReactNode);
@@ -188,13 +192,15 @@ function Table<RecordType extends DefaultRecordType>(props: TableProps<RecordTyp
188192
}
189193

190194
// ==================== Customize =====================
191-
const mergedComponents = React.useMemo(() => mergeObject<TableComponents>(components, {}), [
192-
components,
193-
]);
195+
const mergedComponents = React.useMemo(
196+
() => mergeObject<TableComponents<RecordType>>(components, {}),
197+
[components],
198+
);
194199

195200
const getComponent = React.useCallback<GetComponent>(
196201
(path, defaultComponent) =>
197-
getPathValue<CustomizeComponent, TableComponents>(mergedComponents, path) || defaultComponent,
202+
getPathValue<CustomizeComponent, TableComponents<RecordType>>(mergedComponents, path) ||
203+
defaultComponent,
198204
[mergedComponents],
199205
);
200206

@@ -354,18 +360,25 @@ function Table<RecordType extends DefaultRecordType>(props: TableProps<RecordTyp
354360
/* eslint-enable */
355361
}
356362

357-
const onScroll: React.UIEventHandler<HTMLDivElement> = ({ currentTarget }) => {
358-
const { scrollLeft, scrollWidth, clientWidth } = currentTarget;
363+
const onScroll = ({
364+
currentTarget,
365+
scrollLeft,
366+
}: React.UIEvent<HTMLDivElement> & { scrollLeft?: number }) => {
367+
const mergedScrollLeft = typeof scrollLeft === 'number' ? scrollLeft : currentTarget.scrollLeft;
359368

360-
if (!getScrollTarget() || getScrollTarget() === currentTarget) {
361-
setScrollTarget(currentTarget);
369+
const compareTarget = currentTarget || EMPTY_SCROLL_TARGET;
370+
if (!getScrollTarget() || getScrollTarget() === compareTarget) {
371+
setScrollTarget(compareTarget);
362372

363-
forceScroll(scrollLeft, scrollHeaderRef.current);
364-
forceScroll(scrollLeft, scrollBodyRef.current);
373+
forceScroll(mergedScrollLeft, scrollHeaderRef.current);
374+
forceScroll(mergedScrollLeft, scrollBodyRef.current);
365375
}
366376

367-
setPingedLeft(scrollLeft > 0);
368-
setPingedRight(scrollLeft < scrollWidth - clientWidth);
377+
if (currentTarget) {
378+
const { scrollWidth, clientWidth } = currentTarget;
379+
setPingedLeft(mergedScrollLeft > 0);
380+
setPingedRight(mergedScrollLeft < scrollWidth - clientWidth);
381+
}
369382
};
370383

371384
const triggerOnScroll = () => {
@@ -451,26 +464,40 @@ function Table<RecordType extends DefaultRecordType>(props: TableProps<RecordTyp
451464
);
452465

453466
const footerTable = summary && <Footer>{summary(mergedData)}</Footer>;
467+
const customizeScrollBody = getComponent(['body']) as CustomizeScrollBody<RecordType>;
468+
469+
if (
470+
process.env.NODE_ENV !== 'production' &&
471+
typeof customizeScrollBody === 'function' &&
472+
!fixHeader
473+
) {
474+
warning(false, '`components.body` with render props is only work on `scroll.y`.');
475+
}
454476

455477
if (fixHeader) {
456-
groupTableNode = (
457-
<>
458-
{/* Header Table */}
459-
{showHeader !== false && (
460-
<div
461-
style={{
462-
...scrollXStyle,
463-
marginBottom: fixColumn ? -scrollbarSize : null,
464-
}}
465-
onScroll={onScroll}
466-
ref={scrollHeaderRef}
467-
className={classNames(`${prefixCls}-header`)}
468-
>
469-
<FixedHeader {...headerProps} {...columnContext} />
470-
</div>
471-
)}
478+
let bodyContent: React.ReactNode;
472479

473-
{/* Body Table */}
480+
if (typeof customizeScrollBody === 'function') {
481+
bodyContent = customizeScrollBody(mergedData, {
482+
scrollbarSize,
483+
ref: scrollBodyRef,
484+
onScroll,
485+
});
486+
headerProps.colWidths = flattenColumns.map(({ width }, index) => {
487+
const colWidth = index === columns.length - 1 ? (width as number) - scrollbarSize : width;
488+
489+
if (typeof colWidth === 'number' && !Number.isNaN(colWidth)) {
490+
return colWidth;
491+
}
492+
warning(
493+
false,
494+
'When use `components.body` with render props. Each column should have a fixed value.',
495+
);
496+
497+
return 0;
498+
}) as number[];
499+
} else {
500+
bodyContent = (
474501
<div
475502
style={{
476503
...scrollXStyle,
@@ -491,6 +518,28 @@ function Table<RecordType extends DefaultRecordType>(props: TableProps<RecordTyp
491518
{footerTable}
492519
</TableComponent>
493520
</div>
521+
);
522+
}
523+
524+
groupTableNode = (
525+
<>
526+
{/* Header Table */}
527+
{showHeader !== false && (
528+
<div
529+
style={{
530+
...scrollXStyle,
531+
marginBottom: fixColumn ? -scrollbarSize : null,
532+
}}
533+
onScroll={onScroll}
534+
ref={scrollHeaderRef}
535+
className={classNames(`${prefixCls}-header`)}
536+
>
537+
<FixedHeader {...headerProps} {...columnContext} />
538+
</div>
539+
)}
540+
541+
{/* Body Table */}
542+
{bodyContent}
494543
</>
495544
);
496545
} else {

0 commit comments

Comments
 (0)