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 WebCore {
48
49 using namespace HTMLNames;
50
AXTable(RenderObject * renderer)51 AXTable::AXTable(RenderObject* renderer)
52 : AXRenderObject(renderer)
53 , m_headerContainer(nullptr)
54 , m_isAXTable(true)
55 {
56 }
57
~AXTable()58 AXTable::~AXTable()
59 {
60 }
61
init()62 void AXTable::init()
63 {
64 AXRenderObject::init();
65 m_isAXTable = isTableExposableThroughAccessibility();
66 }
67
create(RenderObject * renderer)68 PassRefPtr<AXTable> AXTable::create(RenderObject* renderer)
69 {
70 return adoptRef(new AXTable(renderer));
71 }
72
hasARIARole() const73 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
isAXTable() const85 bool AXTable::isAXTable() const
86 {
87 if (!m_renderer)
88 return false;
89
90 return m_isAXTable;
91 }
92
elementHasAriaRole(const Element * element)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
isDataTable() const102 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()->rendererIsEditable())
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 Element* rowElement = rows->item(rowIndex);
145 if (elementHasAriaRole(rowElement))
146 return false;
147 if (rowElement->hasTagName(trTag)) {
148 HTMLTableRowElement* row = static_cast<HTMLTableRowElement*>(rowElement);
149 RefPtrWillBeRawPtr<HTMLCollection> cells = row->cells();
150 for (unsigned cellIndex = 0; cellIndex < cells->length(); ++cellIndex) {
151 if (elementHasAriaRole(cells->item(cellIndex)))
152 return false;
153 }
154 }
155 }
156
157 // If there is a caption element, summary, THEAD, or TFOOT section, it's most certainly a data table
158 if (!tableElement->summary().isEmpty() || tableElement->tHead() || tableElement->tFoot() || tableElement->caption())
159 return true;
160
161 // if someone used "rules" attribute than the table should appear
162 if (!tableElement->rules().isEmpty())
163 return true;
164
165 // if there's a colgroup or col element, it's probably a data table.
166 if (Traversal<HTMLTableColElement>::firstChild(*tableElement))
167 return true;
168
169 // go through the cell's and check for tell-tale signs of "data" table status
170 // cells have borders, or use attributes like headers, abbr, scope or axis
171 table->recalcSectionsIfNeeded();
172 RenderTableSection* firstBody = table->firstBody();
173 if (!firstBody)
174 return false;
175
176 int numCols = firstBody->numColumns();
177 int numRows = firstBody->numRows();
178
179 // If there's only one cell, it's not a good AXTable candidate.
180 if (numRows == 1 && numCols == 1)
181 return false;
182
183 // If there are at least 20 rows, we'll call it a data table.
184 if (numRows >= 20)
185 return true;
186
187 // Store the background color of the table to check against cell's background colors.
188 RenderStyle* tableStyle = table->style();
189 if (!tableStyle)
190 return false;
191 Color tableBGColor = tableStyle->visitedDependentColor(CSSPropertyBackgroundColor);
192
193 // check enough of the cells to find if the table matches our criteria
194 // Criteria:
195 // 1) must have at least one valid cell (and)
196 // 2) at least half of cells have borders (or)
197 // 3) at least half of cells have different bg colors than the table, and there is cell spacing
198 unsigned validCellCount = 0;
199 unsigned borderedCellCount = 0;
200 unsigned backgroundDifferenceCellCount = 0;
201 unsigned cellsWithTopBorder = 0;
202 unsigned cellsWithBottomBorder = 0;
203 unsigned cellsWithLeftBorder = 0;
204 unsigned cellsWithRightBorder = 0;
205
206 Color alternatingRowColors[5];
207 int alternatingRowColorCount = 0;
208
209 int headersInFirstColumnCount = 0;
210 for (int row = 0; row < numRows; ++row) {
211
212 int headersInFirstRowCount = 0;
213 for (int col = 0; col < numCols; ++col) {
214 RenderTableCell* cell = firstBody->primaryCellAt(row, col);
215 if (!cell)
216 continue;
217 Node* cellNode = cell->node();
218 if (!cellNode)
219 continue;
220
221 if (cell->width() < 1 || cell->height() < 1)
222 continue;
223
224 validCellCount++;
225
226 bool isTHCell = cellNode->hasTagName(thTag);
227 // If the first row is comprised of all <th> tags, assume it is a data table.
228 if (!row && isTHCell)
229 headersInFirstRowCount++;
230
231 // If the first column is comprised of all <th> tags, assume it is a data table.
232 if (!col && isTHCell)
233 headersInFirstColumnCount++;
234
235 // in this case, the developer explicitly assigned a "data" table attribute
236 if (isHTMLTableCellElement(*cellNode)) {
237 HTMLTableCellElement& cellElement = toHTMLTableCellElement(*cellNode);
238 if (!cellElement.headers().isEmpty() || !cellElement.abbr().isEmpty()
239 || !cellElement.axis().isEmpty() || !cellElement.scope().isEmpty())
240 return true;
241 }
242
243 RenderStyle* renderStyle = cell->style();
244 if (!renderStyle)
245 continue;
246
247 // If the empty-cells style is set, we'll call it a data table.
248 if (renderStyle->emptyCells() == HIDE)
249 return true;
250
251 // If a cell has matching bordered sides, call it a (fully) bordered cell.
252 if ((cell->borderTop() > 0 && cell->borderBottom() > 0)
253 || (cell->borderLeft() > 0 && cell->borderRight() > 0))
254 borderedCellCount++;
255
256 // Also keep track of each individual border, so we can catch tables where most
257 // cells have a bottom border, for example.
258 if (cell->borderTop() > 0)
259 cellsWithTopBorder++;
260 if (cell->borderBottom() > 0)
261 cellsWithBottomBorder++;
262 if (cell->borderLeft() > 0)
263 cellsWithLeftBorder++;
264 if (cell->borderRight() > 0)
265 cellsWithRightBorder++;
266
267 // If the cell has a different color from the table and there is cell spacing,
268 // then it is probably a data table cell (spacing and colors take the place of borders).
269 Color cellColor = renderStyle->visitedDependentColor(CSSPropertyBackgroundColor);
270 if (table->hBorderSpacing() > 0 && table->vBorderSpacing() > 0
271 && tableBGColor != cellColor && cellColor.alpha() != 1)
272 backgroundDifferenceCellCount++;
273
274 // If we've found 10 "good" cells, we don't need to keep searching.
275 if (borderedCellCount >= 10 || backgroundDifferenceCellCount >= 10)
276 return true;
277
278 // For the first 5 rows, cache the background color so we can check if this table has zebra-striped rows.
279 if (row < 5 && row == alternatingRowColorCount) {
280 RenderObject* renderRow = cell->parent();
281 if (!renderRow || !renderRow->isBoxModelObject() || !toRenderBoxModelObject(renderRow)->isTableRow())
282 continue;
283 RenderStyle* rowRenderStyle = renderRow->style();
284 if (!rowRenderStyle)
285 continue;
286 Color rowColor = rowRenderStyle->visitedDependentColor(CSSPropertyBackgroundColor);
287 alternatingRowColors[alternatingRowColorCount] = rowColor;
288 alternatingRowColorCount++;
289 }
290 }
291
292 if (!row && headersInFirstRowCount == numCols && numCols > 1)
293 return true;
294 }
295
296 if (headersInFirstColumnCount == numRows && numRows > 1)
297 return true;
298
299 // if there is less than two valid cells, it's not a data table
300 if (validCellCount <= 1)
301 return false;
302
303 // half of the cells had borders, it's a data table
304 unsigned neededCellCount = validCellCount / 2;
305 if (borderedCellCount >= neededCellCount
306 || cellsWithTopBorder >= neededCellCount
307 || cellsWithBottomBorder >= neededCellCount
308 || cellsWithLeftBorder >= neededCellCount
309 || cellsWithRightBorder >= neededCellCount)
310 return true;
311
312 // half had different background colors, it's a data table
313 if (backgroundDifferenceCellCount >= neededCellCount)
314 return true;
315
316 // Check if there is an alternating row background color indicating a zebra striped style pattern.
317 if (alternatingRowColorCount > 2) {
318 Color firstColor = alternatingRowColors[0];
319 for (int k = 1; k < alternatingRowColorCount; k++) {
320 // If an odd row was the same color as the first row, its not alternating.
321 if (k % 2 == 1 && alternatingRowColors[k] == firstColor)
322 return false;
323 // If an even row is not the same as the first row, its not alternating.
324 if (!(k % 2) && alternatingRowColors[k] != firstColor)
325 return false;
326 }
327 return true;
328 }
329
330 return false;
331 }
332
isTableExposableThroughAccessibility() const333 bool AXTable::isTableExposableThroughAccessibility() const
334 {
335 // The following is a heuristic used to determine if a
336 // <table> should be exposed as an AXTable. The goal
337 // is to only show "data" tables.
338
339 if (!m_renderer)
340 return false;
341
342 // If the developer assigned an aria role to this, then we
343 // shouldn't expose it as a table, unless, of course, the aria
344 // role is a table.
345 if (hasARIARole())
346 return false;
347
348 return isDataTable();
349 }
350
clearChildren()351 void AXTable::clearChildren()
352 {
353 AXRenderObject::clearChildren();
354 m_rows.clear();
355 m_columns.clear();
356
357 if (m_headerContainer) {
358 m_headerContainer->detachFromParent();
359 m_headerContainer = nullptr;
360 }
361 }
362
addChildren()363 void AXTable::addChildren()
364 {
365 if (!isAXTable()) {
366 AXRenderObject::addChildren();
367 return;
368 }
369
370 ASSERT(!m_haveChildren);
371
372 m_haveChildren = true;
373 if (!m_renderer || !m_renderer->isTable())
374 return;
375
376 RenderTable* table = toRenderTable(m_renderer);
377 AXObjectCache* axCache = m_renderer->document().axObjectCache();
378
379 // Go through all the available sections to pull out the rows and add them as children.
380 table->recalcSectionsIfNeeded();
381 RenderTableSection* tableSection = table->topSection();
382 if (!tableSection)
383 return;
384
385 RenderTableSection* initialTableSection = tableSection;
386 while (tableSection) {
387
388 HashSet<AXObject*> appendedRows;
389 unsigned numRows = tableSection->numRows();
390 for (unsigned rowIndex = 0; rowIndex < numRows; ++rowIndex) {
391
392 RenderTableRow* renderRow = tableSection->rowRendererAt(rowIndex);
393 if (!renderRow)
394 continue;
395
396 AXObject* rowObject = axCache->getOrCreate(renderRow);
397 if (!rowObject->isTableRow())
398 continue;
399
400 AXTableRow* row = toAXTableRow(rowObject);
401 // We need to check every cell for a new row, because cell spans
402 // can cause us to miss rows if we just check the first column.
403 if (appendedRows.contains(row))
404 continue;
405
406 row->setRowIndex(static_cast<int>(m_rows.size()));
407 m_rows.append(row);
408 if (!row->accessibilityIsIgnored())
409 m_children.append(row);
410 appendedRows.add(row);
411 }
412
413 tableSection = table->sectionBelow(tableSection, SkipEmptySections);
414 }
415
416 // make the columns based on the number of columns in the first body
417 unsigned length = initialTableSection->numColumns();
418 for (unsigned i = 0; i < length; ++i) {
419 AXTableColumn* column = toAXTableColumn(axCache->getOrCreate(ColumnRole));
420 column->setColumnIndex((int)i);
421 column->setParent(this);
422 m_columns.append(column);
423 if (!column->accessibilityIsIgnored())
424 m_children.append(column);
425 }
426
427 AXObject* headerContainerObject = headerContainer();
428 if (headerContainerObject && !headerContainerObject->accessibilityIsIgnored())
429 m_children.append(headerContainerObject);
430 }
431
headerContainer()432 AXObject* AXTable::headerContainer()
433 {
434 if (m_headerContainer)
435 return m_headerContainer.get();
436
437 AXMockObject* tableHeader = toAXMockObject(axObjectCache()->getOrCreate(TableHeaderContainerRole));
438 tableHeader->setParent(this);
439
440 m_headerContainer = tableHeader;
441 return m_headerContainer.get();
442 }
443
columns()444 AXObject::AccessibilityChildrenVector& AXTable::columns()
445 {
446 updateChildrenIfNecessary();
447
448 return m_columns;
449 }
450
rows()451 AXObject::AccessibilityChildrenVector& AXTable::rows()
452 {
453 updateChildrenIfNecessary();
454
455 return m_rows;
456 }
457
columnHeaders(AccessibilityChildrenVector & headers)458 void AXTable::columnHeaders(AccessibilityChildrenVector& headers)
459 {
460 if (!m_renderer)
461 return;
462
463 updateChildrenIfNecessary();
464
465 unsigned colCount = m_columns.size();
466 for (unsigned k = 0; k < colCount; ++k) {
467 AXObject* header = toAXTableColumn(m_columns[k].get())->headerObject();
468 if (!header)
469 continue;
470 headers.append(header);
471 }
472 }
473
cells(AXObject::AccessibilityChildrenVector & cells)474 void AXTable::cells(AXObject::AccessibilityChildrenVector& cells)
475 {
476 if (!m_renderer)
477 return;
478
479 updateChildrenIfNecessary();
480
481 int numRows = m_rows.size();
482 for (int row = 0; row < numRows; ++row) {
483 AccessibilityChildrenVector rowChildren = m_rows[row]->children();
484 cells.appendVector(rowChildren);
485 }
486 }
487
columnCount()488 unsigned AXTable::columnCount()
489 {
490 updateChildrenIfNecessary();
491
492 return m_columns.size();
493 }
494
rowCount()495 unsigned AXTable::rowCount()
496 {
497 updateChildrenIfNecessary();
498
499 return m_rows.size();
500 }
501
cellForColumnAndRow(unsigned column,unsigned row)502 AXTableCell* AXTable::cellForColumnAndRow(unsigned column, unsigned row)
503 {
504 updateChildrenIfNecessary();
505 if (column >= columnCount() || row >= rowCount())
506 return 0;
507
508 // Iterate backwards through the rows in case the desired cell has a rowspan and exists in a previous row.
509 for (unsigned rowIndexCounter = row + 1; rowIndexCounter > 0; --rowIndexCounter) {
510 unsigned rowIndex = rowIndexCounter - 1;
511 AccessibilityChildrenVector children = m_rows[rowIndex]->children();
512 // Since some cells may have colspans, we have to check the actual range of each
513 // cell to determine which is the right one.
514 for (unsigned colIndexCounter = std::min(static_cast<unsigned>(children.size()), column + 1); colIndexCounter > 0; --colIndexCounter) {
515 unsigned colIndex = colIndexCounter - 1;
516 AXObject* child = children[colIndex].get();
517 ASSERT(child->isTableCell());
518 if (!child->isTableCell())
519 continue;
520
521 pair<unsigned, unsigned> columnRange;
522 pair<unsigned, unsigned> rowRange;
523 AXTableCell* tableCellChild = toAXTableCell(child);
524 tableCellChild->columnIndexRange(columnRange);
525 tableCellChild->rowIndexRange(rowRange);
526
527 if ((column >= columnRange.first && column < (columnRange.first + columnRange.second))
528 && (row >= rowRange.first && row < (rowRange.first + rowRange.second)))
529 return tableCellChild;
530 }
531 }
532
533 return 0;
534 }
535
roleValue() const536 AccessibilityRole AXTable::roleValue() const
537 {
538 if (!isAXTable())
539 return AXRenderObject::roleValue();
540
541 return TableRole;
542 }
543
computeAccessibilityIsIgnored() const544 bool AXTable::computeAccessibilityIsIgnored() const
545 {
546 AXObjectInclusion decision = defaultObjectInclusion();
547 if (decision == IncludeObject)
548 return false;
549 if (decision == IgnoreObject)
550 return true;
551
552 if (!isAXTable())
553 return AXRenderObject::computeAccessibilityIsIgnored();
554
555 return false;
556 }
557
title() const558 String AXTable::title() const
559 {
560 if (!isAXTable())
561 return AXRenderObject::title();
562
563 String title;
564 if (!m_renderer)
565 return title;
566
567 // see if there is a caption
568 Node* tableElement = m_renderer->node();
569 if (isHTMLTableElement(tableElement)) {
570 HTMLTableCaptionElement* caption = toHTMLTableElement(tableElement)->caption();
571 if (caption)
572 title = caption->innerText();
573 }
574
575 // try the standard
576 if (title.isEmpty())
577 title = AXRenderObject::title();
578
579 return title;
580 }
581
582 } // namespace WebCore
583