@shopify/polaris
Version:
Shopify’s admin product component library
645 lines (571 loc) • 23.7 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var React = require('react');
var isEqual = require('react-fast-compare');
var debounce = require('../../utilities/debounce.js');
var css = require('../../utilities/css.js');
var shared = require('../shared.js');
var utilities = require('./utilities.js');
var DataTable$1 = require('./DataTable.scss.js');
var Cell = require('./components/Cell/Cell.js');
var Navigation = require('./components/Navigation/Navigation.js');
var AfterInitialMount = require('../AfterInitialMount/AfterInitialMount.js');
var Sticky = require('../Sticky/Sticky.js');
var hooks = require('../../utilities/i18n/hooks.js');
var EventListener = require('../EventListener/EventListener.js');
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var React__default = /*#__PURE__*/_interopDefaultLegacy(React);
var isEqual__default = /*#__PURE__*/_interopDefaultLegacy(isEqual);
class DataTableInner extends React.PureComponent {
constructor(...args) {
super(...args);
this.state = {
condensed: false,
columnVisibilityData: [],
isScrolledFarthestLeft: true,
isScrolledFarthestRight: false,
rowHovered: undefined
};
this.dataTable = /*#__PURE__*/React.createRef();
this.scrollContainer = /*#__PURE__*/React.createRef();
this.table = /*#__PURE__*/React.createRef();
this.stickyTableHeadingsRow = /*#__PURE__*/React.createRef();
this.tableHeadings = [];
this.stickyHeadings = [];
this.tableHeadingWidths = [];
this.stickyHeaderActive = false;
this.scrollStopTimer = null;
this.handleResize = debounce.debounce(() => {
const {
table: {
current: table
},
scrollContainer: {
current: scrollContainer
}
} = this;
let condensed = false;
if (table && scrollContainer) {
condensed = table.scrollWidth > scrollContainer.clientWidth;
}
this.setState({
condensed,
...this.calculateColumnVisibilityData(condensed)
});
});
this.setCellRef = ({
ref,
index,
inStickyHeader,
inFixedFirstColumn
}) => {
const {
hasFixedFirstColumn
} = this.props;
if (ref == null || hasFixedFirstColumn && !inFixedFirstColumn && index === 0) {
return;
}
if (inStickyHeader) {
this.stickyHeadings[index] = ref;
const button = ref.querySelector('button');
if (button == null) {
return;
}
button.addEventListener('focus', this.handleHeaderButtonFocus);
} else {
this.tableHeadings[index] = ref;
this.tableHeadingWidths[index] = ref.getBoundingClientRect().width;
}
};
this.changeHeadingFocus = () => {
const {
tableHeadings,
stickyHeadings
} = this;
const stickyFocusedItemIndex = stickyHeadings.findIndex(item => {
var _document$activeEleme;
return item === ((_document$activeEleme = document.activeElement) === null || _document$activeEleme === void 0 ? void 0 : _document$activeEleme.parentElement);
});
const tableFocusedItemIndex = tableHeadings.findIndex(item => {
var _document$activeEleme2;
return item === ((_document$activeEleme2 = document.activeElement) === null || _document$activeEleme2 === void 0 ? void 0 : _document$activeEleme2.parentElement);
});
if (stickyFocusedItemIndex < 0 && tableFocusedItemIndex < 0) {
return null;
}
let button;
if (stickyFocusedItemIndex >= 0) {
button = tableHeadings[stickyFocusedItemIndex].querySelector('button');
} else if (tableFocusedItemIndex >= 0) {
button = stickyHeadings[tableFocusedItemIndex].querySelector('button');
}
if (button == null) {
return null;
}
button.style.visibility = 'visible';
button.focus();
button.style.removeProperty('visibility');
};
this.calculateColumnVisibilityData = condensed => {
const {
table: {
current: table
},
scrollContainer: {
current: scrollContainer
},
dataTable: {
current: dataTable
}
} = this;
if (condensed && table && scrollContainer && dataTable) {
const headerCells = table.querySelectorAll(shared.headerCell.selector);
const {
hasFixedFirstColumn
} = this.props;
const firstColumnWidth = hasFixedFirstColumn ? headerCells[0].clientWidth : 0;
if (headerCells.length > 0) {
const firstVisibleColumnIndex = headerCells.length - 1;
const tableLeftVisibleEdge = scrollContainer.scrollLeft + firstColumnWidth;
const tableRightVisibleEdge = scrollContainer.scrollLeft + dataTable.offsetWidth;
const tableData = {
firstVisibleColumnIndex,
tableLeftVisibleEdge,
tableRightVisibleEdge
};
const columnVisibilityData = [...headerCells].map(utilities.measureColumn(tableData));
const lastColumn = columnVisibilityData[columnVisibilityData.length - 1];
const isScrolledFarthestLeft = hasFixedFirstColumn ? tableLeftVisibleEdge === firstColumnWidth : tableLeftVisibleEdge === 0;
return {
columnVisibilityData,
...utilities.getPrevAndCurrentColumns(tableData, columnVisibilityData),
isScrolledFarthestLeft,
isScrolledFarthestRight: lastColumn.rightEdge <= tableRightVisibleEdge
};
}
}
return {
columnVisibilityData: [],
previousColumn: undefined,
currentColumn: undefined
};
};
this.handleHeaderButtonFocus = event => {
if (this.scrollContainer.current == null || event.target == null) {
return;
}
const target = event.target;
const currentCell = target.parentNode;
const tableScrollLeft = this.scrollContainer.current.scrollLeft;
const tableViewableWidth = this.scrollContainer.current.offsetWidth;
const tableRightEdge = tableScrollLeft + tableViewableWidth;
const firstColumnWidth = this.state.columnVisibilityData[0].rightEdge;
const currentColumnLeftEdge = currentCell.offsetLeft;
const currentColumnRightEdge = currentCell.offsetLeft + currentCell.offsetWidth;
if (tableScrollLeft > currentColumnLeftEdge - firstColumnWidth) {
this.scrollContainer.current.scrollLeft = currentColumnLeftEdge - firstColumnWidth;
}
if (currentColumnRightEdge > tableRightEdge) {
this.scrollContainer.current.scrollLeft = currentColumnRightEdge - tableViewableWidth;
}
};
this.stickyHeaderScrolling = () => {
const {
current: stickyTableHeadingsRow
} = this.stickyTableHeadingsRow;
const {
current: scrollContainer
} = this.scrollContainer;
if (stickyTableHeadingsRow == null || scrollContainer == null) {
return;
}
stickyTableHeadingsRow.scrollLeft = scrollContainer.scrollLeft;
};
this.scrollListener = () => {
var _this$scrollContainer;
if (this.scrollStopTimer) {
clearTimeout(this.scrollStopTimer);
}
this.scrollStopTimer = setTimeout(() => {
this.setState(prevState => ({ ...this.calculateColumnVisibilityData(prevState.condensed)
}));
}, 100);
this.setState({
isScrolledFarthestLeft: ((_this$scrollContainer = this.scrollContainer.current) === null || _this$scrollContainer === void 0 ? void 0 : _this$scrollContainer.scrollLeft) === 0
});
if (this.props.stickyHeader && this.stickyHeaderActive) {
this.stickyHeaderScrolling();
}
};
this.handleHover = row => () => {
this.setState({
rowHovered: row
});
};
this.handleFocus = event => {
if (this.scrollContainer.current == null || event.target == null) {
return;
}
const currentCell = event.target.parentNode;
const firstColumnWidth = this.state.columnVisibilityData[0].rightEdge;
const currentColumnLeftEdge = currentCell.offsetLeft;
const desiredScrollLeft = currentColumnLeftEdge - firstColumnWidth;
if (this.scrollContainer.current.scrollLeft > desiredScrollLeft) {
this.scrollContainer.current.scrollLeft = desiredScrollLeft;
}
};
this.navigateTable = direction => {
var _this$state$columnVis;
const {
currentColumn,
previousColumn
} = this.state;
const firstColumnWidth = (_this$state$columnVis = this.state.columnVisibilityData[0]) === null || _this$state$columnVis === void 0 ? void 0 : _this$state$columnVis.rightEdge;
if (!currentColumn || !previousColumn) {
return;
}
let prevWidths = 0;
for (let index = 0; index < currentColumn.index; index++) {
prevWidths += this.state.columnVisibilityData[index].width;
}
const {
current: scrollContainer
} = this.scrollContainer;
const handleScroll = () => {
let newScrollLeft = 0;
if (this.props.hasFixedFirstColumn) {
newScrollLeft = direction === 'right' ? prevWidths - firstColumnWidth + currentColumn.width : prevWidths - previousColumn.width - firstColumnWidth;
} else {
newScrollLeft = direction === 'right' ? currentColumn.rightEdge : previousColumn.leftEdge;
}
if (scrollContainer) {
scrollContainer.scrollLeft = newScrollLeft;
requestAnimationFrame(() => {
this.setState(prevState => ({ ...this.calculateColumnVisibilityData(prevState.condensed)
}));
});
}
};
return handleScroll;
};
this.renderHeading = ({
heading,
headingIndex,
inFixedFirstColumn,
inStickyHeader
}) => {
const {
sortable,
truncate = false,
columnContentTypes,
defaultSortDirection,
initialSortColumnIndex = 0,
verticalAlign,
firstColumnMinWidth
} = this.props;
const {
sortDirection = defaultSortDirection,
sortedColumnIndex = initialSortColumnIndex,
isScrolledFarthestLeft
} = this.state;
let sortableHeadingProps;
const headingCellId = `heading-cell-${headingIndex}`;
const stickyHeaderId = `stickyheader-${headingIndex}`;
const id = inStickyHeader ? stickyHeaderId : headingCellId;
if (sortable) {
const isSortable = sortable[headingIndex];
const isSorted = isSortable && sortedColumnIndex === headingIndex;
const direction = isSorted ? sortDirection : 'none';
sortableHeadingProps = {
defaultSortDirection,
sorted: isSorted,
sortable: isSortable,
sortDirection: direction,
onSort: this.defaultOnSort(headingIndex),
hasFixedFirstColumn: this.props.hasFixedFirstColumn,
inFixedFirstColumn: this.props.hasFixedFirstColumn && inFixedFirstColumn
};
}
let stickyCellWidth;
if (inStickyHeader) {
stickyCellWidth = this.tableHeadingWidths[headingIndex];
}
return /*#__PURE__*/React__default["default"].createElement(Cell.Cell, Object.assign({
setRef: ref => {
this.setCellRef({
ref,
index: headingIndex,
inStickyHeader,
inFixedFirstColumn
});
},
header: true,
stickyHeadingCell: inStickyHeader,
key: id,
content: heading,
contentType: columnContentTypes[headingIndex],
firstColumn: headingIndex === 0,
truncate: truncate
}, sortableHeadingProps, {
verticalAlign: verticalAlign,
handleFocus: this.handleFocus,
stickyCellWidth: stickyCellWidth,
fixedCellVisible: !isScrolledFarthestLeft,
firstColumnMinWidth: firstColumnMinWidth,
inFixedFirstColumn: inFixedFirstColumn
}));
};
this.totalsRowHeading = () => {
const {
i18n,
totals,
totalsName
} = this.props;
const totalsLabel = totalsName ? totalsName : {
singular: i18n.translate('Polaris.DataTable.totalRowHeading'),
plural: i18n.translate('Polaris.DataTable.totalsRowHeading')
};
return totals && totals.filter(total => total !== '').length > 1 ? totalsLabel.plural : totalsLabel.singular;
};
this.renderTotals = (total, index) => {
const id = `totals-cell-${index}`;
const {
truncate = false,
verticalAlign
} = this.props;
let content;
let contentType;
if (index === 0) {
content = this.totalsRowHeading();
}
if (total !== '' && index > 0) {
contentType = 'numeric';
content = total;
}
const totalInFooter = this.props.showTotalsInFooter;
return /*#__PURE__*/React__default["default"].createElement(Cell.Cell, {
total: true,
totalInFooter: totalInFooter,
firstColumn: index === 0,
key: id,
content: content,
contentType: contentType,
truncate: truncate,
verticalAlign: verticalAlign
});
};
this.getColSpan = (rowLength, headingsLength, contentTypesLength, cellIndex) => {
const rowLen = rowLength ? rowLength : 1;
const colLen = headingsLength ? headingsLength : contentTypesLength;
const colSpan = Math.floor(colLen / rowLen);
const remainder = colLen % rowLen;
return cellIndex === 0 ? colSpan + remainder : colSpan;
};
this.defaultRenderRow = ({
row,
index,
inFixedFirstColumn
}) => {
const {
columnContentTypes,
truncate = false,
verticalAlign,
hoverable = true,
headings
} = this.props;
const className = css.classNames(DataTable$1["default"].TableRow, hoverable && DataTable$1["default"].hoverable);
return /*#__PURE__*/React__default["default"].createElement("tr", {
key: `row-${index}`,
className: className,
onMouseEnter: this.handleHover(index),
onMouseLeave: this.handleHover()
}, row.map((content, cellIndex) => {
const hovered = index === this.state.rowHovered;
const id = `cell-${cellIndex}-row-${index}`;
const colSpan = this.getColSpan(row.length, headings.length, columnContentTypes.length, cellIndex);
return /*#__PURE__*/React__default["default"].createElement(Cell.Cell, {
key: id,
content: content,
contentType: columnContentTypes[cellIndex],
firstColumn: cellIndex === 0,
truncate: truncate,
verticalAlign: verticalAlign,
colSpan: colSpan,
hovered: hovered,
inFixedFirstColumn: inFixedFirstColumn
});
}));
};
this.defaultOnSort = headingIndex => {
const {
onSort,
defaultSortDirection = 'ascending',
initialSortColumnIndex
} = this.props;
const {
sortDirection = defaultSortDirection,
sortedColumnIndex = initialSortColumnIndex
} = this.state;
let newSortDirection = defaultSortDirection;
if (sortedColumnIndex === headingIndex) {
newSortDirection = sortDirection === 'ascending' ? 'descending' : 'ascending';
}
const handleSort = () => {
this.setState({
sortDirection: newSortDirection,
sortedColumnIndex: headingIndex
}, () => {
if (onSort) {
onSort(headingIndex, newSortDirection);
}
});
};
return handleSort;
};
}
componentDidMount() {
// We need to defer the calculation in development so the styles have time to be injected.
if (process.env.NODE_ENV === 'development') {
setTimeout(() => {
this.handleResize();
}, 10);
} else {
this.handleResize();
}
}
componentDidUpdate(prevProps) {
if (isEqual__default["default"](prevProps, this.props)) {
return;
}
this.handleResize();
}
componentWillUnmount() {
this.handleResize.cancel();
}
render() {
const {
headings,
totals,
showTotalsInFooter,
rows,
footerContent,
hideScrollIndicator = false,
increasedTableDensity = false,
hasZebraStripingOnData = false,
stickyHeader = false,
hasFixedFirstColumn = false
} = this.props;
const {
condensed,
columnVisibilityData,
isScrolledFarthestLeft,
isScrolledFarthestRight
} = this.state;
const rowCountIsEven = rows.length % 2 === 0;
const className = css.classNames(DataTable$1["default"].DataTable, condensed && DataTable$1["default"].condensed, totals && DataTable$1["default"].ShowTotals, showTotalsInFooter && DataTable$1["default"].ShowTotalsInFooter, hasZebraStripingOnData && DataTable$1["default"].ZebraStripingOnData, hasZebraStripingOnData && rowCountIsEven && DataTable$1["default"].RowCountIsEven);
const wrapperClassName = css.classNames(DataTable$1["default"].TableWrapper, condensed && DataTable$1["default"].condensed, increasedTableDensity && DataTable$1["default"].IncreasedTableDensity, stickyHeader && DataTable$1["default"].StickyHeaderEnabled);
const headingMarkup = /*#__PURE__*/React__default["default"].createElement("tr", null, headings.map((heading, index) => this.renderHeading({
heading,
headingIndex: index,
inFixedFirstColumn: false,
inStickyHeader: false
})));
const totalsMarkup = totals ? /*#__PURE__*/React__default["default"].createElement("tr", null, totals.map(this.renderTotals)) : null;
const firstColumn = rows.map(row => row.slice(0, 1));
const firstHeading = headings.slice(0, 1);
const firstTotal = totals === null || totals === void 0 ? void 0 : totals.slice(0, 1);
const fixedFirstColumn = condensed && hasFixedFirstColumn && /*#__PURE__*/React__default["default"].createElement("table", {
className: css.classNames(DataTable$1["default"].FixedFirstColumn, !isScrolledFarthestLeft && DataTable$1["default"].separate),
style: {
maxWidth: `${columnVisibilityData[0].rightEdge}px`
}
}, /*#__PURE__*/React__default["default"].createElement("thead", null, /*#__PURE__*/React__default["default"].createElement("tr", null, firstHeading.map((heading, index) => this.renderHeading({
heading,
headingIndex: index,
inFixedFirstColumn: true,
inStickyHeader: false
}))), totals && !showTotalsInFooter && /*#__PURE__*/React__default["default"].createElement("tr", null, firstTotal === null || firstTotal === void 0 ? void 0 : firstTotal.map(this.renderTotals))), /*#__PURE__*/React__default["default"].createElement("tbody", null, firstColumn.map((row, index) => this.defaultRenderRow({
row,
index,
inFixedFirstColumn: true
}))), totals && showTotalsInFooter && /*#__PURE__*/React__default["default"].createElement("tfoot", null, /*#__PURE__*/React__default["default"].createElement("tr", null, firstTotal === null || firstTotal === void 0 ? void 0 : firstTotal.map(this.renderTotals))));
const bodyMarkup = rows.map((row, index) => this.defaultRenderRow({
row,
index,
inFixedFirstColumn: false
}));
const footerMarkup = footerContent ? /*#__PURE__*/React__default["default"].createElement("div", {
className: DataTable$1["default"].Footer
}, footerContent) : null;
const headerTotalsMarkup = !showTotalsInFooter ? totalsMarkup : null;
const footerTotalsMarkup = showTotalsInFooter ? /*#__PURE__*/React__default["default"].createElement("tfoot", null, totalsMarkup) : null;
const navigationMarkup = hideScrollIndicator ? null : /*#__PURE__*/React__default["default"].createElement(Navigation.Navigation, {
columnVisibilityData: columnVisibilityData,
isScrolledFarthestLeft: isScrolledFarthestLeft,
isScrolledFarthestRight: isScrolledFarthestRight,
navigateTableLeft: this.navigateTable('left'),
navigateTableRight: this.navigateTable('right'),
fixedFirstColumn: hasFixedFirstColumn
});
const stickyHeaderMarkup = stickyHeader ? /*#__PURE__*/React__default["default"].createElement(AfterInitialMount.AfterInitialMount, null, /*#__PURE__*/React__default["default"].createElement("div", {
className: DataTable$1["default"].StickyTable,
role: "presentation"
}, /*#__PURE__*/React__default["default"].createElement(Sticky.Sticky, {
boundingElement: this.dataTable.current,
onStickyChange: isSticky => {
this.changeHeadingFocus();
this.stickyHeaderActive = isSticky;
}
}, isSticky => {
const stickyHeaderClassNames = css.classNames(DataTable$1["default"].StickyTableHeader, isSticky && DataTable$1["default"]['StickyTableHeader-isSticky'], !isScrolledFarthestLeft && DataTable$1["default"].separate);
const fixedFirstStickyHeading = hasFixedFirstColumn ? /*#__PURE__*/React__default["default"].createElement("table", {
className: css.classNames(!isScrolledFarthestLeft && DataTable$1["default"].separate, DataTable$1["default"].FixedFirstColumn)
}, /*#__PURE__*/React__default["default"].createElement("thead", null, /*#__PURE__*/React__default["default"].createElement("tr", null, this.renderHeading({
heading: headings[0],
headingIndex: 0,
inFixedFirstColumn: true,
inStickyHeader: true
})))) : null;
return /*#__PURE__*/React__default["default"].createElement("table", {
className: stickyHeaderClassNames
}, /*#__PURE__*/React__default["default"].createElement("div", null, navigationMarkup), /*#__PURE__*/React__default["default"].createElement("tr", {
className: DataTable$1["default"].StickyTableHeadingsRow,
ref: this.stickyTableHeadingsRow
}, fixedFirstStickyHeading, headings.map((heading, index) => {
return this.renderHeading({
heading,
headingIndex: index,
inFixedFirstColumn: false,
inStickyHeader: true
});
})));
}))) : null;
return /*#__PURE__*/React__default["default"].createElement("div", {
className: wrapperClassName
}, navigationMarkup, /*#__PURE__*/React__default["default"].createElement("div", {
className: className,
ref: this.dataTable
}, stickyHeaderMarkup, /*#__PURE__*/React__default["default"].createElement("div", {
className: DataTable$1["default"].ScrollContainer,
ref: this.scrollContainer
}, /*#__PURE__*/React__default["default"].createElement(EventListener.EventListener, {
event: "resize",
handler: this.handleResize
}), /*#__PURE__*/React__default["default"].createElement(EventListener.EventListener, {
capture: true,
passive: true,
event: "scroll",
handler: this.scrollListener
}), fixedFirstColumn, /*#__PURE__*/React__default["default"].createElement("table", {
className: DataTable$1["default"].Table,
ref: this.table
}, /*#__PURE__*/React__default["default"].createElement("thead", null, headingMarkup, headerTotalsMarkup), /*#__PURE__*/React__default["default"].createElement("tbody", null, bodyMarkup), footerTotalsMarkup)), footerMarkup));
}
}
function DataTable(props) {
const i18n = hooks.useI18n();
return /*#__PURE__*/React__default["default"].createElement(DataTableInner, Object.assign({}, props, {
i18n: i18n
}));
}
exports.DataTable = DataTable;