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