/* * Copyright (C) 2008 Apple Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. Neither the name of Apple Inc. ("Apple") nor the names of * its contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "config.h" #include "AccessibilityTableCell.h" #include "AXObjectCache.h" #include "AccessibilityTable.h" #include "AccessibilityTableRow.h" #include "ElementIterator.h" #include "HTMLElement.h" #include "HTMLNames.h" #include "RenderObject.h" #include "RenderTableCell.h" namespace WebCore { using namespace HTMLNames; AccessibilityTableCell::AccessibilityTableCell(RenderObject* renderer) : AccessibilityRenderObject(renderer) , m_ariaColIndexFromRow(-1) { } AccessibilityTableCell::~AccessibilityTableCell() { } Ref AccessibilityTableCell::create(RenderObject* renderer) { return adoptRef(*new AccessibilityTableCell(renderer)); } bool AccessibilityTableCell::computeAccessibilityIsIgnored() const { AccessibilityObjectInclusion decision = defaultObjectInclusion(); if (decision == IncludeObject) return false; if (decision == IgnoreObject) return true; // Ignore anonymous table cells as long as they're not in a table (ie. when display:table is used). RenderObject* renderTable = is(m_renderer) ? downcast(*m_renderer).table() : nullptr; bool inTable = renderTable && renderTable->node() && (renderTable->node()->hasTagName(tableTag) || nodeHasRole(renderTable->node(), "grid")); if (!node() && !inTable) return true; if (!isTableCell()) return AccessibilityRenderObject::computeAccessibilityIsIgnored(); return false; } AccessibilityTable* AccessibilityTableCell::parentTable() const { if (!is(m_renderer)) return nullptr; // If the document no longer exists, we might not have an axObjectCache. if (!axObjectCache()) return nullptr; // Do not use getOrCreate. parentTable() can be called while the render tree is being modified // by javascript, and creating a table element may try to access the render tree while in a bad state. // By using only get() implies that the AXTable must be created before AXTableCells. This should // always be the case when AT clients access a table. // https://bugs.webkit.org/show_bug.cgi?id=42652 AccessibilityObject* parentTable = axObjectCache()->get(downcast(*m_renderer).table()); if (!is(parentTable)) return nullptr; // The RenderTableCell's table() object might be anonymous sometimes. We should handle it gracefully // by finding the right table. if (!parentTable->node()) { for (AccessibilityObject* parent = parentObject(); parent; parent = parent->parentObject()) { // If this is a non-anonymous table object, but not an accessibility table, we should stop because // we don't want to choose another ancestor table as this cell's table. if (is(*parent)) { auto& parentTable = downcast(*parent); if (parentTable.isExposableThroughAccessibility()) return &parentTable; if (parentTable.node()) break; } } return nullptr; } return downcast(parentTable); } bool AccessibilityTableCell::isTableCell() const { // If the parent table is an accessibility table, then we are a table cell. // This used to check if the unignoredParent was a row, but that exploded performance if // this was in nested tables. This check should be just as good. AccessibilityObject* parentTable = this->parentTable(); return is(parentTable) && downcast(*parentTable).isExposableThroughAccessibility(); } AccessibilityRole AccessibilityTableCell::determineAccessibilityRole() { // AccessibilityRenderObject::determineAccessibleRole provides any ARIA-supplied // role, falling back on the role to be used if we determine here that the element // should not be exposed as a cell. Thus if we already know it's a cell, return that. AccessibilityRole defaultRole = AccessibilityRenderObject::determineAccessibilityRole(); if (defaultRole == ColumnHeaderRole || defaultRole == RowHeaderRole || defaultRole == CellRole || defaultRole == GridCellRole) return defaultRole; if (!isTableCell()) return defaultRole; if (isColumnHeaderCell()) return ColumnHeaderRole; if (isRowHeaderCell()) return RowHeaderRole; return CellRole; } bool AccessibilityTableCell::isTableHeaderCell() const { return node() && node()->hasTagName(thTag); } bool AccessibilityTableCell::isColumnHeaderCell() const { const AtomicString& scope = getAttribute(scopeAttr); if (scope == "col" || scope == "colgroup") return true; if (scope == "row" || scope == "rowgroup") return false; if (!isTableHeaderCell()) return false; // We are in a situation after checking the scope attribute. // It is an attempt to resolve the type of th element without support in the specification. // Checking tableTag and tbodyTag allows to check the case of direct row placement in the table and lets stop the loop at the table level. for (Node* parentNode = node(); parentNode; parentNode = parentNode->parentNode()) { if (parentNode->hasTagName(theadTag)) return true; if (parentNode->hasTagName(tfootTag)) return false; if (parentNode->hasTagName(tableTag) || parentNode->hasTagName(tbodyTag)) { std::pair rowRange; rowIndexRange(rowRange); if (!rowRange.first) return true; return false; } } return false; } bool AccessibilityTableCell::isRowHeaderCell() const { const AtomicString& scope = getAttribute(scopeAttr); if (scope == "row" || scope == "rowgroup") return true; if (scope == "col" || scope == "colgroup") return false; if (!isTableHeaderCell()) return false; // We are in a situation after checking the scope attribute. // It is an attempt to resolve the type of th element without support in the specification. // Checking tableTag allows to check the case of direct row placement in the table and lets stop the loop at the table level. for (Node* parentNode = node(); parentNode; parentNode = parentNode->parentNode()) { if (parentNode->hasTagName(tfootTag) || parentNode->hasTagName(tbodyTag) || parentNode->hasTagName(tableTag)) { std::pair colRange; columnIndexRange(colRange); if (!colRange.first) return true; return false; } if (parentNode->hasTagName(theadTag)) return false; } return false; } bool AccessibilityTableCell::isTableCellInSameRowGroup(AccessibilityTableCell* otherTableCell) { Node* parentNode = node(); for ( ; parentNode; parentNode = parentNode->parentNode()) { if (parentNode->hasTagName(theadTag) || parentNode->hasTagName(tbodyTag) || parentNode->hasTagName(tfootTag)) break; } Node* otherParentNode = otherTableCell->node(); for ( ; otherParentNode; otherParentNode = otherParentNode->parentNode()) { if (otherParentNode->hasTagName(theadTag) || otherParentNode->hasTagName(tbodyTag) || otherParentNode->hasTagName(tfootTag)) break; } return otherParentNode == parentNode; } bool AccessibilityTableCell::isTableCellInSameColGroup(AccessibilityTableCell* tableCell) { std::pair colRange; columnIndexRange(colRange); std::pair otherColRange; tableCell->columnIndexRange(otherColRange); if (colRange.first <= (otherColRange.first + otherColRange.second)) return true; return false; } String AccessibilityTableCell::expandedTextValue() const { return getAttribute(abbrAttr); } bool AccessibilityTableCell::supportsExpandedTextValue() const { return isTableHeaderCell() && hasAttribute(abbrAttr); } void AccessibilityTableCell::columnHeaders(AccessibilityChildrenVector& headers) { AccessibilityTable* parent = parentTable(); if (!parent) return; // Choose columnHeaders as the place where the "headers" attribute is reported. ariaElementsFromAttribute(headers, headersAttr); // If the headers attribute returned valid values, then do not further search for column headers. if (!headers.isEmpty()) return; std::pair rowRange; rowIndexRange(rowRange); std::pair colRange; columnIndexRange(colRange); for (unsigned row = 0; row < rowRange.first; row++) { AccessibilityTableCell* tableCell = parent->cellForColumnAndRow(colRange.first, row); if (!tableCell || tableCell == this || headers.contains(tableCell)) continue; std::pair childRowRange; tableCell->rowIndexRange(childRowRange); const AtomicString& scope = tableCell->getAttribute(scopeAttr); if (scope == "colgroup" && isTableCellInSameColGroup(tableCell)) headers.append(tableCell); else if (tableCell->isColumnHeaderCell()) headers.append(tableCell); } } void AccessibilityTableCell::rowHeaders(AccessibilityChildrenVector& headers) { AccessibilityTable* parent = parentTable(); if (!parent) return; std::pair rowRange; rowIndexRange(rowRange); std::pair colRange; columnIndexRange(colRange); for (unsigned column = 0; column < colRange.first; column++) { AccessibilityTableCell* tableCell = parent->cellForColumnAndRow(column, rowRange.first); if (!tableCell || tableCell == this || headers.contains(tableCell)) continue; const AtomicString& scope = tableCell->getAttribute(scopeAttr); if (scope == "rowgroup" && isTableCellInSameRowGroup(tableCell)) headers.append(tableCell); else if (tableCell->isRowHeaderCell()) headers.append(tableCell); } } AccessibilityTableRow* AccessibilityTableCell::parentRow() const { AccessibilityObject* parent = parentObjectUnignored(); if (!is(*parent)) return nullptr; return downcast(parent); } void AccessibilityTableCell::rowIndexRange(std::pair& rowRange) const { if (!is(m_renderer)) return; RenderTableCell& renderCell = downcast(*m_renderer); rowRange.second = renderCell.rowSpan(); if (AccessibilityTableRow* parentRow = this->parentRow()) rowRange.first = parentRow->rowIndex(); } void AccessibilityTableCell::columnIndexRange(std::pair& columnRange) const { if (!is(m_renderer)) return; const RenderTableCell& cell = downcast(*m_renderer); columnRange.first = cell.table()->colToEffCol(cell.col()); columnRange.second = cell.table()->colToEffCol(cell.col() + cell.colSpan()) - columnRange.first; } AccessibilityObject* AccessibilityTableCell::titleUIElement() const { // Try to find if the first cell in this row is a . If it is, // then it can act as the title ui element. (This is only in the // case when the table is not appearing as an AXTable.) if (isTableCell() || !is(m_renderer)) return nullptr; // Table cells that are th cannot have title ui elements, since by definition // they are title ui elements Node* node = m_renderer->node(); if (node && node->hasTagName(thTag)) return nullptr; RenderTableCell& renderCell = downcast(*m_renderer); // If this cell is in the first column, there is no need to continue. int col = renderCell.col(); if (!col) return nullptr; int row = renderCell.rowIndex(); RenderTableSection* section = renderCell.section(); if (!section) return nullptr; RenderTableCell* headerCell = section->primaryCellAt(row, 0); if (!headerCell || headerCell == &renderCell) return nullptr; if (!headerCell->element() || !headerCell->element()->hasTagName(thTag)) return nullptr; return axObjectCache()->getOrCreate(headerCell); } int AccessibilityTableCell::ariaColumnIndex() const { const AtomicString& colIndexValue = getAttribute(aria_colindexAttr); if (colIndexValue.toInt() >= 1) return colIndexValue.toInt(); // "ARIA 1.1: If the set of columns which is present in the DOM is contiguous, and if there are no cells which span more than one row // or column in that set, then authors may place aria-colindex on each row, setting the value to the index of the first column of the set." // Here, we let its parent row to set its index beforehand, so we don't have to go through the siblings to calculate the index. AccessibilityTableRow* parentRow = this->parentRow(); if (parentRow && m_ariaColIndexFromRow != -1) return m_ariaColIndexFromRow; return -1; } int AccessibilityTableCell::ariaRowIndex() const { // ARIA 1.1: Authors should place aria-rowindex on each row. Authors may also place // aria-rowindex on all of the children or owned elements of each row. const AtomicString& rowIndexValue = getAttribute(aria_rowindexAttr); if (rowIndexValue.toInt() >= 1) return rowIndexValue.toInt(); if (AccessibilityTableRow* parentRow = this->parentRow()) return parentRow->ariaRowIndex(); return -1; } unsigned AccessibilityTableCell::ariaColumnSpan() const { const AtomicString& colSpanValue = getAttribute(aria_colspanAttr); // ARIA 1.1: Authors must set the value of aria-colspan to an integer greater than or equal to 1. if (colSpanValue.toInt() >= 1) return colSpanValue.toInt(); return 1; } unsigned AccessibilityTableCell::ariaRowSpan() const { const AtomicString& rowSpanValue = getAttribute(aria_rowspanAttr); // ARIA 1.1: Authors must set the value of aria-rowspan to an integer greater than or equal to 0. // Setting the value to 0 indicates that the cell or gridcell is to span all the remaining rows in the row group. if (rowSpanValue == "0") return 0; if (rowSpanValue.toInt() >= 1) return rowSpanValue.toInt(); return 1; } } // namespace WebCore