UNPKG

@shopify/polaris

Version:

Shopify’s product component library

193 lines (192 loc) • 9.81 kB
import React from 'react'; import debounce from 'lodash/debounce'; import isEqual from 'lodash/isEqual'; import { classNames } from '../../utilities/css'; import { headerCell } from '../shared'; import { withAppProvider, } from '../../utilities/with-app-provider'; import { EventListener } from '../EventListener'; import { Cell, Navigation } from './components'; import { measureColumn, getPrevAndCurrentColumns } from './utilities'; import styles from './DataTable.scss'; class DataTable extends React.PureComponent { constructor(props) { super(props); this.state = { condensed: false, columnVisibilityData: [], isScrolledFarthestLeft: true, isScrolledFarthestRight: false, }; this.dataTable = React.createRef(); this.scrollContainer = React.createRef(); this.table = React.createRef(); this.handleResize = debounce(() => { const { table: { current: table }, scrollContainer: { current: scrollContainer }, } = this; let condensed = false; if (table && scrollContainer) { condensed = table.scrollWidth > scrollContainer.clientWidth; } this.setState(Object.assign({ condensed }, this.calculateColumnVisibilityData(condensed))); }); this.calculateColumnVisibilityData = (condensed) => { const { table: { current: table }, scrollContainer: { current: scrollContainer }, dataTable: { current: dataTable }, } = this; if (condensed && table && scrollContainer && dataTable) { const headerCells = table.querySelectorAll(headerCell.selector); const firstVisibleColumnIndex = headerCells.length - 1; const tableLeftVisibleEdge = scrollContainer.scrollLeft; const tableRightVisibleEdge = scrollContainer.scrollLeft + dataTable.offsetWidth; const tableData = { firstVisibleColumnIndex, tableLeftVisibleEdge, tableRightVisibleEdge, }; const columnVisibilityData = [...headerCells].map(measureColumn(tableData)); const lastColumn = columnVisibilityData[columnVisibilityData.length - 1]; return Object.assign({ columnVisibilityData }, getPrevAndCurrentColumns(tableData, columnVisibilityData), { isScrolledFarthestLeft: tableLeftVisibleEdge === 0, isScrolledFarthestRight: lastColumn.rightEdge <= tableRightVisibleEdge }); } return { columnVisibilityData: [], previousColumn: undefined, currentColumn: undefined, }; }; this.scrollListener = () => { this.setState((prevState) => (Object.assign({}, this.calculateColumnVisibilityData(prevState.condensed)))); }; this.navigateTable = (direction) => { const { currentColumn, previousColumn } = this.state; const { current: scrollContainer } = this.scrollContainer; const handleScroll = () => { if (!currentColumn || !previousColumn) { return; } if (scrollContainer) { scrollContainer.scrollLeft = direction === 'right' ? currentColumn.rightEdge : previousColumn.leftEdge; requestAnimationFrame(() => { this.setState((prevState) => (Object.assign({}, this.calculateColumnVisibilityData(prevState.condensed)))); }); } }; return handleScroll; }; this.renderHeadings = (heading, headingIndex) => { const { sortable, truncate = false, columnContentTypes, defaultSortDirection, initialSortColumnIndex = 0, verticalAlign, } = this.props; const { sortDirection = defaultSortDirection, sortedColumnIndex = initialSortColumnIndex, } = this.state; let sortableHeadingProps; const id = `heading-cell-${headingIndex}`; 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), }; } return (<Cell header key={id} content={heading} contentType={columnContentTypes[headingIndex]} firstColumn={headingIndex === 0} truncate={truncate} {...sortableHeadingProps} verticalAlign={verticalAlign}/>); }; 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 (<Cell total totalInFooter={totalInFooter} firstColumn={index === 0} key={id} content={content} contentType={contentType} truncate={truncate} verticalAlign={verticalAlign}/>); }; this.defaultRenderRow = (row, index) => { const className = classNames(styles.TableRow); const { columnContentTypes, truncate = false, verticalAlign } = this.props; return (<tr key={`row-${index}`} className={className}> {row.map((content, cellIndex) => { const id = `cell-${cellIndex}-row-${index}`; return (<Cell key={id} content={content} contentType={columnContentTypes[cellIndex]} firstColumn={cellIndex === 0} truncate={truncate} verticalAlign={verticalAlign}/>); })} </tr>); }; 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; }; const { polaris: { intl }, } = props; this.totalsRowHeading = intl.translate('Polaris.DataTable.totalsRowHeading'); } 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(prevProps, this.props)) { return; } this.handleResize(); } render() { const { headings, totals, showTotalsInFooter, rows, footerContent, } = this.props; const { condensed, columnVisibilityData, isScrolledFarthestLeft, isScrolledFarthestRight, } = this.state; const className = classNames(styles.DataTable, condensed && styles.condensed); const wrapperClassName = classNames(styles.TableWrapper, condensed && styles.condensed); const headingMarkup = <tr>{headings.map(this.renderHeadings)}</tr>; const totalsMarkup = totals ? (<tr>{totals.map(this.renderTotals)}</tr>) : null; const bodyMarkup = rows.map(this.defaultRenderRow); const footerMarkup = footerContent ? (<div className={styles.Footer}>{footerContent}</div>) : null; const headerTotalsMarkup = !showTotalsInFooter ? totalsMarkup : null; const footerTotalsMarkup = showTotalsInFooter ? (<tfoot>{totalsMarkup}</tfoot>) : null; return (<div className={wrapperClassName}> <Navigation columnVisibilityData={columnVisibilityData} isScrolledFarthestLeft={isScrolledFarthestLeft} isScrolledFarthestRight={isScrolledFarthestRight} navigateTableLeft={this.navigateTable('left')} navigateTableRight={this.navigateTable('right')}/> <div className={className} ref={this.dataTable}> <div className={styles.ScrollContainer} ref={this.scrollContainer}> <EventListener event="resize" handler={this.handleResize}/> <EventListener capture event="scroll" handler={this.scrollListener}/> <table className={styles.Table} ref={this.table}> <thead> {headingMarkup} {headerTotalsMarkup} </thead> <tbody>{bodyMarkup}</tbody> {footerTotalsMarkup} </table> </div> {footerMarkup} </div> </div>); } } // Use named export once withAppProvider is refactored away // eslint-disable-next-line import/no-default-export export default withAppProvider()(DataTable);