wix-style-react
Version:
wix-style-react
501 lines • 25.6 kB
JavaScript
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