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