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 "AccessibilityTable.h"
     31 
     32 #include "AXObjectCache.h"
     33 #include "AccessibilityTableCell.h"
     34 #include "AccessibilityTableColumn.h"
     35 #include "AccessibilityTableHeaderContainer.h"
     36 #include "AccessibilityTableRow.h"
     37 #include "HTMLNames.h"
     38 #include "HTMLTableCaptionElement.h"
     39 #include "HTMLTableCellElement.h"
     40 #include "HTMLTableElement.h"
     41 #include "RenderObject.h"
     42 #include "RenderTable.h"
     43 #include "RenderTableCell.h"
     44 #include "RenderTableSection.h"
     45 
     46 namespace WebCore {
     47 
     48 using namespace HTMLNames;
     49 
     50 AccessibilityTable::AccessibilityTable(RenderObject* renderer)
     51     : AccessibilityRenderObject(renderer),
     52     m_headerContainer(0)
     53 {
     54 #if ACCESSIBILITY_TABLES
     55     m_isAccessibilityTable = isTableExposableThroughAccessibility();
     56 #else
     57     m_isAccessibilityTable = false;
     58 #endif
     59 }
     60 
     61 AccessibilityTable::~AccessibilityTable()
     62 {
     63 }
     64 
     65 PassRefPtr<AccessibilityTable> AccessibilityTable::create(RenderObject* renderer)
     66 {
     67     return adoptRef(new AccessibilityTable(renderer));
     68 }
     69 
     70 bool AccessibilityTable::hasARIARole() const
     71 {
     72     if (!m_renderer)
     73         return false;
     74 
     75     AccessibilityRole ariaRole = ariaRoleAttribute();
     76     if (ariaRole != UnknownRole)
     77         return true;
     78 
     79     return false;
     80 }
     81 
     82 bool AccessibilityTable::isAccessibilityTable() const
     83 {
     84     if (!m_renderer)
     85         return false;
     86 
     87     return m_isAccessibilityTable;
     88 }
     89 
     90 bool AccessibilityTable::isDataTable() const
     91 {
     92     if (!m_renderer)
     93         return false;
     94 
     95     // Do not consider it a data table is it has an ARIA role.
     96     if (hasARIARole())
     97         return false;
     98 
     99     // This employs a heuristic to determine if this table should appear.
    100     // Only "data" tables should be exposed as tables.
    101     // Unfortunately, there is no good way to determine the difference
    102     // between a "layout" table and a "data" table.
    103 
    104     RenderTable* table = toRenderTable(m_renderer);
    105     Node* tableNode = table->node();
    106     if (!tableNode || !tableNode->hasTagName(tableTag))
    107         return false;
    108 
    109     // if there is a caption element, summary, THEAD, or TFOOT section, it's most certainly a data table
    110     HTMLTableElement* tableElement = static_cast<HTMLTableElement*>(tableNode);
    111     if (!tableElement->summary().isEmpty() || tableElement->tHead() || tableElement->tFoot() || tableElement->caption())
    112         return true;
    113 
    114     // if someone used "rules" attribute than the table should appear
    115     if (!tableElement->rules().isEmpty())
    116         return true;
    117 
    118     // go through the cell's and check for tell-tale signs of "data" table status
    119     // cells have borders, or use attributes like headers, abbr, scope or axis
    120     RenderTableSection* firstBody = table->firstBody();
    121     if (!firstBody)
    122         return false;
    123 
    124     int numCols = firstBody->numColumns();
    125     int numRows = firstBody->numRows();
    126 
    127     // if there's only one cell, it's not a good AXTable candidate
    128     if (numRows == 1 && numCols == 1)
    129         return false;
    130 
    131     // store the background color of the table to check against cell's background colors
    132     RenderStyle* tableStyle = table->style();
    133     if (!tableStyle)
    134         return false;
    135     Color tableBGColor = tableStyle->visitedDependentColor(CSSPropertyBackgroundColor);
    136 
    137     // check enough of the cells to find if the table matches our criteria
    138     // Criteria:
    139     //   1) must have at least one valid cell (and)
    140     //   2) at least half of cells have borders (or)
    141     //   3) at least half of cells have different bg colors than the table, and there is cell spacing
    142     unsigned validCellCount = 0;
    143     unsigned borderedCellCount = 0;
    144     unsigned backgroundDifferenceCellCount = 0;
    145 
    146     Color alternatingRowColors[5];
    147     int alternatingRowColorCount = 0;
    148 
    149     int headersInFirstColumnCount = 0;
    150     for (int row = 0; row < numRows; ++row) {
    151 
    152         int headersInFirstRowCount = 0;
    153         for (int col = 0; col < numCols; ++col) {
    154             RenderTableCell* cell = firstBody->primaryCellAt(row, col);
    155             if (!cell)
    156                 continue;
    157             Node* cellNode = cell->node();
    158             if (!cellNode)
    159                 continue;
    160 
    161             if (cell->width() < 1 || cell->height() < 1)
    162                 continue;
    163 
    164             validCellCount++;
    165 
    166             HTMLTableCellElement* cellElement = static_cast<HTMLTableCellElement*>(cellNode);
    167 
    168             bool isTHCell = cellElement->hasTagName(thTag);
    169             // If the first row is comprised of all <th> tags, assume it is a data table.
    170             if (!row && isTHCell)
    171                 headersInFirstRowCount++;
    172 
    173             // If the first column is comprised of all <tg> tags, assume it is a data table.
    174             if (!col && isTHCell)
    175                 headersInFirstColumnCount++;
    176 
    177             // in this case, the developer explicitly assigned a "data" table attribute
    178             if (!cellElement->headers().isEmpty() || !cellElement->abbr().isEmpty()
    179                 || !cellElement->axis().isEmpty() || !cellElement->scope().isEmpty())
    180                 return true;
    181 
    182             RenderStyle* renderStyle = cell->style();
    183             if (!renderStyle)
    184                 continue;
    185 
    186             // a cell needs to have matching bordered sides, before it can be considered a bordered cell.
    187             if ((cell->borderTop() > 0 && cell->borderBottom() > 0)
    188                 || (cell->borderLeft() > 0 && cell->borderRight() > 0))
    189                 borderedCellCount++;
    190 
    191             // if the cell has a different color from the table and there is cell spacing,
    192             // then it is probably a data table cell (spacing and colors take the place of borders)
    193             Color cellColor = renderStyle->visitedDependentColor(CSSPropertyBackgroundColor);
    194             if (table->hBorderSpacing() > 0 && table->vBorderSpacing() > 0
    195                 && tableBGColor != cellColor && cellColor.alpha() != 1)
    196                 backgroundDifferenceCellCount++;
    197 
    198             // if we've found 10 "good" cells, we don't need to keep searching
    199             if (borderedCellCount >= 10 || backgroundDifferenceCellCount >= 10)
    200                 return true;
    201 
    202             // For the first 5 rows, cache the background color so we can check if this table has zebra-striped rows.
    203             if (row < 5 && row == alternatingRowColorCount) {
    204                 RenderObject* renderRow = cell->parent();
    205                 if (!renderRow || !renderRow->isBoxModelObject() || !toRenderBoxModelObject(renderRow)->isTableRow())
    206                     continue;
    207                 RenderStyle* rowRenderStyle = renderRow->style();
    208                 if (!rowRenderStyle)
    209                     continue;
    210                 Color rowColor = rowRenderStyle->visitedDependentColor(CSSPropertyBackgroundColor);
    211                 alternatingRowColors[alternatingRowColorCount] = rowColor;
    212                 alternatingRowColorCount++;
    213             }
    214         }
    215 
    216         if (!row && headersInFirstRowCount == numCols && numCols > 1)
    217             return true;
    218     }
    219 
    220     if (headersInFirstColumnCount == numRows && numRows > 1)
    221         return true;
    222 
    223     // if there is less than two valid cells, it's not a data table
    224     if (validCellCount <= 1)
    225         return false;
    226 
    227     // half of the cells had borders, it's a data table
    228     unsigned neededCellCount = validCellCount / 2;
    229     if (borderedCellCount >= neededCellCount)
    230         return true;
    231 
    232     // half had different background colors, it's a data table
    233     if (backgroundDifferenceCellCount >= neededCellCount)
    234         return true;
    235 
    236     // Check if there is an alternating row background color indicating a zebra striped style pattern.
    237     if (alternatingRowColorCount > 2) {
    238         Color firstColor = alternatingRowColors[0];
    239         for (int k = 1; k < alternatingRowColorCount; k++) {
    240             // If an odd row was the same color as the first row, its not alternating.
    241             if (k % 2 == 1 && alternatingRowColors[k] == firstColor)
    242                 return false;
    243             // If an even row is not the same as the first row, its not alternating.
    244             if (!(k % 2) && alternatingRowColors[k] != firstColor)
    245                 return false;
    246         }
    247         return true;
    248     }
    249 
    250     return false;
    251 }
    252 
    253 bool AccessibilityTable::isTableExposableThroughAccessibility() const
    254 {
    255     // The following is a heuristic used to determine if a
    256     // <table> should be exposed as an AXTable. The goal
    257     // is to only show "data" tables.
    258 
    259     if (!m_renderer)
    260         return false;
    261 
    262     // If the developer assigned an aria role to this, then we
    263     // shouldn't expose it as a table, unless, of course, the aria
    264     // role is a table.
    265     if (hasARIARole())
    266         return false;
    267 
    268     // Gtk+ ATs expect all tables to be exposed as tables.
    269 #if PLATFORM(GTK)
    270     Node* tableNode = toRenderTable(m_renderer)->node();
    271     return tableNode && tableNode->hasTagName(tableTag);
    272 #endif
    273 
    274     return isDataTable();
    275 }
    276 
    277 void AccessibilityTable::clearChildren()
    278 {
    279     AccessibilityRenderObject::clearChildren();
    280     m_rows.clear();
    281     m_columns.clear();
    282 }
    283 
    284 void AccessibilityTable::addChildren()
    285 {
    286     if (!isAccessibilityTable()) {
    287         AccessibilityRenderObject::addChildren();
    288         return;
    289     }
    290 
    291     ASSERT(!m_haveChildren);
    292 
    293     m_haveChildren = true;
    294     if (!m_renderer || !m_renderer->isTable())
    295         return;
    296 
    297     RenderTable* table = toRenderTable(m_renderer);
    298     AXObjectCache* axCache = m_renderer->document()->axObjectCache();
    299 
    300     // go through all the available sections to pull out the rows
    301     // and add them as children
    302     RenderTableSection* tableSection = table->header();
    303     if (!tableSection)
    304         tableSection = table->firstBody();
    305 
    306     if (!tableSection)
    307         return;
    308 
    309     RenderTableSection* initialTableSection = tableSection;
    310 
    311     while (tableSection) {
    312 
    313         HashSet<AccessibilityObject*> appendedRows;
    314 
    315         unsigned numRows = tableSection->numRows();
    316         unsigned numCols = tableSection->numColumns();
    317         for (unsigned rowIndex = 0; rowIndex < numRows; ++rowIndex) {
    318             for (unsigned colIndex = 0; colIndex < numCols; ++colIndex) {
    319 
    320                 RenderTableCell* cell = tableSection->primaryCellAt(rowIndex, colIndex);
    321                 if (!cell)
    322                     continue;
    323 
    324                 AccessibilityObject* rowObject = axCache->getOrCreate(cell->parent());
    325                 if (!rowObject->isTableRow())
    326                     continue;
    327 
    328                 AccessibilityTableRow* row = static_cast<AccessibilityTableRow*>(rowObject);
    329                 // we need to check every cell for a new row, because cell spans
    330                 // can cause us to mess rows if we just check the first column
    331                 if (appendedRows.contains(row))
    332                     continue;
    333 
    334                 row->setRowIndex((int)m_rows.size());
    335                 m_rows.append(row);
    336                 if (!row->accessibilityIsIgnored())
    337                     m_children.append(row);
    338 #if PLATFORM(GTK)
    339                 else
    340                     m_children.append(row->children());
    341 #endif
    342                 appendedRows.add(row);
    343             }
    344         }
    345 
    346         tableSection = table->sectionBelow(tableSection, true);
    347     }
    348 
    349     // make the columns based on the number of columns in the first body
    350     unsigned length = initialTableSection->numColumns();
    351     for (unsigned i = 0; i < length; ++i) {
    352         AccessibilityTableColumn* column = static_cast<AccessibilityTableColumn*>(axCache->getOrCreate(ColumnRole));
    353         column->setColumnIndex((int)i);
    354         column->setParentTable(this);
    355         m_columns.append(column);
    356         if (!column->accessibilityIsIgnored())
    357             m_children.append(column);
    358     }
    359 
    360     AccessibilityObject* headerContainerObject = headerContainer();
    361     if (headerContainerObject && !headerContainerObject->accessibilityIsIgnored())
    362         m_children.append(headerContainerObject);
    363 }
    364 
    365 AccessibilityObject* AccessibilityTable::headerContainer()
    366 {
    367     if (m_headerContainer)
    368         return m_headerContainer;
    369 
    370     m_headerContainer = static_cast<AccessibilityTableHeaderContainer*>(axObjectCache()->getOrCreate(TableHeaderContainerRole));
    371     m_headerContainer->setParentTable(this);
    372 
    373     return m_headerContainer;
    374 }
    375 
    376 AccessibilityObject::AccessibilityChildrenVector& AccessibilityTable::columns()
    377 {
    378     updateChildrenIfNecessary();
    379 
    380     return m_columns;
    381 }
    382 
    383 AccessibilityObject::AccessibilityChildrenVector& AccessibilityTable::rows()
    384 {
    385     updateChildrenIfNecessary();
    386 
    387     return m_rows;
    388 }
    389 
    390 void AccessibilityTable::rowHeaders(AccessibilityChildrenVector& headers)
    391 {
    392     if (!m_renderer)
    393         return;
    394 
    395     updateChildrenIfNecessary();
    396 
    397     unsigned rowCount = m_rows.size();
    398     for (unsigned k = 0; k < rowCount; ++k) {
    399         AccessibilityObject* header = static_cast<AccessibilityTableRow*>(m_rows[k].get())->headerObject();
    400         if (!header)
    401             continue;
    402         headers.append(header);
    403     }
    404 }
    405 
    406 void AccessibilityTable::columnHeaders(AccessibilityChildrenVector& headers)
    407 {
    408     if (!m_renderer)
    409         return;
    410 
    411     updateChildrenIfNecessary();
    412 
    413     unsigned colCount = m_columns.size();
    414     for (unsigned k = 0; k < colCount; ++k) {
    415         AccessibilityObject* header = static_cast<AccessibilityTableColumn*>(m_columns[k].get())->headerObject();
    416         if (!header)
    417             continue;
    418         headers.append(header);
    419     }
    420 }
    421 
    422 void AccessibilityTable::cells(AccessibilityObject::AccessibilityChildrenVector& cells)
    423 {
    424     if (!m_renderer)
    425         return;
    426 
    427     updateChildrenIfNecessary();
    428 
    429     int numRows = m_rows.size();
    430     for (int row = 0; row < numRows; ++row) {
    431         AccessibilityChildrenVector rowChildren = m_rows[row]->children();
    432         cells.append(rowChildren);
    433     }
    434 }
    435 
    436 unsigned AccessibilityTable::columnCount()
    437 {
    438     updateChildrenIfNecessary();
    439 
    440     return m_columns.size();
    441 }
    442 
    443 unsigned AccessibilityTable::rowCount()
    444 {
    445     updateChildrenIfNecessary();
    446 
    447     return m_rows.size();
    448 }
    449 
    450 AccessibilityTableCell* AccessibilityTable::cellForColumnAndRow(unsigned column, unsigned row)
    451 {
    452     if (!m_renderer || !m_renderer->isTable())
    453         return 0;
    454 
    455     updateChildrenIfNecessary();
    456 
    457     RenderTable* table = toRenderTable(m_renderer);
    458     RenderTableSection* tableSection = table->header();
    459     if (!tableSection)
    460         tableSection = table->firstBody();
    461 
    462     RenderTableCell* cell = 0;
    463     unsigned rowCount = 0;
    464     unsigned rowOffset = 0;
    465     while (tableSection) {
    466 
    467         unsigned numRows = tableSection->numRows();
    468         unsigned numCols = tableSection->numColumns();
    469 
    470         rowCount += numRows;
    471 
    472         unsigned sectionSpecificRow = row - rowOffset;
    473         if (row < rowCount && column < numCols && sectionSpecificRow < numRows) {
    474             cell = tableSection->primaryCellAt(sectionSpecificRow, column);
    475 
    476             // we didn't find the cell, which means there's spanning happening
    477             // search backwards to find the spanning cell
    478             if (!cell) {
    479 
    480                 // first try rows
    481                 for (int testRow = sectionSpecificRow-1; testRow >= 0; --testRow) {
    482                     cell = tableSection->primaryCellAt(testRow, column);
    483                     // cell overlapped. use this one
    484                     if (cell && ((cell->row() + (cell->rowSpan()-1)) >= (int)sectionSpecificRow))
    485                         break;
    486                     cell = 0;
    487                 }
    488 
    489                 if (!cell) {
    490                     // try cols
    491                     for (int testCol = column-1; testCol >= 0; --testCol) {
    492                         cell = tableSection->primaryCellAt(sectionSpecificRow, testCol);
    493                         // cell overlapped. use this one
    494                         if (cell && ((cell->col() + (cell->colSpan()-1)) >= (int)column))
    495                             break;
    496                         cell = 0;
    497                     }
    498                 }
    499             }
    500         }
    501 
    502         if (cell)
    503             break;
    504 
    505         rowOffset += numRows;
    506         // we didn't find anything between the rows we should have
    507         if (row < rowCount)
    508             break;
    509         tableSection = table->sectionBelow(tableSection, true);
    510     }
    511 
    512     if (!cell)
    513         return 0;
    514 
    515     AccessibilityObject* cellObject = axObjectCache()->getOrCreate(cell);
    516     ASSERT(cellObject->isTableCell());
    517 
    518     return static_cast<AccessibilityTableCell*>(cellObject);
    519 }
    520 
    521 AccessibilityRole AccessibilityTable::roleValue() const
    522 {
    523     if (!isAccessibilityTable())
    524         return AccessibilityRenderObject::roleValue();
    525 
    526     return TableRole;
    527 }
    528 
    529 bool AccessibilityTable::accessibilityIsIgnored() const
    530 {
    531     AccessibilityObjectInclusion decision = accessibilityIsIgnoredBase();
    532     if (decision == IncludeObject)
    533         return false;
    534     if (decision == IgnoreObject)
    535         return true;
    536 
    537     if (!isAccessibilityTable())
    538         return AccessibilityRenderObject::accessibilityIsIgnored();
    539 
    540     return false;
    541 }
    542 
    543 String AccessibilityTable::title() const
    544 {
    545     if (!isAccessibilityTable())
    546         return AccessibilityRenderObject::title();
    547 
    548     String title;
    549     if (!m_renderer)
    550         return title;
    551 
    552     // see if there is a caption
    553     Node* tableElement = m_renderer->node();
    554     if (tableElement && tableElement->hasTagName(tableTag)) {
    555         HTMLTableCaptionElement* caption = static_cast<HTMLTableElement*>(tableElement)->caption();
    556         if (caption)
    557             title = caption->innerText();
    558     }
    559 
    560     // try the standard
    561     if (title.isEmpty())
    562         title = AccessibilityRenderObject::title();
    563 
    564     return title;
    565 }
    566 
    567 } // namespace WebCore
    568