@wix/design-system
Version:
@wix/design-system
389 lines • 21.5 kB
JavaScript
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