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 "HTMLTableSectionElement.h" |
43 | #include "RenderObject.h" |
44 | #include "RenderTable.h" |
45 | #include "RenderTableCell.h" |
46 | #include "RenderTableSection.h" |
47 | |
48 | #include <wtf/Deque.h> |
49 | |
50 | namespace WebCore { |
51 | |
52 | using namespace HTMLNames; |
53 | |
54 | AccessibilityTable::AccessibilityTable(RenderObject* renderer) |
55 | : AccessibilityRenderObject(renderer) |
56 | , m_headerContainer(nullptr) |
57 | , m_isExposableThroughAccessibility(true) |
58 | { |
59 | } |
60 | |
61 | AccessibilityTable::~AccessibilityTable() = default; |
62 | |
63 | void AccessibilityTable::init() |
64 | { |
65 | AccessibilityRenderObject::init(); |
66 | m_isExposableThroughAccessibility = computeIsTableExposableThroughAccessibility(); |
67 | } |
68 | |
69 | Ref<AccessibilityTable> AccessibilityTable::create(RenderObject* renderer) |
70 | { |
71 | return adoptRef(*new AccessibilityTable(renderer)); |
72 | } |
73 | |
74 | bool AccessibilityTable::hasARIARole() const |
75 | { |
76 | if (!m_renderer) |
77 | return false; |
78 | |
79 | AccessibilityRole ariaRole = ariaRoleAttribute(); |
80 | if (ariaRole != AccessibilityRole::Unknown) |
81 | return true; |
82 | |
83 | return false; |
84 | } |
85 | |
86 | bool AccessibilityTable::isExposableThroughAccessibility() const |
87 | { |
88 | if (!m_renderer) |
89 | return false; |
90 | |
91 | return m_isExposableThroughAccessibility; |
92 | } |
93 | |
94 | HTMLTableElement* AccessibilityTable::tableElement() const |
95 | { |
96 | if (!is<RenderTable>(*m_renderer)) |
97 | return nullptr; |
98 | |
99 | RenderTable& table = downcast<RenderTable>(*m_renderer); |
100 | if (is<HTMLTableElement>(table.element())) |
101 | return downcast<HTMLTableElement>(table.element()); |
102 | // Try to find the table element, when the AccessibilityTable is mapped to an anonymous table renderer. |
103 | auto* firstChild = table.firstChild(); |
104 | if (!firstChild || !firstChild->node()) |
105 | return nullptr; |
106 | if (is<HTMLTableElement>(*firstChild->node())) |
107 | return downcast<HTMLTableElement>(firstChild->node()); |
108 | // FIXME: This might find an unrelated parent table element. |
109 | return ancestorsOfType<HTMLTableElement>(*(firstChild->node())).first(); |
110 | } |
111 | |
112 | bool AccessibilityTable::isDataTable() const |
113 | { |
114 | if (!m_renderer) |
115 | return false; |
116 | |
117 | // Do not consider it a data table is it has an ARIA role. |
118 | if (hasARIARole()) |
119 | return false; |
120 | |
121 | // When a section of the document is contentEditable, all tables should be |
122 | // treated as data tables, otherwise users may not be able to work with rich |
123 | // text editors that allow creating and editing tables. |
124 | if (node() && node()->hasEditableStyle()) |
125 | return true; |
126 | |
127 | if (!is<RenderTable>(*m_renderer)) |
128 | return false; |
129 | |
130 | // This employs a heuristic to determine if this table should appear. |
131 | // Only "data" tables should be exposed as tables. |
132 | // Unfortunately, there is no good way to determine the difference |
133 | // between a "layout" table and a "data" table. |
134 | if (HTMLTableElement* tableElement = this->tableElement()) { |
135 | // If there is a caption element, summary, THEAD, or TFOOT section, it's most certainly a data table. |
136 | if (!tableElement->summary().isEmpty() || tableElement->tHead() || tableElement->tFoot() || tableElement->caption()) |
137 | return true; |
138 | |
139 | // If someone used "rules" attribute than the table should appear. |
140 | if (!tableElement->rules().isEmpty()) |
141 | return true; |
142 | |
143 | // If there's a colgroup or col element, it's probably a data table. |
144 | for (const auto& child : childrenOfType<HTMLElement>(*tableElement)) { |
145 | if (child.hasTagName(colTag) || child.hasTagName(colgroupTag)) |
146 | return true; |
147 | } |
148 | } |
149 | |
150 | // The following checks should only apply if this is a real <table> element. |
151 | if (!hasTagName(tableTag)) |
152 | return false; |
153 | |
154 | // If the author has used ARIA to specify a valid column or row count, assume they |
155 | // want us to treat the table as a data table. |
156 | int axColumnCount = getAttribute(aria_colcountAttr).toInt(); |
157 | if (axColumnCount == -1 || axColumnCount > 0) |
158 | return true; |
159 | |
160 | int axRowCount = getAttribute(aria_rowcountAttr).toInt(); |
161 | if (axRowCount == -1 || axRowCount > 0) |
162 | return true; |
163 | |
164 | RenderTable& table = downcast<RenderTable>(*m_renderer); |
165 | // go through the cell's and check for tell-tale signs of "data" table status |
166 | // cells have borders, or use attributes like headers, abbr, scope or axis |
167 | table.recalcSectionsIfNeeded(); |
168 | RenderTableSection* firstBody = table.firstBody(); |
169 | if (!firstBody) |
170 | return false; |
171 | |
172 | int numCols = firstBody->numColumns(); |
173 | int numRows = firstBody->numRows(); |
174 | |
175 | // If there are at least 20 rows, we'll call it a data table. |
176 | if (numRows >= 20) |
177 | return true; |
178 | |
179 | // Store the background color of the table to check against cell's background colors. |
180 | const RenderStyle& tableStyle = table.style(); |
181 | Color tableBGColor = tableStyle.visitedDependentColor(CSSPropertyBackgroundColor); |
182 | |
183 | // check enough of the cells to find if the table matches our criteria |
184 | // Criteria: |
185 | // 1) must have at least one valid cell (and) |
186 | // 2) at least half of cells have borders (or) |
187 | // 3) at least half of cells have different bg colors than the table, and there is cell spacing (or) |
188 | // 4) the valid cell has an ARIA cell-related property |
189 | unsigned validCellCount = 0; |
190 | unsigned borderedCellCount = 0; |
191 | unsigned backgroundDifferenceCellCount = 0; |
192 | unsigned cellsWithTopBorder = 0; |
193 | unsigned cellsWithBottomBorder = 0; |
194 | unsigned cellsWithLeftBorder = 0; |
195 | unsigned cellsWithRightBorder = 0; |
196 | |
197 | Color alternatingRowColors[5]; |
198 | int alternatingRowColorCount = 0; |
199 | |
200 | int headersInFirstColumnCount = 0; |
201 | for (int row = 0; row < numRows; ++row) { |
202 | |
203 | int = 0; |
204 | for (int col = 0; col < numCols; ++col) { |
205 | RenderTableCell* cell = firstBody->primaryCellAt(row, col); |
206 | if (!cell) |
207 | continue; |
208 | |
209 | Element* cellElement = cell->element(); |
210 | if (!cellElement) |
211 | continue; |
212 | |
213 | if (cell->width() < 1 || cell->height() < 1) |
214 | continue; |
215 | |
216 | ++validCellCount; |
217 | |
218 | bool isTHCell = cellElement->hasTagName(thTag); |
219 | // If the first row is comprised of all <th> tags, assume it is a data table. |
220 | if (!row && isTHCell) |
221 | ++headersInFirstRowCount; |
222 | |
223 | // If the first column is comprised of all <th> tags, assume it is a data table. |
224 | if (!col && isTHCell) |
225 | ++headersInFirstColumnCount; |
226 | |
227 | // In this case, the developer explicitly assigned a "data" table attribute. |
228 | if (is<HTMLTableCellElement>(*cellElement)) { |
229 | HTMLTableCellElement& tableCellElement = downcast<HTMLTableCellElement>(*cellElement); |
230 | if (!tableCellElement.headers().isEmpty() || !tableCellElement.abbr().isEmpty() |
231 | || !tableCellElement.axis().isEmpty() || !tableCellElement.scope().isEmpty()) |
232 | return true; |
233 | } |
234 | |
235 | // If the author has used ARIA to specify a valid column or row index, assume they want us |
236 | // to treat the table as a data table. |
237 | int axColumnIndex = cellElement->attributeWithoutSynchronization(aria_colindexAttr).toInt(); |
238 | if (axColumnIndex >= 1) |
239 | return true; |
240 | |
241 | int axRowIndex = cellElement->attributeWithoutSynchronization(aria_rowindexAttr).toInt(); |
242 | if (axRowIndex >= 1) |
243 | return true; |
244 | |
245 | if (auto cellParentElement = cellElement->parentElement()) { |
246 | axRowIndex = cellParentElement->attributeWithoutSynchronization(aria_rowindexAttr).toInt(); |
247 | if (axRowIndex >= 1) |
248 | return true; |
249 | } |
250 | |
251 | // If the author has used ARIA to specify a column or row span, we're supposed to ignore |
252 | // the value for the purposes of exposing the span. But assume they want us to treat the |
253 | // table as a data table. |
254 | int axColumnSpan = cellElement->attributeWithoutSynchronization(aria_colspanAttr).toInt(); |
255 | if (axColumnSpan >= 1) |
256 | return true; |
257 | |
258 | int axRowSpan = cellElement->attributeWithoutSynchronization(aria_rowspanAttr).toInt(); |
259 | if (axRowSpan >= 1) |
260 | return true; |
261 | |
262 | const RenderStyle& renderStyle = cell->style(); |
263 | |
264 | // If the empty-cells style is set, we'll call it a data table. |
265 | if (renderStyle.emptyCells() == EmptyCell::Hide) |
266 | return true; |
267 | |
268 | // If a cell has matching bordered sides, call it a (fully) bordered cell. |
269 | if ((cell->borderTop() > 0 && cell->borderBottom() > 0) |
270 | || (cell->borderLeft() > 0 && cell->borderRight() > 0)) |
271 | ++borderedCellCount; |
272 | |
273 | // Also keep track of each individual border, so we can catch tables where most |
274 | // cells have a bottom border, for example. |
275 | if (cell->borderTop() > 0) |
276 | ++cellsWithTopBorder; |
277 | if (cell->borderBottom() > 0) |
278 | ++cellsWithBottomBorder; |
279 | if (cell->borderLeft() > 0) |
280 | ++cellsWithLeftBorder; |
281 | if (cell->borderRight() > 0) |
282 | ++cellsWithRightBorder; |
283 | |
284 | // If the cell has a different color from the table and there is cell spacing, |
285 | // then it is probably a data table cell (spacing and colors take the place of borders). |
286 | Color cellColor = renderStyle.visitedDependentColor(CSSPropertyBackgroundColor); |
287 | if (table.hBorderSpacing() > 0 && table.vBorderSpacing() > 0 |
288 | && tableBGColor != cellColor && cellColor.alpha() != 1) |
289 | ++backgroundDifferenceCellCount; |
290 | |
291 | // If we've found 10 "good" cells, we don't need to keep searching. |
292 | if (borderedCellCount >= 10 || backgroundDifferenceCellCount >= 10) |
293 | return true; |
294 | |
295 | // For the first 5 rows, cache the background color so we can check if this table has zebra-striped rows. |
296 | if (row < 5 && row == alternatingRowColorCount) { |
297 | RenderElement* renderRow = cell->parent(); |
298 | if (!is<RenderTableRow>(renderRow)) |
299 | continue; |
300 | const RenderStyle& rowRenderStyle = renderRow->style(); |
301 | Color rowColor = rowRenderStyle.visitedDependentColor(CSSPropertyBackgroundColor); |
302 | alternatingRowColors[alternatingRowColorCount] = rowColor; |
303 | ++alternatingRowColorCount; |
304 | } |
305 | } |
306 | |
307 | if (!row && headersInFirstRowCount == numCols && numCols > 1) |
308 | return true; |
309 | } |
310 | |
311 | if (headersInFirstColumnCount == numRows && numRows > 1) |
312 | return true; |
313 | |
314 | // if there is less than two valid cells, it's not a data table |
315 | if (validCellCount <= 1) |
316 | return false; |
317 | |
318 | // half of the cells had borders, it's a data table |
319 | unsigned neededCellCount = validCellCount / 2; |
320 | if (borderedCellCount >= neededCellCount |
321 | || cellsWithTopBorder >= neededCellCount |
322 | || cellsWithBottomBorder >= neededCellCount |
323 | || cellsWithLeftBorder >= neededCellCount |
324 | || cellsWithRightBorder >= neededCellCount) |
325 | return true; |
326 | |
327 | // half had different background colors, it's a data table |
328 | if (backgroundDifferenceCellCount >= neededCellCount) |
329 | return true; |
330 | |
331 | // Check if there is an alternating row background color indicating a zebra striped style pattern. |
332 | if (alternatingRowColorCount > 2) { |
333 | Color firstColor = alternatingRowColors[0]; |
334 | for (int k = 1; k < alternatingRowColorCount; k++) { |
335 | // If an odd row was the same color as the first row, its not alternating. |
336 | if (k % 2 == 1 && alternatingRowColors[k] == firstColor) |
337 | return false; |
338 | // If an even row is not the same as the first row, its not alternating. |
339 | if (!(k % 2) && alternatingRowColors[k] != firstColor) |
340 | return false; |
341 | } |
342 | return true; |
343 | } |
344 | |
345 | return false; |
346 | } |
347 | |
348 | bool AccessibilityTable::computeIsTableExposableThroughAccessibility() const |
349 | { |
350 | // The following is a heuristic used to determine if a |
351 | // <table> should be exposed as an AXTable. The goal |
352 | // is to only show "data" tables. |
353 | |
354 | if (!m_renderer) |
355 | return false; |
356 | |
357 | // If the developer assigned an aria role to this, then we |
358 | // shouldn't expose it as a table, unless, of course, the aria |
359 | // role is a table. |
360 | if (hasARIARole()) |
361 | return false; |
362 | |
363 | return isDataTable(); |
364 | } |
365 | |
366 | void AccessibilityTable::clearChildren() |
367 | { |
368 | AccessibilityRenderObject::clearChildren(); |
369 | m_rows.clear(); |
370 | m_columns.clear(); |
371 | |
372 | if (m_headerContainer) { |
373 | m_headerContainer->detachFromParent(); |
374 | m_headerContainer = nullptr; |
375 | } |
376 | } |
377 | |
378 | void AccessibilityTable::addChildren() |
379 | { |
380 | if (!isExposableThroughAccessibility()) { |
381 | AccessibilityRenderObject::addChildren(); |
382 | return; |
383 | } |
384 | |
385 | ASSERT(!m_haveChildren); |
386 | |
387 | m_haveChildren = true; |
388 | if (!is<RenderTable>(renderer())) |
389 | return; |
390 | |
391 | RenderTable& table = downcast<RenderTable>(*m_renderer); |
392 | // Go through all the available sections to pull out the rows and add them as children. |
393 | table.recalcSectionsIfNeeded(); |
394 | |
395 | if (HTMLTableElement* tableElement = this->tableElement()) { |
396 | if (auto caption = tableElement->caption()) { |
397 | AccessibilityObject* axCaption = axObjectCache()->getOrCreate(caption.get()); |
398 | if (axCaption && !axCaption->accessibilityIsIgnored()) |
399 | m_children.append(axCaption); |
400 | } |
401 | } |
402 | |
403 | unsigned maxColumnCount = 0; |
404 | RenderTableSection* = table.footer(); |
405 | |
406 | for (RenderTableSection* tableSection = table.topSection(); tableSection; tableSection = table.sectionBelow(tableSection, SkipEmptySections)) { |
407 | if (tableSection == footer) |
408 | continue; |
409 | addChildrenFromSection(tableSection, maxColumnCount); |
410 | } |
411 | |
412 | // Process the footer last, in case it was ordered earlier in the DOM. |
413 | if (footer) |
414 | addChildrenFromSection(footer, maxColumnCount); |
415 | |
416 | AXObjectCache* axCache = m_renderer->document().axObjectCache(); |
417 | // make the columns based on the number of columns in the first body |
418 | unsigned length = maxColumnCount; |
419 | for (unsigned i = 0; i < length; ++i) { |
420 | auto& column = downcast<AccessibilityTableColumn>(*axCache->getOrCreate(AccessibilityRole::Column)); |
421 | column.setColumnIndex((int)i); |
422 | column.setParent(this); |
423 | m_columns.append(&column); |
424 | if (!column.accessibilityIsIgnored()) |
425 | m_children.append(&column); |
426 | } |
427 | |
428 | AccessibilityObject* = headerContainer(); |
429 | if (headerContainerObject && !headerContainerObject->accessibilityIsIgnored()) |
430 | m_children.append(headerContainerObject); |
431 | |
432 | // Sometimes the cell gets the wrong role initially because it is created before the parent |
433 | // determines whether it is an accessibility table. Iterate all the cells and allow them to |
434 | // update their roles now that the table knows its status. |
435 | // see bug: https://bugs.webkit.org/show_bug.cgi?id=147001 |
436 | for (const auto& row : m_rows) { |
437 | for (const auto& cell : row->children()) |
438 | cell->updateAccessibilityRole(); |
439 | } |
440 | |
441 | } |
442 | |
443 | void AccessibilityTable::addTableCellChild(AccessibilityObject* rowObject, HashSet<AccessibilityObject*>& appendedRows, unsigned& columnCount) |
444 | { |
445 | if (!rowObject || !is<AccessibilityTableRow>(*rowObject)) |
446 | return; |
447 | |
448 | auto& row = downcast<AccessibilityTableRow>(*rowObject); |
449 | // We need to check every cell for a new row, because cell spans |
450 | // can cause us to miss rows if we just check the first column. |
451 | if (appendedRows.contains(&row)) |
452 | return; |
453 | |
454 | row.setRowIndex(static_cast<int>(m_rows.size())); |
455 | m_rows.append(&row); |
456 | if (!row.accessibilityIsIgnored()) |
457 | m_children.append(&row); |
458 | appendedRows.add(&row); |
459 | |
460 | // store the maximum number of columns |
461 | unsigned rowCellCount = row.children().size(); |
462 | if (rowCellCount > columnCount) |
463 | columnCount = rowCellCount; |
464 | } |
465 | |
466 | void AccessibilityTable::addChildrenFromSection(RenderTableSection* tableSection, unsigned& maxColumnCount) |
467 | { |
468 | ASSERT(tableSection); |
469 | if (!tableSection) |
470 | return; |
471 | |
472 | AXObjectCache* axCache = m_renderer->document().axObjectCache(); |
473 | HashSet<AccessibilityObject*> appendedRows; |
474 | unsigned numRows = tableSection->numRows(); |
475 | for (unsigned rowIndex = 0; rowIndex < numRows; ++rowIndex) { |
476 | |
477 | RenderTableRow* renderRow = tableSection->rowRendererAt(rowIndex); |
478 | if (!renderRow) |
479 | continue; |
480 | |
481 | AccessibilityObject& rowObject = *axCache->getOrCreate(renderRow); |
482 | |
483 | // If the row is anonymous, we should dive deeper into the descendants to try to find a valid row. |
484 | if (renderRow->isAnonymous()) { |
485 | Deque<AccessibilityObject*> queue; |
486 | queue.append(&rowObject); |
487 | |
488 | while (!queue.isEmpty()) { |
489 | AccessibilityObject* obj = queue.takeFirst(); |
490 | if (obj->node() && is<AccessibilityTableRow>(*obj)) { |
491 | addTableCellChild(obj, appendedRows, maxColumnCount); |
492 | continue; |
493 | } |
494 | for (auto* child = obj->firstChild(); child; child = child->nextSibling()) |
495 | queue.append(child); |
496 | } |
497 | } else |
498 | addTableCellChild(&rowObject, appendedRows, maxColumnCount); |
499 | } |
500 | |
501 | maxColumnCount = std::max(tableSection->numColumns(), maxColumnCount); |
502 | } |
503 | |
504 | AccessibilityObject* AccessibilityTable::() |
505 | { |
506 | if (m_headerContainer) |
507 | return m_headerContainer.get(); |
508 | |
509 | auto& = downcast<AccessibilityMockObject>(*axObjectCache()->getOrCreate(AccessibilityRole::TableHeaderContainer)); |
510 | tableHeader.setParent(this); |
511 | |
512 | m_headerContainer = &tableHeader; |
513 | return m_headerContainer.get(); |
514 | } |
515 | |
516 | const AccessibilityObject::AccessibilityChildrenVector& AccessibilityTable::columns() |
517 | { |
518 | updateChildrenIfNecessary(); |
519 | |
520 | return m_columns; |
521 | } |
522 | |
523 | const AccessibilityObject::AccessibilityChildrenVector& AccessibilityTable::rows() |
524 | { |
525 | updateChildrenIfNecessary(); |
526 | |
527 | return m_rows; |
528 | } |
529 | |
530 | void AccessibilityTable::columnHeaders(AccessibilityChildrenVector& ) |
531 | { |
532 | if (!m_renderer) |
533 | return; |
534 | |
535 | updateChildrenIfNecessary(); |
536 | |
537 | // Sometimes m_columns can be reset during the iteration, we cache it here to be safe. |
538 | AccessibilityChildrenVector columnsCopy = m_columns; |
539 | |
540 | for (const auto& column : columnsCopy) { |
541 | if (AccessibilityObject* = downcast<AccessibilityTableColumn>(*column).headerObject()) |
542 | headers.append(header); |
543 | } |
544 | } |
545 | |
546 | void AccessibilityTable::(AccessibilityChildrenVector& ) |
547 | { |
548 | if (!m_renderer) |
549 | return; |
550 | |
551 | updateChildrenIfNecessary(); |
552 | |
553 | // Sometimes m_rows can be reset during the iteration, we cache it here to be safe. |
554 | AccessibilityChildrenVector rowsCopy = m_rows; |
555 | |
556 | for (const auto& row : rowsCopy) { |
557 | if (AccessibilityObject* = downcast<AccessibilityTableRow>(*row).headerObject()) |
558 | headers.append(header); |
559 | } |
560 | } |
561 | |
562 | void AccessibilityTable::visibleRows(AccessibilityChildrenVector& rows) |
563 | { |
564 | if (!m_renderer) |
565 | return; |
566 | |
567 | updateChildrenIfNecessary(); |
568 | |
569 | for (const auto& row : m_rows) { |
570 | if (row && !row->isOffScreen()) |
571 | rows.append(row); |
572 | } |
573 | } |
574 | |
575 | void AccessibilityTable::cells(AccessibilityObject::AccessibilityChildrenVector& cells) |
576 | { |
577 | if (!m_renderer) |
578 | return; |
579 | |
580 | updateChildrenIfNecessary(); |
581 | |
582 | for (const auto& row : m_rows) |
583 | cells.appendVector(row->children()); |
584 | } |
585 | |
586 | unsigned AccessibilityTable::columnCount() |
587 | { |
588 | updateChildrenIfNecessary(); |
589 | |
590 | return m_columns.size(); |
591 | } |
592 | |
593 | unsigned AccessibilityTable::rowCount() |
594 | { |
595 | updateChildrenIfNecessary(); |
596 | |
597 | return m_rows.size(); |
598 | } |
599 | |
600 | int AccessibilityTable::tableLevel() const |
601 | { |
602 | int level = 0; |
603 | for (AccessibilityObject* obj = static_cast<AccessibilityObject*>(const_cast<AccessibilityTable*>(this)); obj; obj = obj->parentObject()) { |
604 | if (is<AccessibilityTable>(*obj) && downcast<AccessibilityTable>(*obj).isExposableThroughAccessibility()) |
605 | ++level; |
606 | } |
607 | |
608 | return level; |
609 | } |
610 | |
611 | AccessibilityTableCell* AccessibilityTable::cellForColumnAndRow(unsigned column, unsigned row) |
612 | { |
613 | updateChildrenIfNecessary(); |
614 | if (column >= columnCount() || row >= rowCount()) |
615 | return nullptr; |
616 | |
617 | // Iterate backwards through the rows in case the desired cell has a rowspan and exists in a previous row. |
618 | for (unsigned rowIndexCounter = row + 1; rowIndexCounter > 0; --rowIndexCounter) { |
619 | unsigned rowIndex = rowIndexCounter - 1; |
620 | const auto& children = m_rows[rowIndex]->children(); |
621 | // Since some cells may have colspans, we have to check the actual range of each |
622 | // cell to determine which is the right one. |
623 | for (unsigned colIndexCounter = std::min(static_cast<unsigned>(children.size()), column + 1); colIndexCounter > 0; --colIndexCounter) { |
624 | unsigned colIndex = colIndexCounter - 1; |
625 | AccessibilityObject* child = children[colIndex].get(); |
626 | ASSERT(is<AccessibilityTableCell>(*child)); |
627 | if (!is<AccessibilityTableCell>(*child)) |
628 | continue; |
629 | |
630 | std::pair<unsigned, unsigned> columnRange; |
631 | std::pair<unsigned, unsigned> rowRange; |
632 | auto& tableCellChild = downcast<AccessibilityTableCell>(*child); |
633 | tableCellChild.columnIndexRange(columnRange); |
634 | tableCellChild.rowIndexRange(rowRange); |
635 | |
636 | if ((column >= columnRange.first && column < (columnRange.first + columnRange.second)) |
637 | && (row >= rowRange.first && row < (rowRange.first + rowRange.second))) |
638 | return &tableCellChild; |
639 | } |
640 | } |
641 | |
642 | return nullptr; |
643 | } |
644 | |
645 | AccessibilityRole AccessibilityTable::roleValue() const |
646 | { |
647 | if (!isExposableThroughAccessibility()) |
648 | return AccessibilityRenderObject::roleValue(); |
649 | |
650 | AccessibilityRole ariaRole = ariaRoleAttribute(); |
651 | if (ariaRole == AccessibilityRole::Grid || ariaRole == AccessibilityRole::TreeGrid) |
652 | return ariaRole; |
653 | |
654 | return AccessibilityRole::Table; |
655 | } |
656 | |
657 | bool AccessibilityTable::computeAccessibilityIsIgnored() const |
658 | { |
659 | AccessibilityObjectInclusion decision = defaultObjectInclusion(); |
660 | if (decision == AccessibilityObjectInclusion::IncludeObject) |
661 | return false; |
662 | if (decision == AccessibilityObjectInclusion::IgnoreObject) |
663 | return true; |
664 | |
665 | if (!isExposableThroughAccessibility()) |
666 | return AccessibilityRenderObject::computeAccessibilityIsIgnored(); |
667 | |
668 | return false; |
669 | } |
670 | |
671 | void AccessibilityTable::titleElementText(Vector<AccessibilityText>& textOrder) const |
672 | { |
673 | String title = this->title(); |
674 | if (!title.isEmpty()) |
675 | textOrder.append(AccessibilityText(title, AccessibilityTextSource::LabelByElement)); |
676 | } |
677 | |
678 | String AccessibilityTable::title() const |
679 | { |
680 | if (!isExposableThroughAccessibility()) |
681 | return AccessibilityRenderObject::title(); |
682 | |
683 | String title; |
684 | if (!m_renderer) |
685 | return title; |
686 | |
687 | // see if there is a caption |
688 | Node* tableElement = m_renderer->node(); |
689 | if (is<HTMLTableElement>(tableElement)) { |
690 | if (auto caption = downcast<HTMLTableElement>(*tableElement).caption()) |
691 | title = caption->innerText(); |
692 | } |
693 | |
694 | // try the standard |
695 | if (title.isEmpty()) |
696 | title = AccessibilityRenderObject::title(); |
697 | |
698 | return title; |
699 | } |
700 | |
701 | int AccessibilityTable::axColumnCount() const |
702 | { |
703 | const AtomicString& colCountValue = getAttribute(aria_colcountAttr); |
704 | int colCountInt = colCountValue.toInt(); |
705 | // The ARIA spec states, "Authors must set the value of aria-colcount to an integer equal to the |
706 | // number of columns in the full table. If the total number of columns is unknown, authors must |
707 | // set the value of aria-colcount to -1 to indicate that the value should not be calculated by |
708 | // the user agent." If we have a valid value, make it available to platforms. |
709 | if (colCountInt == -1 || colCountInt >= (int)m_columns.size()) |
710 | return colCountInt; |
711 | |
712 | return 0; |
713 | } |
714 | |
715 | int AccessibilityTable::axRowCount() const |
716 | { |
717 | const AtomicString& rowCountValue = getAttribute(aria_rowcountAttr); |
718 | int rowCountInt = rowCountValue.toInt(); |
719 | // The ARIA spec states, "Authors must set the value of aria-rowcount to an integer equal to the |
720 | // number of rows in the full table. If the total number of rows is unknown, authors must set |
721 | // the value of aria-rowcount to -1 to indicate that the value should not be calculated by the |
722 | // user agent." If we have a valid value, make it available to platforms. |
723 | if (rowCountInt == -1 || rowCountInt >= (int)m_rows.size()) |
724 | return rowCountInt; |
725 | |
726 | return 0; |
727 | } |
728 | |
729 | } // namespace WebCore |
730 | |