AccessibilityTable.cpp [plain text]
#include "config.h"
#include "AccessibilityTable.h"
#include "AXObjectCache.h"
#include "AccessibilityTableCell.h"
#include "AccessibilityTableColumn.h"
#include "AccessibilityTableHeaderContainer.h"
#include "AccessibilityTableRow.h"
#include "ElementIterator.h"
#include "HTMLNames.h"
#include "HTMLTableCaptionElement.h"
#include "HTMLTableCellElement.h"
#include "HTMLTableElement.h"
#include "HTMLTableSectionElement.h"
#include "RenderObject.h"
#include "RenderTable.h"
#include "RenderTableCell.h"
#include "RenderTableSection.h"
#include <wtf/Deque.h>
namespace WebCore {
using namespace HTMLNames;
AccessibilityTable::AccessibilityTable(RenderObject* renderer)
: AccessibilityRenderObject(renderer)
, m_headerContainer(nullptr)
, m_isExposableThroughAccessibility(true)
{
}
AccessibilityTable::~AccessibilityTable() = default;
void AccessibilityTable::init()
{
AccessibilityRenderObject::init();
m_isExposableThroughAccessibility = computeIsTableExposableThroughAccessibility();
}
Ref<AccessibilityTable> AccessibilityTable::create(RenderObject* renderer)
{
return adoptRef(*new AccessibilityTable(renderer));
}
bool AccessibilityTable::hasARIARole() const
{
if (!m_renderer)
return false;
AccessibilityRole ariaRole = ariaRoleAttribute();
if (ariaRole != AccessibilityRole::Unknown)
return true;
return false;
}
bool AccessibilityTable::isExposableThroughAccessibility() const
{
if (!m_renderer)
return false;
return m_isExposableThroughAccessibility;
}
HTMLTableElement* AccessibilityTable::tableElement() const
{
if (!is<RenderTable>(*m_renderer))
return nullptr;
RenderTable& table = downcast<RenderTable>(*m_renderer);
if (is<HTMLTableElement>(table.element()))
return downcast<HTMLTableElement>(table.element());
auto* firstChild = table.firstChild();
if (!firstChild || !firstChild->node())
return nullptr;
if (is<HTMLTableElement>(*firstChild->node()))
return downcast<HTMLTableElement>(firstChild->node());
return ancestorsOfType<HTMLTableElement>(*(firstChild->node())).first();
}
bool AccessibilityTable::isDataTable() const
{
if (!m_renderer)
return false;
if (hasARIARole())
return false;
if (node() && node()->hasEditableStyle())
return true;
if (!is<RenderTable>(*m_renderer))
return false;
if (HTMLTableElement* tableElement = this->tableElement()) {
if (!tableElement->summary().isEmpty() || tableElement->tHead() || tableElement->tFoot() || tableElement->caption())
return true;
if (!tableElement->rules().isEmpty())
return true;
for (const auto& child : childrenOfType<HTMLElement>(*tableElement)) {
if (child.hasTagName(colTag) || child.hasTagName(colgroupTag))
return true;
}
}
if (!hasTagName(tableTag))
return false;
int axColumnCount = getAttribute(aria_colcountAttr).toInt();
if (axColumnCount == -1 || axColumnCount > 0)
return true;
int axRowCount = getAttribute(aria_rowcountAttr).toInt();
if (axRowCount == -1 || axRowCount > 0)
return true;
RenderTable& table = downcast<RenderTable>(*m_renderer);
table.recalcSectionsIfNeeded();
RenderTableSection* firstBody = table.firstBody();
if (!firstBody)
return false;
int numCols = firstBody->numColumns();
int numRows = firstBody->numRows();
if (numRows >= 20)
return true;
const RenderStyle& tableStyle = table.style();
Color tableBGColor = tableStyle.visitedDependentColor(CSSPropertyBackgroundColor);
unsigned validCellCount = 0;
unsigned borderedCellCount = 0;
unsigned backgroundDifferenceCellCount = 0;
unsigned cellsWithTopBorder = 0;
unsigned cellsWithBottomBorder = 0;
unsigned cellsWithLeftBorder = 0;
unsigned cellsWithRightBorder = 0;
Color alternatingRowColors[5];
int alternatingRowColorCount = 0;
int headersInFirstColumnCount = 0;
for (int row = 0; row < numRows; ++row) {
int headersInFirstRowCount = 0;
for (int col = 0; col < numCols; ++col) {
RenderTableCell* cell = firstBody->primaryCellAt(row, col);
if (!cell)
continue;
Element* cellElement = cell->element();
if (!cellElement)
continue;
if (cell->width() < 1 || cell->height() < 1)
continue;
++validCellCount;
bool isTHCell = cellElement->hasTagName(thTag);
if (!row && isTHCell)
++headersInFirstRowCount;
if (!col && isTHCell)
++headersInFirstColumnCount;
if (is<HTMLTableCellElement>(*cellElement)) {
HTMLTableCellElement& tableCellElement = downcast<HTMLTableCellElement>(*cellElement);
if (!tableCellElement.headers().isEmpty() || !tableCellElement.abbr().isEmpty()
|| !tableCellElement.axis().isEmpty() || !tableCellElement.scope().isEmpty())
return true;
}
int axColumnIndex = cellElement->attributeWithoutSynchronization(aria_colindexAttr).toInt();
if (axColumnIndex >= 1)
return true;
int axRowIndex = cellElement->attributeWithoutSynchronization(aria_rowindexAttr).toInt();
if (axRowIndex >= 1)
return true;
if (auto cellParentElement = cellElement->parentElement()) {
axRowIndex = cellParentElement->attributeWithoutSynchronization(aria_rowindexAttr).toInt();
if (axRowIndex >= 1)
return true;
}
int axColumnSpan = cellElement->attributeWithoutSynchronization(aria_colspanAttr).toInt();
if (axColumnSpan >= 1)
return true;
int axRowSpan = cellElement->attributeWithoutSynchronization(aria_rowspanAttr).toInt();
if (axRowSpan >= 1)
return true;
const RenderStyle& renderStyle = cell->style();
if (renderStyle.emptyCells() == EmptyCell::Hide)
return true;
if ((cell->borderTop() > 0 && cell->borderBottom() > 0)
|| (cell->borderLeft() > 0 && cell->borderRight() > 0))
++borderedCellCount;
if (cell->borderTop() > 0)
++cellsWithTopBorder;
if (cell->borderBottom() > 0)
++cellsWithBottomBorder;
if (cell->borderLeft() > 0)
++cellsWithLeftBorder;
if (cell->borderRight() > 0)
++cellsWithRightBorder;
Color cellColor = renderStyle.visitedDependentColor(CSSPropertyBackgroundColor);
if (table.hBorderSpacing() > 0 && table.vBorderSpacing() > 0
&& tableBGColor != cellColor && cellColor.alpha() != 1)
++backgroundDifferenceCellCount;
if (borderedCellCount >= 10 || backgroundDifferenceCellCount >= 10)
return true;
if (row < 5 && row == alternatingRowColorCount) {
RenderElement* renderRow = cell->parent();
if (!is<RenderTableRow>(renderRow))
continue;
const RenderStyle& rowRenderStyle = renderRow->style();
Color rowColor = rowRenderStyle.visitedDependentColor(CSSPropertyBackgroundColor);
alternatingRowColors[alternatingRowColorCount] = rowColor;
++alternatingRowColorCount;
}
}
if (!row && headersInFirstRowCount == numCols && numCols > 1)
return true;
}
if (headersInFirstColumnCount == numRows && numRows > 1)
return true;
if (validCellCount <= 1)
return false;
unsigned neededCellCount = validCellCount / 2;
if (borderedCellCount >= neededCellCount
|| cellsWithTopBorder >= neededCellCount
|| cellsWithBottomBorder >= neededCellCount
|| cellsWithLeftBorder >= neededCellCount
|| cellsWithRightBorder >= neededCellCount)
return true;
if (backgroundDifferenceCellCount >= neededCellCount)
return true;
if (alternatingRowColorCount > 2) {
Color firstColor = alternatingRowColors[0];
for (int k = 1; k < alternatingRowColorCount; k++) {
if (k % 2 == 1 && alternatingRowColors[k] == firstColor)
return false;
if (!(k % 2) && alternatingRowColors[k] != firstColor)
return false;
}
return true;
}
return false;
}
bool AccessibilityTable::computeIsTableExposableThroughAccessibility() const
{
if (!m_renderer)
return false;
if (hasARIARole())
return false;
return isDataTable();
}
void AccessibilityTable::clearChildren()
{
AccessibilityRenderObject::clearChildren();
m_rows.clear();
m_columns.clear();
if (m_headerContainer) {
m_headerContainer->detachFromParent();
m_headerContainer = nullptr;
}
}
void AccessibilityTable::addChildren()
{
if (!isExposableThroughAccessibility()) {
AccessibilityRenderObject::addChildren();
return;
}
ASSERT(!m_haveChildren);
m_haveChildren = true;
if (!is<RenderTable>(renderer()))
return;
RenderTable& table = downcast<RenderTable>(*m_renderer);
table.recalcSectionsIfNeeded();
if (HTMLTableElement* tableElement = this->tableElement()) {
if (auto caption = tableElement->caption()) {
AccessibilityObject* axCaption = axObjectCache()->getOrCreate(caption.get());
if (axCaption && !axCaption->accessibilityIsIgnored())
m_children.append(axCaption);
}
}
unsigned maxColumnCount = 0;
RenderTableSection* footer = table.footer();
for (RenderTableSection* tableSection = table.topSection(); tableSection; tableSection = table.sectionBelow(tableSection, SkipEmptySections)) {
if (tableSection == footer)
continue;
addChildrenFromSection(tableSection, maxColumnCount);
}
if (footer)
addChildrenFromSection(footer, maxColumnCount);
AXObjectCache* axCache = m_renderer->document().axObjectCache();
unsigned length = maxColumnCount;
for (unsigned i = 0; i < length; ++i) {
auto& column = downcast<AccessibilityTableColumn>(*axCache->getOrCreate(AccessibilityRole::Column));
column.setColumnIndex((int)i);
column.setParent(this);
m_columns.append(&column);
if (!column.accessibilityIsIgnored())
m_children.append(&column);
}
AccessibilityObject* headerContainerObject = headerContainer();
if (headerContainerObject && !headerContainerObject->accessibilityIsIgnored())
m_children.append(headerContainerObject);
for (const auto& row : m_rows) {
for (const auto& cell : row->children())
cell->updateAccessibilityRole();
}
}
void AccessibilityTable::addTableCellChild(AccessibilityObject* rowObject, HashSet<AccessibilityObject*>& appendedRows, unsigned& columnCount)
{
if (!rowObject || !is<AccessibilityTableRow>(*rowObject))
return;
auto& row = downcast<AccessibilityTableRow>(*rowObject);
if (appendedRows.contains(&row))
return;
row.setRowIndex(static_cast<int>(m_rows.size()));
m_rows.append(&row);
if (!row.accessibilityIsIgnored())
m_children.append(&row);
appendedRows.add(&row);
unsigned rowCellCount = row.children().size();
if (rowCellCount > columnCount)
columnCount = rowCellCount;
}
void AccessibilityTable::addChildrenFromSection(RenderTableSection* tableSection, unsigned& maxColumnCount)
{
ASSERT(tableSection);
if (!tableSection)
return;
AXObjectCache* axCache = m_renderer->document().axObjectCache();
HashSet<AccessibilityObject*> appendedRows;
unsigned numRows = tableSection->numRows();
for (unsigned rowIndex = 0; rowIndex < numRows; ++rowIndex) {
RenderTableRow* renderRow = tableSection->rowRendererAt(rowIndex);
if (!renderRow)
continue;
AccessibilityObject& rowObject = *axCache->getOrCreate(renderRow);
if (renderRow->isAnonymous()) {
Deque<AccessibilityObject*> queue;
queue.append(&rowObject);
while (!queue.isEmpty()) {
AccessibilityObject* obj = queue.takeFirst();
if (obj->node() && is<AccessibilityTableRow>(*obj)) {
addTableCellChild(obj, appendedRows, maxColumnCount);
continue;
}
for (auto* child = obj->firstChild(); child; child = child->nextSibling())
queue.append(child);
}
} else
addTableCellChild(&rowObject, appendedRows, maxColumnCount);
}
maxColumnCount = std::max(tableSection->numColumns(), maxColumnCount);
}
AccessibilityObject* AccessibilityTable::headerContainer()
{
if (m_headerContainer)
return m_headerContainer.get();
auto& tableHeader = downcast<AccessibilityMockObject>(*axObjectCache()->getOrCreate(AccessibilityRole::TableHeaderContainer));
tableHeader.setParent(this);
m_headerContainer = &tableHeader;
return m_headerContainer.get();
}
const AccessibilityObject::AccessibilityChildrenVector& AccessibilityTable::columns()
{
updateChildrenIfNecessary();
return m_columns;
}
const AccessibilityObject::AccessibilityChildrenVector& AccessibilityTable::rows()
{
updateChildrenIfNecessary();
return m_rows;
}
void AccessibilityTable::columnHeaders(AccessibilityChildrenVector& headers)
{
if (!m_renderer)
return;
updateChildrenIfNecessary();
AccessibilityChildrenVector columnsCopy = m_columns;
for (const auto& column : columnsCopy) {
if (AccessibilityObject* header = downcast<AccessibilityTableColumn>(*column).headerObject())
headers.append(header);
}
}
void AccessibilityTable::rowHeaders(AccessibilityChildrenVector& headers)
{
if (!m_renderer)
return;
updateChildrenIfNecessary();
AccessibilityChildrenVector rowsCopy = m_rows;
for (const auto& row : rowsCopy) {
if (AccessibilityObject* header = downcast<AccessibilityTableRow>(*row).headerObject())
headers.append(header);
}
}
void AccessibilityTable::visibleRows(AccessibilityChildrenVector& rows)
{
if (!m_renderer)
return;
updateChildrenIfNecessary();
for (const auto& row : m_rows) {
if (row && !row->isOffScreen())
rows.append(row);
}
}
void AccessibilityTable::cells(AccessibilityObject::AccessibilityChildrenVector& cells)
{
if (!m_renderer)
return;
updateChildrenIfNecessary();
for (const auto& row : m_rows)
cells.appendVector(row->children());
}
unsigned AccessibilityTable::columnCount()
{
updateChildrenIfNecessary();
return m_columns.size();
}
unsigned AccessibilityTable::rowCount()
{
updateChildrenIfNecessary();
return m_rows.size();
}
int AccessibilityTable::tableLevel() const
{
int level = 0;
for (AccessibilityObject* obj = static_cast<AccessibilityObject*>(const_cast<AccessibilityTable*>(this)); obj; obj = obj->parentObject()) {
if (is<AccessibilityTable>(*obj) && downcast<AccessibilityTable>(*obj).isExposableThroughAccessibility())
++level;
}
return level;
}
AccessibilityTableCell* AccessibilityTable::cellForColumnAndRow(unsigned column, unsigned row)
{
updateChildrenIfNecessary();
if (column >= columnCount() || row >= rowCount())
return nullptr;
for (unsigned rowIndexCounter = row + 1; rowIndexCounter > 0; --rowIndexCounter) {
unsigned rowIndex = rowIndexCounter - 1;
const auto& children = m_rows[rowIndex]->children();
for (unsigned colIndexCounter = std::min(static_cast<unsigned>(children.size()), column + 1); colIndexCounter > 0; --colIndexCounter) {
unsigned colIndex = colIndexCounter - 1;
AccessibilityObject* child = children[colIndex].get();
ASSERT(is<AccessibilityTableCell>(*child));
if (!is<AccessibilityTableCell>(*child))
continue;
std::pair<unsigned, unsigned> columnRange;
std::pair<unsigned, unsigned> rowRange;
auto& tableCellChild = downcast<AccessibilityTableCell>(*child);
tableCellChild.columnIndexRange(columnRange);
tableCellChild.rowIndexRange(rowRange);
if ((column >= columnRange.first && column < (columnRange.first + columnRange.second))
&& (row >= rowRange.first && row < (rowRange.first + rowRange.second)))
return &tableCellChild;
}
}
return nullptr;
}
AccessibilityRole AccessibilityTable::roleValue() const
{
if (!isExposableThroughAccessibility())
return AccessibilityRenderObject::roleValue();
AccessibilityRole ariaRole = ariaRoleAttribute();
if (ariaRole == AccessibilityRole::Grid || ariaRole == AccessibilityRole::TreeGrid)
return ariaRole;
return AccessibilityRole::Table;
}
bool AccessibilityTable::computeAccessibilityIsIgnored() const
{
AccessibilityObjectInclusion decision = defaultObjectInclusion();
if (decision == AccessibilityObjectInclusion::IncludeObject)
return false;
if (decision == AccessibilityObjectInclusion::IgnoreObject)
return true;
if (!isExposableThroughAccessibility())
return AccessibilityRenderObject::computeAccessibilityIsIgnored();
return false;
}
void AccessibilityTable::titleElementText(Vector<AccessibilityText>& textOrder) const
{
String title = this->title();
if (!title.isEmpty())
textOrder.append(AccessibilityText(title, AccessibilityTextSource::LabelByElement));
}
String AccessibilityTable::title() const
{
if (!isExposableThroughAccessibility())
return AccessibilityRenderObject::title();
String title;
if (!m_renderer)
return title;
Node* tableElement = m_renderer->node();
if (is<HTMLTableElement>(tableElement)) {
if (auto caption = downcast<HTMLTableElement>(*tableElement).caption())
title = caption->innerText();
}
if (title.isEmpty())
title = AccessibilityRenderObject::title();
return title;
}
int AccessibilityTable::axColumnCount() const
{
const AtomicString& colCountValue = getAttribute(aria_colcountAttr);
int colCountInt = colCountValue.toInt();
if (colCountInt == -1 || colCountInt >= (int)m_columns.size())
return colCountInt;
return 0;
}
int AccessibilityTable::axRowCount() const
{
const AtomicString& rowCountValue = getAttribute(aria_rowcountAttr);
int rowCountInt = rowCountValue.toInt();
if (rowCountInt == -1 || rowCountInt >= (int)m_rows.size())
return rowCountInt;
return 0;
}
}