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