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 "AccessibilityTableCell.h"
31
32#include "AXObjectCache.h"
33#include "AccessibilityTable.h"
34#include "AccessibilityTableRow.h"
35#include "ElementIterator.h"
36#include "HTMLElement.h"
37#include "HTMLNames.h"
38#include "RenderObject.h"
39#include "RenderTableCell.h"
40
41namespace WebCore {
42
43using namespace HTMLNames;
44
45AccessibilityTableCell::AccessibilityTableCell(RenderObject* renderer)
46 : AccessibilityRenderObject(renderer)
47 , m_axColIndexFromRow(-1)
48{
49}
50
51AccessibilityTableCell::~AccessibilityTableCell() = default;
52
53Ref<AccessibilityTableCell> AccessibilityTableCell::create(RenderObject* renderer)
54{
55 return adoptRef(*new AccessibilityTableCell(renderer));
56}
57
58bool AccessibilityTableCell::computeAccessibilityIsIgnored() const
59{
60 AccessibilityObjectInclusion decision = defaultObjectInclusion();
61 if (decision == AccessibilityObjectInclusion::IncludeObject)
62 return false;
63 if (decision == AccessibilityObjectInclusion::IgnoreObject)
64 return true;
65
66 // Ignore anonymous table cells as long as they're not in a table (ie. when display:table is used).
67 RenderObject* renderTable = is<RenderTableCell>(renderer()) ? downcast<RenderTableCell>(*m_renderer).table() : nullptr;
68 bool inTable = renderTable && renderTable->node() && (renderTable->node()->hasTagName(tableTag) || nodeHasRole(renderTable->node(), "grid"));
69 if (!node() && !inTable)
70 return true;
71
72 if (!isTableCell())
73 return AccessibilityRenderObject::computeAccessibilityIsIgnored();
74
75 return false;
76}
77
78AccessibilityTable* AccessibilityTableCell::parentTable() const
79{
80 if (!is<RenderTableCell>(renderer()))
81 return nullptr;
82
83 // If the document no longer exists, we might not have an axObjectCache.
84 if (!axObjectCache())
85 return nullptr;
86
87 // Do not use getOrCreate. parentTable() can be called while the render tree is being modified
88 // by javascript, and creating a table element may try to access the render tree while in a bad state.
89 // By using only get() implies that the AXTable must be created before AXTableCells. This should
90 // always be the case when AT clients access a table.
91 // https://bugs.webkit.org/show_bug.cgi?id=42652
92 AccessibilityObject* parentTable = axObjectCache()->get(downcast<RenderTableCell>(*m_renderer).table());
93 if (!is<AccessibilityTable>(parentTable))
94 return nullptr;
95
96 // The RenderTableCell's table() object might be anonymous sometimes. We should handle it gracefully
97 // by finding the right table.
98 if (!parentTable->node()) {
99 for (AccessibilityObject* parent = parentObject(); parent; parent = parent->parentObject()) {
100 // If this is a non-anonymous table object, but not an accessibility table, we should stop because
101 // we don't want to choose another ancestor table as this cell's table.
102 if (is<AccessibilityTable>(*parent)) {
103 auto& parentTable = downcast<AccessibilityTable>(*parent);
104 if (parentTable.isExposableThroughAccessibility())
105 return &parentTable;
106 if (parentTable.node())
107 break;
108 }
109 }
110 return nullptr;
111 }
112
113 return downcast<AccessibilityTable>(parentTable);
114}
115
116bool AccessibilityTableCell::isTableCell() const
117{
118 // If the parent table is an accessibility table, then we are a table cell.
119 // This used to check if the unignoredParent was a row, but that exploded performance if
120 // this was in nested tables. This check should be just as good.
121 AccessibilityObject* parentTable = this->parentTable();
122 return is<AccessibilityTable>(parentTable) && downcast<AccessibilityTable>(*parentTable).isExposableThroughAccessibility();
123}
124
125AccessibilityRole AccessibilityTableCell::determineAccessibilityRole()
126{
127 // AccessibilityRenderObject::determineAccessibleRole provides any ARIA-supplied
128 // role, falling back on the role to be used if we determine here that the element
129 // should not be exposed as a cell. Thus if we already know it's a cell, return that.
130 AccessibilityRole defaultRole = AccessibilityRenderObject::determineAccessibilityRole();
131 if (defaultRole == AccessibilityRole::ColumnHeader || defaultRole == AccessibilityRole::RowHeader || defaultRole == AccessibilityRole::Cell || defaultRole == AccessibilityRole::GridCell)
132 return defaultRole;
133
134 if (!isTableCell())
135 return defaultRole;
136 if (isColumnHeaderCell())
137 return AccessibilityRole::ColumnHeader;
138 if (isRowHeaderCell())
139 return AccessibilityRole::RowHeader;
140
141 return AccessibilityRole::Cell;
142}
143
144bool AccessibilityTableCell::isTableHeaderCell() const
145{
146 return node() && node()->hasTagName(thTag);
147}
148
149bool AccessibilityTableCell::isColumnHeaderCell() const
150{
151 const AtomicString& scope = getAttribute(scopeAttr);
152 if (scope == "col" || scope == "colgroup")
153 return true;
154 if (scope == "row" || scope == "rowgroup")
155 return false;
156 if (!isTableHeaderCell())
157 return false;
158
159 // We are in a situation after checking the scope attribute.
160 // It is an attempt to resolve the type of th element without support in the specification.
161 // Checking tableTag and tbodyTag allows to check the case of direct row placement in the table and lets stop the loop at the table level.
162 for (Node* parentNode = node(); parentNode; parentNode = parentNode->parentNode()) {
163 if (parentNode->hasTagName(theadTag))
164 return true;
165 if (parentNode->hasTagName(tfootTag))
166 return false;
167 if (parentNode->hasTagName(tableTag) || parentNode->hasTagName(tbodyTag)) {
168 std::pair<unsigned, unsigned> rowRange;
169 rowIndexRange(rowRange);
170 if (!rowRange.first)
171 return true;
172 return false;
173 }
174 }
175 return false;
176}
177
178bool AccessibilityTableCell::isRowHeaderCell() const
179{
180 const AtomicString& scope = getAttribute(scopeAttr);
181 if (scope == "row" || scope == "rowgroup")
182 return true;
183 if (scope == "col" || scope == "colgroup")
184 return false;
185 if (!isTableHeaderCell())
186 return false;
187
188 // We are in a situation after checking the scope attribute.
189 // It is an attempt to resolve the type of th element without support in the specification.
190 // Checking tableTag allows to check the case of direct row placement in the table and lets stop the loop at the table level.
191 for (Node* parentNode = node(); parentNode; parentNode = parentNode->parentNode()) {
192 if (parentNode->hasTagName(tfootTag) || parentNode->hasTagName(tbodyTag) || parentNode->hasTagName(tableTag)) {
193 std::pair<unsigned, unsigned> colRange;
194 columnIndexRange(colRange);
195 if (!colRange.first)
196 return true;
197 return false;
198 }
199 if (parentNode->hasTagName(theadTag))
200 return false;
201 }
202 return false;
203}
204
205bool AccessibilityTableCell::isTableCellInSameRowGroup(AccessibilityTableCell* otherTableCell)
206{
207 Node* parentNode = node();
208 for ( ; parentNode; parentNode = parentNode->parentNode()) {
209 if (parentNode->hasTagName(theadTag) || parentNode->hasTagName(tbodyTag) || parentNode->hasTagName(tfootTag))
210 break;
211 }
212
213 Node* otherParentNode = otherTableCell->node();
214 for ( ; otherParentNode; otherParentNode = otherParentNode->parentNode()) {
215 if (otherParentNode->hasTagName(theadTag) || otherParentNode->hasTagName(tbodyTag) || otherParentNode->hasTagName(tfootTag))
216 break;
217 }
218
219 return otherParentNode == parentNode;
220}
221
222
223bool AccessibilityTableCell::isTableCellInSameColGroup(AccessibilityTableCell* tableCell)
224{
225 std::pair<unsigned, unsigned> colRange;
226 columnIndexRange(colRange);
227
228 std::pair<unsigned, unsigned> otherColRange;
229 tableCell->columnIndexRange(otherColRange);
230
231 if (colRange.first <= (otherColRange.first + otherColRange.second))
232 return true;
233 return false;
234}
235
236String AccessibilityTableCell::expandedTextValue() const
237{
238 return getAttribute(abbrAttr);
239}
240
241bool AccessibilityTableCell::supportsExpandedTextValue() const
242{
243 return isTableHeaderCell() && hasAttribute(abbrAttr);
244}
245
246void AccessibilityTableCell::columnHeaders(AccessibilityChildrenVector& headers)
247{
248 AccessibilityTable* parent = parentTable();
249 if (!parent)
250 return;
251
252 // Choose columnHeaders as the place where the "headers" attribute is reported.
253 ariaElementsFromAttribute(headers, headersAttr);
254 // If the headers attribute returned valid values, then do not further search for column headers.
255 if (!headers.isEmpty())
256 return;
257
258 std::pair<unsigned, unsigned> rowRange;
259 rowIndexRange(rowRange);
260
261 std::pair<unsigned, unsigned> colRange;
262 columnIndexRange(colRange);
263
264 for (unsigned row = 0; row < rowRange.first; row++) {
265 AccessibilityTableCell* tableCell = parent->cellForColumnAndRow(colRange.first, row);
266 if (!tableCell || tableCell == this || headers.contains(tableCell))
267 continue;
268
269 std::pair<unsigned, unsigned> childRowRange;
270 tableCell->rowIndexRange(childRowRange);
271
272 const AtomicString& scope = tableCell->getAttribute(scopeAttr);
273 if (scope == "colgroup" && isTableCellInSameColGroup(tableCell))
274 headers.append(tableCell);
275 else if (tableCell->isColumnHeaderCell())
276 headers.append(tableCell);
277 }
278}
279
280void AccessibilityTableCell::rowHeaders(AccessibilityChildrenVector& headers)
281{
282 AccessibilityTable* parent = parentTable();
283 if (!parent)
284 return;
285
286 std::pair<unsigned, unsigned> rowRange;
287 rowIndexRange(rowRange);
288
289 std::pair<unsigned, unsigned> colRange;
290 columnIndexRange(colRange);
291
292 for (unsigned column = 0; column < colRange.first; column++) {
293 AccessibilityTableCell* tableCell = parent->cellForColumnAndRow(column, rowRange.first);
294 if (!tableCell || tableCell == this || headers.contains(tableCell))
295 continue;
296
297 const AtomicString& scope = tableCell->getAttribute(scopeAttr);
298 if (scope == "rowgroup" && isTableCellInSameRowGroup(tableCell))
299 headers.append(tableCell);
300 else if (tableCell->isRowHeaderCell())
301 headers.append(tableCell);
302 }
303}
304
305AccessibilityTableRow* AccessibilityTableCell::parentRow() const
306{
307 AccessibilityObject* parent = parentObjectUnignored();
308 if (!is<AccessibilityTableRow>(*parent))
309 return nullptr;
310 return downcast<AccessibilityTableRow>(parent);
311}
312
313void AccessibilityTableCell::rowIndexRange(std::pair<unsigned, unsigned>& rowRange) const
314{
315 if (!is<RenderTableCell>(renderer()))
316 return;
317
318 RenderTableCell& renderCell = downcast<RenderTableCell>(*m_renderer);
319
320 // ARIA 1.1's aria-rowspan attribute is intended for cells and gridcells which are not contained
321 // in a native table. But if we have a valid author-provided value and do not have an explicit
322 // native host language value for the rowspan, expose the ARIA value.
323 rowRange.second = axRowSpan();
324 if (static_cast<int>(rowRange.second) == -1)
325 rowRange.second = renderCell.rowSpan();
326
327 if (AccessibilityTableRow* parentRow = this->parentRow())
328 rowRange.first = parentRow->rowIndex();
329}
330
331void AccessibilityTableCell::columnIndexRange(std::pair<unsigned, unsigned>& columnRange) const
332{
333 if (!is<RenderTableCell>(renderer()))
334 return;
335
336 const RenderTableCell& cell = downcast<RenderTableCell>(*m_renderer);
337 columnRange.first = cell.table()->colToEffCol(cell.col());
338
339 // ARIA 1.1's aria-colspan attribute is intended for cells and gridcells which are not contained
340 // in a native table. But if we have a valid author-provided value and do not have an explicit
341 // native host language value for the colspan, expose the ARIA value.
342 columnRange.second = axColumnSpan();
343 if (static_cast<int>(columnRange.second) != -1)
344 return;
345
346 columnRange.second = cell.table()->colToEffCol(cell.col() + cell.colSpan()) - columnRange.first;
347}
348
349AccessibilityObject* AccessibilityTableCell::titleUIElement() const
350{
351 // Try to find if the first cell in this row is a <th>. If it is,
352 // then it can act as the title ui element. (This is only in the
353 // case when the table is not appearing as an AXTable.)
354 if (isTableCell() || !is<RenderTableCell>(renderer()))
355 return nullptr;
356
357 // Table cells that are th cannot have title ui elements, since by definition
358 // they are title ui elements
359 Node* node = m_renderer->node();
360 if (node && node->hasTagName(thTag))
361 return nullptr;
362
363 RenderTableCell& renderCell = downcast<RenderTableCell>(*m_renderer);
364
365 // If this cell is in the first column, there is no need to continue.
366 int col = renderCell.col();
367 if (!col)
368 return nullptr;
369
370 int row = renderCell.rowIndex();
371
372 RenderTableSection* section = renderCell.section();
373 if (!section)
374 return nullptr;
375
376 RenderTableCell* headerCell = section->primaryCellAt(row, 0);
377 if (!headerCell || headerCell == &renderCell)
378 return nullptr;
379
380 if (!headerCell->element() || !headerCell->element()->hasTagName(thTag))
381 return nullptr;
382
383 return axObjectCache()->getOrCreate(headerCell);
384}
385
386int AccessibilityTableCell::axColumnIndex() const
387{
388 const AtomicString& colIndexValue = getAttribute(aria_colindexAttr);
389 if (colIndexValue.toInt() >= 1)
390 return colIndexValue.toInt();
391
392 // "ARIA 1.1: If the set of columns which is present in the DOM is contiguous, and if there are no cells which span more than one row
393 // or column in that set, then authors may place aria-colindex on each row, setting the value to the index of the first column of the set."
394 // Here, we let its parent row to set its index beforehand, so we don't have to go through the siblings to calculate the index.
395 AccessibilityTableRow* parentRow = this->parentRow();
396 if (parentRow && m_axColIndexFromRow != -1)
397 return m_axColIndexFromRow;
398
399 return -1;
400}
401
402int AccessibilityTableCell::axRowIndex() const
403{
404 // ARIA 1.1: Authors should place aria-rowindex on each row. Authors may also place
405 // aria-rowindex on all of the children or owned elements of each row.
406 const AtomicString& rowIndexValue = getAttribute(aria_rowindexAttr);
407 if (rowIndexValue.toInt() >= 1)
408 return rowIndexValue.toInt();
409
410 if (AccessibilityTableRow* parentRow = this->parentRow())
411 return parentRow->axRowIndex();
412
413 return -1;
414}
415
416int AccessibilityTableCell::axColumnSpan() const
417{
418 // According to the ARIA spec, "If aria-colpan is used on an element for which the host language
419 // provides an equivalent attribute, user agents must ignore the value of aria-colspan."
420 if (hasAttribute(colspanAttr))
421 return -1;
422
423 const AtomicString& colSpanValue = getAttribute(aria_colspanAttr);
424 // ARIA 1.1: Authors must set the value of aria-colspan to an integer greater than or equal to 1.
425 if (colSpanValue.toInt() >= 1)
426 return colSpanValue.toInt();
427
428 return -1;
429}
430
431int AccessibilityTableCell::axRowSpan() const
432{
433 // According to the ARIA spec, "If aria-rowspan is used on an element for which the host language
434 // provides an equivalent attribute, user agents must ignore the value of aria-rowspan."
435 if (hasAttribute(rowspanAttr))
436 return -1;
437
438 const AtomicString& rowSpanValue = getAttribute(aria_rowspanAttr);
439
440 // ARIA 1.1: Authors must set the value of aria-rowspan to an integer greater than or equal to 0.
441 // Setting the value to 0 indicates that the cell or gridcell is to span all the remaining rows in the row group.
442 if (rowSpanValue == "0")
443 return 0;
444 if (rowSpanValue.toInt() >= 1)
445 return rowSpanValue.toInt();
446
447 return -1;
448}
449
450} // namespace WebCore
451