Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 50 additions & 34 deletions lib/checks/tables/td-headers-attr-evaluate.js
Original file line number Diff line number Diff line change
@@ -1,63 +1,79 @@
import { tokenList } from '../../core/utils';
import { isVisibleToScreenReaders } from '../../commons/dom';
import { getRole } from '../../commons/aria';

// Order determines the priority of reporting
// Only if 0 of higher issues exists will the next be reported
const messageKeys = [
'cell-header-not-in-table',
'cell-header-not-th',
'header-refs-self',
'empty-hdrs' // incomplete
];
const [notInTable, notTh, selfRef, emptyHdrs] = messageKeys;

export default function tdHeadersAttrEvaluate(node) {
const cells = [];
const reviewCells = [];
const badCells = [];

const cellRoleById = {};
for (let rowIndex = 0; rowIndex < node.rows.length; rowIndex++) {
const row = node.rows[rowIndex];

for (let cellIndex = 0; cellIndex < row.cells.length; cellIndex++) {
cells.push(row.cells[cellIndex]);
const cell = row.cells[cellIndex];
cells.push(cell);

// Save header id to set if it's th or td with roles columnheader/rowheader
const cellId = cell.getAttribute('id');
if (cellId) {
cellRoleById[cellId] = getRole(cell);
}
}
}

const ids = cells
.filter(cell => cell.getAttribute('id'))
.map(cell => cell.getAttribute('id'));

const badCells = {
[selfRef]: [],
[notInTable]: [],
[notTh]: [],
[emptyHdrs]: []
};
cells.forEach(cell => {
let isSelf = false;
let notOfTable = false;

if (!cell.hasAttribute('headers') || !isVisibleToScreenReaders(cell)) {
return;
}

const headersAttr = cell.getAttribute('headers').trim();
if (!headersAttr) {
return reviewCells.push(cell);
badCells[emptyHdrs].push(cell);
return;
}

const cellId = cell.getAttribute('id');
// Get a list all the values of the headers attribute
const headers = tokenList(headersAttr);

if (headers.length !== 0) {
// Check if the cell's id is in this list
if (cell.getAttribute('id')) {
isSelf = headers.indexOf(cell.getAttribute('id').trim()) !== -1;
headers.forEach(headerId => {
if (cellId && headerId === cellId) {
// Header references its own cell
badCells[selfRef].push(cell);
} else if (!cellRoleById[headerId]) {
// Header references a cell that is not in the table
badCells[notInTable].push(cell);
} else if (
!['columnheader', 'rowheader'].includes(cellRoleById[headerId])
) {
// Header references a cell that is not a row or column header
badCells[notTh].push(cell);
}
});
});

// Check if the headers are of cells inside the table
notOfTable = headers.some(header => !ids.includes(header));

if (isSelf || notOfTable) {
badCells.push(cell);
for (const messageKey of messageKeys) {
if (badCells[messageKey].length > 0) {
this.relatedNodes(badCells[messageKey]);
if (messageKey === emptyHdrs) {
return undefined;
}
this.data({ messageKey });
return false;
}
});

if (badCells.length > 0) {
this.relatedNodes(badCells);
return false;
}

if (reviewCells.length) {
this.relatedNodes(reviewCells);
return undefined;
}

return true;
}
8 changes: 6 additions & 2 deletions lib/checks/tables/td-headers-attr.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@
"metadata": {
"impact": "serious",
"messages": {
"pass": "The headers attribute is exclusively used to refer to other cells in the table",
"pass": "The headers attribute is exclusively used to refer to other header cells in the table",
"incomplete": "The headers attribute is empty",
"fail": "The headers attribute is not exclusively used to refer to other cells in the table"
"fail": {
"cell-header-not-in-table": "The headers attribute is not exclusively used to refer to other header cells in the table",
"cell-header-not-th": "The header attribute refers a data cell, this must be a header cell so that all screen readers announce it",
"header-refs-self": "The element with headers attribute refers to itself"
}
}
}
}
11 changes: 7 additions & 4 deletions locales/_template.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
{
"lang": "xyz",
"rules": {
"accesskeys": {
"description": "Ensure every accesskey attribute value is unique",
Expand Down Expand Up @@ -1096,9 +1095,13 @@
"fail": "Some non-empty data cells do not have table headers"
},
"td-headers-attr": {
"pass": "The headers attribute is exclusively used to refer to other cells in the table",
"pass": "The headers attribute is exclusively used to refer to other header cells in the table",
"incomplete": "The headers attribute is empty",
"fail": "The headers attribute is not exclusively used to refer to other cells in the table"
"fail": {
"cell-header-not-in-table": "The headers attribute is not exclusively used to refer to other header cells in the table",
"cell-header-not-th": "The header attribute refers a data cell, this must be a header cell so that all screen readers announce it",
"header-refs-self": "The element with headers attribute refers to itself"
}
},
"th-has-data-cells": {
"pass": "All table header cells refer to data cells",
Expand All @@ -1120,4 +1123,4 @@
}
},
"incompleteFallbackMessage": "axe couldn't tell the reason. Time to break out the element inspector!"
}
}
41 changes: 41 additions & 0 deletions test/checks/tables/td-headers-attr.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ describe('td-headers-attr', function () {
);
node = fixture.querySelector('table');
assert.isFalse(check.call(checkContext, node));
assert.deepEqual(checkContext._data, {
messageKey: 'cell-header-not-in-table'
});

fixtureSetup(
'<table id="hi">' +
Expand All @@ -102,6 +105,43 @@ describe('td-headers-attr', function () {
assert.isFalse(check.call(checkContext, node));
});

it('returns false if table cell referenced as header', function () {
fixtureSetup(`
<table>
<tr> <td id="hi">hello</td> </tr>
<tr> <td headers="hi">goodbye</td> </tr>
</table>'
`);

var node = fixture.querySelector('table');
assert.isFalse(check.call(checkContext, node));
assert.deepEqual(checkContext._data, { messageKey: 'cell-header-not-th' });
});

it('returns true if table cell referenced as header with role rowheader or columnheader', function () {
var node;

fixtureSetup(`
<table>
<tr> <td role="rowheader" id="hi">hello</td> </tr>
<tr> <td headers="hi">goodbye</td> </tr>
</table>
`);

node = fixture.querySelector('table');
assert.isTrue(check.call(checkContext, node));

fixtureSetup(`
<table>
<tr> <td role="columnheader" id="hi">hello</td> </tr>
<tr> <td headers="hi">goodbye</td> </tr>
</table>
`);

node = fixture.querySelector('table');
assert.isTrue(check.call(checkContext, node));
});

it('returns false if the header refers to the same cell', function () {
fixtureSetup(
'<table id="hi">' +
Expand All @@ -112,6 +152,7 @@ describe('td-headers-attr', function () {

var node = fixture.querySelector('table');
assert.isFalse(check.call(checkContext, node));
assert.deepEqual(checkContext._data, { messageKey: 'header-refs-self' });
});

it('returns true if td[headers] is hidden', function () {
Expand Down
20 changes: 20 additions & 0 deletions test/integration/rules/td-headers-attr/td-headers-attr.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
<td id="self" headers="self" hidden>World</td>
</table>

<table id="pass5">
<td role="rowheader" id="hdr1">Hello</td>
<td headers="hdr1">World</td>
</table>

<table id="fail1">
<th id="f1h1">Hello</th>
<td headers="f1h1 non-existing">World</td>
Expand All @@ -32,6 +37,21 @@
<td id="self" headers="self">World</td>
</table>

<table id="fail4">
<td id="hdr1">Hello</td>
<td headers="hdr1">World</td>
</table>

<table id="fail5">
<th role="cell" id="th-role-cell-hdr">Hello</td>
<td headers="th-role-cell-hdr">World</td>
</table>

<table id="fail6">
<th role="button" id="th-role-button-hdr">Hello</td>
<td headers="th-role-button-hdr">World</td>
</table>

<table id="inapplicable1" role="none">
<td id="self" headers="self">World</td>
</table>
Expand Down
11 changes: 9 additions & 2 deletions test/integration/rules/td-headers-attr/td-headers-attr.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
{
"description": "td-headers-attr test",
"rule": "td-headers-attr",
"violations": [["#fail1"], ["#fail2"], ["#fail3"]],
"passes": [["#pass1"], ["#pass2"], ["#pass3"], ["#pass4"]]
"violations": [
["#fail1"],
["#fail2"],
["#fail3"],
["#fail4"],
["#fail5"],
["#fail6"]
],
"passes": [["#pass1"], ["#pass2"], ["#pass3"], ["#pass4"], ["#pass5"]]
}