Home | History | Annotate | Download | only in accessibility
      1 /*
      2  * Copyright (C) 2008 Apple Inc. All rights reserved.
      3  *
      4  * Redistribution and use in source and binary forms, with or without
      5  * modification, are permitted provided that the following conditions
      6  * are met:
      7  *
      8  * 1.  Redistributions of source code must retain the above copyright
      9  *     notice, this list of conditions and the following disclaimer.
     10  * 2.  Redistributions in binary form must reproduce the above copyright
     11  *     notice, this list of conditions and the following disclaimer in the
     12  *     documentation and/or other materials provided with the distribution.
     13  * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
     14  *     its contributors may be used to endorse or promote products derived
     15  *     from this software without specific prior written permission.
     16  *
     17  * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
     18  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
     19  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
     20  * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
     21  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
     22  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
     23  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
     24  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     25  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
     26  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     27  */
     28 
     29 #include "config.h"
     30 #include "core/accessibility/AXTable.h"
     31 
     32 #include "core/accessibility/AXObjectCache.h"
     33 #include "core/accessibility/AXTableCell.h"
     34 #include "core/accessibility/AXTableColumn.h"
     35 #include "core/accessibility/AXTableRow.h"
     36 #include "core/dom/ElementTraversal.h"
     37 #include "core/html/HTMLCollection.h"
     38 #include "core/html/HTMLTableCaptionElement.h"
     39 #include "core/html/HTMLTableCellElement.h"
     40 #include "core/html/HTMLTableColElement.h"
     41 #include "core/html/HTMLTableElement.h"
     42 #include "core/html/HTMLTableRowElement.h"
     43 #include "core/html/HTMLTableRowsCollection.h"
     44 #include "core/html/HTMLTableSectionElement.h"
     45 #include "core/rendering/RenderTableCell.h"
     46 
     47 namespace blink {
     48 
     49 using namespace HTMLNames;
     50 
     51 AXTable::AXTable(RenderObject* renderer)
     52     : AXRenderObject(renderer)
     53     , m_headerContainer(nullptr)
     54     , m_isAXTable(true)
     55 {
     56 }
     57 
     58 AXTable::~AXTable()
     59 {
     60 }
     61 
     62 void AXTable::init()
     63 {
     64     AXRenderObject::init();
     65     m_isAXTable = isTableExposableThroughAccessibility();
     66 }
     67 
     68 PassRefPtr<AXTable> AXTable::create(RenderObject* renderer)
     69 {
     70     return adoptRef(new AXTable(renderer));
     71 }
     72 
     73 bool AXTable::hasARIARole() const
     74 {
     75     if (!m_renderer)
     76         return false;
     77 
     78     AccessibilityRole ariaRole = ariaRoleAttribute();
     79     if (ariaRole != UnknownRole)
     80         return true;
     81 
     82     return false;
     83 }
     84 
     85 bool AXTable::isAXTable() const
     86 {
     87     if (!m_renderer)
     88         return false;
     89 
     90     return m_isAXTable;
     91 }
     92 
     93 static bool elementHasAriaRole(const Element* element)
     94 {
     95     if (!element)
     96         return false;
     97 
     98     const AtomicString& ariaRole = element->fastGetAttribute(roleAttr);
     99     return (!ariaRole.isNull() && !ariaRole.isEmpty());
    100 }
    101 
    102 bool AXTable::isDataTable() const
    103 {
    104     if (!m_renderer || !node())
    105         return false;
    106 
    107     // Do not consider it a data table is it has an ARIA role.
    108     if (hasARIARole())
    109         return false;
    110 
    111     // When a section of the document is contentEditable, all tables should be
    112     // treated as data tables, otherwise users may not be able to work with rich
    113     // text editors that allow creating and editing tables.
    114     if (node() && node()->hasEditableStyle())
    115         return true;
    116 
    117     // This employs a heuristic to determine if this table should appear.
    118     // Only "data" tables should be exposed as tables.
    119     // Unfortunately, there is no good way to determine the difference
    120     // between a "layout" table and a "data" table.
    121 
    122     RenderTable* table = toRenderTable(m_renderer);
    123     Node* tableNode = table->node();
    124     if (!isHTMLTableElement(tableNode))
    125         return false;
    126 
    127     // Do not consider it a data table if any of its descendants have an ARIA role.
    128     HTMLTableElement* tableElement = toHTMLTableElement(tableNode);
    129     if (elementHasAriaRole(tableElement->tHead()))
    130         return false;
    131     if (elementHasAriaRole(tableElement->tFoot()))
    132         return false;
    133 
    134     RefPtrWillBeRawPtr<HTMLCollection> bodies = tableElement->tBodies();
    135     for (unsigned bodyIndex = 0; bodyIndex < bodies->length(); ++bodyIndex) {
    136         Element* bodyElement = bodies->item(bodyIndex);
    137         if (elementHasAriaRole(bodyElement))
    138             return false;
    139     }
    140 
    141     RefPtrWillBeRawPtr<HTMLTableRowsCollection> rows = tableElement->rows();
    142     unsigned rowCount = rows->length();
    143     for (unsigned rowIndex = 0; rowIndex < rowCount; ++rowIndex) {
    144         HTMLTableRowElement* rowElement = rows->item(rowIndex);
    145         if (elementHasAriaRole(rowElement))
    146             return false;
    147         RefPtrWillBeRawPtr<HTMLCollection> cells = rowElement->cells();
    148         for (unsigned cellIndex = 0; cellIndex < cells->length(); ++cellIndex) {
    149             if (elementHasAriaRole(cells->item(cellIndex)))
    150                 return false;
    151         }
    152     }
    153 
    154     // If there is a caption element, summary, THEAD, or TFOOT section, it's most certainly a data table
    155     if (!tableElement->summary().isEmpty() || tableElement->tHead() || tableElement->tFoot() || tableElement->caption())
    156         return true;
    157 
    158     // if someone used "rules" attribute than the table should appear
    159     if (!tableElement->rules().isEmpty())
    160         return true;
    161 
    162     // if there's a colgroup or col element, it's probably a data table.
    163     if (Traversal<HTMLTableColElement>::firstChild(*tableElement))
    164         return true;
    165 
    166     // go through the cell's and check for tell-tale signs of "data" table status
    167     // cells have borders, or use attributes like headers, abbr, scope or axis
    168     table->recalcSectionsIfNeeded();
    169     RenderTableSection* firstBody = table->firstBody();
    170     if (!firstBody)
    171         return false;
    172 
    173     int numCols = firstBody->numColumns();
    174     int numRows = firstBody->numRows();
    175 
    176     // If there's only one cell, it's not a good AXTable candidate.
    177     if (numRows == 1 && numCols == 1)
    178         return false;
    179 
    180     // If there are at least 20 rows, we'll call it a data table.
    181     if (numRows >= 20)
    182         return true;
    183 
    184     // Store the background color of the table to check against cell's background colors.
    185     RenderStyle* tableStyle = table->style();
    186     if (!tableStyle)
    187         return false;
    188     Color tableBGColor = tableStyle->visitedDependentColor(CSSPropertyBackgroundColor);
    189 
    190     // check enough of the cells to find if the table matches our criteria
    191     // Criteria:
    192     //   1) must have at least one valid cell (and)
    193     //   2) at least half of cells have borders (or)
    194     //   3) at least half of cells have different bg colors than the table, and there is cell spacing
    195     unsigned validCellCount = 0;
    196     unsigned borderedCellCount = 0;
    197     unsigned backgroundDifferenceCellCount = 0;
    198     unsigned cellsWithTopBorder = 0;
    199     unsigned cellsWithBottomBorder = 0;
    200     unsigned cellsWithLeftBorder = 0;
    201     unsigned cellsWithRightBorder = 0;
    202 
    203     Color alternatingRowColors[5];
    204     int alternatingRowColorCount = 0;
    205 
    206     int headersInFirstColumnCount = 0;
    207     for (int row = 0; row < numRows; ++row) {
    208 
    209         int headersInFirstRowCount = 0;
    210         for (int col = 0; col < numCols; ++col) {
    211             RenderTableCell* cell = firstBody->primaryCellAt(row, col);
    212             if (!cell)
    213                 continue;
    214             Node* cellNode = cell->node();
    215             if (!cellNode)
    216                 continue;
    217 
    218             if (cell->width() < 1 || cell->height() < 1)
    219                 continue;
    220 
    221             validCellCount++;
    222 
    223             bool isTHCell = cellNode->hasTagName(thTag);
    224             // If the first row is comprised of all <th> tags, assume it is a data table.
    225             if (!row && isTHCell)
    226                 headersInFirstRowCount++;
    227 
    228             // If the first column is comprised of all <th> tags, assume it is a data table.
    229             if (!col && isTHCell)
    230                 headersInFirstColumnCount++;
    231 
    232             // in this case, the developer explicitly assigned a "data" table attribute
    233             if (isHTMLTableCellElement(*cellNode)) {
    234                 HTMLTableCellElement& cellElement = toHTMLTableCellElement(*cellNode);
    235                 if (!cellElement.headers().isEmpty() || !cellElement.abbr().isEmpty()
    236                     || !cellElement.axis().isEmpty() || !cellElement.scope().isEmpty())
    237                     return true;
    238             }
    239 
    240             RenderStyle* renderStyle = cell->style();
    241             if (!renderStyle)
    242                 continue;
    243 
    244             // If the empty-cells style is set, we'll call it a data table.
    245             if (renderStyle->emptyCells() == HIDE)
    246                 return true;
    247 
    248             // If a cell has matching bordered sides, call it a (fully) bordered cell.
    249             if ((cell->borderTop() > 0 && cell->borderBottom() > 0)
    250                 || (cell->borderLeft() > 0 && cell->borderRight() > 0))
    251                 borderedCellCount++;
    252 
    253             // Also keep track of each individual border, so we can catch tables where most
    254             // cells have a bottom border, for example.
    255             if (cell->borderTop() > 0)
    256                 cellsWithTopBorder++;
    257             if (cell->borderBottom() > 0)
    258                 cellsWithBottomBorder++;
    259             if (cell->borderLeft() > 0)
    260                 cellsWithLeftBorder++;
    261             if (cell->borderRight() > 0)
    262                 cellsWithRightBorder++;
    263 
    264             // If the cell has a different color from the table and there is cell spacing,
    265             // then it is probably a data table cell (spacing and colors take the place of borders).
    266             Color cellColor = renderStyle->visitedDependentColor(CSSPropertyBackgroundColor);
    267             if (table->hBorderSpacing() > 0 && table->vBorderSpacing() > 0
    268                 && tableBGColor != cellColor && cellColor.alpha() != 1)
    269                 backgroundDifferenceCellCount++;
    270 
    271             // If we've found 10 "good" cells, we don't need to keep searching.
    272             if (borderedCellCount >= 10 || backgroundDifferenceCellCount >= 10)
    273                 return true;
    274 
    275             // For the first 5 rows, cache the background color so we can check if this table has zebra-striped rows.
    276             if (row < 5 && row == alternatingRowColorCount) {
    277                 RenderObject* renderRow = cell->parent();
    278                 if (!renderRow || !renderRow->isBoxModelObject() || !toRenderBoxModelObject(renderRow)->isTableRow())
    279                     continue;
    280                 RenderStyle* rowRenderStyle = renderRow->style();
    281                 if (!rowRenderStyle)
    282                     continue;
    283                 Color rowColor = rowRenderStyle->visitedDependentColor(CSSPropertyBackgroundColor);
    284                 alternatingRowColors[alternatingRowColorCount] = rowColor;
    285                 alternatingRowColorCount++;
    286             }
    287         }
    288 
    289         if (!row && headersInFirstRowCount == numCols && numCols > 1)
    290             return true;
    291     }
    292 
    293     if (headersInFirstColumnCount == numRows && numRows > 1)
    294         return true;
    295 
    296     // if there is less than two valid cells, it's not a data table
    297     if (validCellCount <= 1)
    298         return false;
    299 
    300     // half of the cells had borders, it's a data table
    301     unsigned neededCellCount = validCellCount / 2;
    302     if (borderedCellCount >= neededCellCount
    303         || cellsWithTopBorder >= neededCellCount
    304         || cellsWithBottomBorder >= neededCellCount
    305         || cellsWithLeftBorder >= neededCellCount
    306         || cellsWithRightBorder >= neededCellCount)
    307         return true;
    308 
    309     // half had different background colors, it's a data table
    310     if (backgroundDifferenceCellCount >= neededCellCount)
    311         return true;
    312 
    313     // Check if there is an alternating row background color indicating a zebra striped style pattern.
    314     if (alternatingRowColorCount > 2) {
    315         Color firstColor = alternatingRowColors[0];
    316         for (int k = 1; k < alternatingRowColorCount; k++) {
    317             // If an odd row was the same color as the first row, its not alternating.
    318             if (k % 2 == 1 && alternatingRowColors[k] == firstColor)
    319                 return false;
    320             // If an even row is not the same as the first row, its not alternating.
    321             if (!(k % 2) && alternatingRowColors[k] != firstColor)
    322                 return false;
    323         }
    324         return true;
    325     }
    326 
    327     return false;
    328 }
    329 
    330 bool AXTable::isTableExposableThroughAccessibility() const
    331 {
    332     // The following is a heuristic used to determine if a
    333     // <table> should be exposed as an AXTable. The goal
    334     // is to only show "data" tables.
    335 
    336     if (!m_renderer)
    337         return false;
    338 
    339     // If the developer assigned an aria role to this, then we
    340     // shouldn't expose it as a table, unless, of course, the aria
    341     // role is a table.
    342     if (hasARIARole())
    343         return false;
    344 
    345     return isDataTable();
    346 }
    347 
    348 void AXTable::clearChildren()
    349 {
    350     AXRenderObject::clearChildren();
    351     m_rows.clear();
    352     m_columns.clear();
    353 
    354     if (m_headerContainer) {
    355         m_headerContainer->detachFromParent();
    356         m_headerContainer = nullptr;
    357     }
    358 }
    359 
    360 void AXTable::addChildren()
    361 {
    362     if (!isAXTable()) {
    363         AXRenderObject::addChildren();
    364         return;
    365     }
    366 
    367     ASSERT(!m_haveChildren);
    368 
    369     m_haveChildren = true;
    370     if (!m_renderer || !m_renderer->isTable())
    371         return;
    372 
    373     RenderTable* table = toRenderTable(m_renderer);
    374     AXObjectCache* axCache = m_renderer->document().axObjectCache();
    375 
    376     // Go through all the available sections to pull out the rows and add them as children.
    377     table->recalcSectionsIfNeeded();
    378     RenderTableSection* tableSection = table->topSection();
    379     if (!tableSection)
    380         return;
    381 
    382     RenderTableSection* initialTableSection = tableSection;
    383     while (tableSection) {
    384 
    385         HashSet<AXObject*> appendedRows;
    386         unsigned numRows = tableSection->numRows();
    387         for (unsigned rowIndex = 0; rowIndex < numRows; ++rowIndex) {
    388 
    389             RenderTableRow* renderRow = tableSection->rowRendererAt(rowIndex);
    390             if (!renderRow)
    391                 continue;
    392 
    393             AXObject* rowObject = axCache->getOrCreate(renderRow);
    394             if (!rowObject->isTableRow())
    395                 continue;
    396 
    397             AXTableRow* row = toAXTableRow(rowObject);
    398             // We need to check every cell for a new row, because cell spans
    399             // can cause us to miss rows if we just check the first column.
    400             if (appendedRows.contains(row))
    401                 continue;
    402 
    403             row->setRowIndex(static_cast<int>(m_rows.size()));
    404             m_rows.append(row);
    405             if (!row->accessibilityIsIgnored())
    406                 m_children.append(row);
    407             appendedRows.add(row);
    408         }
    409 
    410         tableSection = table->sectionBelow(tableSection, SkipEmptySections);
    411     }
    412 
    413     // make the columns based on the number of columns in the first body
    414     unsigned length = initialTableSection->numColumns();
    415     for (unsigned i = 0; i < length; ++i) {
    416         AXTableColumn* column = toAXTableColumn(axCache->getOrCreate(ColumnRole));
    417         column->setColumnIndex((int)i);
    418         column->setParent(this);
    419         m_columns.append(column);
    420         if (!column->accessibilityIsIgnored())
    421             m_children.append(column);
    422     }
    423 
    424     AXObject* headerContainerObject = headerContainer();
    425     if (headerContainerObject && !headerContainerObject->accessibilityIsIgnored())
    426         m_children.append(headerContainerObject);
    427 }
    428 
    429 AXObject* AXTable::headerContainer()
    430 {
    431     if (m_headerContainer)
    432         return m_headerContainer.get();
    433 
    434     AXMockObject* tableHeader = toAXMockObject(axObjectCache()->getOrCreate(TableHeaderContainerRole));
    435     tableHeader->setParent(this);
    436 
    437     m_headerContainer = tableHeader;
    438     return m_headerContainer.get();
    439 }
    440 
    441 AXObject::AccessibilityChildrenVector& AXTable::columns()
    442 {
    443     updateChildrenIfNecessary();
    444 
    445     return m_columns;
    446 }
    447 
    448 AXObject::AccessibilityChildrenVector& AXTable::rows()
    449 {
    450     updateChildrenIfNecessary();
    451 
    452     return m_rows;
    453 }
    454 
    455 void AXTable::columnHeaders(AccessibilityChildrenVector& headers)
    456 {
    457     if (!m_renderer)
    458         return;
    459 
    460     updateChildrenIfNecessary();
    461 
    462     unsigned colCount = m_columns.size();
    463     for (unsigned k = 0; k < colCount; ++k) {
    464         AXObject* header = toAXTableColumn(m_columns[k].get())->headerObject();
    465         if (!header)
    466             continue;
    467         headers.append(header);
    468     }
    469 }
    470 
    471 void AXTable::cells(AXObject::AccessibilityChildrenVector& cells)
    472 {
    473     if (!m_renderer)
    474         return;
    475 
    476     updateChildrenIfNecessary();
    477 
    478     int numRows = m_rows.size();
    479     for (int row = 0; row < numRows; ++row) {
    480         AccessibilityChildrenVector rowChildren = m_rows[row]->children();
    481         cells.appendVector(rowChildren);
    482     }
    483 }
    484 
    485 unsigned AXTable::columnCount()
    486 {
    487     updateChildrenIfNecessary();
    488 
    489     return m_columns.size();
    490 }
    491 
    492 unsigned AXTable::rowCount()
    493 {
    494     updateChildrenIfNecessary();
    495 
    496     return m_rows.size();
    497 }
    498 
    499 AXTableCell* AXTable::cellForColumnAndRow(unsigned column, unsigned row)
    500 {
    501     updateChildrenIfNecessary();
    502     if (column >= columnCount() || row >= rowCount())
    503         return 0;
    504 
    505     // Iterate backwards through the rows in case the desired cell has a rowspan and exists in a previous row.
    506     for (unsigned rowIndexCounter = row + 1; rowIndexCounter > 0; --rowIndexCounter) {
    507         unsigned rowIndex = rowIndexCounter - 1;
    508         AccessibilityChildrenVector children = m_rows[rowIndex]->children();
    509         // Since some cells may have colspans, we have to check the actual range of each
    510         // cell to determine which is the right one.
    511         for (unsigned colIndexCounter = std::min(static_cast<unsigned>(children.size()), column + 1); colIndexCounter > 0; --colIndexCounter) {
    512             unsigned colIndex = colIndexCounter - 1;
    513             AXObject* child = children[colIndex].get();
    514             ASSERT(child->isTableCell());
    515             if (!child->isTableCell())
    516                 continue;
    517 
    518             pair<unsigned, unsigned> columnRange;
    519             pair<unsigned, unsigned> rowRange;
    520             AXTableCell* tableCellChild = toAXTableCell(child);
    521             tableCellChild->columnIndexRange(columnRange);
    522             tableCellChild->rowIndexRange(rowRange);
    523 
    524             if ((column >= columnRange.first && column < (columnRange.first + columnRange.second))
    525                 && (row >= rowRange.first && row < (rowRange.first + rowRange.second)))
    526                 return tableCellChild;
    527         }
    528     }
    529 
    530     return 0;
    531 }
    532 
    533 AccessibilityRole AXTable::roleValue() const
    534 {
    535     if (!isAXTable())
    536         return AXRenderObject::roleValue();
    537 
    538     return TableRole;
    539 }
    540 
    541 bool AXTable::computeAccessibilityIsIgnored() const
    542 {
    543     AXObjectInclusion decision = defaultObjectInclusion();
    544     if (decision == IncludeObject)
    545         return false;
    546     if (decision == IgnoreObject)
    547         return true;
    548 
    549     if (!isAXTable())
    550         return AXRenderObject::computeAccessibilityIsIgnored();
    551 
    552     return false;
    553 }
    554 
    555 String AXTable::title() const
    556 {
    557     if (!isAXTable())
    558         return AXRenderObject::title();
    559 
    560     String title;
    561     if (!m_renderer)
    562         return title;
    563 
    564     // see if there is a caption
    565     Node* tableElement = m_renderer->node();
    566     if (isHTMLTableElement(tableElement)) {
    567         HTMLTableCaptionElement* caption = toHTMLTableElement(tableElement)->caption();
    568         if (caption)
    569             title = caption->innerText();
    570     }
    571 
    572     // try the standard
    573     if (title.isEmpty())
    574         title = AXRenderObject::title();
    575 
    576     return title;
    577 }
    578 
    579 } // namespace blink
    580