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,0002,000
Stationery + 18,0008,000
ItemsExpenditureOther
+ + + 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 +
ItemsExpenditureOther
Donuts + 3,0002,000
Stationery + 18,0008,000
+ + +