/* eslint-disable max-len */ /* eslint-disable eqeqeq */ import React, { createRef, ReactNode } from 'react'; import classnames from 'classnames'; import PropTypes from 'prop-types'; import { each, noop, get, stubTrue, omit, isEqual, pick } from 'lodash'; import { strings, cssClasses } from '@douyinfe/semi-foundation/table/constants'; import shallowEqualObjects from '@douyinfe/semi-foundation/utils/shallowEqualObjects'; import TableRowFoundation, { TableRowAdapter } from '@douyinfe/semi-foundation/table/tableRowFoundation'; import { isLastLeftFixed, arrayAdd, isFixedLeft, isFixedRight, isScrollbarColumn, isFirstFixedRight, isInnerColumnKey, isExpandedColumn } from '@douyinfe/semi-foundation/table/utils'; import Store from '@douyinfe/semi-foundation/utils/Store'; import { BaseRowKeyType } from '@douyinfe/semi-foundation/table/foundation'; import BaseComponent from '../../_base/baseComponent'; import TableCell from '../TableCell'; import { ColumnProps, Fixed, TableComponents, Virtualized, ExpandIcon, OnRow, RowExpandable } from '../interface'; export interface BaseRowProps { anyColumnFixed?: boolean; cellWidths?: number[]; className?: string; columns: ColumnProps[]; // required components?: TableComponents; // required disabled?: boolean; expandIcon?: ExpandIcon; expandableRow?: boolean; expanded?: boolean; expandedRow?: boolean; fixed?: Fixed; height?: string | number; hideExpandedColumn?: boolean; hovered: boolean; // required indent?: number; indentSize?: number; index?: number; isSection?: boolean; level?: number; onDidUpdate?: (ref: React.MutableRefObject) => void; onHover?: (mouseEnter: boolean, rowKey: string | number) => void; onRow?: OnRow; onRowClick?: (rowKey: BaseRowKeyType, e: React.MouseEvent, expand: boolean) => void; onRowDoubleClick?: (record: Record, e: React.MouseEvent) => void; onRowMouseEnter?: (record: Record, e: React.MouseEvent) => void; onRowMouseLeave?: (record: Record, e: React.MouseEvent) => void; prefixCls?: string; record?: Record; renderExpandIcon?: RenderExpandIcon; replaceClassName?: string; rowExpandable?: RowExpandable; rowKey?: string | number; // required, this place rowKey is a real key of the row selected?: boolean; store?: Store; style?: React.CSSProperties; virtualized?: Virtualized; visible: boolean; // required } export default class TableRow extends BaseComponent> { static propTypes = { anyColumnFixed: PropTypes.bool, cellWidths: PropTypes.array.isRequired, className: PropTypes.string, columns: PropTypes.array.isRequired, components: PropTypes.object.isRequired, disabled: PropTypes.bool, expandIcon: PropTypes.oneOfType([PropTypes.bool, PropTypes.func, PropTypes.node]), expandableRow: PropTypes.bool, expanded: PropTypes.bool, expandedRow: PropTypes.bool, fixed: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), hideExpandedColumn: PropTypes.bool, hovered: PropTypes.bool.isRequired, indent: PropTypes.number, indentSize: PropTypes.number, index: PropTypes.number, isSection: PropTypes.bool, level: PropTypes.number, onDidUpdate: PropTypes.func, onHover: PropTypes.func, onRow: PropTypes.func, onRowClick: PropTypes.func, onRowContextMenu: PropTypes.func, onRowDoubleClick: PropTypes.func, onRowMouseEnter: PropTypes.func, onRowMouseLeave: PropTypes.func, prefixCls: PropTypes.string, record: PropTypes.object, renderExpandIcon: PropTypes.func, replaceClassName: PropTypes.string, rowExpandable: PropTypes.func, rowKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, // real key of the row selected: PropTypes.bool, store: PropTypes.object, style: PropTypes.object, virtualized: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]), visible: PropTypes.bool.isRequired, }; static defaultProps = { columns: [] as [], rowExpandable: stubTrue, components: { body: { row: 'tr', cell: 'td', }, }, prefixCls: cssClasses.PREFIX, onRow: noop, onRowClick: noop, onRowDoubleClick: noop, onRowMouseEnter: noop, onRowMouseLeave: noop, onHover: noop, onDidUpdate: noop, visible: true, hovered: false, selected: false, disabled: false, }; get adapter(): TableRowAdapter { return { ...super.adapter, notifyClick: (...args) => this.props.onRowClick(...args), notifyDoubleClick: (...args) => this.props.onRowDoubleClick(...args), notifyMouseLeave: (...args) => { this.props.onHover(false, this.props.rowKey); this.props.onRowMouseEnter(...args); }, notifyMouseEnter: (...args) => { this.props.onHover(true, this.props.rowKey); this.props.onRowMouseEnter(...args); }, }; } ref: React.MutableRefObject; constructor(props: BaseRowProps) { super(props); this.ref = createRef(); this.foundation = new TableRowFoundation(this.adapter); } componentDidMount() { // fix #745 // didmount/willUnmount may be called twice when React.StrictMode is true in React 18, we need to ensure that this.cache.customRowProps is correct const { onRow, index, record, } = this.props; const customRowProps = this.adapter.getCache('customRowProps'); if (typeof customRowProps === 'undefined') { const { className: customClassName, style: customStyle, ...rowProps } = onRow(record, index) || {}; this.adapter.setCache('customRowProps', { ...rowProps }); } } shouldComponentUpdate(nextProps: BaseRowProps) { /** * Shallow comparison of incoming props to simulate PureComponent * Deep comparison cellWidths * * 浅层对比传入的 props,模拟 PureComponent * 深比较 cellWidths */ const omitProps = ['cellWidths']; const isPropsShallowEqual = shallowEqualObjects(omit(nextProps, omitProps), omit(this.props, omitProps)); if (!isPropsShallowEqual || !isEqual(pick(nextProps, omitProps), pick(this.props, omitProps))) { return true; } return false; } _cacheNode = (node: any) => { this.ref.current = node; }; // Pass true to render the tree-shaped expand button renderExpandIcon = (record: Record) => { const { renderExpandIcon } = this.props; return renderExpandIcon(record, true); }; renderCells() { const { columns, record, index, prefixCls, fixed, components, expandableRow, level, expandIcon, rowExpandable, isSection, expandedRow, virtualized, indentSize, hideExpandedColumn, cellWidths, selected, expanded, disabled, onDidUpdate, } = this.props; const BodyCell = get(components, 'body.cell', strings.DEFAULT_COMPONENTS.body.cell); const cells: ReactNode[] = []; const displayExpandedColumn = rowExpandable(record); let firstIndex = 0; // const dataColumns = getDataColumns(columns); each(columns, (column, columnIndex) => { const columnKey = get(column, 'key'); const expandableProps: { renderExpandIcon?: (record: Record) => ReactNode; expandIcon?: ExpandIcon; indent?: number } = {}; if (fixed !== 'right') { if (isInnerColumnKey(columnKey)) { firstIndex++; } if (expandableRow && columnIndex === firstIndex) { expandableProps.renderExpandIcon = this.renderExpandIcon; if (hideExpandedColumn || isSection) { expandableProps.expandIcon = expandIcon != null ? expandIcon : true; } } // Only the first data row will be indented if (level != null && columnIndex === firstIndex) { expandableProps.indent = level; if (!expandableRow) { expandableProps.indent = level + 1; } } } if (isExpandedColumn(column) && !displayExpandedColumn) { cells.push(); } else if (!isScrollbarColumn(column)) { const diyProps: { width?: number } = {}; if (BodyCell !== strings.DEFAULT_COMPONENTS.body.cell && virtualized && !expandedRow) { diyProps.width = get(cellWidths, columnIndex); } cells.push( ); } }); return cells; } handleMouseEnter = (e: React.MouseEvent) => { this.foundation.handleMouseEnter(e); const customRowProps = this.adapter.getCache('customRowProps'); if (typeof customRowProps.onMouseEnter === 'function') { customRowProps.onMouseEnter(e); } }; handleMouseLeave = (e: React.MouseEvent) => { this.foundation.handleMouseLeave(e); const customRowProps = this.adapter.getCache('customRowProps'); if (typeof customRowProps.onMouseLeave === 'function') { customRowProps.onMouseLeave(e); } }; handleClick = (e: React.MouseEvent) => { this.foundation.handleClick(e); const customRowProps = this.adapter.getCache('customRowProps'); if (customRowProps && typeof customRowProps.onClick === 'function') { customRowProps.onClick(e); } }; render() { const { style } = this.props; const { components, prefixCls, selected, onRow, index, className, replaceClassName, record, hovered, expanded, expandableRow, level, expandedRow, isSection } = this.props; const BodyRow: any = components.body.row; const { className: customClassName, style: customStyle, ...rowProps } = onRow(record, index) || {}; this.adapter.setCache('customRowProps', { ...rowProps }); const baseRowStyle = { ...style, ...customStyle }; const rowCls = typeof replaceClassName === 'string' && replaceClassName.length ? replaceClassName : classnames( className, `${prefixCls}-row`, { [`${prefixCls}-row-selected`]: selected, [`${prefixCls}-row-expanded`]: expanded, [`${prefixCls}-row-hovered`]: hovered, }, customClassName ); const ariaProps = {}; if (typeof index === 'number') { ariaProps['aria-rowindex'] = index + 1; } if (expandableRow) { ariaProps['aria-expanded'] = expanded; } // if row is expandedRow, set it's level to 2 if (expanded || expandedRow) { ariaProps['aria-level'] = 2; } if (typeof level === 'number') { ariaProps['aria-level'] = level + 1; } if (isSection) { ariaProps['aria-level'] = 1; } return ( {this.renderCells()} ); } } export type RenderExpandIcon = (record: Record, isNested: boolean) => ReactNode | null;