Skip to content

Commit ec7c6c8

Browse files
feat(td-headers-attr): report headers attribute referencing other <td> elements as unsupported (#4589)
Fix for the header attribute check. The check will report cells that references other `<td>` elements. Added unit and integrations tests as requested in corresponding issue. Closes: [#3987](#3987) --------- Co-authored-by: Dan Bjorge <[email protected]>
1 parent b7736de commit ec7c6c8

File tree

9 files changed

+160
-48
lines changed

9 files changed

+160
-48
lines changed

doc/rule-descriptions.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@
7171
| [server-side-image-map](https://dequeuniversity.com/rules/axe/4.10/server-side-image-map?application=RuleDescription) | Ensure that server-side image maps are not used | Minor | cat.text-alternatives, wcag2a, wcag211, section508, section508.22.f, TTv5, TT4.a, EN-301-549, EN-9.2.1.1 | needs&nbsp;review | |
7272
| [summary-name](https://dequeuniversity.com/rules/axe/4.10/summary-name?application=RuleDescription) | Ensure summary elements have discernible text | Serious | cat.name-role-value, wcag2a, wcag412, section508, section508.22.a, TTv5, TT6.a, EN-301-549, EN-9.4.1.2 | failure, needs&nbsp;review | |
7373
| [svg-img-alt](https://dequeuniversity.com/rules/axe/4.10/svg-img-alt?application=RuleDescription) | Ensure &lt;svg&gt; elements with an img, graphics-document or graphics-symbol role have an accessible text | Serious | cat.text-alternatives, wcag2a, wcag111, section508, section508.22.a, TTv5, TT7.a, EN-301-549, EN-9.1.1.1, ACT | failure, needs&nbsp;review | [7d6734](https://act-rules.github.io/rules/7d6734) |
74-
| [td-headers-attr](https://dequeuniversity.com/rules/axe/4.10/td-headers-attr?application=RuleDescription) | Ensure that each cell in a table that uses the headers attribute refers only to other cells in that table | Serious | cat.tables, wcag2a, wcag131, section508, section508.22.g, TTv5, TT14.b, EN-301-549, EN-9.1.3.1 | failure, needs&nbsp;review | [a25f45](https://act-rules.github.io/rules/a25f45) |
74+
| [td-headers-attr](https://dequeuniversity.com/rules/axe/4.10/td-headers-attr?application=RuleDescription) | Ensure that each cell in a table that uses the headers attribute refers only to other &lt;th&gt; elements in that table | Serious | cat.tables, wcag2a, wcag131, section508, section508.22.g, TTv5, TT14.b, EN-301-549, EN-9.1.3.1 | failure, needs&nbsp;review | [a25f45](https://act-rules.github.io/rules/a25f45) |
7575
| [th-has-data-cells](https://dequeuniversity.com/rules/axe/4.10/th-has-data-cells?application=RuleDescription) | Ensure that &lt;th&gt; elements and elements with role=columnheader/rowheader have data cells they describe | Serious | cat.tables, wcag2a, wcag131, section508, section508.22.g, TTv5, TT14.b, EN-301-549, EN-9.1.3.1 | failure, needs&nbsp;review | [d0f69e](https://act-rules.github.io/rules/d0f69e) |
7676
| [valid-lang](https://dequeuniversity.com/rules/axe/4.10/valid-lang?application=RuleDescription) | Ensure lang attributes have valid values | Serious | cat.language, wcag2aa, wcag312, TTv5, TT11.b, EN-301-549, EN-9.3.1.2, ACT | failure | [de46e4](https://act-rules.github.io/rules/de46e4) |
7777
| [video-caption](https://dequeuniversity.com/rules/axe/4.10/video-caption?application=RuleDescription) | Ensure &lt;video&gt; elements have captions | Critical | cat.text-alternatives, wcag2a, wcag122, section508, section508.22.a, TTv5, TT17.a, EN-301-549, EN-9.1.2.2 | needs&nbsp;review | [eac66b](https://act-rules.github.io/rules/eac66b) |
+50-34
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,79 @@
11
import { tokenList } from '../../core/utils';
22
import { isVisibleToScreenReaders } from '../../commons/dom';
3+
import { getRole } from '../../commons/aria';
4+
5+
// Order determines the priority of reporting
6+
// Only if 0 of higher issues exists will the next be reported
7+
const messageKeys = [
8+
'cell-header-not-in-table',
9+
'cell-header-not-th',
10+
'header-refs-self',
11+
'empty-hdrs' // incomplete
12+
];
13+
const [notInTable, notTh, selfRef, emptyHdrs] = messageKeys;
314

415
export default function tdHeadersAttrEvaluate(node) {
516
const cells = [];
6-
const reviewCells = [];
7-
const badCells = [];
8-
17+
const cellRoleById = {};
918
for (let rowIndex = 0; rowIndex < node.rows.length; rowIndex++) {
1019
const row = node.rows[rowIndex];
1120

1221
for (let cellIndex = 0; cellIndex < row.cells.length; cellIndex++) {
13-
cells.push(row.cells[cellIndex]);
22+
const cell = row.cells[cellIndex];
23+
cells.push(cell);
24+
25+
// Save header id to set if it's th or td with roles columnheader/rowheader
26+
const cellId = cell.getAttribute('id');
27+
if (cellId) {
28+
cellRoleById[cellId] = getRole(cell);
29+
}
1430
}
1531
}
1632

17-
const ids = cells
18-
.filter(cell => cell.getAttribute('id'))
19-
.map(cell => cell.getAttribute('id'));
20-
33+
const badCells = {
34+
[selfRef]: new Set(),
35+
[notInTable]: new Set(),
36+
[notTh]: new Set(),
37+
[emptyHdrs]: new Set()
38+
};
2139
cells.forEach(cell => {
22-
let isSelf = false;
23-
let notOfTable = false;
24-
2540
if (!cell.hasAttribute('headers') || !isVisibleToScreenReaders(cell)) {
2641
return;
2742
}
28-
2943
const headersAttr = cell.getAttribute('headers').trim();
3044
if (!headersAttr) {
31-
return reviewCells.push(cell);
45+
badCells[emptyHdrs].add(cell);
46+
return;
3247
}
3348

49+
const cellId = cell.getAttribute('id');
3450
// Get a list all the values of the headers attribute
3551
const headers = tokenList(headersAttr);
36-
37-
if (headers.length !== 0) {
38-
// Check if the cell's id is in this list
39-
if (cell.getAttribute('id')) {
40-
isSelf = headers.indexOf(cell.getAttribute('id').trim()) !== -1;
52+
headers.forEach(headerId => {
53+
if (cellId && headerId === cellId) {
54+
// Header references its own cell
55+
badCells[selfRef].add(cell);
56+
} else if (!cellRoleById[headerId]) {
57+
// Header references a cell that is not in the table
58+
badCells[notInTable].add(cell);
59+
} else if (
60+
!['columnheader', 'rowheader'].includes(cellRoleById[headerId])
61+
) {
62+
// Header references a cell that is not a row or column header
63+
badCells[notTh].add(cell);
4164
}
65+
});
66+
});
4267

43-
// Check if the headers are of cells inside the table
44-
notOfTable = headers.some(header => !ids.includes(header));
45-
46-
if (isSelf || notOfTable) {
47-
badCells.push(cell);
68+
for (const messageKey of messageKeys) {
69+
if (badCells[messageKey].size > 0) {
70+
this.relatedNodes([...badCells[messageKey]]);
71+
if (messageKey === emptyHdrs) {
72+
return undefined;
4873
}
74+
this.data({ messageKey });
75+
return false;
4976
}
50-
});
51-
52-
if (badCells.length > 0) {
53-
this.relatedNodes(badCells);
54-
return false;
55-
}
56-
57-
if (reviewCells.length) {
58-
this.relatedNodes(reviewCells);
59-
return undefined;
6077
}
61-
6278
return true;
6379
}

lib/checks/tables/td-headers-attr.json

+6-2
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@
44
"metadata": {
55
"impact": "serious",
66
"messages": {
7-
"pass": "The headers attribute is exclusively used to refer to other cells in the table",
7+
"pass": "The headers attribute is exclusively used to refer to other header cells in the table",
88
"incomplete": "The headers attribute is empty",
9-
"fail": "The headers attribute is not exclusively used to refer to other cells in the table"
9+
"fail": {
10+
"cell-header-not-in-table": "The headers attribute is not exclusively used to refer to other header cells in the table",
11+
"cell-header-not-th": "The headers attribute must refer to header cells, not data cells",
12+
"header-refs-self": "The element with headers attribute refers to itself"
13+
}
1014
}
1115
}
1216
}

lib/rules/td-headers-attr.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
],
1717
"actIds": ["a25f45"],
1818
"metadata": {
19-
"description": "Ensure that each cell in a table that uses the headers attribute refers only to other cells in that table",
20-
"help": "Table cells that use the headers attribute must only refer to cells in the same table"
19+
"description": "Ensure that each cell in a table that uses the headers attribute refers only to other <th> elements in that table",
20+
"help": "Table cell headers attributes must refer to other <th> elements in the same table"
2121
},
2222
"all": ["td-headers-attr"],
2323
"any": [],

locales/_template.json

+8-4
Original file line numberDiff line numberDiff line change
@@ -402,8 +402,8 @@
402402
"help": "Non-empty <td> elements in larger <table> must have an associated table header"
403403
},
404404
"td-headers-attr": {
405-
"description": "Ensure that each cell in a table that uses the headers attribute refers only to other cells in that table",
406-
"help": "Table cells that use the headers attribute must only refer to cells in the same table"
405+
"description": "Ensure that each cell in a table that uses the headers attribute refers only to other <th> elements in that table",
406+
"help": "Table cell headers attributes must refer to other <th> elements in the same table"
407407
},
408408
"th-has-data-cells": {
409409
"description": "Ensure that <th> elements and elements with role=columnheader/rowheader have data cells they describe",
@@ -1096,9 +1096,13 @@
10961096
"fail": "Some non-empty data cells do not have table headers"
10971097
},
10981098
"td-headers-attr": {
1099-
"pass": "The headers attribute is exclusively used to refer to other cells in the table",
1099+
"pass": "The headers attribute is exclusively used to refer to other header cells in the table",
11001100
"incomplete": "The headers attribute is empty",
1101-
"fail": "The headers attribute is not exclusively used to refer to other cells in the table"
1101+
"fail": {
1102+
"cell-header-not-in-table": "The headers attribute is not exclusively used to refer to other header cells in the table",
1103+
"cell-header-not-th": "The headers attribute must refer to header cells, not data cells",
1104+
"header-refs-self": "The element with headers attribute refers to itself"
1105+
}
11021106
},
11031107
"th-has-data-cells": {
11041108
"pass": "All table header cells refer to data cells",

locales/ru.json

+7-3
Original file line numberDiff line numberDiff line change
@@ -402,8 +402,8 @@
402402
"help": "Непустые элементы <td> в больших таблицах должны иметь связанные заголовки таблицы"
403403
},
404404
"td-headers-attr": {
405-
"description": "Убедитесь, что каждая ячейка в таблице, использующая атрибут headers, ссылается только на другие ячейки в этой таблице",
406-
"help": "Ячейки таблицы, использующие атрибут headers, должны ссылаться только на ячейки в той же таблице"
405+
"description": "Убедитесь, что каждая ячейка в таблице, использующая атрибут headers, ссылается только на другие элементы <th> в этой таблице",
406+
"help": "Атрибуты headers ячеек таблицы должны ссылаться на другие элементы <th> в той же таблице"
407407
},
408408
"th-has-data-cells": {
409409
"description": "Убедитесь, что элементы <th> и элементы с ролью columnheader/rowheader имеют ячейки данных, которые они описывают",
@@ -1098,7 +1098,11 @@
10981098
"td-headers-attr": {
10991099
"pass": "Атрибут headers используется исключительно для ссылки на другие ячейки таблицы",
11001100
"incomplete": "Атрибут headers пуст",
1101-
"fail": "Атрибут headers не используется исключительно для ссылки на другие ячейки таблицы"
1101+
"fail": {
1102+
"cell-header-not-in-table": "Атрибут headers не используется исключительно для ссылки на другие заголовочные ячейки в таблице",
1103+
"cell-header-not-th": "Атрибут headers должен ссылаться на заголовочные ячейки, а не на ячейки с данными",
1104+
"header-refs-self": "Элемент с атрибутом headers ссылается на самого себя"
1105+
}
11021106
},
11031107
"th-has-data-cells": {
11041108
"pass": "Все ячейки заголовков таблицы ссылаются на ячейки данных",

test/checks/tables/td-headers-attr.js

+57
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,9 @@ describe('td-headers-attr', function () {
9191
);
9292
node = fixture.querySelector('table');
9393
assert.isFalse(check.call(checkContext, node));
94+
assert.deepEqual(checkContext._data, {
95+
messageKey: 'cell-header-not-in-table'
96+
});
9497

9598
fixtureSetup(
9699
'<table id="hi">' +
@@ -102,6 +105,59 @@ describe('td-headers-attr', function () {
102105
assert.isFalse(check.call(checkContext, node));
103106
});
104107

108+
it('returns false if table cell referenced as header', function () {
109+
fixtureSetup(`
110+
<table>
111+
<tr> <td id="hi">hello</td> </tr>
112+
<tr> <td headers="hi">goodbye</td> </tr>
113+
</table>
114+
`);
115+
116+
var node = fixture.querySelector('table');
117+
assert.isFalse(check.call(checkContext, node));
118+
assert.deepEqual(checkContext._data, { messageKey: 'cell-header-not-th' });
119+
});
120+
121+
it('returns true if table cell referenced as header with role rowheader or columnheader', function () {
122+
var node;
123+
124+
fixtureSetup(`
125+
<table>
126+
<tr> <td role="rowheader" id="hi">hello</td> </tr>
127+
<tr> <td headers="hi">goodbye</td> </tr>
128+
</table>
129+
`);
130+
131+
node = fixture.querySelector('table');
132+
assert.isTrue(check.call(checkContext, node));
133+
134+
fixtureSetup(`
135+
<table>
136+
<tr> <td role="columnheader" id="hi">hello</td> </tr>
137+
<tr> <td headers="hi">goodbye</td> </tr>
138+
</table>
139+
`);
140+
141+
node = fixture.querySelector('table');
142+
assert.isTrue(check.call(checkContext, node));
143+
});
144+
145+
it('relatedNodes contains each cell only once', function () {
146+
fixtureSetup(`
147+
<table>
148+
<tr> <td id="hi1">hello</td> </tr>
149+
<tr> <td id="hi2">hello</td> </tr>
150+
<tr> <td id="bye" headers="hi1 hi2">goodbye</td> </tr>
151+
</table>'
152+
`);
153+
154+
var node = fixture.querySelector('table');
155+
check.call(checkContext, node);
156+
assert.deepEqual(checkContext._relatedNodes, [
157+
fixture.querySelector('#bye')
158+
]);
159+
});
160+
105161
it('returns false if the header refers to the same cell', function () {
106162
fixtureSetup(
107163
'<table id="hi">' +
@@ -112,6 +168,7 @@ describe('td-headers-attr', function () {
112168

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

117174
it('returns true if td[headers] is hidden', function () {

test/integration/rules/td-headers-attr/td-headers-attr.html

+20
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@
1919
<td id="self" headers="self" hidden>World</td>
2020
</table>
2121

22+
<table id="pass5">
23+
<td role="rowheader" id="hdr1">Hello</td>
24+
<td headers="hdr1">World</td>
25+
</table>
26+
2227
<table id="fail1">
2328
<th id="f1h1">Hello</th>
2429
<td headers="f1h1 non-existing">World</td>
@@ -32,6 +37,21 @@
3237
<td id="self" headers="self">World</td>
3338
</table>
3439

40+
<table id="fail4">
41+
<td id="hdr1">Hello</td>
42+
<td headers="hdr1">World</td>
43+
</table>
44+
45+
<table id="fail5">
46+
<th role="cell" id="th-role-cell-hdr">Hello</th>
47+
<td headers="th-role-cell-hdr">World</td>
48+
</table>
49+
50+
<table id="fail6">
51+
<th role="button" id="th-role-button-hdr">Hello</th>
52+
<td headers="th-role-button-hdr">World</td>
53+
</table>
54+
3555
<table id="inapplicable1" role="none">
3656
<td id="self" headers="self">World</td>
3757
</table>
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
{
22
"description": "td-headers-attr test",
33
"rule": "td-headers-attr",
4-
"violations": [["#fail1"], ["#fail2"], ["#fail3"]],
5-
"passes": [["#pass1"], ["#pass2"], ["#pass3"], ["#pass4"]]
4+
"violations": [
5+
["#fail1"],
6+
["#fail2"],
7+
["#fail3"],
8+
["#fail4"],
9+
["#fail5"],
10+
["#fail6"]
11+
],
12+
"passes": [["#pass1"], ["#pass2"], ["#pass3"], ["#pass4"], ["#pass5"]]
613
}

0 commit comments

Comments
 (0)