From e815a3073248ef36e2a228594161a35abd7571b2 Mon Sep 17 00:00:00 2001 From: Juan Pablo Lomeli Diaz Date: Thu, 30 Jun 2022 20:29:29 +0200 Subject: [PATCH] adds the ability to change the table role (#1076) --- src/components/Table/Table.Head.jsx | 2 + src/components/Table/Table.HeaderCell.jsx | 24 +- src/components/Table/Table.css.js | 4 +- src/components/Table/Table.jsx | 5 + src/components/Table/Table.test.js | 259 ++++++++++++------ .../Table/stories/TableWithSorting.js | 1 + 6 files changed, 208 insertions(+), 87 deletions(-) diff --git a/src/components/Table/Table.Head.jsx b/src/components/Table/Table.Head.jsx index 554f0cfdf..b03ab2096 100644 --- a/src/components/Table/Table.Head.jsx +++ b/src/components/Table/Table.Head.jsx @@ -17,6 +17,7 @@ function TableHead({ selected, selectionKey, sortedInfo, + tableRole, withSelectableRows, }) { function handleChange(_, checked) { @@ -51,6 +52,7 @@ function TableHead({ column={column} isLoading={isLoading} sortedInfo={sortedInfo} + tableRole={tableRole} /> ))} diff --git a/src/components/Table/Table.HeaderCell.jsx b/src/components/Table/Table.HeaderCell.jsx index 1c5b241b0..650010fd9 100644 --- a/src/components/Table/Table.HeaderCell.jsx +++ b/src/components/Table/Table.HeaderCell.jsx @@ -15,20 +15,28 @@ import { generateCustomHeaderCell, } from './Table.utils' -export default function HeaderCell({ column, isLoading, sortedInfo }) { +export default function HeaderCell({ + column, + isLoading, + sortedInfo, + tableRole, +}) { const sorterBtnId = Array.isArray(column.columnKey) ? `${column.columnKey.join('_')}_${column.sortKey}_sorter` : `${column.columnKey}_${column.sortKey}_sorter` - function getColumnSortStatus() { + function isTableSortedByThisColumn() { const colKey = Array.isArray(column.columnKey) ? column.sortKey : column.columnKey - const isTableSortedByThisColumn = + return ( !isNil(sortedInfo) && sortedInfo.order && sortedInfo.columnKey === colKey + ) + } - if (isTableSortedByThisColumn) { + function getColumnSortStatus() { + if (isTableSortedByThisColumn()) { return sortedInfo.order } @@ -81,6 +89,7 @@ export default function HeaderCell({ column, isLoading, sortedInfo }) { if (isSortable) { const columnSortStatus = getColumnSortStatus() + const isSorted = isTableSortedByThisColumn() return ( {renderCellContents()} diff --git a/src/components/Table/Table.css.js b/src/components/Table/Table.css.js index 5fafa96de..8fb6f474d 100644 --- a/src/components/Table/Table.css.js +++ b/src/components/Table/Table.css.js @@ -81,8 +81,8 @@ export const TableUI = styled('table')` background: linear-gradient( to right, ${props => props.theme.bgFocusIndicator}, - ${props => props.theme.bgFocusIndicator} 3px, - transparent 3px, + ${props => props.theme.bgFocusIndicator} 2px, + transparent 2px, transparent 100% ); } diff --git a/src/components/Table/Table.jsx b/src/components/Table/Table.jsx index d3aa17043..55856c673 100644 --- a/src/components/Table/Table.jsx +++ b/src/components/Table/Table.jsx @@ -49,6 +49,7 @@ export function Table({ }, tableClassName, tableDescription, + tableRole = 'table', tableWidth = { min: '700px' }, withColumnChooser = false, withFocusableRows = false, @@ -145,6 +146,7 @@ export function Table({ withSelectableRows && 'selection-enabled', tableClassName )} + role={tableRole} tableWidth={tableWidth} withTallRows={withTallRows} > @@ -160,6 +162,7 @@ export function Table({ state.selectedRows.length === state.currentTableData.length } sortedInfo={sortedInfo} + tableRole={tableRole} withSelectableRows={withSelectableRows} /> ` width */ tableWidth: PropTypes.shape({ min: PropTypes.string, max: PropTypes.string }), /** An object to customize the visual appearance of the table. See [Skins.md](/src/components/Table/docs/Skins.md) */ diff --git a/src/components/Table/Table.test.js b/src/components/Table/Table.test.js index 9f7991bf7..678df5e41 100644 --- a/src/components/Table/Table.test.js +++ b/src/components/Table/Table.test.js @@ -1,5 +1,4 @@ import React from 'react' -import { mount, render as enzymeRender } from 'enzyme' import { render } from '@testing-library/react' import user from '@testing-library/user-event' import { Table, TABLE_CLASSNAME } from './Table' @@ -13,7 +12,7 @@ import { SELECT_ROW, DESELECT_ROW, } from './Table.actionTypes' -import { defaultSkin, alternativeSkin, chooseSkin } from './Table.skins' +import { alternativeSkin } from './Table.skins' import { columnsChooser, createFakeCustomers, @@ -25,33 +24,37 @@ import { createColumnChooserListItems } from './Table.utils' describe('ClassName', () => { test('Wrapper has default className', () => { - const wrapper = enzymeRender() + const { container } = render(
) - expect(wrapper.hasClass(`${TABLE_CLASSNAME}__Wrapper`)).toBeTruthy() + expect( + container.querySelector(`.${TABLE_CLASSNAME}__Wrapper`) + ).toBeInTheDocument() }) test('Applies custom className to wrapper if specified', () => { const className = 'channel-4' - const wrapper = enzymeRender( + const { container } = render(
) - expect(wrapper.hasClass(className)).toBeTruthy() + expect(container.querySelector(`.${TABLE_CLASSNAME}__Wrapper`)).toHaveClass( + className + ) }) test('Table has default className', () => { - const wrapper = enzymeRender(
) + const { getByRole } = render(
) - expect(wrapper.find('table').hasClass(TABLE_CLASSNAME)).toBeTruthy() + expect(getByRole('table')).toHaveClass(TABLE_CLASSNAME) }) test('Applies custom className to table if specified', () => { const className = 'channel-4' - const wrapper = enzymeRender( + const { getByRole } = render(
) - expect(wrapper.find('table').hasClass(className)).toBeTruthy() + expect(getByRole('table')).toHaveClass(className) }) }) @@ -90,20 +93,20 @@ describe('Render', () => { describe('Table Header', () => { test('Renders rows and cells', () => { - const wrapper = mount( + const { container } = render(
) - const thead = wrapper.find('thead') - const headerCells = thead.find('th') - const nameHeaderCell = headerCells.first() + const thead = container.querySelector('thead') + const headerCells = thead.querySelectorAll('th') + const nameHeaderCell = headerCells[0] - expect(thead.exists()).toBeTruthy() + expect(thead).toBeInTheDocument() expect(headerCells.length).toBe(4) - expect(nameHeaderCell.text()).toBe('Name') + expect(nameHeaderCell).toHaveTextContent('Name') }) test('Icon Header cells', () => { @@ -127,47 +130,47 @@ describe('Table Header', () => { }) test('Custom cells', () => { - const wrapper = mount( + const { container } = render(
) - const thead = wrapper.find('thead') - const headerCells = thead.find('th') - const nameHeaderCell = headerCells.first() + const thead = container.querySelector('thead') + const headerCells = thead.querySelectorAll('th') + const nameHeaderCell = headerCells[0] - expect(nameHeaderCell.find('em').exists()).toBeTruthy() + expect(nameHeaderCell.querySelector('em')).toBeInTheDocument() }) }) describe('Table Body', () => { test('Renders rows and cells', () => { const customers = createFakeCustomers({ amount: 10 }) - const wrapper = mount( + const { container } = render(
) - const tbody = wrapper.find('tbody') - const rows = tbody.find('tr') - const firstRow = rows.first() - const cellsInRow = firstRow.find('td') + const tbody = container.querySelector('tbody') + const rows = tbody.querySelectorAll('tr') + const firstRow = rows[0] + const cellsInRow = firstRow.querySelectorAll('td') - expect(tbody.exists()).toBeTruthy() + expect(tbody).toBeInTheDocument() expect(rows.length).toBe(10) expect(cellsInRow.length).toBe(4) - expect(firstRow.hasClass(`${TABLE_CLASSNAME}__Row`)).toBeTruthy() + expect(firstRow).toHaveClass(`${TABLE_CLASSNAME}__Row`) const customer = customers[0] - expect(cellsInRow.at(0).text()).toBe(customer.name) - expect(cellsInRow.at(1).text()).toBe(customer.companyName) - expect(cellsInRow.at(2).text()).toBe(customer.emails) - expect(cellsInRow.at(3).text()).toBe(customer.lastSeen) + expect(cellsInRow[0]).toHaveTextContent(customer.name) + expect(cellsInRow[1]).toHaveTextContent(customer.companyName) + expect(cellsInRow[2]).toHaveTextContent(customer.emails) + expect(cellsInRow[3]).toHaveTextContent(customer.lastSeen) }) test('Renders rows with provided classNames', () => { @@ -176,18 +179,18 @@ describe('Table Body', () => { return { ...info, ...{ className } } }) - const wrapper = mount( + const { container } = render(
) - const tbody = wrapper.find('tbody') - const rows = tbody.find('tr') + const tbody = container.querySelector('tbody') + const rows = tbody.querySelectorAll('tr') customers.forEach((customer, i) => { - expect(rows.get(i).props.className).toContain(customer.className) + expect(rows[i]).toHaveClass(customer.className) }) }) @@ -214,19 +217,19 @@ describe('Table Body', () => { }, ] - const wrapper = mount( + const { container } = render(
) - const tbody = wrapper.find('tbody') - const rows = tbody.find('tr') - const cellsInRow = rows.first().find('td') + const tbody = container.querySelector('tbody') + const rows = tbody.querySelectorAll('tr') + const cellsInRow = rows[0].querySelectorAll('td') - expect(cellsInRow.first().find('.name').exists()).toBeTruthy() - expect(cellsInRow.first().find('.companyName').exists()).toBeTruthy() + expect(cellsInRow[0].querySelector('.name')).toBeInTheDocument() + expect(cellsInRow[0].querySelector('.companyName')).toBeInTheDocument() }) test('should add without-padding class name if clearCellPadding is added to a column', () => { @@ -252,45 +255,37 @@ describe('Table Body', () => { }) describe('Skin', () => { - test('Renders default without specifying skin', () => { - const customers = createFakeCustomers({ amount: 10 }) - const wrapper = mount( -
- ) - - expect(chooseSkin(wrapper.prop('skin'))).toEqual(defaultSkin) - }) - test('Renders default skin', () => { const customers = createFakeCustomers({ amount: 10 }) - const wrapper = mount( + const { getByRole } = render(
) - expect(chooseSkin(wrapper.prop('skin'))).toEqual(defaultSkin) + expect( + window.getComputedStyle(getByRole('table').querySelector('th')) + .backgroundColor + ).toBe('white') }) test('Renders alternative skin', () => { const customers = createFakeCustomers({ amount: 10 }) - const wrapper = mount( + const { getByRole } = render(
) - expect(chooseSkin(wrapper.prop('skin'))).toEqual(alternativeSkin) + expect( + window.getComputedStyle(getByRole('table').querySelector('th')) + .backgroundColor + ).toBe('rgb(229, 233, 236)') //rgb value of getColor('grey.400') }) test('Renders custom skin', () => { @@ -307,7 +302,7 @@ describe('Skin', () => { borderRows: '1px solid blueviolet', borderColumns: '1px solid blueviolet', } - const wrapper = mount( + const { getByRole } = render(
{ /> ) - expect(chooseSkin(wrapper.prop('skin'))).toEqual({ - ...defaultSkin, - ...purpleSkin, - }) + expect( + window.getComputedStyle(getByRole('table').querySelector('td')).color + ).toBe('rebeccapurple') }) }) describe('Is loading state', () => { test('Displays LoadingUI', () => { - const wrapper = enzymeRender( + const { container } = render(
) - expect(wrapper.find(`.${TABLE_CLASSNAME}__Loading`).length).toBeTruthy() + expect( + container.querySelector(`.${TABLE_CLASSNAME}__Loading`) + ).toBeInTheDocument() }) }) @@ -490,8 +486,8 @@ describe('Clickable Rows', () => { }) }) -describe('Sortable', () => { - test('Fires function when header cell is clicked', () => { +describe('Sorting', () => { + test('it should sort when you tell it to sort', () => { const customers = createFakeCustomers({ amount: 5 }) const regularColumnSpy = jest.fn() const compoundColumnSpy = jest.fn() @@ -587,6 +583,108 @@ describe('Sortable', () => { 'descending' ) }) + + test('it should sort when you tell it to sort (role not table)', () => { + const customers = createFakeCustomers({ amount: 5 }) + const regularColumnSpy = jest.fn() + const compoundColumnSpy = jest.fn() + const columns = [ + { + title: 'Name', + columnKey: 'name', + width: '30%', + sortKey: 'name', + sorter: regularColumnSpy, + }, + { + title: 'Customer (sorts by name)', + columnKey: ['name', 'companyName'], + width: '30%', + sortKey: 'companyName', + sorter: compoundColumnSpy, + }, + ] + const { container, rerender } = render( +
+ ) + + // Regular column sorting, should be called with 'columnKey' + expect(container.querySelector(`thead th`)).not.toHaveAttribute('aria-sort') + expect(container.querySelector('thead button')).not.toHaveAttribute( + 'aria-label' + ) + expect(container.querySelector('.is-sortable')).toBeInTheDocument() + + rerender( +
+ ) + + expect(container.querySelector('thead th')).not.toHaveAttribute('aria-sort') + expect(container.querySelector('thead button')).toHaveAttribute( + 'aria-label', + 'Sorted by name ascending' + ) + + user.click(container.querySelector('.c-Table__SortableHeaderCell__title')) + + expect(regularColumnSpy).toHaveBeenCalled() + expect(regularColumnSpy).toHaveBeenCalledWith(columns[0].columnKey) + + expect( + container.querySelector(`.${TABLE_CLASSNAME}__SortableHeaderCell`) + ).toBeInTheDocument() + + expect( + container.querySelector(`.${TABLE_CLASSNAME}__SortableHeaderCell__title`) + ).toBeInTheDocument() + + // Compound column sorting, should be called with 'sortKey' + const customerHeaderCell = container.querySelectorAll('thead th')[1] + + user.click( + customerHeaderCell.querySelector('.c-Table__SortableHeaderCell__title') + ) + + expect(compoundColumnSpy).toHaveBeenCalled() + expect(compoundColumnSpy).toHaveBeenCalledWith(columns[1].sortKey) + + rerender( +
+ ) + + expect(container.querySelector('thead th')).not.toHaveAttribute('aria-sort') + expect(container.querySelector('thead button')).toHaveAttribute( + 'aria-label', + 'Sorted by name descending' + ) + }) }) describe('Expandable', () => { @@ -614,21 +712,21 @@ describe('Expandable', () => { ) expect(container.querySelectorAll('tbody tr').length).toBe(4) - expect(getByRole('button').textContent).toBe('View All') + expect(getByRole('button')).toHaveTextContent('View All') user.click(getByRole('button')) expect(container.querySelectorAll('tbody tr').length).toBe(10) - expect(getByRole('button').textContent).toBe('Collapse') + expect(getByRole('button')).toHaveTextContent('Collapse') user.click(getByRole('button')) expect(container.querySelectorAll('tbody tr').length).toBe(4) - expect(getByRole('button').textContent).toBe('View All') + expect(getByRole('button')).toHaveTextContent('View All') }) test('Table expands/collapses on click of Expander (custom text)', () => { - const wrapper = mount( + const { container } = render(
{ }} /> ) - const expander = wrapper.find(`.${TABLE_CLASSNAME}__Expander`).first() - expander.simulate('click') - expect(expander.text()).toBe('Show me all') + expect( + container.querySelector(`.${TABLE_CLASSNAME}__Expander`) + ).toHaveTextContent('Show me all') - const expander2 = wrapper.find(`.${TABLE_CLASSNAME}__Expander`).first() - expander2.simulate('click') + user.click(container.querySelector(`.${TABLE_CLASSNAME}__Expander`)) - expect(expander2.text()).toBe('Show me the top 4') + expect( + container.querySelector(`.${TABLE_CLASSNAME}__Expander`) + ).toHaveTextContent('Show me the top 4') }) test('Table fires onExpand on click of Expander', () => { diff --git a/src/components/Table/stories/TableWithSorting.js b/src/components/Table/stories/TableWithSorting.js index 35f518998..afaa1a91b 100644 --- a/src/components/Table/stories/TableWithSorting.js +++ b/src/components/Table/stories/TableWithSorting.js @@ -74,6 +74,7 @@ export default class TablePlayground extends Component { data={data} isLoading={isLoading} sortedInfo={sortedInfo} + tableRole="presentation" tableDescription="Example table with sorting" />