From 6942d2aa8526574fe35457496c6396a4b471ebad Mon Sep 17 00:00:00 2001 From: Nikita Klimov Date: Wed, 11 Sep 2024 17:09:37 +0200 Subject: [PATCH 01/18] td ref test --- test/checks/tables/td-headers-attr.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/checks/tables/td-headers-attr.js b/test/checks/tables/td-headers-attr.js index 4fe08bc0b6..25025ed1f8 100644 --- a/test/checks/tables/td-headers-attr.js +++ b/test/checks/tables/td-headers-attr.js @@ -82,6 +82,15 @@ describe('td-headers-attr', function () { node = fixture.querySelector('table'); assert.isFalse(check.call(checkContext, node)); + fixtureSetup( + '' + + ' ' + + ' ' + + '
hello
goodbye
' + ); + node = fixture.querySelector('table'); + assert.isFalse(check.call(checkContext, node)); + fixtureSetup( 'hello' + '' + From 10e58440296a763498665012151dab81db43c661 Mon Sep 17 00:00:00 2001 From: Nikita Klimov Date: Thu, 26 Sep 2024 11:04:41 +0200 Subject: [PATCH 02/18] filter td elements --- lib/checks/tables/td-headers-attr-evaluate.js | 18 +++++++++++++----- .../rules/td-headers-attr/td-headers-attr.html | 5 +++++ .../rules/td-headers-attr/td-headers-attr.json | 2 +- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/lib/checks/tables/td-headers-attr-evaluate.js b/lib/checks/tables/td-headers-attr-evaluate.js index 4275eb47cf..8cf6172c58 100644 --- a/lib/checks/tables/td-headers-attr-evaluate.js +++ b/lib/checks/tables/td-headers-attr-evaluate.js @@ -14,9 +14,17 @@ export default function tdHeadersAttrEvaluate(node) { } } - const ids = cells - .filter(cell => cell.getAttribute('id')) - .map(cell => cell.getAttribute('id')); + // Collect table headers + // Get a list all the values of the headers attributes + // Find header cells inside the table + // Filter td elements and map to ids + const tableHeaders = cells + .filter(cell => cell.getAttribute('headers')) + .flatMap(cell => tokenList(cell.getAttribute('headers').trim())) + .map(headerId => cells.find(cell => cell.getAttribute('id') === headerId)) + .filter(headerCell => headerCell) + .filter(headerCell => headerCell.nodeName.toLowerCase() !== 'td') + .map(headerCell => headerCell.getAttribute('id')); cells.forEach(cell => { let isSelf = false; @@ -40,8 +48,8 @@ export default function tdHeadersAttrEvaluate(node) { isSelf = headers.indexOf(cell.getAttribute('id').trim()) !== -1; } - // Check if the headers are of cells inside the table - notOfTable = headers.some(header => !ids.includes(header)); + // Check if the headers are of headers inside the table + notOfTable = headers.some(header => !tableHeaders.includes(header)); if (isSelf || notOfTable) { badCells.push(cell); 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..5df62c75ce 100644 --- a/test/integration/rules/td-headers-attr/td-headers-attr.html +++ b/test/integration/rules/td-headers-attr/td-headers-attr.html @@ -32,6 +32,11 @@
World
+ + + +
HelloWorld
+
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..ec700800ff 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,6 @@ { "description": "td-headers-attr test", "rule": "td-headers-attr", - "violations": [["#fail1"], ["#fail2"], ["#fail3"]], + "violations": [["#fail1"], ["#fail2"], ["#fail3"], ["#fail4"]], "passes": [["#pass1"], ["#pass2"], ["#pass3"], ["#pass4"]] } From 7942fdeed19a702449564618fb101b277baeac3a Mon Sep 17 00:00:00 2001 From: Nikita Klimov Date: Tue, 29 Oct 2024 17:15:30 +0200 Subject: [PATCH 03/18] reduce looping, exclude cells with role atts --- lib/checks/tables/td-headers-attr-evaluate.js | 50 ++++++++----------- test/checks/tables/td-headers-attr.js | 45 +++++++++++++---- .../td-headers-attr/td-headers-attr.html | 15 ++++++ .../td-headers-attr/td-headers-attr.json | 11 +++- 4 files changed, 82 insertions(+), 39 deletions(-) diff --git a/lib/checks/tables/td-headers-attr-evaluate.js b/lib/checks/tables/td-headers-attr-evaluate.js index 8cf6172c58..993cbabb9f 100644 --- a/lib/checks/tables/td-headers-attr-evaluate.js +++ b/lib/checks/tables/td-headers-attr-evaluate.js @@ -3,33 +3,32 @@ import { isVisibleToScreenReaders } from '../../commons/dom'; export default function tdHeadersAttrEvaluate(node) { const cells = []; - const reviewCells = []; const badCells = []; + const reviewCells = []; + const tableHeaders = new Set(); 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 is th or td with roles columnheader/rowheader + const cellId = cell.getAttribute('id'); + const role = cell.getAttribute('role'); + if ( + cellId && + (cell.nodeName.toLowerCase() === 'th' || + role === 'columnheader' || + role === 'rowheader') + ) { + tableHeaders.add(cellId); + } } } - // Collect table headers - // Get a list all the values of the headers attributes - // Find header cells inside the table - // Filter td elements and map to ids - const tableHeaders = cells - .filter(cell => cell.getAttribute('headers')) - .flatMap(cell => tokenList(cell.getAttribute('headers').trim())) - .map(headerId => cells.find(cell => cell.getAttribute('id') === headerId)) - .filter(headerCell => headerCell) - .filter(headerCell => headerCell.nodeName.toLowerCase() !== 'td') - .map(headerCell => headerCell.getAttribute('id')); - cells.forEach(cell => { - let isSelf = false; - let notOfTable = false; - if (!cell.hasAttribute('headers') || !isVisibleToScreenReaders(cell)) { return; } @@ -41,19 +40,14 @@ export default function tdHeadersAttrEvaluate(node) { // Get a list all the values of the headers attribute const headers = tokenList(headersAttr); + const cellId = cell.getAttribute('id'); - 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; - } - - // Check if the headers are of headers inside the table - notOfTable = headers.some(header => !tableHeaders.includes(header)); + // Check: self reference and check that headers reference an existing header + const isSelf = cellId && headers.includes(cellId); + const notOfTable = headers.some(header => !tableHeaders.has(header)); - if (isSelf || notOfTable) { - badCells.push(cell); - } + if (isSelf || notOfTable) { + badCells.push(cell); } }); diff --git a/test/checks/tables/td-headers-attr.js b/test/checks/tables/td-headers-attr.js index 25025ed1f8..35439fc45c 100644 --- a/test/checks/tables/td-headers-attr.js +++ b/test/checks/tables/td-headers-attr.js @@ -82,15 +82,6 @@ describe('td-headers-attr', function () { node = fixture.querySelector('table'); assert.isFalse(check.call(checkContext, node)); - fixtureSetup( - '' + - ' ' + - ' ' + - '
hello
goodbye
' - ); - node = fixture.querySelector('table'); - assert.isFalse(check.call(checkContext, node)); - fixtureSetup( 'hello' + '' + @@ -111,6 +102,42 @@ describe('td-headers-attr', function () { assert.isFalse(check.call(checkContext, node)); }); + it('returns false if table cell referenced as header', function () { + fixtureSetup( + '
' + + ' ' + + ' ' + + '
hello
goodbye
' + ); + + var node = fixture.querySelector('table'); + assert.isFalse(check.call(checkContext, node)); + }); + + it('returns true if table cell referenced as header with role rowheader or columnheader', function () { + var node; + + fixtureSetup( + '' + + ' ' + + ' ' + + '
hello
goodbye
' + ); + + node = fixture.querySelector('table'); + assert.isTrue(check.call(checkContext, node)); + + fixtureSetup( + '' + + ' ' + + ' ' + + '
hello
goodbye
' + ); + + node = fixture.querySelector('table'); + assert.isTrue(check.call(checkContext, node)); + }); + it('returns false if the header refers to the same cell', function () { fixtureSetup( '' + 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 5df62c75ce..55696c705b 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 @@
+ + + +
HelloWorld
+ @@ -37,6 +42,16 @@
Hello WorldWorld
+ + + +
HelloWorld
+ + + + +
HelloWorld
+
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 ec700800ff..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"], ["#fail4"]], - "passes": [["#pass1"], ["#pass2"], ["#pass3"], ["#pass4"]] + "violations": [ + ["#fail1"], + ["#fail2"], + ["#fail3"], + ["#fail4"], + ["#fail5"], + ["#fail6"] + ], + "passes": [["#pass1"], ["#pass2"], ["#pass3"], ["#pass4"], ["#pass5"]] } From 23fdbce368c34b0825edad9849600a5257df9671 Mon Sep 17 00:00:00 2001 From: Nikita Klimov Date: Wed, 30 Oct 2024 16:07:03 +0200 Subject: [PATCH 04/18] adjust error message --- lib/checks/tables/td-headers-attr-evaluate.js | 18 +++++++++++++++++- lib/checks/tables/td-headers-attr.json | 7 +++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/lib/checks/tables/td-headers-attr-evaluate.js b/lib/checks/tables/td-headers-attr-evaluate.js index 993cbabb9f..54c2d5b2c0 100644 --- a/lib/checks/tables/td-headers-attr-evaluate.js +++ b/lib/checks/tables/td-headers-attr-evaluate.js @@ -14,7 +14,7 @@ export default function tdHeadersAttrEvaluate(node) { const cell = row.cells[cellIndex]; cells.push(cell); - // Save header id to set if it is th or td with roles columnheader/rowheader + // Save header id to set if it's th or td with roles columnheader/rowheader const cellId = cell.getAttribute('id'); const role = cell.getAttribute('role'); if ( @@ -52,7 +52,23 @@ export default function tdHeadersAttrEvaluate(node) { }); if (badCells.length > 0) { + const tdInTable = badCells.some(cell => { + const headers = tokenList(cell.getAttribute('headers')); + return headers.some(header => { + const referencedCell = cells.find(c => c.getAttribute('id') === header); + return ( + referencedCell && + referencedCell.nodeName.toLowerCase() === 'td' && + !referencedCell.getAttribute('role') + ); + }); + }); + + const messageKey = tdInTable + ? 'header-is-data-cell' + : 'header-not-in-table'; this.relatedNodes(badCells); + this.data({ messageKey }); return false; } diff --git a/lib/checks/tables/td-headers-attr.json b/lib/checks/tables/td-headers-attr.json index 8013130f44..93475cd05f 100644 --- a/lib/checks/tables/td-headers-attr.json +++ b/lib/checks/tables/td-headers-attr.json @@ -4,9 +4,12 @@ "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": { + "header-not-in-table": "The headers attribute is not exclusively used to refer to other header cells in the table", + "header-is-data-cell": "The header attribute refers a data cell, this must be a header cell so that all screen readers announce it" + } } } } From de1c148f8aab85370c49eda94b2ac03a09165f5a Mon Sep 17 00:00:00 2001 From: Nikita Klimov Date: Wed, 30 Oct 2024 16:30:02 +0200 Subject: [PATCH 05/18] update locale template --- locales/_template.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/locales/_template.json b/locales/_template.json index b250b8cdaf..71629881fe 100644 --- a/locales/_template.json +++ b/locales/_template.json @@ -1096,9 +1096,12 @@ "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": { + "header-not-in-table": "The headers attribute is not exclusively used to refer to other header cells in the table", + "header-is-data-cell": "The header attribute refers a data cell, this must be a header cell so that all screen readers announce it" + } }, "th-has-data-cells": { "pass": "All table header cells refer to data cells", From 566c45b949eb43fd301bd90afcc0ed943d34026d Mon Sep 17 00:00:00 2001 From: Nikita Klimov Date: Fri, 1 Nov 2024 15:24:55 +0200 Subject: [PATCH 06/18] collect errors by keys --- lib/checks/tables/td-headers-attr-evaluate.js | 94 +++++++++---------- lib/checks/tables/td-headers-attr.json | 5 +- 2 files changed, 49 insertions(+), 50 deletions(-) diff --git a/lib/checks/tables/td-headers-attr-evaluate.js b/lib/checks/tables/td-headers-attr-evaluate.js index 54c2d5b2c0..684064c24d 100644 --- a/lib/checks/tables/td-headers-attr-evaluate.js +++ b/lib/checks/tables/td-headers-attr-evaluate.js @@ -1,12 +1,20 @@ 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 badCells = []; - const reviewCells = []; - const tableHeaders = new Set(); - + const cellRoleById = {}; for (let rowIndex = 0; rowIndex < node.rows.length; rowIndex++) { const row = node.rows[rowIndex]; @@ -16,66 +24,56 @@ export default function tdHeadersAttrEvaluate(node) { // Save header id to set if it's th or td with roles columnheader/rowheader const cellId = cell.getAttribute('id'); - const role = cell.getAttribute('role'); - if ( - cellId && - (cell.nodeName.toLowerCase() === 'th' || - role === 'columnheader' || - role === 'rowheader') - ) { - tableHeaders.add(cellId); + if (cellId) { + cellRoleById[cellId] = getRole(cell); } } } + const badCells = { + [selfRef]: [], + [notInTable]: [], + [notTh]: [], + [emptyHdrs]: [] + }; cells.forEach(cell => { 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); - const cellId = cell.getAttribute('id'); - - // Check: self reference and check that headers reference an existing header - const isSelf = cellId && headers.includes(cellId); - const notOfTable = headers.some(header => !tableHeaders.has(header)); - - if (isSelf || notOfTable) { - badCells.push(cell); - } - }); - - if (badCells.length > 0) { - const tdInTable = badCells.some(cell => { - const headers = tokenList(cell.getAttribute('headers')); - return headers.some(header => { - const referencedCell = cells.find(c => c.getAttribute('id') === header); - return ( - referencedCell && - referencedCell.nodeName.toLowerCase() === 'td' && - !referencedCell.getAttribute('role') - ); - }); + 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); + } }); + }); - const messageKey = tdInTable - ? 'header-is-data-cell' - : 'header-not-in-table'; - this.relatedNodes(badCells); - this.data({ messageKey }); - return false; - } - - if (reviewCells.length) { - this.relatedNodes(reviewCells); - return undefined; + for (const messageKey of messageKeys) { + if (badCells[messageKey].length > 0) { + this.relatedNodes(badCells[messageKey]); + if (messageKey === emptyHdrs) { + return undefined; + } + this.data({ messageKey }); + return false; + } } - return true; } diff --git a/lib/checks/tables/td-headers-attr.json b/lib/checks/tables/td-headers-attr.json index 93475cd05f..597efea1fd 100644 --- a/lib/checks/tables/td-headers-attr.json +++ b/lib/checks/tables/td-headers-attr.json @@ -7,8 +7,9 @@ "pass": "The headers attribute is exclusively used to refer to other header cells in the table", "incomplete": "The headers attribute is empty", "fail": { - "header-not-in-table": "The headers attribute is not exclusively used to refer to other header cells in the table", - "header-is-data-cell": "The header attribute refers a data cell, this must be a header cell so that all screen readers announce it" + "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" } } } From ffb2c4df46f4a5a9c743bbd4b13038bf54ade83f Mon Sep 17 00:00:00 2001 From: Nikita Klimov Date: Fri, 1 Nov 2024 15:32:06 +0200 Subject: [PATCH 07/18] update tests --- test/integration/rules/td-headers-attr/td-headers-attr.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 55696c705b..9a06f7c274 100644 --- a/test/integration/rules/td-headers-attr/td-headers-attr.html +++ b/test/integration/rules/td-headers-attr/td-headers-attr.html @@ -43,12 +43,12 @@ - +
HelloHello World
- +
HelloHello World
From 57bd7c479c1cdafb5375c831e3e9090270054a0e Mon Sep 17 00:00:00 2001 From: Nikita Klimov Date: Fri, 1 Nov 2024 15:37:46 +0200 Subject: [PATCH 08/18] update test ids --- .../rules/td-headers-attr/td-headers-attr.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 9a06f7c274..f7a6975ea3 100644 --- a/test/integration/rules/td-headers-attr/td-headers-attr.html +++ b/test/integration/rules/td-headers-attr/td-headers-attr.html @@ -43,13 +43,13 @@ - +
Hello - WorldHello + World
- +
Hello - WorldHello + World
From 4044fd987d5c6db2af0b5eda5eaa534e99bd1ec4 Mon Sep 17 00:00:00 2001 From: Nikita Klimov Date: Fri, 1 Nov 2024 15:45:30 +0200 Subject: [PATCH 09/18] update locales template --- locales/_template.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/locales/_template.json b/locales/_template.json index 71629881fe..0204112098 100644 --- a/locales/_template.json +++ b/locales/_template.json @@ -1,5 +1,4 @@ { - "lang": "xyz", "rules": { "accesskeys": { "description": "Ensure every accesskey attribute value is unique", @@ -1099,8 +1098,9 @@ "pass": "The headers attribute is exclusively used to refer to other header cells in the table", "incomplete": "The headers attribute is empty", "fail": { - "header-not-in-table": "The headers attribute is not exclusively used to refer to other header cells in the table", - "header-is-data-cell": "The header attribute refers a data cell, this must be a header cell so that all screen readers announce it" + "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": { @@ -1123,4 +1123,4 @@ } }, "incompleteFallbackMessage": "axe couldn't tell the reason. Time to break out the element inspector!" -} +} \ No newline at end of file From 39546fd598f41cbb1e4cce67331300c4d9833d6b Mon Sep 17 00:00:00 2001 From: Nikita Klimov Date: Fri, 1 Nov 2024 19:08:09 +0200 Subject: [PATCH 10/18] fix test, and func --- lib/checks/tables/td-headers-attr-evaluate.js | 2 +- test/checks/tables/td-headers-attr.js | 41 +++++++++++-------- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/lib/checks/tables/td-headers-attr-evaluate.js b/lib/checks/tables/td-headers-attr-evaluate.js index 684064c24d..d92d3c59e6 100644 --- a/lib/checks/tables/td-headers-attr-evaluate.js +++ b/lib/checks/tables/td-headers-attr-evaluate.js @@ -57,7 +57,7 @@ export default function tdHeadersAttrEvaluate(node) { // Header references a cell that is not in the table badCells[notInTable].push(cell); } else if ( - ['columnheader', 'rowheader'].includes(cellRoleById[headerId]) + !['columnheader', 'rowheader'].includes(cellRoleById[headerId]) ) { // Header references a cell that is not a row or column header badCells[notTh].push(cell); diff --git a/test/checks/tables/td-headers-attr.js b/test/checks/tables/td-headers-attr.js index 35439fc45c..c162c092e0 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( '
' + @@ -103,36 +106,37 @@ describe('td-headers-attr', function () { }); it('returns false if table cell referenced as header', function () { - fixtureSetup( - '
' + - ' ' + - ' ' + - '
hello
goodbye
' - ); + fixtureSetup(` + + + +
hello
goodbye
' + `); 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( - '' + - ' ' + - ' ' + - '
hello
goodbye
' - ); + fixtureSetup(` + + + +
hello
goodbye
+ `); node = fixture.querySelector('table'); assert.isTrue(check.call(checkContext, node)); - fixtureSetup( - '' + - ' ' + - ' ' + - '
hello
goodbye
' - ); + fixtureSetup(` + + + +
hello
goodbye
+ `); node = fixture.querySelector('table'); assert.isTrue(check.call(checkContext, node)); @@ -148,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 () { From cacbb30364e7032ed5833dec757edecaefd601be Mon Sep 17 00:00:00 2001 From: Nikita Klimov Date: Tue, 12 Nov 2024 13:09:13 +0200 Subject: [PATCH 11/18] typo --- test/checks/tables/td-headers-attr.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/checks/tables/td-headers-attr.js b/test/checks/tables/td-headers-attr.js index c162c092e0..713f984b2e 100644 --- a/test/checks/tables/td-headers-attr.js +++ b/test/checks/tables/td-headers-attr.js @@ -110,7 +110,7 @@ describe('td-headers-attr', function () { -
hello
goodbye
' + `); var node = fixture.querySelector('table'); From effb9554cced660473c080c6dda659c2f1433d63 Mon Sep 17 00:00:00 2001 From: Nikita Klimov Date: Tue, 12 Nov 2024 20:27:56 +0200 Subject: [PATCH 12/18] adjust messages --- lib/checks/tables/td-headers-attr.json | 2 +- lib/rules/td-headers-attr.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/checks/tables/td-headers-attr.json b/lib/checks/tables/td-headers-attr.json index 597efea1fd..75d42206b4 100644 --- a/lib/checks/tables/td-headers-attr.json +++ b/lib/checks/tables/td-headers-attr.json @@ -8,7 +8,7 @@ "incomplete": "The headers attribute is empty", "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", + "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..629dc6406e 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 using the headers attribute refers only to header cells within that table", + "help": "Table cells using the headers attribute must refer only to header cells or data cells with the role=columnheader/rowheader within the same table." }, "all": ["td-headers-attr"], "any": [], From 39fb1b1061f22ee9a7dc09cae25b8dcee0e195f0 Mon Sep 17 00:00:00 2001 From: Nikita Klimov Date: Tue, 12 Nov 2024 20:56:10 +0200 Subject: [PATCH 13/18] update locale template --- locales/_template.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/locales/_template.json b/locales/_template.json index 0204112098..1f112e7028 100644 --- a/locales/_template.json +++ b/locales/_template.json @@ -401,8 +401,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 using the headers attribute refers only to header cells within that table", + "help": "Table cells using the headers attribute must refer only to header cells or data cells with the role=columnheader/rowheader within the same table." }, "th-has-data-cells": { "description": "Ensure that
elements and elements with role=columnheader/rowheader have data cells they describe", @@ -1099,7 +1099,7 @@ "incomplete": "The headers attribute is empty", "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", + "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" } }, @@ -1123,4 +1123,4 @@ } }, "incompleteFallbackMessage": "axe couldn't tell the reason. Time to break out the element inspector!" -} \ No newline at end of file +} From 9cac9ee1cf3e9b618f51147e22444c4dddadbef5 Mon Sep 17 00:00:00 2001 From: Nikita Klimov Date: Tue, 12 Nov 2024 21:08:00 +0200 Subject: [PATCH 14/18] fi ru locale --- locales/ru.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/locales/ru.json b/locales/ru.json index b161d7d40d..f432d300aa 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -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": "Все ячейки заголовков таблицы ссылаются на ячейки данных", From 17220539c689f56e5cad8903ad4ced70f84ab64a Mon Sep 17 00:00:00 2001 From: Nikita Klimov Date: Tue, 12 Nov 2024 22:19:12 +0200 Subject: [PATCH 15/18] upd docs --- doc/rule-descriptions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/rule-descriptions.md b/doc/rule-descriptions.md index 88e5e5f57d..094a9775ba 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 using the headers attribute refers only to header cells within 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) | From 5d1440e4e0e702d940d9ae146b2b3a451760b1bc Mon Sep 17 00:00:00 2001 From: Nikita Klimov Date: Tue, 12 Nov 2024 22:44:16 +0200 Subject: [PATCH 16/18] replace badcells arr with set --- lib/checks/tables/td-headers-attr-evaluate.js | 20 +++++++++---------- test/checks/tables/td-headers-attr.js | 16 +++++++++++++++ 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/lib/checks/tables/td-headers-attr-evaluate.js b/lib/checks/tables/td-headers-attr-evaluate.js index d92d3c59e6..14f48052f1 100644 --- a/lib/checks/tables/td-headers-attr-evaluate.js +++ b/lib/checks/tables/td-headers-attr-evaluate.js @@ -31,10 +31,10 @@ export default function tdHeadersAttrEvaluate(node) { } const badCells = { - [selfRef]: [], - [notInTable]: [], - [notTh]: [], - [emptyHdrs]: [] + [selfRef]: new Set(), + [notInTable]: new Set(), + [notTh]: new Set(), + [emptyHdrs]: new Set() }; cells.forEach(cell => { if (!cell.hasAttribute('headers') || !isVisibleToScreenReaders(cell)) { @@ -42,7 +42,7 @@ export default function tdHeadersAttrEvaluate(node) { } const headersAttr = cell.getAttribute('headers').trim(); if (!headersAttr) { - badCells[emptyHdrs].push(cell); + badCells[emptyHdrs].add(cell); return; } @@ -52,22 +52,22 @@ export default function tdHeadersAttrEvaluate(node) { headers.forEach(headerId => { if (cellId && headerId === cellId) { // Header references its own cell - badCells[selfRef].push(cell); + badCells[selfRef].add(cell); } else if (!cellRoleById[headerId]) { // Header references a cell that is not in the table - badCells[notInTable].push(cell); + 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].push(cell); + badCells[notTh].add(cell); } }); }); for (const messageKey of messageKeys) { - if (badCells[messageKey].length > 0) { - this.relatedNodes(badCells[messageKey]); + if (badCells[messageKey].size > 0) { + this.relatedNodes([...badCells[messageKey]]); if (messageKey === emptyHdrs) { return undefined; } diff --git a/test/checks/tables/td-headers-attr.js b/test/checks/tables/td-headers-attr.js index 713f984b2e..c12cf33546 100644 --- a/test/checks/tables/td-headers-attr.js +++ b/test/checks/tables/td-headers-attr.js @@ -142,6 +142,22 @@ describe('td-headers-attr', function () { 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( '' + From e467de5c7c94fe42f53967f176a0fb4d32751bc7 Mon Sep 17 00:00:00 2001 From: Nikita Klimov Date: Thu, 14 Nov 2024 15:15:04 +0200 Subject: [PATCH 17/18] update rules text --- doc/rule-descriptions.md | 2 +- lib/rules/td-headers-attr.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/rule-descriptions.md b/doc/rule-descriptions.md index 094a9775ba..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 using the headers attribute refers only to header cells within 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/rules/td-headers-attr.json b/lib/rules/td-headers-attr.json index 629dc6406e..efaa407289 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 using the headers attribute refers only to header cells within that table", - "help": "Table cells using the headers attribute must refer only to header cells or data cells with the role=columnheader/rowheader within 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": [], From 4897ecc932c1c5a9609c8668a5f74e8c4c60c69f Mon Sep 17 00:00:00 2001 From: Nikita Klimov Date: Fri, 15 Nov 2024 17:00:15 +0200 Subject: [PATCH 18/18] fix linter, run build --- .prettierignore | 1 + locales/_template.json | 5 +++-- locales/ru.json | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.prettierignore b/.prettierignore index 2bd1eb1157..6144376615 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,2 +1,3 @@ node_modules/ doc/api +test/integration/rules/td-headers-attr/td-headers-attr.html diff --git a/locales/_template.json b/locales/_template.json index 1f112e7028..57da154737 100644 --- a/locales/_template.json +++ b/locales/_template.json @@ -1,4 +1,5 @@ { + "lang": "xyz", "rules": { "accesskeys": { "description": "Ensure every accesskey attribute value is unique", @@ -401,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 using the headers attribute refers only to header cells within that table", - "help": "Table cells using the headers attribute must refer only to header cells or data cells with the role=columnheader/rowheader within 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", diff --git a/locales/ru.json b/locales/ru.json index f432d300aa..2e0554c0a8 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 имеют ячейки данных, которые они описывают",