index.tsx 30 KB

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