From 3801ce3f174e520ae07d69307b73f6198e56fd9e Mon Sep 17 00:00:00 2001 From: Jackie Chen Date: Wed, 17 May 2017 11:34:44 -0700 Subject: [PATCH] Fix aria roles for table headers and cells Fixes #680. - Add id, aria role, and aria sort to table column headers - Add aria describedby and role to table cells --- docs/Column.md | 1 + source/Table/Column.js | 3 +++ source/Table/Table.jest.js | 52 ++++++++++++++++++++++++++++++++++++-- source/Table/Table.js | 25 +++++++++++++++--- 4 files changed, 76 insertions(+), 5 deletions(-) diff --git a/docs/Column.md b/docs/Column.md index 6c9682eb6..4644c6dd7 100644 --- a/docs/Column.md +++ b/docs/Column.md @@ -16,6 +16,7 @@ Describes the header and cell contents of a table column. | flexShrink | Number | | Flex shrink style; defaults to 1 | | headerClassName | String | | CSS class to apply to this column's header | | headerRenderer | Function | | Optional callback responsible for rendering a column's header column. [Learn more](#headerrenderer) | +| id | String | | Optional id to set on the column header | | label | String | | Header label for this column | | maxWidth | Number | | Maximum width of column; this property will only be used if :flexGrow is greater than 0 | | minWidth | Number | | Minimum width of column | diff --git a/source/Table/Column.js b/source/Table/Column.js index dc2d3003f..6fc3e52bb 100644 --- a/source/Table/Column.js +++ b/source/Table/Column.js @@ -52,6 +52,9 @@ export default class Column extends Component { */ headerRenderer: PropTypes.func.isRequired, + /** Optional id to set on the column header */ + id: PropTypes.string, + /** Header label for this column */ label: PropTypes.string, diff --git a/source/Table/Table.jest.js b/source/Table/Table.jest.js index 70f16f02f..2b92c390b 100644 --- a/source/Table/Table.jest.js +++ b/source/Table/Table.jest.js @@ -41,6 +41,7 @@ describe('Table', () => { cellDataGetter, cellRenderer, columnData = { data: 123 }, + columnID, columnStyle, disableSort = false, headerRenderer, @@ -70,6 +71,7 @@ describe('Table', () => { headerRenderer={headerRenderer} disableSort={disableSort} style={columnStyle} + id={columnID} /> { expect(row.getAttribute('role')).toEqual('row') }) + it('should set aria role on a cell', () => { + const rendered = findDOMNode(render(getMarkup())) + const cell = rendered.querySelector('.ReactVirtualized__Table__rowColumn') + expect(cell.getAttribute('role')).toEqual('gridcell') + }) + + it('should set aria-describedby on a cell when the column has an id', () => { + const columnID = 'column-header-test' + const rendered = findDOMNode(render(getMarkup({ + columnID + }))) + const cell = rendered.querySelector('.ReactVirtualized__Table__rowColumn') + expect(cell.getAttribute('aria-describedby')).toEqual(columnID) + }) + it('should attach a11y properties to a row if :onRowClick is specified', () => { const rendered = findDOMNode(render(getMarkup({ onRowClick: () => {} @@ -903,6 +920,39 @@ describe('Table', () => { expect(row.tabIndex).toEqual(-1) }) + it('should set aria role on a header column', () => { + const rendered = findDOMNode(render(getMarkup())) + const header = rendered.querySelector('.ReactVirtualized__Table__headerColumn') + expect(header.getAttribute('role')).toEqual('columnheader') + }) + + it('should set aria-sort ascending on a header column if the column is sorted ascending', () => { + const rendered = findDOMNode(render(getMarkup({ + sortBy: 'name', + sortDirection: SortDirection.ASC + }))) + const header = rendered.querySelector('.ReactVirtualized__Table__headerColumn') + expect(header.getAttribute('aria-sort')).toEqual('ascending') + }) + + it('should set aria-sort descending on a header column if the column is sorted descending', () => { + const rendered = findDOMNode(render(getMarkup({ + sortBy: 'name', + sortDirection: SortDirection.DESC + }))) + const header = rendered.querySelector('.ReactVirtualized__Table__headerColumn') + expect(header.getAttribute('aria-sort')).toEqual('descending') + }) + + it('should set id on a header column when the column has an id', () => { + const columnID = 'column-header-test' + const rendered = findDOMNode(render(getMarkup({ + columnID + }))) + const header = rendered.querySelector('.ReactVirtualized__Table__headerColumn') + expect(header.getAttribute('id')).toEqual(columnID) + }) + it('should attach a11y properties to a header column if sort is enabled', () => { const rendered = findDOMNode(render(getMarkup({ disableSort: false, @@ -910,7 +960,6 @@ describe('Table', () => { }))) const row = rendered.querySelector('.ReactVirtualized__Table__headerColumn') expect(row.getAttribute('aria-label')).toEqual('Name') - expect(row.getAttribute('role')).toEqual('rowheader') expect(row.tabIndex).toEqual(0) }) @@ -920,7 +969,6 @@ describe('Table', () => { }))) const row = rendered.querySelector('.ReactVirtualized__Table__headerColumn') expect(row.getAttribute('aria-label')).toEqual(null) - expect(row.getAttribute('role')).toEqual(null) expect(row.tabIndex).toEqual(-1) }) }) diff --git a/source/Table/Table.js b/source/Table/Table.js index b08d38fd7..a7c1b2056 100644 --- a/source/Table/Table.js +++ b/source/Table/Table.js @@ -384,7 +384,8 @@ export default class Table extends PureComponent { cellRenderer, className, columnData, - dataKey + dataKey, + id } = column.props const cellData = cellDataGetter({ columnData, dataKey, rowData }) @@ -396,10 +397,18 @@ export default class Table extends PureComponent { ? renderedCell : null + const a11yProps = {} + + if (id) { + a11yProps['aria-describedby'] = id + } + return (
@@ -410,7 +419,7 @@ export default class Table extends PureComponent { _createHeader ({ column, index }) { const { headerClassName, headerStyle, onHeaderClick, sort, sortBy, sortDirection } = this.props - const { dataKey, disableSort, headerRenderer, label, columnData } = column.props + const { dataKey, disableSort, headerRenderer, id, label, columnData } = column.props const sortEnabled = !disableSort && sort const classNames = cn( @@ -455,17 +464,27 @@ export default class Table extends PureComponent { } a11yProps['aria-label'] = column.props['aria-label'] || label || dataKey - a11yProps.role = 'rowheader' a11yProps.tabIndex = 0 a11yProps.onClick = onClick a11yProps.onKeyDown = onKeyDown } + if (sortBy === dataKey) { + a11yProps['aria-sort'] = sortDirection === SortDirection.ASC + ? 'ascending' + : 'descending' + } + + if (id) { + a11yProps.id = id + } + return (
{renderedHeader}