diff --git a/accessibility-checker-engine/src/v2/checker/accessibility/util/legacy.ts b/accessibility-checker-engine/src/v2/checker/accessibility/util/legacy.ts
index 0367d519a..8281bda33 100644
--- a/accessibility-checker-engine/src/v2/checker/accessibility/util/legacy.ts
+++ b/accessibility-checker-engine/src/v2/checker/accessibility/util/legacy.ts
@@ -1097,7 +1097,7 @@ export class RPTUtil {
}
if (!isComplexTable && trNodeCount !== 0) {
// a table with headers not located in the first row or first column
- isComplexTable = thNodeCount > 0 && !RPTUtil.isTableHeaderInFirstRowOrColumn(table);
+ isComplexTable = thNodeCount > 0 && !RPTUtil.tableHeaderExists(table);
}
}
table.RPTUtil_isComplexDataTable = isComplexTable;
@@ -1105,52 +1105,108 @@ export class RPTUtil {
return isComplexTable;
}
+ // Return true if a table cell is hidden or contain no data:
|
+ public static isTableCellEmpty(cell) {
+ if (!cell || !VisUtil.isNodeVisible(cell) || cell.innerHTML.trim().length === 0)
+ return true;
+
+ return false;
+ }
+
+ // Return true if a table row is hidden or contain no data:
or | |
+ public static isTableRowEmpty(row) {
+ if (!row || !row.cells || row.cells.length === 0 || !VisUtil.isNodeVisible(row))
+ return true;
+
+ let passed = true; //empty
+ for (let c=0; passed && c < row.cells.length; c++) {
+ let cell = row.cells[c];
+ passed = RPTUtil.isTableCellEmpty(cell);
+ }
+
+ return passed;
+ }
+
// Return true if a table's header is in the first row or column
- public static isTableHeaderInFirstRowOrColumn(ruleContext) {
+ public static tableHeaderExists(ruleContext) {
- let passed = true;
let rows = ruleContext.rows;
+ if (!rows || rows.length === 0)
+ return null;
+
+ //case 1: header is in the very first row with data
+ //get the first row with data
+ let passed = true;
+ let firstRow = rows[0];
+ for (let r=0; passed && r < rows.length; r++) {
+ firstRow = rows[r];
+ passed = RPTUtil.isTableRowEmpty(firstRow);
+ }
- if (rows != null && rows.length > 0) {
- let firstRow = rows[0];
- // Check if the cells with data in the first row are all TH's
- //passed = firstRow.cells.length > 0 && RPTUtil.getChildByTagHidden(firstRow, "td", false, true).length === 0;
- if (!firstRow.cells)
- passed = false;
- else {
- for (let c=0; passed && c < firstRow.cells.length; c++) {
- let cell = firstRow.cells[c];
- passed = !VisUtil.isNodeVisible(cell) || (cell.innerHTML.trim().length > 0 && cell.nodeName.toLowerCase() === 'th');
- }
- }
-
- // If the first row isn't a header row, try the first column
- if (!passed) {
- // Assume that the first column has all TH's or a TD without data in the first column.
- passed = true;
- for (let i = 0; passed && i < rows.length; ++i) {
- // ignore the rows from tfoot
- if (rows[i].parentNode && rows[i].parentNode.nodeName.toLowerCase() === 'tfoot') continue;
- // If no cells in this row, or no data at all, that's okay too.
- passed = !rows[i].cells ||
- rows[i].cells.length === 0 ||
- rows[i].cells[0].innerHTML.trim().length === 0 ||
- rows[i].cells[0].nodeName.toLowerCase() != "td";
- }
- }
- if (!passed) {
- // Special case - both first row and first column are headers, but they did not use
- // a th for the upper-left cell
- passed = true;
- for (let i = 1; passed && i < firstRow.cells.length; ++i) {
- passed = firstRow.cells[i].nodeName.toLowerCase() != "td";
- }
- for (let i = 1; passed && i < rows.length; ++i) {
- // If no cells in this row, that's okay too.
- passed = !rows[i].cells ||
- rows[i].cells.length === 0 ||
- rows[i].cells[0].nodeName.toLowerCase() != "td";
- }
+ //table contain no data:
+ if (passed)
+ return null;
+
+ // Check if the cells with data in the first data row are all TH's
+ //passed = firstRow.cells.length > 0 && RPTUtil.getChildByTagHidden(firstRow, "td", false, true).length === 0;
+ passed = true;
+ for (let c=0; passed && c < firstRow.cells.length; c++) {
+ let cell = firstRow.cells[c];
+ passed = !RPTUtil.isTableCellEmpty(cell) && cell.nodeName.toLowerCase() === 'th';
+ }
+
+ if (passed)
+ return true;
+
+ //case 2: header is in the very last/bottom row with data
+ //get the last row with data
+ passed = true;
+ let lastRow = rows[rows.length-1];
+ for (let r=rows.length; passed && r >= 0; r++) {
+ lastRow = rows[r];
+ passed = RPTUtil.isTableRowEmpty(lastRow);
+ }
+
+ if (passed) //shouldn't happen!
+ return true;
+
+ // Check if the cells with data in the first data row are all TH's
+ //passed = firstRow.cells.length > 0 && RPTUtil.getChildByTagHidden(firstRow, "td", false, true).length === 0;
+ passed = true;
+ for (let c=0; passed && c < lastRow.cells.length; c++) {
+ let cell = lastRow.cells[c];
+ passed = !RPTUtil.isTableCellEmpty(cell) && cell.nodeName.toLowerCase() === 'th';
+ }
+
+ if (passed)
+ return true;
+
+ // Case 3: header is in the first data columns
+ // Assume that the first column has all TH's or a TD without data in the first column.
+ passed = true;
+ for (let i = 0; passed && i < rows.length; ++i) {
+ // ignore the rows from tfoot
+ if (rows[i].parentNode && rows[i].parentNode.nodeName.toLowerCase() === 'tfoot') continue;
+ // If no cells in this row, or no data at all, that's okay too.
+ passed = !rows[i].cells ||
+ rows[i].cells.length === 0 ||
+ rows[i].cells[0].innerHTML.trim().length === 0 ||
+ rows[i].cells[0].nodeName.toLowerCase() != "td";
+ }
+
+
+ if (!passed) {
+ // Special case - both first row and first column are headers, but they did not use
+ // a th for the upper-left cell
+ passed = true;
+ for (let i = 1; passed && i < firstRow.cells.length; ++i) {
+ passed = firstRow.cells[i].nodeName.toLowerCase() != "td";
+ }
+ for (let i = 1; passed && i < rows.length; ++i) {
+ // If no cells in this row, that's okay too.
+ passed = !rows[i].cells ||
+ rows[i].cells.length === 0 ||
+ rows[i].cells[0].nodeName.toLowerCase() != "td";
}
}
return passed;
diff --git a/accessibility-checker-engine/src/v4/rules/table_headers_exists.ts b/accessibility-checker-engine/src/v4/rules/table_headers_exists.ts
index 0c1fefcd1..d66d9ffd1 100644
--- a/accessibility-checker-engine/src/v4/rules/table_headers_exists.ts
+++ b/accessibility-checker-engine/src/v4/rules/table_headers_exists.ts
@@ -48,11 +48,13 @@ export let table_headers_exists: Rule = {
const ruleContext = context["dom"].node as HTMLTableElement;
// If this is a layout table or there are no rows, the rule does not apply.
let rows = ruleContext.rows;
- if (!RPTUtil.isDataTable(ruleContext) || rows == null || rows.length == 0)
+ if (!RPTUtil.isDataTable(ruleContext) || rows === null || rows.length === 0)
return null;
- let passed = RPTUtil.isTableHeaderInFirstRowOrColumn(ruleContext);
-
+ let passed = RPTUtil.tableHeaderExists(ruleContext);
+ if (passed === null)
+ return;
+
if (!passed) {
return RuleFail("Fail_1");
} else {
diff --git a/accessibility-checker-engine/test/v2/checker/accessibility/rules/table_headers_exists_ruleunit/table_empty.htm b/accessibility-checker-engine/test/v2/checker/accessibility/rules/table_headers_exists_ruleunit/table_empty.htm
new file mode 100755
index 000000000..2fdf94257
--- /dev/null
+++ b/accessibility-checker-engine/test/v2/checker/accessibility/rules/table_headers_exists_ruleunit/table_empty.htm
@@ -0,0 +1,43 @@
+
+
+
+
+
+Data table
+
+
+
+
+
+
+
+
+
diff --git a/accessibility-checker-engine/test/v2/checker/accessibility/rules/table_headers_exists_ruleunit/table_row_headers_bottom_tr_empty.htm b/accessibility-checker-engine/test/v2/checker/accessibility/rules/table_headers_exists_ruleunit/table_row_headers_bottom_tr_empty.htm
new file mode 100755
index 000000000..a82bcfa3e
--- /dev/null
+++ b/accessibility-checker-engine/test/v2/checker/accessibility/rules/table_headers_exists_ruleunit/table_row_headers_bottom_tr_empty.htm
@@ -0,0 +1,73 @@
+
+
+
+
+
+Data table
+
+
+
+
+ Council budget 2018
+
+
+
+ Donuts
+ | 3,000 |
+ 2,000 |
+
+
+ Stationery
+ | 18,000 |
+ 8,000 |
+
+
+ Items |
+ Expenditure |
+ Other |
+
+ | | |
+ | | |
+
+
+
+
+
diff --git a/accessibility-checker-engine/test/v2/checker/accessibility/rules/table_headers_exists_ruleunit/table_row_headers_top_tr_empty.htm b/accessibility-checker-engine/test/v2/checker/accessibility/rules/table_headers_exists_ruleunit/table_row_headers_top_tr_empty.htm
new file mode 100755
index 000000000..2b027cf60
--- /dev/null
+++ b/accessibility-checker-engine/test/v2/checker/accessibility/rules/table_headers_exists_ruleunit/table_row_headers_top_tr_empty.htm
@@ -0,0 +1,73 @@
+
+
+
+
+
+Data table
+
+
+
+
+ Council budget 2018
+
+
+ | | |
+ | | |
+
+ Items |
+ Expenditure |
+ Other |
+
+
+ Donuts
+ | 3,000 |
+ 2,000 |
+
+
+ Stationery
+ | 18,000 |
+ 8,000 |
+
+
+
+
+
+