index.tsx 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860
  1. /* eslint-disable eqeqeq */
  2. /* eslint-disable @typescript-eslint/member-ordering */
  3. /* eslint-disable max-len */
  4. import React, { ReactNode } from 'react';
  5. import PropTypes from 'prop-types';
  6. import { get, size, isMap, each, isEqual, pick, isNull, isFunction } from 'lodash';
  7. import classnames from 'classnames';
  8. import { VariableSizeList as List } from 'react-window';
  9. import {
  10. arrayAdd,
  11. getRecordKey,
  12. isExpanded,
  13. isSelected,
  14. isDisabled,
  15. getRecord,
  16. genExpandedRowKey,
  17. getDefaultVirtualizedRowConfig,
  18. isTreeTable
  19. } from '@douyinfe/semi-foundation/table/utils';
  20. import BodyFoundation, { BodyAdapter, FlattenData, GroupFlattenData } from '@douyinfe/semi-foundation/table/bodyFoundation';
  21. import { strings } from '@douyinfe/semi-foundation/table/constants';
  22. import Store from '@douyinfe/semi-foundation/utils/Store';
  23. import BaseComponent, { BaseProps } from '../../_base/baseComponent';
  24. import { logger } from '../utils';
  25. import ColGroup from '../ColGroup';
  26. import BaseRow from './BaseRow';
  27. import ExpandedRow from './ExpandedRow';
  28. import SectionRow from './SectionRow';
  29. import TableHeader from '../TableHeader';
  30. import ConfigContext from '../../configProvider/context';
  31. import TableContext, { TableContextProps } from '../table-context';
  32. import type {
  33. ExpandedRowRender,
  34. Virtualized,
  35. VirtualizedItemSize,
  36. GetVirtualizedListRef,
  37. ColumnProps,
  38. Size,
  39. BodyScrollEvent,
  40. Scroll,
  41. Fixed,
  42. TableComponents,
  43. RowExpandable,
  44. VirtualizedOnScroll,
  45. Direction,
  46. RowKey
  47. } from '../interface';
  48. export interface BodyProps extends BaseProps {
  49. tableLayout?: 'fixed' | 'auto';
  50. anyColumnFixed?: boolean;
  51. columns?: ColumnProps[];
  52. dataSource?: Record<string, any>[];
  53. disabledRowKeysSet: Set<any>; // required
  54. emptySlot?: ReactNode;
  55. expandedRowKeys?: (string | number)[];
  56. expandedRowRender?: ExpandedRowRender<Record<string, any>>;
  57. fixed?: Fixed;
  58. forwardedRef?: React.MutableRefObject<HTMLDivElement> | ((instance: any) => void);
  59. handleBodyScroll?: (e: BodyScrollEvent) => void;
  60. handleWheel?: (e: React.WheelEvent<HTMLDivElement>) => void;
  61. includeHeader?: boolean;
  62. prefixCls?: string;
  63. scroll?: Scroll;
  64. selectedRowKeysSet: Set<any>; // required
  65. showHeader?: boolean;
  66. size?: Size;
  67. virtualized?: Virtualized;
  68. components?: TableComponents;
  69. store: Store;
  70. groups?: Map<string, Record<string, any>[]>[];
  71. rowKey?: RowKey<Record<string, any>>;
  72. childrenRecordName?: string;
  73. rowExpandable?: RowExpandable<Record<string, any>>;
  74. renderExpandIcon: (record: Record<string, any>, isNested: boolean) => ReactNode | null;
  75. headerRef?: React.MutableRefObject<HTMLDivElement> | ((instance: any) => void);
  76. onScroll?: VirtualizedOnScroll
  77. }
  78. export interface BodyState {
  79. virtualizedData?: Array<FlattenData | GroupFlattenData>;
  80. cache?: {
  81. virtualizedScrollTop?: number;
  82. virtualizedScrollLeft?: number
  83. };
  84. cachedExpandBtnShouldInRow?: boolean;
  85. cachedExpandRelatedProps?: any[]
  86. }
  87. export interface BodyContext {
  88. getVirtualizedListRef: GetVirtualizedListRef;
  89. flattenedColumns: ColumnProps[];
  90. getCellWidths: (flattenedColumns: ColumnProps[]) => number[]
  91. }
  92. class Body extends BaseComponent<BodyProps, BodyState> {
  93. static contextType = TableContext;
  94. static propTypes = {
  95. anyColumnFixed: PropTypes.bool,
  96. childrenRecordName: PropTypes.string,
  97. columns: PropTypes.array,
  98. components: PropTypes.object,
  99. dataSource: PropTypes.array,
  100. disabledRowKeysSet: PropTypes.instanceOf(Set).isRequired,
  101. emptySlot: PropTypes.node,
  102. expandRowByClick: PropTypes.bool,
  103. expandedRowKeys: PropTypes.array,
  104. expandedRowRender: PropTypes.func,
  105. fixed: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
  106. forwardedRef: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
  107. groups: PropTypes.instanceOf(Map),
  108. handleBodyScroll: PropTypes.func,
  109. handleWheel: PropTypes.func,
  110. headerRef: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
  111. includeHeader: PropTypes.bool,
  112. onScroll: PropTypes.func,
  113. prefixCls: PropTypes.string,
  114. renderExpandIcon: PropTypes.func,
  115. rowExpandable: PropTypes.func,
  116. rowKey: PropTypes.oneOfType([PropTypes.string, PropTypes.bool, PropTypes.func]),
  117. scroll: PropTypes.object,
  118. selectedRowKeysSet: PropTypes.instanceOf(Set).isRequired,
  119. showHeader: PropTypes.bool,
  120. size: PropTypes.string,
  121. store: PropTypes.object,
  122. virtualized: PropTypes.oneOfType([PropTypes.bool, PropTypes.object]),
  123. };
  124. ref: React.MutableRefObject<any>;
  125. listRef: React.MutableRefObject<any>;
  126. observer: ResizeObserver;
  127. foundation: BodyFoundation;
  128. cellWidths: number[];
  129. flattenedColumns: ColumnProps[];
  130. context: TableContextProps;
  131. constructor(props: BodyProps, context: BodyContext) {
  132. super(props);
  133. this.ref = React.createRef();
  134. this.state = {
  135. virtualizedData: [],
  136. cache: {
  137. virtualizedScrollTop: null,
  138. virtualizedScrollLeft: null,
  139. },
  140. cachedExpandBtnShouldInRow: null,
  141. cachedExpandRelatedProps: [],
  142. };
  143. this.listRef = React.createRef();
  144. const { getVirtualizedListRef, flattenedColumns, getCellWidths } = context;
  145. if (getVirtualizedListRef) {
  146. if (props.virtualized) {
  147. getVirtualizedListRef(this.listRef);
  148. } else {
  149. console.warn('getVirtualizedListRef only works with virtualized. ' +
  150. 'See https://semi.design/en-US/show/table for more information.');
  151. }
  152. }
  153. this.foundation = new BodyFoundation(this.adapter);
  154. this.flattenedColumns = flattenedColumns;
  155. this.cellWidths = getCellWidths(flattenedColumns);
  156. this.observer = null;
  157. }
  158. get adapter(): BodyAdapter<BodyProps, BodyState> {
  159. return {
  160. ...super.adapter,
  161. setVirtualizedData: (virtualizedData, cb) => this.setState({ virtualizedData }, cb),
  162. setCachedExpandBtnShouldInRow: cachedExpandBtnShouldInRow => this.setState({ cachedExpandBtnShouldInRow }),
  163. setCachedExpandRelatedProps: cachedExpandRelatedProps => this.setState({ cachedExpandRelatedProps }),
  164. observeBodyResize: (bodyWrapDOM: HTMLDivElement) => {
  165. const { setBodyHasScrollbar } = this.context;
  166. // Callback when the size of the body dom content changes, notifying Table.jsx whether the bodyHasScrollBar exists
  167. const resizeCallback = () => {
  168. const update = () => {
  169. const { offsetWidth, clientWidth } = bodyWrapDOM;
  170. const bodyHasScrollBar = clientWidth < offsetWidth;
  171. setBodyHasScrollbar(bodyHasScrollBar);
  172. };
  173. const requestAnimationFrame = window.requestAnimationFrame || window.setTimeout;
  174. requestAnimationFrame(update);
  175. };
  176. // Monitor body dom resize
  177. if (bodyWrapDOM) {
  178. if (get(window, 'ResizeObserver')) {
  179. if (this.observer) {
  180. this.observer.unobserve(bodyWrapDOM);
  181. this.observer = null;
  182. }
  183. this.observer = new ResizeObserver(resizeCallback);
  184. this.observer.observe(bodyWrapDOM);
  185. } else {
  186. logger.warn(
  187. 'The current browser does not support ResizeObserver,' +
  188. 'and the table may be misaligned after plugging and unplugging the mouse and keyboard.' +
  189. 'You can try to refresh it.'
  190. );
  191. }
  192. }
  193. },
  194. unobserveBodyResize: () => {
  195. const bodyWrapDOM = this.ref.current;
  196. if (this.observer) {
  197. this.observer.unobserve(bodyWrapDOM);
  198. this.observer = null;
  199. }
  200. },
  201. };
  202. }
  203. componentDidUpdate(prevProps: BodyProps, prevState: BodyState) {
  204. const { virtualized, dataSource, expandedRowKeys, columns, scroll } = this.props;
  205. if (virtualized) {
  206. if (
  207. prevProps.dataSource !== dataSource ||
  208. prevProps.expandedRowKeys !== expandedRowKeys ||
  209. prevProps.columns !== columns
  210. ) {
  211. this.foundation.initVirtualizedData();
  212. }
  213. }
  214. const expandRelatedProps = strings.EXPAND_RELATED_PROPS;
  215. const newExpandRelatedProps = expandRelatedProps.map(key => get(this.props, key, undefined));
  216. if (!isEqual(newExpandRelatedProps, prevState.cachedExpandRelatedProps)) {
  217. this.foundation.initExpandBtnShouldInRow(newExpandRelatedProps);
  218. }
  219. const scrollY = get(scroll, 'y');
  220. const bodyWrapDOM = this.ref.current;
  221. if (scrollY && scrollY !== get(prevProps, 'scroll.y')) {
  222. this.foundation.observeBodyResize(bodyWrapDOM);
  223. }
  224. }
  225. forwardRef = (node: HTMLDivElement) => {
  226. const { forwardedRef } = this.props;
  227. this.ref.current = node;
  228. this.foundation.observeBodyResize(node);
  229. if (typeof forwardedRef === 'function') {
  230. forwardedRef(node);
  231. } else if (forwardedRef && typeof forwardedRef === 'object') {
  232. forwardedRef.current = node;
  233. }
  234. };
  235. itemSize = (index: number) => {
  236. const { virtualized, size: tableSize } = this.props;
  237. const { virtualizedData } = this.state;
  238. const virtualizedItem = get(virtualizedData, index);
  239. const defaultConfig = getDefaultVirtualizedRowConfig(tableSize, virtualizedItem.sectionRow);
  240. const itemSize = get(virtualized, 'itemSize', defaultConfig.height) as VirtualizedItemSize;
  241. let realSize = itemSize as number;
  242. if (typeof itemSize === 'function') {
  243. realSize = itemSize(index, {
  244. expandedRow: get(virtualizedItem, 'expandedRow', false),
  245. sectionRow: get(virtualizedItem, 'sectionRow', false),
  246. });
  247. }
  248. if (realSize < defaultConfig.minHeight) {
  249. logger.warn(`The computed real \`itemSize\` cannot be less than ${defaultConfig.minHeight}`);
  250. }
  251. return realSize;
  252. };
  253. itemKey = (index: number, data: Array<FlattenData | GroupFlattenData>) => get(data, [index, 'key'], index);
  254. handleRowClick = (rowKey: RowKey<any>, e: React.MouseEvent<HTMLElement>, expand: boolean) => {
  255. const { handleRowExpanded } = this.context;
  256. handleRowExpanded(!expand, rowKey, e);
  257. };
  258. handleVirtualizedScroll = (props = {}) => {
  259. const onScroll: undefined | ((props?: any) => void) = get(this.props.virtualized, 'onScroll');
  260. if (typeof onScroll === 'function') {
  261. onScroll(props);
  262. }
  263. };
  264. /**
  265. * @param {MouseEvent<HTMLDivElement>} e
  266. */
  267. handleVirtualizedBodyScroll = (e: BodyScrollEvent) => {
  268. const { handleBodyScroll } = this.props;
  269. const newScrollLeft = get(e, 'nativeEvent.target.scrollLeft');
  270. const newScrollTop = get(e, 'nativeEvent.target.scrollTop');
  271. if (newScrollTop === this.state.cache.virtualizedScrollTop) {
  272. this.handleVirtualizedScroll({ horizontalScrolling: true });
  273. }
  274. this.state.cache.virtualizedScrollLeft = newScrollLeft;
  275. this.state.cache.virtualizedScrollTop = newScrollTop;
  276. if (typeof handleBodyScroll === 'function') {
  277. handleBodyScroll(e);
  278. }
  279. };
  280. getVirtualizedRowWidth = () => {
  281. const { getCellWidths } = this.context;
  282. const { columns } = this.props;
  283. const cellWidths = getCellWidths(columns);
  284. const rowWidth = arrayAdd(cellWidths, 0, size(columns));
  285. return rowWidth;
  286. };
  287. renderVirtualizedRow = (options: { index?: number; style?: React.CSSProperties; isScrolling?: boolean }) => {
  288. const { index, style } = options;
  289. const { virtualizedData, cachedExpandBtnShouldInRow } = this.state;
  290. const { flattenedColumns } = this.context;
  291. const virtualizedItem: any = get(virtualizedData, [index], {});
  292. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  293. const { key, parentKeys, expandedRow, sectionRow, ...rest } = virtualizedItem;
  294. const rowWidth = this.getVirtualizedRowWidth();
  295. const expandBtnShouldInRow = cachedExpandBtnShouldInRow;
  296. const props = {
  297. ...this.props,
  298. style: {
  299. ...style,
  300. width: rowWidth,
  301. },
  302. ...rest,
  303. columns: flattenedColumns,
  304. index,
  305. expandBtnShouldInRow,
  306. };
  307. // eslint-disable-next-line no-nested-ternary
  308. return sectionRow ?
  309. this.renderSectionRow(props) :
  310. expandedRow ?
  311. this.renderExpandedRow(props) :
  312. this.renderBaseRow(props);
  313. };
  314. // virtualized List innerElementType
  315. renderTbody = React.forwardRef<HTMLDivElement, any>((props: any = {}, ref: React.MutableRefObject<HTMLDivElement> | ((instance: HTMLDivElement) => void)) => (
  316. <div
  317. {...props}
  318. onScroll={(...args) => {
  319. if (props.onScroll) {
  320. props.onScroll(...args);
  321. }
  322. }}
  323. // eslint-disable-next-line react/no-this-in-sfc,react/destructuring-assignment
  324. className={classnames(props.className, `${this.props.prefixCls}-tbody`)}
  325. style={{ ...props.style }}
  326. ref={ref}
  327. />
  328. ));
  329. // virtualized List outerElementType
  330. renderOuter = React.forwardRef<HTMLDivElement, any>((props: any, ref: React.MutableRefObject<HTMLDivElement> | ((instance: HTMLDivElement) => void)) => {
  331. const { children, ...rest } = props;
  332. // eslint-disable-next-line react/no-this-in-sfc
  333. const { handleWheel, prefixCls, emptySlot, dataSource } = this.props;
  334. // eslint-disable-next-line react/no-this-in-sfc
  335. const tableWidth = this.getVirtualizedRowWidth();
  336. const tableCls = classnames(`${prefixCls}`, `${prefixCls}-fixed`);
  337. return (
  338. <div
  339. {...rest}
  340. ref={ref}
  341. onWheel={(...args) => {
  342. if (handleWheel) {
  343. handleWheel(...args);
  344. }
  345. if (rest.onWheel) {
  346. rest.onWheel(...args);
  347. }
  348. }}
  349. onScroll={(...args) => {
  350. // eslint-disable-next-line react/no-this-in-sfc
  351. this.handleVirtualizedBodyScroll(...args);
  352. if (rest.onScroll) {
  353. rest.onScroll(...args);
  354. }
  355. }}
  356. >
  357. <div style={{ width: tableWidth }} className={tableCls}>
  358. {children}
  359. </div>
  360. {size(dataSource) === 0 && emptySlot}
  361. </div>
  362. );
  363. });
  364. onItemsRendered = (props: { overscanStartIndex: number; overscanStopIndex: number; visibleStartIndex: number; visibleStopIndex: number }) => {
  365. if (this.state.cache.virtualizedScrollLeft && this.ref.current) {
  366. this.ref.current.scrollLeft = this.state.cache.virtualizedScrollLeft;
  367. }
  368. };
  369. renderVirtualizedBody = (direction?: Direction) => {
  370. const { scroll, prefixCls, virtualized, columns } = this.props;
  371. const { virtualizedData } = this.state;
  372. const { getCellWidths } = this.context;
  373. const cellWidths = getCellWidths(columns);
  374. if (!size(cellWidths)) {
  375. return null;
  376. }
  377. const rawY = get(scroll, 'y');
  378. const yIsNumber = typeof rawY === 'number';
  379. const y = yIsNumber ? rawY : 600;
  380. if (!yIsNumber) {
  381. logger.warn('You have to specific "scroll.y" which must be a number for table virtualization!');
  382. }
  383. const listStyle = {
  384. width: '100%',
  385. height: virtualizedData?.length ? y : null,
  386. overflowX: 'auto',
  387. overflowY: 'auto',
  388. } as const;
  389. const wrapCls = classnames(`${prefixCls}-body`);
  390. return (
  391. <List<Array<FlattenData | GroupFlattenData>>
  392. {...(typeof virtualized === 'object' ? virtualized : {})}
  393. initialScrollOffset={this.state.cache.virtualizedScrollTop}
  394. onScroll={this.handleVirtualizedScroll}
  395. onItemsRendered={this.onItemsRendered}
  396. ref={this.listRef}
  397. className={wrapCls}
  398. outerRef={this.forwardRef}
  399. height={virtualizedData?.length ? y : 0}
  400. width={listStyle.width}
  401. itemData={virtualizedData}
  402. itemSize={this.itemSize}
  403. itemCount={virtualizedData.length}
  404. itemKey={this.itemKey}
  405. innerElementType={this.renderTbody}
  406. outerElementType={this.renderOuter}
  407. style={{ ...listStyle, direction }}
  408. direction={direction}
  409. >
  410. {this.renderVirtualizedRow}
  411. </List>
  412. );
  413. };
  414. /**
  415. * render group title
  416. * @param {*} props
  417. */
  418. renderSectionRow = (props: RenderSectionRowProps = { groupKey: undefined }) => {
  419. const { dataSource, rowKey, group, groupKey, index } = props;
  420. const sectionRowPickKeys = Object.keys(SectionRow.propTypes);
  421. const sectionRowProps: any = pick(props, sectionRowPickKeys);
  422. const { handleRowExpanded } = this.context;
  423. return (
  424. <SectionRow
  425. {...sectionRowProps}
  426. record={{
  427. groupKey,
  428. records: [...group].map(recordKey => getRecord(dataSource, recordKey, rowKey)),
  429. }}
  430. index={index}
  431. onExpand={handleRowExpanded}
  432. data={dataSource}
  433. key={groupKey || index}
  434. />
  435. );
  436. };
  437. renderExpandedRow = (props: RenderExpandedRowProps = { renderExpandIcon: () => null }) => {
  438. const {
  439. style,
  440. components,
  441. renderExpandIcon,
  442. expandedRowRender,
  443. record,
  444. columns,
  445. expanded,
  446. index,
  447. rowKey,
  448. virtualized,
  449. } = props;
  450. let key = getRecordKey(record, rowKey);
  451. if (key == null) {
  452. key = index;
  453. }
  454. const { flattenedColumns, getCellWidths } = this.context;
  455. // we use memoized cellWidths to avoid re-render expanded row (fix #686)
  456. if (flattenedColumns !== this.flattenedColumns) {
  457. this.flattenedColumns = flattenedColumns;
  458. this.cellWidths = getCellWidths(flattenedColumns);
  459. }
  460. return (
  461. <ExpandedRow
  462. style={style}
  463. components={components}
  464. renderExpandIcon={renderExpandIcon}
  465. expandedRowRender={expandedRowRender}
  466. record={record}
  467. columns={columns}
  468. expanded={expanded}
  469. index={index}
  470. virtualized={virtualized}
  471. key={genExpandedRowKey(key)}
  472. cellWidths={this.cellWidths}
  473. />
  474. );
  475. };
  476. /**
  477. * render base row
  478. * @param {*} props
  479. * @returns
  480. */
  481. renderBaseRow(props: any = {}) {
  482. const {
  483. rowKey,
  484. columns,
  485. expandedRowKeys,
  486. rowExpandable,
  487. record,
  488. index,
  489. level,
  490. expandBtnShouldInRow, // effect the display of the indent span
  491. selectedRowKeysSet,
  492. disabledRowKeysSet,
  493. expandRowByClick,
  494. } = props;
  495. const baseRowPickKeys = Object.keys(BaseRow.propTypes);
  496. const baseRowProps: Record<string, any> = pick(props, baseRowPickKeys);
  497. let key = getRecordKey(record, rowKey);
  498. if (key == null) {
  499. key = index;
  500. }
  501. const expanded = isExpanded(expandedRowKeys, key);
  502. const expandable = rowExpandable && rowExpandable(record);
  503. const expandableProps: {
  504. level?: number;
  505. expanded?: boolean;
  506. expandableRow?: boolean;
  507. onRowClick?: (...args: any[]) => void
  508. } = {
  509. level: undefined,
  510. expanded,
  511. };
  512. if (expandable || expandBtnShouldInRow) {
  513. expandableProps.level = level;
  514. expandableProps.expandableRow = expandable;
  515. if (expandRowByClick) {
  516. expandableProps.onRowClick = this.handleRowClick;
  517. }
  518. }
  519. const selectionProps = {
  520. selected: isSelected(selectedRowKeysSet, key),
  521. disabled: isDisabled(disabledRowKeysSet, key),
  522. };
  523. const { getCellWidths } = this.context;
  524. const cellWidths = getCellWidths(columns, null, true);
  525. return (
  526. <BaseRow
  527. {...baseRowProps}
  528. {...expandableProps}
  529. {...selectionProps}
  530. key={key}
  531. rowKey={key}
  532. cellWidths={cellWidths}
  533. />
  534. );
  535. }
  536. /**
  537. * render grouped rows
  538. * @returns {ReactNode[]} renderedRows
  539. */
  540. renderGroupedRows = () => {
  541. const { groups, dataSource: data, rowKey, expandedRowKeys } = this.props;
  542. const { flattenedColumns } = this.context;
  543. const groupsInData = new Map();
  544. const renderedRows: ReactNode[] = [];
  545. if (groups != null && Array.isArray(data) && data.length) {
  546. data.forEach(record => {
  547. const recordKey = getRecordKey(record, rowKey);
  548. groups.forEach((group: Map<string, Record<string, any>[]>, key: number) => {
  549. if (group.has(recordKey)) {
  550. if (!groupsInData.has(key)) {
  551. groupsInData.set(key, new Set([]));
  552. }
  553. groupsInData.get(key).add(recordKey);
  554. return false;
  555. }
  556. return undefined;
  557. });
  558. });
  559. }
  560. let index = -1;
  561. groupsInData.forEach((group, groupKey) => {
  562. // Calculate the expanded state of the group
  563. const expanded = isExpanded(expandedRowKeys, groupKey);
  564. // Render the title of the group
  565. renderedRows.push(
  566. this.renderSectionRow({
  567. ...this.props,
  568. columns: flattenedColumns,
  569. index: ++index,
  570. group,
  571. groupKey,
  572. expanded,
  573. })
  574. );
  575. // Render the grouped content when the group is expanded
  576. if (expanded) {
  577. const dataInGroup: any[] = [];
  578. group.forEach((recordKey: string) => {
  579. const record = getRecord(data, recordKey, rowKey);
  580. if (record != null) {
  581. dataInGroup.push(record);
  582. }
  583. });
  584. /**
  585. * Render the contents of the group row
  586. */
  587. renderedRows.push(this.renderBodyRows(dataInGroup));
  588. }
  589. });
  590. return renderedRows;
  591. };
  592. renderBodyRows(data: Record<string, any>[] = [], level = 0, renderedRows: ReactNode[] = []) {
  593. const {
  594. rowKey,
  595. expandedRowRender,
  596. expandedRowKeys,
  597. childrenRecordName,
  598. rowExpandable,
  599. } = this.props;
  600. const hasExpandedRowRender = typeof expandedRowRender === 'function';
  601. const expandBtnShouldInRow = this.state.cachedExpandBtnShouldInRow;
  602. const { flattenedColumns } = this.context;
  603. each(data, (record, index) => {
  604. let key = getRecordKey(record, rowKey);
  605. if (key == null) {
  606. key = index;
  607. }
  608. const recordChildren = get(record, childrenRecordName);
  609. const recordHasChildren = Boolean(Array.isArray(recordChildren) && recordChildren.length);
  610. renderedRows.push(
  611. this.renderBaseRow({
  612. ...this.props,
  613. columns: flattenedColumns,
  614. expandBtnShouldInRow,
  615. record,
  616. key,
  617. level,
  618. index,
  619. })
  620. );
  621. // render expand row
  622. const expanded = isExpanded(expandedRowKeys, key);
  623. if (hasExpandedRowRender && rowExpandable && rowExpandable(record) && expanded) {
  624. const currentExpandRow = this.renderExpandedRow({
  625. ...this.props,
  626. columns: flattenedColumns,
  627. level,
  628. index,
  629. record,
  630. expanded,
  631. });
  632. /**
  633. * If expandedRowRender returns falsy, this expanded row will not be rendered
  634. * Render an empty div before v1.19.7
  635. */
  636. if (!isNull(currentExpandRow)) {
  637. renderedRows.push(currentExpandRow);
  638. }
  639. }
  640. // render tree data
  641. if (recordHasChildren && expanded) {
  642. const nestedRows = this.renderBodyRows(recordChildren, level + 1);
  643. renderedRows.push(...nestedRows);
  644. }
  645. });
  646. return renderedRows;
  647. }
  648. renderBody = (direction?: Direction) => {
  649. const {
  650. scroll,
  651. prefixCls,
  652. columns,
  653. components,
  654. fixed,
  655. handleWheel,
  656. headerRef,
  657. handleBodyScroll,
  658. anyColumnFixed,
  659. showHeader,
  660. emptySlot,
  661. includeHeader,
  662. dataSource,
  663. onScroll,
  664. groups,
  665. expandedRowRender,
  666. tableLayout,
  667. } = this.props;
  668. const x = get(scroll, 'x');
  669. const y = get(scroll, 'y');
  670. const bodyStyle: {
  671. maxHeight?: string | number;
  672. overflow?: string;
  673. WebkitTransform?: string
  674. } = {};
  675. const tableStyle: {
  676. width?: string | number
  677. } = {};
  678. const Table = get(components, 'body.outer', 'table') as unknown as typeof React.Component;
  679. const BodyWrapper = (get(components, 'body.wrapper') || 'tbody') as unknown as typeof React.Component;
  680. if (y) {
  681. bodyStyle.maxHeight = y;
  682. }
  683. if (x) {
  684. tableStyle.width = x;
  685. }
  686. if (anyColumnFixed && size(dataSource)) {
  687. // Auto is better than scroll. For example, when there is only scrollY, the scroll axis is not displayed horizontally.
  688. bodyStyle.overflow = 'auto';
  689. // Fix weird webkit render bug
  690. bodyStyle.WebkitTransform = 'translate3d (0, 0, 0)';
  691. }
  692. const colgroup = <ColGroup components={get(components, 'body')} columns={columns} prefixCls={prefixCls} />;
  693. // const tableBody = this.renderBody();
  694. const wrapCls = `${prefixCls}-body`;
  695. const baseTable = (
  696. <div
  697. key="bodyTable"
  698. className={wrapCls}
  699. style={bodyStyle}
  700. ref={this.forwardRef}
  701. onWheel={handleWheel}
  702. onScroll={handleBodyScroll}
  703. >
  704. <Table
  705. role={isMap(groups) || isFunction(expandedRowRender) || isTreeTable({ dataSource }) ? 'treegrid' : 'grid'}
  706. aria-rowcount={dataSource && dataSource.length}
  707. aria-colcount={columns && columns.length}
  708. style={tableStyle}
  709. className={classnames(prefixCls, {
  710. [`${prefixCls}-fixed`]: tableLayout === 'fixed',
  711. })}
  712. >
  713. {colgroup}
  714. {includeHeader && showHeader ? (
  715. <TableHeader {...this.props} ref={headerRef} components={components} columns={columns} />
  716. ) : null}
  717. <BodyWrapper className={`${prefixCls}-tbody`} onScroll={onScroll}>
  718. {isMap(groups) ? this.renderGroupedRows() : this.renderBodyRows(dataSource)}
  719. </BodyWrapper>
  720. </Table>
  721. {emptySlot}
  722. </div>
  723. );
  724. if (fixed && columns.length) {
  725. return (
  726. <div key="bodyTable" className={`${prefixCls}-body-outer`}>
  727. {baseTable}
  728. </div>
  729. );
  730. }
  731. return baseTable;
  732. };
  733. render() {
  734. const { virtualized } = this.props;
  735. const { direction } = this.context;
  736. return virtualized ? this.renderVirtualizedBody(direction) : this.renderBody(direction);
  737. }
  738. }
  739. export default React.forwardRef<HTMLDivElement, Omit<BodyProps, 'forwardedRef'>>(function TableBody(props, ref) {
  740. return <Body {...props} forwardedRef={ref} />;
  741. });
  742. export interface RenderExpandedRowProps {
  743. style?: React.CSSProperties;
  744. components?: TableComponents;
  745. renderExpandIcon: (record?: Record<string, any>, isNested?: boolean) => ReactNode | null;
  746. expandedRowRender?: ExpandedRowRender<Record<string, any>>;
  747. record?: Record<string, any>;
  748. columns?: ColumnProps[];
  749. expanded?: boolean;
  750. index?: number;
  751. rowKey?: RowKey<Record<string, any>>;
  752. virtualized?: Virtualized;
  753. level?: number
  754. }
  755. export interface RenderSectionRowProps {
  756. dataSource?: Record<string, any>[];
  757. columns?: ColumnProps[];
  758. rowKey?: RowKey<Record<string, any>>;
  759. group?: any;
  760. groupKey: string | number;
  761. index?: number;
  762. expanded?: boolean
  763. }