blob: 84c2132e7341323cba12015aa11a3a8a16ca02e7 [file] [log] [blame]
/*
* Copyright (C) 2008 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
* its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "modules/accessibility/AXTable.h"
#include "core/dom/AccessibleNode.h"
#include "core/dom/ElementTraversal.h"
#include "core/editing/EditingUtilities.h"
#include "core/html/HTMLCollection.h"
#include "core/html/HTMLTableCaptionElement.h"
#include "core/html/HTMLTableCellElement.h"
#include "core/html/HTMLTableColElement.h"
#include "core/html/HTMLTableElement.h"
#include "core/html/HTMLTableRowElement.h"
#include "core/html/HTMLTableRowsCollection.h"
#include "core/html/HTMLTableSectionElement.h"
#include "core/layout/LayoutTableCell.h"
#include "modules/accessibility/AXObjectCacheImpl.h"
#include "modules/accessibility/AXTableCell.h"
#include "modules/accessibility/AXTableColumn.h"
#include "modules/accessibility/AXTableRow.h"
namespace blink {
using namespace HTMLNames;
AXTable::AXTable(LayoutObject* layout_object,
AXObjectCacheImpl& ax_object_cache)
: AXLayoutObject(layout_object, ax_object_cache),
header_container_(nullptr),
is_ax_table_(true) {}
AXTable::~AXTable() {}
void AXTable::Init() {
AXLayoutObject::Init();
is_ax_table_ = IsTableExposableThroughAccessibility();
}
AXTable* AXTable::Create(LayoutObject* layout_object,
AXObjectCacheImpl& ax_object_cache) {
return new AXTable(layout_object, ax_object_cache);
}
bool AXTable::HasARIARole() const {
if (!layout_object_)
return false;
AccessibilityRole aria_role = AriaRoleAttribute();
if (aria_role != kUnknownRole)
return true;
return false;
}
bool AXTable::IsAXTable() const {
if (!layout_object_)
return false;
return is_ax_table_;
}
static bool ElementHasAriaRole(const Element* element) {
if (!element)
return false;
const AtomicString& aria_role = element->FastGetAttribute(roleAttr);
return (!aria_role.IsNull() && !aria_role.IsEmpty());
}
bool AXTable::IsDataTable() const {
if (!layout_object_ || !GetNode())
return false;
// Do not consider it a data table if it has an ARIA role.
if (HasARIARole())
return false;
// When a section of the document is contentEditable, all tables should be
// treated as data tables, otherwise users may not be able to work with rich
// text editors that allow creating and editing tables.
if (GetNode() && HasEditableStyle(*GetNode()))
return true;
// This employs a heuristic to determine if this table should appear.
// Only "data" tables should be exposed as tables.
// Unfortunately, there is no good way to determine the difference
// between a "layout" table and a "data" table.
LayoutTable* table = ToLayoutTable(layout_object_);
Node* table_node = table->GetNode();
if (!IsHTMLTableElement(table_node))
return false;
// Do not consider it a data table if any of its descendants have an ARIA
// role.
HTMLTableElement* table_element = ToHTMLTableElement(table_node);
if (ElementHasAriaRole(table_element->tHead()))
return false;
if (ElementHasAriaRole(table_element->tFoot()))
return false;
HTMLCollection* bodies = table_element->tBodies();
for (unsigned body_index = 0; body_index < bodies->length(); ++body_index) {
Element* body_element = bodies->item(body_index);
if (ElementHasAriaRole(body_element))
return false;
}
HTMLTableRowsCollection* rows = table_element->rows();
unsigned row_count = rows->length();
for (unsigned row_index = 0; row_index < row_count; ++row_index) {
HTMLTableRowElement* row_element = rows->Item(row_index);
if (ElementHasAriaRole(row_element))
return false;
HTMLCollection* cells = row_element->cells();
for (unsigned cell_index = 0; cell_index < cells->length(); ++cell_index) {
if (ElementHasAriaRole(cells->item(cell_index)))
return false;
}
}
// If there is a caption element, summary, THEAD, or TFOOT section, it's most
// certainly a data table
if (!table_element->Summary().IsEmpty() || table_element->tHead() ||
table_element->tFoot() || table_element->caption())
return true;
// if someone used "rules" attribute than the table should appear
if (!table_element->Rules().IsEmpty())
return true;
// if there's a colgroup or col element, it's probably a data table.
if (Traversal<HTMLTableColElement>::FirstChild(*table_element))
return true;
// go through the cell's and check for tell-tale signs of "data" table status
// cells have borders, or use attributes like headers, abbr, scope or axis
table->RecalcSectionsIfNeeded();
LayoutTableSection* first_body = table->FirstBody();
if (!first_body)
return false;
int num_cols_in_first_body = first_body->NumEffectiveColumns();
int num_rows = first_body->NumRows();
// If there's only one cell, it's not a good AXTable candidate.
if (num_rows == 1 && num_cols_in_first_body == 1)
return false;
// If there are at least 20 rows, we'll call it a data table.
if (num_rows >= 20)
return true;
// Store the background color of the table to check against cell's background
// colors.
const ComputedStyle* table_style = table->Style();
if (!table_style)
return false;
Color table_bg_color =
table_style->VisitedDependentColor(CSSPropertyBackgroundColor);
// check enough of the cells to find if the table matches our criteria
// Criteria:
// 1) must have at least one valid cell (and)
// 2) at least half of cells have borders (or)
// 3) at least half of cells have different bg colors than the table, and
// there is cell spacing
unsigned valid_cell_count = 0;
unsigned bordered_cell_count = 0;
unsigned background_difference_cell_count = 0;
unsigned cells_with_top_border = 0;
unsigned cells_with_bottom_border = 0;
unsigned cells_with_left_border = 0;
unsigned cells_with_right_border = 0;
Color alternating_row_colors[5];
int alternating_row_color_count = 0;
int headers_in_first_column_count = 0;
for (int row = 0; row < num_rows; ++row) {
int headers_in_first_row_count = 0;
int n_cols = first_body->NumCols(row);
for (int col = 0; col < n_cols; ++col) {
LayoutTableCell* cell = first_body->PrimaryCellAt(row, col);
if (!cell)
continue;
Node* cell_node = cell->GetNode();
if (!cell_node)
continue;
if (cell->Size().Width() < 1 || cell->Size().Height() < 1)
continue;
valid_cell_count++;
bool is_th_cell = cell_node->HasTagName(thTag);
// If the first row is comprised of all <th> tags, assume it is a data
// table.
if (!row && is_th_cell)
headers_in_first_row_count++;
// If the first column is comprised of all <th> tags, assume it is a data
// table.
if (!col && is_th_cell)
headers_in_first_column_count++;
// In this case, the developer explicitly assigned a "data" table
// attribute.
if (IsHTMLTableCellElement(*cell_node)) {
HTMLTableCellElement& cell_element = ToHTMLTableCellElement(*cell_node);
if (!cell_element.Headers().IsEmpty() ||
!cell_element.Abbr().IsEmpty() || !cell_element.Axis().IsEmpty() ||
!cell_element.FastGetAttribute(scopeAttr).IsEmpty())
return true;
}
const ComputedStyle* computed_style = cell->Style();
if (!computed_style)
continue;
// If the empty-cells style is set, we'll call it a data table.
if (computed_style->EmptyCells() == EEmptyCells::kHide)
return true;
// If a cell has matching bordered sides, call it a (fully) bordered cell.
if ((cell->BorderTop() > 0 && cell->BorderBottom() > 0) ||
(cell->BorderLeft() > 0 && cell->BorderRight() > 0))
bordered_cell_count++;
// Also keep track of each individual border, so we can catch tables where
// most cells have a bottom border, for example.
if (cell->BorderTop() > 0)
cells_with_top_border++;
if (cell->BorderBottom() > 0)
cells_with_bottom_border++;
if (cell->BorderLeft() > 0)
cells_with_left_border++;
if (cell->BorderRight() > 0)
cells_with_right_border++;
// If the cell has a different color from the table and there is cell
// spacing, then it is probably a data table cell (spacing and colors take
// the place of borders).
Color cell_color =
computed_style->VisitedDependentColor(CSSPropertyBackgroundColor);
if (table->HBorderSpacing() > 0 && table->VBorderSpacing() > 0 &&
table_bg_color != cell_color && cell_color.Alpha() != 1)
background_difference_cell_count++;
// If we've found 10 "good" cells, we don't need to keep searching.
if (bordered_cell_count >= 10 || background_difference_cell_count >= 10)
return true;
// For the first 5 rows, cache the background color so we can check if
// this table has zebra-striped rows.
if (row < 5 && row == alternating_row_color_count) {
LayoutObject* layout_row = cell->Parent();
if (!layout_row || !layout_row->IsBoxModelObject() ||
!ToLayoutBoxModelObject(layout_row)->IsTableRow())
continue;
const ComputedStyle* row_computed_style = layout_row->Style();
if (!row_computed_style)
continue;
Color row_color = row_computed_style->VisitedDependentColor(
CSSPropertyBackgroundColor);
alternating_row_colors[alternating_row_color_count] = row_color;
alternating_row_color_count++;
}
}
if (!row && headers_in_first_row_count == num_cols_in_first_body &&
num_cols_in_first_body > 1)
return true;
}
if (headers_in_first_column_count == num_rows && num_rows > 1)
return true;
// if there is less than two valid cells, it's not a data table
if (valid_cell_count <= 1)
return false;
// half of the cells had borders, it's a data table
unsigned needed_cell_count = valid_cell_count / 2;
if (bordered_cell_count >= needed_cell_count ||
cells_with_top_border >= needed_cell_count ||
cells_with_bottom_border >= needed_cell_count ||
cells_with_left_border >= needed_cell_count ||
cells_with_right_border >= needed_cell_count)
return true;
// half had different background colors, it's a data table
if (background_difference_cell_count >= needed_cell_count)
return true;
// Check if there is an alternating row background color indicating a zebra
// striped style pattern.
if (alternating_row_color_count > 2) {
Color first_color = alternating_row_colors[0];
for (int k = 1; k < alternating_row_color_count; k++) {
// If an odd row was the same color as the first row, its not alternating.
if (k % 2 == 1 && alternating_row_colors[k] == first_color)
return false;
// If an even row is not the same as the first row, its not alternating.
if (!(k % 2) && alternating_row_colors[k] != first_color)
return false;
}
return true;
}
return false;
}
bool AXTable::IsTableExposableThroughAccessibility() const {
// The following is a heuristic used to determine if a
// <table> should be exposed as an AXTable. The goal
// is to only show "data" tables.
if (!layout_object_)
return false;
// If the developer assigned an aria role to this, then we
// shouldn't expose it as a table, unless, of course, the aria
// role is a table.
if (HasARIARole())
return false;
return IsDataTable();
}
void AXTable::ClearChildren() {
AXLayoutObject::ClearChildren();
rows_.clear();
columns_.clear();
if (header_container_) {
header_container_->DetachFromParent();
header_container_ = nullptr;
}
}
void AXTable::AddChildren() {
DCHECK(!IsDetached());
if (!IsAXTable()) {
AXLayoutObject::AddChildren();
return;
}
DCHECK(!have_children_);
have_children_ = true;
if (!layout_object_ || !layout_object_->IsTable())
return;
LayoutTable* table = ToLayoutTable(layout_object_);
AXObjectCacheImpl& ax_cache = AXObjectCache();
Node* table_node = table->GetNode();
if (!IsHTMLTableElement(table_node))
return;
// Add caption
if (HTMLTableCaptionElement* caption =
ToHTMLTableElement(table_node)->caption()) {
AXObject* caption_object = ax_cache.GetOrCreate(caption);
if (caption_object && !caption_object->AccessibilityIsIgnored())
children_.push_back(caption_object);
}
// Go through all the available sections to pull out the rows and add them as
// children.
table->RecalcSectionsIfNeeded();
LayoutTableSection* table_section = table->TopSection();
if (!table_section)
return;
LayoutTableSection* initial_table_section = table_section;
while (table_section) {
HeapHashSet<Member<AXObject>> appended_rows;
unsigned num_rows = table_section->NumRows();
for (unsigned row_index = 0; row_index < num_rows; ++row_index) {
LayoutTableRow* layout_row = table_section->RowLayoutObjectAt(row_index);
if (!layout_row)
continue;
AXObject* row_object = ax_cache.GetOrCreate(layout_row);
if (!row_object || !row_object->IsTableRow())
continue;
AXTableRow* row = ToAXTableRow(row_object);
// We need to check every cell for a new row, because cell spans
// can cause us to miss rows if we just check the first column.
if (appended_rows.Contains(row))
continue;
row->SetRowIndex(static_cast<int>(rows_.size()));
rows_.push_back(row);
if (!row->AccessibilityIsIgnored())
children_.push_back(row);
appended_rows.insert(row);
}
table_section = table->SectionBelow(table_section, kSkipEmptySections);
}
// make the columns based on the number of columns in the first body
unsigned length = initial_table_section->NumEffectiveColumns();
for (unsigned i = 0; i < length; ++i) {
AXTableColumn* column = ToAXTableColumn(ax_cache.GetOrCreate(kColumnRole));
column->SetColumnIndex((int)i);
column->SetParent(this);
columns_.push_back(column);
if (!column->AccessibilityIsIgnored())
children_.push_back(column);
}
AXObject* header_container_object = HeaderContainer();
if (header_container_object &&
!header_container_object->AccessibilityIsIgnored())
children_.push_back(header_container_object);
}
AXObject* AXTable::HeaderContainer() {
if (header_container_)
return header_container_.Get();
AXMockObject* table_header =
ToAXMockObject(AXObjectCache().GetOrCreate(kTableHeaderContainerRole));
table_header->SetParent(this);
header_container_ = table_header;
return header_container_.Get();
}
const AXObject::AXObjectVector& AXTable::Columns() {
UpdateChildrenIfNecessary();
return columns_;
}
const AXObject::AXObjectVector& AXTable::Rows() {
UpdateChildrenIfNecessary();
return rows_;
}
void AXTable::ColumnHeaders(AXObjectVector& headers) {
if (!layout_object_)
return;
UpdateChildrenIfNecessary();
unsigned column_count = columns_.size();
for (unsigned c = 0; c < column_count; c++) {
AXObject* column = columns_[c].Get();
if (column->IsTableCol())
ToAXTableColumn(column)->HeaderObjectsForColumn(headers);
}
}
void AXTable::RowHeaders(AXObjectVector& headers) {
if (!layout_object_)
return;
UpdateChildrenIfNecessary();
unsigned row_count = rows_.size();
for (unsigned r = 0; r < row_count; r++) {
AXObject* row = rows_[r].Get();
if (row->IsTableRow())
ToAXTableRow(rows_[r].Get())->HeaderObjectsForRow(headers);
}
}
int AXTable::AriaColumnCount() {
int32_t col_count;
if (!HasAOMPropertyOrARIAAttribute(AOMIntProperty::kColCount, col_count))
return 0;
if (col_count > static_cast<int>(ColumnCount()))
return col_count;
// Spec says that if all of the columns are present in the DOM, it
// is not necessary to set this attribute as the user agent can
// automatically calculate the total number of columns.
// It returns 0 in order not to set this attribute.
if (col_count == static_cast<int>(ColumnCount()) || col_count != -1)
return 0;
return -1;
}
int AXTable::AriaRowCount() {
int32_t row_count;
if (!HasAOMPropertyOrARIAAttribute(AOMIntProperty::kRowCount, row_count))
return 0;
if (row_count > static_cast<int>(RowCount()))
return row_count;
// Spec says that if all of the rows are present in the DOM, it is
// not necessary to set this attribute as the user agent can
// automatically calculate the total number of rows.
// It returns 0 in order not to set this attribute.
if (row_count == (int)RowCount() || row_count != -1)
return 0;
// In the spec, -1 explicitly means an unknown number of rows.
return -1;
}
unsigned AXTable::ColumnCount() {
UpdateChildrenIfNecessary();
return columns_.size();
}
unsigned AXTable::RowCount() {
UpdateChildrenIfNecessary();
return rows_.size();
}
AXTableCell* AXTable::CellForColumnAndRow(unsigned column, unsigned row) {
UpdateChildrenIfNecessary();
if (column >= ColumnCount() || row >= RowCount())
return nullptr;
// Iterate backwards through the rows in case the desired cell has a rowspan
// and exists in a previous row.
for (unsigned row_index_counter = row + 1; row_index_counter > 0;
--row_index_counter) {
unsigned row_index = row_index_counter - 1;
const auto& children = rows_[row_index]->Children();
// Since some cells may have colspans, we have to check the actual range of
// each cell to determine which is the right one.
for (unsigned col_index_counter =
std::min(static_cast<unsigned>(children.size()), column + 1);
col_index_counter > 0; --col_index_counter) {
unsigned col_index = col_index_counter - 1;
AXObject* child = children[col_index].Get();
if (!child->IsTableCell())
continue;
std::pair<unsigned, unsigned> column_range;
std::pair<unsigned, unsigned> row_range;
AXTableCell* table_cell_child = ToAXTableCell(child);
table_cell_child->ColumnIndexRange(column_range);
table_cell_child->RowIndexRange(row_range);
if ((column >= column_range.first &&
column < (column_range.first + column_range.second)) &&
(row >= row_range.first &&
row < (row_range.first + row_range.second)))
return table_cell_child;
}
}
return nullptr;
}
AccessibilityRole AXTable::RoleValue() const {
if (!IsAXTable())
return AXLayoutObject::RoleValue();
return kTableRole;
}
bool AXTable::ComputeAccessibilityIsIgnored(
IgnoredReasons* ignored_reasons) const {
AXObjectInclusion decision = DefaultObjectInclusion(ignored_reasons);
if (decision == kIncludeObject)
return false;
if (decision == kIgnoreObject)
return true;
if (!IsAXTable())
return AXLayoutObject::ComputeAccessibilityIsIgnored(ignored_reasons);
return false;
}
void AXTable::Trace(blink::Visitor* visitor) {
visitor->Trace(rows_);
visitor->Trace(columns_);
visitor->Trace(header_container_);
AXLayoutObject::Trace(visitor);
}
} // namespace blink