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