UNPKG

@shopify/polaris

Version:

Shopify’s admin product component library

645 lines (571 loc) • 23.7 kB
'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;