diff --git a/doc/rule-descriptions.md b/doc/rule-descriptions.md
index 88e5e5f57d..d6ff97a26a 100644
--- a/doc/rule-descriptions.md
+++ b/doc/rule-descriptions.md
@@ -71,7 +71,7 @@
| [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 review | |
| [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 review | |
| [svg-img-alt](https://dequeuniversity.com/rules/axe/4.10/svg-img-alt?application=RuleDescription) | Ensure <svg> 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 review | [7d6734](https://act-rules.github.io/rules/7d6734) |
-| [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 review | [a25f45](https://act-rules.github.io/rules/a25f45) |
+| [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 <th> 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 review | [a25f45](https://act-rules.github.io/rules/a25f45) |
| [th-has-data-cells](https://dequeuniversity.com/rules/axe/4.10/th-has-data-cells?application=RuleDescription) | Ensure that <th> 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 review | [d0f69e](https://act-rules.github.io/rules/d0f69e) |
| [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) |
| [video-caption](https://dequeuniversity.com/rules/axe/4.10/video-caption?application=RuleDescription) | Ensure <video> 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 review | [eac66b](https://act-rules.github.io/rules/eac66b) |
diff --git a/lib/checks/tables/td-headers-attr-evaluate.js b/lib/checks/tables/td-headers-attr-evaluate.js
index 4275eb47cf..14f48052f1 100644
--- a/lib/checks/tables/td-headers-attr-evaluate.js
+++ b/lib/checks/tables/td-headers-attr-evaluate.js
@@ -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]: new Set(),
+ [notInTable]: new Set(),
+ [notTh]: new Set(),
+ [emptyHdrs]: new Set()
+ };
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].add(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].add(cell);
+ } else if (!cellRoleById[headerId]) {
+ // Header references a cell that is not in the table
+ badCells[notInTable].add(cell);
+ } else if (
+ !['columnheader', 'rowheader'].includes(cellRoleById[headerId])
+ ) {
+ // Header references a cell that is not a row or column header
+ badCells[notTh].add(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].size > 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;
}
diff --git a/lib/checks/tables/td-headers-attr.json b/lib/checks/tables/td-headers-attr.json
index 8013130f44..75d42206b4 100644
--- a/lib/checks/tables/td-headers-attr.json
+++ b/lib/checks/tables/td-headers-attr.json
@@ -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 headers attribute must refer to header cells, not data cells",
+ "header-refs-self": "The element with headers attribute refers to itself"
+ }
}
}
}
diff --git a/lib/rules/td-headers-attr.json b/lib/rules/td-headers-attr.json
index b729c6acd0..22d7dd2488 100644
--- a/lib/rules/td-headers-attr.json
+++ b/lib/rules/td-headers-attr.json
@@ -16,8 +16,8 @@
],
"actIds": ["a25f45"],
"metadata": {
- "description": "Ensure that each cell in a table that uses the headers attribute refers only to other cells in that table",
- "help": "Table cells that use the headers attribute must only refer to cells in the same table"
+ "description": "Ensure that each cell in a table that uses the headers attribute refers only to other
elements in that table",
+ "help": "Table cell headers attributes must refer to other | elements in the same table"
},
"all": ["td-headers-attr"],
"any": [],
diff --git a/locales/_template.json b/locales/_template.json
index b250b8cdaf..a607fb058a 100644
--- a/locales/_template.json
+++ b/locales/_template.json
@@ -402,8 +402,8 @@
"help": "Non-empty | elements in larger must have an associated table header"
},
"td-headers-attr": {
- "description": "Ensure that each cell in a table that uses the headers attribute refers only to other cells in that table",
- "help": "Table cells that use the headers attribute must only refer to cells in the same table"
+ "description": "Ensure that each cell in a table that uses the headers attribute refers only to other elements in that table",
+ "help": "Table cell headers attributes must refer to other | elements in the same table"
},
"th-has-data-cells": {
"description": "Ensure that | elements and elements with role=columnheader/rowheader have data cells they describe",
@@ -1096,9 +1096,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 headers attribute must refer to header cells, not data cells",
+ "header-refs-self": "The element with headers attribute refers to itself"
+ }
},
"th-has-data-cells": {
"pass": "All table header cells refer to data cells",
diff --git a/locales/ru.json b/locales/ru.json
index b161d7d40d..29c80cee50 100644
--- a/locales/ru.json
+++ b/locales/ru.json
@@ -402,8 +402,8 @@
"help": "Непустые элементы | в больших таблицах должны иметь связанные заголовки таблицы"
},
"td-headers-attr": {
- "description": "Убедитесь, что каждая ячейка в таблице, использующая атрибут headers, ссылается только на другие ячейки в этой таблице",
- "help": "Ячейки таблицы, использующие атрибут headers, должны ссылаться только на ячейки в той же таблице"
+ "description": "Убедитесь, что каждая ячейка в таблице, использующая атрибут headers, ссылается только на другие элементы | в этой таблице",
+ "help": "Атрибуты headers ячеек таблицы должны ссылаться на другие элементы | в той же таблице"
},
"th-has-data-cells": {
"description": "Убедитесь, что элементы | и элементы с ролью columnheader/rowheader имеют ячейки данных, которые они описывают",
@@ -1098,7 +1098,11 @@
"td-headers-attr": {
"pass": "Атрибут headers используется исключительно для ссылки на другие ячейки таблицы",
"incomplete": "Атрибут headers пуст",
- "fail": "Атрибут headers не используется исключительно для ссылки на другие ячейки таблицы"
+ "fail": {
+ "cell-header-not-in-table": "Атрибут headers не используется исключительно для ссылки на другие заголовочные ячейки в таблице",
+ "cell-header-not-th": "Атрибут headers должен ссылаться на заголовочные ячейки, а не на ячейки с данными",
+ "header-refs-self": "Элемент с атрибутом headers ссылается на самого себя"
+ }
},
"th-has-data-cells": {
"pass": "Все ячейки заголовков таблицы ссылаются на ячейки данных",
diff --git a/test/checks/tables/td-headers-attr.js b/test/checks/tables/td-headers-attr.js
index 4fe08bc0b6..c12cf33546 100644
--- a/test/checks/tables/td-headers-attr.js
+++ b/test/checks/tables/td-headers-attr.js
@@ -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(
'' +
@@ -102,6 +105,59 @@ describe('td-headers-attr', function () {
assert.isFalse(check.call(checkContext, node));
});
+ it('returns false if table cell referenced as header', function () {
+ fixtureSetup(`
+
+ `);
+
+ 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(`
+
+ `);
+
+ node = fixture.querySelector('table');
+ assert.isTrue(check.call(checkContext, node));
+
+ fixtureSetup(`
+
+ `);
+
+ node = fixture.querySelector('table');
+ assert.isTrue(check.call(checkContext, node));
+ });
+
+ it('relatedNodes contains each cell only once', function () {
+ fixtureSetup(`
+
+ hello |
+ hello |
+ goodbye |
+ '
+ `);
+
+ var node = fixture.querySelector('table');
+ check.call(checkContext, node);
+ assert.deepEqual(checkContext._relatedNodes, [
+ fixture.querySelector('#bye')
+ ]);
+ });
+
it('returns false if the header refers to the same cell', function () {
fixtureSetup(
'' +
@@ -112,6 +168,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 () {
diff --git a/test/integration/rules/td-headers-attr/td-headers-attr.html b/test/integration/rules/td-headers-attr/td-headers-attr.html
index 64fc6cb33c..df4543b7f5 100644
--- a/test/integration/rules/td-headers-attr/td-headers-attr.html
+++ b/test/integration/rules/td-headers-attr/td-headers-attr.html
@@ -19,6 +19,11 @@
World |
+
+
Hello |
World |
@@ -32,6 +37,21 @@
World |
+
+
+
+
+
+
diff --git a/test/integration/rules/td-headers-attr/td-headers-attr.json b/test/integration/rules/td-headers-attr/td-headers-attr.json
index e687869b6b..093cc82ddc 100644
--- a/test/integration/rules/td-headers-attr/td-headers-attr.json
+++ b/test/integration/rules/td-headers-attr/td-headers-attr.json
@@ -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"]]
}
| |