UNPKG

wix-style-react

Version:
501 lines • 25.6 kB
import React, { Component, memo } from 'react'; import PropTypes from 'prop-types'; import { SortByArrowUp, SortByArrowDown, } 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 { TooltipCommonProps } from '../../common/PropTypes/TooltipCommon'; import { st, classes } from './DataTable.st.css'; import InfiniteScroll from '../../utils/InfiniteScroll'; import InfoIcon from '../../InfoIcon'; import { virtualRowsAreEqual, getStickyColumnStyle } from './DataTable.utils'; import { WixStyleReactMaskingContext } from '../../WixStyleReactMaskingProvider/context'; import DataTableRow from './components/DataTableRow'; export const DataTableHeader = props => { const { dataHook, horizontalScroll, leftShadowVisible, rightShadowVisible, stickyColumns, rowVerticalPadding, } = props; const headerContainerRef = React.useRef(); 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("div", { "data-hook": dataHook, ref: headerContainerRef, className: classNames({ [classes.tableHeaderScrollContent]: horizontalScroll, }) }, React.createElement("table", { style: { width: props.width }, className: st(classes.table, { rowVerticalPadding, }) }, React.createElement(TableHeader, { ...props })))); return horizontalScroll ? wrapWithHorizontalScroll(table) : table; }; DataTableHeader.propTypes = { width: PropTypes.string, }; 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.renderTable = rowsToRender => { const { dataHook, showLastRowDivider, horizontalScroll, rowVerticalPadding, removeRowDetailsPadding, dragAndDrop, onDragStart, onDragEnd, onDragCancel, isDragAndDropDisabled, data, } = this.props; const style = { width: this.props.width }; const className = st(classes.table, { removeRowDetailsPadding, showLastRowDivider, rowVerticalPadding, }); let table = (React.createElement("table", { id: this.props.id, style: style, className: className }, !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 = (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) => this.renderVirtualizedTableElement({ ...props, ref })); this.renderVirtualizedTableElement = ({ children, ...rest }) => { const { dragAndDrop, data, onDragStart, onDragEnd, onDragCancel, isDragAndDropDisabled, horizontalScroll, } = this.props; let table = (React.createElement("table", { ...rest }, 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.renderTable(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.renderHeaderCell = (column, colNum) => { const { stickyColumns, columns, isApplyColumnWidthStyle } = this.props; const stickyColumnStyle = colNum < stickyColumns ? getStickyColumnStyle(columns, column) : undefined; const widthStyle = isApplyColumnWidthStyle ? column.width ? { width: column.width } : { flex: 1 } : { width: column.width }; 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', ...stickyColumnStyle, }; const optionalHeaderCellProps = {}; if (column.sortable) { optionalHeaderCellProps.onClick = () => this.props.onSortClick && this.props.onSortClick(column, colNum); } const isSticky = colNum < stickyColumns; return (React.createElement("th", { key: column.key ?? colNum, "data-hook": column.dataHook, style: style, "data-sticky": isSticky, 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("div", { 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', }) }, column.title, this.renderSortingArrow(column.sortDescending, colNum), this.renderInfoTooltip(column.infoTooltipProps, colNum)))); }; } get style() { return classes; } render() { const { columns, dragAndDrop, isDragAndDropDisabled, headerRowClass, headerClass, } = this.props; // Backwards compatibility - if `dragAndDrop` is set but createDragHandleColumn is `null` (older version), still render the drag-handle header cell const dragHandleHeaderCell = dragAndDrop && dragAndDrop.createDragHandleColumn == null && !isDragAndDropDisabled ? (React.createElement("th", { width: "50px", className: classNames(this.style.thText, this.style.dnd, { [this.style.thSkinStandard]: !this.props.skin || this.props.skin === 'standard', [this.style.thSkinNeutral]: this.props.skin === 'neutral', }) })) : null; return (React.createElement("thead", { style: { display: this.props.hideHeaderAccessible ? 'none' : undefined, }, className: classNames(headerClass) }, React.createElement("tr", { className: classNames(headerRowClass) }, dragHandleHeaderCell, columns.map(this.renderHeaderCell)))); } } TableHeader.propTypes = { onSortClick: PropTypes.func, thPadding: PropTypes.string, thHeight: PropTypes.string, thFontSize: PropTypes.string, thBorder: PropTypes.string, thColor: PropTypes.string, thOpacity: PropTypes.string, thLetterSpacing: PropTypes.string, thBoxShadow: PropTypes.string, headerRowClass: PropTypes.string, headerClass: PropTypes.string, columns: PropTypes.array, skin: PropTypes.oneOf(['standard', 'neutral']), leftShadowVisible: PropTypes.bool, rightShadowVisible: PropTypes.bool, dragAndDrop: PropTypes.object, hideHeaderAccessible: PropTypes.bool, }; 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.propTypes = { dataHook: PropTypes.string, /** An id to pass to the table */ id: PropTypes.string, /** The data to display. (If data.id exists then it will be used as the React key value for each row, otherwise, the rowIndex will be used) */ data: validateData, /** Configuration of the table's columns. See table below */ columns: PropTypes.arrayOf(PropTypes.shape({ title: PropTypes.oneOfType([PropTypes.node, PropTypes.string]).isRequired, render: PropTypes.func.isRequired, sortable: PropTypes.bool, infoTooltipProps: PropTypes.shape(TooltipCommonProps), sortDescending: PropTypes.bool, align: PropTypes.oneOf(['start', 'center', 'end']), important: PropTypes.bool, style: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), stickyActionCell: PropTypes.bool, })).isRequired, /** Should the table show the header when data is empty */ showHeaderWhenEmpty: PropTypes.bool, /** A string data-hook to apply to all table body rows. or a func which calculates the data-hook for each row - Signature: `(rowData, rowNum) => string` */ rowDataHook: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), /** A class to apply to all table body rows */ rowClass: PropTypes.string, /** A class to apply to the header table row */ headerRowClass: PropTypes.string, /** A class to apply to the table header */ headerClass: PropTypes.string, /** A func that gets row data and returns a class(es) to apply to that specific row */ dynamicRowClass: PropTypes.func, /** A func that gets row data and returns boolean if row is selected or not */ isRowSelected: PropTypes.func, /** A func that gets row data and returns boolean if row is highlighted or not */ isRowHighlight: PropTypes.func, /** A callback method to be called on row click. Signature: `onRowClick(rowData, rowNum)` */ onRowClick: PropTypes.func, /** A callback method to be called on row mouse enter. Signature: `onMouseEnterRow(rowData, rowNum)` */ onMouseEnterRow: PropTypes.func, /** A callback method to be called on row mouse leave. Signature: `onMouseLeaveRow(rowData, rowNum)` */ onMouseLeaveRow: PropTypes.func, /** If true, table will not render all data to begin with, but will gradually render the data as the user scrolls */ infiniteScroll: PropTypes.bool, /** If infiniteScroll is on, this prop will determine how many rows will be rendered on each load */ itemsPerPage: PropTypes.number, /** The width of the fixed table. Can be in percentages or pixels. */ width: PropTypes.string, /** Table styling. Supports `standard` and `neutral`. */ skin: PropTypes.oneOf(['standard', 'neutral']), /** A callback when more items are requested by the user. */ loadMore: PropTypes.func, /** Whether there are more items to be loaded. Event listeners are removed if false. */ hasMore: PropTypes.bool, /** The loader to show when loading more items. */ loader: PropTypes.node, /** Add scroll listeners to the window, or else, the component's parentNode. */ useWindow: PropTypes.bool, /** Add scroll listeners to specified DOM Object. */ scrollElement: PropTypes.object, /** Indicates whether to invoke `loadMore` on the initial rendering. */ initialLoad: PropTypes.bool, /** Table cell vertical padding: * - `large`: 24px * - `medium`: 18px * - `small`: 15px * - `tiny`: 12px * */ rowVerticalPadding: PropTypes.oneOf(['tiny', 'small', 'medium', 'large']), /** this prop is deprecated and should not be used * @deprecated */ thPadding: PropTypes.string, /** this prop is deprecated and should not be used * @deprecated */ thHeight: PropTypes.string, /** this prop is deprecated and should not be used * @deprecated */ thFontSize: PropTypes.string, /** this prop is deprecated and should not be used * @deprecated */ thBorder: PropTypes.string, /** this prop is deprecated and should not be used * @deprecated */ thColor: PropTypes.string, /** this prop is deprecated and should not be used * @deprecated */ thOpacity: PropTypes.string, /** this prop is deprecated and should not be used * @deprecated */ thBoxShadow: PropTypes.string, /** this prop is deprecated and should not be used * @deprecated */ thLetterSpacing: PropTypes.string, /** Function that returns React component that will be rendered in row details section. Example: `rowDetails={(row, rowNum) => <MyRowDetailsComponent {...row} />}` */ rowDetails: PropTypes.func, /** Removes row details padding */ removeRowDetailsPadding: PropTypes.bool, /** Allows to open multiple row details */ allowMultiDetailsExpansion: PropTypes.bool, /** Should we remove the header of the table from DOM. */ hideHeader: PropTypes.bool, /** Should we hide the header of the table with display: none. */ hideHeaderAccessible: PropTypes.bool, /** A flag specifying weather to show a divider after the last row */ showLastRowDivider: PropTypes.bool, /** A flag specifying weather to apply column width style to table cells */ isApplyColumnWidthStyle: PropTypes.bool, /** ++EXPERIMENTAL++ virtualize the table scrolling for long list items */ virtualized: PropTypes.bool, /** ++EXPERIMENTAL++ Set virtualized table height */ virtualizedTableHeight: PropTypes.number, /** ++EXPERIMENTAL++ Set virtualized table row height */ virtualizedLineHeight: PropTypes.number, /** ++EXPERIMENTAL++ Set ref on virtualized List containing table rows */ virtualizedListRef: PropTypes.any, /** array of selected ids in the table. Note that `isRowSelected` prop provides greater selection logic flexibility and is recommended to use instead. */ selectedRowsIds: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])), /** A callback function called on each column title click. Signature `onSortClick(colData, colNum)` */ onSortClick: PropTypes.func, /** a function which will be called for every row in `data` to specify if it should appear as disabled. Example: `isRowDisabled={(rowData) => !rowData.isEnabled}` */ isRowDisabled: PropTypes.func, /* Horizontal scroll support props. */ horizontalScroll: PropTypes.bool, stickyColumns: PropTypes.number, leftShadowVisible: PropTypes.bool, rightShadowVisible: PropTypes.bool, onUpdateScrollShadows: PropTypes.func, dragAndDrop: PropTypes.object, onDragEnd: PropTypes.func, onDragStart: PropTypes.func, onDragCancel: PropTypes.func, isDragAndDropDisabled: PropTypes.bool, }; DataTable.displayName = 'DataTable'; export default DataTable; //# sourceMappingURL=DataTable.js.map