diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /accessible/html/HTMLTableAccessible.cpp | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | accessible/html/HTMLTableAccessible.cpp | 769 |
1 files changed, 769 insertions, 0 deletions
diff --git a/accessible/html/HTMLTableAccessible.cpp b/accessible/html/HTMLTableAccessible.cpp new file mode 100644 index 0000000000..b2b4e65460 --- /dev/null +++ b/accessible/html/HTMLTableAccessible.cpp @@ -0,0 +1,769 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "HTMLTableAccessible.h" + +#include <stdint.h> +#include "mozilla/DebugOnly.h" + +#include "nsAccessibilityService.h" +#include "AccAttributes.h" +#include "ARIAMap.h" +#include "CacheConstants.h" +#include "DocAccessible.h" +#include "LocalAccessible-inl.h" +#include "nsTextEquivUtils.h" +#include "Relation.h" +#include "Role.h" +#include "States.h" + +#include "mozilla/PresShell.h" +#include "mozilla/a11y/TableAccessible.h" +#include "mozilla/a11y/TableCellAccessible.h" +#include "mozilla/Assertions.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/HTMLTableElement.h" +#include "mozilla/dom/NameSpaceConstants.h" +#include "nsCaseTreatment.h" +#include "nsColor.h" +#include "nsCOMPtr.h" +#include "nsCoreUtils.h" +#include "nsDebug.h" +#include "nsIHTMLCollection.h" +#include "nsITableCellLayout.h" +#include "nsFrameSelection.h" +#include "nsError.h" +#include "nsGkAtoms.h" +#include "nsLiteralString.h" +#include "nsMargin.h" +#include "nsQueryFrame.h" +#include "nsSize.h" +#include "nsStringFwd.h" +#include "nsTableCellFrame.h" +#include "nsTableWrapperFrame.h" + +using namespace mozilla; +using namespace mozilla::dom; +using namespace mozilla::a11y; + +//////////////////////////////////////////////////////////////////////////////// +// HTMLTableCellAccessible +//////////////////////////////////////////////////////////////////////////////// + +HTMLTableCellAccessible::HTMLTableCellAccessible(nsIContent* aContent, + DocAccessible* aDoc) + : HyperTextAccessibleWrap(aContent, aDoc) { + mType = eHTMLTableCellType; + mGenericTypes |= eTableCell; +} + +//////////////////////////////////////////////////////////////////////////////// +// HTMLTableCellAccessible: LocalAccessible implementation + +role HTMLTableCellAccessible::NativeRole() const { + // We implement this rather than using the markup maps because we only want + // this role to be returned if this is a valid cell. An invalid cell (e.g. if + // the table has role="none") won't use this class, so it will get a generic + // role, since the markup map doesn't specify a role. + if (mContent->IsMathMLElement(nsGkAtoms::mtd_)) { + return roles::MATHML_CELL; + } + return roles::CELL; +} + +uint64_t HTMLTableCellAccessible::NativeState() const { + uint64_t state = HyperTextAccessibleWrap::NativeState(); + + nsIFrame* frame = mContent->GetPrimaryFrame(); + NS_ASSERTION(frame, "No frame for valid cell accessible!"); + + if (frame && frame->IsSelected()) { + state |= states::SELECTED; + } + + return state; +} + +uint64_t HTMLTableCellAccessible::NativeInteractiveState() const { + return HyperTextAccessibleWrap::NativeInteractiveState() | states::SELECTABLE; +} + +already_AddRefed<AccAttributes> HTMLTableCellAccessible::NativeAttributes() { + RefPtr<AccAttributes> attributes = + HyperTextAccessibleWrap::NativeAttributes(); + + // We only need to expose table-cell-index to clients. If we're in the content + // process, we don't need this, so building a CachedTableAccessible is very + // wasteful. This will be exposed by RemoteAccessible in the parent process + // instead. + if (!IPCAccessibilityActive()) { + if (const TableCellAccessible* cell = AsTableCell()) { + TableAccessible* table = cell->Table(); + const uint32_t row = cell->RowIdx(); + const uint32_t col = cell->ColIdx(); + const int32_t cellIdx = table->CellIndexAt(row, col); + if (cellIdx != -1) { + attributes->SetAttribute(nsGkAtoms::tableCellIndex, cellIdx); + } + } + } + + // abbr attribute + + // Pick up object attribute from abbr DOM element (a child of the cell) or + // from abbr DOM attribute. + nsString abbrText; + if (ChildCount() == 1) { + LocalAccessible* abbr = LocalFirstChild(); + if (abbr->IsAbbreviation()) { + nsIContent* firstChildNode = abbr->GetContent()->GetFirstChild(); + if (firstChildNode) { + nsTextEquivUtils::AppendTextEquivFromTextContent(firstChildNode, + &abbrText); + } + } + } + if (abbrText.IsEmpty()) { + mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::abbr, + abbrText); + } + + if (!abbrText.IsEmpty()) { + attributes->SetAttribute(nsGkAtoms::abbr, std::move(abbrText)); + } + + // axis attribute + nsString axisText; + mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::axis, axisText); + if (!axisText.IsEmpty()) { + attributes->SetAttribute(nsGkAtoms::axis, std::move(axisText)); + } + + return attributes.forget(); +} + +void HTMLTableCellAccessible::DOMAttributeChanged(int32_t aNameSpaceID, + nsAtom* aAttribute, + int32_t aModType, + const nsAttrValue* aOldValue, + uint64_t aOldState) { + HyperTextAccessibleWrap::DOMAttributeChanged(aNameSpaceID, aAttribute, + aModType, aOldValue, aOldState); + + if (aAttribute == nsGkAtoms::headers || aAttribute == nsGkAtoms::abbr || + aAttribute == nsGkAtoms::scope) { + mDoc->FireDelayedEvent(nsIAccessibleEvent::EVENT_OBJECT_ATTRIBUTE_CHANGED, + this); + if (HTMLTableAccessible* table = Table()) { + // Modifying these attributes can also modify our table's classification + // as either a layout or data table. Queue an update on the table itself + // to re-compute our "layout guess" + mDoc->QueueCacheUpdate(table, CacheDomain::Table); + } + mDoc->QueueCacheUpdate(this, CacheDomain::Table); + } else if (aAttribute == nsGkAtoms::rowspan || + aAttribute == nsGkAtoms::colspan) { + if (HTMLTableAccessible* table = Table()) { + // Modifying these attributes can also modify our table's classification + // as either a layout or data table. Queue an update on the table itself + // to re-compute our "layout guess" + mDoc->QueueCacheUpdate(table, CacheDomain::Table); + } + mDoc->QueueCacheUpdate(this, CacheDomain::Table); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// HTMLTableCellAccessible implementation + +HTMLTableAccessible* HTMLTableCellAccessible::Table() const { + LocalAccessible* parent = const_cast<HTMLTableCellAccessible*>(this); + while ((parent = parent->LocalParent())) { + if (parent->IsHTMLTable()) { + return HTMLTableAccessible::GetFrom(parent); + } + } + + return nullptr; +} + +uint32_t HTMLTableCellAccessible::ColExtent() const { + int32_t rowIdx = -1, colIdx = -1; + if (NS_FAILED(GetCellIndexes(rowIdx, colIdx))) { + // This probably isn't a table according to the layout engine; e.g. it has + // display: block. + return 1; + } + + HTMLTableAccessible* table = Table(); + if (NS_WARN_IF(!table)) { + // This can happen where there is a <tr> inside a <div role="table"> such as + // in Monorail. + return 1; + } + + return table->ColExtentAt(rowIdx, colIdx); +} + +uint32_t HTMLTableCellAccessible::RowExtent() const { + int32_t rowIdx = -1, colIdx = -1; + if (NS_FAILED(GetCellIndexes(rowIdx, colIdx))) { + // This probably isn't a table according to the layout engine; e.g. it has + // display: block. + return 1; + } + + HTMLTableAccessible* table = Table(); + if (NS_WARN_IF(!table)) { + // This can happen where there is a <tr> inside a <div role="table"> such as + // in Monorail. + return 1; + } + + return table->RowExtentAt(rowIdx, colIdx); +} + +//////////////////////////////////////////////////////////////////////////////// +// HTMLTableCellAccessible: protected implementation + +nsITableCellLayout* HTMLTableCellAccessible::GetCellLayout() const { + return do_QueryFrame(mContent->GetPrimaryFrame()); +} + +nsresult HTMLTableCellAccessible::GetCellIndexes(int32_t& aRowIdx, + int32_t& aColIdx) const { + nsITableCellLayout* cellLayout = GetCellLayout(); + NS_ENSURE_STATE(cellLayout); + + return cellLayout->GetCellIndexes(aRowIdx, aColIdx); +} + +//////////////////////////////////////////////////////////////////////////////// +// HTMLTableHeaderCellAccessible +//////////////////////////////////////////////////////////////////////////////// + +HTMLTableHeaderCellAccessible::HTMLTableHeaderCellAccessible( + nsIContent* aContent, DocAccessible* aDoc) + : HTMLTableCellAccessible(aContent, aDoc) {} + +//////////////////////////////////////////////////////////////////////////////// +// HTMLTableHeaderCellAccessible: LocalAccessible implementation + +role HTMLTableHeaderCellAccessible::NativeRole() const { + dom::Element* el = Elm(); + if (!el) { + return roles::NOTHING; + } + + // Check value of @scope attribute. + static mozilla::dom::Element::AttrValuesArray scopeValues[] = { + nsGkAtoms::col, nsGkAtoms::colgroup, nsGkAtoms::row, nsGkAtoms::rowgroup, + nullptr}; + int32_t valueIdx = el->FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::scope, + scopeValues, eCaseMatters); + + switch (valueIdx) { + case 0: + case 1: + return roles::COLUMNHEADER; + case 2: + case 3: + return roles::ROWHEADER; + } + + dom::Element* nextEl = el->GetNextElementSibling(); + dom::Element* prevEl = el->GetPreviousElementSibling(); + // If this is the only cell in its row, it's a column header. + if (!nextEl && !prevEl) { + return roles::COLUMNHEADER; + } + const bool nextIsHeader = nextEl && nsCoreUtils::IsHTMLTableHeader(nextEl); + const bool prevIsHeader = prevEl && nsCoreUtils::IsHTMLTableHeader(prevEl); + // If this has a header on both sides, it is a column header. + if (prevIsHeader && nextIsHeader) { + return roles::COLUMNHEADER; + } + // If this has a header on one side and only a single normal cell on the + // other, it's a column header. + if (nextIsHeader && prevEl && !prevEl->GetPreviousElementSibling()) { + return roles::COLUMNHEADER; + } + if (prevIsHeader && nextEl && !nextEl->GetNextElementSibling()) { + return roles::COLUMNHEADER; + } + // If this has a normal cell next to it, it 's a row header. + if ((nextEl && !nextIsHeader) || (prevEl && !prevIsHeader)) { + return roles::ROWHEADER; + } + // If this has a row span, it could be a row header. + if (RowExtent() > 1) { + // It isn't a row header if it has 1 or more consecutive headers next to it. + if (prevIsHeader && + (!prevEl->GetPreviousElementSibling() || + nsCoreUtils::IsHTMLTableHeader(prevEl->GetPreviousElementSibling()))) { + return roles::COLUMNHEADER; + } + if (nextIsHeader && + (!nextEl->GetNextElementSibling() || + nsCoreUtils::IsHTMLTableHeader(nextEl->GetNextElementSibling()))) { + return roles::COLUMNHEADER; + } + return roles::ROWHEADER; + } + // Otherwise, assume it's a column header. + return roles::COLUMNHEADER; +} + +//////////////////////////////////////////////////////////////////////////////// +// HTMLTableRowAccessible +//////////////////////////////////////////////////////////////////////////////// + +// LocalAccessible protected +ENameValueFlag HTMLTableRowAccessible::NativeName(nsString& aName) const { + // For table row accessibles, we only want to calculate the name from the + // sub tree if an ARIA role is present. + if (HasStrongARIARole()) { + return AccessibleWrap::NativeName(aName); + } + + return eNameOK; +} + +//////////////////////////////////////////////////////////////////////////////// +// HTMLTableAccessible +//////////////////////////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////////////////////////// +// HTMLTableAccessible: LocalAccessible + +bool HTMLTableAccessible::InsertChildAt(uint32_t aIndex, + LocalAccessible* aChild) { + // Move caption accessible so that it's the first child. Check for the first + // caption only, because nsAccessibilityService ensures we don't create + // accessibles for the other captions, since only the first is actually + // visible. + return HyperTextAccessible::InsertChildAt( + aChild->IsHTMLCaption() ? 0 : aIndex, aChild); +} + +uint64_t HTMLTableAccessible::NativeState() const { + return LocalAccessible::NativeState() | states::READONLY; +} + +ENameValueFlag HTMLTableAccessible::NativeName(nsString& aName) const { + ENameValueFlag nameFlag = LocalAccessible::NativeName(aName); + if (!aName.IsEmpty()) { + return nameFlag; + } + + // Use table caption as a name. + LocalAccessible* caption = Caption(); + if (caption) { + nsIContent* captionContent = caption->GetContent(); + if (captionContent) { + nsTextEquivUtils::AppendTextEquivFromContent(this, captionContent, + &aName); + if (!aName.IsEmpty()) { + return eNameOK; + } + } + } + + // If no caption then use summary as a name. + mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::summary, aName); + return eNameOK; +} + +void HTMLTableAccessible::DOMAttributeChanged(int32_t aNameSpaceID, + nsAtom* aAttribute, + int32_t aModType, + const nsAttrValue* aOldValue, + uint64_t aOldState) { + HyperTextAccessibleWrap::DOMAttributeChanged(aNameSpaceID, aAttribute, + aModType, aOldValue, aOldState); + + if (aAttribute == nsGkAtoms::summary) { + nsAutoString name; + ARIAName(name); + if (name.IsEmpty()) { + if (!Caption()) { + // XXX: Should really be checking if caption provides a name. + mDoc->FireDelayedEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, this); + } + } + + mDoc->FireDelayedEvent(nsIAccessibleEvent::EVENT_OBJECT_ATTRIBUTE_CHANGED, + this); + mDoc->QueueCacheUpdate(this, CacheDomain::Table); + } +} + +already_AddRefed<AccAttributes> HTMLTableAccessible::NativeAttributes() { + RefPtr<AccAttributes> attributes = AccessibleWrap::NativeAttributes(); + + if (mContent->IsMathMLElement(nsGkAtoms::mtable_)) { + GetAccService()->MarkupAttributes(this, attributes); + } + + if (IsProbablyLayoutTable()) { + attributes->SetAttribute(nsGkAtoms::layout_guess, true); + } + + return attributes.forget(); +} + +//////////////////////////////////////////////////////////////////////////////// +// HTMLTableAccessible: LocalAccessible + +Relation HTMLTableAccessible::RelationByType(RelationType aType) const { + Relation rel = AccessibleWrap::RelationByType(aType); + if (aType == RelationType::LABELLED_BY) { + rel.AppendTarget(Caption()); + } + + return rel; +} + +//////////////////////////////////////////////////////////////////////////////// +// HTMLTableAccessible: Table + +LocalAccessible* HTMLTableAccessible::Caption() const { + LocalAccessible* child = mChildren.SafeElementAt(0, nullptr); + // Since this is an HTML table the caption needs to be a caption + // element with no ARIA role (except for a reduntant role='caption'). + // If we did a full Role() calculation here we risk getting into an infinite + // loop where the parent role would depend on its name which would need to be + // calculated by retrieving the caption (bug 1420773.) + return child && child->NativeRole() == roles::CAPTION && + (!child->HasStrongARIARole() || + child->IsARIARole(nsGkAtoms::caption)) + ? child + : nullptr; +} + +uint32_t HTMLTableAccessible::ColCount() const { + nsTableWrapperFrame* tableFrame = GetTableWrapperFrame(); + return tableFrame ? tableFrame->GetColCount() : 0; +} + +uint32_t HTMLTableAccessible::RowCount() { + nsTableWrapperFrame* tableFrame = GetTableWrapperFrame(); + return tableFrame ? tableFrame->GetRowCount() : 0; +} + +uint32_t HTMLTableAccessible::ColExtentAt(uint32_t aRowIdx, uint32_t aColIdx) { + nsTableWrapperFrame* tableFrame = GetTableWrapperFrame(); + if (!tableFrame) { + return 1; + } + + return tableFrame->GetEffectiveColSpanAt(aRowIdx, aColIdx); +} + +uint32_t HTMLTableAccessible::RowExtentAt(uint32_t aRowIdx, uint32_t aColIdx) { + nsTableWrapperFrame* tableFrame = GetTableWrapperFrame(); + if (!tableFrame) { + return 1; + } + + return tableFrame->GetEffectiveRowSpanAt(aRowIdx, aColIdx); +} + +bool HTMLTableAccessible::IsProbablyLayoutTable() { + // Implement a heuristic to determine if table is most likely used for layout. + + // XXX do we want to look for rowspan or colspan, especialy that span all but + // a couple cells at the beginning or end of a row/col, and especially when + // they occur at the edge of a table? + + // XXX For now debugging descriptions are always on via SHOW_LAYOUT_HEURISTIC + // This will allow release trunk builds to be used by testers to refine + // the algorithm. Integrate it into Logging. + // Change to |#define SHOW_LAYOUT_HEURISTIC DEBUG| before final release +#ifdef SHOW_LAYOUT_HEURISTIC +# define RETURN_LAYOUT_ANSWER(isLayout, heuristic) \ + { \ + mLayoutHeuristic = isLayout \ + ? nsLiteralString(u"layout table: " heuristic) \ + : nsLiteralString(u"data table: " heuristic); \ + return isLayout; \ + } +#else +# define RETURN_LAYOUT_ANSWER(isLayout, heuristic) \ + { return isLayout; } +#endif + + MOZ_ASSERT(!IsDefunct(), "Table accessible should not be defunct"); + + // Need to see all elements while document is being edited. + if (Document()->State() & states::EDITABLE) { + RETURN_LAYOUT_ANSWER(false, "In editable document"); + } + + // Check to see if an ARIA role overrides the role from native markup, + // but for which we still expose table semantics (treegrid, for example). + if (HasARIARole()) { + RETURN_LAYOUT_ANSWER(false, "Has role attribute"); + } + + dom::Element* el = Elm(); + if (el->IsMathMLElement(nsGkAtoms::mtable_)) { + RETURN_LAYOUT_ANSWER(false, "MathML matrix"); + } + + MOZ_ASSERT(el->IsHTMLElement(nsGkAtoms::table), + "Table should not be built by CSS display:table style"); + + // Check if datatable attribute has "0" value. + if (el->AttrValueIs(kNameSpaceID_None, nsGkAtoms::datatable, u"0"_ns, + eCaseMatters)) { + RETURN_LAYOUT_ANSWER(true, "Has datatable = 0 attribute, it's for layout"); + } + + // Check for legitimate data table attributes. + if (el->Element::HasNonEmptyAttr(nsGkAtoms::summary)) { + RETURN_LAYOUT_ANSWER(false, "Has summary -- legitimate table structures"); + } + + // Check for legitimate data table elements. + LocalAccessible* caption = LocalFirstChild(); + if (caption && caption->IsHTMLCaption() && caption->HasChildren()) { + RETURN_LAYOUT_ANSWER(false, + "Not empty caption -- legitimate table structures"); + } + + for (nsIContent* childElm = el->GetFirstChild(); childElm; + childElm = childElm->GetNextSibling()) { + if (!childElm->IsHTMLElement()) continue; + + if (childElm->IsAnyOfHTMLElements(nsGkAtoms::col, nsGkAtoms::colgroup, + nsGkAtoms::tfoot, nsGkAtoms::thead)) { + RETURN_LAYOUT_ANSWER( + false, + "Has col, colgroup, tfoot or thead -- legitimate table structures"); + } + + if (childElm->IsHTMLElement(nsGkAtoms::tbody)) { + for (nsIContent* rowElm = childElm->GetFirstChild(); rowElm; + rowElm = rowElm->GetNextSibling()) { + if (rowElm->IsHTMLElement(nsGkAtoms::tr)) { + if (LocalAccessible* row = Document()->GetAccessible(rowElm)) { + if (const nsRoleMapEntry* roleMapEntry = row->ARIARoleMap()) { + if (roleMapEntry->role != roles::ROW) { + RETURN_LAYOUT_ANSWER(true, "Repurposed tr with different role"); + } + } + } + + for (nsIContent* cellElm = rowElm->GetFirstChild(); cellElm; + cellElm = cellElm->GetNextSibling()) { + if (cellElm->IsHTMLElement()) { + if (cellElm->NodeInfo()->Equals(nsGkAtoms::th)) { + RETURN_LAYOUT_ANSWER(false, + "Has th -- legitimate table structures"); + } + + if (cellElm->AsElement()->HasAttr(kNameSpaceID_None, + nsGkAtoms::headers) || + cellElm->AsElement()->HasAttr(kNameSpaceID_None, + nsGkAtoms::scope) || + cellElm->AsElement()->HasAttr(kNameSpaceID_None, + nsGkAtoms::abbr)) { + RETURN_LAYOUT_ANSWER(false, + "Has headers, scope, or abbr attribute -- " + "legitimate table structures"); + } + + if (LocalAccessible* cell = Document()->GetAccessible(cellElm)) { + if (const nsRoleMapEntry* roleMapEntry = cell->ARIARoleMap()) { + if (roleMapEntry->role != roles::CELL && + roleMapEntry->role != roles::COLUMNHEADER && + roleMapEntry->role != roles::ROWHEADER && + roleMapEntry->role != roles::GRID_CELL) { + RETURN_LAYOUT_ANSWER(true, + "Repurposed cell with different role"); + } + } + if (cell->ChildCount() == 1 && + cell->LocalFirstChild()->IsAbbreviation()) { + RETURN_LAYOUT_ANSWER( + false, "has abbr -- legitimate table structures"); + } + } + } + } + } + } + } + } + + // Check for nested tables. + nsCOMPtr<nsIHTMLCollection> nestedTables = + el->GetElementsByTagName(u"table"_ns); + if (nestedTables->Length() > 0) { + RETURN_LAYOUT_ANSWER(true, "Has a nested table within it"); + } + + // If only 1 column or only 1 row, it's for layout. + auto colCount = ColCount(); + if (colCount <= 1) { + RETURN_LAYOUT_ANSWER(true, "Has only 1 column"); + } + auto rowCount = RowCount(); + if (rowCount <= 1) { + RETURN_LAYOUT_ANSWER(true, "Has only 1 row"); + } + + // Check for many columns. + if (colCount >= 5) { + RETURN_LAYOUT_ANSWER(false, ">=5 columns"); + } + + // Now we know there are 2-4 columns and 2 or more rows. Check to see if + // there are visible borders on the cells. + // XXX currently, we just check the first cell -- do we really need to do + // more? + nsTableWrapperFrame* tableFrame = do_QueryFrame(el->GetPrimaryFrame()); + if (!tableFrame) { + RETURN_LAYOUT_ANSWER(false, "table with no frame!"); + } + + nsIFrame* cellFrame = tableFrame->GetCellFrameAt(0, 0); + if (!cellFrame) { + RETURN_LAYOUT_ANSWER(false, "table's first cell has no frame!"); + } + + nsMargin border = cellFrame->StyleBorder()->GetComputedBorder(); + if (border.top && border.bottom && border.left && border.right) { + RETURN_LAYOUT_ANSWER(false, "Has nonzero border-width on table cell"); + } + + // Rules for non-bordered tables with 2-4 columns and 2+ rows from here on + // forward. + + // Check for styled background color across rows (alternating background + // color is a common feature for data tables). + auto childCount = ChildCount(); + nscolor rowColor = 0; + nscolor prevRowColor; + for (auto childIdx = 0U; childIdx < childCount; childIdx++) { + LocalAccessible* child = LocalChildAt(childIdx); + if (child->IsHTMLTableRow()) { + prevRowColor = rowColor; + nsIFrame* rowFrame = child->GetFrame(); + MOZ_ASSERT(rowFrame, "Table hierarchy got screwed up"); + if (!rowFrame) { + RETURN_LAYOUT_ANSWER(false, "Unexpected table hierarchy"); + } + + rowColor = rowFrame->StyleBackground()->BackgroundColor(rowFrame); + + if (childIdx > 0 && prevRowColor != rowColor) { + RETURN_LAYOUT_ANSWER(false, + "2 styles of row background color, non-bordered"); + } + } + } + + // Check for many rows. + const uint32_t kMaxLayoutRows = 20; + if (rowCount > kMaxLayoutRows) { // A ton of rows, this is probably for data + RETURN_LAYOUT_ANSWER(false, ">= kMaxLayoutRows (20) and non-bordered"); + } + + // Check for very wide table. + nsIFrame* documentFrame = Document()->GetFrame(); + nsSize documentSize = documentFrame->GetSize(); + if (documentSize.width > 0) { + nsSize tableSize = GetFrame()->GetSize(); + int32_t percentageOfDocWidth = (100 * tableSize.width) / documentSize.width; + if (percentageOfDocWidth > 95) { + // 3-4 columns, no borders, not a lot of rows, and 95% of the doc's width + // Probably for layout + RETURN_LAYOUT_ANSWER( + true, "<= 4 columns, table width is 95% of document width"); + } + } + + // Two column rules. + if (rowCount * colCount <= 10) { + RETURN_LAYOUT_ANSWER(true, "2-4 columns, 10 cells or less, non-bordered"); + } + + static const nsLiteralString tags[] = {u"embed"_ns, u"object"_ns, + u"iframe"_ns}; + for (const auto& tag : tags) { + nsCOMPtr<nsIHTMLCollection> descendants = el->GetElementsByTagName(tag); + if (descendants->Length() > 0) { + RETURN_LAYOUT_ANSWER(true, + "Has no borders, and has iframe, object or embed, " + "typical of advertisements"); + } + } + + RETURN_LAYOUT_ANSWER(false, + "No layout factor strong enough, so will guess data"); +} + +//////////////////////////////////////////////////////////////////////////////// +// HTMLTableAccessible: protected implementation + +void HTMLTableAccessible::Description(nsString& aDescription) const { + // Helpful for debugging layout vs. data tables + aDescription.Truncate(); + LocalAccessible::Description(aDescription); + if (!aDescription.IsEmpty()) { + return; + } + + // Use summary as description if it weren't used as a name. + // XXX: get rid code duplication with NameInternal(). + LocalAccessible* caption = Caption(); + if (caption) { + nsIContent* captionContent = caption->GetContent(); + if (captionContent) { + nsAutoString captionText; + nsTextEquivUtils::AppendTextEquivFromContent(this, captionContent, + &captionText); + + if (!captionText.IsEmpty()) { // summary isn't used as a name. + mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::summary, + aDescription); + } + } + } + +#ifdef SHOW_LAYOUT_HEURISTIC + if (aDescription.IsEmpty()) { + bool isProbablyForLayout = IsProbablyLayoutTable(); + aDescription = mLayoutHeuristic; + } + printf("\nTABLE: %s\n", NS_ConvertUTF16toUTF8(mLayoutHeuristic).get()); +#endif +} + +nsTableWrapperFrame* HTMLTableAccessible::GetTableWrapperFrame() const { + nsTableWrapperFrame* tableFrame = do_QueryFrame(mContent->GetPrimaryFrame()); + if (tableFrame && tableFrame->PrincipalChildList().FirstChild()) { + return tableFrame; + } + + return nullptr; +} + +//////////////////////////////////////////////////////////////////////////////// +// HTMLCaptionAccessible +//////////////////////////////////////////////////////////////////////////////// + +Relation HTMLCaptionAccessible::RelationByType(RelationType aType) const { + Relation rel = HyperTextAccessible::RelationByType(aType); + if (aType == RelationType::LABEL_FOR) { + rel.AppendTarget(LocalParent()); + } + + return rel; +} + +role HTMLCaptionAccessible::NativeRole() const { return roles::CAPTION; } |