diff --git a/lib/rules/table-or-grid-role-matches.js b/lib/rules/table-or-grid-role-matches.js
new file mode 100644
index 0000000000..1443b0acdf
--- /dev/null
+++ b/lib/rules/table-or-grid-role-matches.js
@@ -0,0 +1,6 @@
+import { getRole } from '../commons/aria';
+
+export default function tableOrGridRoleMatches(_, vNode) {
+ const role = getRole(vNode);
+ return ['treegrid', 'grid', 'table'].includes(role);
+}
diff --git a/lib/rules/td-headers-attr.json b/lib/rules/td-headers-attr.json
index b3104d0f7c..141c4a0933 100644
--- a/lib/rules/td-headers-attr.json
+++ b/lib/rules/td-headers-attr.json
@@ -1,6 +1,7 @@
{
"id": "td-headers-attr",
"selector": "table",
+ "matches": "table-or-grid-role-matches",
"tags": ["cat.tables", "wcag2a", "wcag131", "section508", "section508.22.g"],
"actIds": ["a25f45"],
"metadata": {
diff --git a/test/checks/tables/td-headers-attr.js b/test/checks/tables/td-headers-attr.js
index f9ba57dd84..8df3cc03e3 100644
--- a/test/checks/tables/td-headers-attr.js
+++ b/test/checks/tables/td-headers-attr.js
@@ -1,15 +1,15 @@
-describe('td-headers-attr', function() {
+describe('td-headers-attr', function () {
'use strict';
var fixture = document.getElementById('fixture');
var checkContext = axe.testUtils.MockCheckContext();
- afterEach(function() {
+ afterEach(function () {
fixture.innerHTML = '';
checkContext.reset();
});
- it('returns true no headers attribute is present', function() {
+ it('returns true no headers attribute is present', function () {
fixture.innerHTML =
'
' +
' hi | hello |
' +
@@ -22,7 +22,7 @@ describe('td-headers-attr', function() {
);
});
- it('returns true if a valid header is present', function() {
+ it('returns true if a valid header is present', function () {
fixture.innerHTML =
'' +
' hello |
' +
@@ -35,7 +35,7 @@ describe('td-headers-attr', function() {
);
});
- it('returns true if multiple valid headers are present', function() {
+ it('returns true if multiple valid headers are present', function () {
fixture.innerHTML =
'' +
' hello | hello |
' +
@@ -48,7 +48,7 @@ describe('td-headers-attr', function() {
);
});
- it('returns true with an empty header', function() {
+ it('returns true with an empty header', function () {
fixture.innerHTML =
'' +
' |
' +
@@ -61,7 +61,7 @@ describe('td-headers-attr', function() {
);
});
- it('returns undefined if headers is empty', function() {
+ it('returns undefined if headers is empty', function () {
fixture.innerHTML =
'' +
' |
' +
@@ -74,7 +74,7 @@ describe('td-headers-attr', function() {
);
});
- it('returns false if the header is a table cell', function() {
+ it('returns false if the header is a table cell', function () {
var node;
fixture.innerHTML =
@@ -109,7 +109,7 @@ describe('td-headers-attr', function() {
);
});
- it('returns false if the header refers to the same cell', function() {
+ it('returns false if the header refers to the same cell', function () {
fixture.innerHTML =
'' +
' hello |
' +
diff --git a/test/integration/rules/td-headers-attr/td-headers-attr.html b/test/integration/rules/td-headers-attr/td-headers-attr.html
new file mode 100644
index 0000000000..c0afe4060a
--- /dev/null
+++ b/test/integration/rules/td-headers-attr/td-headers-attr.html
@@ -0,0 +1,40 @@
+
+
+
+
+
+ Hello |
+ Hello |
+ 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
new file mode 100644
index 0000000000..cda7bf99cb
--- /dev/null
+++ b/test/integration/rules/td-headers-attr/td-headers-attr.json
@@ -0,0 +1,6 @@
+{
+ "description": "td-headers-attr test",
+ "rule": "td-headers-attr",
+ "violations": [["#fail1"], ["#fail2"], ["#fail3"]],
+ "passes": [["#pass1"], ["#pass2"], ["#pass3"]]
+}
diff --git a/test/rule-matches/table-or-grid-role-matches.js b/test/rule-matches/table-or-grid-role-matches.js
new file mode 100644
index 0000000000..9d196ec3f1
--- /dev/null
+++ b/test/rule-matches/table-or-grid-role-matches.js
@@ -0,0 +1,51 @@
+describe('table-or-grid-role-matches', () => {
+ const { queryFixture } = axe.testUtils;
+ const rule = axe.utils.getRule('td-headers-attr');
+
+ it(`returns true for tables without role`, () => {
+ const vNode = queryFixture(``);
+ assert.isTrue(rule.matches(vNode.actualNode, vNode));
+ });
+
+ ['table', 'grid', 'treegrid'].forEach(role => {
+ it(`returns true for tables with role=${role}`, () => {
+ const vNode = queryFixture(``);
+ assert.isTrue(rule.matches(vNode.actualNode, vNode));
+ });
+ });
+
+ ['region', 'presentation', 'none'].forEach(role => {
+ it(`returns false for tables with role=${role}`, () => {
+ const vNode = queryFixture(``);
+ assert.isFalse(rule.matches(vNode.actualNode, vNode));
+ });
+ });
+
+ it(`returns true for tables with an invalid role`, () => {
+ const vNode = queryFixture(``);
+ assert.isTrue(rule.matches(vNode.actualNode, vNode));
+ });
+
+ it(`returns true for focusable tables with role=none`, () => {
+ const vNode = queryFixture(``);
+ assert.isTrue(rule.matches(vNode.actualNode, vNode));
+ });
+
+ it(`returns true for tables with role=none but with a global ARIA attribute`, () => {
+ const vNode =
+ queryFixture(``);
+ assert.isTrue(rule.matches(vNode.actualNode, vNode));
+ });
+});