diff --git a/UNRELEASED.md b/UNRELEASED.md index ff05efd7f83..142666c2aa8 100644 --- a/UNRELEASED.md +++ b/UNRELEASED.md @@ -27,6 +27,7 @@ Use [the changelog guidelines](https://git.io/polaris-changelog-guidelines) to f ### Bug fixes - Fixes `monochrome` variant of `Link` and `Button` components to support multi-line link text ([#1686](https://github.com/Shopify/polaris-react/pull/1686)) +- Fixed the first column of `DataTable` not rendering in iOS Safari (([#1605](https://github.com/Shopify/polaris-react/pull/1605)) ### Documentation diff --git a/src/components/DataTable/DataTable.scss b/src/components/DataTable/DataTable.scss index 51150e7dd16..bae2c9001ac 100644 --- a/src/components/DataTable/DataTable.scss +++ b/src/components/DataTable/DataTable.scss @@ -1,4 +1,4 @@ -$fixed-column-width: rem(145px); +$first-column-width: rem(145px); $breakpoint: 768px; .DataTable { @@ -6,13 +6,7 @@ $breakpoint: 768px; max-width: 100vw; } -.collapsed { - .Table { - &::after { - display: block; - } - } - +.condensed { .Navigation { display: flex; align-items: center; @@ -24,16 +18,6 @@ $breakpoint: 768px; justify-content: flex-end; } } - - .ScrollContainer { - margin-left: rem($fixed-column-width); - } -} - -.hasFooter { - .ScrollContainer { - margin-bottom: rem(52px); - } } .Navigation { @@ -57,29 +41,12 @@ $breakpoint: 768px; .ScrollContainer { overflow-x: auto; - // account for a mysterious gap in Safari when not collapsed - margin-left: rem(140px); -webkit-overflow-scrolling: touch; } .Table { width: 100%; border-spacing: 0; - - &::after { - content: ''; - position: absolute; - top: 0; - bottom: 0; - left: $fixed-column-width; - display: none; - width: rem(6px); - background: linear-gradient( - to right, - rgba(color('black'), 0.12), - rgba(color('black'), 0) - ); - } } .TableRow { @@ -90,10 +57,6 @@ $breakpoint: 768px; } } -.TableFoot { - border-bottom: 0; -} - .Cell { padding: spacing(); border-bottom: border-width() solid color('sky', 'light'); @@ -103,26 +66,21 @@ $breakpoint: 768px; vertical-align: top; } -.Cell-numeric { - text-align: right; -} - -.Cell-fixed { +.Cell-firstColumn { @include text-emphasis-normal; - @include text-breakword; - position: absolute; - top: auto; - left: 0; - width: $fixed-column-width; - white-space: unset; text-align: left; - backface-visibility: hidden; // stops painting on scroll (due to positioning) + white-space: normal; +} + +.Cell-numeric { + text-align: right; } .Cell-truncated { white-space: nowrap; overflow-x: hidden; text-overflow: ellipsis; + max-width: $first-column-width; } .Cell-header { @@ -180,16 +138,9 @@ $breakpoint: 768px; border-bottom: border(); } -.Cell-footer { - @include text-emphasis-normal; - position: absolute; - top: 100%; - left: 0; - width: 100%; - border-bottom: 0; +.Footer { + padding: spacing(); background: color('sky', 'light'); color: color('ink', 'lighter'); - white-space: unset; text-align: center; - backface-visibility: hidden; // stop painting on scroll (due to positioning) } diff --git a/src/components/DataTable/DataTable.tsx b/src/components/DataTable/DataTable.tsx index acdfbae1f82..ee286e7c448 100644 --- a/src/components/DataTable/DataTable.tsx +++ b/src/components/DataTable/DataTable.tsx @@ -51,10 +51,8 @@ export interface Props { class DataTable extends React.PureComponent { state: DataTableState = { - collapsed: false, + condensed: false, columnVisibilityData: [], - heights: [], - preservedScrollPosition: {}, isScrolledFarthestLeft: true, isScrolledFarthestRight: false, }; @@ -65,28 +63,21 @@ class DataTable extends React.PureComponent { private totalsRowHeading: string; private handleResize = debounce(() => { - const {footerContent, truncate} = this.props; const { table: {current: table}, scrollContainer: {current: scrollContainer}, } = this; - let collapsed = false; + + let condensed = false; + if (table && scrollContainer) { - collapsed = table.scrollWidth > scrollContainer.clientWidth; - scrollContainer.scrollLeft = 0; + condensed = table.scrollWidth > scrollContainer.clientWidth; } - this.setState( - { - collapsed, - heights: [], - ...this.calculateColumnVisibilityData(collapsed), - }, - () => { - if (footerContent || !truncate) { - this.setHeightsAndScrollPosition(); - } - }, - ); + + this.setState({ + condensed, + ...this.calculateColumnVisibilityData(condensed), + }); }); constructor(props: CombinedProps) { @@ -114,94 +105,35 @@ class DataTable extends React.PureComponent { } render() { + const {headings, totals, rows, footerContent} = this.props; const { - columnContentTypes, - headings, - totals, - rows, - truncate, - footerContent, - sortable, - defaultSortDirection = 'ascending', - initialSortColumnIndex = 0, - } = this.props; - - const { - collapsed, + condensed, columnVisibilityData, - heights, - sortedColumnIndex = initialSortColumnIndex, - sortDirection = defaultSortDirection, isScrolledFarthestLeft, isScrolledFarthestRight, } = this.state; const className = classNames( styles.DataTable, - collapsed && styles.collapsed, - footerContent && styles.hasFooter, + condensed && styles.condensed, ); const wrapperClassName = classNames( styles.TableWrapper, - collapsed && styles.collapsed, + condensed && styles.condensed, ); - const footerClassName = classNames(footerContent && styles.TableFoot); - - const footerMarkup = footerContent ? ( - - {this.renderFooter()} - - ) : null; + const headingMarkup = {headings.map(this.renderHeadings)}; const totalsMarkup = totals ? ( {totals.map(this.renderTotals)} ) : null; - const headingMarkup = ( - - {headings.map((heading, headingIndex) => { - let sortableHeadingProps; - const id = `heading-cell-${headingIndex}`; - - if (sortable) { - const isSortable = sortable[headingIndex]; - const isSorted = sortedColumnIndex === headingIndex; - const direction = isSorted ? sortDirection : 'none'; - - sortableHeadingProps = { - defaultSortDirection, - sorted: isSorted, - sortable: isSortable, - sortDirection: direction, - onSort: this.defaultOnSort(headingIndex), - }; - } - - const height = !truncate ? heights[0] : undefined; - - return ( - - ); - })} - - ); - const bodyMarkup = rows.map(this.defaultRenderRow); - const style = footerContent - ? {marginBottom: `${heights[heights.length - 1]}px`} - : undefined; + + const footerMarkup = footerContent ? ( +
{footerContent}
+ ) : null; return (
@@ -213,11 +145,7 @@ class DataTable extends React.PureComponent { navigateTableRight={this.navigateTable('right')} />
-
+
{ {totalsMarkup} {bodyMarkup} - {footerMarkup}
+ {footerMarkup}
); } - private tallestCellHeights = () => { - const {footerContent, truncate} = this.props; - const { - table: {current: table}, - } = this; - let {heights} = this.state; - if (table) { - const rows = Array.from(table.getElementsByTagName('tr')); - - if (!truncate) { - return (heights = rows.map((row) => { - const fixedCell = (row.childNodes as NodeListOf)[0]; - return Math.max(row.clientHeight, fixedCell.clientHeight); - })); - } - - if (footerContent) { - const footerCellHeight = (rows[rows.length - 1] - .childNodes as NodeListOf)[0].clientHeight; - heights = [footerCellHeight]; - } - } - - return heights; - }; - - private resetScrollPosition = () => { - const { - scrollContainer: {current: scrollContainer}, - } = this; - if (scrollContainer) { - const { - preservedScrollPosition: {left, top}, - } = this.state; - if (left) { - scrollContainer.scrollLeft = left; - } - if (top) { - window.scrollTo(0, top); - } - } - }; - - private setHeightsAndScrollPosition = () => { - this.setState( - {heights: this.tallestCellHeights()}, - this.resetScrollPosition, - ); - }; - - private calculateColumnVisibilityData = (collapsed: boolean) => { + private calculateColumnVisibilityData = (condensed: boolean) => { const { table: {current: table}, scrollContainer: {current: scrollContainer}, dataTable: {current: dataTable}, } = this; - if (collapsed && table && scrollContainer && dataTable) { + + if (condensed && table && scrollContainer && dataTable) { const headerCells = table.querySelectorAll( headerCell.selector, ) as NodeListOf; - const collapsedHeaderCells = Array.from(headerCells).slice(1); - const fixedColumnWidth = headerCells[0].offsetWidth; - const firstVisibleColumnIndex = collapsedHeaderCells.length - 1; - const tableLeftVisibleEdge = - scrollContainer.scrollLeft + fixedColumnWidth; + + const firstVisibleColumnIndex = headerCells.length - 1; + const tableLeftVisibleEdge = scrollContainer.scrollLeft; + const tableRightVisibleEdge = scrollContainer.scrollLeft + dataTable.offsetWidth; + const tableData = { - fixedColumnWidth, firstVisibleColumnIndex, tableLeftVisibleEdge, tableRightVisibleEdge, }; - const columnVisibilityData = collapsedHeaderCells.map( + const columnVisibilityData = [...headerCells].map( measureColumn(tableData), ); const lastColumn = columnVisibilityData[columnVisibilityData.length - 1]; return { - fixedColumnWidth, columnVisibilityData, ...getPrevAndCurrentColumns(tableData, columnVisibilityData), - isScrolledFarthestLeft: tableLeftVisibleEdge === fixedColumnWidth, + isScrolledFarthestLeft: tableLeftVisibleEdge === 0, isScrolledFarthestRight: lastColumn.rightEdge <= tableRightVisibleEdge, }; } @@ -336,30 +213,28 @@ class DataTable extends React.PureComponent { private scrollListener = () => { this.setState((prevState) => ({ - ...this.calculateColumnVisibilityData(prevState.collapsed), + ...this.calculateColumnVisibilityData(prevState.condensed), })); }; private navigateTable = (direction: string) => { - const {currentColumn, previousColumn, fixedColumnWidth} = this.state; - const { - scrollContainer: {current: scrollContainer}, - } = this; + const {currentColumn, previousColumn} = this.state; + const {current: scrollContainer} = this.scrollContainer; const handleScroll = () => { - if (!currentColumn || !previousColumn || !fixedColumnWidth) { + if (!currentColumn || !previousColumn) { return; } if (scrollContainer) { scrollContainer.scrollLeft = direction === 'right' - ? currentColumn.rightEdge - fixedColumnWidth - : previousColumn.leftEdge - fixedColumnWidth; + ? currentColumn.rightEdge + : previousColumn.leftEdge; requestAnimationFrame(() => { this.setState((prevState) => ({ - ...this.calculateColumnVisibilityData(prevState.collapsed), + ...this.calculateColumnVisibilityData(prevState.condensed), })); }); } @@ -368,9 +243,52 @@ class DataTable extends React.PureComponent { return handleScroll; }; + private renderHeadings = (heading: string, headingIndex: number) => { + const { + sortable, + truncate = false, + columnContentTypes, + defaultSortDirection, + initialSortColumnIndex = 0, + } = 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 ( + + ); + }; + private renderTotals = (total: TableData, index: number) => { const id = `totals-cell-${index}`; - const {heights} = this.state; const {truncate = false} = this.props; let content; @@ -388,10 +306,8 @@ class DataTable extends React.PureComponent { return ( { private defaultRenderRow = (row: TableData[], index: number) => { const className = classNames(styles.TableRow); - const { - columnContentTypes, - totals, - footerContent, - truncate = false, - } = this.props; - const {heights} = this.state; - const bodyCellHeights = totals ? heights.slice(2) : heights.slice(1); - - if (footerContent) { - bodyCellHeights.pop(); - } + const {columnContentTypes, truncate = false} = this.props; return ( @@ -422,11 +327,9 @@ class DataTable extends React.PureComponent { return ( ); @@ -435,25 +338,9 @@ class DataTable extends React.PureComponent { ); }; - private renderFooter = () => { - const {heights} = this.state; - const footerCellHeight = heights[heights.length - 1]; - - return ( - - ); - }; - private defaultOnSort = (headingIndex: number) => { const { onSort, - truncate, defaultSortDirection = 'ascending', initialSortColumnIndex, } = this.props; @@ -479,16 +366,6 @@ class DataTable extends React.PureComponent { () => { if (onSort) { onSort(headingIndex, newSortDirection); - - if (!truncate && this.scrollContainer.current) { - const preservedScrollPosition = { - left: this.scrollContainer.current.scrollLeft, - top: window.scrollY, - }; - - this.setState({preservedScrollPosition}); - this.handleResize(); - } } }, ); diff --git a/src/components/DataTable/components/Cell/Cell.tsx b/src/components/DataTable/components/Cell/Cell.tsx index 035b7b27c64..9b1ce7a5171 100644 --- a/src/components/DataTable/components/Cell/Cell.tsx +++ b/src/components/DataTable/components/Cell/Cell.tsx @@ -10,15 +10,12 @@ import {SortDirection} from '../../types'; import styles from '../../DataTable.scss'; export interface Props { - testID?: string; - height?: number; content?: React.ReactNode; contentType?: string; - fixed?: boolean; + firstColumn?: boolean; truncate?: boolean; header?: boolean; total?: boolean; - footer?: boolean; sorted?: boolean; sortable?: boolean; sortDirection?: SortDirection; @@ -29,32 +26,28 @@ export interface Props { type CombinedProps = Props & WithAppProviderProps; function Cell({ - height, content, contentType, - fixed, + firstColumn, truncate, header, total, - footer, sorted, sortable, sortDirection, - defaultSortDirection, + defaultSortDirection = 'ascending', polaris: { intl: {translate}, }, onSort, }: CombinedProps) { const numeric = contentType === 'numeric'; - const className = classNames( styles.Cell, - fixed && styles['Cell-fixed'], - fixed && truncate && styles['Cell-truncated'], + firstColumn && styles['Cell-firstColumn'], + firstColumn && truncate && styles['Cell-truncated'], header && styles['Cell-header'], total && styles['Cell-total'], - footer && styles['Cell-footer'], numeric && styles['Cell-numeric'], sortable && styles['Cell-sortable'], sorted && styles['Cell-sorted'], @@ -66,13 +59,8 @@ function Cell({ ); const iconClassName = classNames(sortable && styles.Icon); - - const style = { - height: height ? `${height}px` : undefined, - }; - const direction = sorted ? sortDirection : defaultSortDirection; - const source = direction === 'ascending' ? CaretUpMinor : CaretDownMinor; + const source = direction === 'descending' ? CaretDownMinor : CaretUpMinor; const oppositeDirection = sortDirection === 'ascending' ? 'descending' : 'ascending'; @@ -102,23 +90,20 @@ function Cell({ className={className} scope="col" aria-sort={sortDirection} - style={style} > {columnHeadingContent} ) : ( - + {content} ); const cellMarkup = - header || fixed ? ( + header || firstColumn ? ( headingMarkup ) : ( - - {content} - + {content} ); return cellMarkup; diff --git a/src/components/DataTable/components/Cell/tests/Cell.test.tsx b/src/components/DataTable/components/Cell/tests/Cell.test.tsx new file mode 100644 index 00000000000..1a7e49cb541 --- /dev/null +++ b/src/components/DataTable/components/Cell/tests/Cell.test.tsx @@ -0,0 +1,263 @@ +import * as React from 'react'; +import {CaretUpMinor, CaretDownMinor} from '@shopify/polaris-icons'; +import { + mountWithAppProvider, + shallowWithAppProvider, + trigger, +} from 'test-utilities'; + +import {Icon} from '../../../..'; +import Cell from '../Cell'; + +describe('', () => { + describe('content', () => { + it('sets text content when provided', () => { + const cellContent = 'Data'; + const cell = shallowWithAppProvider(); + + expect(cell.text()).toBe(cellContent); + }); + + it('sets markup content when provided', () => { + const cellContent = 'Data'; + const cellMarkup =

{cellContent}

; + const cell = shallowWithAppProvider(); + + expect(cell.find('p')).toHaveLength(1); + expect(cell.text()).toBe(cellContent); + }); + }); + + describe('firstColumn', () => { + it('renders a table heading element when true', () => { + const cell = shallowWithAppProvider(); + + expect(cell.find('th')).toHaveLength(1); + }); + }); + + describe('header', () => { + it('renders a table heading element when true', () => { + const cell = shallowWithAppProvider(); + + expect(cell.find('th')).toHaveLength(1); + }); + }); + + describe('sorted', () => { + it('sets the aria-sort attribute to the sortDirection when the table is currently sorted by that column', () => { + const sortDirection = 'ascending'; + const cell = shallowWithAppProvider( + , + ); + + expect(cell.prop('aria-sort')).toBe(sortDirection); + }); + + it('sets the aria-sort attribute to none when the table is not currently sorted by that column', () => { + const sortDirection = 'none'; + const cell = shallowWithAppProvider( + , + ); + + expect(cell.prop('aria-sort')).toBe('none'); + }); + }); + + describe('sortable', () => { + it('renders an Icon when table is sortable by that column', () => { + const cell = shallowWithAppProvider(); + + expect(cell.find(Icon)).toHaveLength(1); + }); + + it('renders no Icon when table is not sortable by that column', () => { + const cell = shallowWithAppProvider( + , + ); + + expect(cell.find(Icon)).not.toHaveLength(1); + }); + }); + + describe('sortDirection', () => { + describe('when set to none', () => { + it('renders a down caret Icon when defaultSortDirection is descending', () => { + const cell = shallowWithAppProvider( + , + ); + + expect(cell.find(Icon).prop('source')).toBe(CaretDownMinor); + }); + + it('renders an up caret Icon when defaultSortDirection is ascending', () => { + const cell = shallowWithAppProvider( + , + ); + + expect(cell.find(Icon).prop('source')).toBe(CaretUpMinor); + }); + }); + + describe('when set to ascending', () => { + it('renders an up caret Icon when table is currently sorted by that column', () => { + const cell = shallowWithAppProvider( + , + ); + + expect(cell.find(Icon).prop('source')).toBe(CaretUpMinor); + }); + + it('renders an Icon with an accessibility label indicating the next sort direction is descending', () => { + const cell = mountWithAppProvider( + , + ); + + const expectedLabel = cell + .instance() + .context.polaris.intl.translate( + 'Polaris.DataTable.sortAccessibilityLabel', + { + direction: 'descending', + }, + ); + + expect(cell.find(Icon).prop('accessibilityLabel')).toBe(expectedLabel); + }); + }); + + describe('when set to descending', () => { + it('renders a down caret Icon when table is currently sorted by that column', () => { + const cell = shallowWithAppProvider( + , + ); + + expect(cell.find(Icon).prop('source')).toBe(CaretDownMinor); + }); + + it('renders an Icon with an accessibility label indicating the next sort direction is ascending', () => { + const cell = mountWithAppProvider( + , + ); + + const expectedLabel = cell + .instance() + .context.polaris.intl.translate( + 'Polaris.DataTable.sortAccessibilityLabel', + { + direction: 'ascending', + }, + ); + + expect(cell.find(Icon).prop('accessibilityLabel')).toBe(expectedLabel); + }); + }); + }); + + describe('defaultSortDirection', () => { + describe('when set to none', () => { + it('renders an up caret Icon when table is not currently sorted by that column', () => { + const cell = shallowWithAppProvider( + , + ); + + expect(cell.find(Icon).prop('source')).toBe(CaretUpMinor); + }); + }); + describe('when set to ascending', () => { + it('renders an up caret Icon when table is not currently sorted by that column', () => { + const cell = shallowWithAppProvider( + , + ); + + expect(cell.find(Icon).prop('source')).toBe(CaretUpMinor); + }); + }); + + describe('when set to descending', () => { + it('renders a down caret Icon when table is not currently sorted by that column', () => { + const cell = shallowWithAppProvider( + , + ); + + expect(cell.find(Icon).prop('source')).toBe(CaretDownMinor); + }); + }); + }); + + describe('onSort', () => { + it('gets called when a sortable cell heading is clicked', () => { + const sortSpy = jest.fn(); + const cell = shallowWithAppProvider( + , + ); + + trigger(cell.find('button'), 'onClick'); + + expect(sortSpy).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/src/components/DataTable/tests/DataTable.test.tsx b/src/components/DataTable/tests/DataTable.test.tsx index 8fe7c4e4190..b74296a786a 100644 --- a/src/components/DataTable/tests/DataTable.test.tsx +++ b/src/components/DataTable/tests/DataTable.test.tsx @@ -1,27 +1,9 @@ import * as React from 'react'; -import {mountWithAppProvider, findByTestID} from 'test-utilities'; -import {isEdgeVisible, getPrevAndCurrentColumns} from '../utilities'; -import {Cell, Navigation} from '../components'; +import {mountWithAppProvider, trigger} from 'test-utilities'; +import {Cell} from '../components'; import DataTable, {Props} from '../DataTable'; -interface DataTableTestProps { - sortable?: Props['sortable']; - defaultSortDirection?: Props['defaultSortDirection']; - initialSortColumnIndex?: Props['initialSortColumnIndex']; - onSort?: Props['onSort']; -} - -const sortable = [false, true, false, false, true, false]; -const columnContentTypes: Props['columnContentTypes'] = [ - 'text', - 'numeric', - 'numeric', - 'numeric', - 'numeric', -]; -const spyOnSort = jest.fn(); - -function setup(propOverrides?: DataTableTestProps) { +describe('', () => { const headings = ['Product', 'Price', 'Order Number', 'Quantity', 'Subtotal']; const rows = [ [ @@ -34,152 +16,316 @@ function setup(propOverrides?: DataTableTestProps) { ['Emerald Silk Gown', '$230.00', 124689, 32, '$19,090.00'], ['Mauve Cashmere Scarf', '$445.00', 124533, 140, '$14,240.00'], ]; - const summary = ['', '', '', 255, '$155,830.00']; - - const props = { - columnContentTypes, - headings, - rows, - summary, - ...propOverrides, - }; - const dataTable = mountWithAppProvider(); - - return { - ...props, - dataTable, - twoClicks: true, - }; -} -describe('', () => { - it('renders a table, thead and table body rows', () => { - const {dataTable} = setup(); + const columnContentTypes: Props['columnContentTypes'] = [ + 'text', + 'numeric', + 'numeric', + 'numeric', + 'numeric', + ]; + + const defaultProps: Props = {columnContentTypes, headings, rows}; + + describe('columnContentTypes', () => { + it('sets the provided contentType of Cells in each column', () => { + const headings = ['Column 1', 'Column 2']; + const rows = [['Cell 1', '2']]; + const columnContentTypes: Props['columnContentTypes'] = [ + 'text', + 'numeric', + ]; + const dataTable = mountWithAppProvider( + , + ); + + const cells = dataTable.find(Cell); + const firstColumnCells = cells.filterWhere( + (cell) => cell.prop('firstColumn') === true, + ); + + const secondColumnCells = cells.filterWhere( + (cell) => cell.prop('firstColumn') !== true, + ); + + expect(cells).toHaveLength(4); - expect(dataTable.find('table')).toHaveLength(1); - expect(dataTable.find('thead')).toHaveLength(1); - expect(dataTable.find('thead th')).toHaveLength(5); - expect(dataTable.find('tbody tr')).toHaveLength(3); + firstColumnCells.forEach((cell) => + expect(cell.prop('contentType')).toBe('text'), + ); + + secondColumnCells.forEach((cell) => + expect(cell.prop('contentType')).toBe('numeric'), + ); + }); }); - it('defaults to non-sorting column headings', () => { - const {dataTable} = setup(); - const sortableHeadings = dataTable.find(Cell).filter({sortable: true}); + describe('headings', () => { + it('renders a single table header row', () => { + const headings = ['Heading 1', 'Heading 2', 'Heading 3']; + const dataTable = mountWithAppProvider( + , + ); - expect(sortableHeadings).toHaveLength(0); + expect(dataTable.find('thead tr')).toHaveLength(1); + }); + + it('renders each header Cell with the content provided', () => { + const headings = ['Heading 1', 'Heading 2', 'Heading 3']; + const dataTable = mountWithAppProvider( + , + ); + + const headingCells = dataTable.find('thead tr').find(Cell); + + headingCells.forEach((headingCell, headingCellIndex) => + expect(headingCell.text()).toBe(headings[headingCellIndex]), + ); + }); }); - it('initial sort column defaults to first column if not specified', () => { - const firstColumnSortable = [true, true, false, false, true, false]; - const {dataTable} = setup({ - sortable: firstColumnSortable, - onSort: spyOnSort, + describe('totals', () => { + it('renders a second table header row with totals', () => { + const totals = ['', '$20.00', '']; + const dataTable = mountWithAppProvider( + , + ); + + expect(dataTable.find('thead tr')).toHaveLength(2); + + const totalsRow = dataTable.find('thead tr').at(1); + + expect(totalsRow.text()).toContain(totals.join('')); + }); + + it('sets the content of the first total Cell to the totals row heading', () => { + const totals = ['', '', '']; + const dataTable = mountWithAppProvider( + , + ); + + expect(dataTable.find('thead tr')).toHaveLength(2); + + const expectedTotalsHeadingContent = dataTable + .instance() + .context.polaris.intl.translate('Polaris.DataTable.totalsRowHeading'); + + const firstTotalCell = dataTable + .find(Cell) + .filterWhere((cell) => cell.prop('total') === true) + .first(); + + expect(firstTotalCell.prop('content')).toBe(expectedTotalsHeadingContent); }); - const firstHeadingCell = findByTestID(dataTable, `heading-cell-${0}`); - expect(firstHeadingCell.props().sorted).toBe(true); + it('sets the contentType of non-empty total Cells to numeric', () => { + const totals = ['', '$20.00', '']; + const dataTable = mountWithAppProvider( + , + ); + const totalsCells = dataTable + .find(Cell) + .filterWhere((cell) => cell.prop('total') === true); + + const nonEmptyTotalCells = totalsCells.filterWhere( + (cell) => cell.prop('contentType') === 'numeric', + ); + + const secondTotalsCell = totalsCells.at(1); + + expect(nonEmptyTotalCells).toHaveLength(1); + expect(secondTotalsCell.prop('contentType')).toBe('numeric'); + }); + + it('renders an empty Cell for falsey total values', () => { + const totals = ['', '', '']; + const dataTable = mountWithAppProvider( + , + ); + + expect(dataTable.find('thead tr')).toHaveLength(2); + + const totalsCells = dataTable + .find(Cell) + .filterWhere( + (cell) => + cell.prop('total') === true && cell.prop('firstColumn') !== true, + ); + + totalsCells.forEach((total) => expect(total.text()).toBe('')); + }); + }); + + describe('rows', () => { + it('renders a table body row for each list of table data provided', () => { + const rows = [['First row'], ['Second row'], ['Third row']]; + const dataTable = mountWithAppProvider( + , + ); + + expect(dataTable.find('tbody tr')).toHaveLength(3); + }); + }); + + describe('truncate', () => { + it('defaults to false', () => { + const dataTable = mountWithAppProvider(); + + const firstColumnCells = dataTable + .find(Cell) + .filterWhere((cell) => cell.prop('firstColumn') === true); + + firstColumnCells.forEach((cell) => + expect(cell.prop('truncate')).toBe(false), + ); + }); + + it('passes the value provided to its cells', () => { + const dataTable = mountWithAppProvider( + , + ); + + const firstColumnCells = dataTable + .find(Cell) + .filterWhere((cell) => cell.prop('firstColumn') === true); + + firstColumnCells.forEach((cell) => + expect(cell.prop('truncate')).toBe(true), + ); + }); }); - it('sets specified initial sort column', () => { - const {dataTable} = setup({ - sortable, - onSort: spyOnSort, - initialSortColumnIndex: 4, + describe('footerContent', () => { + it('renders string footer content when provided', () => { + const footerContent = 'Footer text'; + const dataTable = mountWithAppProvider( + , + ); + + expect(dataTable.text()).toContain(footerContent); }); - const fifthHeadingCell = findByTestID(dataTable, `heading-cell-${4}`); - expect(fifthHeadingCell.props().sorted).toBe(true); + it('renders JSX footer content when provided', () => { + const footerContent =
Footer text
; + const dataTable = mountWithAppProvider( + , + ); + + expect(dataTable.containsMatchingElement(footerContent)).toBe(true); + }); }); - describe('', () => { - const {dataTable} = setup(); - it('passes props', () => { - expect( - dataTable - .find(Cell) - .first() - .prop('header'), - ).toBe(true); - expect( - dataTable - .find(Cell) - .first() - .prop('content'), - ).toStrictEqual('Product'); - expect( - dataTable - .find(Cell) - .first() - .prop('contentType'), - ).toStrictEqual('text'); + describe('sortable', () => { + it('defaults to a non-sortable table', () => { + const dataTable = mountWithAppProvider(); + const cells = dataTable.find(Cell); + + cells.forEach((cell) => expect(cell.find('button')).toHaveLength(0)); + }); + + it('renders a sortable header Cell for each true index', () => { + const sortable = [false, true, false, false, true]; + const dataTable = mountWithAppProvider( + , + ); + + const sortableCells = dataTable + .find(Cell) + .filterWhere((cell) => cell.prop('sortable') === true); + + expect(sortableCells).toHaveLength(2); + }); + + it('renders a plain header Cell for each false index', () => { + const sortable = [false, true, false, false, true]; + const dataTable = mountWithAppProvider( + , + ); + + const nonSortableCells = dataTable + .find(Cell) + .filterWhere((cell) => cell.prop('sortable') === false); + + expect(nonSortableCells).toHaveLength(3); }); }); - describe('', () => { - const {dataTable} = setup(); - it('passes scroll props', () => { - expect( - dataTable - .find(Navigation) - .first() - .prop('isScrolledFarthestLeft'), - ).toBe(true); - expect( - dataTable - .find(Navigation) - .first() - .prop('isScrolledFarthestRight'), - ).toBe(false); + describe('defaultSortDirection', () => { + it('passes the value down to the Cell', () => { + const sortable = [false, true, false, false, true]; + const dataTable = mountWithAppProvider( + , + ); + + const firstHeadingCell = dataTable + .find(Cell) + .filterWhere((cell) => cell.props().header === true) + .first(); + + expect(firstHeadingCell.prop('defaultSortDirection')).toBe('ascending'); }); }); - describe('isEdgeVisible()', () => { - it('returns true if there is enough room', () => { - const position = 175; - const tableStart = 145; - const tableEnd = 205; + describe('initialSortColumnIndex', () => { + it('defaults to first column if not specified', () => { + const sortable = [true, true, false, false, true, false]; + const dataTable = mountWithAppProvider( + , + ); - const isVisible = isEdgeVisible(position, tableStart, tableEnd); + const firstHeadingCell = dataTable + .find(Cell) + .filterWhere((cell) => cell.props().header === true) + .first(); - expect(isVisible).toBe(true); + expect(firstHeadingCell.props().sorted).toBe(true); }); - it('returns false if there is not enough room', () => { - const position = 175; - const tableStart = 145; - const tableEnd = 200; + it('sets specified initial sort column', () => { + const sortable = [true, true, false, false, true, false]; + const initialSortColumnIndex = 4; + const dataTable = mountWithAppProvider( + , + ); - const isVisible = isEdgeVisible(position, tableStart, tableEnd); + const fifthHeadingCell = dataTable + .find(Cell) + .filterWhere((cell) => cell.props().header === true) + .at(initialSortColumnIndex); - expect(isVisible).toBe(false); + expect(fifthHeadingCell.props().sorted).toBe(true); }); }); - describe('getPrevAndCurrentColumns()', () => { - it('returns the calculated measurements', () => { - const columnVisibilityData = [ - {leftEdge: 145, rightEdge: 236, isVisible: true}, - {leftEdge: 236, rightEdge: 357, isVisible: true}, - {leftEdge: 357, rightEdge: 474, isVisible: true}, - {leftEdge: 474, rightEdge: 601, isVisible: true}, - ]; + describe('onSort', () => { + it('gets called when sortable column heading is clicked', () => { + const spyOnSort = jest.fn(); + const sortable = [true, false, false, false, false]; + const dataTable = mountWithAppProvider( + , + ); + + const firstHeadingCell = dataTable + .find(Cell) + .filterWhere((cell) => cell.props().header === true) + .first(); + + trigger(firstHeadingCell, 'onSort'); - const tableData = { - fixedColumnWidth: 145, - firstVisibleColumnIndex: 3, - tableLeftVisibleEdge: 145, - tableRightVisibleEdge: 551, - }; - - const actualMeasurement = getPrevAndCurrentColumns( - tableData, - columnVisibilityData, - ); - const expectedMeasurement = { - previousColumn: {leftEdge: 357, rightEdge: 474, isVisible: true}, - currentColumn: {leftEdge: 474, rightEdge: 601, isVisible: true}, - }; - expect(actualMeasurement).toStrictEqual(expectedMeasurement); + expect(spyOnSort).toHaveBeenCalledTimes(1); }); }); }); diff --git a/src/components/DataTable/types.ts b/src/components/DataTable/types.ts index 8171f43eb46..fbc241cc1b0 100644 --- a/src/components/DataTable/types.ts +++ b/src/components/DataTable/types.ts @@ -6,21 +6,13 @@ export interface ColumnVisibilityData { isVisible?: boolean; } -interface ScrollPosition { - left?: number; - top?: number; -} - export interface DataTableState { - collapsed: boolean; + condensed: boolean; columnVisibilityData: ColumnVisibilityData[]; previousColumn?: ColumnVisibilityData; currentColumn?: ColumnVisibilityData; sortedColumnIndex?: number; sortDirection?: SortDirection; - heights: number[]; - fixedColumnWidth?: number; - preservedScrollPosition: ScrollPosition; isScrolledFarthestLeft?: boolean; isScrolledFarthestRight?: boolean; } diff --git a/src/components/DataTable/utilities.ts b/src/components/DataTable/utilities.ts index 507fef9fe41..1947b178297 100644 --- a/src/components/DataTable/utilities.ts +++ b/src/components/DataTable/utilities.ts @@ -1,7 +1,6 @@ import {DataTableState} from './types'; interface TableMeasurements { - fixedColumnWidth: number; firstVisibleColumnIndex: number; tableLeftVisibleEdge: number; tableRightVisibleEdge: number; @@ -13,10 +12,9 @@ export function measureColumn(tableData: TableMeasurements) { firstVisibleColumnIndex, tableLeftVisibleEdge: tableStart, tableRightVisibleEdge: tableEnd, - fixedColumnWidth, } = tableData; - const leftEdge = column.offsetLeft + fixedColumnWidth; + const leftEdge = column.offsetLeft; const rightEdge = leftEdge + column.offsetWidth; const isVisibleLeft = isEdgeVisible(leftEdge, tableStart, tableEnd); const isVisibleRight = isEdgeVisible(rightEdge, tableStart, tableEnd);