UNPKG

@wix/design-system

Version:

@wix/design-system

389 lines 21.5 kB
import React, { Component, memo, useRef, useEffect } from 'react'; import { SortByArrowDown, SortByArrowUp, } from '@wix/wix-ui-icons-common/system'; import classNames from 'classnames'; import { VariableSizeList as List } from 'react-window'; import { ScrollSyncPane } from 'react-scroll-sync'; import { classes, st } from './DataTable.st.css.js'; import InfiniteScroll from '../../utils/InfiniteScroll'; import InfoIcon from '../../InfoIcon'; import Text from '../../Text'; import { virtualRowsAreEqual } from './DataTable.utils'; import { WixStyleReactMaskingContext } from '../../WixStyleReactMaskingProvider/context'; import DataTableRow from './components/DataTableRow'; import { TableFloatingScrollBarInitializer } from './TableFloatingScrollBarInitializer'; import TableResizeHandle from '../components/TableResizeHandle'; import { ColumnResizeConsumer } from '../ColumnResize'; export const DataTableHeader = props => { const { dataHook, horizontalScroll, leftShadowVisible, rightShadowVisible, stickyColumns, rowVerticalPadding, } = props; const headerContainerRef = React.useRef(null); const wrapWithHorizontalScroll = table => (React.createElement("div", { className: classNames(classes.scrollWrapper, { [classes.leftShadowVisible]: !!leftShadowVisible, [classes.rightShadowVisible]: !!rightShadowVisible, [classes.withStickyColumns]: !!stickyColumns, }) }, React.createElement(ScrollSyncPane, { attachTo: headerContainerRef }, table))); const table = (React.createElement(ColumnResizeConsumer, null, ({ getTableWidth }) => (React.createElement("div", { "data-hook": dataHook, ref: headerContainerRef, className: classNames({ [classes.tableHeaderScrollContent]: horizontalScroll, }) }, React.createElement("table", { style: { width: getTableWidth(props.width) }, className: st(classes.table, { rowVerticalPadding, }) }, React.createElement(TableHeader, { ...props })))))); return horizontalScroll ? wrapWithHorizontalScroll(table) : table; }; class DataTable extends React.Component { constructor(props) { super(props); this._updateScrollShadows = () => { const { onUpdateScrollShadows } = this.props; if (!onUpdateScrollShadows) { return; } const { scrollLeft, scrollWidth, clientWidth } = this.contentRef.current; const leftShadowVisible = scrollLeft > 0; const rightShadowVisible = scrollWidth - scrollLeft > clientWidth; onUpdateScrollShadows(leftShadowVisible, rightShadowVisible); }; this.wrapWithInfiniteScroll = table => { return (React.createElement(InfiniteScroll, { ref: this.props.infiniteScrollRef, pageStart: 0, loadMore: this.loadMore, data: this.props.data, hasMore: !this.props.controlled ? this.state.currentPage < this.state.lastPage || this.props.hasMore : this.props.hasMore, loader: this.props.loader, useWindow: this.props.useWindow, scrollElement: this.props.scrollElement, initialLoad: this.props.initialLoad }, table)); }; this.wrapWithHorizontalScroll = (table, attachTo) => { const { leftShadowVisible, rightShadowVisible, stickyColumns } = this.props; return (React.createElement("div", { className: classNames(this.style.scrollWrapper, { [this.style.leftShadowVisible]: !!leftShadowVisible, [this.style.rightShadowVisible]: !!rightShadowVisible, [this.style.withStickyColumns]: !!stickyColumns, }) }, React.createElement(ScrollSyncPane, { attachTo: attachTo }, table))); }; this.renderTableWithWidthContext = rowsToRender => { return (React.createElement(ColumnResizeConsumer, null, ({ getTableWidth }) => this.renderTable(rowsToRender, getTableWidth))); }; this.renderTable = (rowsToRender, getTableWidth) => { const { dataHook, showLastRowDivider, horizontalScroll, rowVerticalPadding, removeRowDetailsPadding, dragAndDrop, onDragStart, onDragEnd, onDragCancel, isDragAndDropDisabled, data, tableRef, resizable, columns, } = this.props; const style = { width: getTableWidth(this.props.width) }; const className = st(classes.table, { removeRowDetailsPadding, showLastRowDivider, rowVerticalPadding, }); let table = (React.createElement("table", { id: this.props.id, style: style, className: className, ref: tableRef }, !this.props.hideHeader && React.createElement(TableHeader, { ...this.props }), this.renderBody(rowsToRender))); if (dragAndDrop) { const { DroppableTableContext } = dragAndDrop; table = (React.createElement(DroppableTableContext, { items: data, onDragStart: onDragStart, onDragEnd: onDragEnd, onDragCancel: onDragCancel, isDragAndDropDisabled: isDragAndDropDisabled, horizontalScroll: horizontalScroll, className: className, style: style, renderRow: this.renderRowWithMaskingClassNames, renderTableContainer: table => { let child = (React.createElement("div", { className: classNames({ [classes.tableHeaderScrollContent]: horizontalScroll, }) }, table)); if (horizontalScroll) { child = this.wrapWithHorizontalScroll(child); } return child; } }, table)); } table = horizontalScroll ? (React.createElement(TableFloatingScrollBarInitializer, null, table)) : (table); table = (React.createElement("div", { "data-hook": dataHook, ...(horizontalScroll ? { className: classes.tableBodyScrollContent, ref: this.contentRef, onScroll: this._updateScrollShadows, } : undefined) }, table)); return table; }; this.renderBody = rows => { const { BodyElementType = 'tbody' } = this.props; return (React.createElement(WixStyleReactMaskingContext.Consumer, null, ({ maskingClassNames }) => (React.createElement(BodyElementType, null, rows.map((rowData, index) => this.renderRow({ rowData, rowNum: index, maskingClassNames })))))); }; this.renderRowWithMaskingClassNames = ({ rowData, rowNum, style, isDragOverlay, }) => (React.createElement(WixStyleReactMaskingContext.Consumer, null, ({ maskingClassNames }) => this.renderRow({ rowData, rowNum, style, maskingClassNames, isDragOverlay, }))); this.renderRow = rowProps => { return (React.createElement(DataTableRow, { key: rowProps.rowNum, ...rowProps, ...this.props, toggleRowDetails: this.toggleRowDetails, showDetails: !!this.state.selectedRows.get(rowProps.rowData) })); }; this.calcLastPage = ({ data, itemsPerPage }) => Math.ceil(data.length / itemsPerPage) - 1; this.loadMore = () => { if (!this.props.controlled && this.state.currentPage < this.state.lastPage) { this.setState({ currentPage: this.state.currentPage + 1 }); } else { this.props.loadMore && this.props.loadMore(); } }; this.toggleRowDetails = selectedRow => { const { selectedRows } = this.state; const allowMultipleRowDetails = this.props.allowMultiDetailsExpansion && !this.props.virtualized; const newSelectedRows = new Map([ ...(allowMultipleRowDetails ? [...selectedRows] : []), [selectedRow, !selectedRows.get(selectedRow)], ]); this.setState({ selectedRows: newSelectedRows }); }; this.renderVirtualizedRow = ({ data, index, style }) => this.renderRow({ rowData: data.data[index], rowNum: index, style }); this.renderVirtualizedMemoizedRow = memo(this.renderVirtualizedRow, virtualRowsAreEqual); this.getVirtualRowHeight = () => this.props.virtualizedLineHeight; this.virtualizedTableElementWithRefForward = React.forwardRef((props, ref) => (React.createElement(ColumnResizeConsumer, null, ({ getTableWidth }) => this.renderVirtualizedTableElement({ ...props, ref }, getTableWidth)))); this.renderVirtualizedTableElement = ({ children, ...rest }, getTableWidth) => { const { dragAndDrop, data, onDragStart, onDragEnd, onDragCancel, isDragAndDropDisabled, horizontalScroll, resizable, columns, } = this.props; const tableStyle = { ...rest.style, width: getTableWidth(rest.style?.width || '100%'), }; let table = (React.createElement("table", { ...rest, style: tableStyle }, React.createElement(TableHeader, { ...this.props }), children)); if (dragAndDrop) { const { DroppableTableContext } = dragAndDrop; table = (React.createElement(DroppableTableContext, { items: data, onDragStart: onDragStart, onDragEnd: onDragEnd, onDragCancel: onDragCancel, isDragAndDropDisabled: isDragAndDropDisabled, horizontalScroll: horizontalScroll, className: classNames(this.style.table, this.style.virtualized), renderRow: this.renderRow }, table)); } return table; }; this.renderVirtualizedTable = () => { const { dataHook, data, virtualizedTableHeight, virtualizedListRef } = this.props; return (React.createElement("div", { "data-hook": dataHook }, React.createElement(List, { ref: virtualizedListRef, className: classNames(this.style.table, this.style.virtualized), height: virtualizedTableHeight, itemCount: data.length, itemData: this.props, width: "100%", itemSize: this.getVirtualRowHeight, outerElementType: this.virtualizedTableElementWithRefForward, innerElementType: "tbody" }, this.renderVirtualizedMemoizedRow))); }; let state = { selectedRows: new Map(), }; if (!props.controlled && props.infiniteScroll) { state = { ...state, ...this.createInitialScrollingState(props) }; } this.state = state; this.contentRef = React.createRef(); if (props.horizontalScroll && 'ResizeObserver' in window) { this.contentResizeObserver = new ResizeObserver(this._updateScrollShadows); } } componentDidMount() { const { contentResizeObserver, contentRef } = this; if (contentResizeObserver && contentRef.current) { contentResizeObserver.observe(contentRef.current); } } componentWillUnmount() { const { contentResizeObserver, contentRef } = this; if (contentResizeObserver && contentRef.current) { contentResizeObserver.unobserve(contentRef.current); } } get style() { return classes; } UNSAFE_componentWillReceiveProps(nextProps) { let isLoadingMore = false; if (!this.props.controlled && this.props.infiniteScroll && nextProps.data !== this.props.data) { if (nextProps.data instanceof Array && this.props.data instanceof Array) { isLoadingMore = true; const lastPage = this.calcLastPage(nextProps); const currentPage = this.state.currentPage < lastPage ? this.state.currentPage + 1 : this.state.currentPage; this.setState({ lastPage, currentPage }); } if (!isLoadingMore) { this.setState(this.createInitialScrollingState(nextProps)); } } } createInitialScrollingState(props) { return { currentPage: 0, lastPage: this.calcLastPage(props) }; } render() { const { virtualized, data, showHeaderWhenEmpty, horizontalScroll, infiniteScroll, itemsPerPage, controlled, } = this.props; if (!data.length && !showHeaderWhenEmpty) { return null; } if (virtualized) { return this.renderVirtualizedTable(data); } const rowsToRender = !controlled && infiniteScroll ? data.slice(0, (this.state.currentPage + 1) * itemsPerPage) : data; let table = this.renderTableWithWidthContext(rowsToRender); if (horizontalScroll) { table = this.wrapWithHorizontalScroll(table, this.contentRef); } if (infiniteScroll) { table = this.wrapWithInfiniteScroll(table); } return table; } } class TableHeader extends Component { constructor() { super(...arguments); this.renderSortingArrow = (sortDescending, colNum) => { if (sortDescending === undefined) { return null; } const Arrow = sortDescending ? SortByArrowDown : SortByArrowUp; return (React.createElement("span", { "data-hook": `${colNum}_title`, className: this.style.sortArrow }, React.createElement(Arrow, { height: 12, "data-hook": sortDescending ? 'sort_arrow_dec' : 'sort_arrow_asc' }))); }; this.renderInfoTooltip = (tooltipProps, colNum) => { if (tooltipProps === undefined) { return null; } const { content, ...otherTooltipProps } = tooltipProps; return (React.createElement(InfoIcon, { content: content, tooltipProps: otherTooltipProps, dataHook: `${colNum}_info_tooltip`, className: this.style.infoTooltipWrapper })); }; this.renderTitleSuffix = (titleSuffix, colNum) => { if (!titleSuffix) { return null; } return (React.createElement("span", { "data-hook": `${colNum}_title_suffix`, className: this.style.titleSuffix, onClick: e => e.stopPropagation(), onKeyDown: e => e.stopPropagation(), role: "presentation" }, titleSuffix)); }; this.renderHeaderCell = (column, colNum, resizeHelpers) => { const { stickyColumns, columns, isApplyColumnWidthStyle, resizable: isTableResizable, } = this.props; const { resizeColumn, startColumnResize, endColumnResize, getEffectiveColumnWidth, getStickyColumnStyle, } = resizeHelpers; const { id, disabled, hideDisabledResizeHandle } = column.resizeProps ?? {}; const isSticky = colNum < stickyColumns; const isColumnResizable = isTableResizable && id !== undefined; const isLastColumn = colNum === columns.length - 1; const shouldShowHandle = isColumnResizable && !(disabled && hideDisabledResizeHandle) && !isLastColumn; const stickyColumnStyle = isSticky ? getStickyColumnStyle(columns, column) : undefined; const currentWidth = getEffectiveColumnWidth(column); const widthStyle = isApplyColumnWidthStyle ? currentWidth ? { width: currentWidth } : {} : { width: currentWidth }; const style = { ...(isApplyColumnWidthStyle && typeof column.style !== 'function' ? column.style : undefined), ...widthStyle, padding: this.props.thPadding, height: this.props.thHeight, fontSize: this.props.thFontSize, border: this.props.thBorder, boxShadow: this.props.thBoxShadow, color: this.props.thColor, opacity: this.props.thOpacity, letterSpacing: this.props.thLetterSpacing, cursor: column.sortable === undefined ? 'auto' : 'pointer', position: isColumnResizable && !isSticky ? 'relative' : undefined, ...stickyColumnStyle, }; const optionalHeaderCellProps = {}; const hasTitleSuffix = !!column.titleSuffix; const Element = column.sortable && !hasTitleSuffix ? 'button' : 'div'; if (column.sortable) { optionalHeaderCellProps.onClick = () => this.props.onSortClick && this.props.onSortClick(column, colNum); } const elementProps = column.sortable ? hasTitleSuffix ? { role: 'button', tabIndex: 0, onKeyDown: e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.props.onSortClick?.(column, colNum); } }, } : { type: 'button' } : undefined; return (React.createElement("th", { key: column.key ?? colNum, "data-hook": column.dataHook, style: style, "data-sticky": isSticky, "aria-sort": column.sortable ? column.sortDescending === true ? 'descending' : column.sortDescending === false ? 'ascending' : 'none' : undefined, className: classNames(this.style.thText, { [this.style.thSkinStandard]: !this.props.skin || this.props.skin === 'standard', [this.style.thSkinNeutral]: this.props.skin === 'neutral', [this.style.sticky]: isSticky, [this.style.lastSticky]: colNum === stickyColumns - 1, [this.style.stickyActionCell]: column.stickyActionCell, }), ...optionalHeaderCellProps }, React.createElement(Element, { ...elementProps, className: classNames(this.style.thContainer, { [this.style.alignStart]: !column.align || column.align === 'start', [this.style.alignCenter]: column.align === 'center', [this.style.alignEnd]: column.align === 'end', }) }, typeof column.title === 'string' && column.key !== 'bulk-selection-cell' ? (React.createElement(Text, { className: this.style.titleText, ellipsis: true, showTooltip: true, size: "small", weight: "thin" }, column.title)) : (column.title), this.renderSortingArrow(column.sortDescending, colNum), this.renderInfoTooltip(column.infoTooltipProps, colNum), this.renderTitleSuffix(column.titleSuffix, colNum)), shouldShowHandle && (React.createElement(TableResizeHandle, { columnId: id, currentWidth: currentWidth, onResize: resizeColumn, onResizeStart: startColumnResize, onResizeEnd: endColumnResize, dataHook: "table-resize-handle", disabled: disabled, skin: this.props.skin })))); }; } get style() { return classes; } render() { const { columns, dragAndDrop, isDragAndDropDisabled, headerRowClass, headerClass, } = this.props; return (React.createElement(ColumnResizeConsumer, null, ({ getEffectiveColumnWidth, resizeColumn, startColumnResize, endColumnResize, getStickyColumnStyle, }) => (React.createElement("thead", { style: { display: this.props.hideHeaderAccessible ? 'none' : undefined, }, className: classNames(headerClass) }, React.createElement("tr", { className: classNames(headerRowClass) }, columns.map((column, colNum) => { const resizeHelpers = { getEffectiveColumnWidth, resizeColumn, startColumnResize, endColumnResize, getStickyColumnStyle, }; return this.renderHeaderCell(column, colNum, resizeHelpers); })))))); } } function validateData(props, propName) { if (props[propName]) { if (props[propName].constructor && props[propName].constructor.name && props[propName].constructor.name.toLowerCase().indexOf('array') > -1) { return null; } else { return Error('Data element must be an array type'); } } return null; } DataTable.defaultProps = { data: [], columns: [], selectedRowsIds: [], isRowSelected: null, showHeaderWhenEmpty: false, infiniteScroll: false, itemsPerPage: 20, width: '100%', loadMore: null, hasMore: false, initialLoad: true, loader: React.createElement("div", { className: "loader" }, "Loading ..."), scrollElement: null, useWindow: true, showLastRowDivider: true, virtualizedLineHeight: 60, skin: 'standard', horizontalScroll: false, stickyColumns: 0, isRowDisabled: () => false, rowVerticalPadding: 'small', removeRowDetailsPadding: false, dragAndDrop: null, }; DataTable.displayName = 'DataTable'; export default DataTable; //# sourceMappingURL=DataTable.js.map