summaryrefslogtreecommitdiffstats
path: root/accessible/generic
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
commit43a97878ce14b72f0981164f87f2e35e14151312 (patch)
tree620249daf56c0258faa40cbdcf9cfba06de2a846 /accessible/generic
parentInitial commit. (diff)
downloadfirefox-upstream.tar.xz
firefox-upstream.zip
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--accessible/generic/ARIAGridAccessible-inl.h36
-rw-r--r--accessible/generic/ARIAGridAccessible.cpp625
-rw-r--r--accessible/generic/ARIAGridAccessible.h133
-rw-r--r--accessible/generic/ApplicationAccessible.cpp144
-rw-r--r--accessible/generic/ApplicationAccessible.h109
-rw-r--r--accessible/generic/BaseAccessibles.cpp167
-rw-r--r--accessible/generic/BaseAccessibles.h139
-rw-r--r--accessible/generic/DocAccessible-inl.h191
-rw-r--r--accessible/generic/DocAccessible.cpp2800
-rw-r--r--accessible/generic/DocAccessible.h827
-rw-r--r--accessible/generic/FormControlAccessible.cpp84
-rw-r--r--accessible/generic/FormControlAccessible.h65
-rw-r--r--accessible/generic/HyperTextAccessible-inl.h129
-rw-r--r--accessible/generic/HyperTextAccessible.cpp2357
-rw-r--r--accessible/generic/HyperTextAccessible.h455
-rw-r--r--accessible/generic/ImageAccessible.cpp263
-rw-r--r--accessible/generic/ImageAccessible.h94
-rw-r--r--accessible/generic/LocalAccessible-inl.h114
-rw-r--r--accessible/generic/LocalAccessible.cpp3977
-rw-r--r--accessible/generic/LocalAccessible.h1066
-rw-r--r--accessible/generic/OuterDocAccessible.cpp236
-rw-r--r--accessible/generic/OuterDocAccessible.h80
-rw-r--r--accessible/generic/RootAccessible.cpp709
-rw-r--r--accessible/generic/RootAccessible.h93
-rw-r--r--accessible/generic/TableAccessible.cpp317
-rw-r--r--accessible/generic/TableAccessible.h72
-rw-r--r--accessible/generic/TableCellAccessible.cpp165
-rw-r--r--accessible/generic/TableCellAccessible.h40
-rw-r--r--accessible/generic/TextLeafAccessible.cpp44
-rw-r--r--accessible/generic/TextLeafAccessible.h46
-rw-r--r--accessible/generic/moz.build74
31 files changed, 15651 insertions, 0 deletions
diff --git a/accessible/generic/ARIAGridAccessible-inl.h b/accessible/generic/ARIAGridAccessible-inl.h
new file mode 100644
index 0000000000..ed2c6e2278
--- /dev/null
+++ b/accessible/generic/ARIAGridAccessible-inl.h
@@ -0,0 +1,36 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_a11y_ARIAGridAccessible_inl_h__
+#define mozilla_a11y_ARIAGridAccessible_inl_h__
+
+#include "ARIAGridAccessible.h"
+
+#include "AccIterator.h"
+#include "nsAccUtils.h"
+
+namespace mozilla {
+namespace a11y {
+
+inline int32_t ARIAGridCellAccessible::RowIndexFor(
+ LocalAccessible* aRow) const {
+ LocalAccessible* table = nsAccUtils::TableFor(aRow);
+ if (table) {
+ int32_t rowIdx = 0;
+ LocalAccessible* row = nullptr;
+ AccIterator rowIter(table, filters::GetRow);
+ while ((row = rowIter.Next()) && row != aRow) rowIdx++;
+
+ if (row) return rowIdx;
+ }
+
+ return -1;
+}
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/generic/ARIAGridAccessible.cpp b/accessible/generic/ARIAGridAccessible.cpp
new file mode 100644
index 0000000000..399370835a
--- /dev/null
+++ b/accessible/generic/ARIAGridAccessible.cpp
@@ -0,0 +1,625 @@
+/* -*- 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 "ARIAGridAccessible-inl.h"
+
+#include "LocalAccessible-inl.h"
+#include "AccAttributes.h"
+#include "AccIterator.h"
+#include "nsAccUtils.h"
+#include "Role.h"
+#include "States.h"
+
+#include "mozilla/dom/Element.h"
+#include "nsComponentManagerUtils.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+////////////////////////////////////////////////////////////////////////////////
+// ARIAGridAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+////////////////////////////////////////////////////////////////////////////////
+// Constructor
+
+ARIAGridAccessible::ARIAGridAccessible(nsIContent* aContent,
+ DocAccessible* aDoc)
+ : HyperTextAccessibleWrap(aContent, aDoc) {
+ mGenericTypes |= eTable;
+}
+
+role ARIAGridAccessible::NativeRole() const {
+ a11y::role r = GetAccService()->MarkupRole(mContent);
+ return r != roles::NOTHING ? r : roles::TABLE;
+}
+
+already_AddRefed<AccAttributes> ARIAGridAccessible::NativeAttributes() {
+ RefPtr<AccAttributes> attributes = AccessibleWrap::NativeAttributes();
+
+ if (IsProbablyLayoutTable()) {
+ attributes->SetAttribute(nsGkAtoms::layout_guess, true);
+ }
+
+ return attributes.forget();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Table
+
+uint32_t ARIAGridAccessible::ColCount() const {
+ AccIterator rowIter(this, filters::GetRow);
+ LocalAccessible* row = rowIter.Next();
+ if (!row) return 0;
+
+ AccIterator cellIter(row, filters::GetCell);
+ LocalAccessible* cell = nullptr;
+
+ uint32_t colCount = 0;
+ while ((cell = cellIter.Next())) {
+ MOZ_ASSERT(cell->IsTableCell(), "No table or grid cell!");
+ colCount += cell->AsTableCell()->ColExtent();
+ }
+
+ return colCount;
+}
+
+uint32_t ARIAGridAccessible::RowCount() {
+ uint32_t rowCount = 0;
+ AccIterator rowIter(this, filters::GetRow);
+ while (rowIter.Next()) rowCount++;
+
+ return rowCount;
+}
+
+LocalAccessible* ARIAGridAccessible::CellAt(uint32_t aRowIndex,
+ uint32_t aColumnIndex) {
+ LocalAccessible* row = RowAt(aRowIndex);
+ if (!row) return nullptr;
+
+ return CellInRowAt(row, aColumnIndex);
+}
+
+bool ARIAGridAccessible::IsColSelected(uint32_t aColIdx) {
+ if (IsARIARole(nsGkAtoms::table)) return false;
+
+ AccIterator rowIter(this, filters::GetRow);
+ LocalAccessible* row = rowIter.Next();
+ if (!row) return false;
+
+ do {
+ if (!nsAccUtils::IsARIASelected(row)) {
+ LocalAccessible* cell = CellInRowAt(row, aColIdx);
+ if (!cell || !nsAccUtils::IsARIASelected(cell)) return false;
+ }
+ } while ((row = rowIter.Next()));
+
+ return true;
+}
+
+bool ARIAGridAccessible::IsRowSelected(uint32_t aRowIdx) {
+ if (IsARIARole(nsGkAtoms::table)) return false;
+
+ LocalAccessible* row = RowAt(aRowIdx);
+ if (!row) return false;
+
+ if (!nsAccUtils::IsARIASelected(row)) {
+ AccIterator cellIter(row, filters::GetCell);
+ LocalAccessible* cell = nullptr;
+ while ((cell = cellIter.Next())) {
+ if (!nsAccUtils::IsARIASelected(cell)) return false;
+ }
+ }
+
+ return true;
+}
+
+bool ARIAGridAccessible::IsCellSelected(uint32_t aRowIdx, uint32_t aColIdx) {
+ if (IsARIARole(nsGkAtoms::table)) return false;
+
+ LocalAccessible* row = RowAt(aRowIdx);
+ if (!row) return false;
+
+ if (!nsAccUtils::IsARIASelected(row)) {
+ LocalAccessible* cell = CellInRowAt(row, aColIdx);
+ if (!cell || !nsAccUtils::IsARIASelected(cell)) return false;
+ }
+
+ return true;
+}
+
+uint32_t ARIAGridAccessible::SelectedCellCount() {
+ if (IsARIARole(nsGkAtoms::table)) return 0;
+
+ uint32_t count = 0, colCount = ColCount();
+
+ AccIterator rowIter(this, filters::GetRow);
+ LocalAccessible* row = nullptr;
+
+ while ((row = rowIter.Next())) {
+ if (nsAccUtils::IsARIASelected(row)) {
+ count += colCount;
+ continue;
+ }
+
+ AccIterator cellIter(row, filters::GetCell);
+ LocalAccessible* cell = nullptr;
+
+ while ((cell = cellIter.Next())) {
+ if (nsAccUtils::IsARIASelected(cell)) count++;
+ }
+ }
+
+ return count;
+}
+
+uint32_t ARIAGridAccessible::SelectedColCount() {
+ if (IsARIARole(nsGkAtoms::table)) return 0;
+
+ uint32_t colCount = ColCount();
+ if (!colCount) return 0;
+
+ AccIterator rowIter(this, filters::GetRow);
+ LocalAccessible* row = rowIter.Next();
+ if (!row) return 0;
+
+ nsTArray<bool> isColSelArray(colCount);
+ isColSelArray.AppendElements(colCount);
+ memset(isColSelArray.Elements(), true, colCount * sizeof(bool));
+
+ uint32_t selColCount = colCount;
+ do {
+ if (nsAccUtils::IsARIASelected(row)) continue;
+
+ AccIterator cellIter(row, filters::GetCell);
+ LocalAccessible* cell = nullptr;
+ for (uint32_t colIdx = 0; (cell = cellIter.Next()) && colIdx < colCount;
+ colIdx++) {
+ if (isColSelArray[colIdx] && !nsAccUtils::IsARIASelected(cell)) {
+ isColSelArray[colIdx] = false;
+ selColCount--;
+ }
+ }
+ } while ((row = rowIter.Next()));
+
+ return selColCount;
+}
+
+uint32_t ARIAGridAccessible::SelectedRowCount() {
+ if (IsARIARole(nsGkAtoms::table)) return 0;
+
+ uint32_t count = 0;
+
+ AccIterator rowIter(this, filters::GetRow);
+ LocalAccessible* row = nullptr;
+
+ while ((row = rowIter.Next())) {
+ if (nsAccUtils::IsARIASelected(row)) {
+ count++;
+ continue;
+ }
+
+ AccIterator cellIter(row, filters::GetCell);
+ LocalAccessible* cell = cellIter.Next();
+ if (!cell) continue;
+
+ bool isRowSelected = true;
+ do {
+ if (!nsAccUtils::IsARIASelected(cell)) {
+ isRowSelected = false;
+ break;
+ }
+ } while ((cell = cellIter.Next()));
+
+ if (isRowSelected) count++;
+ }
+
+ return count;
+}
+
+void ARIAGridAccessible::SelectedCells(nsTArray<Accessible*>* aCells) {
+ if (IsARIARole(nsGkAtoms::table)) return;
+
+ AccIterator rowIter(this, filters::GetRow);
+
+ LocalAccessible* row = nullptr;
+ while ((row = rowIter.Next())) {
+ AccIterator cellIter(row, filters::GetCell);
+ LocalAccessible* cell = nullptr;
+
+ if (nsAccUtils::IsARIASelected(row)) {
+ while ((cell = cellIter.Next())) aCells->AppendElement(cell);
+
+ continue;
+ }
+
+ while ((cell = cellIter.Next())) {
+ if (nsAccUtils::IsARIASelected(cell)) aCells->AppendElement(cell);
+ }
+ }
+}
+
+void ARIAGridAccessible::SelectedCellIndices(nsTArray<uint32_t>* aCells) {
+ if (IsARIARole(nsGkAtoms::table)) return;
+
+ uint32_t colCount = ColCount();
+
+ AccIterator rowIter(this, filters::GetRow);
+ LocalAccessible* row = nullptr;
+ for (uint32_t rowIdx = 0; (row = rowIter.Next()); rowIdx++) {
+ if (nsAccUtils::IsARIASelected(row)) {
+ for (uint32_t colIdx = 0; colIdx < colCount; colIdx++) {
+ aCells->AppendElement(rowIdx * colCount + colIdx);
+ }
+
+ continue;
+ }
+
+ AccIterator cellIter(row, filters::GetCell);
+ LocalAccessible* cell = nullptr;
+ for (uint32_t colIdx = 0; (cell = cellIter.Next()); colIdx++) {
+ if (nsAccUtils::IsARIASelected(cell)) {
+ aCells->AppendElement(rowIdx * colCount + colIdx);
+ }
+ }
+ }
+}
+
+void ARIAGridAccessible::SelectedColIndices(nsTArray<uint32_t>* aCols) {
+ if (IsARIARole(nsGkAtoms::table)) return;
+
+ uint32_t colCount = ColCount();
+ if (!colCount) return;
+
+ AccIterator rowIter(this, filters::GetRow);
+ LocalAccessible* row = rowIter.Next();
+ if (!row) return;
+
+ nsTArray<bool> isColSelArray(colCount);
+ isColSelArray.AppendElements(colCount);
+ memset(isColSelArray.Elements(), true, colCount * sizeof(bool));
+
+ do {
+ if (nsAccUtils::IsARIASelected(row)) continue;
+
+ AccIterator cellIter(row, filters::GetCell);
+ LocalAccessible* cell = nullptr;
+ for (uint32_t colIdx = 0; (cell = cellIter.Next()) && colIdx < colCount;
+ colIdx++) {
+ if (isColSelArray[colIdx] && !nsAccUtils::IsARIASelected(cell)) {
+ isColSelArray[colIdx] = false;
+ }
+ }
+ } while ((row = rowIter.Next()));
+
+ for (uint32_t colIdx = 0; colIdx < colCount; colIdx++) {
+ if (isColSelArray[colIdx]) aCols->AppendElement(colIdx);
+ }
+}
+
+void ARIAGridAccessible::SelectedRowIndices(nsTArray<uint32_t>* aRows) {
+ if (IsARIARole(nsGkAtoms::table)) return;
+
+ AccIterator rowIter(this, filters::GetRow);
+ LocalAccessible* row = nullptr;
+ for (uint32_t rowIdx = 0; (row = rowIter.Next()); rowIdx++) {
+ if (nsAccUtils::IsARIASelected(row)) {
+ aRows->AppendElement(rowIdx);
+ continue;
+ }
+
+ AccIterator cellIter(row, filters::GetCell);
+ LocalAccessible* cell = cellIter.Next();
+ if (!cell) continue;
+
+ bool isRowSelected = true;
+ do {
+ if (!nsAccUtils::IsARIASelected(cell)) {
+ isRowSelected = false;
+ break;
+ }
+ } while ((cell = cellIter.Next()));
+
+ if (isRowSelected) aRows->AppendElement(rowIdx);
+ }
+}
+
+void ARIAGridAccessible::SelectRow(uint32_t aRowIdx) {
+ if (IsARIARole(nsGkAtoms::table)) return;
+
+ AccIterator rowIter(this, filters::GetRow);
+
+ LocalAccessible* row = nullptr;
+ for (uint32_t rowIdx = 0; (row = rowIter.Next()); rowIdx++) {
+ DebugOnly<nsresult> rv = SetARIASelected(row, rowIdx == aRowIdx);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "SetARIASelected() Shouldn't fail!");
+ }
+}
+
+void ARIAGridAccessible::SelectCol(uint32_t aColIdx) {
+ if (IsARIARole(nsGkAtoms::table)) return;
+
+ AccIterator rowIter(this, filters::GetRow);
+
+ LocalAccessible* row = nullptr;
+ while ((row = rowIter.Next())) {
+ // Unselect all cells in the row.
+ DebugOnly<nsresult> rv = SetARIASelected(row, false);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "SetARIASelected() Shouldn't fail!");
+
+ // Select cell at the column index.
+ LocalAccessible* cell = CellInRowAt(row, aColIdx);
+ if (cell) SetARIASelected(cell, true);
+ }
+}
+
+void ARIAGridAccessible::UnselectRow(uint32_t aRowIdx) {
+ if (IsARIARole(nsGkAtoms::table)) return;
+
+ LocalAccessible* row = RowAt(aRowIdx);
+ if (row) SetARIASelected(row, false);
+}
+
+void ARIAGridAccessible::UnselectCol(uint32_t aColIdx) {
+ if (IsARIARole(nsGkAtoms::table)) return;
+
+ AccIterator rowIter(this, filters::GetRow);
+
+ LocalAccessible* row = nullptr;
+ while ((row = rowIter.Next())) {
+ LocalAccessible* cell = CellInRowAt(row, aColIdx);
+ if (cell) SetARIASelected(cell, false);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Protected
+
+nsresult ARIAGridAccessible::SetARIASelected(LocalAccessible* aAccessible,
+ bool aIsSelected, bool aNotify) {
+ if (IsARIARole(nsGkAtoms::table)) return NS_OK;
+
+ nsIContent* content = aAccessible->GetContent();
+ NS_ENSURE_STATE(content);
+
+ nsresult rv = NS_OK;
+ if (content->IsElement()) {
+ if (aIsSelected) {
+ rv = content->AsElement()->SetAttr(
+ kNameSpaceID_None, nsGkAtoms::aria_selected, u"true"_ns, aNotify);
+ } else {
+ rv = content->AsElement()->SetAttr(
+ kNameSpaceID_None, nsGkAtoms::aria_selected, u"false"_ns, aNotify);
+ }
+ }
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // No "smart" select/unselect for internal call.
+ if (!aNotify) return NS_OK;
+
+ // If row or cell accessible was selected then we're able to not bother about
+ // selection of its cells or its row because our algorithm is row oriented,
+ // i.e. we check selection on row firstly and then on cells.
+ if (aIsSelected) return NS_OK;
+
+ roles::Role role = aAccessible->Role();
+
+ // If the given accessible is row that was unselected then remove
+ // aria-selected from cell accessible.
+ if (role == roles::ROW) {
+ AccIterator cellIter(aAccessible, filters::GetCell);
+ LocalAccessible* cell = nullptr;
+
+ while ((cell = cellIter.Next())) {
+ rv = SetARIASelected(cell, false, false);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return NS_OK;
+ }
+
+ // If the given accessible is cell that was unselected and its row is selected
+ // then remove aria-selected from row and put aria-selected on
+ // siblings cells.
+ if (role == roles::GRID_CELL || role == roles::ROWHEADER ||
+ role == roles::COLUMNHEADER) {
+ LocalAccessible* row = aAccessible->LocalParent();
+
+ if (row && row->Role() == roles::ROW && nsAccUtils::IsARIASelected(row)) {
+ rv = SetARIASelected(row, false, false);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ AccIterator cellIter(row, filters::GetCell);
+ LocalAccessible* cell = nullptr;
+ while ((cell = cellIter.Next())) {
+ if (cell != aAccessible) {
+ rv = SetARIASelected(cell, true, false);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// ARIARowAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+ARIARowAccessible::ARIARowAccessible(nsIContent* aContent, DocAccessible* aDoc)
+ : HyperTextAccessibleWrap(aContent, aDoc) {
+ mGenericTypes |= eTableRow;
+}
+
+role ARIARowAccessible::NativeRole() const {
+ a11y::role r = GetAccService()->MarkupRole(mContent);
+ return r != roles::NOTHING ? r : roles::ROW;
+}
+
+GroupPos ARIARowAccessible::GroupPosition() {
+ int32_t count = 0, index = 0;
+ LocalAccessible* table = nsAccUtils::TableFor(this);
+ if (table) {
+ if (nsCoreUtils::GetUIntAttr(table->GetContent(), nsGkAtoms::aria_rowcount,
+ &count) &&
+ nsCoreUtils::GetUIntAttr(mContent, nsGkAtoms::aria_rowindex, &index)) {
+ return GroupPos(0, index, count);
+ }
+
+ // Deal with the special case here that tables and grids can have rows
+ // which are wrapped in generic text container elements. Exclude tree grids
+ // because these are dealt with elsewhere.
+ if (table->Role() == roles::TABLE) {
+ LocalAccessible* row = nullptr;
+ AccIterator rowIter(table, filters::GetRow);
+ while ((row = rowIter.Next())) {
+ index++;
+ if (row == this) {
+ break;
+ }
+ }
+
+ if (row) {
+ count = table->AsTable()->RowCount();
+ return GroupPos(0, index, count);
+ }
+ }
+ }
+
+ return AccessibleWrap::GroupPosition();
+}
+
+// LocalAccessible protected
+ENameValueFlag ARIARowAccessible::NativeName(nsString& aName) const {
+ // We want to calculate the name from content only if an ARIA role is
+ // present. ARIARowAccessible might also be used by tables with
+ // display:block; styling, in which case we do not want the name from
+ // content.
+ if (HasStrongARIARole()) {
+ return AccessibleWrap::NativeName(aName);
+ }
+
+ return eNameOK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// ARIAGridCellAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+////////////////////////////////////////////////////////////////////////////////
+// Constructor
+
+ARIAGridCellAccessible::ARIAGridCellAccessible(nsIContent* aContent,
+ DocAccessible* aDoc)
+ : HyperTextAccessibleWrap(aContent, aDoc) {
+ mGenericTypes |= eTableCell;
+}
+
+role ARIAGridCellAccessible::NativeRole() const {
+ const a11y::role r = GetAccService()->MarkupRole(mContent);
+ if (r != role::NOTHING) {
+ return r;
+ }
+
+ // Special case to handle th elements mapped to ARIA grid cells.
+ if (GetContent() && GetContent()->IsHTMLElement(nsGkAtoms::th)) {
+ return GetHeaderCellRole(this);
+ }
+
+ return role::CELL;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// TableCell
+
+TableAccessible* ARIAGridCellAccessible::Table() const {
+ LocalAccessible* table = nsAccUtils::TableFor(Row());
+ return table ? table->AsTable() : nullptr;
+}
+
+uint32_t ARIAGridCellAccessible::ColIdx() const {
+ LocalAccessible* row = Row();
+ if (!row) return 0;
+
+ int32_t indexInRow = IndexInParent();
+ uint32_t colIdx = 0;
+ for (int32_t idx = 0; idx < indexInRow; idx++) {
+ LocalAccessible* cell = row->LocalChildAt(idx);
+ if (cell->IsTableCell()) {
+ colIdx += cell->AsTableCell()->ColExtent();
+ }
+ }
+
+ return colIdx;
+}
+
+uint32_t ARIAGridCellAccessible::RowIdx() const { return RowIndexFor(Row()); }
+
+bool ARIAGridCellAccessible::Selected() {
+ LocalAccessible* row = Row();
+ if (!row) return false;
+
+ return nsAccUtils::IsARIASelected(row) || nsAccUtils::IsARIASelected(this);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// LocalAccessible
+
+void ARIAGridCellAccessible::ApplyARIAState(uint64_t* aState) const {
+ HyperTextAccessibleWrap::ApplyARIAState(aState);
+
+ // Return if the gridcell has aria-selected="true".
+ if (*aState & states::SELECTED) return;
+
+ // Check aria-selected="true" on the row.
+ LocalAccessible* row = LocalParent();
+ if (!row || row->Role() != roles::ROW) return;
+
+ nsIContent* rowContent = row->GetContent();
+ if (nsAccUtils::HasDefinedARIAToken(rowContent, nsGkAtoms::aria_selected) &&
+ !nsAccUtils::ARIAAttrValueIs(rowContent->AsElement(),
+ nsGkAtoms::aria_selected, nsGkAtoms::_false,
+ eCaseMatters)) {
+ *aState |= states::SELECTABLE | states::SELECTED;
+ }
+}
+
+already_AddRefed<AccAttributes> ARIAGridCellAccessible::NativeAttributes() {
+ RefPtr<AccAttributes> attributes =
+ HyperTextAccessibleWrap::NativeAttributes();
+
+ // Expose "table-cell-index" attribute.
+ LocalAccessible* thisRow = Row();
+ if (!thisRow) return attributes.forget();
+
+ int32_t rowIdx = RowIndexFor(thisRow);
+ if (rowIdx == -1) { // error
+ return attributes.forget();
+ }
+
+ int32_t colIdx = 0, colCount = 0;
+ uint32_t childCount = thisRow->ChildCount();
+ for (uint32_t childIdx = 0; childIdx < childCount; childIdx++) {
+ LocalAccessible* child = thisRow->LocalChildAt(childIdx);
+ if (child == this) colIdx = colCount;
+
+ roles::Role role = child->Role();
+ if (role == roles::CELL || role == roles::GRID_CELL ||
+ role == roles::ROWHEADER || role == roles::COLUMNHEADER) {
+ colCount++;
+ }
+ }
+
+ attributes->SetAttribute(nsGkAtoms::tableCellIndex,
+ rowIdx * colCount + colIdx);
+
+#ifdef DEBUG
+ RefPtr<nsAtom> cppClass = NS_Atomize(u"cppclass"_ns);
+ attributes->SetAttributeStringCopy(cppClass, u"ARIAGridCellAccessible"_ns);
+#endif
+
+ return attributes.forget();
+}
diff --git a/accessible/generic/ARIAGridAccessible.h b/accessible/generic/ARIAGridAccessible.h
new file mode 100644
index 0000000000..79d48a58d2
--- /dev/null
+++ b/accessible/generic/ARIAGridAccessible.h
@@ -0,0 +1,133 @@
+/* -*- 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/. */
+
+#ifndef MOZILLA_A11Y_ARIAGridAccessible_h_
+#define MOZILLA_A11Y_ARIAGridAccessible_h_
+
+#include "HyperTextAccessibleWrap.h"
+#include "TableAccessible.h"
+#include "TableCellAccessible.h"
+
+namespace mozilla {
+namespace a11y {
+
+/**
+ * Accessible for ARIA grid and treegrid.
+ */
+class ARIAGridAccessible : public HyperTextAccessibleWrap,
+ public TableAccessible {
+ public:
+ ARIAGridAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(ARIAGridAccessible,
+ HyperTextAccessibleWrap)
+
+ // LocalAccessible
+ virtual a11y::role NativeRole() const override;
+ virtual already_AddRefed<AccAttributes> NativeAttributes() override;
+ virtual TableAccessible* AsTable() override { return this; }
+
+ // TableAccessible
+ virtual uint32_t ColCount() const override;
+ virtual uint32_t RowCount() override;
+ virtual LocalAccessible* CellAt(uint32_t aRowIndex,
+ uint32_t aColumnIndex) override;
+ virtual bool IsColSelected(uint32_t aColIdx) override;
+ virtual bool IsRowSelected(uint32_t aRowIdx) override;
+ virtual bool IsCellSelected(uint32_t aRowIdx, uint32_t aColIdx) override;
+ virtual uint32_t SelectedCellCount() override;
+ virtual uint32_t SelectedColCount() override;
+ virtual uint32_t SelectedRowCount() override;
+ virtual void SelectedCells(nsTArray<Accessible*>* aCells) override;
+ virtual void SelectedCellIndices(nsTArray<uint32_t>* aCells) override;
+ virtual void SelectedColIndices(nsTArray<uint32_t>* aCols) override;
+ virtual void SelectedRowIndices(nsTArray<uint32_t>* aRows) override;
+ virtual void SelectCol(uint32_t aColIdx) override;
+ virtual void SelectRow(uint32_t aRowIdx) override;
+ virtual void UnselectCol(uint32_t aColIdx) override;
+ virtual void UnselectRow(uint32_t aRowIdx) override;
+ virtual LocalAccessible* AsAccessible() override { return this; }
+
+ protected:
+ virtual ~ARIAGridAccessible() {}
+
+ /**
+ * Set aria-selected attribute value on DOM node of the given accessible.
+ *
+ * @param aAccessible [in] accessible
+ * @param aIsSelected [in] new value of aria-selected attribute
+ * @param aNotify [in, optional] specifies if DOM should be notified
+ * about attribute change (used internally).
+ */
+ nsresult SetARIASelected(LocalAccessible* aAccessible, bool aIsSelected,
+ bool aNotify = true);
+};
+
+/**
+ * Accessible for ARIA row.
+ */
+class ARIARowAccessible : public HyperTextAccessibleWrap {
+ public:
+ ARIARowAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(ARIARowAccessible,
+ HyperTextAccessibleWrap)
+
+ // LocalAccessible
+ virtual a11y::role NativeRole() const override;
+ virtual mozilla::a11y::GroupPos GroupPosition() override;
+
+ protected:
+ virtual ~ARIARowAccessible() {}
+
+ // LocalAccessible
+ virtual ENameValueFlag NativeName(nsString& aName) const override;
+};
+
+/**
+ * Accessible for ARIA gridcell and rowheader/columnheader.
+ */
+class ARIAGridCellAccessible : public HyperTextAccessibleWrap,
+ public TableCellAccessible {
+ public:
+ ARIAGridCellAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(ARIAGridCellAccessible,
+ HyperTextAccessibleWrap)
+
+ // LocalAccessible
+ virtual a11y::role NativeRole() const override;
+ virtual TableCellAccessible* AsTableCell() override { return this; }
+ virtual void ApplyARIAState(uint64_t* aState) const override;
+ virtual already_AddRefed<AccAttributes> NativeAttributes() override;
+
+ protected:
+ virtual ~ARIAGridCellAccessible() {}
+
+ /**
+ * Return a containing row.
+ */
+ LocalAccessible* Row() const {
+ LocalAccessible* row = LocalParent();
+ return row && row->IsTableRow() ? row : nullptr;
+ }
+
+ /**
+ * Return index of the given row.
+ * Returns -1 upon error.
+ */
+ int32_t RowIndexFor(LocalAccessible* aRow) const;
+
+ // TableCellAccessible
+ virtual TableAccessible* Table() const override;
+ virtual uint32_t ColIdx() const override;
+ virtual uint32_t RowIdx() const override;
+ virtual bool Selected() override;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/generic/ApplicationAccessible.cpp b/accessible/generic/ApplicationAccessible.cpp
new file mode 100644
index 0000000000..8e3baa29e5
--- /dev/null
+++ b/accessible/generic/ApplicationAccessible.cpp
@@ -0,0 +1,144 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=2:tabstop=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 "ApplicationAccessible.h"
+
+#include "AccAttributes.h"
+#include "LocalAccessible-inl.h"
+#include "nsAccessibilityService.h"
+#include "nsAccUtils.h"
+#include "Relation.h"
+#include "Role.h"
+#include "States.h"
+
+#include "nsServiceManagerUtils.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/Components.h"
+#include "nsGlobalWindow.h"
+#include "nsIStringBundle.h"
+
+using namespace mozilla::a11y;
+
+ApplicationAccessible::ApplicationAccessible()
+ : AccessibleWrap(nullptr, nullptr) {
+ mType = eApplicationType;
+ mAppInfo = do_GetService("@mozilla.org/xre/app-info;1");
+ MOZ_ASSERT(mAppInfo, "no application info");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// nsIAccessible
+
+ENameValueFlag ApplicationAccessible::Name(nsString& aName) const {
+ aName.Truncate();
+
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ mozilla::components::StringBundle::Service();
+
+ NS_ASSERTION(bundleService, "String bundle service must be present!");
+ if (!bundleService) return eNameOK;
+
+ nsCOMPtr<nsIStringBundle> bundle;
+ nsresult rv = bundleService->CreateBundle(
+ "chrome://branding/locale/brand.properties", getter_AddRefs(bundle));
+ if (NS_FAILED(rv)) return eNameOK;
+
+ nsAutoString appName;
+ rv = bundle->GetStringFromName("brandShortName", appName);
+ if (NS_FAILED(rv) || appName.IsEmpty()) {
+ NS_WARNING("brandShortName not found, using default app name");
+ appName.AssignLiteral("Gecko based application");
+ }
+
+ aName.Assign(appName);
+ return eNameOK;
+}
+
+void ApplicationAccessible::Description(nsString& aDescription) const {
+ aDescription.Truncate();
+}
+
+void ApplicationAccessible::Value(nsString& aValue) const { aValue.Truncate(); }
+
+uint64_t ApplicationAccessible::State() {
+ return IsDefunct() ? states::DEFUNCT : 0;
+}
+
+already_AddRefed<AccAttributes> ApplicationAccessible::NativeAttributes() {
+ RefPtr<AccAttributes> attributes = new AccAttributes();
+ return attributes.forget();
+}
+
+GroupPos ApplicationAccessible::GroupPosition() { return GroupPos(); }
+
+LocalAccessible* ApplicationAccessible::LocalChildAtPoint(
+ int32_t aX, int32_t aY, EWhichChildAtPoint aWhichChild) {
+ return nullptr;
+}
+
+Accessible* ApplicationAccessible::FocusedChild() {
+ LocalAccessible* focus = FocusMgr()->FocusedLocalAccessible();
+ if (focus && focus->LocalParent() == this) {
+ return focus;
+ }
+
+ return nullptr;
+}
+
+Relation ApplicationAccessible::RelationByType(
+ RelationType aRelationType) const {
+ return Relation();
+}
+
+mozilla::LayoutDeviceIntRect ApplicationAccessible::Bounds() const {
+ return mozilla::LayoutDeviceIntRect();
+}
+
+nsRect ApplicationAccessible::BoundsInAppUnits() const { return nsRect(); }
+
+////////////////////////////////////////////////////////////////////////////////
+// LocalAccessible public methods
+
+void ApplicationAccessible::Shutdown() { mAppInfo = nullptr; }
+
+void ApplicationAccessible::ApplyARIAState(uint64_t* aState) const {}
+
+role ApplicationAccessible::NativeRole() const { return roles::APP_ROOT; }
+
+uint64_t ApplicationAccessible::NativeState() const { return 0; }
+
+KeyBinding ApplicationAccessible::AccessKey() const { return KeyBinding(); }
+
+void ApplicationAccessible::Init() {
+ // Basically children are kept updated by Append/RemoveChild method calls.
+ // However if there are open windows before accessibility was started
+ // then we need to make sure root accessibles for open windows are created so
+ // that all root accessibles are stored in application accessible children
+ // array.
+
+ nsGlobalWindowOuter::OuterWindowByIdTable* windowsById =
+ nsGlobalWindowOuter::GetWindowsTable();
+
+ if (!windowsById) {
+ return;
+ }
+
+ for (const auto& window : windowsById->Values()) {
+ if (window->GetDocShell() && window->IsRootOuterWindow()) {
+ if (RefPtr<dom::Document> docNode = window->GetExtantDoc()) {
+ GetAccService()->GetDocAccessible(docNode); // ensure creation
+ }
+ }
+ }
+}
+
+LocalAccessible* ApplicationAccessible::GetSiblingAtOffset(
+ int32_t aOffset, nsresult* aError) const {
+ if (aError) *aError = NS_OK; // fail peacefully
+
+ return nullptr;
+}
diff --git a/accessible/generic/ApplicationAccessible.h b/accessible/generic/ApplicationAccessible.h
new file mode 100644
index 0000000000..1b21ca8e8b
--- /dev/null
+++ b/accessible/generic/ApplicationAccessible.h
@@ -0,0 +1,109 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=2:tabstop=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/. */
+
+#ifndef mozilla_a11y_ApplicationAccessible_h__
+#define mozilla_a11y_ApplicationAccessible_h__
+
+#include "AccessibleWrap.h"
+
+#include "nsIXULAppInfo.h"
+
+namespace mozilla {
+namespace a11y {
+
+/**
+ * ApplicationAccessible is for the whole application of Mozilla.
+ * Only one instance of ApplicationAccessible exists for one Mozilla instance.
+ * And this one should be created when Mozilla Startup (if accessibility
+ * feature has been enabled) and destroyed when Mozilla Shutdown.
+ *
+ * All the accessibility objects for toplevel windows are direct children of
+ * the ApplicationAccessible instance.
+ */
+
+class ApplicationAccessible : public AccessibleWrap {
+ public:
+ ApplicationAccessible();
+
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(ApplicationAccessible, AccessibleWrap)
+
+ // LocalAccessible
+ virtual void Shutdown() override;
+ virtual LayoutDeviceIntRect Bounds() const override;
+ virtual nsRect BoundsInAppUnits() const override;
+ virtual already_AddRefed<AccAttributes> NativeAttributes() override;
+ virtual GroupPos GroupPosition() override;
+ virtual ENameValueFlag Name(nsString& aName) const override;
+ virtual void ApplyARIAState(uint64_t* aState) const override;
+ virtual void Description(nsString& aDescription) const override;
+ virtual void Value(nsString& aValue) const override;
+ virtual mozilla::a11y::role NativeRole() const override;
+ virtual uint64_t State() override;
+ virtual uint64_t NativeState() const override;
+ virtual Relation RelationByType(RelationType aType) const override;
+
+ virtual LocalAccessible* LocalChildAtPoint(
+ int32_t aX, int32_t aY, EWhichChildAtPoint aWhichChild) override;
+ virtual Accessible* FocusedChild() override;
+
+ // ActionAccessible
+ virtual KeyBinding AccessKey() const override;
+
+ // ApplicationAccessible
+ void Init();
+
+ void AppName(nsAString& aName) const {
+ MOZ_ASSERT(mAppInfo, "no application info");
+
+ if (mAppInfo) {
+ nsAutoCString cname;
+ mAppInfo->GetName(cname);
+ AppendUTF8toUTF16(cname, aName);
+ }
+ }
+
+ void AppVersion(nsAString& aVersion) const {
+ MOZ_ASSERT(mAppInfo, "no application info");
+
+ if (mAppInfo) {
+ nsAutoCString cversion;
+ mAppInfo->GetVersion(cversion);
+ AppendUTF8toUTF16(cversion, aVersion);
+ }
+ }
+
+ void PlatformName(nsAString& aName) const { aName.AssignLiteral("Gecko"); }
+
+ void PlatformVersion(nsAString& aVersion) const {
+ MOZ_ASSERT(mAppInfo, "no application info");
+
+ if (mAppInfo) {
+ nsAutoCString cversion;
+ mAppInfo->GetPlatformVersion(cversion);
+ AppendUTF8toUTF16(cversion, aVersion);
+ }
+ }
+
+ protected:
+ virtual ~ApplicationAccessible() {}
+
+ // LocalAccessible
+ virtual LocalAccessible* GetSiblingAtOffset(
+ int32_t aOffset, nsresult* aError = nullptr) const override;
+
+ private:
+ nsCOMPtr<nsIXULAppInfo> mAppInfo;
+};
+
+inline ApplicationAccessible* LocalAccessible::AsApplication() {
+ return IsApplication() ? static_cast<ApplicationAccessible*>(this) : nullptr;
+}
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/generic/BaseAccessibles.cpp b/accessible/generic/BaseAccessibles.cpp
new file mode 100644
index 0000000000..085c9a80e4
--- /dev/null
+++ b/accessible/generic/BaseAccessibles.cpp
@@ -0,0 +1,167 @@
+/* -*- 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 "BaseAccessibles.h"
+
+#include "LocalAccessible-inl.h"
+#include "HyperTextAccessibleWrap.h"
+#include "nsAccessibilityService.h"
+#include "nsAccUtils.h"
+#include "nsCoreUtils.h"
+#include "Role.h"
+#include "States.h"
+#include "nsIURI.h"
+
+using namespace mozilla::a11y;
+
+////////////////////////////////////////////////////////////////////////////////
+// LeafAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+LeafAccessible::LeafAccessible(nsIContent* aContent, DocAccessible* aDoc)
+ : AccessibleWrap(aContent, aDoc) {
+ mStateFlags |= eNoKidsFromDOM;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// LeafAccessible: LocalAccessible public
+
+LocalAccessible* LeafAccessible::LocalChildAtPoint(
+ int32_t aX, int32_t aY, EWhichChildAtPoint aWhichChild) {
+ // Don't walk into leaf accessibles.
+ return this;
+}
+
+bool LeafAccessible::InsertChildAt(uint32_t aIndex, LocalAccessible* aChild) {
+ MOZ_ASSERT_UNREACHABLE("InsertChildAt called on leaf accessible!");
+ return false;
+}
+
+bool LeafAccessible::RemoveChild(LocalAccessible* aChild) {
+ MOZ_ASSERT_UNREACHABLE("RemoveChild called on leaf accessible!");
+ return false;
+}
+
+bool LeafAccessible::IsAcceptableChild(nsIContent* aEl) const {
+ // No children for leaf accessible.
+ return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// LinkableAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+////////////////////////////////////////////////////////////////////////////////
+// LinkableAccessible. nsIAccessible
+
+void LinkableAccessible::TakeFocus() const {
+ if (const LocalAccessible* actionAcc = ActionWalk()) {
+ actionAcc->TakeFocus();
+ } else {
+ AccessibleWrap::TakeFocus();
+ }
+}
+
+uint64_t LinkableAccessible::NativeLinkState() const {
+ bool isLink;
+ const LocalAccessible* actionAcc = ActionWalk(&isLink);
+ if (isLink) {
+ return states::LINKED | (actionAcc->LinkState() & states::TRAVERSED);
+ }
+
+ return 0;
+}
+
+void LinkableAccessible::Value(nsString& aValue) const {
+ aValue.Truncate();
+
+ LocalAccessible::Value(aValue);
+ if (!aValue.IsEmpty()) {
+ return;
+ }
+
+ bool isLink;
+ const LocalAccessible* actionAcc = ActionWalk(&isLink);
+ if (isLink) {
+ actionAcc->Value(aValue);
+ }
+}
+
+const LocalAccessible* LinkableAccessible::ActionWalk(bool* aIsLink,
+ bool* aIsOnclick) const {
+ if (aIsOnclick) {
+ *aIsOnclick = false;
+ }
+ if (aIsLink) {
+ *aIsLink = false;
+ }
+
+ if (HasPrimaryAction()) {
+ if (aIsOnclick) {
+ *aIsOnclick = true;
+ }
+
+ return nullptr;
+ }
+
+ const Accessible* actionAcc = ActionAncestor();
+
+ const LocalAccessible* localAction =
+ actionAcc ? const_cast<Accessible*>(actionAcc)->AsLocal() : nullptr;
+
+ if (!localAction) {
+ return nullptr;
+ }
+
+ if (localAction->LinkState() & states::LINKED) {
+ if (aIsLink) {
+ *aIsLink = true;
+ }
+ } else if (aIsOnclick) {
+ *aIsOnclick = true;
+ }
+
+ return localAction;
+}
+
+KeyBinding LinkableAccessible::AccessKey() const {
+ if (const LocalAccessible* actionAcc =
+ const_cast<LinkableAccessible*>(this)->ActionWalk()) {
+ return actionAcc->AccessKey();
+ }
+
+ return LocalAccessible::AccessKey();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// LinkableAccessible: HyperLinkAccessible
+
+already_AddRefed<nsIURI> LinkableAccessible::AnchorURIAt(
+ uint32_t aAnchorIndex) const {
+ bool isLink;
+ const LocalAccessible* actionAcc = ActionWalk(&isLink);
+ if (isLink) {
+ NS_ASSERTION(actionAcc->IsLink(), "HyperLink isn't implemented.");
+
+ if (actionAcc->IsLink()) {
+ return actionAcc->AnchorURIAt(aAnchorIndex);
+ }
+ }
+
+ return nullptr;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// DummyAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+uint64_t DummyAccessible::NativeState() const { return 0; }
+uint64_t DummyAccessible::NativeInteractiveState() const { return 0; }
+
+uint64_t DummyAccessible::NativeLinkState() const { return 0; }
+
+bool DummyAccessible::NativelyUnavailable() const { return false; }
+
+void DummyAccessible::ApplyARIAState(uint64_t* aState) const {}
diff --git a/accessible/generic/BaseAccessibles.h b/accessible/generic/BaseAccessibles.h
new file mode 100644
index 0000000000..d3373ed2a5
--- /dev/null
+++ b/accessible/generic/BaseAccessibles.h
@@ -0,0 +1,139 @@
+/* -*- 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/. */
+
+#ifndef mozilla_a11y_BaseAccessibles_h__
+#define mozilla_a11y_BaseAccessibles_h__
+
+#include "AccessibleWrap.h"
+#include "HyperTextAccessibleWrap.h"
+
+class nsIContent;
+
+/**
+ * This file contains a number of classes that are used as base
+ * classes for the different accessibility implementations of
+ * the HTML and XUL widget sets. --jgaunt
+ */
+
+namespace mozilla {
+namespace a11y {
+
+/**
+ * Leaf version of DOM Accessible -- has no children
+ */
+class LeafAccessible : public AccessibleWrap {
+ public:
+ LeafAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ // nsISupports
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(LeafAccessible, AccessibleWrap)
+
+ // LocalAccessible
+ virtual LocalAccessible* LocalChildAtPoint(
+ int32_t aX, int32_t aY, EWhichChildAtPoint aWhichChild) override;
+ bool InsertChildAt(uint32_t aIndex, LocalAccessible* aChild) final;
+ bool RemoveChild(LocalAccessible* aChild) final;
+
+ virtual bool IsAcceptableChild(nsIContent* aEl) const override;
+
+ protected:
+ virtual ~LeafAccessible() {}
+};
+
+/**
+ * Used for text or image accessible nodes contained by link accessibles or
+ * accessibles for nodes with registered click event handler. It knows how to
+ * report the state of the host link (traveled or not) and can focus the host
+ * accessible programmatically.
+ */
+class LinkableAccessible : public AccessibleWrap {
+ public:
+ LinkableAccessible(nsIContent* aContent, DocAccessible* aDoc)
+ : AccessibleWrap(aContent, aDoc) {}
+
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(LinkableAccessible, AccessibleWrap)
+
+ // LocalAccessible
+ virtual void Value(nsString& aValue) const override;
+ virtual uint64_t NativeLinkState() const override;
+ virtual void TakeFocus() const override;
+
+ // ActionAccessible
+ virtual KeyBinding AccessKey() const override;
+
+ // ActionAccessible helpers
+ const LocalAccessible* ActionWalk(bool* aIsLink = nullptr,
+ bool* aIsOnclick = nullptr) const;
+ // HyperLinkAccessible
+ virtual already_AddRefed<nsIURI> AnchorURIAt(
+ uint32_t aAnchorIndex) const override;
+
+ protected:
+ virtual ~LinkableAccessible() {}
+};
+
+/**
+ * A simple accessible that gets its enumerated role.
+ */
+template <a11y::role R>
+class EnumRoleAccessible : public AccessibleWrap {
+ public:
+ EnumRoleAccessible(nsIContent* aContent, DocAccessible* aDoc)
+ : AccessibleWrap(aContent, aDoc) {}
+
+ NS_IMETHOD QueryInterface(REFNSIID aIID, void** aPtr) override {
+ return LocalAccessible::QueryInterface(aIID, aPtr);
+ }
+
+ // LocalAccessible
+ virtual a11y::role NativeRole() const override { return R; }
+
+ protected:
+ virtual ~EnumRoleAccessible() {}
+};
+
+/**
+ * Like EnumRoleAccessible, but with text support.
+ */
+template <a11y::role R>
+class EnumRoleHyperTextAccessible : public HyperTextAccessibleWrap {
+ public:
+ EnumRoleHyperTextAccessible(nsIContent* aContent, DocAccessible* aDoc)
+ : HyperTextAccessibleWrap(aContent, aDoc) {}
+
+ // LocalAccessible
+ virtual a11y::role NativeRole() const override { return R; }
+
+ protected:
+ virtual ~EnumRoleHyperTextAccessible() {}
+};
+
+/**
+ * A wrapper accessible around native accessible to connect it with
+ * crossplatform accessible tree.
+ */
+class DummyAccessible : public AccessibleWrap {
+ public:
+ explicit DummyAccessible(DocAccessible* aDocument = nullptr)
+ : AccessibleWrap(nullptr, aDocument) {
+ // IsDefunct() asserts if mContent is null, which is always true for
+ // DummyAccessible. We can prevent this by setting eSharedNode.
+ mStateFlags |= eSharedNode;
+ }
+
+ uint64_t NativeState() const final;
+ uint64_t NativeInteractiveState() const final;
+ uint64_t NativeLinkState() const final;
+ bool NativelyUnavailable() const final;
+ void ApplyARIAState(uint64_t* aState) const final;
+
+ protected:
+ virtual ~DummyAccessible() {}
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/generic/DocAccessible-inl.h b/accessible/generic/DocAccessible-inl.h
new file mode 100644
index 0000000000..493ca1fd21
--- /dev/null
+++ b/accessible/generic/DocAccessible-inl.h
@@ -0,0 +1,191 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_a11y_DocAccessible_inl_h_
+#define mozilla_a11y_DocAccessible_inl_h_
+
+#include "DocAccessible.h"
+#include "nsAccessibilityService.h"
+#include "nsAccessiblePivot.h"
+#include "NotificationController.h"
+#include "States.h"
+#include "nsIScrollableFrame.h"
+#include "mozilla/dom/DocumentInlines.h"
+
+#ifdef A11Y_LOG
+# include "Logging.h"
+#endif
+
+namespace mozilla {
+namespace a11y {
+
+inline LocalAccessible* DocAccessible::AccessibleOrTrueContainer(
+ nsINode* aNode, bool aNoContainerIfPruned) const {
+ // HTML comboboxes have no-content list accessible as an intermediate
+ // containing all options.
+ LocalAccessible* container =
+ GetAccessibleOrContainer(aNode, aNoContainerIfPruned);
+ if (container && container->IsHTMLCombobox()) {
+ return container->LocalFirstChild();
+ }
+ return container;
+}
+
+inline nsIAccessiblePivot* DocAccessible::VirtualCursor() {
+ if (!mVirtualCursor) {
+ mVirtualCursor = new nsAccessiblePivot(this);
+ mVirtualCursor->AddObserver(this);
+ }
+ return mVirtualCursor;
+}
+
+inline bool DocAccessible::IsContentLoaded() const {
+ // eDOMLoaded flag check is used for error pages as workaround to make this
+ // method return correct result since error pages do not receive 'pageshow'
+ // event and as consequence Document::IsShowing() returns false.
+ return mDocumentNode && mDocumentNode->IsVisible() &&
+ (mDocumentNode->IsShowing() || HasLoadState(eDOMLoaded));
+}
+
+inline bool DocAccessible::IsHidden() const { return mDocumentNode->Hidden(); }
+
+inline void DocAccessible::FireDelayedEvent(AccEvent* aEvent) {
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eDocLoad)) logging::DocLoadEventFired(aEvent);
+#endif
+
+ mNotificationController->QueueEvent(aEvent);
+}
+
+inline void DocAccessible::FireDelayedEvent(uint32_t aEventType,
+ LocalAccessible* aTarget) {
+ RefPtr<AccEvent> event = new AccEvent(aEventType, aTarget);
+ FireDelayedEvent(event);
+}
+
+inline void DocAccessible::BindChildDocument(DocAccessible* aDocument) {
+ mNotificationController->ScheduleChildDocBinding(aDocument);
+}
+
+template <class Class, class... Args>
+inline void DocAccessible::HandleNotification(
+ Class* aInstance, typename TNotification<Class, Args...>::Callback aMethod,
+ Args*... aArgs) {
+ if (mNotificationController) {
+ mNotificationController->HandleNotification<Class, Args...>(
+ aInstance, aMethod, aArgs...);
+ }
+}
+
+inline void DocAccessible::UpdateText(nsIContent* aTextNode) {
+ NS_ASSERTION(mNotificationController, "The document was shut down!");
+
+ // Ignore the notification if initial tree construction hasn't been done yet.
+ if (mNotificationController && HasLoadState(eTreeConstructed)) {
+ mNotificationController->ScheduleTextUpdate(aTextNode);
+ }
+}
+
+inline void DocAccessible::NotifyOfLoad(uint32_t aLoadEventType) {
+ mLoadState |= eDOMLoaded;
+ mLoadEventType = aLoadEventType;
+
+ // If the document is loaded completely then network activity was presumingly
+ // caused by file loading. Fire busy state change event.
+ if (HasLoadState(eCompletelyLoaded) && IsLoadEventTarget()) {
+ RefPtr<AccEvent> stateEvent =
+ new AccStateChangeEvent(this, states::BUSY, false);
+ FireDelayedEvent(stateEvent);
+ }
+}
+
+inline void DocAccessible::MaybeNotifyOfValueChange(
+ LocalAccessible* aAccessible) {
+ if (aAccessible->IsCombobox() || aAccessible->Role() == roles::ENTRY ||
+ aAccessible->Role() == roles::SPINBUTTON) {
+ FireDelayedEvent(nsIAccessibleEvent::EVENT_TEXT_VALUE_CHANGE, aAccessible);
+ }
+}
+
+inline LocalAccessible* DocAccessible::GetAccessibleEvenIfNotInMapOrContainer(
+ nsINode* aNode) const {
+ LocalAccessible* acc = GetAccessibleEvenIfNotInMap(aNode);
+ return acc ? acc : GetContainerAccessible(aNode);
+}
+
+inline void DocAccessible::CreateSubtree(LocalAccessible* aChild) {
+ // If a focused node has been shown then it could mean its frame was recreated
+ // while the node stays focused and we need to fire focus event on
+ // the accessible we just created. If the queue contains a focus event for
+ // this node already then it will be suppressed by this one.
+ LocalAccessible* focusedAcc = nullptr;
+ CacheChildrenInSubtree(aChild, &focusedAcc);
+
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eVerbose)) {
+ logging::Tree("TREE", "Created subtree", aChild);
+ }
+#endif
+
+ // Fire events for ARIA elements.
+ if (aChild->HasARIARole()) {
+ roles::Role role = aChild->ARIARole();
+ if (role == roles::MENUPOPUP) {
+ FireDelayedEvent(nsIAccessibleEvent::EVENT_MENUPOPUP_START, aChild);
+ } else if (role == roles::ALERT) {
+ FireDelayedEvent(nsIAccessibleEvent::EVENT_ALERT, aChild);
+ }
+ }
+
+ // XXX: do we really want to send focus to focused DOM node not taking into
+ // account active item?
+ if (focusedAcc) {
+ FocusMgr()->DispatchFocusEvent(this, focusedAcc);
+ SelectionMgr()->SetControlSelectionListener(
+ focusedAcc->GetNode()->AsElement());
+ }
+}
+
+inline DocAccessible::AttrRelProviders* DocAccessible::GetRelProviders(
+ dom::Element* aElement, const nsAString& aID) const {
+ DependentIDsHashtable* hash = mDependentIDsHashes.Get(
+ aElement->GetUncomposedDocOrConnectedShadowRoot());
+ if (hash) {
+ return hash->Get(aID);
+ }
+ return nullptr;
+}
+
+inline DocAccessible::AttrRelProviders* DocAccessible::GetOrCreateRelProviders(
+ dom::Element* aElement, const nsAString& aID) {
+ dom::DocumentOrShadowRoot* docOrShadowRoot =
+ aElement->GetUncomposedDocOrConnectedShadowRoot();
+ DependentIDsHashtable* hash =
+ mDependentIDsHashes.GetOrInsertNew(docOrShadowRoot);
+
+ return hash->GetOrInsertNew(aID);
+}
+
+inline void DocAccessible::RemoveRelProvidersIfEmpty(dom::Element* aElement,
+ const nsAString& aID) {
+ dom::DocumentOrShadowRoot* docOrShadowRoot =
+ aElement->GetUncomposedDocOrConnectedShadowRoot();
+ DependentIDsHashtable* hash = mDependentIDsHashes.Get(docOrShadowRoot);
+ if (hash) {
+ AttrRelProviders* providers = hash->Get(aID);
+ if (providers && providers->Length() == 0) {
+ hash->Remove(aID);
+ if (mDependentIDsHashes.IsEmpty()) {
+ mDependentIDsHashes.Remove(docOrShadowRoot);
+ }
+ }
+ }
+}
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/generic/DocAccessible.cpp b/accessible/generic/DocAccessible.cpp
new file mode 100644
index 0000000000..42747aee46
--- /dev/null
+++ b/accessible/generic/DocAccessible.cpp
@@ -0,0 +1,2800 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "LocalAccessible-inl.h"
+#include "AccIterator.h"
+#include "AccAttributes.h"
+#include "CachedTableAccessible.h"
+#include "DocAccessible-inl.h"
+#include "DocAccessibleChild.h"
+#include "EventTree.h"
+#include "HTMLImageMapAccessible.h"
+#include "mozilla/ProfilerMarkers.h"
+#include "nsAccCache.h"
+#include "nsAccessiblePivot.h"
+#include "nsAccUtils.h"
+#include "nsEventShell.h"
+#include "nsIIOService.h"
+#include "nsLayoutUtils.h"
+#include "nsTextEquivUtils.h"
+#include "Pivot.h"
+#include "Role.h"
+#include "RootAccessible.h"
+#include "TreeWalker.h"
+#include "xpcAccessibleDocument.h"
+
+#include "nsCommandManager.h"
+#include "nsContentUtils.h"
+#include "nsIDocShell.h"
+#include "mozilla/dom/Document.h"
+#include "nsPIDOMWindow.h"
+#include "nsIContentInlines.h"
+#include "nsIEditingSession.h"
+#include "nsIFrame.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsImageFrame.h"
+#include "nsViewManager.h"
+#include "nsIScrollableFrame.h"
+#include "nsUnicharUtils.h"
+#include "nsIURI.h"
+#include "nsIWebNavigation.h"
+#include "nsFocusManager.h"
+#include "nsTHashSet.h"
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/Components.h" // for mozilla::components
+#include "mozilla/EditorBase.h"
+#include "mozilla/HTMLEditor.h"
+#include "mozilla/ipc/ProcessChild.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/StaticPrefs_accessibility.h"
+#include "mozilla/a11y/DocAccessibleParent.h"
+#include "mozilla/dom/AncestorIterator.h"
+#include "mozilla/dom/BrowserChild.h"
+#include "mozilla/dom/BrowserParent.h"
+#include "mozilla/dom/DocumentType.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/HTMLSelectElement.h"
+#include "mozilla/dom/MutationEventBinding.h"
+#include "mozilla/dom/UserActivation.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+////////////////////////////////////////////////////////////////////////////////
+// Static member initialization
+
+static nsStaticAtom* const kRelationAttrs[] = {nsGkAtoms::aria_labelledby,
+ nsGkAtoms::aria_describedby,
+ nsGkAtoms::aria_details,
+ nsGkAtoms::aria_owns,
+ nsGkAtoms::aria_controls,
+ nsGkAtoms::aria_flowto,
+ nsGkAtoms::aria_errormessage,
+ nsGkAtoms::_for,
+ nsGkAtoms::control};
+
+static const uint32_t kRelationAttrsLen = ArrayLength(kRelationAttrs);
+
+////////////////////////////////////////////////////////////////////////////////
+// Constructor/desctructor
+
+DocAccessible::DocAccessible(dom::Document* aDocument,
+ PresShell* aPresShell)
+ : // XXX don't pass a document to the LocalAccessible constructor so that
+ // we don't set mDoc until our vtable is fully setup. If we set mDoc
+ // before setting up the vtable we will call LocalAccessible::AddRef()
+ // but not the overrides of it for subclasses. It is important to call
+ // those overrides to avoid confusing leak checking machinary.
+ HyperTextAccessibleWrap(nullptr, nullptr),
+ // XXX aaronl should we use an algorithm for the initial cache size?
+ mAccessibleCache(kDefaultCacheLength),
+ mNodeToAccessibleMap(kDefaultCacheLength),
+ mDocumentNode(aDocument),
+ mLoadState(eTreeConstructionPending),
+ mDocFlags(0),
+ mViewportCacheDirty(false),
+ mLoadEventType(0),
+ mPrevStateBits(0),
+ mVirtualCursor(nullptr),
+ mPresShell(aPresShell),
+ mIPCDoc(nullptr) {
+ mGenericTypes |= eDocument;
+ mStateFlags |= eNotNodeMapEntry;
+ mDoc = this;
+
+ MOZ_ASSERT(mPresShell, "should have been given a pres shell");
+ mPresShell->SetDocAccessible(this);
+}
+
+DocAccessible::~DocAccessible() {
+ NS_ASSERTION(!mPresShell, "LastRelease was never called!?!");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// nsISupports
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(DocAccessible)
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(DocAccessible,
+ LocalAccessible)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mNotificationController)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mVirtualCursor)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mChildDocuments)
+ for (const auto& hashEntry : tmp->mDependentIDsHashes.Values()) {
+ for (const auto& providers : hashEntry->Values()) {
+ for (int32_t provIdx = providers->Length() - 1; provIdx >= 0; provIdx--) {
+ NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(
+ cb, "content of dependent ids hash entry of document accessible");
+
+ const auto& provider = (*providers)[provIdx];
+ cb.NoteXPCOMChild(provider->mContent);
+ }
+ }
+ }
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAccessibleCache)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAnchorJumpElm)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mInvalidationList)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPendingUpdates)
+ for (const auto& ar : tmp->mARIAOwnsHash.Values()) {
+ for (uint32_t i = 0; i < ar->Length(); i++) {
+ NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, "mARIAOwnsHash entry item");
+ cb.NoteXPCOMChild(ar->ElementAt(i));
+ }
+ }
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(DocAccessible, LocalAccessible)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mNotificationController)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mVirtualCursor)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mChildDocuments)
+ tmp->mDependentIDsHashes.Clear();
+ tmp->mNodeToAccessibleMap.Clear();
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mAccessibleCache)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mAnchorJumpElm)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mInvalidationList)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mPendingUpdates)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_REFERENCE
+ tmp->mARIAOwnsHash.Clear();
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DocAccessible)
+ NS_INTERFACE_MAP_ENTRY(nsIDocumentObserver)
+ NS_INTERFACE_MAP_ENTRY(nsIMutationObserver)
+ NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
+ NS_INTERFACE_MAP_ENTRY(nsIAccessiblePivotObserver)
+NS_INTERFACE_MAP_END_INHERITING(HyperTextAccessible)
+
+NS_IMPL_ADDREF_INHERITED(DocAccessible, HyperTextAccessible)
+NS_IMPL_RELEASE_INHERITED(DocAccessible, HyperTextAccessible)
+
+////////////////////////////////////////////////////////////////////////////////
+// nsIAccessible
+
+ENameValueFlag DocAccessible::Name(nsString& aName) const {
+ aName.Truncate();
+
+ if (mParent) {
+ mParent->Name(aName); // Allow owning iframe to override the name
+ }
+ if (aName.IsEmpty()) {
+ // Allow name via aria-labelledby or title attribute
+ LocalAccessible::Name(aName);
+ }
+ if (aName.IsEmpty()) {
+ Title(aName); // Try title element
+ }
+ if (aName.IsEmpty()) { // Last resort: use URL
+ URL(aName);
+ }
+
+ return eNameOK;
+}
+
+// LocalAccessible public method
+role DocAccessible::NativeRole() const {
+ nsCOMPtr<nsIDocShell> docShell = nsCoreUtils::GetDocShellFor(mDocumentNode);
+ if (docShell) {
+ nsCOMPtr<nsIDocShellTreeItem> sameTypeRoot;
+ docShell->GetInProcessSameTypeRootTreeItem(getter_AddRefs(sameTypeRoot));
+ int32_t itemType = docShell->ItemType();
+ if (sameTypeRoot == docShell) {
+ // Root of content or chrome tree
+ if (itemType == nsIDocShellTreeItem::typeChrome) {
+ return roles::CHROME_WINDOW;
+ }
+
+ if (itemType == nsIDocShellTreeItem::typeContent) {
+ return roles::DOCUMENT;
+ }
+ } else if (itemType == nsIDocShellTreeItem::typeContent) {
+ return roles::DOCUMENT;
+ }
+ }
+
+ return roles::PANE; // Fall back;
+}
+
+void DocAccessible::Description(nsString& aDescription) const {
+ if (mParent) mParent->Description(aDescription);
+
+ if (HasOwnContent() && aDescription.IsEmpty()) {
+ nsTextEquivUtils::GetTextEquivFromIDRefs(this, nsGkAtoms::aria_describedby,
+ aDescription);
+ }
+}
+
+// LocalAccessible public method
+uint64_t DocAccessible::NativeState() const {
+ // Document is always focusable.
+ uint64_t state =
+ states::FOCUSABLE; // keep in sync with NativeInteractiveState() impl
+ if (FocusMgr()->IsFocused(this)) state |= states::FOCUSED;
+
+ // Expose stale state until the document is ready (DOM is loaded and tree is
+ // constructed).
+ if (!HasLoadState(eReady)) state |= states::STALE;
+
+ // Expose state busy until the document and all its subdocuments is completely
+ // loaded.
+ if (!HasLoadState(eCompletelyLoaded)) state |= states::BUSY;
+
+ nsIFrame* frame = GetFrame();
+ if (!frame || !frame->IsVisibleConsideringAncestors(
+ nsIFrame::VISIBILITY_CROSS_CHROME_CONTENT_BOUNDARY)) {
+ state |= states::INVISIBLE | states::OFFSCREEN;
+ }
+
+ RefPtr<EditorBase> editorBase = GetEditor();
+ state |= editorBase ? states::EDITABLE : states::READONLY;
+
+ return state;
+}
+
+uint64_t DocAccessible::NativeInteractiveState() const {
+ // Document is always focusable.
+ return states::FOCUSABLE;
+}
+
+bool DocAccessible::NativelyUnavailable() const { return false; }
+
+// LocalAccessible public method
+void DocAccessible::ApplyARIAState(uint64_t* aState) const {
+ // Grab states from content element.
+ if (mContent) LocalAccessible::ApplyARIAState(aState);
+
+ // Allow iframe/frame etc. to have final state override via ARIA.
+ if (mParent) mParent->ApplyARIAState(aState);
+}
+
+Accessible* DocAccessible::FocusedChild() {
+ // Return an accessible for the current global focus, which does not have to
+ // be contained within the current document.
+ return FocusMgr()->FocusedAccessible();
+}
+
+void DocAccessible::TakeFocus() const {
+ // Focus the document.
+ nsFocusManager* fm = nsFocusManager::GetFocusManager();
+ RefPtr<dom::Element> newFocus;
+ dom::AutoHandlingUserInputStatePusher inputStatePusher(true);
+ fm->MoveFocus(mDocumentNode->GetWindow(), nullptr,
+ nsFocusManager::MOVEFOCUS_ROOT, 0, getter_AddRefs(newFocus));
+}
+
+// HyperTextAccessible method
+already_AddRefed<EditorBase> DocAccessible::GetEditor() const {
+ // Check if document is editable (designMode="on" case). Otherwise check if
+ // the html:body (for HTML document case) or document element is editable.
+ if (!mDocumentNode->IsInDesignMode() &&
+ (!mContent || !mContent->HasFlag(NODE_IS_EDITABLE))) {
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIDocShell> docShell = mDocumentNode->GetDocShell();
+ if (!docShell) {
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIEditingSession> editingSession;
+ docShell->GetEditingSession(getter_AddRefs(editingSession));
+ if (!editingSession) return nullptr; // No editing session interface
+
+ RefPtr<HTMLEditor> htmlEditor =
+ editingSession->GetHTMLEditorForWindow(mDocumentNode->GetWindow());
+ if (!htmlEditor) {
+ return nullptr;
+ }
+
+ bool isEditable = false;
+ htmlEditor->GetIsDocumentEditable(&isEditable);
+ if (isEditable) {
+ return htmlEditor.forget();
+ }
+
+ return nullptr;
+}
+
+// DocAccessible public method
+
+void DocAccessible::URL(nsAString& aURL) const {
+ aURL.Truncate();
+ nsCOMPtr<nsISupports> container = mDocumentNode->GetContainer();
+ nsCOMPtr<nsIWebNavigation> webNav(do_GetInterface(container));
+ if (MOZ_UNLIKELY(!webNav)) {
+ return;
+ }
+
+ nsCOMPtr<nsIURI> uri;
+ webNav->GetCurrentURI(getter_AddRefs(uri));
+ if (MOZ_UNLIKELY(!uri)) {
+ return;
+ }
+ // Let's avoid treating too long URI in the main process for avoiding
+ // memory fragmentation as far as possible.
+ if (uri->SchemeIs("data") || uri->SchemeIs("blob")) {
+ return;
+ }
+
+ nsCOMPtr<nsIIOService> io = mozilla::components::IO::Service();
+ if (NS_WARN_IF(!io)) {
+ return;
+ }
+ nsCOMPtr<nsIURI> exposableURI;
+ if (NS_FAILED(io->CreateExposableURI(uri, getter_AddRefs(exposableURI))) ||
+ MOZ_UNLIKELY(!exposableURI)) {
+ return;
+ }
+ nsAutoCString theURL;
+ if (NS_SUCCEEDED(exposableURI->GetSpec(theURL))) {
+ CopyUTF8toUTF16(theURL, aURL);
+ }
+}
+
+void DocAccessible::Title(nsString& aTitle) const {
+ mDocumentNode->GetTitle(aTitle);
+}
+
+void DocAccessible::MimeType(nsAString& aType) const {
+ mDocumentNode->GetContentType(aType);
+}
+
+void DocAccessible::DocType(nsAString& aType) const {
+ dom::DocumentType* docType = mDocumentNode->GetDoctype();
+ if (docType) docType->GetPublicId(aType);
+}
+
+void DocAccessible::QueueCacheUpdate(LocalAccessible* aAcc,
+ uint64_t aNewDomain) {
+ if (!mIPCDoc || !StaticPrefs::accessibility_cache_enabled_AtStartup()) {
+ return;
+ }
+ uint64_t& domain = mQueuedCacheUpdates.LookupOrInsert(aAcc, 0);
+ domain |= aNewDomain;
+ Controller()->ScheduleProcessing();
+}
+
+void DocAccessible::QueueCacheUpdateForDependentRelations(
+ LocalAccessible* aAcc) {
+ if (!mIPCDoc || !StaticPrefs::accessibility_cache_enabled_AtStartup() ||
+ !aAcc || !aAcc->Elm() || !aAcc->IsInDocument() || aAcc->IsDefunct()) {
+ return;
+ }
+ nsAutoString ID;
+ aAcc->DOMNodeID(ID);
+ if (AttrRelProviders* list = GetRelProviders(aAcc->Elm(), ID)) {
+ // We call this function when we've noticed an ID change, or when an acc
+ // is getting bound to its document. We need to ensure any existing accs
+ // that depend on this acc's ID have their rel cache entries updated.
+ for (const auto& provider : *list) {
+ LocalAccessible* relatedAcc = GetAccessible(provider->mContent);
+ if (!relatedAcc || relatedAcc->IsDefunct() ||
+ !relatedAcc->IsInDocument() ||
+ mInsertedAccessibles.Contains(relatedAcc)) {
+ continue;
+ }
+ QueueCacheUpdate(relatedAcc, CacheDomain::Relations);
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// LocalAccessible
+
+void DocAccessible::Init() {
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eDocCreate)) {
+ logging::DocCreate("document initialize", mDocumentNode, this);
+ }
+#endif
+
+ // Initialize notification controller.
+ mNotificationController = new NotificationController(this, mPresShell);
+
+ // Mark the DocAccessible as loaded if its DOM document is already loaded at
+ // this point. This can happen for one of three reasons:
+ // 1. A11y was started late.
+ // 2. DOM loading for a document (probably an in-process iframe) completed
+ // before its Accessible container was created.
+ // 3. The PresShell for the document was created after DOM loading completed.
+ // In that case, we tried to create the DocAccessible when DOM loading
+ // completed, but we can't create a DocAccessible without a PresShell, so
+ // this failed. The DocAccessible was subsequently created due to a layout
+ // notification.
+ if (mDocumentNode->GetReadyStateEnum() ==
+ dom::Document::READYSTATE_COMPLETE) {
+ mLoadState |= eDOMLoaded;
+ // If this happened due to reasons 1 or 2, it isn't *necessary* to fire a
+ // doc load complete event. If it happened due to reason 3, we need to fire
+ // doc load complete because clients (especially tests) might be waiting
+ // for the document to load using this event. We can't distinguish why this
+ // happened at this point, so just fire it regardless. It won't do any
+ // harm even if it isn't necessary. We set mLoadEventType here and it will
+ // be fired in ProcessLoad as usual.
+ mLoadEventType = nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE;
+ } else if (mDocumentNode->IsInitialDocument()) {
+ // The initial about:blank document will never finish loading, so we can
+ // immediately mark it loaded to avoid waiting for its load.
+ mLoadState |= eDOMLoaded;
+ }
+
+ AddEventListeners();
+}
+
+void DocAccessible::Shutdown() {
+ if (!mPresShell) { // already shutdown
+ return;
+ }
+
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eDocDestroy)) {
+ logging::DocDestroy("document shutdown", mDocumentNode, this);
+ }
+#endif
+
+ // Mark the document as shutdown before AT is notified about the document
+ // removal from its container (valid for root documents on ATK and due to
+ // some reason for MSAA, refer to bug 757392 for details).
+ mStateFlags |= eIsDefunct;
+
+ if (mNotificationController) {
+ mNotificationController->Shutdown();
+ mNotificationController = nullptr;
+ }
+
+ RemoveEventListeners();
+
+ // mParent->RemoveChild clears mParent, but we need to know whether we were a
+ // child later, so use a flag.
+ const bool isChild = !!mParent;
+ if (mParent) {
+ DocAccessible* parentDocument = mParent->Document();
+ if (parentDocument) parentDocument->RemoveChildDocument(this);
+
+ mParent->RemoveChild(this);
+ MOZ_ASSERT(!mParent, "Parent has to be null!");
+ }
+
+ mPresShell->SetDocAccessible(nullptr);
+ mPresShell = nullptr; // Avoid reentrancy
+
+ // Walk the array backwards because child documents remove themselves from the
+ // array as they are shutdown.
+ int32_t childDocCount = mChildDocuments.Length();
+ for (int32_t idx = childDocCount - 1; idx >= 0; idx--) {
+ mChildDocuments[idx]->Shutdown();
+ }
+
+ mChildDocuments.Clear();
+ // mQueuedCacheUpdates can contain a reference to this document (ex. if the
+ // doc is scrollable and we're sending a scroll position update). Clear the
+ // map here to avoid creating ref cycles.
+ mQueuedCacheUpdates.Clear();
+
+ // XXX thinking about ordering?
+ if (mIPCDoc) {
+ MOZ_ASSERT(IPCAccessibilityActive());
+ mIPCDoc->Shutdown();
+ MOZ_ASSERT(!mIPCDoc);
+ }
+
+ if (mVirtualCursor) {
+ mVirtualCursor->RemoveObserver(this);
+ mVirtualCursor = nullptr;
+ }
+
+ mDependentIDsHashes.Clear();
+ mNodeToAccessibleMap.Clear();
+
+ mAnchorJumpElm = nullptr;
+ mInvalidationList.Clear();
+ mPendingUpdates.Clear();
+
+ for (auto iter = mAccessibleCache.Iter(); !iter.Done(); iter.Next()) {
+ LocalAccessible* accessible = iter.Data();
+ MOZ_ASSERT(accessible);
+ if (accessible) {
+ // This might have been focused with FocusManager::ActiveItemChanged. In
+ // that case, we must notify FocusManager so that it clears the active
+ // item. Otherwise, it will hold on to a defunct Accessible. Normally,
+ // this happens in UnbindFromDocument, but we don't call that when the
+ // whole document shuts down.
+ if (FocusMgr()->WasLastFocused(accessible)) {
+ FocusMgr()->ActiveItemChanged(nullptr);
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eFocus)) {
+ logging::ActiveItemChangeCausedBy("doc shutdown", accessible);
+ }
+#endif
+ }
+ if (!accessible->IsDefunct()) {
+ // Unlink parent to avoid its cleaning overhead in shutdown.
+ accessible->mParent = nullptr;
+ accessible->Shutdown();
+ }
+ }
+ iter.Remove();
+ }
+
+ HyperTextAccessibleWrap::Shutdown();
+
+ MOZ_ASSERT(GetAccService());
+ GetAccService()->NotifyOfDocumentShutdown(
+ this, mDocumentNode,
+ // Make sure we don't shut down AccService while a parent document is
+ // still shutting down. The parent will allow service shutdown when it
+ // reaches this point.
+ /* aAllowServiceShutdown */ !isChild);
+ mDocumentNode = nullptr;
+}
+
+nsIFrame* DocAccessible::GetFrame() const {
+ nsIFrame* root = nullptr;
+ if (mPresShell) {
+ root = mPresShell->GetRootFrame();
+ }
+
+ return root;
+}
+
+nsINode* DocAccessible::GetNode() const { return mDocumentNode; }
+
+// DocAccessible protected member
+nsRect DocAccessible::RelativeBounds(nsIFrame** aRelativeFrame) const {
+ *aRelativeFrame = GetFrame();
+
+ dom::Document* document = mDocumentNode;
+ dom::Document* parentDoc = nullptr;
+
+ nsRect bounds;
+ while (document) {
+ PresShell* presShell = document->GetPresShell();
+ if (!presShell) {
+ return nsRect();
+ }
+
+ nsRect scrollPort;
+ nsIScrollableFrame* sf = presShell->GetRootScrollFrameAsScrollable();
+ if (sf) {
+ scrollPort = sf->GetScrollPortRect();
+ } else {
+ nsIFrame* rootFrame = presShell->GetRootFrame();
+ if (!rootFrame) return nsRect();
+
+ scrollPort = rootFrame->GetRect();
+ }
+
+ if (parentDoc) { // After first time thru loop
+ // XXXroc bogus code! scrollPort is relative to the viewport of
+ // this document, but we're intersecting rectangles derived from
+ // multiple documents and assuming they're all in the same coordinate
+ // system. See bug 514117.
+ bounds.IntersectRect(scrollPort, bounds);
+ } else { // First time through loop
+ bounds = scrollPort;
+ }
+
+ document = parentDoc = document->GetInProcessParentDocument();
+ }
+
+ return bounds;
+}
+
+// DocAccessible protected member
+nsresult DocAccessible::AddEventListeners() {
+ SelectionMgr()->AddDocSelectionListener(mPresShell);
+
+ // Add document observer.
+ mDocumentNode->AddObserver(this);
+ return NS_OK;
+}
+
+// DocAccessible protected member
+nsresult DocAccessible::RemoveEventListeners() {
+ // Remove listeners associated with content documents
+ NS_ASSERTION(mDocumentNode, "No document during removal of listeners.");
+
+ if (mDocumentNode) {
+ mDocumentNode->RemoveObserver(this);
+ }
+
+ if (mScrollWatchTimer) {
+ mScrollWatchTimer->Cancel();
+ mScrollWatchTimer = nullptr;
+ NS_RELEASE_THIS(); // Kung fu death grip
+ }
+
+ SelectionMgr()->RemoveDocSelectionListener(mPresShell);
+ return NS_OK;
+}
+
+void DocAccessible::ScrollTimerCallback(nsITimer* aTimer, void* aClosure) {
+ DocAccessible* docAcc = reinterpret_cast<DocAccessible*>(aClosure);
+
+ if (docAcc) {
+ // Dispatch a scroll-end for all entries in table. They have not
+ // been scrolled in at least `kScrollEventInterval`.
+ for (auto iter = docAcc->mLastScrollingDispatch.Iter(); !iter.Done();
+ iter.Next()) {
+ docAcc->DispatchScrollingEvent(iter.Key(),
+ nsIAccessibleEvent::EVENT_SCROLLING_END);
+ iter.Remove();
+ }
+
+ if (docAcc->mScrollWatchTimer) {
+ docAcc->mScrollWatchTimer = nullptr;
+ NS_RELEASE(docAcc); // Release kung fu death grip
+ }
+ }
+}
+
+void DocAccessible::HandleScroll(nsINode* aTarget) {
+ nsINode* target = aTarget;
+ LocalAccessible* targetAcc = GetAccessible(target);
+ if (!targetAcc && target->IsInNativeAnonymousSubtree()) {
+ // The scroll event for textareas comes from a native anonymous div. We need
+ // the closest non-anonymous ancestor to get the right Accessible.
+ target = target->GetClosestNativeAnonymousSubtreeRootParent();
+ targetAcc = GetAccessible(target);
+ }
+ // Regardless of our scroll timer, we need to send a cache update
+ // to ensure the next Bounds() query accurately reflects our position
+ // after scrolling.
+ if (targetAcc) {
+ QueueCacheUpdate(targetAcc, CacheDomain::ScrollPosition);
+ }
+
+ const uint32_t kScrollEventInterval = 100;
+ // If we haven't dispatched a scrolling event for a target in at least
+ // kScrollEventInterval milliseconds, dispatch one now.
+ mLastScrollingDispatch.WithEntryHandle(target, [&](auto&& lastDispatch) {
+ const TimeStamp now = TimeStamp::Now();
+
+ if (!lastDispatch ||
+ (now - lastDispatch.Data()).ToMilliseconds() >= kScrollEventInterval) {
+ // We can't fire events on a document whose tree isn't constructed yet.
+ if (HasLoadState(eTreeConstructed)) {
+ DispatchScrollingEvent(target, nsIAccessibleEvent::EVENT_SCROLLING);
+ }
+ lastDispatch.InsertOrUpdate(now);
+ }
+ });
+
+ // If timer callback is still pending, push it 100ms into the future.
+ // When scrolling ends and we don't fire this callback anymore, the
+ // timer callback will fire and dispatch an EVENT_SCROLLING_END.
+ if (mScrollWatchTimer) {
+ mScrollWatchTimer->SetDelay(kScrollEventInterval);
+ } else {
+ NS_NewTimerWithFuncCallback(getter_AddRefs(mScrollWatchTimer),
+ ScrollTimerCallback, this, kScrollEventInterval,
+ nsITimer::TYPE_ONE_SHOT,
+ "a11y::DocAccessible::ScrollPositionDidChange");
+ if (mScrollWatchTimer) {
+ NS_ADDREF_THIS(); // Kung fu death grip
+ }
+ }
+}
+
+std::pair<nsPoint, nsRect> DocAccessible::ComputeScrollData(
+ LocalAccessible* aAcc) {
+ nsPoint scrollPoint;
+ nsRect scrollRange;
+
+ if (nsIFrame* frame = aAcc->GetFrame()) {
+ nsIScrollableFrame* sf = aAcc == this
+ ? mPresShell->GetRootScrollFrameAsScrollable()
+ : frame->GetScrollTargetFrame();
+
+ // If there is no scrollable frame, it's likely a scroll in a popup, like
+ // <select>. Return a scroll offset and range of 0. The scroll info
+ // is currently only used on Android, and popups are rendered natively
+ // there.
+ if (sf) {
+ scrollPoint = sf->GetScrollPosition() * mPresShell->GetResolution();
+ scrollRange = sf->GetScrollRange();
+ scrollRange.ScaleRoundOut(mPresShell->GetResolution());
+ }
+ }
+
+ return {scrollPoint, scrollRange};
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// nsIAccessiblePivotObserver
+
+NS_IMETHODIMP
+DocAccessible::OnPivotChanged(nsIAccessiblePivot* aPivot,
+ nsIAccessible* aOldAccessible, int32_t aOldStart,
+ int32_t aOldEnd, nsIAccessible* aNewAccessible,
+ int32_t aNewStart, int32_t aNewEnd,
+ PivotMoveReason aReason,
+ TextBoundaryType aBoundaryType,
+ bool aIsFromUserInput) {
+ RefPtr<AccEvent> event = new AccVCChangeEvent(
+ this, (aOldAccessible ? aOldAccessible->ToInternalAccessible() : nullptr),
+ aOldStart, aOldEnd,
+ (aNewAccessible ? aNewAccessible->ToInternalAccessible() : nullptr),
+ aNewStart, aNewEnd, aReason, aBoundaryType,
+ aIsFromUserInput ? eFromUserInput : eNoUserInput);
+ nsEventShell::FireEvent(event);
+
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// nsIDocumentObserver
+
+NS_IMPL_NSIDOCUMENTOBSERVER_CORE_STUB(DocAccessible)
+NS_IMPL_NSIDOCUMENTOBSERVER_LOAD_STUB(DocAccessible)
+
+void DocAccessible::AttributeWillChange(dom::Element* aElement,
+ int32_t aNameSpaceID,
+ nsAtom* aAttribute, int32_t aModType) {
+ LocalAccessible* accessible = GetAccessible(aElement);
+ if (!accessible) {
+ if (aElement != mContent) return;
+
+ accessible = this;
+ }
+
+ // Update dependent IDs cache. Take care of elements that are accessible
+ // because dependent IDs cache doesn't contain IDs from non accessible
+ // elements.
+ if (aModType != dom::MutationEvent_Binding::ADDITION) {
+ RemoveDependentIDsFor(accessible, aAttribute);
+ }
+
+ if (aAttribute == nsGkAtoms::id) {
+ if (accessible->IsActiveDescendant()) {
+ RefPtr<AccEvent> event =
+ new AccStateChangeEvent(accessible, states::ACTIVE, false);
+ FireDelayedEvent(event);
+ }
+
+ RelocateARIAOwnedIfNeeded(aElement);
+ }
+
+ if (aAttribute == nsGkAtoms::aria_activedescendant) {
+ if (LocalAccessible* activeDescendant = accessible->CurrentItem()) {
+ RefPtr<AccEvent> event =
+ new AccStateChangeEvent(activeDescendant, states::ACTIVE, false);
+ FireDelayedEvent(event);
+ }
+ }
+
+ // If attribute affects accessible's state, store the old state so we can
+ // later compare it against the state of the accessible after the attribute
+ // change.
+ if (accessible->AttributeChangesState(aAttribute)) {
+ mPrevStateBits = accessible->State();
+ } else {
+ mPrevStateBits = 0;
+ }
+}
+
+void DocAccessible::NativeAnonymousChildListChange(nsIContent* aContent,
+ bool aIsRemove) {
+ if (aIsRemove) {
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eTree)) {
+ logging::MsgBegin("TREE", "Anonymous content removed; doc: %p", this);
+ logging::Node("node", aContent);
+ logging::MsgEnd();
+ }
+#endif
+
+ ContentRemoved(aContent);
+ }
+}
+
+void DocAccessible::AttributeChanged(dom::Element* aElement,
+ int32_t aNameSpaceID, nsAtom* aAttribute,
+ int32_t aModType,
+ const nsAttrValue* aOldValue) {
+ NS_ASSERTION(!IsDefunct(),
+ "Attribute changed called on defunct document accessible!");
+
+ // Proceed even if the element is not accessible because element may become
+ // accessible if it gets certain attribute.
+ if (UpdateAccessibleOnAttrChange(aElement, aAttribute)) return;
+
+ // Update the accessible tree on aria-hidden change. Make sure to not create
+ // a tree under aria-hidden='true'.
+ if (aAttribute == nsGkAtoms::aria_hidden) {
+ if (aria::HasDefinedARIAHidden(aElement)) {
+ ContentRemoved(aElement);
+ } else {
+ ContentInserted(aElement, aElement->GetNextSibling());
+ }
+ return;
+ }
+
+ LocalAccessible* accessible = GetAccessible(aElement);
+ if (!accessible) {
+ if (mContent == aElement) {
+ // The attribute change occurred on the root content of this
+ // DocAccessible, so handle it as an attribute change on this.
+ accessible = this;
+ } else {
+ if (aModType == dom::MutationEvent_Binding::ADDITION &&
+ aria::AttrCharacteristicsFor(aAttribute) & ATTR_GLOBAL) {
+ // The element doesn't have an Accessible, but a global ARIA attribute
+ // was just added, which means we should probably create an Accessible.
+ ContentInserted(aElement, aElement->GetNextSibling());
+ return;
+ }
+ // The element doesn't have an Accessible, so ignore the attribute
+ // change.
+ return;
+ }
+ }
+
+ MOZ_ASSERT(accessible->IsBoundToParent() || accessible->IsDoc(),
+ "DOM attribute change on an accessible detached from the tree");
+
+ if (aAttribute == nsGkAtoms::id) {
+ dom::Element* elm = accessible->Elm();
+ RelocateARIAOwnedIfNeeded(elm);
+ ARIAActiveDescendantIDMaybeMoved(accessible);
+ accessible->SendCache(CacheDomain::DOMNodeID, CacheUpdateType::Update);
+ QueueCacheUpdateForDependentRelations(accessible);
+ }
+
+ // The activedescendant universal property redirects accessible focus events
+ // to the element with the id that activedescendant points to. Make sure
+ // the tree up to date before processing. In other words, when a node has just
+ // been inserted, the tree won't be up to date yet, so we must always schedule
+ // an async notification so that a newly inserted node will be present in
+ // the tree.
+ if (aAttribute == nsGkAtoms::aria_activedescendant) {
+ mNotificationController
+ ->ScheduleNotification<DocAccessible, LocalAccessible>(
+ this, &DocAccessible::ARIAActiveDescendantChanged, accessible);
+ return;
+ }
+
+ // Defer to accessible any needed actions like changing states or emiting
+ // events.
+ accessible->DOMAttributeChanged(aNameSpaceID, aAttribute, aModType, aOldValue,
+ mPrevStateBits);
+
+ // Update dependent IDs cache. We handle elements with accessibles.
+ // If the accessible or element with the ID doesn't exist yet the cache will
+ // be updated when they are added.
+ if (aModType == dom::MutationEvent_Binding::MODIFICATION ||
+ aModType == dom::MutationEvent_Binding::ADDITION) {
+ AddDependentIDsFor(accessible, aAttribute);
+ }
+}
+
+void DocAccessible::ARIAAttributeDefaultWillChange(dom::Element* aElement,
+ nsAtom* aAttribute,
+ int32_t aModType) {
+ NS_ASSERTION(!IsDefunct(),
+ "Attribute changed called on defunct document accessible!");
+
+ if (aElement->HasAttr(aAttribute)) {
+ return;
+ }
+
+ AttributeWillChange(aElement, kNameSpaceID_None, aAttribute, aModType);
+}
+
+void DocAccessible::ARIAAttributeDefaultChanged(dom::Element* aElement,
+ nsAtom* aAttribute,
+ int32_t aModType) {
+ NS_ASSERTION(!IsDefunct(),
+ "Attribute changed called on defunct document accessible!");
+
+ if (aElement->HasAttr(aAttribute)) {
+ return;
+ }
+
+ AttributeChanged(aElement, kNameSpaceID_None, aAttribute, aModType, nullptr);
+}
+
+void DocAccessible::ARIAActiveDescendantChanged(LocalAccessible* aAccessible) {
+ if (dom::Element* elm = aAccessible->Elm()) {
+ nsAutoString id;
+ if (elm->GetAttr(nsGkAtoms::aria_activedescendant, id)) {
+ dom::Element* activeDescendantElm = IDRefsIterator::GetElem(elm, id);
+ if (activeDescendantElm) {
+ LocalAccessible* activeDescendant = GetAccessible(activeDescendantElm);
+ if (activeDescendant) {
+ RefPtr<AccEvent> event =
+ new AccStateChangeEvent(activeDescendant, states::ACTIVE, true);
+ FireDelayedEvent(event);
+ if (aAccessible->IsActiveWidget()) {
+ FocusMgr()->ActiveItemChanged(activeDescendant, false);
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eFocus)) {
+ logging::ActiveItemChangeCausedBy("ARIA activedescedant changed",
+ activeDescendant);
+ }
+#endif
+ }
+ return;
+ }
+ }
+ }
+
+ // aria-activedescendant was cleared or changed to a non-existent node.
+ // Move focus back to the element itself.
+ FocusMgr()->ActiveItemChanged(aAccessible, false);
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eFocus)) {
+ logging::ActiveItemChangeCausedBy("ARIA activedescedant cleared",
+ aAccessible);
+ }
+#endif
+ }
+}
+
+void DocAccessible::ContentAppended(nsIContent* aFirstNewContent) {
+ MaybeHandleChangeToHiddenNameOrDescription(aFirstNewContent);
+}
+
+void DocAccessible::ElementStateChanged(dom::Document* aDocument,
+ dom::Element* aElement,
+ dom::ElementState aStateMask) {
+ if (aStateMask.HasState(dom::ElementState::READWRITE) &&
+ aElement == mDocumentNode->GetRootElement()) {
+ // This handles changes to designMode. contentEditable is handled by
+ // LocalAccessible::AttributeChangesState and
+ // LocalAccessible::DOMAttributeChanged.
+ const bool isEditable =
+ aElement->State().HasState(dom::ElementState::READWRITE);
+ RefPtr<AccEvent> event =
+ new AccStateChangeEvent(this, states::EDITABLE, isEditable);
+ FireDelayedEvent(event);
+ event = new AccStateChangeEvent(this, states::READONLY, !isEditable);
+ FireDelayedEvent(event);
+ }
+
+ LocalAccessible* accessible = GetAccessible(aElement);
+ if (!accessible) return;
+
+ if (aStateMask.HasState(dom::ElementState::CHECKED)) {
+ LocalAccessible* widget = accessible->ContainerWidget();
+ if (widget && widget->IsSelect()) {
+ // Changing selection here changes what we cache for
+ // the viewport.
+ SetViewportCacheDirty(true);
+ AccSelChangeEvent::SelChangeType selChangeType =
+ aElement->State().HasState(dom::ElementState::CHECKED)
+ ? AccSelChangeEvent::eSelectionAdd
+ : AccSelChangeEvent::eSelectionRemove;
+ RefPtr<AccEvent> event =
+ new AccSelChangeEvent(widget, accessible, selChangeType);
+ FireDelayedEvent(event);
+ return;
+ }
+
+ RefPtr<AccEvent> event = new AccStateChangeEvent(
+ accessible, states::CHECKED,
+ aElement->State().HasState(dom::ElementState::CHECKED));
+ FireDelayedEvent(event);
+ }
+
+ if (aStateMask.HasState(dom::ElementState::INVALID)) {
+ RefPtr<AccEvent> event =
+ new AccStateChangeEvent(accessible, states::INVALID, true);
+ FireDelayedEvent(event);
+ }
+
+ if (aStateMask.HasState(dom::ElementState::REQUIRED)) {
+ RefPtr<AccEvent> event =
+ new AccStateChangeEvent(accessible, states::REQUIRED);
+ FireDelayedEvent(event);
+ }
+
+ if (aStateMask.HasState(dom::ElementState::VISITED)) {
+ RefPtr<AccEvent> event =
+ new AccStateChangeEvent(accessible, states::TRAVERSED, true);
+ FireDelayedEvent(event);
+ }
+
+ // We only expose dom::ElementState::DEFAULT on buttons, but we can get
+ // notifications for other controls like checkboxes.
+ if (aStateMask.HasState(dom::ElementState::DEFAULT) &&
+ accessible->IsButton()) {
+ RefPtr<AccEvent> event =
+ new AccStateChangeEvent(accessible, states::DEFAULT);
+ FireDelayedEvent(event);
+ }
+}
+
+void DocAccessible::CharacterDataWillChange(nsIContent* aContent,
+ const CharacterDataChangeInfo&) {}
+
+void DocAccessible::CharacterDataChanged(nsIContent* aContent,
+ const CharacterDataChangeInfo&) {}
+
+void DocAccessible::ContentInserted(nsIContent* aChild) {
+ MaybeHandleChangeToHiddenNameOrDescription(aChild);
+}
+
+void DocAccessible::ContentRemoved(nsIContent* aChildNode,
+ nsIContent* aPreviousSiblingNode) {
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eTree)) {
+ logging::MsgBegin("TREE", "DOM content removed; doc: %p", this);
+ logging::Node("container node", aChildNode->GetParent());
+ logging::Node("content node", aChildNode);
+ logging::MsgEnd();
+ }
+#endif
+ // This one and content removal notification from layout may result in
+ // double processing of same subtrees. If it pops up in profiling, then
+ // consider reusing a document node cache to reject these notifications early.
+ ContentRemoved(aChildNode);
+}
+
+void DocAccessible::ParentChainChanged(nsIContent* aContent) {}
+
+////////////////////////////////////////////////////////////////////////////////
+// LocalAccessible
+
+#ifdef A11Y_LOG
+nsresult DocAccessible::HandleAccEvent(AccEvent* aEvent) {
+ if (logging::IsEnabled(logging::eDocLoad)) {
+ logging::DocLoadEventHandled(aEvent);
+ }
+
+ return HyperTextAccessible::HandleAccEvent(aEvent);
+}
+#endif
+
+////////////////////////////////////////////////////////////////////////////////
+// Public members
+
+nsPresContext* DocAccessible::PresContext() const {
+ return mPresShell->GetPresContext();
+}
+
+void* DocAccessible::GetNativeWindow() const {
+ if (!mPresShell) {
+ return nullptr;
+ }
+
+ nsViewManager* vm = mPresShell->GetViewManager();
+ if (!vm) return nullptr;
+
+ nsCOMPtr<nsIWidget> widget = vm->GetRootWidget();
+ if (widget) return widget->GetNativeData(NS_NATIVE_WINDOW);
+
+ return nullptr;
+}
+
+LocalAccessible* DocAccessible::GetAccessibleByUniqueIDInSubtree(
+ void* aUniqueID) {
+ LocalAccessible* child = GetAccessibleByUniqueID(aUniqueID);
+ if (child) return child;
+
+ uint32_t childDocCount = mChildDocuments.Length();
+ for (uint32_t childDocIdx = 0; childDocIdx < childDocCount; childDocIdx++) {
+ DocAccessible* childDocument = mChildDocuments.ElementAt(childDocIdx);
+ child = childDocument->GetAccessibleByUniqueIDInSubtree(aUniqueID);
+ if (child) return child;
+ }
+
+ return nullptr;
+}
+
+LocalAccessible* DocAccessible::GetAccessibleOrContainer(
+ nsINode* aNode, bool aNoContainerIfPruned) const {
+ if (!aNode || !aNode->GetComposedDoc()) {
+ return nullptr;
+ }
+
+ nsINode* start = aNode;
+ if (auto* shadowRoot = dom::ShadowRoot::FromNode(aNode)) {
+ // This can happen, for example, when called within
+ // SelectionManager::ProcessSelectionChanged due to focusing a direct
+ // child of a shadow root.
+ // GetFlattenedTreeParent works on children of a shadow root, but not the
+ // shadow root itself.
+ start = shadowRoot->GetHost();
+ if (!start) {
+ return nullptr;
+ }
+ }
+
+ for (nsINode* currNode : dom::InclusiveFlatTreeAncestors(*start)) {
+ // No container if is inside of aria-hidden subtree.
+ if (aNoContainerIfPruned && currNode->IsElement() &&
+ aria::HasDefinedARIAHidden(currNode->AsElement())) {
+ return nullptr;
+ }
+
+ // Check if node is in zero-sized map
+ if (aNoContainerIfPruned && currNode->IsHTMLElement(nsGkAtoms::map)) {
+ if (nsIFrame* frame = currNode->AsContent()->GetPrimaryFrame()) {
+ if (nsLayoutUtils::GetAllInFlowRectsUnion(frame, frame->GetParent())
+ .IsEmpty()) {
+ return nullptr;
+ }
+ }
+ }
+
+ if (LocalAccessible* accessible = GetAccessible(currNode)) {
+ return accessible;
+ }
+ }
+
+ return nullptr;
+}
+
+LocalAccessible* DocAccessible::GetContainerAccessible(nsINode* aNode) const {
+ return aNode ? GetAccessibleOrContainer(aNode->GetFlattenedTreeParentNode())
+ : nullptr;
+}
+
+LocalAccessible* DocAccessible::GetAccessibleOrDescendant(
+ nsINode* aNode) const {
+ LocalAccessible* acc = GetAccessible(aNode);
+ if (acc) return acc;
+
+ if (aNode == mContent || aNode == mDocumentNode->GetRootElement()) {
+ // If the node is the doc's body or root element, return the doc accessible.
+ return const_cast<DocAccessible*>(this);
+ }
+
+ acc = GetContainerAccessible(aNode);
+ if (acc) {
+ TreeWalker walker(acc, aNode->AsContent(),
+ TreeWalker::eWalkCache | TreeWalker::eScoped);
+ return walker.Next();
+ }
+
+ return nullptr;
+}
+
+void DocAccessible::BindToDocument(LocalAccessible* aAccessible,
+ const nsRoleMapEntry* aRoleMapEntry) {
+ // Put into DOM node cache.
+ if (aAccessible->IsNodeMapEntry()) {
+ mNodeToAccessibleMap.InsertOrUpdate(aAccessible->GetNode(), aAccessible);
+ }
+
+ // Put into unique ID cache.
+ mAccessibleCache.InsertOrUpdate(aAccessible->UniqueID(), RefPtr{aAccessible});
+
+ aAccessible->SetRoleMapEntry(aRoleMapEntry);
+
+ if (aAccessible->HasOwnContent()) {
+ AddDependentIDsFor(aAccessible);
+
+ nsIContent* content = aAccessible->GetContent();
+ if (content->IsElement() &&
+ content->AsElement()->HasAttr(nsGkAtoms::aria_owns)) {
+ mNotificationController->ScheduleRelocation(aAccessible);
+ }
+ }
+
+ if (mIPCDoc) {
+ mInsertedAccessibles.EnsureInserted(aAccessible);
+ }
+
+ QueueCacheUpdateForDependentRelations(aAccessible);
+}
+
+void DocAccessible::UnbindFromDocument(LocalAccessible* aAccessible) {
+ NS_ASSERTION(mAccessibleCache.GetWeak(aAccessible->UniqueID()),
+ "Unbinding the unbound accessible!");
+
+ // Fire focus event on accessible having DOM focus if last focus was removed
+ // from the tree.
+ if (FocusMgr()->WasLastFocused(aAccessible)) {
+ FocusMgr()->ActiveItemChanged(nullptr);
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eFocus)) {
+ logging::ActiveItemChangeCausedBy("tree shutdown", aAccessible);
+ }
+#endif
+ }
+
+ // Remove an accessible from node-to-accessible map if it exists there.
+ if (aAccessible->IsNodeMapEntry() &&
+ mNodeToAccessibleMap.Get(aAccessible->GetNode()) == aAccessible) {
+ mNodeToAccessibleMap.Remove(aAccessible->GetNode());
+ }
+
+ aAccessible->mStateFlags |= eIsNotInDocument;
+
+ // Update XPCOM part.
+ xpcAccessibleDocument* xpcDoc = GetAccService()->GetCachedXPCDocument(this);
+ if (xpcDoc) xpcDoc->NotifyOfShutdown(aAccessible);
+
+ void* uniqueID = aAccessible->UniqueID();
+
+ NS_ASSERTION(!aAccessible->IsDefunct(), "Shutdown the shutdown accessible!");
+ aAccessible->Shutdown();
+
+ mAccessibleCache.Remove(uniqueID);
+}
+
+void DocAccessible::ContentInserted(nsIContent* aStartChildNode,
+ nsIContent* aEndChildNode) {
+ // Ignore content insertions until we constructed accessible tree. Otherwise
+ // schedule tree update on content insertion after layout.
+ if (!mNotificationController || !HasLoadState(eTreeConstructed)) {
+ return;
+ }
+
+ // The frame constructor guarantees that only ranges with the same parent
+ // arrive here in presence of dynamic changes to the page, see
+ // nsCSSFrameConstructor::IssueSingleInsertNotifications' callers.
+ nsINode* parent = aStartChildNode->GetFlattenedTreeParentNode();
+ if (!parent) {
+ return;
+ }
+
+ LocalAccessible* container = AccessibleOrTrueContainer(parent);
+ if (!container) {
+ return;
+ }
+
+ AutoTArray<nsCOMPtr<nsIContent>, 10> list;
+ for (nsIContent* node = aStartChildNode; node != aEndChildNode;
+ node = node->GetNextSibling()) {
+ MOZ_ASSERT(parent == node->GetFlattenedTreeParentNode());
+ if (PruneOrInsertSubtree(node)) {
+ list.AppendElement(node);
+ }
+ }
+
+ mNotificationController->ScheduleContentInsertion(container, list);
+}
+
+void DocAccessible::ScheduleTreeUpdate(nsIContent* aContent) {
+ if (mPendingUpdates.Contains(aContent)) {
+ return;
+ }
+ mPendingUpdates.AppendElement(aContent);
+ mNotificationController->ScheduleProcessing();
+}
+
+void DocAccessible::ProcessPendingUpdates() {
+ auto updates = std::move(mPendingUpdates);
+ for (auto update : updates) {
+ if (update->GetComposedDoc() != mDocumentNode) {
+ continue;
+ }
+ // The pruning logic will take care of avoiding unnecessary notifications.
+ ContentInserted(update, update->GetNextSibling());
+ }
+}
+
+bool DocAccessible::PruneOrInsertSubtree(nsIContent* aRoot) {
+ bool insert = false;
+
+ // In the case that we are, or are in, a shadow host, we need to assure
+ // some accessibles are removed if they are not rendered anymore.
+ nsIContent* shadowHost =
+ aRoot->GetShadowRoot() ? aRoot : aRoot->GetContainingShadowHost();
+ if (shadowHost) {
+ // Check all explicit children in the host, if they are not slotted
+ // then remove their accessibles and subtrees.
+ for (nsIContent* childNode = shadowHost->GetFirstChild(); childNode;
+ childNode = childNode->GetNextSibling()) {
+ if (!childNode->GetPrimaryFrame() &&
+ !nsCoreUtils::CanCreateAccessibleWithoutFrame(childNode)) {
+ ContentRemoved(childNode);
+ }
+ }
+
+ // If this is a slot, check to see if its fallback content is rendered,
+ // if not - remove it.
+ if (aRoot->IsHTMLElement(nsGkAtoms::slot)) {
+ for (nsIContent* childNode = aRoot->GetFirstChild(); childNode;
+ childNode = childNode->GetNextSibling()) {
+ if (!childNode->GetPrimaryFrame() &&
+ !nsCoreUtils::CanCreateAccessibleWithoutFrame(childNode)) {
+ ContentRemoved(childNode);
+ }
+ }
+ }
+ }
+
+ // If we already have an accessible, check if we need to remove it, recreate
+ // it, or keep it in place.
+ LocalAccessible* acc = GetAccessible(aRoot);
+ if (acc) {
+ MOZ_ASSERT(aRoot == acc->GetContent(),
+ "LocalAccessible has differing content!");
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eTree)) {
+ logging::MsgBegin(
+ "TREE", "inserted content already has accessible; doc: %p", this);
+ logging::Node("content node", aRoot);
+ logging::AccessibleInfo("accessible node", acc);
+ logging::MsgEnd();
+ }
+#endif
+
+ nsIFrame* frame = acc->GetFrame();
+ if (frame) {
+ acc->MaybeQueueCacheUpdateForStyleChanges();
+ }
+
+ // LocalAccessible has no frame and it's not display:contents. Remove it.
+ // As well as removing the a11y subtree, we must also remove Accessibles
+ // for DOM descendants, since some of these might be relocated Accessibles
+ // and their DOM nodes are now hidden as well.
+ if (!frame && !nsCoreUtils::CanCreateAccessibleWithoutFrame(aRoot)) {
+ ContentRemoved(aRoot);
+ return false;
+ }
+
+ // If it's a XULLabel it was probably reframed because a `value` attribute
+ // was added. The accessible creates its text leaf upon construction, so we
+ // need to recreate. Remove it, and schedule for reconstruction.
+ if (acc->IsXULLabel()) {
+ ContentRemoved(acc);
+ return true;
+ }
+
+ // It is a broken image that is being reframed because it either got
+ // or lost an `alt` tag that would rerender this node as text.
+ if (frame && (acc->IsImage() != (frame->AccessibleType() == eImageType))) {
+ ContentRemoved(aRoot);
+ return true;
+ }
+
+ // If the frame is an OuterDoc frame but this isn't an OuterDocAccessible,
+ // we need to recreate the LocalAccessible. This can happen for embed or
+ // object elements if their embedded content changes to be web content.
+ if (frame && !acc->IsOuterDoc() &&
+ frame->AccessibleType() == eOuterDocType) {
+ ContentRemoved(aRoot);
+ return true;
+ }
+
+ // If the content is focused, and is being re-framed, reset the selection
+ // listener for the node because the previous selection listener is on the
+ // old frame.
+ if (aRoot->IsElement() && FocusMgr()->HasDOMFocus(aRoot)) {
+ SelectionMgr()->SetControlSelectionListener(aRoot->AsElement());
+ }
+
+ // If the accessible is a table, or table part, its layout table
+ // status may have changed. We need to invalidate the associated
+ // mac table cache, which listens for the following event. We don't
+ // use this cache when the core cache is enabled, so to minimise event
+ // traffic only fire this event when that cache is off.
+ if (acc->IsTable() || acc->IsTableRow() || acc->IsTableCell()) {
+ LocalAccessible* table = nsAccUtils::TableFor(acc);
+ if (table && table->IsTable()) {
+ if (!StaticPrefs::accessibility_cache_enabled_AtStartup()) {
+ FireDelayedEvent(nsIAccessibleEvent::EVENT_TABLE_STYLING_CHANGED,
+ table);
+ }
+ QueueCacheUpdate(table, CacheDomain::Table);
+ }
+ }
+
+ // The accessible can be reparented or reordered in its parent.
+ // We schedule it for reinsertion. For example, a slotted element
+ // can change its slot attribute to a different slot.
+ insert = true;
+
+ // If the frame is invisible, remove it.
+ // Normally, layout sends explicit a11y notifications for visibility
+ // changes (see SendA11yNotifications in RestyleManager). However, if a
+ // visibility change also reconstructs the frame, we must handle it here.
+ if (frame && !frame->StyleVisibility()->IsVisible()) {
+ ContentRemoved(aRoot);
+ // There might be visible descendants, so we want to walk the subtree.
+ // However, we know we don't want to reinsert this node, so we set insert
+ // to false.
+ insert = false;
+ }
+ } else {
+ // If there is no current accessible, and the node has a frame, or is
+ // display:contents, schedule it for insertion.
+ if (aRoot->GetPrimaryFrame() ||
+ nsCoreUtils::CanCreateAccessibleWithoutFrame(aRoot)) {
+ // This may be a new subtree, the insertion process will recurse through
+ // its descendants.
+ if (!GetAccessibleOrDescendant(aRoot)) {
+ return true;
+ }
+
+ // Content is not an accessible, but has accessible descendants.
+ // We schedule this container for insertion strictly for the case where it
+ // itself now needs an accessible. We will still need to recurse into the
+ // descendant content to prune accessibles, and in all likelyness to
+ // insert accessibles since accessible insertions will likeley get missed
+ // in an existing subtree.
+ insert = true;
+ }
+ }
+
+ if (LocalAccessible* container = AccessibleOrTrueContainer(aRoot)) {
+ AutoTArray<nsCOMPtr<nsIContent>, 10> list;
+ dom::AllChildrenIterator iter =
+ dom::AllChildrenIterator(aRoot, nsIContent::eAllChildren, true);
+ while (nsIContent* childNode = iter.GetNextChild()) {
+ if (PruneOrInsertSubtree(childNode)) {
+ list.AppendElement(childNode);
+ }
+ }
+
+ if (!list.IsEmpty()) {
+ mNotificationController->ScheduleContentInsertion(container, list);
+ }
+ }
+
+ return insert;
+}
+
+void DocAccessible::RecreateAccessible(nsIContent* aContent) {
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eTree)) {
+ logging::MsgBegin("TREE", "accessible recreated");
+ logging::Node("content", aContent);
+ logging::MsgEnd();
+ }
+#endif
+
+ // XXX: we shouldn't recreate whole accessible subtree, instead we should
+ // subclass hide and show events to handle them separately and implement their
+ // coalescence with normal hide and show events. Note, in this case they
+ // should be coalesced with normal show/hide events.
+ ContentRemoved(aContent);
+ ContentInserted(aContent, aContent->GetNextSibling());
+}
+
+void DocAccessible::ProcessInvalidationList() {
+ // Invalidate children of container accessible for each element in
+ // invalidation list. Allow invalidation list insertions while container
+ // children are recached.
+ for (uint32_t idx = 0; idx < mInvalidationList.Length(); idx++) {
+ nsIContent* content = mInvalidationList[idx];
+ if (!HasAccessible(content) && content->HasID()) {
+ LocalAccessible* container = GetContainerAccessible(content);
+ if (container) {
+ // Check if the node is a target of aria-owns, and if so, don't process
+ // it here and let DoARIAOwnsRelocation process it.
+ AttrRelProviders* list = GetRelProviders(
+ content->AsElement(), nsDependentAtomString(content->GetID()));
+ bool shouldProcess = !!list;
+ if (shouldProcess) {
+ for (uint32_t idx = 0; idx < list->Length(); idx++) {
+ if (list->ElementAt(idx)->mRelAttr == nsGkAtoms::aria_owns) {
+ shouldProcess = false;
+ break;
+ }
+ }
+
+ if (shouldProcess) {
+ ProcessContentInserted(container, content);
+ }
+ }
+ }
+ }
+ }
+
+ mInvalidationList.Clear();
+}
+
+void DocAccessible::ProcessQueuedCacheUpdates() {
+ if (!StaticPrefs::accessibility_cache_enabled_AtStartup()) {
+ return;
+ }
+
+ AUTO_PROFILER_MARKER_TEXT("DocAccessible::ProcessQueuedCacheUpdates", A11Y,
+ {}, ""_ns);
+ // DO NOT ADD CODE ABOVE THIS BLOCK: THIS CODE IS MEASURING TIMINGS.
+
+ nsTArray<CacheData> data;
+ for (auto iter = mQueuedCacheUpdates.Iter(); !iter.Done(); iter.Next()) {
+ LocalAccessible* acc = iter.Key();
+ uint64_t domain = iter.UserData();
+ if (acc && acc->IsInDocument() && !acc->IsDefunct()) {
+ RefPtr<AccAttributes> fields =
+ acc->BundleFieldsForCache(domain, CacheUpdateType::Update);
+
+ if (fields->Count()) {
+ data.AppendElement(CacheData(
+ acc->IsDoc() ? 0 : reinterpret_cast<uint64_t>(acc->UniqueID()),
+ fields));
+ }
+ }
+ }
+
+ mQueuedCacheUpdates.Clear();
+
+ if (mViewportCacheDirty) {
+ RefPtr<AccAttributes> fields =
+ BundleFieldsForCache(CacheDomain::Viewport, CacheUpdateType::Update);
+ if (fields->Count()) {
+ data.AppendElement(CacheData(0, fields));
+ }
+ mViewportCacheDirty = false;
+ }
+
+ if (data.Length()) {
+ IPCDoc()->SendCache(CacheUpdateType::Update, data, false);
+ }
+}
+
+void DocAccessible::SendAccessiblesWillMove() {
+ if (!mIPCDoc) {
+ return;
+ }
+ nsTArray<uint64_t> ids;
+ for (LocalAccessible* acc : mMovedAccessibles) {
+ // If acc is defunct or not in a document, it was removed after it was
+ // moved.
+ if (!acc->IsDefunct() && acc->IsInDocument()) {
+ ids.AppendElement(reinterpret_cast<uintptr_t>(acc->UniqueID()));
+ }
+ }
+ if (!ids.IsEmpty()) {
+ mIPCDoc->SendAccessiblesWillMove(ids);
+ }
+}
+
+LocalAccessible* DocAccessible::GetAccessibleEvenIfNotInMap(
+ nsINode* aNode) const {
+ if (!aNode->IsContent() ||
+ !aNode->AsContent()->IsHTMLElement(nsGkAtoms::area)) {
+ return GetAccessible(aNode);
+ }
+
+ // XXX Bug 135040, incorrect when multiple images use the same map.
+ nsIFrame* frame = aNode->AsContent()->GetPrimaryFrame();
+ nsImageFrame* imageFrame = do_QueryFrame(frame);
+ if (imageFrame) {
+ LocalAccessible* parent = GetAccessible(imageFrame->GetContent());
+ if (parent) {
+ LocalAccessible* area =
+ parent->AsImageMap()->GetChildAccessibleFor(aNode);
+ if (area) return area;
+
+ return nullptr;
+ }
+ }
+
+ return GetAccessible(aNode);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Protected members
+
+void DocAccessible::NotifyOfLoading(bool aIsReloading) {
+ // Mark the document accessible as loading, if it stays alive then we'll mark
+ // it as loaded when we receive proper notification.
+ mLoadState &= ~eDOMLoaded;
+
+ if (!IsLoadEventTarget()) return;
+
+ if (aIsReloading && !mLoadEventType &&
+ // We can't fire events on a document whose tree isn't constructed yet.
+ HasLoadState(eTreeConstructed)) {
+ // Fire reload and state busy events on existing document accessible while
+ // event from user input flag can be calculated properly and accessible
+ // is alive. When new document gets loaded then this one is destroyed.
+ RefPtr<AccEvent> reloadEvent =
+ new AccEvent(nsIAccessibleEvent::EVENT_DOCUMENT_RELOAD, this);
+ nsEventShell::FireEvent(reloadEvent);
+ }
+
+ // Fire state busy change event. Use delayed event since we don't care
+ // actually if event isn't delivered when the document goes away like a shot.
+ RefPtr<AccEvent> stateEvent =
+ new AccStateChangeEvent(this, states::BUSY, true);
+ FireDelayedEvent(stateEvent);
+}
+
+void DocAccessible::DoInitialUpdate() {
+ AUTO_PROFILER_MARKER_TEXT("DocAccessible::DoInitialUpdate", A11Y, {}, ""_ns);
+ // DO NOT ADD CODE ABOVE THIS BLOCK: THIS CODE IS MEASURING TIMINGS.
+
+ if (nsCoreUtils::IsTopLevelContentDocInProcess(mDocumentNode)) {
+ mDocFlags |= eTopLevelContentDocInProcess;
+ if (IPCAccessibilityActive()) {
+ nsIDocShell* docShell = mDocumentNode->GetDocShell();
+ if (RefPtr<dom::BrowserChild> browserChild =
+ dom::BrowserChild::GetFrom(docShell)) {
+ // In content processes, top level content documents are always
+ // RootAccessibles.
+ MOZ_ASSERT(IsRoot());
+ DocAccessibleChild* ipcDoc = IPCDoc();
+ if (ipcDoc) {
+ browserChild->SetTopLevelDocAccessibleChild(ipcDoc);
+ } else {
+ ipcDoc = new DocAccessibleChild(this, browserChild);
+ SetIPCDoc(ipcDoc);
+ // Subsequent initialization might depend on being able to get the
+ // top level DocAccessibleChild, so set that as early as possible.
+ browserChild->SetTopLevelDocAccessibleChild(ipcDoc);
+
+#if defined(XP_WIN)
+ IAccessibleHolder holder;
+ int32_t childID;
+ if (StaticPrefs::accessibility_cache_enabled_AtStartup()) {
+ childID = 0;
+ } else {
+ holder = CreateHolderFromAccessible(WrapNotNull(this));
+ MOZ_ASSERT(!holder.IsNull());
+ childID = MsaaAccessible::GetChildIDFor(this);
+ }
+#else
+ int32_t holder = 0, childID = 0;
+#endif
+ browserChild->SendPDocAccessibleConstructor(
+ ipcDoc, nullptr, 0, mDocumentNode->GetBrowsingContext(), childID,
+ holder);
+#if !defined(XP_WIN)
+ ipcDoc->SendPDocAccessiblePlatformExtConstructor();
+#endif
+ }
+#if !defined(XP_WIN)
+ // It's safe for us to mark top level documents as constructed in the
+ // parent process without receiving an explicit message, since we can
+ // never get queries for this document or descendants before parent
+ // process construction is complete.
+ ipcDoc->SetConstructedInParentProcess();
+#endif
+ }
+ }
+ }
+
+ mLoadState |= eTreeConstructed;
+
+ // Set up a root element and ARIA role mapping.
+ UpdateRootElIfNeeded();
+
+ // Build initial tree.
+ CacheChildrenInSubtree(this);
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eVerbose)) {
+ logging::Tree("TREE", "Initial subtree", this);
+ }
+ if (logging::IsEnabled(logging::eTreeSize)) {
+ logging::TreeSize("TREE SIZE", "Initial subtree", this);
+ }
+#endif
+
+ // Fire reorder event after the document tree is constructed. Note, since
+ // this reorder event is processed by parent document then events targeted to
+ // this document may be fired prior to this reorder event. If this is
+ // a problem then consider to keep event processing per tab document.
+ if (!IsRoot()) {
+ RefPtr<AccReorderEvent> reorderEvent = new AccReorderEvent(LocalParent());
+ ParentDocument()->FireDelayedEvent(reorderEvent);
+ }
+
+ if (ipc::ProcessChild::ExpectingShutdown()) {
+ return;
+ }
+ if (IPCAccessibilityActive()) {
+ DocAccessibleChild* ipcDoc = IPCDoc();
+ MOZ_ASSERT(ipcDoc);
+ if (ipcDoc) {
+ if (StaticPrefs::accessibility_cache_enabled_AtStartup()) {
+ // If we're caching, we should send an initial update for this document
+ // and its attributes. Each acc contained in this doc will have its
+ // initial update sent in `InsertIntoIpcTree`.
+ SendCache(CacheDomain::All, CacheUpdateType::Initial);
+ }
+
+ for (auto idx = 0U; idx < mChildren.Length(); idx++) {
+ ipcDoc->InsertIntoIpcTree(this, mChildren.ElementAt(idx), idx, true);
+ }
+ }
+ }
+}
+
+void DocAccessible::ProcessLoad() {
+ mLoadState |= eCompletelyLoaded;
+
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eDocLoad)) {
+ logging::DocCompleteLoad(this, IsLoadEventTarget());
+ }
+#endif
+
+ // Do not fire document complete/stop events for root chrome document
+ // accessibles and for frame/iframe documents because
+ // a) screen readers start working on focus event in the case of root chrome
+ // documents
+ // b) document load event on sub documents causes screen readers to act is if
+ // entire page is reloaded.
+ if (!IsLoadEventTarget()) return;
+
+ // Fire complete/load stopped if the load event type is given.
+ if (mLoadEventType) {
+ RefPtr<AccEvent> loadEvent = new AccEvent(mLoadEventType, this);
+ FireDelayedEvent(loadEvent);
+
+ mLoadEventType = 0;
+ }
+
+ // Fire busy state change event.
+ RefPtr<AccEvent> stateEvent =
+ new AccStateChangeEvent(this, states::BUSY, false);
+ FireDelayedEvent(stateEvent);
+}
+
+void DocAccessible::AddDependentIDsFor(LocalAccessible* aRelProvider,
+ nsAtom* aRelAttr) {
+ dom::Element* relProviderEl = aRelProvider->Elm();
+ if (!relProviderEl) return;
+
+ for (uint32_t idx = 0; idx < kRelationAttrsLen; idx++) {
+ nsStaticAtom* relAttr = kRelationAttrs[idx];
+ if (aRelAttr && aRelAttr != relAttr) continue;
+
+ if (relAttr == nsGkAtoms::_for) {
+ if (!relProviderEl->IsAnyOfHTMLElements(nsGkAtoms::label,
+ nsGkAtoms::output)) {
+ continue;
+ }
+
+ } else if (relAttr == nsGkAtoms::control) {
+ if (!relProviderEl->IsAnyOfXULElements(nsGkAtoms::label,
+ nsGkAtoms::description)) {
+ continue;
+ }
+ }
+
+ IDRefsIterator iter(this, relProviderEl, relAttr);
+ while (true) {
+ const nsDependentSubstring id = iter.NextID();
+ if (id.IsEmpty()) break;
+
+ AttrRelProviders* providers = GetOrCreateRelProviders(relProviderEl, id);
+ if (providers) {
+ AttrRelProvider* provider = new AttrRelProvider(relAttr, relProviderEl);
+ if (provider) {
+ providers->AppendElement(provider);
+
+ // We've got here during the children caching. If the referenced
+ // content is not accessible then store it to pend its container
+ // children invalidation (this happens immediately after the caching
+ // is finished).
+ nsIContent* dependentContent = iter.GetElem(id);
+ if (dependentContent) {
+ if (!HasAccessible(dependentContent)) {
+ mInvalidationList.AppendElement(dependentContent);
+ }
+ }
+ }
+ }
+ }
+
+ // If the relation attribute is given then we don't have anything else to
+ // check.
+ if (aRelAttr) break;
+ }
+
+ // Make sure to schedule the tree update if needed.
+ mNotificationController->ScheduleProcessing();
+}
+
+void DocAccessible::RemoveDependentIDsFor(LocalAccessible* aRelProvider,
+ nsAtom* aRelAttr) {
+ dom::Element* relProviderElm = aRelProvider->Elm();
+ if (!relProviderElm) return;
+
+ for (uint32_t idx = 0; idx < kRelationAttrsLen; idx++) {
+ nsStaticAtom* relAttr = kRelationAttrs[idx];
+ if (aRelAttr && aRelAttr != kRelationAttrs[idx]) continue;
+
+ IDRefsIterator iter(this, relProviderElm, relAttr);
+ while (true) {
+ const nsDependentSubstring id = iter.NextID();
+ if (id.IsEmpty()) break;
+
+ AttrRelProviders* providers = GetRelProviders(relProviderElm, id);
+ if (providers) {
+ providers->RemoveElementsBy(
+ [relAttr, relProviderElm](const auto& provider) {
+ return provider->mRelAttr == relAttr &&
+ provider->mContent == relProviderElm;
+ });
+
+ RemoveRelProvidersIfEmpty(relProviderElm, id);
+ }
+ }
+
+ // If the relation attribute is given then we don't have anything else to
+ // check.
+ if (aRelAttr) break;
+ }
+}
+
+bool DocAccessible::UpdateAccessibleOnAttrChange(dom::Element* aElement,
+ nsAtom* aAttribute) {
+ if (aAttribute == nsGkAtoms::role) {
+ // It is common for js libraries to set the role on the body element after
+ // the document has loaded. In this case we just update the role map entry.
+ if (mContent == aElement) {
+ SetRoleMapEntryForDoc(aElement);
+ if (mIPCDoc) {
+ mIPCDoc->SendRoleChangedEvent(Role(), mRoleMapEntryIndex);
+ }
+
+ return true;
+ }
+
+ // Recreate the accessible when role is changed because we might require a
+ // different accessible class for the new role or the accessible may expose
+ // a different sets of interfaces (COM restriction).
+ RecreateAccessible(aElement);
+
+ return true;
+ }
+
+ if (aAttribute == nsGkAtoms::multiple) {
+ if (dom::HTMLSelectElement* select =
+ dom::HTMLSelectElement::FromNode(aElement)) {
+ if (select->Size() <= 1) {
+ // Adding the 'multiple' attribute to a select that has a size of 1
+ // creates a listbox as opposed to a combobox with a popup combobox
+ // list. Removing the attribute does the opposite.
+ RecreateAccessible(aElement);
+ return true;
+ }
+ }
+ }
+
+ if (aAttribute == nsGkAtoms::size &&
+ aElement->IsHTMLElement(nsGkAtoms::select)) {
+ // Changing the size of a select element can potentially change it from a
+ // combobox button to a listbox with different underlying implementations.
+ RecreateAccessible(aElement);
+ return true;
+ }
+
+ if (aAttribute == nsGkAtoms::type) {
+ // If the input[type] changes, we should recreate the accessible.
+ RecreateAccessible(aElement);
+ return true;
+ }
+
+ if (aElement->IsHTMLElement(nsGkAtoms::img) && aAttribute == nsGkAtoms::alt) {
+ // If alt text changes on an img element, we may want to create or remove an
+ // accessible for that img.
+ if (nsAccessibilityService::ShouldCreateImgAccessible(aElement, this)) {
+ if (GetAccessible(aElement)) {
+ // If the accessible already exists, there's no need to create one.
+ return false;
+ }
+ ContentInserted(aElement, aElement->GetNextSibling());
+ } else {
+ ContentRemoved(aElement);
+ }
+ return true;
+ }
+
+ return false;
+}
+
+void DocAccessible::UpdateRootElIfNeeded() {
+ dom::Element* rootEl = mDocumentNode->GetBodyElement();
+ if (!rootEl) {
+ rootEl = mDocumentNode->GetRootElement();
+ }
+ if (rootEl != mContent) {
+ mContent = rootEl;
+ SetRoleMapEntryForDoc(rootEl);
+ if (mIPCDoc) {
+ mIPCDoc->SendRoleChangedEvent(Role(), mRoleMapEntryIndex);
+ }
+ }
+}
+
+/**
+ * Content insertion helper.
+ */
+class InsertIterator final {
+ public:
+ InsertIterator(LocalAccessible* aContext,
+ const nsTArray<nsCOMPtr<nsIContent>>* aNodes)
+ : mChild(nullptr),
+ mChildBefore(nullptr),
+ mWalker(aContext),
+ mNodes(aNodes),
+ mNodesIdx(0) {
+ MOZ_ASSERT(aContext, "No context");
+ MOZ_ASSERT(aNodes, "No nodes to search for accessible elements");
+ MOZ_COUNT_CTOR(InsertIterator);
+ }
+ MOZ_COUNTED_DTOR(InsertIterator)
+
+ LocalAccessible* Context() const { return mWalker.Context(); }
+ LocalAccessible* Child() const { return mChild; }
+ LocalAccessible* ChildBefore() const { return mChildBefore; }
+ DocAccessible* Document() const { return mWalker.Document(); }
+
+ /**
+ * Iterates to a next accessible within the inserted content.
+ */
+ bool Next();
+
+ void Rejected() {
+ mChild = nullptr;
+ mChildBefore = nullptr;
+ }
+
+ private:
+ LocalAccessible* mChild;
+ LocalAccessible* mChildBefore;
+ TreeWalker mWalker;
+
+ const nsTArray<nsCOMPtr<nsIContent>>* mNodes;
+ nsTHashSet<nsPtrHashKey<const nsIContent>> mProcessedNodes;
+ uint32_t mNodesIdx;
+};
+
+bool InsertIterator::Next() {
+ if (mNodesIdx > 0) {
+ // If we already processed the first node in the mNodes list,
+ // check if we can just use the walker to get its next sibling.
+ LocalAccessible* nextChild = mWalker.Next();
+ if (nextChild) {
+ mChildBefore = mChild;
+ mChild = nextChild;
+ return true;
+ }
+ }
+
+ while (mNodesIdx < mNodes->Length()) {
+ nsIContent* node = mNodes->ElementAt(mNodesIdx++);
+ // Check to see if we already processed this node with this iterator.
+ // this can happen if we get two redundant insertions in the case of a
+ // text and frame insertion.
+ if (!mProcessedNodes.EnsureInserted(node)) {
+ continue;
+ }
+
+ LocalAccessible* container = Document()->AccessibleOrTrueContainer(
+ node->GetFlattenedTreeParentNode(), true);
+ // Ignore nodes that are not contained by the container anymore.
+ // The container might be changed, for example, because of the subsequent
+ // overlapping content insertion (i.e. other content was inserted between
+ // this inserted content and its container or the content was reinserted
+ // into different container of unrelated part of tree). To avoid a double
+ // processing of the content insertion ignore this insertion notification.
+ // Note, the inserted content might be not in tree at all at this point
+ // what means there's no container. Ignore the insertion too.
+ if (container != Context()) {
+ continue;
+ }
+
+ // HTML comboboxes have no-content list accessible as an intermediate
+ // containing all options.
+ if (container->IsHTMLCombobox()) {
+ container = container->LocalFirstChild();
+ }
+
+ if (!container->IsAcceptableChild(node)) {
+ continue;
+ }
+
+#ifdef A11Y_LOG
+ logging::TreeInfo("traversing an inserted node", logging::eVerbose,
+ "container", container, "node", node);
+#endif
+
+ nsIContent* prevNode = mChild ? mChild->GetContent() : nullptr;
+ if (prevNode && prevNode->GetNextSibling() == node) {
+ // If inserted nodes are siblings then just move the walker next.
+ LocalAccessible* nextChild = mWalker.Scope(node);
+ if (nextChild) {
+ mChildBefore = mChild;
+ mChild = nextChild;
+ return true;
+ }
+ } else {
+ // Otherwise use a new walker to find this node in the container's
+ // subtree, and retrieve its preceding sibling.
+ TreeWalker finder(container);
+ if (finder.Seek(node)) {
+ mChild = mWalker.Scope(node);
+ if (mChild) {
+ MOZ_ASSERT(!mChild->IsRelocated(), "child cannot be aria owned");
+ mChildBefore = finder.Prev();
+ return true;
+ }
+ }
+ }
+ }
+
+ return false;
+}
+
+void DocAccessible::ProcessContentInserted(
+ LocalAccessible* aContainer, const nsTArray<nsCOMPtr<nsIContent>>* aNodes) {
+ // Process insertions if the container accessible is still in tree.
+ if (!aContainer->IsInDocument()) {
+ return;
+ }
+
+ // If new root content has been inserted then update it.
+ if (aContainer == this) {
+ UpdateRootElIfNeeded();
+ }
+
+ InsertIterator iter(aContainer, aNodes);
+ if (!iter.Next()) {
+ return;
+ }
+
+#ifdef A11Y_LOG
+ logging::TreeInfo("children before insertion", logging::eVerbose, aContainer);
+#endif
+
+ TreeMutation mt(aContainer);
+ do {
+ LocalAccessible* parent = iter.Child()->LocalParent();
+ if (parent) {
+ LocalAccessible* previousSibling = iter.ChildBefore();
+ if (parent != aContainer ||
+ iter.Child()->LocalPrevSibling() != previousSibling) {
+ if (previousSibling && previousSibling->LocalParent() != aContainer) {
+ // previousSibling hasn't been moved into aContainer yet.
+ // previousSibling should be later in the insertion list, so the tree
+ // will get adjusted when we process it later.
+ MOZ_DIAGNOSTIC_ASSERT(parent == aContainer,
+ "Child moving to new parent, but previous "
+ "sibling in wrong parent");
+ continue;
+ }
+#ifdef A11Y_LOG
+ logging::TreeInfo("relocating accessible", 0, "old parent", parent,
+ "new parent", aContainer, "child", iter.Child(),
+ nullptr);
+#endif
+ MoveChild(iter.Child(), aContainer,
+ previousSibling ? previousSibling->IndexInParent() + 1 : 0);
+ }
+ continue;
+ }
+
+ if (aContainer->InsertAfter(iter.Child(), iter.ChildBefore())) {
+#ifdef A11Y_LOG
+ logging::TreeInfo("accessible was inserted", 0, "container", aContainer,
+ "child", iter.Child(), nullptr);
+#endif
+
+ CreateSubtree(iter.Child());
+ mt.AfterInsertion(iter.Child());
+ continue;
+ }
+
+ MOZ_ASSERT_UNREACHABLE("accessible was rejected");
+ iter.Rejected();
+ } while (iter.Next());
+
+ mt.Done();
+
+#ifdef A11Y_LOG
+ logging::TreeInfo("children after insertion", logging::eVerbose, aContainer);
+#endif
+
+ FireEventsOnInsertion(aContainer);
+}
+
+void DocAccessible::ProcessContentInserted(LocalAccessible* aContainer,
+ nsIContent* aNode) {
+ if (!aContainer->IsInDocument()) {
+ return;
+ }
+
+#ifdef A11Y_LOG
+ logging::TreeInfo("children before insertion", logging::eVerbose, aContainer);
+#endif
+
+#ifdef A11Y_LOG
+ logging::TreeInfo("traversing an inserted node", logging::eVerbose,
+ "container", aContainer, "node", aNode);
+#endif
+
+ TreeWalker walker(aContainer);
+ if (aContainer->IsAcceptableChild(aNode) && walker.Seek(aNode)) {
+ LocalAccessible* child = GetAccessible(aNode);
+ if (!child) {
+ child = GetAccService()->CreateAccessible(aNode, aContainer);
+ }
+
+ if (child) {
+ TreeMutation mt(aContainer);
+ if (!aContainer->InsertAfter(child, walker.Prev())) {
+ return;
+ }
+ CreateSubtree(child);
+ mt.AfterInsertion(child);
+ mt.Done();
+
+ FireEventsOnInsertion(aContainer);
+ }
+ }
+
+#ifdef A11Y_LOG
+ logging::TreeInfo("children after insertion", logging::eVerbose, aContainer);
+#endif
+}
+
+void DocAccessible::FireEventsOnInsertion(LocalAccessible* aContainer) {
+ // Check to see if change occurred inside an alert, and fire an EVENT_ALERT
+ // if it did.
+ if (aContainer->IsAlert() || aContainer->IsInsideAlert()) {
+ LocalAccessible* ancestor = aContainer;
+ do {
+ if (ancestor->IsAlert()) {
+ FireDelayedEvent(nsIAccessibleEvent::EVENT_ALERT, ancestor);
+ break;
+ }
+ } while ((ancestor = ancestor->LocalParent()));
+ }
+}
+
+void DocAccessible::ContentRemoved(LocalAccessible* aChild) {
+ MOZ_DIAGNOSTIC_ASSERT(aChild != this, "Should never be called for the doc");
+ LocalAccessible* parent = aChild->LocalParent();
+ MOZ_DIAGNOSTIC_ASSERT(parent, "Unattached accessible from tree");
+
+#ifdef A11Y_LOG
+ logging::TreeInfo("process content removal", 0, "container", parent, "child",
+ aChild, nullptr);
+#endif
+
+ // XXX: event coalescence may kill us
+ RefPtr<LocalAccessible> kungFuDeathGripChild(aChild);
+
+ TreeMutation mt(parent);
+ mt.BeforeRemoval(aChild);
+
+ if (aChild->IsDefunct()) {
+ MOZ_ASSERT_UNREACHABLE("Event coalescence killed the accessible");
+ mt.Done();
+ return;
+ }
+
+ MOZ_DIAGNOSTIC_ASSERT(aChild->LocalParent(), "Alive but unparented #1");
+
+ if (aChild->IsRelocated()) {
+ nsTArray<RefPtr<LocalAccessible>>* owned = mARIAOwnsHash.Get(parent);
+ MOZ_ASSERT(owned, "IsRelocated flag is out of sync with mARIAOwnsHash");
+ owned->RemoveElement(aChild);
+ if (owned->Length() == 0) {
+ mARIAOwnsHash.Remove(parent);
+ }
+ }
+ MOZ_DIAGNOSTIC_ASSERT(aChild->LocalParent(), "Unparented #2");
+ UncacheChildrenInSubtree(aChild);
+ parent->RemoveChild(aChild);
+
+ mt.Done();
+}
+
+void DocAccessible::ContentRemoved(nsIContent* aContentNode) {
+ // If child node is not accessible then look for its accessible children.
+ LocalAccessible* acc = GetAccessible(aContentNode);
+ if (acc) {
+ ContentRemoved(acc);
+ }
+
+ dom::AllChildrenIterator iter =
+ dom::AllChildrenIterator(aContentNode, nsIContent::eAllChildren, true);
+ while (nsIContent* childNode = iter.GetNextChild()) {
+ ContentRemoved(childNode);
+ }
+
+ // If this node has a shadow root, remove its explicit children too.
+ // The host node may be removed after the shadow root was attached, and
+ // before we asynchronously prune the light DOM and construct the shadow DOM.
+ // If this is a case where the node does not have its own accessible, we will
+ // not recurse into its current children, so we need to use an
+ // ExplicitChildIterator in order to get its accessible children in the light
+ // DOM, since they are not accessible anymore via AllChildrenIterator.
+ if (aContentNode->GetShadowRoot()) {
+ for (nsIContent* childNode = aContentNode->GetFirstChild(); childNode;
+ childNode = childNode->GetNextSibling()) {
+ ContentRemoved(childNode);
+ }
+ }
+}
+
+bool DocAccessible::RelocateARIAOwnedIfNeeded(nsIContent* aElement) {
+ if (!aElement->HasID()) return false;
+
+ AttrRelProviders* list = GetRelProviders(
+ aElement->AsElement(), nsDependentAtomString(aElement->GetID()));
+ if (list) {
+ for (uint32_t idx = 0; idx < list->Length(); idx++) {
+ if (list->ElementAt(idx)->mRelAttr == nsGkAtoms::aria_owns) {
+ LocalAccessible* owner = GetAccessible(list->ElementAt(idx)->mContent);
+ if (owner) {
+ mNotificationController->ScheduleRelocation(owner);
+ return true;
+ }
+ }
+ }
+ }
+
+ return false;
+}
+
+void DocAccessible::DoARIAOwnsRelocation(LocalAccessible* aOwner) {
+ MOZ_ASSERT(aOwner, "aOwner must be a valid pointer");
+ MOZ_ASSERT(aOwner->Elm(), "aOwner->Elm() must be a valid pointer");
+
+#ifdef A11Y_LOG
+ logging::TreeInfo("aria owns relocation", logging::eVerbose, aOwner);
+#endif
+
+ nsTArray<RefPtr<LocalAccessible>>* owned =
+ mARIAOwnsHash.GetOrInsertNew(aOwner);
+
+ IDRefsIterator iter(this, aOwner->Elm(), nsGkAtoms::aria_owns);
+ uint32_t idx = 0;
+ while (nsIContent* childEl = iter.NextElem()) {
+ LocalAccessible* child = GetAccessible(childEl);
+ auto insertIdx = aOwner->ChildCount() - owned->Length() + idx;
+
+ // Make an attempt to create an accessible if it wasn't created yet.
+ if (!child) {
+ // An owned child cannot be an ancestor of the owner.
+ bool ok = true;
+ bool check = true;
+ for (LocalAccessible* parent = aOwner; parent && !parent->IsDoc();
+ parent = parent->LocalParent()) {
+ if (check) {
+ if (parent->Elm()->IsInclusiveDescendantOf(childEl)) {
+ ok = false;
+ break;
+ }
+ }
+ // We need to do the DOM descendant check again whenever the DOM
+ // lineage changes. If parent is relocated, that means the next
+ // ancestor will have a different DOM lineage.
+ check = parent->IsRelocated();
+ }
+ if (!ok) {
+ continue;
+ }
+
+ if (aOwner->IsAcceptableChild(childEl)) {
+ child = GetAccService()->CreateAccessible(childEl, aOwner);
+ if (child) {
+ TreeMutation imut(aOwner);
+ aOwner->InsertChildAt(insertIdx, child);
+ imut.AfterInsertion(child);
+ imut.Done();
+
+ child->SetRelocated(true);
+ owned->InsertElementAt(idx, child);
+ idx++;
+
+ // Create subtree before adjusting the insertion index, since subtree
+ // creation may alter children in the container.
+ CreateSubtree(child);
+ FireEventsOnInsertion(aOwner);
+ }
+ }
+ continue;
+ }
+
+#ifdef A11Y_LOG
+ logging::TreeInfo("aria owns traversal", logging::eVerbose, "candidate",
+ child, nullptr);
+#endif
+
+ if (owned->IndexOf(child) < idx) {
+ continue; // ignore second entry of same ID
+ }
+
+ // Same child on same position, no change.
+ if (child->LocalParent() == aOwner) {
+ int32_t indexInParent = child->IndexInParent();
+
+ // The child is being placed in its current index,
+ // eg. aria-owns='id1 id2 id3' is changed to aria-owns='id3 id2 id1'.
+ if (indexInParent == static_cast<int32_t>(insertIdx)) {
+ MOZ_ASSERT(child->IsRelocated(),
+ "A child, having an index in parent from aria ownded "
+ "indices range, has to be aria owned");
+ MOZ_ASSERT(owned->ElementAt(idx) == child,
+ "Unexpected child in ARIA owned array");
+ idx++;
+ continue;
+ }
+
+ // The child is being inserted directly after its current index,
+ // resulting in a no-move case. This will happen when a parent aria-owns
+ // its last ordinal child:
+ // <ul aria-owns='id2'><li id='id1'></li><li id='id2'></li></ul>
+ if (indexInParent == static_cast<int32_t>(insertIdx) - 1) {
+ MOZ_ASSERT(!child->IsRelocated(),
+ "Child should be in its ordinal position");
+ child->SetRelocated(true);
+ owned->InsertElementAt(idx, child);
+ idx++;
+ continue;
+ }
+ }
+
+ MOZ_ASSERT(owned->SafeElementAt(idx) != child, "Already in place!");
+
+ // A new child is found, check for loops.
+ if (child->LocalParent() != aOwner) {
+ // Child is aria-owned by another container, skip.
+ if (child->IsRelocated()) {
+ continue;
+ }
+
+ LocalAccessible* parent = aOwner;
+ while (parent && parent != child && !parent->IsDoc()) {
+ parent = parent->LocalParent();
+ }
+ // A referred child cannot be a parent of the owner.
+ if (parent == child) {
+ continue;
+ }
+ }
+
+ if (MoveChild(child, aOwner, insertIdx)) {
+ child->SetRelocated(true);
+ MOZ_ASSERT(owned == mARIAOwnsHash.Get(aOwner));
+ owned = mARIAOwnsHash.GetOrInsertNew(aOwner);
+ owned->InsertElementAt(idx, child);
+ idx++;
+ }
+ }
+
+ // Put back children that are not seized anymore.
+ PutChildrenBack(owned, idx);
+ if (owned->Length() == 0) {
+ mARIAOwnsHash.Remove(aOwner);
+ }
+}
+
+void DocAccessible::PutChildrenBack(
+ nsTArray<RefPtr<LocalAccessible>>* aChildren, uint32_t aStartIdx) {
+ MOZ_ASSERT(aStartIdx <= aChildren->Length(), "Wrong removal index");
+
+ for (auto idx = aStartIdx; idx < aChildren->Length(); idx++) {
+ LocalAccessible* child = aChildren->ElementAt(idx);
+ if (!child->IsInDocument()) {
+ continue;
+ }
+
+ // Remove the child from the owner
+ LocalAccessible* owner = child->LocalParent();
+ if (!owner) {
+ NS_ERROR("Cannot put the child back. No parent, a broken tree.");
+ continue;
+ }
+
+#ifdef A11Y_LOG
+ logging::TreeInfo("aria owns put child back", 0, "old parent", owner,
+ "child", child, nullptr);
+#endif
+
+ // Unset relocated flag to find an insertion point for the child.
+ child->SetRelocated(false);
+
+ nsIContent* content = child->GetContent();
+ int32_t idxInParent = -1;
+ LocalAccessible* origContainer =
+ AccessibleOrTrueContainer(content->GetFlattenedTreeParentNode());
+ if (origContainer) {
+ TreeWalker walker(origContainer);
+ if (walker.Seek(content)) {
+ LocalAccessible* prevChild = walker.Prev();
+ if (prevChild) {
+ idxInParent = prevChild->IndexInParent() + 1;
+ MOZ_DIAGNOSTIC_ASSERT(origContainer == prevChild->LocalParent(),
+ "Broken tree");
+ origContainer = prevChild->LocalParent();
+ } else {
+ idxInParent = 0;
+ }
+ }
+ }
+
+ // The child may have already be in its ordinal place for 2 reasons:
+ // 1. It was the last ordinal child, and the first aria-owned child.
+ // given: <ul id="list" aria-owns="b"><li id="a"></li><li
+ // id="b"></li></ul> after load: $("list").setAttribute("aria-owns", "");
+ // 2. The preceding adopted children were just reclaimed, eg:
+ // given: <ul id="list"><li id="b"></li></ul>
+ // after load: $("list").setAttribute("aria-owns", "a b");
+ // later: $("list").setAttribute("aria-owns", "");
+ if (origContainer != owner || child->IndexInParent() != idxInParent) {
+ DebugOnly<bool> moved = MoveChild(child, origContainer, idxInParent);
+ MOZ_ASSERT(moved, "Failed to put child back.");
+ } else {
+ MOZ_ASSERT(!child->LocalPrevSibling() ||
+ !child->LocalPrevSibling()->IsRelocated(),
+ "No relocated child should appear before this one");
+ MOZ_ASSERT(!child->LocalNextSibling() ||
+ child->LocalNextSibling()->IsRelocated(),
+ "No ordinal child should appear after this one");
+ }
+ }
+
+ aChildren->RemoveLastElements(aChildren->Length() - aStartIdx);
+}
+
+void DocAccessible::TrackMovedAccessible(LocalAccessible* aAcc) {
+ // If an Accessible is inserted and moved during the same tick, don't track
+ // it as a move because it hasn't been shown yet.
+ if (!mInsertedAccessibles.Contains(aAcc)) {
+ mMovedAccessibles.EnsureInserted(aAcc);
+ }
+ // When we move an Accessible, we're also moving its descendants.
+ for (uint32_t c = 0, count = aAcc->ContentChildCount(); c < count; ++c) {
+ TrackMovedAccessible(aAcc->ContentChildAt(c));
+ }
+}
+
+bool DocAccessible::MoveChild(LocalAccessible* aChild,
+ LocalAccessible* aNewParent,
+ int32_t aIdxInParent) {
+ MOZ_ASSERT(aChild, "No child");
+ MOZ_ASSERT(aChild->LocalParent(), "No parent");
+ // We can't guarantee MoveChild works correctly for accessibilities storing
+ // children outside mChildren.
+ MOZ_ASSERT(
+ aIdxInParent <= static_cast<int32_t>(aNewParent->mChildren.Length()),
+ "Wrong insertion point for a moving child");
+
+ LocalAccessible* curParent = aChild->LocalParent();
+
+ if (!aNewParent->IsAcceptableChild(aChild->GetContent())) {
+ return false;
+ }
+
+#ifdef A11Y_LOG
+ logging::TreeInfo("move child", 0, "old parent", curParent, "new parent",
+ aNewParent, "child", aChild, nullptr);
+#endif
+
+ // Forget aria-owns info in case of ARIA owned element. The caller is expected
+ // to update it if needed.
+ if (aChild->IsRelocated()) {
+ aChild->SetRelocated(false);
+ nsTArray<RefPtr<LocalAccessible>>* owned = mARIAOwnsHash.Get(curParent);
+ MOZ_ASSERT(owned, "IsRelocated flag is out of sync with mARIAOwnsHash");
+ owned->RemoveElement(aChild);
+ if (owned->Length() == 0) {
+ mARIAOwnsHash.Remove(curParent);
+ }
+ }
+
+ if (curParent == aNewParent) {
+ MOZ_ASSERT(aChild->IndexInParent() != aIdxInParent, "No move case");
+ curParent->RelocateChild(aIdxInParent, aChild);
+ if (mIPCDoc) {
+ TrackMovedAccessible(aChild);
+ }
+
+#ifdef A11Y_LOG
+ logging::TreeInfo("move child: parent tree after", logging::eVerbose,
+ curParent);
+#endif
+ return true;
+ }
+
+ // If the child cannot be re-inserted into the tree, then make sure to remove
+ // it from its present parent and then shutdown it.
+ bool hasInsertionPoint =
+ (aIdxInParent >= 0) &&
+ (aIdxInParent <= static_cast<int32_t>(aNewParent->mChildren.Length()));
+
+ TreeMutation rmut(curParent);
+ rmut.BeforeRemoval(aChild, hasInsertionPoint && TreeMutation::kNoShutdown);
+ curParent->RemoveChild(aChild);
+ rmut.Done();
+
+ // No insertion point for the child.
+ if (!hasInsertionPoint) {
+ return true;
+ }
+
+ TreeMutation imut(aNewParent);
+ aNewParent->InsertChildAt(aIdxInParent, aChild);
+ if (mIPCDoc) {
+ TrackMovedAccessible(aChild);
+ }
+ imut.AfterInsertion(aChild);
+ imut.Done();
+
+#ifdef A11Y_LOG
+ logging::TreeInfo("move child: old parent tree after", logging::eVerbose,
+ curParent);
+ logging::TreeInfo("move child: new parent tree after", logging::eVerbose,
+ aNewParent);
+#endif
+
+ return true;
+}
+
+void DocAccessible::CacheChildrenInSubtree(LocalAccessible* aRoot,
+ LocalAccessible** aFocusedAcc) {
+ // If the accessible is focused then report a focus event after all related
+ // mutation events.
+ if (aFocusedAcc && !*aFocusedAcc &&
+ FocusMgr()->HasDOMFocus(aRoot->GetContent())) {
+ *aFocusedAcc = aRoot;
+ }
+
+ LocalAccessible* root =
+ aRoot->IsHTMLCombobox() ? aRoot->LocalFirstChild() : aRoot;
+ if (root->KidsFromDOM()) {
+ TreeMutation mt(root, TreeMutation::kNoEvents);
+ TreeWalker walker(root);
+ while (LocalAccessible* child = walker.Next()) {
+ if (child->IsBoundToParent()) {
+ MoveChild(child, root, root->mChildren.Length());
+ continue;
+ }
+
+ root->AppendChild(child);
+ mt.AfterInsertion(child);
+
+ CacheChildrenInSubtree(child, aFocusedAcc);
+ }
+ mt.Done();
+ }
+
+ // Fire events for ARIA elements.
+ if (!aRoot->HasARIARole()) {
+ return;
+ }
+
+ // XXX: we should delay document load complete event if the ARIA document
+ // has aria-busy.
+ roles::Role role = aRoot->ARIARole();
+ if (!aRoot->IsDoc() &&
+ (role == roles::DIALOG || role == roles::NON_NATIVE_DOCUMENT)) {
+ FireDelayedEvent(nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE, aRoot);
+ }
+}
+
+void DocAccessible::UncacheChildrenInSubtree(LocalAccessible* aRoot) {
+ aRoot->mStateFlags |= eIsNotInDocument;
+ RemoveDependentIDsFor(aRoot);
+
+ // The parent of the removed subtree is about to be cleared, so we must do
+ // this here rather than in LocalAccessible::UnbindFromParent because we need
+ // the ancestry for this to work.
+ if (StaticPrefs::accessibility_cache_enabled_AtStartup() &&
+ (aRoot->IsTable() || aRoot->IsTableCell())) {
+ CachedTableAccessible::Invalidate(aRoot);
+ }
+
+ nsTArray<RefPtr<LocalAccessible>>* owned = mARIAOwnsHash.Get(aRoot);
+ uint32_t count = aRoot->ContentChildCount();
+ for (uint32_t idx = 0; idx < count; idx++) {
+ LocalAccessible* child = aRoot->ContentChildAt(idx);
+
+ if (child->IsRelocated()) {
+ MOZ_ASSERT(owned, "IsRelocated flag is out of sync with mARIAOwnsHash");
+ owned->RemoveElement(child);
+ if (owned->Length() == 0) {
+ mARIAOwnsHash.Remove(aRoot);
+ owned = nullptr;
+ }
+ }
+
+ // Removing this accessible from the document doesn't mean anything about
+ // accessibles for subdocuments, so skip removing those from the tree.
+ if (!child->IsDoc()) {
+ UncacheChildrenInSubtree(child);
+ }
+ }
+
+ if (aRoot->IsNodeMapEntry() &&
+ mNodeToAccessibleMap.Get(aRoot->GetNode()) == aRoot) {
+ mNodeToAccessibleMap.Remove(aRoot->GetNode());
+ }
+}
+
+void DocAccessible::ShutdownChildrenInSubtree(LocalAccessible* aAccessible) {
+ // Traverse through children and shutdown them before this accessible. When
+ // child gets shutdown then it removes itself from children array of its
+ // parent. Use jdx index to process the cases if child is not attached to the
+ // parent and as result doesn't remove itself from its children.
+ uint32_t count = aAccessible->ContentChildCount();
+ for (uint32_t idx = 0, jdx = 0; idx < count; idx++) {
+ LocalAccessible* child = aAccessible->ContentChildAt(jdx);
+ if (!child->IsBoundToParent()) {
+ NS_ERROR("Parent refers to a child, child doesn't refer to parent!");
+ jdx++;
+ }
+
+ // Don't cross document boundaries. The outerdoc shutdown takes care about
+ // its subdocument.
+ if (!child->IsDoc()) ShutdownChildrenInSubtree(child);
+ }
+
+ UnbindFromDocument(aAccessible);
+}
+
+bool DocAccessible::IsLoadEventTarget() const {
+ nsCOMPtr<nsIDocShellTreeItem> treeItem = mDocumentNode->GetDocShell();
+ if (!treeItem) {
+ return false;
+ }
+
+ nsCOMPtr<nsIDocShellTreeItem> parentTreeItem;
+ treeItem->GetInProcessParent(getter_AddRefs(parentTreeItem));
+
+ // Not a root document.
+ if (parentTreeItem) {
+ // Return true if it's either:
+ // a) tab document;
+ nsCOMPtr<nsIDocShellTreeItem> rootTreeItem;
+ treeItem->GetInProcessRootTreeItem(getter_AddRefs(rootTreeItem));
+ if (parentTreeItem == rootTreeItem) return true;
+
+ // b) frame/iframe document and its parent document is not in loading state
+ // Note: we can get notifications while document is loading (and thus
+ // while there's no parent document yet).
+ DocAccessible* parentDoc = ParentDocument();
+ return parentDoc && parentDoc->HasLoadState(eCompletelyLoaded);
+ }
+
+ // It's content (not chrome) root document.
+ return (treeItem->ItemType() == nsIDocShellTreeItem::typeContent);
+}
+
+void DocAccessible::SetIPCDoc(DocAccessibleChild* aIPCDoc) {
+ MOZ_ASSERT(!mIPCDoc || !aIPCDoc, "Clobbering an attached IPCDoc!");
+ mIPCDoc = aIPCDoc;
+}
+
+void DocAccessible::DispatchScrollingEvent(nsINode* aTarget,
+ uint32_t aEventType) {
+ LocalAccessible* acc = GetAccessible(aTarget);
+ if (!acc) {
+ return;
+ }
+
+ nsIFrame* frame = acc->GetFrame();
+ if (!frame) {
+ // Although the accessible had a frame at scroll time, it may now be gone
+ // because of display: contents.
+ return;
+ }
+
+ auto [scrollPoint, scrollRange] = ComputeScrollData(acc);
+
+ int32_t appUnitsPerDevPixel =
+ mPresShell->GetPresContext()->AppUnitsPerDevPixel();
+
+ LayoutDeviceIntPoint scrollPointDP = LayoutDevicePoint::FromAppUnitsToNearest(
+ scrollPoint, appUnitsPerDevPixel);
+ LayoutDeviceIntRect scrollRangeDP =
+ LayoutDeviceRect::FromAppUnitsToNearest(scrollRange, appUnitsPerDevPixel);
+
+ RefPtr<AccEvent> event =
+ new AccScrollingEvent(aEventType, acc, scrollPointDP.x, scrollPointDP.y,
+ scrollRangeDP.width, scrollRangeDP.height);
+ nsEventShell::FireEvent(event);
+}
+
+void DocAccessible::ARIAActiveDescendantIDMaybeMoved(
+ LocalAccessible* aAccessible) {
+ LocalAccessible* widget = nullptr;
+ if (aAccessible->IsActiveDescendant(&widget) && widget) {
+ // The active descendant might have just been inserted and may not be in the
+ // tree yet. Therefore, schedule this async to ensure the tree is up to
+ // date.
+ mNotificationController
+ ->ScheduleNotification<DocAccessible, LocalAccessible>(
+ this, &DocAccessible::ARIAActiveDescendantChanged, widget);
+ }
+}
+
+void DocAccessible::SetRoleMapEntryForDoc(dom::Element* aElement) {
+ const nsRoleMapEntry* entry = aria::GetRoleMap(aElement);
+ if (!entry || entry->role == roles::APPLICATION ||
+ entry->role == roles::DIALOG ||
+ // Role alert isn't valid on the body element according to the ARIA spec,
+ // but it's useful for our UI; e.g. the WebRTC sharing indicator.
+ (entry->role == roles::ALERT && !mDocumentNode->IsContentDocument())) {
+ SetRoleMapEntry(entry);
+ return;
+ }
+ // No other ARIA roles are valid on body elements.
+ SetRoleMapEntry(nullptr);
+}
+
+LocalAccessible* DocAccessible::GetAccessible(nsINode* aNode) const {
+ return aNode == mDocumentNode ? const_cast<DocAccessible*>(this)
+ : mNodeToAccessibleMap.Get(aNode);
+}
+
+bool DocAccessible::HasPrimaryAction() const {
+ if (HyperTextAccessible::HasPrimaryAction()) {
+ return true;
+ }
+ // mContent is normally the body, but there might be a click listener on the
+ // root.
+ dom::Element* root = mDocumentNode->GetRootElement();
+ if (mContent != root) {
+ return nsCoreUtils::HasClickListener(root);
+ }
+ return false;
+}
+
+void DocAccessible::ActionNameAt(uint8_t aIndex, nsAString& aName) {
+ aName.Truncate();
+ if (aIndex != 0) {
+ return;
+ }
+ if (HasPrimaryAction()) {
+ aName.AssignLiteral("click");
+ }
+}
+
+void DocAccessible::MaybeHandleChangeToHiddenNameOrDescription(
+ nsIContent* aChild) {
+ if (!HasLoadState(eTreeConstructed)) {
+ return;
+ }
+ for (nsIContent* content = aChild; content; content = content->GetParent()) {
+ if (HasAccessible(content)) {
+ // This node isn't hidden. Events for name/description dependents will be
+ // fired elsewhere.
+ break;
+ }
+ nsAtom* id = content->GetID();
+ if (!id) {
+ continue;
+ }
+ auto* providers =
+ GetRelProviders(content->AsElement(), nsDependentAtomString(id));
+ if (!providers) {
+ continue;
+ }
+ for (auto& provider : *providers) {
+ if (provider->mRelAttr != nsGkAtoms::aria_labelledby &&
+ provider->mRelAttr != nsGkAtoms::aria_describedby) {
+ continue;
+ }
+ LocalAccessible* dependentAcc = GetAccessible(provider->mContent);
+ if (!dependentAcc) {
+ continue;
+ }
+ FireDelayedEvent(provider->mRelAttr == nsGkAtoms::aria_labelledby
+ ? nsIAccessibleEvent::EVENT_NAME_CHANGE
+ : nsIAccessibleEvent::EVENT_DESCRIPTION_CHANGE,
+ dependentAcc);
+ }
+ }
+}
diff --git a/accessible/generic/DocAccessible.h b/accessible/generic/DocAccessible.h
new file mode 100644
index 0000000000..f0453ce766
--- /dev/null
+++ b/accessible/generic/DocAccessible.h
@@ -0,0 +1,827 @@
+/* -*- 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/. */
+
+#ifndef mozilla_a11y_DocAccessible_h__
+#define mozilla_a11y_DocAccessible_h__
+
+#include "nsIAccessiblePivot.h"
+
+#include "HyperTextAccessibleWrap.h"
+#include "AccEvent.h"
+
+#include "nsClassHashtable.h"
+#include "nsTHashMap.h"
+#include "mozilla/UniquePtr.h"
+#include "nsIDocumentObserver.h"
+#include "nsITimer.h"
+#include "nsTHashSet.h"
+#include "nsWeakReference.h"
+
+class nsAccessiblePivot;
+
+const uint32_t kDefaultCacheLength = 128;
+
+namespace mozilla {
+
+class EditorBase;
+class PresShell;
+
+namespace dom {
+class Document;
+}
+
+namespace a11y {
+
+class DocManager;
+class NotificationController;
+class DocAccessibleChild;
+class RelatedAccIterator;
+template <class Class, class... Args>
+class TNotification;
+
+/**
+ * An accessibility tree node that originated in a content process and
+ * represents a document. Tabs, in-process iframes, and out-of-process iframes
+ * all use this class to represent the doc they contain.
+ */
+class DocAccessible : public HyperTextAccessibleWrap,
+ public nsIDocumentObserver,
+ public nsSupportsWeakReference,
+ public nsIAccessiblePivotObserver {
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(DocAccessible, LocalAccessible)
+
+ NS_DECL_NSIACCESSIBLEPIVOTOBSERVER
+
+ protected:
+ typedef mozilla::dom::Document Document;
+
+ public:
+ DocAccessible(Document* aDocument, PresShell* aPresShell);
+
+ // nsIDocumentObserver
+ NS_DECL_NSIDOCUMENTOBSERVER
+
+ // LocalAccessible
+ virtual void Init();
+ virtual void Shutdown() override;
+ virtual nsIFrame* GetFrame() const override;
+ virtual nsINode* GetNode() const override;
+ Document* DocumentNode() const { return mDocumentNode; }
+
+ virtual mozilla::a11y::ENameValueFlag Name(nsString& aName) const override;
+ virtual void Description(nsString& aDescription) const override;
+ virtual Accessible* FocusedChild() override;
+ virtual mozilla::a11y::role NativeRole() const override;
+ virtual uint64_t NativeState() const override;
+ virtual uint64_t NativeInteractiveState() const override;
+ virtual bool NativelyUnavailable() const override;
+ virtual void ApplyARIAState(uint64_t* aState) const override;
+
+ virtual void TakeFocus() const override;
+
+#ifdef A11Y_LOG
+ virtual nsresult HandleAccEvent(AccEvent* aEvent) override;
+#endif
+
+ virtual nsRect RelativeBounds(nsIFrame** aRelativeFrame) const override;
+
+ // ActionAccessible
+ virtual bool HasPrimaryAction() const override;
+ virtual void ActionNameAt(uint8_t aIndex, nsAString& aName) override;
+
+ // HyperTextAccessible
+ virtual already_AddRefed<EditorBase> GetEditor() const override;
+
+ // DocAccessible
+
+ /**
+ * Return document URL.
+ */
+ void URL(nsAString& aURL) const;
+
+ /**
+ * Return DOM document title.
+ */
+ void Title(nsString& aTitle) const;
+
+ /**
+ * Return DOM document mime type.
+ */
+ void MimeType(nsAString& aType) const;
+ /**
+ * Return DOM document type.
+ */
+ void DocType(nsAString& aType) const;
+
+ /**
+ * Adds an entry to mQueuedCacheUpdates indicating aAcc requires
+ * a cache update on domain aNewDomain. If we've already queued an update
+ * for aAcc, aNewDomain is or'd with the existing domain(s)
+ * and the map is updated. Otherwise, the entry is simply inserted.
+ * This function also schedules processing on the controller.
+ * Note that this CANNOT be used for anything which fires events, since events
+ * must be fired after their associated cache update.
+ */
+ void QueueCacheUpdate(LocalAccessible* aAcc, uint64_t aNewDomain);
+
+ /**
+ * Walks the mDependentIDsHashes list for the given accessible and
+ * queues a CacheDomain::Relations cache update fore each related acc.
+ * We call this when we observe an ID mutation or when an acc is bound
+ * to its document.
+ */
+ void QueueCacheUpdateForDependentRelations(LocalAccessible* aAcc);
+
+ /**
+ * Return virtual cursor associated with the document.
+ */
+ nsIAccessiblePivot* VirtualCursor();
+
+ /**
+ * Returns true if the instance has shutdown.
+ */
+ bool HasShutdown() const { return !mPresShell; }
+
+ /**
+ * Return presentation shell for this document accessible.
+ */
+ PresShell* PresShellPtr() const {
+ MOZ_DIAGNOSTIC_ASSERT(!HasShutdown());
+ return mPresShell;
+ }
+
+ /**
+ * Return the presentation shell's context.
+ */
+ nsPresContext* PresContext() const;
+
+ /**
+ * Return true if associated DOM document was loaded and isn't unloading.
+ */
+ bool IsContentLoaded() const;
+
+ bool IsHidden() const;
+
+ void SetViewportCacheDirty(bool aDirty) { mViewportCacheDirty = aDirty; }
+
+ /**
+ * Document load states.
+ */
+ enum LoadState {
+ // initial tree construction is pending
+ eTreeConstructionPending = 0,
+ // initial tree construction done
+ eTreeConstructed = 1,
+ // DOM document is loaded.
+ eDOMLoaded = 1 << 1,
+ // document is ready
+ eReady = eTreeConstructed | eDOMLoaded,
+ // document and all its subdocuments are ready
+ eCompletelyLoaded = eReady | 1 << 2
+ };
+
+ /**
+ * Return true if the document has given document state.
+ */
+ bool HasLoadState(LoadState aState) const {
+ return (mLoadState & static_cast<uint32_t>(aState)) ==
+ static_cast<uint32_t>(aState);
+ }
+
+ /**
+ * Return a native window handler or pointer depending on platform.
+ */
+ virtual void* GetNativeWindow() const;
+
+ /**
+ * Return the parent document.
+ */
+ DocAccessible* ParentDocument() const {
+ return mParent ? mParent->Document() : nullptr;
+ }
+
+ /**
+ * Return the child document count.
+ */
+ uint32_t ChildDocumentCount() const { return mChildDocuments.Length(); }
+
+ /**
+ * Return the child document at the given index.
+ */
+ DocAccessible* GetChildDocumentAt(uint32_t aIndex) const {
+ return mChildDocuments.SafeElementAt(aIndex, nullptr);
+ }
+
+ /**
+ * Fire accessible event asynchronously.
+ */
+ void FireDelayedEvent(AccEvent* aEvent);
+ void FireDelayedEvent(uint32_t aEventType, LocalAccessible* aTarget);
+ void FireEventsOnInsertion(LocalAccessible* aContainer);
+
+ /**
+ * Fire value change event on the given accessible if applicable.
+ */
+ void MaybeNotifyOfValueChange(LocalAccessible* aAccessible);
+
+ /**
+ * Get/set the anchor jump.
+ */
+ LocalAccessible* AnchorJump() {
+ return GetAccessibleOrContainer(mAnchorJumpElm);
+ }
+
+ void SetAnchorJump(nsIContent* aTargetNode) { mAnchorJumpElm = aTargetNode; }
+
+ /**
+ * Bind the child document to the tree.
+ */
+ void BindChildDocument(DocAccessible* aDocument);
+
+ /**
+ * Process the generic notification.
+ *
+ * @note The caller must guarantee that the given instance still exists when
+ * notification is processed.
+ * @see NotificationController::HandleNotification
+ */
+ template <class Class, class... Args>
+ void HandleNotification(
+ Class* aInstance,
+ typename TNotification<Class, Args...>::Callback aMethod, Args*... aArgs);
+
+ /**
+ * Return the cached accessible by the given DOM node if it's in subtree of
+ * this document accessible or the document accessible itself, otherwise null.
+ *
+ * @return the accessible object
+ */
+ LocalAccessible* GetAccessible(nsINode* aNode) const;
+
+ /**
+ * Return an accessible for the given node even if the node is not in
+ * document's node map cache (like HTML area element).
+ *
+ * XXX: it should be really merged with GetAccessible().
+ */
+ LocalAccessible* GetAccessibleEvenIfNotInMap(nsINode* aNode) const;
+ LocalAccessible* GetAccessibleEvenIfNotInMapOrContainer(nsINode* aNode) const;
+
+ /**
+ * Return whether the given DOM node has an accessible or not.
+ */
+ bool HasAccessible(nsINode* aNode) const { return GetAccessible(aNode); }
+
+ /**
+ * Return the cached accessible by the given unique ID within this document.
+ *
+ * @note the unique ID matches with the uniqueID() of Accessible
+ *
+ * @param aUniqueID [in] the unique ID used to cache the node.
+ */
+ LocalAccessible* GetAccessibleByUniqueID(void* aUniqueID) {
+ return UniqueID() == aUniqueID ? this : mAccessibleCache.GetWeak(aUniqueID);
+ }
+
+ /**
+ * Return the cached accessible by the given unique ID looking through
+ * this and nested documents.
+ */
+ LocalAccessible* GetAccessibleByUniqueIDInSubtree(void* aUniqueID);
+
+ /**
+ * Return an accessible for the given DOM node or container accessible if
+ * the node is not accessible. If aNoContainerIfPruned is true it will return
+ * null if the node is in a pruned subtree (eg. aria-hidden or unselected deck
+ * panel)
+ */
+ LocalAccessible* GetAccessibleOrContainer(
+ nsINode* aNode, bool aNoContainerIfPruned = false) const;
+
+ /**
+ * Return a container accessible for the given DOM node.
+ */
+ LocalAccessible* GetContainerAccessible(nsINode* aNode) const;
+
+ /**
+ * Return an accessible for the given node if any, or an immediate accessible
+ * container for it.
+ */
+ LocalAccessible* AccessibleOrTrueContainer(
+ nsINode* aNode, bool aNoContainerIfPruned = false) const;
+
+ /**
+ * Return an accessible for the given node or its first accessible descendant.
+ */
+ LocalAccessible* GetAccessibleOrDescendant(nsINode* aNode) const;
+
+ /**
+ * Returns aria-owns seized child at the given index.
+ */
+ LocalAccessible* ARIAOwnedAt(LocalAccessible* aParent,
+ uint32_t aIndex) const {
+ nsTArray<RefPtr<LocalAccessible>>* children = mARIAOwnsHash.Get(aParent);
+ if (children) {
+ return children->SafeElementAt(aIndex);
+ }
+ return nullptr;
+ }
+ uint32_t ARIAOwnedCount(LocalAccessible* aParent) const {
+ nsTArray<RefPtr<LocalAccessible>>* children = mARIAOwnsHash.Get(aParent);
+ return children ? children->Length() : 0;
+ }
+
+ /**
+ * Return true if the given ID is referred by relation attribute.
+ */
+ bool IsDependentID(dom::Element* aElement, const nsAString& aID) const {
+ return GetRelProviders(aElement, aID);
+ }
+
+ /**
+ * Initialize the newly created accessible and put it into document caches.
+ *
+ * @param aAccessible [in] created accessible
+ * @param aRoleMapEntry [in] the role map entry role the ARIA role or
+ * nullptr if none
+ */
+ void BindToDocument(LocalAccessible* aAccessible,
+ const nsRoleMapEntry* aRoleMapEntry);
+
+ /**
+ * Remove from document and shutdown the given accessible.
+ */
+ void UnbindFromDocument(LocalAccessible* aAccessible);
+
+ /**
+ * Notify the document accessible that content was inserted.
+ */
+ void ContentInserted(nsIContent* aStartChildNode, nsIContent* aEndChildNode);
+
+ /**
+ * @see nsAccessibilityService::ScheduleAccessibilitySubtreeUpdate
+ */
+ void ScheduleTreeUpdate(nsIContent* aContent);
+
+ /**
+ * Update the tree on content removal.
+ */
+ void ContentRemoved(LocalAccessible* aAccessible);
+ void ContentRemoved(nsIContent* aContentNode);
+
+ /**
+ * Updates accessible tree when rendered text is changed.
+ */
+ void UpdateText(nsIContent* aTextNode);
+
+ /**
+ * Recreate an accessible, results in hide/show events pair.
+ */
+ void RecreateAccessible(nsIContent* aContent);
+
+ /**
+ * Schedule ARIA owned element relocation if needed. Return true if relocation
+ * was scheduled.
+ */
+ bool RelocateARIAOwnedIfNeeded(nsIContent* aEl);
+
+ /**
+ * Return a notification controller associated with the document.
+ */
+ NotificationController* Controller() const { return mNotificationController; }
+
+ /**
+ * If this document is in a content process return the object responsible for
+ * communicating with the main process for it.
+ */
+ DocAccessibleChild* IPCDoc() const { return mIPCDoc; }
+
+ /**
+ * Notify the document that a DOM node has been scrolled. document will
+ * dispatch throttled accessibility events for scrolling, and a scroll-end
+ * event. This function also queues a cache update for ScrollPosition.
+ */
+ void HandleScroll(nsINode* aTarget);
+
+ /**
+ * Retrieves the scroll frame (if it exists) for the given accessible
+ * and returns its scroll position and scroll range. If the given
+ * accessible is `this`, return the scroll position and range of
+ * the root scroll frame. Return values have been scaled by the
+ * PresShell's resolution.
+ */
+ std::pair<nsPoint, nsRect> ComputeScrollData(LocalAccessible* aAcc);
+
+ protected:
+ virtual ~DocAccessible();
+
+ void LastRelease();
+
+ // DocAccessible
+ virtual nsresult AddEventListeners();
+ virtual nsresult RemoveEventListeners();
+
+ /**
+ * Marks this document as loaded or loading.
+ */
+ void NotifyOfLoad(uint32_t aLoadEventType);
+ void NotifyOfLoading(bool aIsReloading);
+
+ friend class DocManager;
+
+ /**
+ * Perform initial update (create accessible tree).
+ * Can be overridden by wrappers to prepare initialization work.
+ */
+ virtual void DoInitialUpdate();
+
+ /**
+ * Updates root element and picks up ARIA role on it if any.
+ */
+ void UpdateRootElIfNeeded();
+
+ /**
+ * Process document load notification, fire document load and state busy
+ * events if applicable.
+ */
+ void ProcessLoad();
+
+ /**
+ * Append the given document accessible to this document's child document
+ * accessibles.
+ */
+ bool AppendChildDocument(DocAccessible* aChildDocument) {
+ // XXX(Bug 1631371) Check if this should use a fallible operation as it
+ // pretended earlier, or change the return type to void.
+ mChildDocuments.AppendElement(aChildDocument);
+ return true;
+ }
+
+ /**
+ * Remove the given document accessible from this document's child document
+ * accessibles.
+ */
+ void RemoveChildDocument(DocAccessible* aChildDocument) {
+ mChildDocuments.RemoveElement(aChildDocument);
+ }
+
+ /**
+ * Add dependent IDs pointed by accessible element by relation attribute to
+ * cache. If the relation attribute is missed then all relation attributes
+ * are checked.
+ *
+ * @param aRelProvider [in] accessible that element has relation attribute
+ * @param aRelAttr [in, optional] relation attribute
+ */
+ void AddDependentIDsFor(LocalAccessible* aRelProvider,
+ nsAtom* aRelAttr = nullptr);
+
+ /**
+ * Remove dependent IDs pointed by accessible element by relation attribute
+ * from cache. If the relation attribute is absent then all relation
+ * attributes are checked.
+ *
+ * @param aRelProvider [in] accessible that element has relation attribute
+ * @param aRelAttr [in, optional] relation attribute
+ */
+ void RemoveDependentIDsFor(LocalAccessible* aRelProvider,
+ nsAtom* aRelAttr = nullptr);
+
+ /**
+ * Update or recreate an accessible depending on a changed attribute.
+ *
+ * @param aElement [in] the element the attribute was changed on
+ * @param aAttribute [in] the changed attribute
+ * @return true if an action was taken on the attribute change
+ */
+ bool UpdateAccessibleOnAttrChange(mozilla::dom::Element* aElement,
+ nsAtom* aAttribute);
+
+ /**
+ * Process ARIA active-descendant attribute change.
+ */
+ void ARIAActiveDescendantChanged(LocalAccessible* aAccessible);
+
+ /**
+ * Update the accessible tree for inserted content.
+ */
+ void ProcessContentInserted(
+ LocalAccessible* aContainer,
+ const nsTArray<nsCOMPtr<nsIContent>>* aInsertedContent);
+ void ProcessContentInserted(LocalAccessible* aContainer,
+ nsIContent* aInsertedContent);
+
+ /**
+ * Used to notify the document to make it process the invalidation list.
+ *
+ * While children are cached we may encounter the case there's no accessible
+ * for referred content by related accessible. Store these related nodes to
+ * invalidate their containers later.
+ */
+ void ProcessInvalidationList();
+
+ /**
+ * Process mPendingUpdates
+ */
+ void ProcessPendingUpdates();
+
+ /**
+ * Called from NotificationController to process this doc's
+ * mQueuedCacheUpdates list. For each acc in the map, this function
+ * sends a cache update with its corresponding CacheDomain.
+ */
+ void ProcessQueuedCacheUpdates();
+
+ /**
+ * Only works in content process documents.
+ */
+ bool IsAccessibleBeingMoved(LocalAccessible* aAcc) {
+ return mMovedAccessibles.Contains(aAcc);
+ }
+
+ /**
+ * Called from NotificationController before mutation events are processed to
+ * notify the parent process which Accessibles are being moved (if any).
+ */
+ void SendAccessiblesWillMove();
+
+ /**
+ * Called from NotificationController after all mutation events have been
+ * processed to clear our data about Accessibles that were moved during this
+ * tick.
+ */
+ void ClearMovedAccessibles() {
+ mMovedAccessibles.Clear();
+ mInsertedAccessibles.Clear();
+ }
+
+ /**
+ * Steals or puts back accessible subtrees.
+ */
+ void DoARIAOwnsRelocation(LocalAccessible* aOwner);
+
+ /**
+ * Moves children back under their original parents.
+ */
+ void PutChildrenBack(nsTArray<RefPtr<LocalAccessible>>* aChildren,
+ uint32_t aStartIdx);
+
+ bool MoveChild(LocalAccessible* aChild, LocalAccessible* aNewParent,
+ int32_t aIdxInParent);
+
+ /**
+ * Create accessible tree.
+ *
+ * @param aRoot [in] a root of subtree to create
+ * @param aFocusedAcc [in, optional] a focused accessible under created
+ * subtree if any
+ */
+ void CacheChildrenInSubtree(LocalAccessible* aRoot,
+ LocalAccessible** aFocusedAcc = nullptr);
+ void CreateSubtree(LocalAccessible* aRoot);
+
+ /**
+ * Remove accessibles in subtree from node to accessible map.
+ */
+ void UncacheChildrenInSubtree(LocalAccessible* aRoot);
+
+ /**
+ * Shutdown any cached accessible in the subtree.
+ *
+ * @param aAccessible [in] the root of the subrtee to invalidate accessible
+ * child/parent refs in
+ */
+ void ShutdownChildrenInSubtree(LocalAccessible* aAccessible);
+
+ /**
+ * Return true if the document is a target of document loading events
+ * (for example, state busy change or document reload events).
+ *
+ * Rules: The root chrome document accessible is never an event target
+ * (for example, Firefox UI window). If the sub document is loaded within its
+ * parent document then the parent document is a target only (aka events
+ * coalescence).
+ */
+ bool IsLoadEventTarget() const;
+
+ /*
+ * Set the object responsible for communicating with the main process on
+ * behalf of this document.
+ */
+ void SetIPCDoc(DocAccessibleChild* aIPCDoc);
+
+ friend class DocAccessibleChildBase;
+
+ /**
+ * Used to fire scrolling end event after page scroll.
+ *
+ * @param aTimer [in] the timer object
+ * @param aClosure [in] the document accessible where scrolling happens
+ */
+ static void ScrollTimerCallback(nsITimer* aTimer, void* aClosure);
+
+ void DispatchScrollingEvent(nsINode* aTarget, uint32_t aEventType);
+
+ /**
+ * Check if an id attribute change affects aria-activedescendant and handle
+ * the aria-activedescendant change if appropriate.
+ * If the currently focused element has aria-activedescendant and an
+ * element's id changes to match this, the id was probably moved from the
+ * previous active descendant, thus making this element the new active
+ * descendant. In that case, accessible focus must be changed accordingly.
+ */
+ void ARIAActiveDescendantIDMaybeMoved(LocalAccessible* aAccessible);
+
+ /**
+ * Traverse content subtree and for each node do one of 3 things:
+ * 1. Check if content node has an accessible that should be removed and
+ * remove it.
+ * 2. Check if content node has an accessible that needs to be recreated.
+ * Remove it and schedule it for reinsertion.
+ * 3. Check if content node has no accessible but needs one. Schedule one for
+ * insertion.
+ *
+ * Returns true if the root node should be reinserted.
+ */
+ bool PruneOrInsertSubtree(nsIContent* aRoot);
+
+ protected:
+ /**
+ * State and property flags, kept by mDocFlags.
+ */
+ enum {
+ // Whether the document is a top level content document in this process.
+ eTopLevelContentDocInProcess = 1 << 0
+ };
+
+ /**
+ * Cache of accessibles within this document accessible.
+ */
+ AccessibleHashtable mAccessibleCache;
+ nsTHashMap<nsPtrHashKey<const nsINode>, LocalAccessible*>
+ mNodeToAccessibleMap;
+
+ Document* mDocumentNode;
+ nsCOMPtr<nsITimer> mScrollWatchTimer;
+ nsTHashMap<nsPtrHashKey<nsINode>, TimeStamp> mLastScrollingDispatch;
+
+ /**
+ * Bit mask of document load states (@see LoadState).
+ */
+ uint32_t mLoadState : 3;
+
+ /**
+ * Bit mask of other states and props.
+ */
+ uint32_t mDocFlags : 27;
+
+ /**
+ * Tracks whether we have seen changes to this document's content that
+ * indicate we should re-send the viewport cache we use for hittesting.
+ * This value is set in `BundleFieldsForCache` and processed in
+ * `ProcessQueuedCacheUpdates`.
+ */
+ bool mViewportCacheDirty : 1;
+
+ /**
+ * Type of document load event fired after the document is loaded completely.
+ */
+ uint32_t mLoadEventType;
+
+ /**
+ * Reference to anchor jump element.
+ */
+ nsCOMPtr<nsIContent> mAnchorJumpElm;
+
+ /**
+ * A generic state (see items below) before the attribute value was changed.
+ * @see AttributeWillChange and AttributeChanged notifications.
+ */
+
+ // Previous state bits before attribute change
+ uint64_t mPrevStateBits;
+
+ nsTArray<RefPtr<DocAccessible>> mChildDocuments;
+
+ /**
+ * The virtual cursor of the document.
+ */
+ RefPtr<nsAccessiblePivot> mVirtualCursor;
+
+ /**
+ * A storage class for pairing content with one of its relation attributes.
+ */
+ class AttrRelProvider {
+ public:
+ AttrRelProvider(nsAtom* aRelAttr, nsIContent* aContent)
+ : mRelAttr(aRelAttr), mContent(aContent) {}
+
+ nsAtom* mRelAttr;
+ nsCOMPtr<nsIContent> mContent;
+
+ private:
+ AttrRelProvider();
+ AttrRelProvider(const AttrRelProvider&);
+ AttrRelProvider& operator=(const AttrRelProvider&);
+ };
+
+ typedef nsTArray<mozilla::UniquePtr<AttrRelProvider>> AttrRelProviders;
+ typedef nsClassHashtable<nsStringHashKey, AttrRelProviders>
+ DependentIDsHashtable;
+
+ /**
+ * Returns/creates/removes attribute relation providers associated with
+ * a DOM document if the element is in uncomposed document or associated
+ * with shadow DOM the element is in.
+ */
+ AttrRelProviders* GetRelProviders(dom::Element* aElement,
+ const nsAString& aID) const;
+ AttrRelProviders* GetOrCreateRelProviders(dom::Element* aElement,
+ const nsAString& aID);
+ void RemoveRelProvidersIfEmpty(dom::Element* aElement, const nsAString& aID);
+
+ /**
+ * The cache of IDs pointed by relation attributes.
+ */
+ nsClassHashtable<nsPtrHashKey<dom::DocumentOrShadowRoot>,
+ DependentIDsHashtable>
+ mDependentIDsHashes;
+
+ friend class RelatedAccIterator;
+
+ /**
+ * Used for our caching algorithm. We store the list of nodes that should be
+ * invalidated.
+ *
+ * @see ProcessInvalidationList
+ */
+ nsTArray<RefPtr<nsIContent>> mInvalidationList;
+
+ /**
+ * Holds a list of aria-owns relocations.
+ */
+ nsClassHashtable<nsPtrHashKey<LocalAccessible>,
+ nsTArray<RefPtr<LocalAccessible>>>
+ mARIAOwnsHash;
+
+ /**
+ * Keeps a list of pending subtrees to update post-refresh.
+ */
+ nsTArray<RefPtr<nsIContent>> mPendingUpdates;
+
+ /**
+ * Used to process notification from core and accessible events.
+ */
+ RefPtr<NotificationController> mNotificationController;
+ friend class EventTree;
+ friend class NotificationController;
+
+ private:
+ void SetRoleMapEntryForDoc(dom::Element* aElement);
+
+ /**
+ * This must be called whenever an Accessible is moved in a content process.
+ * It keeps track of Accessibles moved during this tick.
+ */
+ void TrackMovedAccessible(LocalAccessible* aAcc);
+
+ /**
+ * For hidden subtrees, fire a name/description change event if the subtree
+ * is a target of aria-labelledby/describedby.
+ * This does nothing if it is called on a node which is not part of a hidden
+ * aria-labelledby/describedby target.
+ */
+ void MaybeHandleChangeToHiddenNameOrDescription(nsIContent* aChild);
+
+ PresShell* mPresShell;
+
+ // Exclusively owned by IPDL so don't manually delete it!
+ DocAccessibleChild* mIPCDoc;
+
+ // A hash map between LocalAccessibles and CacheDomains, tracking
+ // cache updates that have been queued during the current tick
+ // but not yet sent. It is possible for this map to contain a reference
+ // to the document it lives on. We clear the list in Shutdown() to
+ // avoid cyclical references.
+ nsTHashMap<RefPtr<LocalAccessible>, uint64_t> mQueuedCacheUpdates;
+
+ // A set of Accessibles moved during this tick. Only used in content
+ // processes.
+ nsTHashSet<RefPtr<LocalAccessible>> mMovedAccessibles;
+ // A set of Accessibles inserted during this tick. Only used in content
+ // processes. This is needed to prevent insertions + moves of the same
+ // Accessible in the same tick from being tracked as moves.
+ nsTHashSet<RefPtr<LocalAccessible>> mInsertedAccessibles;
+};
+
+inline DocAccessible* LocalAccessible::AsDoc() {
+ return IsDoc() ? static_cast<DocAccessible*>(this) : nullptr;
+}
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/generic/FormControlAccessible.cpp b/accessible/generic/FormControlAccessible.cpp
new file mode 100644
index 0000000000..91cc9fab6d
--- /dev/null
+++ b/accessible/generic/FormControlAccessible.cpp
@@ -0,0 +1,84 @@
+/* -*- 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/. */
+
+// NOTE: alphabetically ordered
+
+#include "FormControlAccessible.h"
+
+#include "mozilla/dom/HTMLInputElement.h"
+#include "mozilla/FloatingPoint.h"
+#include "Role.h"
+
+using namespace mozilla::a11y;
+
+////////////////////////////////////////////////////////////////////////////////
+// CheckboxAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+role CheckboxAccessible::NativeRole() const { return roles::CHECKBUTTON; }
+
+void CheckboxAccessible::ActionNameAt(uint8_t aIndex, nsAString& aName) {
+ if (aIndex == eAction_Click) {
+ uint64_t state = NativeState();
+ if (state & states::CHECKED) {
+ aName.AssignLiteral("uncheck");
+ } else if (state & states::MIXED) {
+ aName.AssignLiteral("cycle");
+ } else {
+ aName.AssignLiteral("check");
+ }
+ }
+}
+
+bool CheckboxAccessible::HasPrimaryAction() const { return true; }
+
+uint64_t CheckboxAccessible::NativeState() const {
+ uint64_t state = LeafAccessible::NativeState();
+
+ state |= states::CHECKABLE;
+ dom::HTMLInputElement* input = dom::HTMLInputElement::FromNode(mContent);
+ if (input) { // HTML:input@type="checkbox"
+ if (input->Indeterminate()) {
+ return state | states::MIXED;
+ }
+
+ if (input->Checked()) {
+ return state | states::CHECKED;
+ }
+
+ } else if (mContent->AsElement()->AttrValueIs(
+ kNameSpaceID_None, nsGkAtoms::checked, nsGkAtoms::_true,
+ eCaseMatters)) { // XUL checkbox
+ return state | states::CHECKED;
+ }
+
+ return state;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// CheckboxAccessible: Widgets
+
+bool CheckboxAccessible::IsWidget() const { return true; }
+
+////////////////////////////////////////////////////////////////////////////////
+// RadioButtonAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+RadioButtonAccessible::RadioButtonAccessible(nsIContent* aContent,
+ DocAccessible* aDoc)
+ : LeafAccessible(aContent, aDoc) {}
+
+bool RadioButtonAccessible::HasPrimaryAction() const { return true; }
+
+void RadioButtonAccessible::ActionNameAt(uint8_t aIndex, nsAString& aName) {
+ if (aIndex == eAction_Click) aName.AssignLiteral("select");
+}
+
+role RadioButtonAccessible::NativeRole() const { return roles::RADIOBUTTON; }
+
+////////////////////////////////////////////////////////////////////////////////
+// RadioButtonAccessible: Widgets
+
+bool RadioButtonAccessible::IsWidget() const { return true; }
diff --git a/accessible/generic/FormControlAccessible.h b/accessible/generic/FormControlAccessible.h
new file mode 100644
index 0000000000..44c142d4e2
--- /dev/null
+++ b/accessible/generic/FormControlAccessible.h
@@ -0,0 +1,65 @@
+/* -*- 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/. */
+
+#ifndef MOZILLA_A11Y_FormControlAccessible_H_
+#define MOZILLA_A11Y_FormControlAccessible_H_
+
+#include "BaseAccessibles.h"
+
+namespace mozilla {
+namespace a11y {
+
+/**
+ * Checkbox accessible.
+ */
+class CheckboxAccessible : public LeafAccessible {
+ public:
+ enum { eAction_Click = 0 };
+
+ CheckboxAccessible(nsIContent* aContent, DocAccessible* aDoc)
+ : LeafAccessible(aContent, aDoc) {
+ // Ignore "CheckboxStateChange" DOM event in lieu of document observer
+ // state change notification.
+ if (aContent->IsHTMLElement()) {
+ mStateFlags |= eIgnoreDOMUIEvent;
+ }
+ }
+
+ // LocalAccessible
+ virtual mozilla::a11y::role NativeRole() const override;
+ virtual uint64_t NativeState() const override;
+
+ // ActionAccessible
+ virtual void ActionNameAt(uint8_t aIndex, nsAString& aName) override;
+ virtual bool HasPrimaryAction() const override;
+
+ // Widgets
+ virtual bool IsWidget() const override;
+};
+
+/**
+ * Generic class used for radio buttons.
+ */
+class RadioButtonAccessible : public LeafAccessible {
+ public:
+ RadioButtonAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ // LocalAccessible
+ virtual mozilla::a11y::role NativeRole() const override;
+
+ // ActionAccessible
+ virtual void ActionNameAt(uint8_t aIndex, nsAString& aName) override;
+ virtual bool HasPrimaryAction() const override;
+
+ enum { eAction_Click = 0 };
+
+ // Widgets
+ virtual bool IsWidget() const override;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/generic/HyperTextAccessible-inl.h b/accessible/generic/HyperTextAccessible-inl.h
new file mode 100644
index 0000000000..3bb0530c9c
--- /dev/null
+++ b/accessible/generic/HyperTextAccessible-inl.h
@@ -0,0 +1,129 @@
+/* -*- 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/. */
+
+#ifndef mozilla_a11y_HyperTextAccessible_inl_h__
+#define mozilla_a11y_HyperTextAccessible_inl_h__
+
+#include "HyperTextAccessible.h"
+
+#include "nsAccUtils.h"
+
+#include "nsIClipboard.h"
+#include "nsFrameSelection.h"
+
+#include "mozilla/EditorBase.h"
+
+namespace mozilla {
+namespace a11y {
+
+inline void HyperTextAccessible::SetCaretOffset(int32_t aOffset) {
+ SetSelectionRange(aOffset, aOffset);
+ // XXX: Force cache refresh until a good solution for AT emulation of user
+ // input is implemented (AccessFu caret movement).
+ SelectionMgr()->UpdateCaretOffset(this, aOffset);
+}
+
+inline bool HyperTextAccessible::AddToSelection(int32_t aStartOffset,
+ int32_t aEndOffset) {
+ dom::Selection* domSel = DOMSelection();
+ return domSel &&
+ SetSelectionBoundsAt(domSel->RangeCount(), aStartOffset, aEndOffset);
+}
+
+inline void HyperTextAccessible::ReplaceText(const nsAString& aText) {
+ if (aText.Length() == 0) {
+ DeleteText(0, CharacterCount());
+ return;
+ }
+
+ SetSelectionRange(0, CharacterCount());
+
+ RefPtr<EditorBase> editorBase = GetEditor();
+ if (!editorBase) {
+ return;
+ }
+
+ DebugOnly<nsresult> rv = editorBase->InsertTextAsAction(aText);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to insert the new text");
+}
+
+inline void HyperTextAccessible::InsertText(const nsAString& aText,
+ int32_t aPosition) {
+ RefPtr<EditorBase> editorBase = GetEditor();
+ if (editorBase) {
+ SetSelectionRange(aPosition, aPosition);
+ DebugOnly<nsresult> rv = editorBase->InsertTextAsAction(aText);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to insert the text");
+ }
+}
+
+inline void HyperTextAccessible::CopyText(int32_t aStartPos, int32_t aEndPos) {
+ RefPtr<EditorBase> editorBase = GetEditor();
+ if (editorBase) {
+ SetSelectionRange(aStartPos, aEndPos);
+ editorBase->Copy();
+ }
+}
+
+inline void HyperTextAccessible::CutText(int32_t aStartPos, int32_t aEndPos) {
+ RefPtr<EditorBase> editorBase = GetEditor();
+ if (editorBase) {
+ SetSelectionRange(aStartPos, aEndPos);
+ editorBase->Cut();
+ }
+}
+
+inline void HyperTextAccessible::DeleteText(int32_t aStartPos,
+ int32_t aEndPos) {
+ RefPtr<EditorBase> editorBase = GetEditor();
+ if (!editorBase) {
+ return;
+ }
+ SetSelectionRange(aStartPos, aEndPos);
+ DebugOnly<nsresult> rv =
+ editorBase->DeleteSelectionAsAction(nsIEditor::eNone, nsIEditor::eStrip);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to delete text");
+}
+
+inline void HyperTextAccessible::PasteText(int32_t aPosition) {
+ RefPtr<EditorBase> editorBase = GetEditor();
+ if (editorBase) {
+ SetSelectionRange(aPosition, aPosition);
+ editorBase->PasteAsAction(nsIClipboard::kGlobalClipboard, true);
+ }
+}
+
+inline uint32_t HyperTextAccessible::AdjustCaretOffset(uint32_t aOffset) const {
+ // It is the same character offset when the caret is visually at the very
+ // end of a line or the start of a new line (soft line break). Getting text
+ // at the line should provide the line with the visual caret, otherwise
+ // screen readers will announce the wrong line as the user presses up or
+ // down arrow and land at the end of a line.
+ if (aOffset > 0 && IsCaretAtEndOfLine()) return aOffset - 1;
+
+ return aOffset;
+}
+
+inline bool HyperTextAccessible::IsCaretAtEndOfLine() const {
+ RefPtr<nsFrameSelection> frameSelection = FrameSelection();
+ return frameSelection && frameSelection->GetHint() == CARET_ASSOCIATE_BEFORE;
+}
+
+inline already_AddRefed<nsFrameSelection> HyperTextAccessible::FrameSelection()
+ const {
+ nsIFrame* frame = GetFrame();
+ return frame ? frame->GetFrameSelection() : nullptr;
+}
+
+inline dom::Selection* HyperTextAccessible::DOMSelection() const {
+ RefPtr<nsFrameSelection> frameSelection = FrameSelection();
+ return frameSelection ? frameSelection->GetSelection(SelectionType::eNormal)
+ : nullptr;
+}
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/generic/HyperTextAccessible.cpp b/accessible/generic/HyperTextAccessible.cpp
new file mode 100644
index 0000000000..fec1b31b28
--- /dev/null
+++ b/accessible/generic/HyperTextAccessible.cpp
@@ -0,0 +1,2357 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 sw=2 et tw=78: */
+/* 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 "HyperTextAccessible-inl.h"
+
+#include "nsAccessibilityService.h"
+#include "nsAccessiblePivot.h"
+#include "nsIAccessibleTypes.h"
+#include "AccAttributes.h"
+#include "DocAccessible.h"
+#include "HTMLListAccessible.h"
+#include "LocalAccessible-inl.h"
+#include "Pivot.h"
+#include "Relation.h"
+#include "Role.h"
+#include "States.h"
+#include "TextAttrs.h"
+#include "TextLeafRange.h"
+#include "TextRange.h"
+#include "TreeWalker.h"
+
+#include "nsCaret.h"
+#include "nsContentUtils.h"
+#include "nsDebug.h"
+#include "nsFocusManager.h"
+#include "nsIEditingSession.h"
+#include "nsContainerFrame.h"
+#include "nsFrameSelection.h"
+#include "nsILineIterator.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsIScrollableFrame.h"
+#include "nsIMathMLFrame.h"
+#include "nsRange.h"
+#include "nsTextFragment.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/BinarySearch.h"
+#include "mozilla/EditorBase.h"
+#include "mozilla/HTMLEditor.h"
+#include "mozilla/IntegerRange.h"
+#include "mozilla/MathAlgorithms.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/StaticPrefs_accessibility.h"
+#include "mozilla/StaticPrefs_layout.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/HTMLBRElement.h"
+#include "mozilla/dom/HTMLHeadingElement.h"
+#include "mozilla/dom/Selection.h"
+#include "gfxSkipChars.h"
+#include <algorithm>
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+/**
+ * This class is used in HyperTextAccessible to search for paragraph
+ * boundaries.
+ */
+class ParagraphBoundaryRule : public PivotRule {
+ public:
+ explicit ParagraphBoundaryRule(LocalAccessible* aAnchor,
+ uint32_t aAnchorTextoffset,
+ nsDirection aDirection,
+ bool aSkipAnchorSubtree = false)
+ : mAnchor(aAnchor),
+ mAnchorTextOffset(aAnchorTextoffset),
+ mDirection(aDirection),
+ mSkipAnchorSubtree(aSkipAnchorSubtree),
+ mLastMatchTextOffset(0) {}
+
+ virtual uint16_t Match(Accessible* aAcc) override {
+ MOZ_ASSERT(aAcc && aAcc->IsLocal());
+ LocalAccessible* acc = aAcc->AsLocal();
+ if (acc->IsOuterDoc()) {
+ // The child document might be remote and we can't (and don't want to)
+ // handle remote documents. Also, iframes are inline anyway and thus
+ // can't be paragraph boundaries. Therefore, skip this unconditionally.
+ return nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
+ }
+
+ uint16_t result = nsIAccessibleTraversalRule::FILTER_IGNORE;
+ if (mSkipAnchorSubtree && acc == mAnchor) {
+ result |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
+ }
+
+ // First, deal with the case that we encountered a line break, for example,
+ // a br in a paragraph.
+ if (acc->Role() == roles::WHITESPACE) {
+ result |= nsIAccessibleTraversalRule::FILTER_MATCH;
+ return result;
+ }
+
+ // Now, deal with the case that we encounter a new block level accessible.
+ // This also means a new paragraph boundary start.
+ nsIFrame* frame = acc->GetFrame();
+ if (frame && frame->IsBlockFrame() &&
+ acc->Role() != roles::LISTITEM_MARKER) {
+ result |= nsIAccessibleTraversalRule::FILTER_MATCH;
+ return result;
+ }
+
+ // A text leaf can contain a line break if it's pre-formatted text.
+ if (acc->IsTextLeaf()) {
+ nsAutoString name;
+ acc->Name(name);
+ int32_t offset;
+ if (mDirection == eDirPrevious) {
+ if (acc == mAnchor && mAnchorTextOffset == 0) {
+ // We're already at the start of this node, so there can be no line
+ // break before.
+ return result;
+ }
+ // If we began on a line break, we don't want to match it, so search
+ // from 1 before our anchor offset.
+ offset =
+ name.RFindChar('\n', acc == mAnchor ? mAnchorTextOffset - 1 : -1);
+ } else {
+ offset = name.FindChar('\n', acc == mAnchor ? mAnchorTextOffset : 0);
+ }
+ if (offset != -1) {
+ // Line ebreak!
+ mLastMatchTextOffset = offset;
+ result |= nsIAccessibleTraversalRule::FILTER_MATCH;
+ }
+ }
+
+ return result;
+ }
+
+ // This is only valid if the last match was a text leaf. It returns the
+ // offset of the line break character in that text leaf.
+ uint32_t GetLastMatchTextOffset() { return mLastMatchTextOffset; }
+
+ private:
+ LocalAccessible* mAnchor;
+ uint32_t mAnchorTextOffset;
+ nsDirection mDirection;
+ bool mSkipAnchorSubtree;
+ uint32_t mLastMatchTextOffset;
+};
+
+/**
+ * This class is used in HyperTextAccessible::FindParagraphStartOffset to
+ * search forward exactly one step from a match found by the above.
+ * It should only be initialized with a boundary, and it will skip that
+ * boundary's sub tree if it is a block element boundary.
+ */
+class SkipParagraphBoundaryRule : public PivotRule {
+ public:
+ explicit SkipParagraphBoundaryRule(Accessible* aBoundary)
+ : mBoundary(aBoundary) {}
+
+ virtual uint16_t Match(Accessible* aAcc) override {
+ MOZ_ASSERT(aAcc && aAcc->IsLocal());
+ // If matching the boundary, skip its sub tree.
+ if (aAcc == mBoundary) {
+ return nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
+ }
+ return nsIAccessibleTraversalRule::FILTER_MATCH;
+ }
+
+ private:
+ Accessible* mBoundary;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+// HyperTextAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+HyperTextAccessible::HyperTextAccessible(nsIContent* aNode, DocAccessible* aDoc)
+ : AccessibleWrap(aNode, aDoc) {
+ mType = eHyperTextType;
+ mGenericTypes |= eHyperText;
+}
+
+role HyperTextAccessible::NativeRole() const {
+ a11y::role r = GetAccService()->MarkupRole(mContent);
+ if (r != roles::NOTHING) return r;
+
+ nsIFrame* frame = GetFrame();
+ if (frame && frame->IsInlineFrame()) return roles::TEXT;
+
+ return roles::TEXT_CONTAINER;
+}
+
+uint64_t HyperTextAccessible::NativeState() const {
+ uint64_t states = AccessibleWrap::NativeState();
+
+ if (IsEditable()) {
+ states |= states::EDITABLE;
+
+ } else if (mContent->IsHTMLElement(nsGkAtoms::article)) {
+ // We want <article> to behave like a document in terms of readonly state.
+ states |= states::READONLY;
+ }
+
+ nsIFrame* frame = GetFrame();
+ if ((states & states::EDITABLE) || (frame && frame->IsSelectable(nullptr))) {
+ // If the accessible is editable the layout selectable state only disables
+ // mouse selection, but keyboard (shift+arrow) selection is still possible.
+ states |= states::SELECTABLE_TEXT;
+ }
+
+ return states;
+}
+
+bool HyperTextAccessible::IsEditable() const {
+ if (!mContent) {
+ return false;
+ }
+ return mContent->AsElement()->State().HasState(dom::ElementState::READWRITE);
+}
+
+LayoutDeviceIntRect HyperTextAccessible::GetBoundsInFrame(
+ nsIFrame* aFrame, uint32_t aStartRenderedOffset,
+ uint32_t aEndRenderedOffset) {
+ nsPresContext* presContext = mDoc->PresContext();
+ if (!aFrame->IsTextFrame()) {
+ return LayoutDeviceIntRect::FromAppUnitsToNearest(
+ aFrame->GetScreenRectInAppUnits(), presContext->AppUnitsPerDevPixel());
+ }
+
+ // Substring must be entirely within the same text node.
+ int32_t startContentOffset, endContentOffset;
+ nsresult rv = RenderedToContentOffset(aFrame, aStartRenderedOffset,
+ &startContentOffset);
+ NS_ENSURE_SUCCESS(rv, LayoutDeviceIntRect());
+ rv = RenderedToContentOffset(aFrame, aEndRenderedOffset, &endContentOffset);
+ NS_ENSURE_SUCCESS(rv, LayoutDeviceIntRect());
+
+ nsIFrame* frame;
+ int32_t startContentOffsetInFrame;
+ // Get the right frame continuation -- not really a child, but a sibling of
+ // the primary frame passed in
+ rv = aFrame->GetChildFrameContainingOffset(
+ startContentOffset, false, &startContentOffsetInFrame, &frame);
+ NS_ENSURE_SUCCESS(rv, LayoutDeviceIntRect());
+
+ nsRect screenRect;
+ while (frame && startContentOffset < endContentOffset) {
+ // Start with this frame's screen rect, which we will shrink based on
+ // the substring we care about within it. We will then add that frame to
+ // the total screenRect we are returning.
+ nsRect frameScreenRect = frame->GetScreenRectInAppUnits();
+
+ // Get the length of the substring in this frame that we want the bounds for
+ auto [startFrameTextOffset, endFrameTextOffset] = frame->GetOffsets();
+ int32_t frameTotalTextLength = endFrameTextOffset - startFrameTextOffset;
+ int32_t seekLength = endContentOffset - startContentOffset;
+ int32_t frameSubStringLength =
+ std::min(frameTotalTextLength - startContentOffsetInFrame, seekLength);
+
+ // Add the point where the string starts to the frameScreenRect
+ nsPoint frameTextStartPoint;
+ rv = frame->GetPointFromOffset(startContentOffset, &frameTextStartPoint);
+ NS_ENSURE_SUCCESS(rv, LayoutDeviceIntRect());
+
+ // Use the point for the end offset to calculate the width
+ nsPoint frameTextEndPoint;
+ rv = frame->GetPointFromOffset(startContentOffset + frameSubStringLength,
+ &frameTextEndPoint);
+ NS_ENSURE_SUCCESS(rv, LayoutDeviceIntRect());
+
+ frameScreenRect.SetRectX(
+ frameScreenRect.X() +
+ std::min(frameTextStartPoint.x, frameTextEndPoint.x),
+ mozilla::Abs(frameTextStartPoint.x - frameTextEndPoint.x));
+
+ screenRect.UnionRect(frameScreenRect, screenRect);
+
+ // Get ready to loop back for next frame continuation
+ startContentOffset += frameSubStringLength;
+ startContentOffsetInFrame = 0;
+ frame = frame->GetNextContinuation();
+ }
+
+ return LayoutDeviceIntRect::FromAppUnitsToNearest(
+ screenRect, presContext->AppUnitsPerDevPixel());
+}
+
+uint32_t HyperTextAccessible::DOMPointToOffset(nsINode* aNode,
+ int32_t aNodeOffset,
+ bool aIsEndOffset) const {
+ if (!aNode) return 0;
+
+ uint32_t offset = 0;
+ nsINode* findNode = nullptr;
+
+ if (aNodeOffset == -1) {
+ findNode = aNode;
+
+ } else if (aNode->IsText()) {
+ // For text nodes, aNodeOffset comes in as a character offset
+ // Text offset will be added at the end, if we find the offset in this
+ // hypertext We want the "skipped" offset into the text (rendered text
+ // without the extra whitespace)
+ nsIFrame* frame = aNode->AsContent()->GetPrimaryFrame();
+ NS_ENSURE_TRUE(frame, 0);
+
+ nsresult rv = ContentToRenderedOffset(frame, aNodeOffset, &offset);
+ NS_ENSURE_SUCCESS(rv, 0);
+
+ findNode = aNode;
+
+ } else {
+ // findNode could be null if aNodeOffset == # of child nodes, which means
+ // one of two things:
+ // 1) there are no children, and the passed-in node is not mContent -- use
+ // parentContent for the node to find
+ // 2) there are no children and the passed-in node is mContent, which means
+ // we're an empty nsIAccessibleText
+ // 3) there are children and we're at the end of the children
+
+ findNode = aNode->GetChildAt_Deprecated(aNodeOffset);
+ if (!findNode) {
+ if (aNodeOffset == 0) {
+ if (aNode == GetNode()) {
+ // Case #1: this accessible has no children and thus has empty text,
+ // we can only be at hypertext offset 0.
+ return 0;
+ }
+
+ // Case #2: there are no children, we're at this node.
+ findNode = aNode;
+ } else if (aNodeOffset == static_cast<int32_t>(aNode->GetChildCount())) {
+ // Case #3: we're after the last child, get next node to this one.
+ for (nsINode* tmpNode = aNode;
+ !findNode && tmpNode && tmpNode != mContent;
+ tmpNode = tmpNode->GetParent()) {
+ findNode = tmpNode->GetNextSibling();
+ }
+ }
+ }
+ }
+
+ // Get accessible for this findNode, or if that node isn't accessible, use the
+ // accessible for the next DOM node which has one (based on forward depth
+ // first search)
+ LocalAccessible* descendant = nullptr;
+ if (findNode) {
+ dom::HTMLBRElement* brElement = dom::HTMLBRElement::FromNode(findNode);
+ if (brElement && brElement->IsPaddingForEmptyEditor()) {
+ // This <br> is the hacky "padding <br> element" used when there is no
+ // text in the editor.
+ return 0;
+ }
+
+ descendant = mDoc->GetAccessible(findNode);
+ if (!descendant && findNode->IsContent()) {
+ LocalAccessible* container = mDoc->GetContainerAccessible(findNode);
+ if (container) {
+ TreeWalker walker(container, findNode->AsContent(),
+ TreeWalker::eWalkContextTree);
+ descendant = walker.Next();
+ if (!descendant) descendant = container;
+ }
+ }
+ }
+
+ return TransformOffset(descendant, offset, aIsEndOffset);
+}
+
+uint32_t HyperTextAccessible::TransformOffset(LocalAccessible* aDescendant,
+ uint32_t aOffset,
+ bool aIsEndOffset) const {
+ // From the descendant, go up and get the immediate child of this hypertext.
+ uint32_t offset = aOffset;
+ LocalAccessible* descendant = aDescendant;
+ while (descendant) {
+ LocalAccessible* parent = descendant->LocalParent();
+ if (parent == this) return GetChildOffset(descendant) + offset;
+
+ // This offset no longer applies because the passed-in text object is not
+ // a child of the hypertext. This happens when there are nested hypertexts,
+ // e.g. <div>abc<h1>def</h1>ghi</div>. Thus we need to adjust the offset
+ // to make it relative the hypertext.
+ // If the end offset is not supposed to be inclusive and the original point
+ // is not at 0 offset then the returned offset should be after an embedded
+ // character the original point belongs to.
+ if (aIsEndOffset) {
+ // Similar to our special casing in FindOffset, we add handling for
+ // bulleted lists here because PeekOffset returns the inner text node
+ // for a list when it should return the list bullet.
+ // We manually set the offset so the error doesn't propagate up.
+ if (offset == 0 && parent && parent->IsHTMLListItem() &&
+ descendant->LocalPrevSibling() &&
+ descendant->LocalPrevSibling() ==
+ parent->AsHTMLListItem()->Bullet()) {
+ offset = 0;
+ } else {
+ offset = (offset > 0 || descendant->IndexInParent() > 0) ? 1 : 0;
+ }
+ } else {
+ offset = 0;
+ }
+
+ descendant = parent;
+ }
+
+ // If the given a11y point cannot be mapped into offset relative this
+ // hypertext offset then return length as fallback value.
+ return CharacterCount();
+}
+
+DOMPoint HyperTextAccessible::OffsetToDOMPoint(int32_t aOffset) const {
+ // 0 offset is valid even if no children. In this case the associated editor
+ // is empty so return a DOM point for editor root element.
+ if (aOffset == 0) {
+ RefPtr<EditorBase> editorBase = GetEditor();
+ if (editorBase) {
+ if (editorBase->IsEmpty()) {
+ return DOMPoint(editorBase->GetRoot(), 0);
+ }
+ }
+ }
+
+ int32_t childIdx = GetChildIndexAtOffset(aOffset);
+ if (childIdx == -1) return DOMPoint();
+
+ LocalAccessible* child = LocalChildAt(childIdx);
+ int32_t innerOffset = aOffset - GetChildOffset(childIdx);
+
+ // A text leaf case.
+ if (child->IsTextLeaf()) {
+ // The point is inside the text node. This is always true for any text leaf
+ // except a last child one. See assertion below.
+ if (aOffset < GetChildOffset(childIdx + 1)) {
+ nsIContent* content = child->GetContent();
+ int32_t idx = 0;
+ if (NS_FAILED(RenderedToContentOffset(content->GetPrimaryFrame(),
+ innerOffset, &idx))) {
+ return DOMPoint();
+ }
+
+ return DOMPoint(content, idx);
+ }
+
+ // Set the DOM point right after the text node.
+ MOZ_ASSERT(static_cast<uint32_t>(aOffset) == CharacterCount());
+ innerOffset = 1;
+ }
+
+ // Case of embedded object. The point is either before or after the element.
+ NS_ASSERTION(innerOffset == 0 || innerOffset == 1, "A wrong inner offset!");
+ nsINode* node = child->GetNode();
+ nsINode* parentNode = node->GetParentNode();
+ return parentNode ? DOMPoint(parentNode,
+ parentNode->ComputeIndexOf_Deprecated(node) +
+ innerOffset)
+ : DOMPoint();
+}
+
+uint32_t HyperTextAccessible::FindOffset(uint32_t aOffset,
+ nsDirection aDirection,
+ nsSelectionAmount aAmount,
+ EWordMovementType aWordMovementType) {
+ NS_ASSERTION(aDirection == eDirPrevious || aAmount != eSelectBeginLine,
+ "eSelectBeginLine should only be used with eDirPrevious");
+
+ // Find a leaf accessible frame to start with. PeekOffset wants this.
+ HyperTextAccessible* text = this;
+ LocalAccessible* child = nullptr;
+ int32_t innerOffset = aOffset;
+
+ do {
+ int32_t childIdx = text->GetChildIndexAtOffset(innerOffset);
+
+ // We can have an empty text leaf as our only child. Since empty text
+ // leaves are not accessible we then have no children, but 0 is a valid
+ // innerOffset.
+ if (childIdx == -1) {
+ NS_ASSERTION(innerOffset == 0 && !text->ChildCount(), "No childIdx?");
+ return DOMPointToOffset(text->GetNode(), 0, aDirection == eDirNext);
+ }
+
+ child = text->LocalChildAt(childIdx);
+
+ // HTML list items may need special processing because PeekOffset doesn't
+ // work with list bullets.
+ if (text->IsHTMLListItem()) {
+ HTMLLIAccessible* li = text->AsHTMLListItem();
+ if (child == li->Bullet()) {
+ // XXX: the logic is broken for multichar bullets in moving by
+ // char/cluster/word cases.
+ if (text != this) {
+ return aDirection == eDirPrevious ? TransformOffset(text, 0, false)
+ : TransformOffset(text, 1, true);
+ }
+ if (aDirection == eDirPrevious) return 0;
+
+ uint32_t nextOffset = GetChildOffset(1);
+ if (nextOffset == 0) return 0;
+
+ switch (aAmount) {
+ case eSelectLine:
+ case eSelectEndLine:
+ // Ask a text leaf next (if not empty) to the bullet for an offset
+ // since list item may be multiline.
+ return nextOffset < CharacterCount()
+ ? FindOffset(nextOffset, aDirection, aAmount,
+ aWordMovementType)
+ : nextOffset;
+
+ default:
+ return nextOffset;
+ }
+ }
+ }
+
+ innerOffset -= text->GetChildOffset(childIdx);
+
+ text = child->AsHyperText();
+ } while (text);
+
+ nsIFrame* childFrame = child->GetFrame();
+ if (!childFrame) {
+ NS_ERROR("No child frame");
+ return 0;
+ }
+
+ int32_t innerContentOffset = innerOffset;
+ if (child->IsTextLeaf()) {
+ NS_ASSERTION(childFrame->IsTextFrame(), "Wrong frame!");
+ RenderedToContentOffset(childFrame, innerOffset, &innerContentOffset);
+ }
+
+ nsIFrame* frameAtOffset = childFrame;
+ int32_t unusedOffsetInFrame = 0;
+ childFrame->GetChildFrameContainingOffset(
+ innerContentOffset, true, &unusedOffsetInFrame, &frameAtOffset);
+
+ const bool kIsJumpLinesOk = true; // okay to jump lines
+ const bool kIsScrollViewAStop = false; // do not stop at scroll views
+ const bool kIsKeyboardSelect = true; // is keyboard selection
+ const bool kIsVisualBidi = false; // use visual order for bidi text
+ nsPeekOffsetStruct pos(
+ aAmount, aDirection, innerContentOffset, nsPoint(0, 0), kIsJumpLinesOk,
+ kIsScrollViewAStop, kIsKeyboardSelect, kIsVisualBidi, false,
+ nsPeekOffsetStruct::ForceEditableRegion::No, aWordMovementType, false);
+ nsresult rv = frameAtOffset->PeekOffset(&pos);
+
+ // PeekOffset fails on last/first lines of the text in certain cases.
+ bool fallBackToSelectEndLine = false;
+ if (NS_FAILED(rv) && aAmount == eSelectLine) {
+ fallBackToSelectEndLine = aDirection == eDirNext;
+ pos.mAmount = fallBackToSelectEndLine ? eSelectEndLine : eSelectBeginLine;
+ frameAtOffset->PeekOffset(&pos);
+ }
+ if (!pos.mResultContent) {
+ NS_ERROR("No result content!");
+ return 0;
+ }
+
+ // Turn the resulting DOM point into an offset.
+ uint32_t hyperTextOffset = DOMPointToOffset(
+ pos.mResultContent, pos.mContentOffset, aDirection == eDirNext);
+
+ if (fallBackToSelectEndLine && IsLineEndCharAt(hyperTextOffset)) {
+ // We used eSelectEndLine, but the caller requested eSelectLine.
+ // If there's a '\n' at the end of the line, eSelectEndLine will stop on
+ // it rather than after it. This is not what we want, since the caller
+ // wants the next line, not the same line.
+ ++hyperTextOffset;
+ }
+
+ if (aDirection == eDirPrevious) {
+ // If we reached the end during search, this means we didn't find the DOM
+ // point and we're actually at the start of the paragraph
+ if (hyperTextOffset == CharacterCount()) return 0;
+
+ // PeekOffset stops right before bullet so return 0 to workaround it.
+ if (IsHTMLListItem() && aAmount == eSelectBeginLine &&
+ hyperTextOffset > 0) {
+ LocalAccessible* prevOffsetChild = GetChildAtOffset(hyperTextOffset - 1);
+ if (prevOffsetChild == AsHTMLListItem()->Bullet()) return 0;
+ }
+ }
+
+ return hyperTextOffset;
+}
+
+uint32_t HyperTextAccessible::FindWordBoundary(
+ uint32_t aOffset, nsDirection aDirection,
+ EWordMovementType aWordMovementType) {
+ uint32_t orig =
+ FindOffset(aOffset, aDirection, eSelectWord, aWordMovementType);
+ if (aWordMovementType != eStartWord) {
+ return orig;
+ }
+ if (aDirection == eDirPrevious) {
+ // When layout.word_select.stop_at_punctuation is true (the default),
+ // for a word beginning with punctuation, layout treats the punctuation
+ // as the start of the word when moving next. However, when moving
+ // previous, layout stops *after* the punctuation. We want to be
+ // consistent regardless of movement direction and always treat punctuation
+ // as the start of a word.
+ if (!StaticPrefs::layout_word_select_stop_at_punctuation()) {
+ return orig;
+ }
+ // Case 1: Example: "a @"
+ // If aOffset is 2 or 3, orig will be 0, but it should be 2. That is,
+ // previous word moved back too far.
+ LocalAccessible* child = GetChildAtOffset(orig);
+ if (child && child->IsHyperText()) {
+ // For a multi-word embedded object, previous word correctly goes back
+ // to the start of the word (the embedded object). Next word (below)
+ // incorrectly stops after the embedded object in this case, so return
+ // the already correct result.
+ // Example: "a x y b", where "x y" is an embedded link
+ // If aOffset is 4, orig will be 2, which is correct.
+ // If we get the next word (below), we'll end up returning 3 instead.
+ return orig;
+ }
+ uint32_t next = FindOffset(orig, eDirNext, eSelectWord, eStartWord);
+ if (next < aOffset) {
+ // Next word stopped on punctuation.
+ return next;
+ }
+ // case 2: example: "a @@b"
+ // If aOffset is 2, 3 or 4, orig will be 4, but it should be 2. That is,
+ // previous word didn't go back far enough.
+ if (orig == 0) {
+ return orig;
+ }
+ // Walk backwards by offset, getting the next word.
+ // In the loop, o is unsigned, so o >= 0 will always be true and won't
+ // prevent us from decrementing at 0. Instead, we check that o doesn't
+ // wrap around.
+ for (uint32_t o = orig - 1; o < orig; --o) {
+ next = FindOffset(o, eDirNext, eSelectWord, eStartWord);
+ if (next == orig) {
+ // Next word and previous word were consistent. This
+ // punctuation problem isn't applicable here.
+ break;
+ }
+ if (next < orig) {
+ // Next word stopped on punctuation.
+ return next;
+ }
+ }
+ } else {
+ // When layout.word_select.stop_at_punctuation is true (the default),
+ // when positioned on punctuation in the middle of a word, next word skips
+ // the rest of the word. However, when positioned before the punctuation,
+ // next word moves just after the punctuation. We want to be consistent
+ // regardless of starting position and always stop just after the
+ // punctuation.
+ // Next word can move too far when positioned on white space too.
+ // Example: "a b@c"
+ // If aOffset is 3, orig will be 5, but it should be 4. That is, next word
+ // moved too far.
+ if (aOffset == 0) {
+ return orig;
+ }
+ uint32_t prev = FindOffset(orig, eDirPrevious, eSelectWord, eStartWord);
+ if (prev <= aOffset) {
+ // orig definitely isn't too far forward.
+ return orig;
+ }
+ // Walk backwards by offset, getting the next word.
+ // In the loop, o is unsigned, so o >= 0 will always be true and won't
+ // prevent us from decrementing at 0. Instead, we check that o doesn't
+ // wrap around.
+ for (uint32_t o = aOffset - 1; o < aOffset; --o) {
+ uint32_t next = FindOffset(o, eDirNext, eSelectWord, eStartWord);
+ if (next > aOffset && next < orig) {
+ return next;
+ }
+ if (next <= aOffset) {
+ break;
+ }
+ }
+ }
+ return orig;
+}
+
+uint32_t HyperTextAccessible::FindLineBoundary(
+ uint32_t aOffset, EWhichLineBoundary aWhichLineBoundary) {
+ // Note: empty last line doesn't have own frame (a previous line contains '\n'
+ // character instead) thus when it makes a difference we need to process this
+ // case separately (otherwise operations are performed on previous line).
+ switch (aWhichLineBoundary) {
+ case ePrevLineBegin: {
+ // Fetch a previous line and move to its start (as arrow up and home keys
+ // were pressed).
+ if (IsEmptyLastLineOffset(aOffset)) {
+ return FindOffset(aOffset, eDirPrevious, eSelectBeginLine);
+ }
+
+ uint32_t tmpOffset = FindOffset(aOffset, eDirPrevious, eSelectLine);
+ return FindOffset(tmpOffset, eDirPrevious, eSelectBeginLine);
+ }
+
+ case ePrevLineEnd: {
+ if (IsEmptyLastLineOffset(aOffset)) return aOffset - 1;
+
+ // If offset is at first line then return 0 (first line start).
+ uint32_t tmpOffset = FindOffset(aOffset, eDirPrevious, eSelectBeginLine);
+ if (tmpOffset == 0) return 0;
+
+ // Otherwise move to end of previous line (as arrow up and end keys were
+ // pressed).
+ tmpOffset = FindOffset(aOffset, eDirPrevious, eSelectLine);
+ return FindOffset(tmpOffset, eDirNext, eSelectEndLine);
+ }
+
+ case eThisLineBegin: {
+ if (IsEmptyLastLineOffset(aOffset)) return aOffset;
+
+ // Move to begin of the current line (as home key was pressed).
+ uint32_t thisLineBeginOffset =
+ FindOffset(aOffset, eDirPrevious, eSelectBeginLine);
+ if (IsCharAt(thisLineBeginOffset, kEmbeddedObjectChar)) {
+ // We landed on an embedded character, don't mess with possible embedded
+ // line breaks, and assume the offset is correct.
+ return thisLineBeginOffset;
+ }
+
+ // Sometimes, there is the possibility layout returned an
+ // offset smaller than it should. Sanity-check by moving to the end of the
+ // previous line and see if that has a greater offset.
+ uint32_t tmpOffset = FindOffset(aOffset, eDirPrevious, eSelectLine);
+ tmpOffset = FindOffset(tmpOffset, eDirNext, eSelectEndLine);
+ if (tmpOffset > thisLineBeginOffset && tmpOffset < aOffset) {
+ // We found a previous line offset. Return the next character after it
+ // as our start offset if it points to a line end char.
+ return IsLineEndCharAt(tmpOffset) ? tmpOffset + 1 : tmpOffset;
+ }
+ return thisLineBeginOffset;
+ }
+
+ case eThisLineEnd:
+ if (IsEmptyLastLineOffset(aOffset)) return aOffset;
+
+ // Move to end of the current line (as end key was pressed).
+ return FindOffset(aOffset, eDirNext, eSelectEndLine);
+
+ case eNextLineBegin: {
+ if (IsEmptyLastLineOffset(aOffset)) return aOffset;
+
+ // Move to begin of the next line if any (arrow down and home keys),
+ // otherwise end of the current line (arrow down only).
+ uint32_t tmpOffset = FindOffset(aOffset, eDirNext, eSelectLine);
+ uint32_t characterCount = CharacterCount();
+ if (tmpOffset == characterCount) {
+ return tmpOffset;
+ }
+
+ // Now, simulate the Home key on the next line to get its real offset.
+ uint32_t nextLineBeginOffset =
+ FindOffset(tmpOffset, eDirPrevious, eSelectBeginLine);
+ // Sometimes, there are line breaks inside embedded characters. If this
+ // is the case, the cursor is after the line break, but the offset will
+ // be that of the embedded character, which points to before the line
+ // break. We definitely want the line break included.
+ if (IsCharAt(nextLineBeginOffset, kEmbeddedObjectChar)) {
+ // We can determine if there is a line break by pressing End from
+ // the queried offset. If there is a line break, the offset will be 1
+ // greater, since this line ends with the embed. If there is not, the
+ // value will be different even if a line break follows right after the
+ // embed.
+ uint32_t thisLineEndOffset =
+ FindOffset(aOffset, eDirNext, eSelectEndLine);
+ if (thisLineEndOffset == nextLineBeginOffset + 1) {
+ // If we're querying the offset of the embedded character, we want
+ // the end offset of the parent line instead. Press End
+ // once more from the current position, which is after the embed.
+ if (nextLineBeginOffset == aOffset) {
+ uint32_t thisLineEndOffset2 =
+ FindOffset(thisLineEndOffset, eDirNext, eSelectEndLine);
+ // The above returns an offset exclusive the final line break, so we
+ // need to add 1 to it to return an inclusive end offset. Make sure
+ // we don't overshoot if we've started from another embedded
+ // character that has a line break, or landed on another embedded
+ // character, or if the result is the very end.
+ return (thisLineEndOffset2 == characterCount ||
+ (IsCharAt(thisLineEndOffset, kEmbeddedObjectChar) &&
+ thisLineEndOffset2 == thisLineEndOffset + 1) ||
+ IsCharAt(thisLineEndOffset2, kEmbeddedObjectChar))
+ ? thisLineEndOffset2
+ : thisLineEndOffset2 + 1;
+ }
+
+ return thisLineEndOffset;
+ }
+ return nextLineBeginOffset;
+ }
+
+ // If the resulting offset is not greater than the offset we started from,
+ // layout could not find the offset for us. This can happen with certain
+ // inline-block elements.
+ if (nextLineBeginOffset <= aOffset) {
+ // Walk forward from the offset we started from up to tmpOffset,
+ // stopping after a line end character.
+ nextLineBeginOffset = aOffset;
+ while (nextLineBeginOffset < tmpOffset) {
+ if (IsLineEndCharAt(nextLineBeginOffset)) {
+ return nextLineBeginOffset + 1;
+ }
+ nextLineBeginOffset++;
+ }
+ }
+
+ return nextLineBeginOffset;
+ }
+
+ case eNextLineEnd: {
+ if (IsEmptyLastLineOffset(aOffset)) return aOffset;
+
+ // Move to next line end (as down arrow and end key were pressed).
+ uint32_t tmpOffset = FindOffset(aOffset, eDirNext, eSelectLine);
+ if (tmpOffset == CharacterCount()) return tmpOffset;
+
+ return FindOffset(tmpOffset, eDirNext, eSelectEndLine);
+ }
+ }
+
+ return 0;
+}
+
+int32_t HyperTextAccessible::FindParagraphStartOffset(uint32_t aOffset) {
+ // Because layout often gives us offsets that are incompatible with
+ // accessibility API requirements, for example when a paragraph contains
+ // presentational line breaks as found in Google Docs, use the accessibility
+ // tree to find the start offset instead.
+ LocalAccessible* child = GetChildAtOffset(aOffset);
+ if (!child) {
+ return -1; // Invalid offset
+ }
+
+ // Use the pivot class to search for the start offset.
+ Pivot p = Pivot(this);
+ ParagraphBoundaryRule boundaryRule = ParagraphBoundaryRule(
+ child, child->IsTextLeaf() ? aOffset - GetChildOffset(child) : 0,
+ eDirPrevious);
+ Accessible* match = p.Prev(child, boundaryRule, true);
+ if (!match || match->AsLocal() == this) {
+ // Found nothing, or pivot found the root of the search, startOffset is 0.
+ // This will include all relevant text nodes.
+ return 0;
+ }
+
+ if (match == child) {
+ // We started out on a boundary.
+ if (match->Role() == roles::WHITESPACE) {
+ // We are on a line break boundary, so force pivot to find the previous
+ // boundary. What we want is any text before this, if any.
+ match = p.Prev(match, boundaryRule);
+ if (!match || match->AsLocal() == this) {
+ // Same as before, we landed on the root, so offset is definitely 0.
+ return 0;
+ }
+ } else if (!match->AsLocal()->IsTextLeaf()) {
+ // The match is a block element, which is always a starting point, so
+ // just return its offset.
+ return TransformOffset(match->AsLocal(), 0, false);
+ }
+ }
+
+ if (match->AsLocal()->IsTextLeaf()) {
+ // ParagraphBoundaryRule only returns a text leaf if it contains a line
+ // break. We want to stop after that.
+ return TransformOffset(match->AsLocal(),
+ boundaryRule.GetLastMatchTextOffset() + 1, false);
+ }
+
+ // This is a previous boundary, we don't want to include it itself.
+ // So, walk forward one accessible, excluding the descendants of this
+ // boundary if it is a block element. The below call to Next should always be
+ // initialized with a boundary.
+ SkipParagraphBoundaryRule goForwardOneRule = SkipParagraphBoundaryRule(match);
+ match = p.Next(match, goForwardOneRule);
+ // We already know that the search skipped over at least one accessible,
+ // so match can't be null. Get its transformed offset.
+ MOZ_ASSERT(match);
+ return TransformOffset(match->AsLocal(), 0, false);
+}
+
+int32_t HyperTextAccessible::FindParagraphEndOffset(uint32_t aOffset) {
+ // Because layout often gives us offsets that are incompatible with
+ // accessibility API requirements, for example when a paragraph contains
+ // presentational line breaks as found in Google Docs, use the accessibility
+ // tree to find the end offset instead.
+ LocalAccessible* child = GetChildAtOffset(aOffset);
+ if (!child) {
+ return -1; // invalid offset
+ }
+
+ // Use the pivot class to search for the end offset.
+ Pivot p = Pivot(this);
+ ParagraphBoundaryRule boundaryRule = ParagraphBoundaryRule(
+ child, child->IsTextLeaf() ? aOffset - GetChildOffset(child) : 0,
+ eDirNext,
+ // In order to encompass all paragraphs inside embedded objects, not just
+ // the first, we want to skip the anchor's subtree.
+ /* aSkipAnchorSubtree */ true);
+ // Search forward for the end offset, including child. We don't want
+ // to go beyond this point if this offset indicates a paragraph boundary.
+ Accessible* match = p.Next(child, boundaryRule, true);
+ if (match) {
+ // Found something of relevance, adjust end offset.
+ LocalAccessible* matchAcc = match->AsLocal();
+ uint32_t matchOffset;
+ if (matchAcc->IsTextLeaf()) {
+ // ParagraphBoundaryRule only returns a text leaf if it contains a line
+ // break.
+ matchOffset = boundaryRule.GetLastMatchTextOffset() + 1;
+ } else if (matchAcc->Role() != roles::WHITESPACE && matchAcc != child) {
+ // We found a block boundary that wasn't our origin. We want to stop
+ // right on it, not after it, since we don't want to include the content
+ // of the block.
+ matchOffset = 0;
+ } else {
+ matchOffset = nsAccUtils::TextLength(matchAcc);
+ }
+ return TransformOffset(matchAcc, matchOffset, true);
+ }
+
+ // Didn't find anything, end offset is character count.
+ return CharacterCount();
+}
+
+void HyperTextAccessible::TextBeforeOffset(int32_t aOffset,
+ AccessibleTextBoundary aBoundaryType,
+ int32_t* aStartOffset,
+ int32_t* aEndOffset,
+ nsAString& aText) {
+ if (StaticPrefs::accessibility_cache_enabled_AtStartup()) {
+ // This isn't strictly related to caching, but this new text implementation
+ // is being developed to make caching feasible. We put it behind this pref
+ // to make it easy to test while it's still under development.
+ return HyperTextAccessibleBase::TextBeforeOffset(
+ aOffset, aBoundaryType, aStartOffset, aEndOffset, aText);
+ }
+
+ *aStartOffset = *aEndOffset = 0;
+ aText.Truncate();
+
+ if (aBoundaryType == nsIAccessibleText::BOUNDARY_PARAGRAPH) {
+ // Not supported, bail out with empty text.
+ return;
+ }
+
+ index_t convertedOffset = ConvertMagicOffset(aOffset);
+ if (!convertedOffset.IsValid() || convertedOffset > CharacterCount()) {
+ NS_ERROR("Wrong in offset!");
+ return;
+ }
+
+ uint32_t adjustedOffset = convertedOffset;
+ if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) {
+ adjustedOffset = AdjustCaretOffset(adjustedOffset);
+ }
+
+ switch (aBoundaryType) {
+ case nsIAccessibleText::BOUNDARY_CHAR:
+ if (convertedOffset != 0) {
+ CharAt(convertedOffset - 1, aText, aStartOffset, aEndOffset);
+ }
+ break;
+
+ case nsIAccessibleText::BOUNDARY_WORD_START: {
+ // If the offset is a word start (except text length offset) then move
+ // backward to find a start offset (end offset is the given offset).
+ // Otherwise move backward twice to find both start and end offsets.
+ if (adjustedOffset == CharacterCount()) {
+ *aEndOffset =
+ FindWordBoundary(adjustedOffset, eDirPrevious, eStartWord);
+ *aStartOffset = FindWordBoundary(*aEndOffset, eDirPrevious, eStartWord);
+ } else {
+ *aStartOffset =
+ FindWordBoundary(adjustedOffset, eDirPrevious, eStartWord);
+ *aEndOffset = FindWordBoundary(*aStartOffset, eDirNext, eStartWord);
+ if (*aEndOffset != static_cast<int32_t>(adjustedOffset)) {
+ *aEndOffset = *aStartOffset;
+ *aStartOffset =
+ FindWordBoundary(*aEndOffset, eDirPrevious, eStartWord);
+ }
+ }
+ TextSubstring(*aStartOffset, *aEndOffset, aText);
+ break;
+ }
+
+ case nsIAccessibleText::BOUNDARY_WORD_END: {
+ // Move word backward twice to find start and end offsets.
+ *aEndOffset = FindWordBoundary(convertedOffset, eDirPrevious, eEndWord);
+ *aStartOffset = FindWordBoundary(*aEndOffset, eDirPrevious, eEndWord);
+ TextSubstring(*aStartOffset, *aEndOffset, aText);
+ break;
+ }
+
+ case nsIAccessibleText::BOUNDARY_LINE_START:
+ *aStartOffset = FindLineBoundary(adjustedOffset, ePrevLineBegin);
+ *aEndOffset = FindLineBoundary(adjustedOffset, eThisLineBegin);
+ TextSubstring(*aStartOffset, *aEndOffset, aText);
+ break;
+
+ case nsIAccessibleText::BOUNDARY_LINE_END: {
+ *aEndOffset = FindLineBoundary(adjustedOffset, ePrevLineEnd);
+ int32_t tmpOffset = *aEndOffset;
+ // Adjust offset if line is wrapped.
+ if (*aEndOffset != 0 && !IsLineEndCharAt(*aEndOffset)) tmpOffset--;
+
+ *aStartOffset = FindLineBoundary(tmpOffset, ePrevLineEnd);
+ TextSubstring(*aStartOffset, *aEndOffset, aText);
+ break;
+ }
+ }
+}
+
+void HyperTextAccessible::TextAtOffset(int32_t aOffset,
+ AccessibleTextBoundary aBoundaryType,
+ int32_t* aStartOffset,
+ int32_t* aEndOffset, nsAString& aText) {
+ if (StaticPrefs::accessibility_cache_enabled_AtStartup()) {
+ // This isn't strictly related to caching, but this new text implementation
+ // is being developed to make caching feasible. We put it behind this pref
+ // to make it easy to test while it's still under development.
+ return HyperTextAccessibleBase::TextAtOffset(
+ aOffset, aBoundaryType, aStartOffset, aEndOffset, aText);
+ }
+
+ *aStartOffset = *aEndOffset = 0;
+ aText.Truncate();
+
+ uint32_t adjustedOffset = ConvertMagicOffset(aOffset);
+ if (adjustedOffset == std::numeric_limits<uint32_t>::max()) {
+ NS_ERROR("Wrong given offset!");
+ return;
+ }
+
+ switch (aBoundaryType) {
+ case nsIAccessibleText::BOUNDARY_CHAR:
+ // Return no char if caret is at the end of wrapped line (case of no line
+ // end character). Returning a next line char is confusing for AT.
+ if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET &&
+ IsCaretAtEndOfLine()) {
+ *aStartOffset = *aEndOffset = adjustedOffset;
+ } else {
+ CharAt(adjustedOffset, aText, aStartOffset, aEndOffset);
+ }
+ break;
+
+ case nsIAccessibleText::BOUNDARY_WORD_START:
+ if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) {
+ adjustedOffset = AdjustCaretOffset(adjustedOffset);
+ }
+
+ *aEndOffset = FindWordBoundary(adjustedOffset, eDirNext, eStartWord);
+ *aStartOffset = FindWordBoundary(*aEndOffset, eDirPrevious, eStartWord);
+ TextSubstring(*aStartOffset, *aEndOffset, aText);
+ break;
+
+ case nsIAccessibleText::BOUNDARY_WORD_END:
+ // Ignore the spec and follow what WebKitGtk does because Orca expects it,
+ // i.e. return a next word at word end offset of the current word
+ // (WebKitGtk behavior) instead the current word (AKT spec).
+ *aEndOffset = FindWordBoundary(adjustedOffset, eDirNext, eEndWord);
+ *aStartOffset = FindWordBoundary(*aEndOffset, eDirPrevious, eEndWord);
+ TextSubstring(*aStartOffset, *aEndOffset, aText);
+ break;
+
+ case nsIAccessibleText::BOUNDARY_LINE_START:
+ if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) {
+ adjustedOffset = AdjustCaretOffset(adjustedOffset);
+ }
+
+ *aStartOffset = FindLineBoundary(adjustedOffset, eThisLineBegin);
+ *aEndOffset = FindLineBoundary(adjustedOffset, eNextLineBegin);
+ TextSubstring(*aStartOffset, *aEndOffset, aText);
+ break;
+
+ case nsIAccessibleText::BOUNDARY_LINE_END:
+ if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) {
+ adjustedOffset = AdjustCaretOffset(adjustedOffset);
+ }
+
+ // In contrast to word end boundary we follow the spec here.
+ *aStartOffset = FindLineBoundary(adjustedOffset, ePrevLineEnd);
+ *aEndOffset = FindLineBoundary(adjustedOffset, eThisLineEnd);
+ TextSubstring(*aStartOffset, *aEndOffset, aText);
+ break;
+
+ case nsIAccessibleText::BOUNDARY_PARAGRAPH: {
+ if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) {
+ adjustedOffset = AdjustCaretOffset(adjustedOffset);
+ }
+
+ if (IsEmptyLastLineOffset(adjustedOffset)) {
+ // We are on the last line of a paragraph where there is no text.
+ // For example, in a textarea where a new line has just been inserted.
+ // In this case, return offsets for an empty line without text content.
+ *aStartOffset = *aEndOffset = adjustedOffset;
+ break;
+ }
+
+ *aStartOffset = FindParagraphStartOffset(adjustedOffset);
+ *aEndOffset = FindParagraphEndOffset(adjustedOffset);
+ TextSubstring(*aStartOffset, *aEndOffset, aText);
+ break;
+ }
+ }
+}
+
+void HyperTextAccessible::TextAfterOffset(int32_t aOffset,
+ AccessibleTextBoundary aBoundaryType,
+ int32_t* aStartOffset,
+ int32_t* aEndOffset,
+ nsAString& aText) {
+ if (StaticPrefs::accessibility_cache_enabled_AtStartup()) {
+ // This isn't strictly related to caching, but this new text implementation
+ // is being developed to make caching feasible. We put it behind this pref
+ // to make it easy to test while it's still under development.
+ return HyperTextAccessibleBase::TextAfterOffset(
+ aOffset, aBoundaryType, aStartOffset, aEndOffset, aText);
+ }
+
+ *aStartOffset = *aEndOffset = 0;
+ aText.Truncate();
+
+ if (aBoundaryType == nsIAccessibleText::BOUNDARY_PARAGRAPH) {
+ // Not supported, bail out with empty text.
+ return;
+ }
+
+ index_t convertedOffset = ConvertMagicOffset(aOffset);
+ if (!convertedOffset.IsValid() || convertedOffset > CharacterCount()) {
+ NS_ERROR("Wrong in offset!");
+ return;
+ }
+
+ uint32_t adjustedOffset = convertedOffset;
+ if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) {
+ adjustedOffset = AdjustCaretOffset(adjustedOffset);
+ }
+
+ switch (aBoundaryType) {
+ case nsIAccessibleText::BOUNDARY_CHAR:
+ // If caret is at the end of wrapped line (case of no line end character)
+ // then char after the offset is a first char at next line.
+ if (adjustedOffset >= CharacterCount()) {
+ *aStartOffset = *aEndOffset = CharacterCount();
+ } else {
+ CharAt(adjustedOffset + 1, aText, aStartOffset, aEndOffset);
+ }
+ break;
+
+ case nsIAccessibleText::BOUNDARY_WORD_START:
+ // Move word forward twice to find start and end offsets.
+ *aStartOffset = FindWordBoundary(adjustedOffset, eDirNext, eStartWord);
+ *aEndOffset = FindWordBoundary(*aStartOffset, eDirNext, eStartWord);
+ TextSubstring(*aStartOffset, *aEndOffset, aText);
+ break;
+
+ case nsIAccessibleText::BOUNDARY_WORD_END:
+ // If the offset is a word end (except 0 offset) then move forward to find
+ // end offset (start offset is the given offset). Otherwise move forward
+ // twice to find both start and end offsets.
+ if (convertedOffset == 0) {
+ *aStartOffset = FindWordBoundary(convertedOffset, eDirNext, eEndWord);
+ *aEndOffset = FindWordBoundary(*aStartOffset, eDirNext, eEndWord);
+ } else {
+ *aEndOffset = FindWordBoundary(convertedOffset, eDirNext, eEndWord);
+ *aStartOffset = FindWordBoundary(*aEndOffset, eDirPrevious, eEndWord);
+ if (*aStartOffset != static_cast<int32_t>(convertedOffset)) {
+ *aStartOffset = *aEndOffset;
+ *aEndOffset = FindWordBoundary(*aStartOffset, eDirNext, eEndWord);
+ }
+ }
+ TextSubstring(*aStartOffset, *aEndOffset, aText);
+ break;
+
+ case nsIAccessibleText::BOUNDARY_LINE_START:
+ *aStartOffset = FindLineBoundary(adjustedOffset, eNextLineBegin);
+ *aEndOffset = FindLineBoundary(*aStartOffset, eNextLineBegin);
+ TextSubstring(*aStartOffset, *aEndOffset, aText);
+ break;
+
+ case nsIAccessibleText::BOUNDARY_LINE_END:
+ *aStartOffset = FindLineBoundary(adjustedOffset, eThisLineEnd);
+ *aEndOffset = FindLineBoundary(adjustedOffset, eNextLineEnd);
+ TextSubstring(*aStartOffset, *aEndOffset, aText);
+ break;
+ }
+}
+
+already_AddRefed<AccAttributes> HyperTextAccessible::TextAttributes(
+ bool aIncludeDefAttrs, int32_t aOffset, int32_t* aStartOffset,
+ int32_t* aEndOffset) {
+ if (StaticPrefs::accessibility_cache_enabled_AtStartup()) {
+ // This isn't strictly related to caching, but this new text implementation
+ // is being developed to make caching feasible. We put it behind this pref
+ // to make it easy to test while it's still under development.
+ return HyperTextAccessibleBase::TextAttributes(aIncludeDefAttrs, aOffset,
+ aStartOffset, aEndOffset);
+ }
+
+ // 1. Get each attribute and its ranges one after another.
+ // 2. As we get each new attribute, we pass the current start and end offsets
+ // as in/out parameters. In other words, as attributes are collected,
+ // the attribute range itself can only stay the same or get smaller.
+
+ RefPtr<AccAttributes> attributes = new AccAttributes();
+ *aStartOffset = *aEndOffset = 0;
+ index_t offset = ConvertMagicOffset(aOffset);
+ if (!offset.IsValid() || offset > CharacterCount()) {
+ NS_ERROR("Wrong in offset!");
+ return attributes.forget();
+ }
+
+ LocalAccessible* accAtOffset = GetChildAtOffset(offset);
+ if (!accAtOffset) {
+ // Offset 0 is correct offset when accessible has empty text. Include
+ // default attributes if they were requested, otherwise return empty set.
+ if (offset == 0) {
+ if (aIncludeDefAttrs) {
+ TextAttrsMgr textAttrsMgr(this);
+ textAttrsMgr.GetAttributes(attributes);
+ }
+ }
+ return attributes.forget();
+ }
+
+ int32_t accAtOffsetIdx = accAtOffset->IndexInParent();
+ uint32_t startOffset = GetChildOffset(accAtOffsetIdx);
+ uint32_t endOffset = GetChildOffset(accAtOffsetIdx + 1);
+ int32_t offsetInAcc = offset - startOffset;
+
+ TextAttrsMgr textAttrsMgr(this, aIncludeDefAttrs, accAtOffset,
+ accAtOffsetIdx);
+ textAttrsMgr.GetAttributes(attributes, &startOffset, &endOffset);
+
+ // Compute spelling attributes on text accessible only.
+ nsIFrame* offsetFrame = accAtOffset->GetFrame();
+ if (offsetFrame && offsetFrame->IsTextFrame()) {
+ int32_t nodeOffset = 0;
+ RenderedToContentOffset(offsetFrame, offsetInAcc, &nodeOffset);
+
+ // Set 'misspelled' text attribute.
+ // FYI: Max length of text in a text node is less than INT32_MAX (see
+ // NS_MAX_TEXT_FRAGMENT_LENGTH) so that nodeOffset should always
+ // be 0 or greater.
+ MOZ_DIAGNOSTIC_ASSERT(accAtOffset->GetNode()->IsText());
+ MOZ_DIAGNOSTIC_ASSERT(nodeOffset >= 0);
+ GetSpellTextAttr(accAtOffset->GetNode(), static_cast<uint32_t>(nodeOffset),
+ &startOffset, &endOffset, attributes);
+ }
+
+ *aStartOffset = startOffset;
+ *aEndOffset = endOffset;
+ return attributes.forget();
+}
+
+already_AddRefed<AccAttributes> HyperTextAccessible::DefaultTextAttributes() {
+ RefPtr<AccAttributes> attributes = new AccAttributes();
+
+ TextAttrsMgr textAttrsMgr(this);
+ textAttrsMgr.GetAttributes(attributes);
+ return attributes.forget();
+}
+
+void HyperTextAccessible::SetMathMLXMLRoles(AccAttributes* aAttributes) {
+ // Add MathML xmlroles based on the position inside the parent.
+ LocalAccessible* parent = LocalParent();
+ if (parent) {
+ switch (parent->Role()) {
+ case roles::MATHML_CELL:
+ case roles::MATHML_ENCLOSED:
+ case roles::MATHML_ERROR:
+ case roles::MATHML_MATH:
+ case roles::MATHML_ROW:
+ case roles::MATHML_SQUARE_ROOT:
+ case roles::MATHML_STYLE:
+ if (Role() == roles::MATHML_OPERATOR) {
+ // This is an operator inside an <mrow> (or an inferred <mrow>).
+ // See http://www.w3.org/TR/MathML3/chapter3.html#presm.inferredmrow
+ // XXX We should probably do something similar for MATHML_FENCED, but
+ // operators do not appear in the accessible tree. See bug 1175747.
+ nsIMathMLFrame* mathMLFrame = do_QueryFrame(GetFrame());
+ if (mathMLFrame) {
+ nsEmbellishData embellishData;
+ mathMLFrame->GetEmbellishData(embellishData);
+ if (NS_MATHML_EMBELLISH_IS_FENCE(embellishData.flags)) {
+ if (!LocalPrevSibling()) {
+ aAttributes->SetAttribute(nsGkAtoms::xmlroles,
+ nsGkAtoms::open_fence);
+ } else if (!LocalNextSibling()) {
+ aAttributes->SetAttribute(nsGkAtoms::xmlroles,
+ nsGkAtoms::close_fence);
+ }
+ }
+ if (NS_MATHML_EMBELLISH_IS_SEPARATOR(embellishData.flags)) {
+ aAttributes->SetAttribute(nsGkAtoms::xmlroles,
+ nsGkAtoms::separator_);
+ }
+ }
+ }
+ break;
+ case roles::MATHML_FRACTION:
+ aAttributes->SetAttribute(
+ nsGkAtoms::xmlroles, IndexInParent() == 0 ? nsGkAtoms::numerator
+ : nsGkAtoms::denominator);
+ break;
+ case roles::MATHML_ROOT:
+ aAttributes->SetAttribute(
+ nsGkAtoms::xmlroles,
+ IndexInParent() == 0 ? nsGkAtoms::base : nsGkAtoms::root_index);
+ break;
+ case roles::MATHML_SUB:
+ aAttributes->SetAttribute(
+ nsGkAtoms::xmlroles,
+ IndexInParent() == 0 ? nsGkAtoms::base : nsGkAtoms::subscript);
+ break;
+ case roles::MATHML_SUP:
+ aAttributes->SetAttribute(
+ nsGkAtoms::xmlroles,
+ IndexInParent() == 0 ? nsGkAtoms::base : nsGkAtoms::superscript);
+ break;
+ case roles::MATHML_SUB_SUP: {
+ int32_t index = IndexInParent();
+ aAttributes->SetAttribute(
+ nsGkAtoms::xmlroles,
+ index == 0
+ ? nsGkAtoms::base
+ : (index == 1 ? nsGkAtoms::subscript : nsGkAtoms::superscript));
+ } break;
+ case roles::MATHML_UNDER:
+ aAttributes->SetAttribute(
+ nsGkAtoms::xmlroles,
+ IndexInParent() == 0 ? nsGkAtoms::base : nsGkAtoms::underscript);
+ break;
+ case roles::MATHML_OVER:
+ aAttributes->SetAttribute(
+ nsGkAtoms::xmlroles,
+ IndexInParent() == 0 ? nsGkAtoms::base : nsGkAtoms::overscript);
+ break;
+ case roles::MATHML_UNDER_OVER: {
+ int32_t index = IndexInParent();
+ aAttributes->SetAttribute(nsGkAtoms::xmlroles,
+ index == 0
+ ? nsGkAtoms::base
+ : (index == 1 ? nsGkAtoms::underscript
+ : nsGkAtoms::overscript));
+ } break;
+ case roles::MATHML_MULTISCRIPTS: {
+ // Get the <multiscripts> base.
+ nsIContent* child;
+ bool baseFound = false;
+ for (child = parent->GetContent()->GetFirstChild(); child;
+ child = child->GetNextSibling()) {
+ if (child->IsMathMLElement()) {
+ baseFound = true;
+ break;
+ }
+ }
+ if (baseFound) {
+ nsIContent* content = GetContent();
+ if (child == content) {
+ // We are the base.
+ aAttributes->SetAttribute(nsGkAtoms::xmlroles, nsGkAtoms::base);
+ } else {
+ // Browse the list of scripts to find us and determine our type.
+ bool postscript = true;
+ bool subscript = true;
+ for (child = child->GetNextSibling(); child;
+ child = child->GetNextSibling()) {
+ if (!child->IsMathMLElement()) continue;
+ if (child->IsMathMLElement(nsGkAtoms::mprescripts_)) {
+ postscript = false;
+ subscript = true;
+ continue;
+ }
+ if (child == content) {
+ if (postscript) {
+ aAttributes->SetAttribute(nsGkAtoms::xmlroles,
+ subscript ? nsGkAtoms::subscript
+ : nsGkAtoms::superscript);
+ } else {
+ aAttributes->SetAttribute(nsGkAtoms::xmlroles,
+ subscript
+ ? nsGkAtoms::presubscript
+ : nsGkAtoms::presuperscript);
+ }
+ break;
+ }
+ subscript = !subscript;
+ }
+ }
+ }
+ } break;
+ default:
+ break;
+ }
+ }
+}
+
+already_AddRefed<AccAttributes> HyperTextAccessible::NativeAttributes() {
+ RefPtr<AccAttributes> attributes = AccessibleWrap::NativeAttributes();
+
+ // 'formatting' attribute is deprecated, 'display' attribute should be
+ // instead.
+ nsIFrame* frame = GetFrame();
+ if (frame && frame->IsBlockFrame()) {
+ attributes->SetAttribute(nsGkAtoms::formatting, nsGkAtoms::block);
+ }
+
+ if (FocusMgr()->IsFocused(this)) {
+ int32_t lineNumber = CaretLineNumber();
+ if (lineNumber >= 1) {
+ attributes->SetAttribute(nsGkAtoms::lineNumber, lineNumber);
+ }
+ }
+
+ if (HasOwnContent()) {
+ GetAccService()->MarkupAttributes(this, attributes);
+ if (mContent->IsMathMLElement()) SetMathMLXMLRoles(attributes);
+ }
+
+ return attributes.forget();
+}
+
+int32_t HyperTextAccessible::OffsetAtPoint(int32_t aX, int32_t aY,
+ uint32_t aCoordType) {
+ nsIFrame* hyperFrame = GetFrame();
+ if (!hyperFrame) return -1;
+
+ LayoutDeviceIntPoint coords =
+ nsAccUtils::ConvertToScreenCoords(aX, aY, aCoordType, this);
+
+ nsPresContext* presContext = mDoc->PresContext();
+ nsPoint coordsInAppUnits = LayoutDeviceIntPoint::ToAppUnits(
+ coords, presContext->AppUnitsPerDevPixel());
+
+ nsRect frameScreenRect = hyperFrame->GetScreenRectInAppUnits();
+ if (!frameScreenRect.Contains(coordsInAppUnits.x, coordsInAppUnits.y)) {
+ return -1; // Not found
+ }
+
+ nsPoint pointInHyperText(coordsInAppUnits.x - frameScreenRect.X(),
+ coordsInAppUnits.y - frameScreenRect.Y());
+
+ // Go through the frames to check if each one has the point.
+ // When one does, add up the character offsets until we have a match
+
+ // We have an point in an accessible child of this, now we need to add up the
+ // offsets before it to what we already have
+ int32_t offset = 0;
+ uint32_t childCount = ChildCount();
+ for (uint32_t childIdx = 0; childIdx < childCount; childIdx++) {
+ LocalAccessible* childAcc = mChildren[childIdx];
+
+ nsIFrame* primaryFrame = childAcc->GetFrame();
+ NS_ENSURE_TRUE(primaryFrame, -1);
+
+ nsIFrame* frame = primaryFrame;
+ while (frame) {
+ nsIContent* content = frame->GetContent();
+ NS_ENSURE_TRUE(content, -1);
+ nsPoint pointInFrame = pointInHyperText - frame->GetOffsetTo(hyperFrame);
+ nsSize frameSize = frame->GetSize();
+ if (pointInFrame.x < frameSize.width &&
+ pointInFrame.y < frameSize.height) {
+ // Finished
+ if (frame->IsTextFrame()) {
+ nsIFrame::ContentOffsets contentOffsets =
+ frame->GetContentOffsetsFromPointExternal(
+ pointInFrame, nsIFrame::IGNORE_SELECTION_STYLE);
+ if (contentOffsets.IsNull() || contentOffsets.content != content) {
+ return -1; // Not found
+ }
+ uint32_t addToOffset;
+ nsresult rv = ContentToRenderedOffset(
+ primaryFrame, contentOffsets.offset, &addToOffset);
+ NS_ENSURE_SUCCESS(rv, -1);
+ offset += addToOffset;
+ }
+ return offset;
+ }
+ frame = frame->GetNextContinuation();
+ }
+
+ offset += nsAccUtils::TextLength(childAcc);
+ }
+
+ return -1; // Not found
+}
+
+LayoutDeviceIntRect HyperTextAccessible::TextBounds(int32_t aStartOffset,
+ int32_t aEndOffset,
+ uint32_t aCoordType) {
+ index_t startOffset = ConvertMagicOffset(aStartOffset);
+ index_t endOffset = ConvertMagicOffset(aEndOffset);
+ if (!startOffset.IsValid() || !endOffset.IsValid() ||
+ startOffset > endOffset || endOffset > CharacterCount()) {
+ NS_ERROR("Wrong in offset");
+ return LayoutDeviceIntRect();
+ }
+
+ if (CharacterCount() == 0) {
+ nsPresContext* presContext = mDoc->PresContext();
+ // Empty content, use our own bound to at least get x,y coordinates
+ nsIFrame* frame = GetFrame();
+ if (!frame) {
+ return LayoutDeviceIntRect();
+ }
+ return LayoutDeviceIntRect::FromAppUnitsToNearest(
+ frame->GetScreenRectInAppUnits(), presContext->AppUnitsPerDevPixel());
+ }
+
+ int32_t childIdx = GetChildIndexAtOffset(startOffset);
+ if (childIdx == -1) return LayoutDeviceIntRect();
+
+ LayoutDeviceIntRect bounds;
+ int32_t prevOffset = GetChildOffset(childIdx);
+ int32_t offset1 = startOffset - prevOffset;
+
+ while (childIdx < static_cast<int32_t>(ChildCount())) {
+ nsIFrame* frame = LocalChildAt(childIdx++)->GetFrame();
+ if (!frame) {
+ MOZ_ASSERT_UNREACHABLE("No frame for a child!");
+ continue;
+ }
+
+ int32_t nextOffset = GetChildOffset(childIdx);
+ if (nextOffset >= static_cast<int32_t>(endOffset)) {
+ bounds.UnionRect(
+ bounds, GetBoundsInFrame(frame, offset1, endOffset - prevOffset));
+ break;
+ }
+
+ bounds.UnionRect(bounds,
+ GetBoundsInFrame(frame, offset1, nextOffset - prevOffset));
+
+ prevOffset = nextOffset;
+ offset1 = 0;
+ }
+
+ // This document may have a resolution set, we will need to multiply
+ // the document-relative coordinates by that value and re-apply the doc's
+ // screen coordinates.
+ nsPresContext* presContext = mDoc->PresContext();
+ nsIFrame* rootFrame = presContext->PresShell()->GetRootFrame();
+ LayoutDeviceIntRect orgRectPixels =
+ LayoutDeviceIntRect::FromAppUnitsToNearest(
+ rootFrame->GetScreenRectInAppUnits(),
+ presContext->AppUnitsPerDevPixel());
+ bounds.MoveBy(-orgRectPixels.X(), -orgRectPixels.Y());
+ bounds.ScaleRoundOut(presContext->PresShell()->GetResolution());
+ bounds.MoveBy(orgRectPixels.X(), orgRectPixels.Y());
+
+ auto boundsX = bounds.X();
+ auto boundsY = bounds.Y();
+ nsAccUtils::ConvertScreenCoordsTo(&boundsX, &boundsY, aCoordType, this);
+ bounds.MoveTo(boundsX, boundsY);
+ return bounds;
+}
+
+already_AddRefed<EditorBase> HyperTextAccessible::GetEditor() const {
+ if (!mContent->HasFlag(NODE_IS_EDITABLE)) {
+ // If we're inside an editable container, then return that container's
+ // editor
+ LocalAccessible* ancestor = LocalParent();
+ while (ancestor) {
+ HyperTextAccessible* hyperText = ancestor->AsHyperText();
+ if (hyperText) {
+ // Recursion will stop at container doc because it has its own impl
+ // of GetEditor()
+ return hyperText->GetEditor();
+ }
+
+ ancestor = ancestor->LocalParent();
+ }
+
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIDocShell> docShell = nsCoreUtils::GetDocShellFor(mContent);
+ nsCOMPtr<nsIEditingSession> editingSession;
+ docShell->GetEditingSession(getter_AddRefs(editingSession));
+ if (!editingSession) return nullptr; // No editing session interface
+
+ dom::Document* docNode = mDoc->DocumentNode();
+ RefPtr<HTMLEditor> htmlEditor =
+ editingSession->GetHTMLEditorForWindow(docNode->GetWindow());
+ return htmlEditor.forget();
+}
+
+/**
+ * =================== Caret & Selection ======================
+ */
+
+nsresult HyperTextAccessible::SetSelectionRange(int32_t aStartPos,
+ int32_t aEndPos) {
+ // Before setting the selection range, we need to ensure that the editor
+ // is initialized. (See bug 804927.)
+ // Otherwise, it's possible that lazy editor initialization will override
+ // the selection we set here and leave the caret at the end of the text.
+ // By calling GetEditor here, we ensure that editor initialization is
+ // completed before we set the selection.
+ RefPtr<EditorBase> editorBase = GetEditor();
+
+ bool isFocusable = InteractiveState() & states::FOCUSABLE;
+
+ // If accessible is focusable then focus it before setting the selection to
+ // neglect control's selection changes on focus if any (for example, inputs
+ // that do select all on focus).
+ // some input controls
+ if (isFocusable) TakeFocus();
+
+ RefPtr<dom::Selection> domSel = DOMSelection();
+ NS_ENSURE_STATE(domSel);
+
+ // Set up the selection.
+ for (const uint32_t idx : Reversed(IntegerRange(1u, domSel->RangeCount()))) {
+ MOZ_ASSERT(domSel->RangeCount() == idx + 1);
+ RefPtr<nsRange> range{domSel->GetRangeAt(idx)};
+ if (!range) {
+ break; // The range count has been changed by somebody else.
+ }
+ domSel->RemoveRangeAndUnselectFramesAndNotifyListeners(*range,
+ IgnoreErrors());
+ }
+ SetSelectionBoundsAt(0, aStartPos, aEndPos);
+
+ // Make sure it is visible
+ domSel->ScrollIntoView(nsISelectionController::SELECTION_FOCUS_REGION,
+ ScrollAxis(), ScrollAxis(),
+ dom::Selection::SCROLL_FOR_CARET_MOVE |
+ dom::Selection::SCROLL_OVERFLOW_HIDDEN);
+
+ // When selection is done, move the focus to the selection if accessible is
+ // not focusable. That happens when selection is set within hypertext
+ // accessible.
+ if (isFocusable) return NS_OK;
+
+ nsFocusManager* DOMFocusManager = nsFocusManager::GetFocusManager();
+ if (DOMFocusManager) {
+ NS_ENSURE_TRUE(mDoc, NS_ERROR_FAILURE);
+ dom::Document* docNode = mDoc->DocumentNode();
+ NS_ENSURE_TRUE(docNode, NS_ERROR_FAILURE);
+ nsCOMPtr<nsPIDOMWindowOuter> window = docNode->GetWindow();
+ RefPtr<dom::Element> result;
+ DOMFocusManager->MoveFocus(
+ window, nullptr, nsIFocusManager::MOVEFOCUS_CARET,
+ nsIFocusManager::FLAG_BYMOVEFOCUS, getter_AddRefs(result));
+ }
+
+ return NS_OK;
+}
+
+int32_t HyperTextAccessible::CaretOffset() const {
+ // Not focused focusable accessible except document accessible doesn't have
+ // a caret.
+ if (!IsDoc() && !FocusMgr()->IsFocused(this) &&
+ (InteractiveState() & states::FOCUSABLE)) {
+ return -1;
+ }
+
+ // Check cached value.
+ int32_t caretOffset = -1;
+ HyperTextAccessible* text = SelectionMgr()->AccessibleWithCaret(&caretOffset);
+
+ // Use cached value if it corresponds to this accessible.
+ if (caretOffset != -1) {
+ if (text == this) return caretOffset;
+
+ nsINode* textNode = text->GetNode();
+ // Ignore offset if cached accessible isn't a text leaf.
+ if (nsCoreUtils::IsAncestorOf(GetNode(), textNode)) {
+ return TransformOffset(text, textNode->IsText() ? caretOffset : 0, false);
+ }
+ }
+
+ // No caret if the focused node is not inside this DOM node and this DOM node
+ // is not inside of focused node.
+ FocusManager::FocusDisposition focusDisp =
+ FocusMgr()->IsInOrContainsFocus(this);
+ if (focusDisp == FocusManager::eNone) return -1;
+
+ // Turn the focus node and offset of the selection into caret hypretext
+ // offset.
+ dom::Selection* domSel = DOMSelection();
+ NS_ENSURE_TRUE(domSel, -1);
+
+ nsINode* focusNode = domSel->GetFocusNode();
+ uint32_t focusOffset = domSel->FocusOffset();
+
+ // No caret if this DOM node is inside of focused node but the selection's
+ // focus point is not inside of this DOM node.
+ if (focusDisp == FocusManager::eContainedByFocus) {
+ nsINode* resultNode =
+ nsCoreUtils::GetDOMNodeFromDOMPoint(focusNode, focusOffset);
+
+ nsINode* thisNode = GetNode();
+ if (resultNode != thisNode &&
+ !nsCoreUtils::IsAncestorOf(thisNode, resultNode)) {
+ return -1;
+ }
+ }
+
+ return DOMPointToOffset(focusNode, focusOffset);
+}
+
+int32_t HyperTextAccessible::CaretLineNumber() {
+ // Provide the line number for the caret, relative to the
+ // currently focused node. Use a 1-based index
+ RefPtr<nsFrameSelection> frameSelection = FrameSelection();
+ if (!frameSelection) return -1;
+
+ dom::Selection* domSel = frameSelection->GetSelection(SelectionType::eNormal);
+ if (!domSel) return -1;
+
+ nsINode* caretNode = domSel->GetFocusNode();
+ if (!caretNode || !caretNode->IsContent()) return -1;
+
+ nsIContent* caretContent = caretNode->AsContent();
+ if (!nsCoreUtils::IsAncestorOf(GetNode(), caretContent)) return -1;
+
+ int32_t returnOffsetUnused;
+ uint32_t caretOffset = domSel->FocusOffset();
+ CaretAssociationHint hint = frameSelection->GetHint();
+ nsIFrame* caretFrame = frameSelection->GetFrameForNodeOffset(
+ caretContent, caretOffset, hint, &returnOffsetUnused);
+ NS_ENSURE_TRUE(caretFrame, -1);
+
+ AutoAssertNoDomMutations guard; // The nsILineIterators below will break if
+ // the DOM is modified while they're in use!
+ int32_t lineNumber = 1;
+ nsILineIterator* lineIterForCaret = nullptr;
+ nsIContent* hyperTextContent = IsContent() ? mContent.get() : nullptr;
+ while (caretFrame) {
+ if (hyperTextContent == caretFrame->GetContent()) {
+ return lineNumber; // Must be in a single line hyper text, there is no
+ // line iterator
+ }
+ nsContainerFrame* parentFrame = caretFrame->GetParent();
+ if (!parentFrame) break;
+
+ // Add lines for the sibling frames before the caret
+ nsIFrame* sibling = parentFrame->PrincipalChildList().FirstChild();
+ while (sibling && sibling != caretFrame) {
+ nsILineIterator* lineIterForSibling = sibling->GetLineIterator();
+ if (lineIterForSibling) {
+ // For the frames before that grab all the lines
+ int32_t addLines = lineIterForSibling->GetNumLines();
+ lineNumber += addLines;
+ }
+ sibling = sibling->GetNextSibling();
+ }
+
+ // Get the line number relative to the container with lines
+ if (!lineIterForCaret) { // Add the caret line just once
+ lineIterForCaret = parentFrame->GetLineIterator();
+ if (lineIterForCaret) {
+ // Ancestor of caret
+ int32_t addLines = lineIterForCaret->FindLineContaining(caretFrame);
+ lineNumber += addLines;
+ }
+ }
+
+ caretFrame = parentFrame;
+ }
+
+ MOZ_ASSERT_UNREACHABLE(
+ "DOM ancestry had this hypertext but frame ancestry didn't");
+ return lineNumber;
+}
+
+LayoutDeviceIntRect HyperTextAccessible::GetCaretRect(nsIWidget** aWidget) {
+ *aWidget = nullptr;
+
+ RefPtr<nsCaret> caret = mDoc->PresShellPtr()->GetCaret();
+ NS_ENSURE_TRUE(caret, LayoutDeviceIntRect());
+
+ bool isVisible = caret->IsVisible();
+ if (!isVisible) return LayoutDeviceIntRect();
+
+ nsRect rect;
+ nsIFrame* frame = caret->GetGeometry(&rect);
+ if (!frame || rect.IsEmpty()) return LayoutDeviceIntRect();
+
+ nsPoint offset;
+ // Offset from widget origin to the frame origin, which includes chrome
+ // on the widget.
+ *aWidget = frame->GetNearestWidget(offset);
+ NS_ENSURE_TRUE(*aWidget, LayoutDeviceIntRect());
+ rect.MoveBy(offset);
+
+ LayoutDeviceIntRect caretRect = LayoutDeviceIntRect::FromUnknownRect(
+ rect.ToOutsidePixels(frame->PresContext()->AppUnitsPerDevPixel()));
+ // clang-format off
+ // ((content screen origin) - (content offset in the widget)) = widget origin on the screen
+ // clang-format on
+ caretRect.MoveBy((*aWidget)->WidgetToScreenOffset() -
+ (*aWidget)->GetClientOffset());
+
+ // Correct for character size, so that caret always matches the size of
+ // the character. This is important for font size transitions, and is
+ // necessary because the Gecko caret uses the previous character's size as
+ // the user moves forward in the text by character.
+ int32_t caretOffset = CaretOffset();
+ if (NS_WARN_IF(caretOffset == -1)) {
+ // The caret offset will be -1 if this Accessible isn't focused. Note that
+ // the DOM node contaning the caret might be focused, but the Accessible
+ // might not be; e.g. due to an autocomplete popup suggestion having a11y
+ // focus.
+ return LayoutDeviceIntRect();
+ }
+ LayoutDeviceIntRect charRect = CharBounds(
+ caretOffset, nsIAccessibleCoordinateType::COORDTYPE_SCREEN_RELATIVE);
+ if (!charRect.IsEmpty()) {
+ caretRect.SetTopEdge(charRect.Y());
+ }
+ return caretRect;
+}
+
+void HyperTextAccessible::GetSelectionDOMRanges(SelectionType aSelectionType,
+ nsTArray<nsRange*>* aRanges) {
+ // Ignore selection if it is not visible.
+ RefPtr<nsFrameSelection> frameSelection = FrameSelection();
+ if (!frameSelection || frameSelection->GetDisplaySelection() <=
+ nsISelectionController::SELECTION_HIDDEN) {
+ return;
+ }
+
+ dom::Selection* domSel = frameSelection->GetSelection(aSelectionType);
+ if (!domSel) return;
+
+ nsINode* startNode = GetNode();
+
+ RefPtr<EditorBase> editorBase = GetEditor();
+ if (editorBase) {
+ startNode = editorBase->GetRoot();
+ }
+
+ if (!startNode) return;
+
+ uint32_t childCount = startNode->GetChildCount();
+ nsresult rv = domSel->GetRangesForIntervalArray(startNode, 0, startNode,
+ childCount, true, aRanges);
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ // Remove collapsed ranges
+ aRanges->RemoveElementsBy(
+ [](const auto& range) { return range->Collapsed(); });
+}
+
+int32_t HyperTextAccessible::SelectionCount() {
+ nsTArray<nsRange*> ranges;
+ GetSelectionDOMRanges(SelectionType::eNormal, &ranges);
+ return ranges.Length();
+}
+
+bool HyperTextAccessible::SelectionBoundsAt(int32_t aSelectionNum,
+ int32_t* aStartOffset,
+ int32_t* aEndOffset) {
+ *aStartOffset = *aEndOffset = 0;
+
+ nsTArray<nsRange*> ranges;
+ GetSelectionDOMRanges(SelectionType::eNormal, &ranges);
+
+ uint32_t rangeCount = ranges.Length();
+ if (aSelectionNum < 0 || aSelectionNum >= static_cast<int32_t>(rangeCount)) {
+ return false;
+ }
+
+ nsRange* range = ranges[aSelectionNum];
+
+ // Get start and end points.
+ nsINode* startNode = range->GetStartContainer();
+ nsINode* endNode = range->GetEndContainer();
+ uint32_t startOffset = range->StartOffset();
+ uint32_t endOffset = range->EndOffset();
+
+ // Make sure start is before end, by swapping DOM points. This occurs when
+ // the user selects backwards in the text.
+ const Maybe<int32_t> order =
+ nsContentUtils::ComparePoints(endNode, endOffset, startNode, startOffset);
+
+ if (!order) {
+ MOZ_ASSERT_UNREACHABLE();
+ return false;
+ }
+
+ if (*order < 0) {
+ std::swap(startNode, endNode);
+ std::swap(startOffset, endOffset);
+ }
+
+ if (!startNode->IsInclusiveDescendantOf(mContent)) {
+ *aStartOffset = 0;
+ } else {
+ *aStartOffset =
+ DOMPointToOffset(startNode, AssertedCast<int32_t>(startOffset));
+ }
+
+ if (!endNode->IsInclusiveDescendantOf(mContent)) {
+ *aEndOffset = CharacterCount();
+ } else {
+ *aEndOffset =
+ DOMPointToOffset(endNode, AssertedCast<int32_t>(endOffset), true);
+ }
+ return true;
+}
+
+bool HyperTextAccessible::SetSelectionBoundsAt(int32_t aSelectionNum,
+ int32_t aStartOffset,
+ int32_t aEndOffset) {
+ index_t startOffset = ConvertMagicOffset(aStartOffset);
+ index_t endOffset = ConvertMagicOffset(aEndOffset);
+ if (!startOffset.IsValid() || !endOffset.IsValid() ||
+ std::max(startOffset, endOffset) > CharacterCount()) {
+ NS_ERROR("Wrong in offset");
+ return false;
+ }
+
+ TextRange range(this, this, startOffset, this, endOffset);
+ return range.SetSelectionAt(aSelectionNum);
+}
+
+bool HyperTextAccessible::RemoveFromSelection(int32_t aSelectionNum) {
+ RefPtr<dom::Selection> domSel = DOMSelection();
+ if (!domSel) return false;
+
+ if (aSelectionNum < 0 ||
+ aSelectionNum >= static_cast<int32_t>(domSel->RangeCount())) {
+ return false;
+ }
+
+ const RefPtr<nsRange> range{
+ domSel->GetRangeAt(static_cast<uint32_t>(aSelectionNum))};
+ domSel->RemoveRangeAndUnselectFramesAndNotifyListeners(*range,
+ IgnoreErrors());
+ return true;
+}
+
+void HyperTextAccessible::ScrollSubstringTo(int32_t aStartOffset,
+ int32_t aEndOffset,
+ uint32_t aScrollType) {
+ TextRange range(this, this, aStartOffset, this, aEndOffset);
+ range.ScrollIntoView(aScrollType);
+}
+
+void HyperTextAccessible::ScrollSubstringToPoint(int32_t aStartOffset,
+ int32_t aEndOffset,
+ uint32_t aCoordinateType,
+ int32_t aX, int32_t aY) {
+ nsIFrame* frame = GetFrame();
+ if (!frame) return;
+
+ LayoutDeviceIntPoint coords =
+ nsAccUtils::ConvertToScreenCoords(aX, aY, aCoordinateType, this);
+
+ RefPtr<nsRange> domRange = nsRange::Create(mContent);
+ TextRange range(this, this, aStartOffset, this, aEndOffset);
+ if (!range.AssignDOMRange(domRange)) {
+ return;
+ }
+
+ nsPresContext* presContext = frame->PresContext();
+ nsPoint coordsInAppUnits = LayoutDeviceIntPoint::ToAppUnits(
+ coords, presContext->AppUnitsPerDevPixel());
+
+ bool initialScrolled = false;
+ nsIFrame* parentFrame = frame;
+ while ((parentFrame = parentFrame->GetParent())) {
+ nsIScrollableFrame* scrollableFrame = do_QueryFrame(parentFrame);
+ if (scrollableFrame) {
+ if (!initialScrolled) {
+ // Scroll substring to the given point. Turn the point into percents
+ // relative scrollable area to use nsCoreUtils::ScrollSubstringTo.
+ nsRect frameRect = parentFrame->GetScreenRectInAppUnits();
+ nscoord offsetPointX = coordsInAppUnits.x - frameRect.X();
+ nscoord offsetPointY = coordsInAppUnits.y - frameRect.Y();
+
+ nsSize size(parentFrame->GetSize());
+
+ // avoid divide by zero
+ size.width = size.width ? size.width : 1;
+ size.height = size.height ? size.height : 1;
+
+ int16_t hPercent = offsetPointX * 100 / size.width;
+ int16_t vPercent = offsetPointY * 100 / size.height;
+
+ nsresult rv = nsCoreUtils::ScrollSubstringTo(
+ frame, domRange,
+ ScrollAxis(WhereToScroll(vPercent), WhenToScroll::Always),
+ ScrollAxis(WhereToScroll(hPercent), WhenToScroll::Always));
+ if (NS_FAILED(rv)) return;
+
+ initialScrolled = true;
+ } else {
+ // Substring was scrolled to the given point already inside its closest
+ // scrollable area. If there are nested scrollable areas then make
+ // sure we scroll lower areas to the given point inside currently
+ // traversed scrollable area.
+ nsCoreUtils::ScrollFrameToPoint(parentFrame, frame, coords);
+ }
+ }
+ frame = parentFrame;
+ }
+}
+
+void HyperTextAccessible::EnclosingRange(a11y::TextRange& aRange) const {
+ if (IsTextField()) {
+ aRange.Set(mDoc, const_cast<HyperTextAccessible*>(this), 0,
+ const_cast<HyperTextAccessible*>(this), CharacterCount());
+ } else {
+ aRange.Set(mDoc, mDoc, 0, mDoc, mDoc->CharacterCount());
+ }
+}
+
+void HyperTextAccessible::SelectionRanges(
+ nsTArray<a11y::TextRange>* aRanges) const {
+ dom::Selection* sel = DOMSelection();
+ if (!sel) {
+ return;
+ }
+
+ TextRange::TextRangesFromSelection(sel, aRanges);
+}
+
+void HyperTextAccessible::VisibleRanges(
+ nsTArray<a11y::TextRange>* aRanges) const {}
+
+void HyperTextAccessible::RangeByChild(LocalAccessible* aChild,
+ a11y::TextRange& aRange) const {
+ HyperTextAccessible* ht = aChild->AsHyperText();
+ if (ht) {
+ aRange.Set(mDoc, ht, 0, ht, ht->CharacterCount());
+ return;
+ }
+
+ LocalAccessible* child = aChild;
+ LocalAccessible* parent = nullptr;
+ while ((parent = child->LocalParent()) && !(ht = parent->AsHyperText())) {
+ child = parent;
+ }
+
+ // If no text then return collapsed text range, otherwise return a range
+ // containing the text enclosed by the given child.
+ if (ht) {
+ int32_t childIdx = child->IndexInParent();
+ int32_t startOffset = ht->GetChildOffset(childIdx);
+ int32_t endOffset =
+ child->IsTextLeaf() ? ht->GetChildOffset(childIdx + 1) : startOffset;
+ aRange.Set(mDoc, ht, startOffset, ht, endOffset);
+ }
+}
+
+void HyperTextAccessible::RangeAtPoint(int32_t aX, int32_t aY,
+ a11y::TextRange& aRange) const {
+ LocalAccessible* child =
+ mDoc->LocalChildAtPoint(aX, aY, EWhichChildAtPoint::DeepestChild);
+ if (!child) return;
+
+ LocalAccessible* parent = nullptr;
+ while ((parent = child->LocalParent()) && !parent->IsHyperText()) {
+ child = parent;
+ }
+
+ // Return collapsed text range for the point.
+ if (parent) {
+ HyperTextAccessible* ht = parent->AsHyperText();
+ int32_t offset = ht->GetChildOffset(child);
+ aRange.Set(mDoc, ht, offset, ht, offset);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// LocalAccessible public
+
+// LocalAccessible protected
+ENameValueFlag HyperTextAccessible::NativeName(nsString& aName) const {
+ // Check @alt attribute for invalid img elements.
+ if (mContent->IsHTMLElement(nsGkAtoms::img)) {
+ mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::alt, aName);
+ if (!aName.IsEmpty()) return eNameOK;
+ }
+
+ ENameValueFlag nameFlag = AccessibleWrap::NativeName(aName);
+ if (!aName.IsEmpty()) return nameFlag;
+
+ // Get name from title attribute for HTML abbr and acronym elements making it
+ // a valid name from markup. Otherwise their name isn't picked up by recursive
+ // name computation algorithm. See NS_OK_NAME_FROM_TOOLTIP.
+ if (IsAbbreviation() && mContent->AsElement()->GetAttr(
+ kNameSpaceID_None, nsGkAtoms::title, aName)) {
+ aName.CompressWhitespace();
+ }
+
+ return eNameOK;
+}
+
+void HyperTextAccessible::Shutdown() {
+ mOffsets.Clear();
+ AccessibleWrap::Shutdown();
+}
+
+bool HyperTextAccessible::RemoveChild(LocalAccessible* aAccessible) {
+ const int32_t childIndex = aAccessible->IndexInParent();
+ if (childIndex < static_cast<int32_t>(mOffsets.Length())) {
+ mOffsets.RemoveLastElements(mOffsets.Length() - childIndex);
+ }
+
+ return AccessibleWrap::RemoveChild(aAccessible);
+}
+
+bool HyperTextAccessible::InsertChildAt(uint32_t aIndex,
+ LocalAccessible* aChild) {
+ if (aIndex < mOffsets.Length()) {
+ mOffsets.RemoveLastElements(mOffsets.Length() - aIndex);
+ }
+
+ return AccessibleWrap::InsertChildAt(aIndex, aChild);
+}
+
+Relation HyperTextAccessible::RelationByType(RelationType aType) const {
+ Relation rel = LocalAccessible::RelationByType(aType);
+
+ switch (aType) {
+ case RelationType::NODE_CHILD_OF:
+ if (HasOwnContent() && mContent->IsMathMLElement()) {
+ LocalAccessible* parent = LocalParent();
+ if (parent) {
+ nsIContent* parentContent = parent->GetContent();
+ if (parentContent &&
+ parentContent->IsMathMLElement(nsGkAtoms::mroot_)) {
+ // Add a relation pointing to the parent <mroot>.
+ rel.AppendTarget(parent);
+ }
+ }
+ }
+ break;
+ case RelationType::NODE_PARENT_OF:
+ if (HasOwnContent() && mContent->IsMathMLElement(nsGkAtoms::mroot_)) {
+ LocalAccessible* base = LocalChildAt(0);
+ LocalAccessible* index = LocalChildAt(1);
+ if (base && index) {
+ // Append the <mroot> children in the order index, base.
+ rel.AppendTarget(index);
+ rel.AppendTarget(base);
+ }
+ }
+ break;
+ default:
+ break;
+ }
+
+ return rel;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// HyperTextAccessible public static
+
+nsresult HyperTextAccessible::ContentToRenderedOffset(
+ nsIFrame* aFrame, int32_t aContentOffset, uint32_t* aRenderedOffset) const {
+ if (!aFrame) {
+ // Current frame not rendered -- this can happen if text is set on
+ // something with display: none
+ *aRenderedOffset = 0;
+ return NS_OK;
+ }
+
+ if (IsTextField()) {
+ *aRenderedOffset = aContentOffset;
+ return NS_OK;
+ }
+
+ NS_ASSERTION(aFrame->IsTextFrame(), "Need text frame for offset conversion");
+ NS_ASSERTION(aFrame->GetPrevContinuation() == nullptr,
+ "Call on primary frame only");
+
+ nsIFrame::RenderedText text =
+ aFrame->GetRenderedText(aContentOffset, aContentOffset + 1,
+ nsIFrame::TextOffsetType::OffsetsInContentText,
+ nsIFrame::TrailingWhitespace::DontTrim);
+ *aRenderedOffset = text.mOffsetWithinNodeRenderedText;
+
+ return NS_OK;
+}
+
+nsresult HyperTextAccessible::RenderedToContentOffset(
+ nsIFrame* aFrame, uint32_t aRenderedOffset, int32_t* aContentOffset) const {
+ if (IsTextField()) {
+ *aContentOffset = aRenderedOffset;
+ return NS_OK;
+ }
+
+ *aContentOffset = 0;
+ NS_ENSURE_TRUE(aFrame, NS_ERROR_FAILURE);
+
+ NS_ASSERTION(aFrame->IsTextFrame(), "Need text frame for offset conversion");
+ NS_ASSERTION(aFrame->GetPrevContinuation() == nullptr,
+ "Call on primary frame only");
+
+ nsIFrame::RenderedText text =
+ aFrame->GetRenderedText(aRenderedOffset, aRenderedOffset + 1,
+ nsIFrame::TextOffsetType::OffsetsInRenderedText,
+ nsIFrame::TrailingWhitespace::DontTrim);
+ *aContentOffset = text.mOffsetWithinNodeText;
+
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// HyperTextAccessible protected
+
+nsresult HyperTextAccessible::GetDOMPointByFrameOffset(
+ nsIFrame* aFrame, int32_t aOffset, LocalAccessible* aAccessible,
+ DOMPoint* aPoint) {
+ NS_ENSURE_ARG(aAccessible);
+
+ if (!aFrame) {
+ // If the given frame is null then set offset after the DOM node of the
+ // given accessible.
+ NS_ASSERTION(!aAccessible->IsDoc(),
+ "Shouldn't be called on document accessible!");
+
+ nsIContent* content = aAccessible->GetContent();
+ NS_ASSERTION(content, "Shouldn't operate on defunct accessible!");
+
+ nsIContent* parent = content->GetParent();
+
+ aPoint->idx = parent->ComputeIndexOf_Deprecated(content) + 1;
+ aPoint->node = parent;
+
+ } else if (aFrame->IsTextFrame()) {
+ nsIContent* content = aFrame->GetContent();
+ NS_ENSURE_STATE(content);
+
+ nsIFrame* primaryFrame = content->GetPrimaryFrame();
+ nsresult rv =
+ RenderedToContentOffset(primaryFrame, aOffset, &(aPoint->idx));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ aPoint->node = content;
+
+ } else {
+ nsIContent* content = aFrame->GetContent();
+ NS_ENSURE_STATE(content);
+
+ nsIContent* parent = content->GetParent();
+ NS_ENSURE_STATE(parent);
+
+ aPoint->idx = parent->ComputeIndexOf_Deprecated(content);
+ aPoint->node = parent;
+ }
+
+ return NS_OK;
+}
+
+// HyperTextAccessible
+void HyperTextAccessible::GetSpellTextAttr(nsINode* aNode, uint32_t aNodeOffset,
+ uint32_t* aStartOffset,
+ uint32_t* aEndOffset,
+ AccAttributes* aAttributes) {
+ RefPtr<nsFrameSelection> fs = FrameSelection();
+ if (!fs) return;
+
+ dom::Selection* domSel = fs->GetSelection(SelectionType::eSpellCheck);
+ if (!domSel) return;
+
+ const uint32_t rangeCount = domSel->RangeCount();
+ if (!rangeCount) {
+ return;
+ }
+
+ uint32_t startOffset = 0, endOffset = 0;
+ for (const uint32_t idx : IntegerRange(rangeCount)) {
+ MOZ_ASSERT(domSel->RangeCount() == rangeCount);
+ const nsRange* range = domSel->GetRangeAt(idx);
+ MOZ_ASSERT(range);
+ if (range->Collapsed()) continue;
+
+ // See if the point comes after the range in which case we must continue in
+ // case there is another range after this one.
+ nsINode* endNode = range->GetEndContainer();
+ uint32_t endNodeOffset = range->EndOffset();
+ Maybe<int32_t> order = nsContentUtils::ComparePoints(
+ aNode, aNodeOffset, endNode, endNodeOffset);
+ if (NS_WARN_IF(!order)) {
+ continue;
+ }
+
+ if (*order >= 0) {
+ continue;
+ }
+
+ // At this point our point is either in this range or before it but after
+ // the previous range. So we check to see if the range starts before the
+ // point in which case the point is in the missspelled range, otherwise it
+ // must be before the range and after the previous one if any.
+ nsINode* startNode = range->GetStartContainer();
+ int32_t startNodeOffset = range->StartOffset();
+ order = nsContentUtils::ComparePoints(startNode, startNodeOffset, aNode,
+ aNodeOffset);
+ if (!order) {
+ // As (`aNode`, `aNodeOffset`) is comparable to the end of the range, it
+ // should also be comparable to the range's start. Returning here
+ // prevents crashes in release builds.
+ MOZ_ASSERT_UNREACHABLE();
+ return;
+ }
+
+ if (*order <= 0) {
+ startOffset = DOMPointToOffset(startNode, startNodeOffset);
+
+ endOffset = DOMPointToOffset(endNode, endNodeOffset);
+
+ if (startOffset > *aStartOffset) *aStartOffset = startOffset;
+
+ if (endOffset < *aEndOffset) *aEndOffset = endOffset;
+
+ aAttributes->SetAttribute(nsGkAtoms::invalid, nsGkAtoms::spelling);
+
+ return;
+ }
+
+ // This range came after the point.
+ endOffset = DOMPointToOffset(startNode, startNodeOffset);
+
+ if (idx > 0) {
+ const nsRange* prevRange = domSel->GetRangeAt(idx - 1);
+ startOffset = DOMPointToOffset(prevRange->GetEndContainer(),
+ prevRange->EndOffset());
+ }
+
+ // The previous range might not be within this accessible. In that case,
+ // DOMPointToOffset returns length as a fallback. We don't want to use
+ // that offset if so, hence the startOffset < *aEndOffset check.
+ if (startOffset > *aStartOffset && startOffset < *aEndOffset) {
+ *aStartOffset = startOffset;
+ }
+
+ if (endOffset < *aEndOffset) *aEndOffset = endOffset;
+
+ return;
+ }
+
+ // We never found a range that ended after the point, therefore we know that
+ // the point is not in a range, that we do not need to compute an end offset,
+ // and that we should use the end offset of the last range to compute the
+ // start offset of the text attribute range.
+ const nsRange* prevRange = domSel->GetRangeAt(rangeCount - 1);
+ startOffset =
+ DOMPointToOffset(prevRange->GetEndContainer(), prevRange->EndOffset());
+
+ // The previous range might not be within this accessible. In that case,
+ // DOMPointToOffset returns length as a fallback. We don't want to use
+ // that offset if so, hence the startOffset < *aEndOffset check.
+ if (startOffset > *aStartOffset && startOffset < *aEndOffset) {
+ *aStartOffset = startOffset;
+ }
+}
diff --git a/accessible/generic/HyperTextAccessible.h b/accessible/generic/HyperTextAccessible.h
new file mode 100644
index 0000000000..c0fa53fd2a
--- /dev/null
+++ b/accessible/generic/HyperTextAccessible.h
@@ -0,0 +1,455 @@
+/* -*- 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/. */
+
+#ifndef mozilla_a11y_HyperTextAccessible_h__
+#define mozilla_a11y_HyperTextAccessible_h__
+
+#include "AccessibleWrap.h"
+#include "mozilla/a11y/HyperTextAccessibleBase.h"
+#include "nsIAccessibleText.h"
+#include "nsIAccessibleTypes.h"
+#include "nsIFrame.h" // only for nsSelectionAmount
+#include "nsISelectionController.h"
+#include "nsDirection.h"
+#include "WordMovementType.h"
+
+class nsFrameSelection;
+class nsIFrame;
+class nsRange;
+class nsIWidget;
+
+namespace mozilla {
+class EditorBase;
+namespace dom {
+class Selection;
+}
+
+namespace a11y {
+
+class TextLeafPoint;
+class TextRange;
+
+struct DOMPoint {
+ DOMPoint() : node(nullptr), idx(0) {}
+ DOMPoint(nsINode* aNode, int32_t aIdx) : node(aNode), idx(aIdx) {}
+
+ nsINode* node;
+ int32_t idx;
+};
+
+/**
+ * Special Accessible that knows how contain both text and embedded objects
+ */
+class HyperTextAccessible : public AccessibleWrap,
+ public HyperTextAccessibleBase {
+ public:
+ HyperTextAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(HyperTextAccessible, AccessibleWrap)
+
+ // LocalAccessible
+ virtual already_AddRefed<AccAttributes> NativeAttributes() override;
+ virtual mozilla::a11y::role NativeRole() const override;
+ virtual uint64_t NativeState() const override;
+
+ virtual void Shutdown() override;
+ virtual bool RemoveChild(LocalAccessible* aAccessible) override;
+ virtual bool InsertChildAt(uint32_t aIndex, LocalAccessible* aChild) override;
+ virtual Relation RelationByType(RelationType aType) const override;
+
+ /**
+ * Return whether the associated content is editable.
+ */
+ bool IsEditable() const;
+
+ // HyperTextAccessible (static helper method)
+
+ // Convert content offset to rendered text offset
+ nsresult ContentToRenderedOffset(nsIFrame* aFrame, int32_t aContentOffset,
+ uint32_t* aRenderedOffset) const;
+
+ // Convert rendered text offset to content offset
+ nsresult RenderedToContentOffset(nsIFrame* aFrame, uint32_t aRenderedOffset,
+ int32_t* aContentOffset) const;
+
+ //////////////////////////////////////////////////////////////////////////////
+ // HyperLinkAccessible
+
+ /**
+ * Return link accessible at the given index.
+ */
+ LocalAccessible* LinkAt(uint32_t aIndex) { return EmbeddedChildAt(aIndex); }
+
+ //////////////////////////////////////////////////////////////////////////////
+ // HyperTextAccessible: DOM point to text offset conversions.
+
+ /**
+ * Turn a DOM point (node and offset) into a character offset of this
+ * hypertext. Will look for closest match when the DOM node does not have
+ * an accessible object associated with it. Will return an offset for the end
+ * of the string if the node is not found.
+ *
+ * @param aNode [in] the node to look for
+ * @param aNodeOffset [in] the offset to look for
+ * if -1 just look directly for the node
+ * if >=0 and aNode is text, this represents a char
+ * offset if >=0 and aNode is not text, this represents a child node offset
+ * @param aIsEndOffset [in] if true, then this offset is not inclusive. The
+ * character indicated by the offset returned is at [offset - 1]. This means
+ * if the passed-in offset is really in a descendant, then the offset
+ * returned will come just after the relevant embedded object characer. If
+ * false, then the offset is inclusive. The character indicated by the offset
+ * returned is at [offset]. If the passed-in offset in inside a descendant,
+ * then the returned offset will be on the relevant embedded object char.
+ */
+ uint32_t DOMPointToOffset(nsINode* aNode, int32_t aNodeOffset,
+ bool aIsEndOffset = false) const;
+
+ /**
+ * Transform the given a11y point into the offset relative this hypertext.
+ */
+ uint32_t TransformOffset(LocalAccessible* aDescendant, uint32_t aOffset,
+ bool aIsEndOffset) const;
+
+ /**
+ * Convert the given offset into DOM point.
+ *
+ * If offset is at text leaf then DOM point is (text node, offsetInTextNode),
+ * if before embedded object then (parent node, indexInParent), if after then
+ * (parent node, indexInParent + 1).
+ */
+ DOMPoint OffsetToDOMPoint(int32_t aOffset) const;
+
+ //////////////////////////////////////////////////////////////////////////////
+ // TextAccessible
+
+ using HyperTextAccessibleBase::CharAt;
+
+ char16_t CharAt(int32_t aOffset) {
+ nsAutoString charAtOffset;
+ CharAt(aOffset, charAtOffset);
+ return charAtOffset.CharAt(0);
+ }
+
+ /**
+ * Return true if char at the given offset equals to given char.
+ */
+ bool IsCharAt(int32_t aOffset, char16_t aChar) {
+ return CharAt(aOffset) == aChar;
+ }
+
+ /**
+ * Return true if terminal char is at the given offset.
+ */
+ bool IsLineEndCharAt(int32_t aOffset) { return IsCharAt(aOffset, '\n'); }
+
+ virtual void TextBeforeOffset(int32_t aOffset,
+ AccessibleTextBoundary aBoundaryType,
+ int32_t* aStartOffset, int32_t* aEndOffset,
+ nsAString& aText) override;
+ virtual void TextAtOffset(int32_t aOffset,
+ AccessibleTextBoundary aBoundaryType,
+ int32_t* aStartOffset, int32_t* aEndOffset,
+ nsAString& aText) override;
+ virtual void TextAfterOffset(int32_t aOffset,
+ AccessibleTextBoundary aBoundaryType,
+ int32_t* aStartOffset, int32_t* aEndOffset,
+ nsAString& aText) override;
+
+ virtual already_AddRefed<AccAttributes> TextAttributes(
+ bool aIncludeDefAttrs, int32_t aOffset, int32_t* aStartOffset,
+ int32_t* aEndOffset) override;
+
+ virtual already_AddRefed<AccAttributes> DefaultTextAttributes() override;
+
+ // HyperTextAccessibleBase provides an overload which takes an Accessible.
+ using HyperTextAccessibleBase::GetChildOffset;
+
+ virtual LocalAccessible* GetChildAtOffset(uint32_t aOffset) const override {
+ return LocalChildAt(GetChildIndexAtOffset(aOffset));
+ }
+
+ /**
+ * Return an offset at the given point.
+ */
+ int32_t OffsetAtPoint(int32_t aX, int32_t aY, uint32_t aCoordType) override;
+
+ LayoutDeviceIntRect TextBounds(
+ int32_t aStartOffset, int32_t aEndOffset,
+ uint32_t aCoordType =
+ nsIAccessibleCoordinateType::COORDTYPE_SCREEN_RELATIVE) override;
+
+ LayoutDeviceIntRect CharBounds(int32_t aOffset,
+ uint32_t aCoordType) override {
+ int32_t endOffset = aOffset == static_cast<int32_t>(CharacterCount())
+ ? aOffset
+ : aOffset + 1;
+ return TextBounds(aOffset, endOffset, aCoordType);
+ }
+
+ /**
+ * Get/set caret offset, if no caret then -1.
+ */
+ virtual int32_t CaretOffset() const override;
+ virtual void SetCaretOffset(int32_t aOffset) override;
+
+ /**
+ * Provide the line number for the caret.
+ * @return 1-based index for the line number with the caret
+ */
+ int32_t CaretLineNumber();
+
+ /**
+ * Return the caret rect and the widget containing the caret within this
+ * text accessible.
+ *
+ * @param [out] the widget containing the caret
+ * @return the caret rect
+ */
+ mozilla::LayoutDeviceIntRect GetCaretRect(nsIWidget** aWidget);
+
+ /**
+ * Return true if caret is at end of line.
+ */
+ bool IsCaretAtEndOfLine() const;
+
+ virtual int32_t SelectionCount() override;
+
+ virtual bool SelectionBoundsAt(int32_t aSelectionNum, int32_t* aStartOffset,
+ int32_t* aEndOffset) override;
+
+ /*
+ * Changes the start and end offset of the specified selection.
+ * @return true if succeeded
+ */
+ // TODO: annotate this with `MOZ_CAN_RUN_SCRIPT` instead.
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY bool SetSelectionBoundsAt(int32_t aSelectionNum,
+ int32_t aStartOffset,
+ int32_t aEndOffset);
+
+ /**
+ * Adds a selection bounded by the specified offsets.
+ * @return true if succeeded
+ */
+ bool AddToSelection(int32_t aStartOffset, int32_t aEndOffset);
+
+ /*
+ * Removes the specified selection.
+ * @return true if succeeded
+ */
+ // TODO: annotate this with `MOZ_CAN_RUN_SCRIPT` instead.
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY bool RemoveFromSelection(int32_t aSelectionNum);
+
+ /**
+ * Scroll the given text range into view.
+ */
+ void ScrollSubstringTo(int32_t aStartOffset, int32_t aEndOffset,
+ uint32_t aScrollType);
+
+ /**
+ * Scroll the given text range to the given point.
+ */
+ void ScrollSubstringToPoint(int32_t aStartOffset, int32_t aEndOffset,
+ uint32_t aCoordinateType, int32_t aX, int32_t aY);
+
+ /**
+ * Return a range that encloses the text control or the document this
+ * accessible belongs to.
+ */
+ void EnclosingRange(TextRange& aRange) const;
+
+ virtual void SelectionRanges(nsTArray<TextRange>* aRanges) const override;
+
+ /**
+ * Return an array of disjoint ranges of visible text within the text control
+ * or the document this accessible belongs to.
+ */
+ void VisibleRanges(nsTArray<TextRange>* aRanges) const;
+
+ /**
+ * Return a range containing the given accessible.
+ */
+ void RangeByChild(LocalAccessible* aChild, TextRange& aRange) const;
+
+ /**
+ * Return a range containing an accessible at the given point.
+ */
+ void RangeAtPoint(int32_t aX, int32_t aY, TextRange& aRange) const;
+
+ //////////////////////////////////////////////////////////////////////////////
+ // EditableTextAccessible
+
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY void ReplaceText(const nsAString& aText);
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY void InsertText(const nsAString& aText,
+ int32_t aPosition);
+ void CopyText(int32_t aStartPos, int32_t aEndPos);
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY void CutText(int32_t aStartPos, int32_t aEndPos);
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY void DeleteText(int32_t aStartPos,
+ int32_t aEndPos);
+ MOZ_CAN_RUN_SCRIPT
+ void PasteText(int32_t aPosition);
+
+ /**
+ * Return the editor associated with the accessible.
+ * The result may be either TextEditor or HTMLEditor.
+ */
+ virtual already_AddRefed<EditorBase> GetEditor() const;
+
+ /**
+ * Return DOM selection object for the accessible.
+ */
+ dom::Selection* DOMSelection() const;
+
+ protected:
+ virtual ~HyperTextAccessible() {}
+
+ // LocalAccessible
+ virtual ENameValueFlag NativeName(nsString& aName) const override;
+
+ // HyperTextAccessible
+
+ /**
+ * Adjust an offset the caret stays at to get a text by line boundary.
+ */
+ uint32_t AdjustCaretOffset(uint32_t aOffset) const;
+
+ /**
+ * Return true if the given offset points to terminal empty line if any.
+ */
+ bool IsEmptyLastLineOffset(int32_t aOffset) {
+ return aOffset == static_cast<int32_t>(CharacterCount()) &&
+ IsLineEndCharAt(aOffset - 1);
+ }
+
+ /**
+ * Return an offset of the found word boundary.
+ */
+ uint32_t FindWordBoundary(uint32_t aOffset, nsDirection aDirection,
+ EWordMovementType aWordMovementType);
+
+ /**
+ * Used to get begin/end of previous/this/next line. Note: end of line
+ * is an offset right before '\n' character if any, the offset is right after
+ * '\n' character is begin of line. In case of wrap word breaks these offsets
+ * are equal.
+ */
+ enum EWhichLineBoundary {
+ ePrevLineBegin,
+ ePrevLineEnd,
+ eThisLineBegin,
+ eThisLineEnd,
+ eNextLineBegin,
+ eNextLineEnd
+ };
+
+ /**
+ * Return an offset for requested line boundary. See constants above.
+ */
+ uint32_t FindLineBoundary(uint32_t aOffset,
+ EWhichLineBoundary aWhichLineBoundary);
+
+ /**
+ * Find the start offset for a paragraph , taking into account
+ * inner block elements and line breaks.
+ */
+ int32_t FindParagraphStartOffset(uint32_t aOffset);
+
+ /**
+ * Find the end offset for a paragraph , taking into account
+ * inner block elements and line breaks.
+ */
+ int32_t FindParagraphEndOffset(uint32_t aOffset);
+
+ /**
+ * Return an offset corresponding to the given direction and selection amount
+ * relative the given offset. A helper used to find word or line boundaries.
+ */
+ uint32_t FindOffset(uint32_t aOffset, nsDirection aDirection,
+ nsSelectionAmount aAmount,
+ EWordMovementType aWordMovementType = eDefaultBehavior);
+
+ /**
+ * Return the boundaries (in dev pixels) of the substring in case of textual
+ * frame or frame boundaries in case of non textual frame, offsets are
+ * ignored.
+ */
+ LayoutDeviceIntRect GetBoundsInFrame(nsIFrame* aFrame,
+ uint32_t aStartRenderedOffset,
+ uint32_t aEndRenderedOffset);
+
+ // Selection helpers
+
+ /**
+ * Return frame selection object for the accessible.
+ */
+ already_AddRefed<nsFrameSelection> FrameSelection() const;
+
+ /**
+ * Return selection ranges within the accessible subtree.
+ */
+ void GetSelectionDOMRanges(SelectionType aSelectionType,
+ nsTArray<nsRange*>* aRanges);
+
+ // TODO: annotate this with `MOZ_CAN_RUN_SCRIPT` instead.
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult SetSelectionRange(int32_t aStartPos,
+ int32_t aEndPos);
+
+ // Helpers
+ nsresult GetDOMPointByFrameOffset(nsIFrame* aFrame, int32_t aOffset,
+ LocalAccessible* aAccessible,
+ mozilla::a11y::DOMPoint* aPoint);
+
+ /**
+ * Set 'misspelled' text attribute and return range offsets where the
+ * attibute is stretched. If the text is not misspelled at the given offset
+ * then we expose only range offsets where text is not misspelled. The method
+ * is used by TextAttributes() method.
+ *
+ * @param aIncludeDefAttrs [in] points whether text attributes having default
+ * values of attributes should be included
+ * @param aSourceNode [in] the node we start to traverse from
+ * @param aStartOffset [in, out] the start offset
+ * @param aEndOffset [in, out] the end offset
+ * @param aAttributes [out, optional] result attributes
+ */
+ void GetSpellTextAttr(nsINode* aNode, uint32_t aNodeOffset,
+ uint32_t* aStartOffset, uint32_t* aEndOffset,
+ AccAttributes* aAttributes);
+
+ /**
+ * Set xml-roles attributes for MathML elements.
+ * @param aAttributes
+ */
+ void SetMathMLXMLRoles(AccAttributes* aAttributes);
+
+ // HyperTextAccessibleBase
+ virtual const Accessible* Acc() const override { return this; }
+
+ virtual nsTArray<int32_t>& GetCachedHyperTextOffsets() override {
+ return mOffsets;
+ }
+
+ private:
+ /**
+ * End text offsets array.
+ */
+ mutable nsTArray<int32_t> mOffsets;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+// LocalAccessible downcasting method
+
+inline HyperTextAccessible* LocalAccessible::AsHyperText() {
+ return IsHyperText() ? static_cast<HyperTextAccessible*>(this) : nullptr;
+}
+
+inline HyperTextAccessibleBase* LocalAccessible::AsHyperTextBase() {
+ return AsHyperText();
+}
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/generic/ImageAccessible.cpp b/accessible/generic/ImageAccessible.cpp
new file mode 100644
index 0000000000..d45c693654
--- /dev/null
+++ b/accessible/generic/ImageAccessible.cpp
@@ -0,0 +1,263 @@
+/* -*- 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 "ImageAccessible.h"
+
+#include "DocAccessible-inl.h"
+#include "LocalAccessible-inl.h"
+#include "nsAccUtils.h"
+#include "Role.h"
+#include "AccAttributes.h"
+#include "AccIterator.h"
+#include "CacheConstants.h"
+#include "States.h"
+
+#include "imgIContainer.h"
+#include "imgIRequest.h"
+#include "nsGenericHTMLElement.h"
+#include "mozilla/dom/BrowsingContext.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/MutationEventBinding.h"
+#include "nsContentUtils.h"
+#include "nsIImageLoadingContent.h"
+#include "nsPIDOMWindow.h"
+#include "nsIURI.h"
+
+namespace mozilla::a11y {
+
+NS_IMPL_ISUPPORTS_INHERITED(ImageAccessible, LinkableAccessible,
+ imgINotificationObserver)
+
+////////////////////////////////////////////////////////////////////////////////
+// ImageAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+ImageAccessible::ImageAccessible(nsIContent* aContent, DocAccessible* aDoc)
+ : LinkableAccessible(aContent, aDoc),
+ mImageRequestStatus(imgIRequest::STATUS_NONE) {
+ mType = eImageType;
+ nsCOMPtr<nsIImageLoadingContent> content(do_QueryInterface(mContent));
+ if (content) {
+ content->AddNativeObserver(this);
+ nsCOMPtr<imgIRequest> imageRequest;
+ content->GetRequest(nsIImageLoadingContent::CURRENT_REQUEST,
+ getter_AddRefs(imageRequest));
+ if (imageRequest) {
+ imageRequest->GetImageStatus(&mImageRequestStatus);
+ }
+ }
+}
+
+ImageAccessible::~ImageAccessible() {}
+
+////////////////////////////////////////////////////////////////////////////////
+// LocalAccessible public
+
+void ImageAccessible::Shutdown() {
+ nsCOMPtr<nsIImageLoadingContent> content(do_QueryInterface(mContent));
+ if (content) {
+ content->RemoveNativeObserver(this);
+ }
+
+ LinkableAccessible::Shutdown();
+}
+
+uint64_t ImageAccessible::NativeState() const {
+ // The state is a bitfield, get our inherited state, then logically OR it with
+ // states::ANIMATED if this is an animated image.
+
+ uint64_t state = LinkableAccessible::NativeState();
+
+ if (mImageRequestStatus & imgIRequest::STATUS_IS_ANIMATED) {
+ state |= states::ANIMATED;
+ }
+
+ if (!(mImageRequestStatus & imgIRequest::STATUS_SIZE_AVAILABLE)) {
+ nsIFrame* frame = GetFrame();
+ MOZ_ASSERT(!frame || frame->AccessibleType() == eImageType ||
+ frame->AccessibleType() == a11y::eHTMLImageMapType ||
+ frame->IsImageBoxFrame());
+ if (frame && !(frame->GetStateBits() & IMAGE_SIZECONSTRAINED)) {
+ // The size of this image hasn't been constrained and we haven't loaded
+ // enough of the image to know its size yet. This means it currently
+ // has 0 width and height.
+ state |= states::INVISIBLE;
+ }
+ }
+
+ return state;
+}
+
+ENameValueFlag ImageAccessible::NativeName(nsString& aName) const {
+ mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::alt, aName);
+ if (!aName.IsEmpty()) return eNameOK;
+
+ ENameValueFlag nameFlag = LocalAccessible::NativeName(aName);
+ if (!aName.IsEmpty()) return nameFlag;
+
+ return eNameOK;
+}
+
+role ImageAccessible::NativeRole() const { return roles::GRAPHIC; }
+
+void ImageAccessible::DOMAttributeChanged(int32_t aNameSpaceID,
+ nsAtom* aAttribute, int32_t aModType,
+ const nsAttrValue* aOldValue,
+ uint64_t aOldState) {
+ LinkableAccessible::DOMAttributeChanged(aNameSpaceID, aAttribute, aModType,
+ aOldValue, aOldState);
+
+ if (aAttribute == nsGkAtoms::longdesc &&
+ (aModType == dom::MutationEvent_Binding::ADDITION ||
+ aModType == dom::MutationEvent_Binding::REMOVAL)) {
+ SendCache(CacheDomain::Actions, CacheUpdateType::Update);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// LocalAccessible
+
+uint8_t ImageAccessible::ActionCount() const {
+ uint8_t actionCount = LinkableAccessible::ActionCount();
+ return HasLongDesc() ? actionCount + 1 : actionCount;
+}
+
+void ImageAccessible::ActionNameAt(uint8_t aIndex, nsAString& aName) {
+ aName.Truncate();
+ if (IsLongDescIndex(aIndex) && HasLongDesc()) {
+ aName.AssignLiteral("showlongdesc");
+ } else {
+ LinkableAccessible::ActionNameAt(aIndex, aName);
+ }
+}
+
+bool ImageAccessible::DoAction(uint8_t aIndex) const {
+ // Get the long description uri and open in a new window.
+ if (!IsLongDescIndex(aIndex)) return LinkableAccessible::DoAction(aIndex);
+
+ nsCOMPtr<nsIURI> uri = GetLongDescURI();
+ if (!uri) return false;
+
+ nsAutoCString utf8spec;
+ uri->GetSpec(utf8spec);
+ NS_ConvertUTF8toUTF16 spec(utf8spec);
+
+ dom::Document* document = mContent->OwnerDoc();
+ nsCOMPtr<nsPIDOMWindowOuter> piWindow = document->GetWindow();
+ if (!piWindow) return false;
+
+ RefPtr<dom::BrowsingContext> tmp;
+ return NS_SUCCEEDED(piWindow->Open(spec, u""_ns, u""_ns,
+ /* aLoadInfo = */ nullptr,
+ /* aForceNoOpener = */ false,
+ getter_AddRefs(tmp)));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// ImageAccessible
+
+LayoutDeviceIntPoint ImageAccessible::Position(uint32_t aCoordType) {
+ LayoutDeviceIntPoint point = Bounds().TopLeft();
+ nsAccUtils::ConvertScreenCoordsTo(&point.x.value, &point.y.value, aCoordType,
+ this);
+ return point;
+}
+
+LayoutDeviceIntSize ImageAccessible::Size() { return Bounds().Size(); }
+
+// LocalAccessible
+already_AddRefed<AccAttributes> ImageAccessible::NativeAttributes() {
+ RefPtr<AccAttributes> attributes = LinkableAccessible::NativeAttributes();
+
+ nsString src;
+ mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::src, src);
+ if (!src.IsEmpty()) attributes->SetAttribute(nsGkAtoms::src, std::move(src));
+
+ return attributes.forget();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Private methods
+
+already_AddRefed<nsIURI> ImageAccessible::GetLongDescURI() const {
+ if (mContent->AsElement()->HasAttr(kNameSpaceID_None, nsGkAtoms::longdesc)) {
+ // To check if longdesc contains an invalid url.
+ nsAutoString longdesc;
+ mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::longdesc,
+ longdesc);
+ if (longdesc.FindChar(' ') != -1 || longdesc.FindChar('\t') != -1 ||
+ longdesc.FindChar('\r') != -1 || longdesc.FindChar('\n') != -1) {
+ return nullptr;
+ }
+ nsCOMPtr<nsIURI> uri;
+ nsContentUtils::NewURIWithDocumentCharset(getter_AddRefs(uri), longdesc,
+ mContent->OwnerDoc(),
+ mContent->GetBaseURI());
+ return uri.forget();
+ }
+
+ DocAccessible* document = Document();
+ if (document) {
+ IDRefsIterator iter(document, mContent, nsGkAtoms::aria_describedby);
+ while (nsIContent* target = iter.NextElem()) {
+ if ((target->IsHTMLElement(nsGkAtoms::a) ||
+ target->IsHTMLElement(nsGkAtoms::area)) &&
+ target->AsElement()->HasAttr(kNameSpaceID_None, nsGkAtoms::href)) {
+ nsGenericHTMLElement* element = nsGenericHTMLElement::FromNode(target);
+
+ nsCOMPtr<nsIURI> uri;
+ element->GetURIAttr(nsGkAtoms::href, nullptr, getter_AddRefs(uri));
+ return uri.forget();
+ }
+ }
+ }
+
+ return nullptr;
+}
+
+bool ImageAccessible::IsLongDescIndex(uint8_t aIndex) const {
+ return aIndex == LinkableAccessible::ActionCount();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// imgINotificationObserver
+
+void ImageAccessible::Notify(imgIRequest* aRequest, int32_t aType,
+ const nsIntRect* aData) {
+ if (aType != imgINotificationObserver::FRAME_COMPLETE &&
+ aType != imgINotificationObserver::LOAD_COMPLETE &&
+ aType != imgINotificationObserver::DECODE_COMPLETE) {
+ // We should update our state if the whole image was decoded,
+ // or the first frame in the case of a gif.
+ return;
+ }
+
+ if (IsDefunct() || !mParent) {
+ return;
+ }
+
+ uint32_t status = imgIRequest::STATUS_NONE;
+ aRequest->GetImageStatus(&status);
+
+ if ((status ^ mImageRequestStatus) & imgIRequest::STATUS_SIZE_AVAILABLE) {
+ nsIFrame* frame = GetFrame();
+ if (frame && !(frame->GetStateBits() & IMAGE_SIZECONSTRAINED)) {
+ RefPtr<AccEvent> event = new AccStateChangeEvent(
+ this, states::INVISIBLE,
+ !(status & imgIRequest::STATUS_SIZE_AVAILABLE));
+ mDoc->FireDelayedEvent(event);
+ }
+ }
+
+ if ((status ^ mImageRequestStatus) & imgIRequest::STATUS_IS_ANIMATED) {
+ RefPtr<AccEvent> event = new AccStateChangeEvent(
+ this, states::ANIMATED, (status & imgIRequest::STATUS_IS_ANIMATED));
+ mDoc->FireDelayedEvent(event);
+ }
+
+ mImageRequestStatus = status;
+}
+
+} // namespace mozilla::a11y
diff --git a/accessible/generic/ImageAccessible.h b/accessible/generic/ImageAccessible.h
new file mode 100644
index 0000000000..1a072c5f90
--- /dev/null
+++ b/accessible/generic/ImageAccessible.h
@@ -0,0 +1,94 @@
+/* -*- 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/. */
+
+#ifndef mozilla_a11y_ImageAccessible_h__
+#define mozilla_a11y_ImageAccessible_h__
+
+#include "BaseAccessibles.h"
+#include "imgINotificationObserver.h"
+
+namespace mozilla {
+namespace a11y {
+
+/* LocalAccessible for supporting images
+ * supports:
+ * - gets name, role
+ * - support basic state
+ */
+class ImageAccessible : public LinkableAccessible,
+ public imgINotificationObserver {
+ public:
+ ImageAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_IMGINOTIFICATIONOBSERVER
+
+ // LocalAccessible
+ virtual void Shutdown() override;
+ virtual a11y::role NativeRole() const override;
+ virtual uint64_t NativeState() const override;
+ virtual already_AddRefed<AccAttributes> NativeAttributes() override;
+
+ // ActionAccessible
+ virtual uint8_t ActionCount() const override;
+ virtual void ActionNameAt(uint8_t aIndex, nsAString& aName) override;
+ virtual bool DoAction(uint8_t aIndex) const override;
+
+ // ImageAccessible
+ LayoutDeviceIntPoint Position(uint32_t aCoordType);
+ LayoutDeviceIntSize Size();
+
+ /**
+ * Return whether the element has a longdesc URI.
+ */
+ bool HasLongDesc() const {
+ nsCOMPtr<nsIURI> uri = GetLongDescURI();
+ return uri;
+ }
+
+ protected:
+ virtual ~ImageAccessible();
+
+ // LocalAccessible
+ virtual ENameValueFlag NativeName(nsString& aName) const override;
+
+ virtual void DOMAttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute,
+ int32_t aModType,
+ const nsAttrValue* aOldValue,
+ uint64_t aOldState) override;
+
+ private:
+ /**
+ * Return an URI for showlongdesc action if any.
+ */
+ already_AddRefed<nsIURI> GetLongDescURI() const;
+
+ /**
+ * Used by ActionNameAt and DoAction to ensure the index for opening the
+ * longdesc URL is valid.
+ * It is always assumed that the highest possible index opens the longdesc.
+ * This doesn't check that there is actually a longdesc, just that the index
+ * would be correct if there was one.
+ *
+ * @param aIndex The 0-based index to be tested.
+ *
+ * @returns true if index is valid for longdesc action.
+ */
+ inline bool IsLongDescIndex(uint8_t aIndex) const;
+
+ uint32_t mImageRequestStatus;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+// LocalAccessible downcasting method
+
+inline ImageAccessible* LocalAccessible::AsImage() {
+ return IsImage() ? static_cast<ImageAccessible*>(this) : nullptr;
+}
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/generic/LocalAccessible-inl.h b/accessible/generic/LocalAccessible-inl.h
new file mode 100644
index 0000000000..4692b85186
--- /dev/null
+++ b/accessible/generic/LocalAccessible-inl.h
@@ -0,0 +1,114 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_a11y_Accessible_inl_h_
+#define mozilla_a11y_Accessible_inl_h_
+
+#include "DocAccessible.h"
+#include "ARIAMap.h"
+#include "nsCoreUtils.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/PresShell.h"
+
+#ifdef A11Y_LOG
+# include "Logging.h"
+#endif
+
+namespace mozilla {
+namespace a11y {
+
+inline mozilla::a11y::role LocalAccessible::Role() const {
+ const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
+ if (!roleMapEntry || roleMapEntry->roleRule != kUseMapRole) {
+ return ARIATransformRole(NativeRole());
+ }
+
+ return ARIATransformRole(roleMapEntry->role);
+}
+
+inline mozilla::a11y::role LocalAccessible::ARIARole() {
+ const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
+ if (!roleMapEntry || roleMapEntry->roleRule != kUseMapRole) {
+ return mozilla::a11y::roles::NOTHING;
+ }
+
+ return ARIATransformRole(roleMapEntry->role);
+}
+
+inline void LocalAccessible::SetRoleMapEntry(
+ const nsRoleMapEntry* aRoleMapEntry) {
+ mRoleMapEntryIndex = aria::GetIndexFromRoleMap(aRoleMapEntry);
+}
+
+inline bool LocalAccessible::IsSearchbox() const {
+ const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
+ return (roleMapEntry && roleMapEntry->Is(nsGkAtoms::searchbox)) ||
+ (mContent->IsHTMLElement(nsGkAtoms::input) &&
+ mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
+ nsGkAtoms::search, eCaseMatters));
+}
+
+inline bool LocalAccessible::NativeHasNumericValue() const {
+ return mGenericTypes & eNumericValue;
+}
+
+inline bool LocalAccessible::ARIAHasNumericValue() const {
+ const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
+ if (!roleMapEntry || roleMapEntry->valueRule == eNoValue) return false;
+
+ if (roleMapEntry->valueRule == eHasValueMinMaxIfFocusable) {
+ return InteractiveState() & states::FOCUSABLE;
+ }
+
+ return true;
+}
+
+inline bool LocalAccessible::HasNumericValue() const {
+ return NativeHasNumericValue() || ARIAHasNumericValue();
+}
+
+inline bool LocalAccessible::IsDefunct() const {
+ MOZ_ASSERT(mStateFlags & eIsDefunct || IsApplication() || IsDoc() ||
+ mStateFlags & eSharedNode || mContent,
+ "No content");
+ return mStateFlags & eIsDefunct;
+}
+
+inline void LocalAccessible::ScrollTo(uint32_t aHow) const {
+ if (mContent) {
+ RefPtr<PresShell> presShell = mDoc->PresShellPtr();
+ nsCOMPtr<nsIContent> content = mContent;
+ nsCoreUtils::ScrollTo(presShell, content, aHow);
+ }
+}
+
+inline bool LocalAccessible::InsertAfter(LocalAccessible* aNewChild,
+ LocalAccessible* aRefChild) {
+ MOZ_ASSERT(aNewChild, "No new child to insert");
+
+ if (aRefChild && aRefChild->LocalParent() != this) {
+#ifdef A11Y_LOG
+ logging::TreeInfo("broken accessible tree", 0, "parent", this,
+ "prev sibling parent", aRefChild->LocalParent(), "child",
+ aNewChild, nullptr);
+ if (logging::IsEnabled(logging::eVerbose)) {
+ logging::Tree("TREE", "Document tree", mDoc);
+ logging::DOMTree("TREE", "DOM document tree", mDoc);
+ }
+#endif
+ MOZ_ASSERT_UNREACHABLE("Broken accessible tree");
+ mDoc->UnbindFromDocument(aNewChild);
+ return false;
+ }
+
+ return InsertChildAt(aRefChild ? aRefChild->IndexInParent() + 1 : 0,
+ aNewChild);
+}
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/generic/LocalAccessible.cpp b/accessible/generic/LocalAccessible.cpp
new file mode 100644
index 0000000000..674de4d1ee
--- /dev/null
+++ b/accessible/generic/LocalAccessible.cpp
@@ -0,0 +1,3977 @@
+/* -*- 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 "AccEvent.h"
+#include "LocalAccessible-inl.h"
+
+#include "EmbeddedObjCollector.h"
+#include "AccAttributes.h"
+#include "AccGroupInfo.h"
+#include "AccIterator.h"
+#include "CacheConstants.h"
+#include "CachedTableAccessible.h"
+#include "DocAccessible-inl.h"
+#include "mozilla/a11y/AccAttributes.h"
+#include "nsAccUtils.h"
+#include "nsAccessibilityService.h"
+#include "ApplicationAccessible.h"
+#include "nsAccessiblePivot.h"
+#include "nsGenericHTMLElement.h"
+#include "NotificationController.h"
+#include "nsEventShell.h"
+#include "nsTextEquivUtils.h"
+#include "DocAccessibleChild.h"
+#include "EventTree.h"
+#include "OuterDocAccessible.h"
+#include "Pivot.h"
+#include "Relation.h"
+#include "Role.h"
+#include "RootAccessible.h"
+#include "States.h"
+#include "StyleInfo.h"
+#include "TextLeafRange.h"
+#include "TextRange.h"
+#include "TableAccessible.h"
+#include "TableCellAccessible.h"
+#include "TreeWalker.h"
+#include "HTMLElementAccessibles.h"
+#include "HTMLSelectAccessible.h"
+#include "ImageAccessible.h"
+
+#include "nsIDOMXULButtonElement.h"
+#include "nsIDOMXULSelectCntrlEl.h"
+#include "nsIDOMXULSelectCntrlItemEl.h"
+#include "nsINodeList.h"
+#include "nsPIDOMWindow.h"
+
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/HTMLFormElement.h"
+#include "mozilla/dom/HTMLAnchorElement.h"
+#include "mozilla/gfx/Matrix.h"
+#include "nsIContent.h"
+#include "nsIFormControl.h"
+
+#include "nsLayoutUtils.h"
+#include "nsPresContext.h"
+#include "nsIFrame.h"
+#include "nsTextFrame.h"
+#include "nsView.h"
+#include "nsIDocShellTreeItem.h"
+#include "nsIScrollableFrame.h"
+#include "nsStyleStructInlines.h"
+#include "nsFocusManager.h"
+
+#include "nsString.h"
+#include "nsUnicharUtils.h"
+#include "nsReadableUtils.h"
+#include "prdtoa.h"
+#include "nsAtom.h"
+#include "nsIURI.h"
+#include "nsArrayUtils.h"
+#include "nsWhitespaceTokenizer.h"
+#include "nsAttrName.h"
+#include "nsContainerFrame.h"
+
+#include "mozilla/Assertions.h"
+#include "mozilla/BasicEvents.h"
+#include "mozilla/Components.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/FloatingPoint.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/Unused.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/ProfilerMarkers.h"
+#include "mozilla/StaticPrefs_accessibility.h"
+#include "mozilla/StaticPrefs_ui.h"
+#include "mozilla/dom/CanvasRenderingContext2D.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/HTMLCanvasElement.h"
+#include "mozilla/dom/HTMLBodyElement.h"
+#include "mozilla/dom/HTMLLabelElement.h"
+#include "mozilla/dom/KeyboardEventBinding.h"
+#include "mozilla/dom/TreeWalker.h"
+#include "mozilla/dom/UserActivation.h"
+#include "mozilla/dom/MutationEventBinding.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+////////////////////////////////////////////////////////////////////////////////
+// LocalAccessible: nsISupports and cycle collection
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(LocalAccessible)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(LocalAccessible)
+ tmp->Shutdown();
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(LocalAccessible)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mContent, mDoc)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(LocalAccessible)
+ NS_INTERFACE_MAP_ENTRY_CONCRETE(LocalAccessible)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, LocalAccessible)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(LocalAccessible)
+NS_IMPL_CYCLE_COLLECTING_RELEASE_WITH_DESTROY(LocalAccessible, LastRelease())
+
+LocalAccessible::LocalAccessible(nsIContent* aContent, DocAccessible* aDoc)
+ : Accessible(),
+ mContent(aContent),
+ mDoc(aDoc),
+ mParent(nullptr),
+ mIndexInParent(-1),
+ mBounds(),
+ mFirstLineStart(-1),
+ mStateFlags(0),
+ mContextFlags(0),
+ mReorderEventTarget(false),
+ mShowEventTarget(false),
+ mHideEventTarget(false),
+ mIndexOfEmbeddedChild(-1),
+ mGroupInfo(nullptr) {}
+
+LocalAccessible::~LocalAccessible() {
+ NS_ASSERTION(!mDoc, "LastRelease was never called!?!");
+}
+
+ENameValueFlag LocalAccessible::Name(nsString& aName) const {
+ aName.Truncate();
+
+ if (!HasOwnContent()) return eNameOK;
+
+ ARIAName(aName);
+ if (!aName.IsEmpty()) return eNameOK;
+
+ ENameValueFlag nameFlag = NativeName(aName);
+ if (!aName.IsEmpty()) return nameFlag;
+
+ // In the end get the name from tooltip.
+ if (mContent->IsHTMLElement()) {
+ if (mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::title,
+ aName)) {
+ aName.CompressWhitespace();
+ return eNameFromTooltip;
+ }
+ } else if (mContent->IsXULElement()) {
+ if (mContent->AsElement()->GetAttr(kNameSpaceID_None,
+ nsGkAtoms::tooltiptext, aName)) {
+ aName.CompressWhitespace();
+ return eNameFromTooltip;
+ }
+ } else if (mContent->IsSVGElement()) {
+ // If user agents need to choose among multiple 'desc' or 'title'
+ // elements for processing, the user agent shall choose the first one.
+ for (nsIContent* childElm = mContent->GetFirstChild(); childElm;
+ childElm = childElm->GetNextSibling()) {
+ if (childElm->IsSVGElement(nsGkAtoms::desc)) {
+ nsTextEquivUtils::AppendTextEquivFromContent(this, childElm, &aName);
+ return eNameFromTooltip;
+ }
+ }
+ }
+
+ aName.SetIsVoid(true);
+
+ return nameFlag;
+}
+
+void LocalAccessible::Description(nsString& aDescription) const {
+ // There are 4 conditions that make an accessible have no accDescription:
+ // 1. it's a text node; or
+ // 2. It has no ARIA describedby or description property
+ // 3. it doesn't have an accName; or
+ // 4. its title attribute already equals to its accName nsAutoString name;
+
+ if (!HasOwnContent() || mContent->IsText()) return;
+
+ ARIADescription(aDescription);
+
+ if (aDescription.IsEmpty()) {
+ NativeDescription(aDescription);
+
+ if (aDescription.IsEmpty()) {
+ // Keep the Name() method logic.
+ if (mContent->IsHTMLElement()) {
+ mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::title,
+ aDescription);
+ } else if (mContent->IsXULElement()) {
+ mContent->AsElement()->GetAttr(kNameSpaceID_None,
+ nsGkAtoms::tooltiptext, aDescription);
+ } else if (mContent->IsSVGElement()) {
+ for (nsIContent* childElm = mContent->GetFirstChild(); childElm;
+ childElm = childElm->GetNextSibling()) {
+ if (childElm->IsSVGElement(nsGkAtoms::desc)) {
+ nsTextEquivUtils::AppendTextEquivFromContent(this, childElm,
+ &aDescription);
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ if (!aDescription.IsEmpty()) {
+ aDescription.CompressWhitespace();
+ nsAutoString name;
+ Name(name);
+ // Don't expose a description if it is the same as the name.
+ if (aDescription.Equals(name)) aDescription.Truncate();
+ }
+}
+
+KeyBinding LocalAccessible::AccessKey() const {
+ if (!HasOwnContent()) return KeyBinding();
+
+ uint32_t key = nsCoreUtils::GetAccessKeyFor(mContent);
+ if (!key && mContent->IsElement()) {
+ LocalAccessible* label = nullptr;
+
+ // Copy access key from label node.
+ if (mContent->IsHTMLElement()) {
+ // Unless it is labeled via an ancestor <label>, in which case that would
+ // be redundant.
+ HTMLLabelIterator iter(Document(), this,
+ HTMLLabelIterator::eSkipAncestorLabel);
+ label = iter.Next();
+ }
+ if (!label) {
+ XULLabelIterator iter(Document(), mContent);
+ label = iter.Next();
+ }
+
+ if (label) key = nsCoreUtils::GetAccessKeyFor(label->GetContent());
+ }
+
+ if (!key) return KeyBinding();
+
+ // Get modifier mask. Use ui.key.generalAccessKey (unless it is -1).
+ switch (StaticPrefs::ui_key_generalAccessKey()) {
+ case -1:
+ break;
+ case dom::KeyboardEvent_Binding::DOM_VK_SHIFT:
+ return KeyBinding(key, KeyBinding::kShift);
+ case dom::KeyboardEvent_Binding::DOM_VK_CONTROL:
+ return KeyBinding(key, KeyBinding::kControl);
+ case dom::KeyboardEvent_Binding::DOM_VK_ALT:
+ return KeyBinding(key, KeyBinding::kAlt);
+ case dom::KeyboardEvent_Binding::DOM_VK_META:
+ return KeyBinding(key, KeyBinding::kMeta);
+ default:
+ return KeyBinding();
+ }
+
+ // Determine the access modifier used in this context.
+ dom::Document* document = mContent->GetComposedDoc();
+ if (!document) return KeyBinding();
+
+ nsCOMPtr<nsIDocShellTreeItem> treeItem(document->GetDocShell());
+ if (!treeItem) return KeyBinding();
+
+ nsresult rv = NS_ERROR_FAILURE;
+ int32_t modifierMask = 0;
+ switch (treeItem->ItemType()) {
+ case nsIDocShellTreeItem::typeChrome:
+ modifierMask = StaticPrefs::ui_key_chromeAccess();
+ rv = NS_OK;
+ break;
+ case nsIDocShellTreeItem::typeContent:
+ modifierMask = StaticPrefs::ui_key_contentAccess();
+ rv = NS_OK;
+ break;
+ }
+
+ return NS_SUCCEEDED(rv) ? KeyBinding(key, modifierMask) : KeyBinding();
+}
+
+KeyBinding LocalAccessible::KeyboardShortcut() const { return KeyBinding(); }
+
+uint64_t LocalAccessible::VisibilityState() const {
+ if (IPCAccessibilityActive() &&
+ StaticPrefs::accessibility_cache_enabled_AtStartup()) {
+ // Visibility states must be calculated by RemoteAccessible, so there's no
+ // point calculating them here.
+ return 0;
+ }
+ nsIFrame* frame = GetFrame();
+ if (!frame) {
+ // Element having display:contents is considered visible semantically,
+ // despite it doesn't have a visually visible box.
+ if (nsCoreUtils::IsDisplayContents(mContent)) {
+ return states::OFFSCREEN;
+ }
+ return states::INVISIBLE;
+ }
+
+ if (!frame->StyleVisibility()->IsVisible()) return states::INVISIBLE;
+
+ // It's invisible if the presshell is hidden by a visibility:hidden element in
+ // an ancestor document.
+ if (frame->PresShell()->IsUnderHiddenEmbedderElement()) {
+ return states::INVISIBLE;
+ }
+
+ // Offscreen state if the document's visibility state is not visible.
+ if (Document()->IsHidden()) return states::OFFSCREEN;
+
+ // Walk the parent frame chain to see if the frame is in background tab or
+ // scrolled out.
+ nsIFrame* curFrame = frame;
+ do {
+ nsView* view = curFrame->GetView();
+ if (view && view->GetVisibility() == nsViewVisibility_kHide) {
+ return states::INVISIBLE;
+ }
+
+ if (nsLayoutUtils::IsPopup(curFrame)) {
+ return 0;
+ }
+
+ if (curFrame->StyleUIReset()->mMozSubtreeHiddenOnlyVisually) {
+ // Offscreen state for background tab content.
+ return states::OFFSCREEN;
+ }
+
+ nsIFrame* parentFrame = curFrame->GetParent();
+ // If contained by scrollable frame then check that at least 12 pixels
+ // around the object is visible, otherwise the object is offscreen.
+ nsIScrollableFrame* scrollableFrame = do_QueryFrame(parentFrame);
+ const nscoord kMinPixels = nsPresContext::CSSPixelsToAppUnits(12);
+ if (scrollableFrame) {
+ nsRect scrollPortRect = scrollableFrame->GetScrollPortRect();
+ nsRect frameRect = nsLayoutUtils::TransformFrameRectToAncestor(
+ frame, frame->GetRectRelativeToSelf(), parentFrame);
+ if (!scrollPortRect.Contains(frameRect)) {
+ scrollPortRect.Deflate(kMinPixels, kMinPixels);
+ if (!scrollPortRect.Intersects(frameRect)) return states::OFFSCREEN;
+ }
+ }
+
+ if (!parentFrame) {
+ parentFrame = nsLayoutUtils::GetCrossDocParentFrameInProcess(curFrame);
+ // Even if we couldn't find the parent frame, it might mean we are in an
+ // out-of-process iframe, try to see if |frame| is scrolled out in an
+ // scrollable frame in a cross-process ancestor document.
+ if (!parentFrame &&
+ nsLayoutUtils::FrameIsMostlyScrolledOutOfViewInCrossProcess(
+ frame, kMinPixels)) {
+ return states::OFFSCREEN;
+ }
+ }
+
+ curFrame = parentFrame;
+ } while (curFrame);
+
+ // Zero area rects can occur in the first frame of a multi-frame text flow,
+ // in which case the rendered text is not empty and the frame should not be
+ // marked invisible.
+ // XXX Can we just remove this check? Why do we need to mark empty
+ // text invisible?
+ if (frame->IsTextFrame() && !(frame->GetStateBits() & NS_FRAME_OUT_OF_FLOW) &&
+ frame->GetRect().IsEmpty()) {
+ nsIFrame::RenderedText text = frame->GetRenderedText(
+ 0, UINT32_MAX, nsIFrame::TextOffsetType::OffsetsInContentText,
+ nsIFrame::TrailingWhitespace::DontTrim);
+ if (text.mString.IsEmpty()) {
+ return states::INVISIBLE;
+ }
+ }
+
+ return 0;
+}
+
+uint64_t LocalAccessible::NativeState() const {
+ uint64_t state = 0;
+
+ if (!IsInDocument()) state |= states::STALE;
+
+ if (HasOwnContent() && mContent->IsElement()) {
+ dom::ElementState elementState = mContent->AsElement()->State();
+
+ if (elementState.HasState(dom::ElementState::INVALID)) {
+ state |= states::INVALID;
+ }
+
+ if (elementState.HasState(dom::ElementState::REQUIRED)) {
+ state |= states::REQUIRED;
+ }
+
+ state |= NativeInteractiveState();
+ }
+
+ // Gather states::INVISIBLE and states::OFFSCREEN flags for this object.
+ state |= VisibilityState();
+
+ nsIFrame* frame = GetFrame();
+ if (frame) {
+ if (frame->GetStateBits() & NS_FRAME_OUT_OF_FLOW) state |= states::FLOATING;
+
+ // XXX we should look at layout for non XUL box frames, but need to decide
+ // how that interacts with ARIA.
+ if (HasOwnContent() && mContent->IsXULElement() && frame->IsXULBoxFrame()) {
+ const nsStyleXUL* xulStyle = frame->StyleXUL();
+ if (xulStyle && frame->IsXULBoxFrame()) {
+ // In XUL all boxes are either vertical or horizontal
+ if (xulStyle->mBoxOrient == StyleBoxOrient::Vertical) {
+ state |= states::VERTICAL;
+ } else {
+ state |= states::HORIZONTAL;
+ }
+ }
+ }
+ }
+
+ // Check if a XUL element has the popup attribute (an attached popup menu).
+ if (HasOwnContent() && mContent->IsXULElement() &&
+ mContent->AsElement()->HasAttr(kNameSpaceID_None, nsGkAtoms::popup)) {
+ state |= states::HASPOPUP;
+ }
+
+ // Bypass the link states specialization for non links.
+ const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
+ if (!roleMapEntry || roleMapEntry->roleRule == kUseNativeRole ||
+ roleMapEntry->role == roles::LINK) {
+ state |= NativeLinkState();
+ }
+
+ return state;
+}
+
+uint64_t LocalAccessible::NativeInteractiveState() const {
+ if (!mContent->IsElement()) return 0;
+
+ if (NativelyUnavailable()) return states::UNAVAILABLE;
+
+ nsIFrame* frame = GetFrame();
+ if (frame && frame->IsFocusable()) return states::FOCUSABLE;
+
+ return 0;
+}
+
+uint64_t LocalAccessible::NativeLinkState() const { return 0; }
+
+bool LocalAccessible::NativelyUnavailable() const {
+ if (mContent->IsHTMLElement()) return mContent->AsElement()->IsDisabled();
+
+ return mContent->IsElement() && mContent->AsElement()->AttrValueIs(
+ kNameSpaceID_None, nsGkAtoms::disabled,
+ nsGkAtoms::_true, eCaseMatters);
+}
+
+Accessible* LocalAccessible::ChildAtPoint(int32_t aX, int32_t aY,
+ EWhichChildAtPoint aWhichChild) {
+ Accessible* child = LocalChildAtPoint(aX, aY, aWhichChild);
+ if (aWhichChild != EWhichChildAtPoint::DirectChild && child &&
+ child->IsOuterDoc()) {
+ child = child->ChildAtPoint(aX, aY, aWhichChild);
+ }
+
+ return child;
+}
+
+LocalAccessible* LocalAccessible::LocalChildAtPoint(
+ int32_t aX, int32_t aY, EWhichChildAtPoint aWhichChild) {
+ // If we can't find the point in a child, we will return the fallback answer:
+ // we return |this| if the point is within it, otherwise nullptr.
+ LocalAccessible* fallbackAnswer = nullptr;
+ LayoutDeviceIntRect rect = Bounds();
+ if (rect.Contains(aX, aY)) fallbackAnswer = this;
+
+ if (nsAccUtils::MustPrune(this)) { // Do not dig any further
+ return fallbackAnswer;
+ }
+
+ // Search an accessible at the given point starting from accessible document
+ // because containing block (see CSS2) for out of flow element (for example,
+ // absolutely positioned element) may be different from its DOM parent and
+ // therefore accessible for containing block may be different from accessible
+ // for DOM parent but GetFrameForPoint() should be called for containing block
+ // to get an out of flow element.
+ DocAccessible* accDocument = Document();
+ NS_ENSURE_TRUE(accDocument, nullptr);
+
+ nsIFrame* rootFrame = accDocument->GetFrame();
+ NS_ENSURE_TRUE(rootFrame, nullptr);
+
+ nsIFrame* startFrame = rootFrame;
+
+ // Check whether the point is at popup content.
+ nsIWidget* rootWidget = rootFrame->GetView()->GetNearestWidget(nullptr);
+ NS_ENSURE_TRUE(rootWidget, nullptr);
+
+ LayoutDeviceIntRect rootRect = rootWidget->GetScreenBounds();
+
+ auto point = LayoutDeviceIntPoint(aX - rootRect.X(), aY - rootRect.Y());
+
+ nsIFrame* popupFrame = nsLayoutUtils::GetPopupFrameForPoint(
+ accDocument->PresContext()->GetRootPresContext(), rootWidget, point);
+ if (popupFrame) {
+ // If 'this' accessible is not inside the popup then ignore the popup when
+ // searching an accessible at point.
+ DocAccessible* popupDoc =
+ GetAccService()->GetDocAccessible(popupFrame->GetContent()->OwnerDoc());
+ LocalAccessible* popupAcc =
+ popupDoc->GetAccessibleOrContainer(popupFrame->GetContent());
+ LocalAccessible* popupChild = this;
+ while (popupChild && !popupChild->IsDoc() && popupChild != popupAcc) {
+ popupChild = popupChild->LocalParent();
+ }
+
+ if (popupChild == popupAcc) startFrame = popupFrame;
+ }
+
+ nsPresContext* presContext = startFrame->PresContext();
+ nsRect screenRect = startFrame->GetScreenRectInAppUnits();
+ nsPoint offset(presContext->DevPixelsToAppUnits(aX) - screenRect.X(),
+ presContext->DevPixelsToAppUnits(aY) - screenRect.Y());
+
+ nsIFrame* foundFrame = nsLayoutUtils::GetFrameForPoint(
+ RelativeTo{startFrame, ViewportType::Visual}, offset);
+
+ nsIContent* content = nullptr;
+ if (!foundFrame || !(content = foundFrame->GetContent())) {
+ return fallbackAnswer;
+ }
+
+ // Get accessible for the node with the point or the first accessible in
+ // the DOM parent chain.
+ DocAccessible* contentDocAcc =
+ GetAccService()->GetDocAccessible(content->OwnerDoc());
+
+ // contentDocAcc in some circumstances can be nullptr. See bug 729861
+ NS_ASSERTION(contentDocAcc, "could not get the document accessible");
+ if (!contentDocAcc) return fallbackAnswer;
+
+ LocalAccessible* accessible =
+ contentDocAcc->GetAccessibleOrContainer(content);
+ if (!accessible) return fallbackAnswer;
+
+ // Hurray! We have an accessible for the frame that layout gave us.
+ // Since DOM node of obtained accessible may be out of flow then we should
+ // ensure obtained accessible is a child of this accessible.
+ LocalAccessible* child = accessible;
+ while (child != this) {
+ LocalAccessible* parent = child->LocalParent();
+ if (!parent) {
+ // Reached the top of the hierarchy. These bounds were inside an
+ // accessible that is not a descendant of this one.
+ return fallbackAnswer;
+ }
+
+ // If we landed on a legitimate child of |this|, and we want the direct
+ // child, return it here.
+ if (parent == this && aWhichChild == EWhichChildAtPoint::DirectChild) {
+ return child;
+ }
+
+ child = parent;
+ }
+
+ // Manually walk through accessible children and see if the are within this
+ // point. Skip offscreen or invisible accessibles. This takes care of cases
+ // where layout won't walk into things for us, such as image map areas and
+ // sub documents (XXX: subdocuments should be handled by methods of
+ // OuterDocAccessibles).
+ uint32_t childCount = accessible->ChildCount();
+ if (childCount == 1 && accessible->IsOuterDoc() &&
+ accessible->FirstChild()->IsRemote()) {
+ // No local children.
+ return accessible;
+ }
+ for (uint32_t childIdx = 0; childIdx < childCount; childIdx++) {
+ LocalAccessible* child = accessible->LocalChildAt(childIdx);
+
+ LayoutDeviceIntRect childRect = child->Bounds();
+ if (childRect.Contains(aX, aY) &&
+ (child->State() & states::INVISIBLE) == 0) {
+ if (aWhichChild == EWhichChildAtPoint::DeepestChild) {
+ return child->LocalChildAtPoint(aX, aY,
+ EWhichChildAtPoint::DeepestChild);
+ }
+
+ return child;
+ }
+ }
+
+ return accessible;
+}
+
+nsIFrame* LocalAccessible::FindNearestAccessibleAncestorFrame() {
+ nsIFrame* frame = GetFrame();
+ if (IsDoc()) {
+ // We bound documents by their own frame, which is their PresShell's root
+ // frame. We cache the document offset elsewhere in BundleFieldsForCache
+ // using the nsGkAtoms::crossorigin attribute.
+ MOZ_ASSERT(frame, "DocAccessibles should always have a frame");
+ return frame;
+ }
+
+ // Iterate through accessible's ancestors to find one with a frame.
+ LocalAccessible* ancestor = mParent;
+ while (ancestor) {
+ if (nsIFrame* boundingFrame = ancestor->GetFrame()) {
+ return boundingFrame;
+ }
+ ancestor = ancestor->LocalParent();
+ }
+
+ MOZ_ASSERT_UNREACHABLE("No ancestor with frame?");
+ return nsLayoutUtils::GetContainingBlockForClientRect(frame);
+}
+
+nsRect LocalAccessible::ParentRelativeBounds() {
+ nsIFrame* frame = GetFrame();
+ if (frame && mContent) {
+ nsIFrame* boundingFrame = FindNearestAccessibleAncestorFrame();
+ nsRect result = nsLayoutUtils::GetAllInFlowRectsUnion(frame, boundingFrame);
+
+ if (result.IsEmpty()) {
+ // If we end up with a 0x0 rect from above (or one with negative
+ // height/width) we should try using the ink overflow rect instead. If we
+ // use this rect, our relative bounds will match the bounds of what
+ // appears visually. We do this because some web authors (icloud.com for
+ // example) employ things like 0x0 buttons with visual overflow. Without
+ // this, such frames aren't navigable by screen readers.
+ result = frame->InkOverflowRectRelativeToSelf();
+ nsLayoutUtils::TransformRect(frame, boundingFrame, result);
+ }
+
+ if (nsIScrollableFrame* sf =
+ mParent == mDoc
+ ? mDoc->PresShellPtr()->GetRootScrollFrameAsScrollable()
+ : boundingFrame->GetScrollTargetFrame()) {
+ // If boundingFrame has a scroll position, result is currently relative
+ // to that. Instead, we want result to remain the same regardless of
+ // scrolling. We then subtract the scroll position later when calculating
+ // absolute bounds. We do this because we don't want to push cache
+ // updates for the bounds of all descendants every time we scroll.
+ nsPoint scrollPos = sf->GetScrollPosition().ApplyResolution(
+ mDoc->PresShellPtr()->GetResolution());
+ result.MoveBy(scrollPos.x, scrollPos.y);
+ }
+
+ return result;
+ }
+
+ return nsRect();
+}
+
+nsRect LocalAccessible::RelativeBounds(nsIFrame** aBoundingFrame) const {
+ nsIFrame* frame = GetFrame();
+ if (frame && mContent) {
+ *aBoundingFrame = nsLayoutUtils::GetContainingBlockForClientRect(frame);
+ nsRect unionRect = nsLayoutUtils::GetAllInFlowRectsUnion(
+ frame, *aBoundingFrame, nsLayoutUtils::RECTS_ACCOUNT_FOR_TRANSFORMS);
+
+ if (unionRect.IsEmpty()) {
+ // If we end up with a 0x0 rect from above (or one with negative
+ // height/width) we should try using the ink overflow rect instead. If we
+ // use this rect, our relative bounds will match the bounds of what
+ // appears visually. We do this because some web authors (icloud.com for
+ // example) employ things like 0x0 buttons with visual overflow. Without
+ // this, such frames aren't navigable by screen readers.
+ nsRect overflow = frame->InkOverflowRectRelativeToSelf();
+ nsLayoutUtils::TransformRect(frame, *aBoundingFrame, overflow);
+ return overflow;
+ }
+
+ return unionRect;
+ }
+
+ return nsRect();
+}
+
+nsRect LocalAccessible::BoundsInAppUnits() const {
+ nsIFrame* boundingFrame = nullptr;
+ nsRect unionRectTwips = RelativeBounds(&boundingFrame);
+ if (!boundingFrame) {
+ return nsRect();
+ }
+
+ PresShell* presShell = mDoc->PresContext()->PresShell();
+
+ // We need to inverse translate with the offset of the edge of the visual
+ // viewport from top edge of the layout viewport.
+ nsPoint viewportOffset = presShell->GetVisualViewportOffset() -
+ presShell->GetLayoutViewportOffset();
+ unionRectTwips.MoveBy(-viewportOffset);
+
+ // We need to take into account a non-1 resolution set on the presshell.
+ // This happens with async pinch zooming. Here we scale the bounds before
+ // adding the screen-relative offset.
+ unionRectTwips.ScaleRoundOut(presShell->GetResolution());
+ // We have the union of the rectangle, now we need to put it in absolute
+ // screen coords.
+ nsRect orgRectPixels = boundingFrame->GetScreenRectInAppUnits();
+ unionRectTwips.MoveBy(orgRectPixels.X(), orgRectPixels.Y());
+
+ return unionRectTwips;
+}
+
+LayoutDeviceIntRect LocalAccessible::Bounds() const {
+ return LayoutDeviceIntRect::FromAppUnitsToNearest(
+ BoundsInAppUnits(), mDoc->PresContext()->AppUnitsPerDevPixel());
+}
+
+void LocalAccessible::SetSelected(bool aSelect) {
+ if (!HasOwnContent()) return;
+
+ LocalAccessible* select = nsAccUtils::GetSelectableContainer(this, State());
+ if (select) {
+ if (select->State() & states::MULTISELECTABLE) {
+ if (mContent->IsElement() && ARIARoleMap()) {
+ if (aSelect) {
+ mContent->AsElement()->SetAttr(
+ kNameSpaceID_None, nsGkAtoms::aria_selected, u"true"_ns, true);
+ } else {
+ mContent->AsElement()->UnsetAttr(kNameSpaceID_None,
+ nsGkAtoms::aria_selected, true);
+ }
+ }
+ return;
+ }
+
+ if (aSelect) TakeFocus();
+ }
+}
+
+void LocalAccessible::TakeSelection() {
+ LocalAccessible* select = nsAccUtils::GetSelectableContainer(this, State());
+ if (select) {
+ if (select->State() & states::MULTISELECTABLE) select->UnselectAll();
+ SetSelected(true);
+ }
+}
+
+void LocalAccessible::TakeFocus() const {
+ nsIFrame* frame = GetFrame();
+ if (!frame) return;
+
+ nsIContent* focusContent = mContent;
+
+ // If the accessible focus is managed by container widget then focus the
+ // widget and set the accessible as its current item.
+ if (!frame->IsFocusable()) {
+ LocalAccessible* widget = ContainerWidget();
+ if (widget && widget->AreItemsOperable()) {
+ nsIContent* widgetElm = widget->GetContent();
+ nsIFrame* widgetFrame = widgetElm->GetPrimaryFrame();
+ if (widgetFrame && widgetFrame->IsFocusable()) {
+ focusContent = widgetElm;
+ widget->SetCurrentItem(this);
+ }
+ }
+ }
+
+ if (RefPtr<nsFocusManager> fm = nsFocusManager::GetFocusManager()) {
+ dom::AutoHandlingUserInputStatePusher inputStatePusher(true);
+ // XXXbz: Can we actually have a non-element content here?
+ RefPtr<dom::Element> element = dom::Element::FromNodeOrNull(focusContent);
+ fm->SetFocus(element, 0);
+ }
+}
+
+void LocalAccessible::NameFromAssociatedXULLabel(DocAccessible* aDocument,
+ nsIContent* aElm,
+ nsString& aName) {
+ LocalAccessible* label = nullptr;
+ XULLabelIterator iter(aDocument, aElm);
+ while ((label = iter.Next())) {
+ // Check if label's value attribute is used
+ label->Elm()->GetAttr(kNameSpaceID_None, nsGkAtoms::value, aName);
+ if (aName.IsEmpty()) {
+ // If no value attribute, a non-empty label must contain
+ // children that define its text -- possibly using HTML
+ nsTextEquivUtils::AppendTextEquivFromContent(label, label->Elm(), &aName);
+ }
+ }
+ aName.CompressWhitespace();
+}
+
+void LocalAccessible::XULElmName(DocAccessible* aDocument, nsIContent* aElm,
+ nsString& aName) {
+ /**
+ * 3 main cases for XUL Controls to be labeled
+ * 1 - control contains label="foo"
+ * 2 - non-child label contains control="controlID"
+ * - label has either value="foo" or children
+ * 3 - name from subtree; e.g. a child label element
+ * Cases 1 and 2 are handled here.
+ * Case 3 is handled by GetNameFromSubtree called in NativeName.
+ * Once a label is found, the search is discontinued, so a control
+ * that has a label attribute as well as having a label external to
+ * the control that uses the control="controlID" syntax will use
+ * the label attribute for its Name.
+ */
+
+ // CASE #1 (via label attribute) -- great majority of the cases
+ // Only do this if this is not a select control element, which uses label
+ // attribute to indicate, which option is selected.
+ nsCOMPtr<nsIDOMXULSelectControlElement> select =
+ aElm->AsElement()->AsXULSelectControl();
+ if (!select) {
+ aElm->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::label, aName);
+ }
+
+ // CASE #2 -- label as <label control="id" ... ></label>
+ if (aName.IsEmpty()) {
+ NameFromAssociatedXULLabel(aDocument, aElm, aName);
+ }
+
+ aName.CompressWhitespace();
+}
+
+nsresult LocalAccessible::HandleAccEvent(AccEvent* aEvent) {
+ NS_ENSURE_ARG_POINTER(aEvent);
+
+ if (profiler_thread_is_being_profiled_for_markers()) {
+ nsAutoCString strEventType;
+ GetAccService()->GetStringEventType(aEvent->GetEventType(), strEventType);
+ nsAutoCString strMarker;
+ strMarker.AppendLiteral("A11y Event - ");
+ strMarker.Append(strEventType);
+ PROFILER_MARKER_UNTYPED(strMarker, A11Y);
+ }
+
+ if (IPCAccessibilityActive() && Document()) {
+ DocAccessibleChild* ipcDoc = mDoc->IPCDoc();
+ // If ipcDoc is null, we can't fire the event to the client. We shouldn't
+ // have fired the event in the first place, since this makes events
+ // inconsistent for local and remote documents. To avoid this, don't call
+ // nsEventShell::FireEvent on a DocAccessible for which
+ // HasLoadState(eTreeConstructed) is false.
+ MOZ_ASSERT(ipcDoc);
+ if (ipcDoc) {
+ uint64_t id = aEvent->GetAccessible()->ID();
+
+ switch (aEvent->GetEventType()) {
+ case nsIAccessibleEvent::EVENT_SHOW:
+ ipcDoc->ShowEvent(downcast_accEvent(aEvent));
+ break;
+
+ case nsIAccessibleEvent::EVENT_HIDE:
+ ipcDoc->SendHideEvent(id, aEvent->IsFromUserInput());
+ break;
+
+ case nsIAccessibleEvent::EVENT_INNER_REORDER:
+ case nsIAccessibleEvent::EVENT_REORDER:
+ if (IsTable()) {
+ SendCache(CacheDomain::Table, CacheUpdateType::Update);
+ }
+
+#if defined(XP_WIN)
+ if (StaticPrefs::accessibility_cache_enabled_AtStartup() &&
+ HasOwnContent() && mContent->IsMathMLElement()) {
+ // For any change in a MathML subtree, update the innerHTML cache on
+ // the root math element.
+ for (LocalAccessible* acc = this; acc; acc = acc->LocalParent()) {
+ if (acc->HasOwnContent() &&
+ acc->mContent->IsMathMLElement(nsGkAtoms::math)) {
+ mDoc->QueueCacheUpdate(acc, CacheDomain::InnerHTML);
+ }
+ }
+ }
+#endif // defined(XP_WIN)
+
+ // reorder events on the application acc aren't necessary to tell the
+ // parent about new top level documents.
+ if (!aEvent->GetAccessible()->IsApplication()) {
+ ipcDoc->SendEvent(id, aEvent->GetEventType());
+ }
+ break;
+ case nsIAccessibleEvent::EVENT_STATE_CHANGE: {
+ AccStateChangeEvent* event = downcast_accEvent(aEvent);
+ ipcDoc->SendStateChangeEvent(id, event->GetState(),
+ event->IsStateEnabled());
+ break;
+ }
+ case nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED: {
+ AccCaretMoveEvent* event = downcast_accEvent(aEvent);
+ ipcDoc->SendCaretMoveEvent(
+ id, event->GetCaretOffset(), event->IsSelectionCollapsed(),
+ event->IsAtEndOfLine(), event->GetGranularity());
+ break;
+ }
+ case nsIAccessibleEvent::EVENT_TEXT_INSERTED:
+ case nsIAccessibleEvent::EVENT_TEXT_REMOVED: {
+ AccTextChangeEvent* event = downcast_accEvent(aEvent);
+ const nsString& text = event->ModifiedText();
+#if defined(XP_WIN)
+ // On Windows with the cache disabled, events for live region updates
+ // containing embedded objects require us to dispatch synchronous
+ // events.
+ bool sync = !StaticPrefs::accessibility_cache_enabled_AtStartup() &&
+ text.Contains(L'\xfffc') &&
+ nsAccUtils::IsARIALive(aEvent->GetAccessible());
+#endif
+ ipcDoc->SendTextChangeEvent(id, text, event->GetStartOffset(),
+ event->GetLength(),
+ event->IsTextInserted(),
+ event->IsFromUserInput()
+#if defined(XP_WIN)
+ // This parameter only exists on Windows.
+ ,
+ sync
+#endif
+ );
+ break;
+ }
+ case nsIAccessibleEvent::EVENT_SELECTION:
+ case nsIAccessibleEvent::EVENT_SELECTION_ADD:
+ case nsIAccessibleEvent::EVENT_SELECTION_REMOVE: {
+ AccSelChangeEvent* selEvent = downcast_accEvent(aEvent);
+ ipcDoc->SendSelectionEvent(id, selEvent->Widget()->ID(),
+ aEvent->GetEventType());
+ break;
+ }
+ case nsIAccessibleEvent::EVENT_VIRTUALCURSOR_CHANGED: {
+ AccVCChangeEvent* vcEvent = downcast_accEvent(aEvent);
+ LocalAccessible* position = vcEvent->NewAccessible();
+ LocalAccessible* oldPosition = vcEvent->OldAccessible();
+ ipcDoc->SendVirtualCursorChangeEvent(
+ id, oldPosition ? oldPosition->ID() : 0,
+ vcEvent->OldStartOffset(), vcEvent->OldEndOffset(),
+ position ? position->ID() : 0, vcEvent->NewStartOffset(),
+ vcEvent->NewEndOffset(), vcEvent->Reason(),
+ vcEvent->BoundaryType(), vcEvent->IsFromUserInput());
+ break;
+ }
+#if defined(XP_WIN)
+ case nsIAccessibleEvent::EVENT_FOCUS: {
+ ipcDoc->SendFocusEvent(id);
+ break;
+ }
+#endif
+ case nsIAccessibleEvent::EVENT_SCROLLING_END:
+ case nsIAccessibleEvent::EVENT_SCROLLING: {
+ AccScrollingEvent* scrollingEvent = downcast_accEvent(aEvent);
+ ipcDoc->SendScrollingEvent(
+ id, aEvent->GetEventType(), scrollingEvent->ScrollX(),
+ scrollingEvent->ScrollY(), scrollingEvent->MaxScrollX(),
+ scrollingEvent->MaxScrollY());
+ break;
+ }
+#if !defined(XP_WIN)
+ case nsIAccessibleEvent::EVENT_ANNOUNCEMENT: {
+ AccAnnouncementEvent* announcementEvent = downcast_accEvent(aEvent);
+ ipcDoc->SendAnnouncementEvent(id, announcementEvent->Announcement(),
+ announcementEvent->Priority());
+ break;
+ }
+#endif // !defined(XP_WIN)
+ case nsIAccessibleEvent::EVENT_TEXT_SELECTION_CHANGED: {
+#if defined(XP_WIN)
+ if (!StaticPrefs::accessibility_cache_enabled_AtStartup()) {
+ // On Windows, when the cache is disabled, we have to defer events
+ // until we are notified that the DocAccessibleParent has been
+ // constructed, which needs specific code for each event payload.
+ // Since we don't need a special event payload for text selection in
+ // this case anyway, just send it as a generic event.
+ ipcDoc->SendEvent(id, aEvent->GetEventType());
+ break;
+ }
+#endif // defined(XP_WIN)
+ AccTextSelChangeEvent* textSelChangeEvent = downcast_accEvent(aEvent);
+ AutoTArray<TextRange, 1> ranges;
+ textSelChangeEvent->SelectionRanges(&ranges);
+ nsTArray<TextRangeData> textRangeData(ranges.Length());
+ for (size_t i = 0; i < ranges.Length(); i++) {
+ const TextRange& range = ranges.ElementAt(i);
+ LocalAccessible* start = range.StartContainer()->AsLocal();
+ LocalAccessible* end = range.EndContainer()->AsLocal();
+ textRangeData.AppendElement(TextRangeData(start->ID(), end->ID(),
+ range.StartOffset(),
+ range.EndOffset()));
+ }
+ ipcDoc->SendTextSelectionChangeEvent(id, textRangeData);
+ break;
+ }
+ case nsIAccessibleEvent::EVENT_DESCRIPTION_CHANGE:
+ case nsIAccessibleEvent::EVENT_NAME_CHANGE: {
+ SendCache(CacheDomain::NameAndDescription, CacheUpdateType::Update);
+ ipcDoc->SendEvent(id, aEvent->GetEventType());
+ break;
+ }
+ case nsIAccessibleEvent::EVENT_TEXT_VALUE_CHANGE:
+ case nsIAccessibleEvent::EVENT_VALUE_CHANGE: {
+ SendCache(CacheDomain::Value, CacheUpdateType::Update);
+ ipcDoc->SendEvent(id, aEvent->GetEventType());
+ break;
+ }
+ default:
+ ipcDoc->SendEvent(id, aEvent->GetEventType());
+ }
+ }
+ }
+
+ if (nsCoreUtils::AccEventObserversExist()) {
+ nsCoreUtils::DispatchAccEvent(MakeXPCEvent(aEvent));
+ }
+
+ return NS_OK;
+}
+
+already_AddRefed<AccAttributes> LocalAccessible::Attributes() {
+ RefPtr<AccAttributes> attributes = NativeAttributes();
+ if (!HasOwnContent() || !mContent->IsElement()) return attributes.forget();
+
+ // 'xml-roles' attribute coming from ARIA.
+ nsString xmlRoles;
+ if (nsAccUtils::GetARIAAttr(mContent->AsElement(), nsGkAtoms::role,
+ xmlRoles) &&
+ !xmlRoles.IsEmpty()) {
+ attributes->SetAttribute(nsGkAtoms::xmlroles, std::move(xmlRoles));
+ } else if (nsAtom* landmark = LandmarkRole()) {
+ // 'xml-roles' attribute for landmark.
+ attributes->SetAttribute(nsGkAtoms::xmlroles, landmark);
+ }
+
+ // Expose object attributes from ARIA attributes.
+ aria::AttrIterator attribIter(mContent);
+ while (attribIter.Next()) {
+ attribIter.ExposeAttr(attributes);
+ }
+
+ // If there is no aria-live attribute then expose default value of 'live'
+ // object attribute used for ARIA role of this accessible.
+ const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
+ if (roleMapEntry) {
+ if (roleMapEntry->Is(nsGkAtoms::searchbox)) {
+ attributes->SetAttribute(nsGkAtoms::textInputType, nsGkAtoms::search);
+ }
+
+ if (!attributes->HasAttribute(nsGkAtoms::aria_live)) {
+ nsString live;
+ if (nsAccUtils::GetLiveAttrValue(roleMapEntry->liveAttRule, live)) {
+ attributes->SetAttribute(nsGkAtoms::aria_live, std::move(live));
+ }
+ }
+ }
+
+ return attributes.forget();
+}
+
+already_AddRefed<AccAttributes> LocalAccessible::NativeAttributes() {
+ RefPtr<AccAttributes> attributes = new AccAttributes();
+
+ // We support values, so expose the string value as well, via the valuetext
+ // object attribute. We test for the value interface because we don't want
+ // to expose traditional Value() information such as URL's on links and
+ // documents, or text in an input.
+ if (HasNumericValue()) {
+ nsString valuetext;
+ Value(valuetext);
+ attributes->SetAttribute(nsGkAtoms::aria_valuetext, std::move(valuetext));
+ }
+
+ // Expose checkable object attribute if the accessible has checkable state
+ if (State() & states::CHECKABLE) {
+ attributes->SetAttribute(nsGkAtoms::checkable, true);
+ }
+
+ // Expose 'explicit-name' attribute.
+ nsAutoString name;
+ if (Name(name) != eNameFromSubtree && !name.IsVoid()) {
+ attributes->SetAttribute(nsGkAtoms::explicit_name, true);
+ }
+
+ // Group attributes (level/setsize/posinset)
+ GroupPos groupPos = GroupPosition();
+ nsAccUtils::SetAccGroupAttrs(attributes, groupPos.level, groupPos.setSize,
+ groupPos.posInSet);
+
+ bool hierarchical = false;
+ uint32_t itemCount = AccGroupInfo::TotalItemCount(this, &hierarchical);
+ if (itemCount) {
+ attributes->SetAttribute(nsGkAtoms::child_item_count,
+ static_cast<int32_t>(itemCount));
+ }
+
+ if (hierarchical) {
+ attributes->SetAttribute(nsGkAtoms::tree, true);
+ }
+
+ // If the accessible doesn't have own content (such as list item bullet or
+ // xul tree item) then don't calculate content based attributes.
+ if (!HasOwnContent()) return attributes.forget();
+
+ nsEventShell::GetEventAttributes(GetNode(), attributes);
+
+ // Get container-foo computed live region properties based on the closest
+ // container with the live region attribute. Inner nodes override outer nodes
+ // within the same document. The inner nodes can be used to override live
+ // region behavior on more general outer nodes.
+ nsAccUtils::SetLiveContainerAttributes(attributes, this);
+
+ if (!mContent->IsElement()) return attributes.forget();
+
+ nsString id;
+ if (nsCoreUtils::GetID(mContent, id)) {
+ attributes->SetAttribute(nsGkAtoms::id, std::move(id));
+ }
+
+ // Expose class because it may have useful microformat information.
+ nsString _class;
+ if (mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::_class,
+ _class)) {
+ attributes->SetAttribute(nsGkAtoms::_class, std::move(_class));
+ }
+
+ // Expose tag.
+ attributes->SetAttribute(nsGkAtoms::tag, mContent->NodeInfo()->NameAtom());
+
+ // Expose draggable object attribute.
+ if (auto htmlElement = nsGenericHTMLElement::FromNode(mContent)) {
+ if (htmlElement->Draggable()) {
+ attributes->SetAttribute(nsGkAtoms::draggable, true);
+ }
+ }
+
+ // Don't calculate CSS-based object attributes when:
+ // 1. There is no frame (e.g. the accessible is unattached from the tree).
+ // 2. This is an image map area. CSS is irrelevant here. Furthermore, we won't
+ // be able to get the computed style if the map is unslotted in a shadow host.
+ if (!mContent->GetPrimaryFrame() ||
+ mContent->IsHTMLElement(nsGkAtoms::area)) {
+ return attributes.forget();
+ }
+
+ // CSS style based object attributes.
+ nsAutoString value;
+ StyleInfo styleInfo(mContent->AsElement());
+
+ // Expose 'display' attribute.
+ RefPtr<nsAtom> displayValue = styleInfo.Display();
+ attributes->SetAttribute(nsGkAtoms::display, displayValue);
+
+ // Expose 'text-align' attribute.
+ RefPtr<nsAtom> textAlignValue = styleInfo.TextAlign();
+ attributes->SetAttribute(nsGkAtoms::textAlign, textAlignValue);
+
+ // Expose 'text-indent' attribute.
+ mozilla::LengthPercentage textIndent = styleInfo.TextIndent();
+ if (textIndent.ConvertsToLength()) {
+ attributes->SetAttribute(nsGkAtoms::textIndent,
+ textIndent.ToLengthInCSSPixels());
+ } else if (textIndent.ConvertsToPercentage()) {
+ attributes->SetAttribute(nsGkAtoms::textIndent, textIndent.ToPercentage());
+ }
+
+ // Expose 'margin-left' attribute.
+ attributes->SetAttribute(nsGkAtoms::marginLeft, styleInfo.MarginLeft());
+
+ // Expose 'margin-right' attribute.
+ attributes->SetAttribute(nsGkAtoms::marginRight, styleInfo.MarginRight());
+
+ // Expose 'margin-top' attribute.
+ attributes->SetAttribute(nsGkAtoms::marginTop, styleInfo.MarginTop());
+
+ // Expose 'margin-bottom' attribute.
+ attributes->SetAttribute(nsGkAtoms::marginBottom, styleInfo.MarginBottom());
+
+ // Expose data-at-shortcutkeys attribute for web applications and virtual
+ // cursors. Currently mostly used by JAWS.
+ nsString atShortcutKeys;
+ if (mContent->AsElement()->GetAttr(
+ kNameSpaceID_None, nsGkAtoms::dataAtShortcutkeys, atShortcutKeys)) {
+ attributes->SetAttribute(nsGkAtoms::dataAtShortcutkeys,
+ std::move(atShortcutKeys));
+ }
+
+ return attributes.forget();
+}
+
+bool LocalAccessible::AttributeChangesState(nsAtom* aAttribute) {
+ return aAttribute == nsGkAtoms::aria_disabled ||
+ aAttribute == nsGkAtoms::disabled ||
+ aAttribute == nsGkAtoms::tabindex ||
+ aAttribute == nsGkAtoms::aria_required ||
+ aAttribute == nsGkAtoms::aria_invalid ||
+ aAttribute == nsGkAtoms::aria_expanded ||
+ aAttribute == nsGkAtoms::aria_checked ||
+ (aAttribute == nsGkAtoms::aria_pressed && IsButton()) ||
+ aAttribute == nsGkAtoms::aria_readonly ||
+ aAttribute == nsGkAtoms::aria_current ||
+ aAttribute == nsGkAtoms::aria_haspopup ||
+ aAttribute == nsGkAtoms::aria_busy ||
+ aAttribute == nsGkAtoms::aria_multiline ||
+ aAttribute == nsGkAtoms::aria_multiselectable ||
+ aAttribute == nsGkAtoms::contenteditable;
+}
+
+void LocalAccessible::DOMAttributeChanged(int32_t aNameSpaceID,
+ nsAtom* aAttribute, int32_t aModType,
+ const nsAttrValue* aOldValue,
+ uint64_t aOldState) {
+ // Fire accessible event after short timer, because we need to wait for
+ // DOM attribute & resulting layout to actually change. Otherwise,
+ // assistive technology will retrieve the wrong state/value/selection info.
+
+ // XXX todo
+ // We still need to handle special HTML cases here
+ // For example, if an <img>'s usemap attribute is modified
+ // Otherwise it may just be a state change, for example an object changing
+ // its visibility
+ //
+ // XXX todo: report aria state changes for "undefined" literal value changes
+ // filed as bug 472142
+ //
+ // XXX todo: invalidate accessible when aria state changes affect exposed
+ // role filed as bug 472143
+
+ if (AttributeChangesState(aAttribute)) {
+ uint64_t currState = State();
+ uint64_t diffState = currState ^ aOldState;
+ if (diffState) {
+ for (uint64_t state = 1; state <= states::LAST_ENTRY; state <<= 1) {
+ if (diffState & state) {
+ RefPtr<AccEvent> stateChangeEvent =
+ new AccStateChangeEvent(this, state, (currState & state));
+ mDoc->FireDelayedEvent(stateChangeEvent);
+ }
+ }
+ }
+ }
+
+ // When a details object has its open attribute changed
+ // we should fire a state-change event on the accessible of
+ // its main summary
+ if (aAttribute == nsGkAtoms::open) {
+ // FromDetails checks if the given accessible belongs to
+ // a details frame and also locates the accessible of its
+ // main summary.
+ if (HTMLSummaryAccessible* summaryAccessible =
+ HTMLSummaryAccessible::FromDetails(this)) {
+ RefPtr<AccEvent> expandedChangeEvent =
+ new AccStateChangeEvent(summaryAccessible, states::EXPANDED);
+ mDoc->FireDelayedEvent(expandedChangeEvent);
+ return;
+ }
+ }
+
+ // Check for namespaced ARIA attribute
+ if (aNameSpaceID == kNameSpaceID_None) {
+ // Check for hyphenated aria-foo property?
+ if (StringBeginsWith(nsDependentAtomString(aAttribute), u"aria-"_ns)) {
+ uint8_t attrFlags = aria::AttrCharacteristicsFor(aAttribute);
+ if (!(attrFlags & ATTR_BYPASSOBJ)) {
+ mDoc->QueueCacheUpdate(this, CacheDomain::ARIA);
+ // For aria attributes like drag and drop changes we fire a generic
+ // attribute change event; at least until native API comes up with a
+ // more meaningful event.
+ RefPtr<AccEvent> event =
+ new AccObjectAttrChangedEvent(this, aAttribute);
+ mDoc->FireDelayedEvent(event);
+ }
+ }
+ }
+
+ dom::Element* elm = Elm();
+
+ if (HasNumericValue() &&
+ (aAttribute == nsGkAtoms::aria_valuemax ||
+ aAttribute == nsGkAtoms::aria_valuemin || aAttribute == nsGkAtoms::min ||
+ aAttribute == nsGkAtoms::max || aAttribute == nsGkAtoms::step)) {
+ SendCache(CacheDomain::Value, CacheUpdateType::Update);
+ return;
+ }
+
+ // Fire text value change event whenever aria-valuetext is changed.
+ if (aAttribute == nsGkAtoms::aria_valuetext) {
+ mDoc->FireDelayedEvent(nsIAccessibleEvent::EVENT_TEXT_VALUE_CHANGE, this);
+ return;
+ }
+
+ if (aAttribute == nsGkAtoms::aria_valuenow) {
+ if (!nsAccUtils::HasARIAAttr(elm, nsGkAtoms::aria_valuetext) ||
+ nsAccUtils::ARIAAttrValueIs(elm, nsGkAtoms::aria_valuetext,
+ nsGkAtoms::_empty, eCaseMatters)) {
+ // Fire numeric value change event when aria-valuenow is changed and
+ // aria-valuetext is empty
+ mDoc->FireDelayedEvent(nsIAccessibleEvent::EVENT_VALUE_CHANGE, this);
+ } else {
+ // We need to update the cache here since we won't get an event if
+ // aria-valuenow is shadowed by aria-valuetext.
+ SendCache(CacheDomain::Value, CacheUpdateType::Update);
+ }
+ return;
+ }
+
+ if (aAttribute == nsGkAtoms::aria_owns) {
+ mDoc->Controller()->ScheduleRelocation(this);
+ }
+
+ // Fire name change and description change events.
+ if (aAttribute == nsGkAtoms::aria_label) {
+ // A valid aria-labelledby would take precedence so an aria-label change
+ // won't change the name.
+ IDRefsIterator iter(mDoc, elm, nsGkAtoms::aria_labelledby);
+ if (!iter.NextElem()) {
+ mDoc->FireDelayedEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, this);
+ }
+ return;
+ }
+
+ if (aAttribute == nsGkAtoms::aria_describedby) {
+ mDoc->QueueCacheUpdate(this, CacheDomain::Relations);
+ mDoc->FireDelayedEvent(nsIAccessibleEvent::EVENT_DESCRIPTION_CHANGE, this);
+ if (aModType == dom::MutationEvent_Binding::MODIFICATION ||
+ aModType == dom::MutationEvent_Binding::ADDITION) {
+ // The subtrees of the new aria-describedby targets might be used to
+ // compute the description for this. Therefore, we need to set
+ // the eHasDescriptionDependent flag on all Accessibles in these subtrees.
+ IDRefsIterator iter(mDoc, elm, nsGkAtoms::aria_describedby);
+ while (LocalAccessible* target = iter.Next()) {
+ target->ModifySubtreeContextFlags(eHasDescriptionDependent, true);
+ }
+ }
+ return;
+ }
+
+ if (aAttribute == nsGkAtoms::aria_labelledby) {
+ // We only queue cache updates for explicit relations. Implicit, reverse
+ // relations are handled in ApplyCache and stored in a map on the remote
+ // document itself.
+ mDoc->QueueCacheUpdate(this, CacheDomain::Relations);
+ mDoc->FireDelayedEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, this);
+ if (aModType == dom::MutationEvent_Binding::MODIFICATION ||
+ aModType == dom::MutationEvent_Binding::ADDITION) {
+ // The subtrees of the new aria-labelledby targets might be used to
+ // compute the name for this. Therefore, we need to set
+ // the eHasNameDependent flag on all Accessibles in these subtrees.
+ IDRefsIterator iter(mDoc, elm, nsGkAtoms::aria_labelledby);
+ while (LocalAccessible* target = iter.Next()) {
+ target->ModifySubtreeContextFlags(eHasNameDependent, true);
+ }
+ }
+ return;
+ }
+
+ if ((aAttribute == nsGkAtoms::aria_expanded ||
+ aAttribute == nsGkAtoms::href) &&
+ (aModType == dom::MutationEvent_Binding::ADDITION ||
+ aModType == dom::MutationEvent_Binding::REMOVAL)) {
+ // The presence of aria-expanded adds an expand/collapse action.
+ SendCache(CacheDomain::Actions, CacheUpdateType::Update);
+ }
+
+ if (aAttribute == nsGkAtoms::href) {
+ mDoc->QueueCacheUpdate(this, CacheDomain::Value);
+ }
+
+ if (aAttribute == nsGkAtoms::aria_controls ||
+ aAttribute == nsGkAtoms::aria_flowto) {
+ mDoc->QueueCacheUpdate(this, CacheDomain::Relations);
+ }
+
+ if (aAttribute == nsGkAtoms::alt &&
+ !nsAccUtils::HasARIAAttr(elm, nsGkAtoms::aria_label) &&
+ !elm->HasAttr(nsGkAtoms::aria_labelledby)) {
+ mDoc->FireDelayedEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, this);
+ return;
+ }
+
+ if (aAttribute == nsGkAtoms::title) {
+ nsAutoString name;
+ ARIAName(name);
+ if (name.IsEmpty()) {
+ NativeName(name);
+ if (name.IsEmpty()) {
+ mDoc->FireDelayedEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, this);
+ return;
+ }
+ }
+
+ if (!elm->HasAttr(nsGkAtoms::aria_describedby)) {
+ mDoc->FireDelayedEvent(nsIAccessibleEvent::EVENT_DESCRIPTION_CHANGE,
+ this);
+ }
+
+ return;
+ }
+
+ // ARIA or XUL selection
+ if ((mContent->IsXULElement() && aAttribute == nsGkAtoms::selected) ||
+ aAttribute == nsGkAtoms::aria_selected) {
+ LocalAccessible* widget = nsAccUtils::GetSelectableContainer(this, State());
+ if (widget) {
+ AccSelChangeEvent::SelChangeType selChangeType;
+ if (aNameSpaceID != kNameSpaceID_None) {
+ selChangeType = elm->AttrValueIs(aNameSpaceID, aAttribute,
+ nsGkAtoms::_true, eCaseMatters)
+ ? AccSelChangeEvent::eSelectionAdd
+ : AccSelChangeEvent::eSelectionRemove;
+ } else {
+ selChangeType = nsAccUtils::ARIAAttrValueIs(
+ elm, aAttribute, nsGkAtoms::_true, eCaseMatters)
+ ? AccSelChangeEvent::eSelectionAdd
+ : AccSelChangeEvent::eSelectionRemove;
+ }
+
+ RefPtr<AccEvent> event =
+ new AccSelChangeEvent(widget, this, selChangeType);
+ mDoc->FireDelayedEvent(event);
+ if (aAttribute == nsGkAtoms::aria_selected) {
+ mDoc->QueueCacheUpdate(this, CacheDomain::State);
+ }
+ }
+
+ return;
+ }
+
+ if (aAttribute == nsGkAtoms::aria_level ||
+ aAttribute == nsGkAtoms::aria_setsize ||
+ aAttribute == nsGkAtoms::aria_posinset) {
+ SendCache(CacheDomain::GroupInfo, CacheUpdateType::Update);
+ return;
+ }
+
+ if (aAttribute == nsGkAtoms::accesskey) {
+ mDoc->QueueCacheUpdate(this, CacheDomain::Actions);
+ }
+
+ if (aAttribute == nsGkAtoms::name &&
+ (mContent && mContent->IsHTMLElement(nsGkAtoms::a))) {
+ // If an anchor's name changed, it's possible a LINKS_TO relation
+ // also changed. Push a cache update for Relations.
+ mDoc->QueueCacheUpdate(this, CacheDomain::Relations);
+ }
+
+ if (aAttribute == nsGkAtoms::slot &&
+ !mContent->GetFlattenedTreeParentNode() && this != mDoc) {
+ // This is inside a shadow host but is no longer slotted.
+ mDoc->ContentRemoved(this);
+ }
+}
+
+void LocalAccessible::ARIAGroupPosition(int32_t* aLevel, int32_t* aSetSize,
+ int32_t* aPosInSet) const {
+ if (!mContent) {
+ return;
+ }
+
+ if (aLevel) {
+ nsCoreUtils::GetUIntAttr(mContent, nsGkAtoms::aria_level, aLevel);
+ }
+ if (aSetSize) {
+ nsCoreUtils::GetUIntAttr(mContent, nsGkAtoms::aria_setsize, aSetSize);
+ }
+ if (aPosInSet) {
+ nsCoreUtils::GetUIntAttr(mContent, nsGkAtoms::aria_posinset, aPosInSet);
+ }
+}
+
+uint64_t LocalAccessible::State() {
+ if (IsDefunct()) return states::DEFUNCT;
+
+ uint64_t state = NativeState();
+ // Apply ARIA states to be sure accessible states will be overridden.
+ ApplyARIAState(&state);
+
+ const uint32_t kExpandCollapseStates = states::COLLAPSED | states::EXPANDED;
+ if ((state & kExpandCollapseStates) == kExpandCollapseStates) {
+ // Cannot be both expanded and collapsed -- this happens in ARIA expanded
+ // combobox because of limitation of ARIAMap.
+ // XXX: Perhaps we will be able to make this less hacky if we support
+ // extended states in ARIAMap, e.g. derive COLLAPSED from
+ // EXPANDABLE && !EXPANDED.
+ state &= ~states::COLLAPSED;
+ }
+
+ if (!(state & states::UNAVAILABLE)) {
+ state |= states::ENABLED | states::SENSITIVE;
+
+ // If the object is a current item of container widget then mark it as
+ // ACTIVE. This allows screen reader virtual buffer modes to know which
+ // descendant is the current one that would get focus if the user navigates
+ // to the container widget.
+ LocalAccessible* widget = ContainerWidget();
+ if (widget && widget->CurrentItem() == this) state |= states::ACTIVE;
+ }
+
+ if ((state & states::COLLAPSED) || (state & states::EXPANDED)) {
+ state |= states::EXPANDABLE;
+ }
+
+ ApplyImplicitState(state);
+ return state;
+}
+
+void LocalAccessible::ApplyARIAState(uint64_t* aState) const {
+ if (!mContent->IsElement()) return;
+
+ dom::Element* element = mContent->AsElement();
+
+ // Test for universal states first
+ *aState |= aria::UniversalStatesFor(element);
+
+ const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
+ if (roleMapEntry) {
+ // We only force the readonly bit off if we have a real mapping for the aria
+ // role. This preserves the ability for screen readers to use readonly
+ // (primarily on the document) as the hint for creating a virtual buffer.
+ if (roleMapEntry->role != roles::NOTHING) *aState &= ~states::READONLY;
+
+ if (mContent->HasID()) {
+ // If has a role & ID and aria-activedescendant on the container, assume
+ // focusable.
+ const LocalAccessible* ancestor = this;
+ while ((ancestor = ancestor->LocalParent()) && !ancestor->IsDoc()) {
+ dom::Element* el = ancestor->Elm();
+ if (el && el->HasAttr(nsGkAtoms::aria_activedescendant)) {
+ *aState |= states::FOCUSABLE;
+ break;
+ }
+ }
+ }
+ }
+
+ if (*aState & states::FOCUSABLE) {
+ // Propogate aria-disabled from ancestors down to any focusable descendant.
+ const LocalAccessible* ancestor = this;
+ while ((ancestor = ancestor->LocalParent()) && !ancestor->IsDoc()) {
+ dom::Element* el = ancestor->Elm();
+ if (el && nsAccUtils::ARIAAttrValueIs(el, nsGkAtoms::aria_disabled,
+ nsGkAtoms::_true, eCaseMatters)) {
+ *aState |= states::UNAVAILABLE;
+ break;
+ }
+ }
+ } else {
+ // Sometimes, we use aria-activedescendant targeting something which isn't
+ // actually a descendant. This is technically a spec violation, but it's a
+ // useful hack which makes certain things much easier. For example, we use
+ // this for "fake focus" for multi select browser tabs and Quantumbar
+ // autocomplete suggestions.
+ // In these cases, the aria-activedescendant code above won't make the
+ // active item focusable. It doesn't make sense for something to have
+ // focus when it isn't focusable, so fix that here.
+ if (FocusMgr()->IsActiveItem(this)) {
+ *aState |= states::FOCUSABLE;
+ }
+ }
+
+ // special case: A native button element whose role got transformed by ARIA to
+ // a toggle button Also applies to togglable button menus, like in the Dev
+ // Tools Web Console.
+ if (IsButton() || IsMenuButton()) {
+ aria::MapToState(aria::eARIAPressed, element, aState);
+ }
+
+ if (!roleMapEntry) return;
+
+ *aState |= roleMapEntry->state;
+
+ if (aria::MapToState(roleMapEntry->attributeMap1, element, aState) &&
+ aria::MapToState(roleMapEntry->attributeMap2, element, aState) &&
+ aria::MapToState(roleMapEntry->attributeMap3, element, aState)) {
+ aria::MapToState(roleMapEntry->attributeMap4, element, aState);
+ }
+
+ // ARIA gridcell inherits readonly state from the grid until it's overridden.
+ if ((roleMapEntry->Is(nsGkAtoms::gridcell) ||
+ roleMapEntry->Is(nsGkAtoms::columnheader) ||
+ roleMapEntry->Is(nsGkAtoms::rowheader)) &&
+ !nsAccUtils::HasDefinedARIAToken(mContent, nsGkAtoms::aria_readonly)) {
+ const TableCellAccessible* cell = AsTableCell();
+ if (cell) {
+ TableAccessible* table = cell->Table();
+ if (table) {
+ LocalAccessible* grid = table->AsAccessible();
+ uint64_t gridState = 0;
+ grid->ApplyARIAState(&gridState);
+ *aState |= gridState & states::READONLY;
+ }
+ }
+ }
+}
+
+void LocalAccessible::Value(nsString& aValue) const {
+ if (HasNumericValue()) {
+ // aria-valuenow is a number, and aria-valuetext is the optional text
+ // equivalent. For the string value, we will try the optional text
+ // equivalent first.
+ if (!mContent->IsElement()) {
+ return;
+ }
+
+ if (!nsAccUtils::GetARIAAttr(mContent->AsElement(),
+ nsGkAtoms::aria_valuetext, aValue)) {
+ if (!NativeHasNumericValue()) {
+ double checkValue = CurValue();
+ if (!IsNaN(checkValue)) {
+ aValue.AppendFloat(checkValue);
+ }
+ }
+ }
+ return;
+ }
+
+ const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
+ if (!roleMapEntry) {
+ return;
+ }
+
+ // Value of textbox is a textified subtree.
+ if (roleMapEntry->Is(nsGkAtoms::textbox)) {
+ nsTextEquivUtils::GetTextEquivFromSubtree(this, aValue);
+ return;
+ }
+
+ // Value of combobox is a text of current or selected item.
+ if (roleMapEntry->Is(nsGkAtoms::combobox)) {
+ LocalAccessible* option = CurrentItem();
+ if (!option) {
+ uint32_t childCount = ChildCount();
+ for (uint32_t idx = 0; idx < childCount; idx++) {
+ LocalAccessible* child = mChildren.ElementAt(idx);
+ if (child->IsListControl()) {
+ Accessible* acc = child->GetSelectedItem(0);
+ option = acc ? acc->AsLocal() : nullptr;
+ break;
+ }
+ }
+ }
+
+ // If there's a selected item, get the value from it. Otherwise, determine
+ // the value from descendant elements.
+ nsTextEquivUtils::GetTextEquivFromSubtree(option ? option : this, aValue);
+ }
+}
+
+double LocalAccessible::MaxValue() const {
+ double checkValue = AttrNumericValue(nsGkAtoms::aria_valuemax);
+ if (IsNaN(checkValue) && !NativeHasNumericValue()) {
+ // aria-valuemax isn't present and this element doesn't natively provide a
+ // maximum value. Use the ARIA default.
+ const nsRoleMapEntry* roleMap = ARIARoleMap();
+ if (roleMap && roleMap->role == roles::SPINBUTTON) {
+ return UnspecifiedNaN<double>();
+ }
+ return 100;
+ }
+ return checkValue;
+}
+
+double LocalAccessible::MinValue() const {
+ double checkValue = AttrNumericValue(nsGkAtoms::aria_valuemin);
+ if (IsNaN(checkValue) && !NativeHasNumericValue()) {
+ // aria-valuemin isn't present and this element doesn't natively provide a
+ // minimum value. Use the ARIA default.
+ const nsRoleMapEntry* roleMap = ARIARoleMap();
+ if (roleMap && roleMap->role == roles::SPINBUTTON) {
+ return UnspecifiedNaN<double>();
+ }
+ return 0;
+ }
+ return checkValue;
+}
+
+double LocalAccessible::Step() const {
+ return UnspecifiedNaN<double>(); // no mimimum increment (step) in ARIA.
+}
+
+double LocalAccessible::CurValue() const {
+ double checkValue = AttrNumericValue(nsGkAtoms::aria_valuenow);
+ if (IsNaN(checkValue) && !NativeHasNumericValue()) {
+ // aria-valuenow isn't present and this element doesn't natively provide a
+ // current value. Use the ARIA default.
+ const nsRoleMapEntry* roleMap = ARIARoleMap();
+ if (roleMap && roleMap->role == roles::SPINBUTTON) {
+ return UnspecifiedNaN<double>();
+ }
+ double minValue = MinValue();
+ return minValue + ((MaxValue() - minValue) / 2);
+ }
+
+ return checkValue;
+}
+
+bool LocalAccessible::SetCurValue(double aValue) {
+ const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
+ if (!roleMapEntry || roleMapEntry->valueRule == eNoValue) return false;
+
+ const uint32_t kValueCannotChange = states::READONLY | states::UNAVAILABLE;
+ if (State() & kValueCannotChange) return false;
+
+ double checkValue = MinValue();
+ if (!IsNaN(checkValue) && aValue < checkValue) return false;
+
+ checkValue = MaxValue();
+ if (!IsNaN(checkValue) && aValue > checkValue) return false;
+
+ nsAutoString strValue;
+ strValue.AppendFloat(aValue);
+
+ if (!mContent->IsElement()) return true;
+
+ return NS_SUCCEEDED(mContent->AsElement()->SetAttr(
+ kNameSpaceID_None, nsGkAtoms::aria_valuenow, strValue, true));
+}
+
+role LocalAccessible::ARIATransformRole(role aRole) const {
+ // Beginning with ARIA 1.1, user agents are expected to use the native host
+ // language role of the element when the region role is used without a name.
+ // https://rawgit.com/w3c/aria/master/core-aam/core-aam.html#role-map-region
+ //
+ // XXX: While the name computation algorithm can be non-trivial in the general
+ // case, it should not be especially bad here: If the author hasn't used the
+ // region role, this calculation won't occur. And the region role's name
+ // calculation rule excludes name from content. That said, this use case is
+ // another example of why we should consider caching the accessible name. See:
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1378235.
+ if (aRole == roles::REGION) {
+ nsAutoString name;
+ Name(name);
+ return name.IsEmpty() ? NativeRole() : aRole;
+ }
+
+ // XXX: these unfortunate exceptions don't fit into the ARIA table. This is
+ // where the accessible role depends on both the role and ARIA state.
+ if (aRole == roles::PUSHBUTTON) {
+ if (nsAccUtils::HasDefinedARIAToken(mContent, nsGkAtoms::aria_pressed)) {
+ // For simplicity, any existing pressed attribute except "" or "undefined"
+ // indicates a toggle.
+ return roles::TOGGLE_BUTTON;
+ }
+
+ if (mContent->IsElement() &&
+ nsAccUtils::ARIAAttrValueIs(mContent->AsElement(),
+ nsGkAtoms::aria_haspopup, nsGkAtoms::_true,
+ eCaseMatters)) {
+ // For button with aria-haspopup="true".
+ return roles::BUTTONMENU;
+ }
+
+ } else if (aRole == roles::LISTBOX) {
+ // A listbox inside of a combobox needs a special role because of ATK
+ // mapping to menu.
+ if (mParent && mParent->IsCombobox()) {
+ return roles::COMBOBOX_LIST;
+ }
+
+ } else if (aRole == roles::OPTION) {
+ if (mParent && mParent->Role() == roles::COMBOBOX_LIST) {
+ return roles::COMBOBOX_OPTION;
+ }
+
+ } else if (aRole == roles::MENUITEM) {
+ // Menuitem has a submenu.
+ if (mContent->IsElement() &&
+ nsAccUtils::ARIAAttrValueIs(mContent->AsElement(),
+ nsGkAtoms::aria_haspopup, nsGkAtoms::_true,
+ eCaseMatters)) {
+ return roles::PARENT_MENUITEM;
+ }
+
+ } else if (aRole == roles::CELL) {
+ // A cell inside an ancestor table element that has a grid role needs a
+ // gridcell role
+ // (https://www.w3.org/TR/html-aam-1.0/#html-element-role-mappings).
+ const TableCellAccessible* cell = AsTableCell();
+ if (cell) {
+ TableAccessible* table = cell->Table();
+ if (table && table->AsAccessible()->IsARIARole(nsGkAtoms::grid)) {
+ return roles::GRID_CELL;
+ }
+ }
+ }
+
+ return aRole;
+}
+
+role LocalAccessible::NativeRole() const { return roles::NOTHING; }
+
+uint8_t LocalAccessible::ActionCount() const {
+ return HasPrimaryAction() || ActionAncestor() ? 1 : 0;
+}
+
+void LocalAccessible::ActionNameAt(uint8_t aIndex, nsAString& aName) {
+ aName.Truncate();
+
+ if (aIndex != 0) return;
+
+ uint32_t actionRule = GetActionRule();
+
+ switch (actionRule) {
+ case eActivateAction:
+ aName.AssignLiteral("activate");
+ return;
+
+ case eClickAction:
+ aName.AssignLiteral("click");
+ return;
+
+ case ePressAction:
+ aName.AssignLiteral("press");
+ return;
+
+ case eCheckUncheckAction: {
+ uint64_t state = State();
+ if (state & states::CHECKED) {
+ aName.AssignLiteral("uncheck");
+ } else if (state & states::MIXED) {
+ aName.AssignLiteral("cycle");
+ } else {
+ aName.AssignLiteral("check");
+ }
+ return;
+ }
+
+ case eJumpAction:
+ aName.AssignLiteral("jump");
+ return;
+
+ case eOpenCloseAction:
+ if (State() & states::COLLAPSED) {
+ aName.AssignLiteral("open");
+ } else {
+ aName.AssignLiteral("close");
+ }
+ return;
+
+ case eSelectAction:
+ aName.AssignLiteral("select");
+ return;
+
+ case eSwitchAction:
+ aName.AssignLiteral("switch");
+ return;
+
+ case eSortAction:
+ aName.AssignLiteral("sort");
+ return;
+
+ case eExpandAction:
+ if (State() & states::COLLAPSED) {
+ aName.AssignLiteral("expand");
+ } else {
+ aName.AssignLiteral("collapse");
+ }
+ return;
+ }
+
+ if (ActionAncestor()) {
+ aName.AssignLiteral("click ancestor");
+ return;
+ }
+}
+
+bool LocalAccessible::DoAction(uint8_t aIndex) const {
+ if (aIndex != 0) return false;
+
+ if (HasPrimaryAction() || ActionAncestor()) {
+ DoCommand();
+ return true;
+ }
+
+ return false;
+}
+
+bool LocalAccessible::HasPrimaryAction() const {
+ return GetActionRule() != eNoAction;
+}
+
+nsIContent* LocalAccessible::GetAtomicRegion() const {
+ nsIContent* loopContent = mContent;
+ nsAutoString atomic;
+ while (loopContent &&
+ (!loopContent->IsElement() ||
+ !nsAccUtils::GetARIAAttr(loopContent->AsElement(),
+ nsGkAtoms::aria_atomic, atomic))) {
+ loopContent = loopContent->GetParent();
+ }
+
+ return atomic.EqualsLiteral("true") ? loopContent : nullptr;
+}
+
+Relation LocalAccessible::RelationByType(RelationType aType) const {
+ if (!HasOwnContent()) return Relation();
+
+ const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
+
+ // Relationships are defined on the same content node that the role would be
+ // defined on.
+ switch (aType) {
+ case RelationType::LABELLED_BY: {
+ Relation rel(
+ new IDRefsIterator(mDoc, mContent, nsGkAtoms::aria_labelledby));
+ if (mContent->IsHTMLElement()) {
+ rel.AppendIter(new HTMLLabelIterator(Document(), this));
+ }
+ rel.AppendIter(new XULLabelIterator(Document(), mContent));
+
+ return rel;
+ }
+
+ case RelationType::LABEL_FOR: {
+ Relation rel(new RelatedAccIterator(Document(), mContent,
+ nsGkAtoms::aria_labelledby));
+ if (mContent->IsXULElement(nsGkAtoms::label)) {
+ rel.AppendIter(new IDRefsIterator(mDoc, mContent, nsGkAtoms::control));
+ }
+
+ return rel;
+ }
+
+ case RelationType::DESCRIBED_BY: {
+ Relation rel(
+ new IDRefsIterator(mDoc, mContent, nsGkAtoms::aria_describedby));
+ if (mContent->IsXULElement()) {
+ rel.AppendIter(new XULDescriptionIterator(Document(), mContent));
+ }
+
+ return rel;
+ }
+
+ case RelationType::DESCRIPTION_FOR: {
+ Relation rel(new RelatedAccIterator(Document(), mContent,
+ nsGkAtoms::aria_describedby));
+
+ // This affectively adds an optional control attribute to xul:description,
+ // which only affects accessibility, by allowing the description to be
+ // tied to a control.
+ if (mContent->IsXULElement(nsGkAtoms::description)) {
+ rel.AppendIter(new IDRefsIterator(mDoc, mContent, nsGkAtoms::control));
+ }
+
+ return rel;
+ }
+
+ case RelationType::NODE_CHILD_OF: {
+ Relation rel;
+ // This is an ARIA tree or treegrid that doesn't use owns, so we need to
+ // get the parent the hard way.
+ if (roleMapEntry && (roleMapEntry->role == roles::OUTLINEITEM ||
+ roleMapEntry->role == roles::LISTITEM ||
+ roleMapEntry->role == roles::ROW)) {
+ Accessible* parent = const_cast<LocalAccessible*>(this)
+ ->GetOrCreateGroupInfo()
+ ->ConceptualParent();
+ if (parent) {
+ MOZ_ASSERT(parent->IsLocal());
+ rel.AppendTarget(parent->AsLocal());
+ }
+ }
+
+ // If this is an OOP iframe document, we can't support NODE_CHILD_OF
+ // here, since the iframe resides in a different process. This is fine
+ // because the client will then request the parent instead, which will be
+ // correctly handled by platform code.
+ if (XRE_IsContentProcess() && IsRoot()) {
+ dom::Document* doc =
+ const_cast<LocalAccessible*>(this)->AsDoc()->DocumentNode();
+ dom::BrowsingContext* bc = doc->GetBrowsingContext();
+ MOZ_ASSERT(bc);
+ if (!bc->Top()->IsInProcess()) {
+ return rel;
+ }
+ }
+
+ // If accessible is in its own Window, or is the root of a document,
+ // then we should provide NODE_CHILD_OF relation so that MSAA clients
+ // can easily get to true parent instead of getting to oleacc's
+ // ROLE_WINDOW accessible which will prevent us from going up further
+ // (because it is system generated and has no idea about the hierarchy
+ // above it).
+ nsIFrame* frame = GetFrame();
+ if (frame) {
+ nsView* view = frame->GetView();
+ if (view) {
+ nsIScrollableFrame* scrollFrame = do_QueryFrame(frame);
+ if (scrollFrame || view->GetWidget() || !frame->GetParent()) {
+ rel.AppendTarget(LocalParent());
+ }
+ }
+ }
+
+ return rel;
+ }
+
+ case RelationType::NODE_PARENT_OF: {
+ // ARIA tree or treegrid can do the hierarchy by @aria-level, ARIA trees
+ // also can be organized by groups.
+ if (roleMapEntry && (roleMapEntry->role == roles::OUTLINEITEM ||
+ roleMapEntry->role == roles::LISTITEM ||
+ roleMapEntry->role == roles::ROW ||
+ roleMapEntry->role == roles::OUTLINE ||
+ roleMapEntry->role == roles::LIST ||
+ roleMapEntry->role == roles::TREE_TABLE)) {
+ return Relation(new ItemIterator(this));
+ }
+
+ return Relation();
+ }
+
+ case RelationType::CONTROLLED_BY:
+ return Relation(new RelatedAccIterator(Document(), mContent,
+ nsGkAtoms::aria_controls));
+
+ case RelationType::CONTROLLER_FOR: {
+ Relation rel(
+ new IDRefsIterator(mDoc, mContent, nsGkAtoms::aria_controls));
+ rel.AppendIter(new HTMLOutputIterator(Document(), mContent));
+ return rel;
+ }
+
+ case RelationType::FLOWS_TO:
+ return Relation(
+ new IDRefsIterator(mDoc, mContent, nsGkAtoms::aria_flowto));
+
+ case RelationType::FLOWS_FROM:
+ return Relation(
+ new RelatedAccIterator(Document(), mContent, nsGkAtoms::aria_flowto));
+
+ case RelationType::MEMBER_OF: {
+ if (Role() == roles::RADIOBUTTON) {
+ /* If we see a radio button role here, we're dealing with an aria
+ * radio button (because input=radio buttons are
+ * HTMLRadioButtonAccessibles) */
+ Relation rel = Relation();
+ LocalAccessible* currParent = LocalParent();
+ while (currParent && currParent->Role() != roles::RADIO_GROUP) {
+ currParent = currParent->LocalParent();
+ }
+
+ if (currParent && currParent->Role() == roles::RADIO_GROUP) {
+ /* If we found a radiogroup parent, search for all
+ * roles::RADIOBUTTON children and add them to our relation.
+ * This search will include the radio button this method
+ * was called from, which is expected. */
+ Pivot p = Pivot(currParent);
+ PivotRoleRule rule(roles::RADIOBUTTON);
+ Accessible* match = p.Next(currParent, rule);
+ while (match) {
+ MOZ_ASSERT(match->IsLocal(),
+ "We shouldn't find any remote accs while building our "
+ "relation!");
+ rel.AppendTarget(match->AsLocal());
+ match = p.Next(match, rule);
+ }
+ }
+
+ /* By webkit's standard, aria radio buttons do not get grouped
+ * if they lack a group parent, so we return an empty
+ * relation here if the above check fails. */
+
+ return rel;
+ }
+
+ return Relation(mDoc, GetAtomicRegion());
+ }
+
+ case RelationType::LINKS_TO: {
+ Relation rel = Relation();
+ if (Role() == roles::LINK) {
+ dom::HTMLAnchorElement* anchor =
+ dom::HTMLAnchorElement::FromNode(mContent);
+ if (!anchor) {
+ return rel;
+ }
+ // If this node is an anchor element, query its hash to find the
+ // target.
+ nsAutoString hash;
+ anchor->GetHash(hash);
+ if (hash.IsEmpty()) {
+ return rel;
+ }
+
+ // GetHash returns an ID or name with a leading '#', trim it so we can
+ // search the doc by ID or name alone.
+ hash.Trim("#");
+ if (dom::Element* elm = mContent->OwnerDoc()->GetElementById(hash)) {
+ rel.AppendTarget(mDoc->GetAccessibleOrContainer(elm));
+ } else if (nsCOMPtr<nsINodeList> list =
+ mContent->OwnerDoc()->GetElementsByName(hash)) {
+ // Loop through the named nodes looking for the first anchor
+ uint32_t length = list->Length();
+ for (uint32_t i = 0; i < length; i++) {
+ nsIContent* node = list->Item(i);
+ if (node->IsHTMLElement(nsGkAtoms::a)) {
+ rel.AppendTarget(mDoc->GetAccessibleOrContainer(node));
+ break;
+ }
+ }
+ }
+ }
+
+ return rel;
+ }
+
+ case RelationType::SUBWINDOW_OF:
+ case RelationType::EMBEDS:
+ case RelationType::EMBEDDED_BY:
+ case RelationType::POPUP_FOR:
+ case RelationType::PARENT_WINDOW_OF:
+ return Relation();
+
+ case RelationType::DEFAULT_BUTTON: {
+ if (mContent->IsHTMLElement()) {
+ // HTML form controls implements nsIFormControl interface.
+ nsCOMPtr<nsIFormControl> control(do_QueryInterface(mContent));
+ if (control) {
+ if (dom::HTMLFormElement* form = control->GetForm()) {
+ return Relation(mDoc, form->GetDefaultSubmitElement());
+ }
+ }
+ } else {
+ // In XUL, use first <button default="true" .../> in the document
+ dom::Document* doc = mContent->OwnerDoc();
+ nsIContent* buttonEl = nullptr;
+ if (doc->AllowXULXBL()) {
+ nsCOMPtr<nsIHTMLCollection> possibleDefaultButtons =
+ doc->GetElementsByAttribute(u"default"_ns, u"true"_ns);
+ if (possibleDefaultButtons) {
+ uint32_t length = possibleDefaultButtons->Length();
+ // Check for button in list of default="true" elements
+ for (uint32_t count = 0; count < length && !buttonEl; count++) {
+ nsIContent* item = possibleDefaultButtons->Item(count);
+ RefPtr<nsIDOMXULButtonElement> button =
+ item->IsElement() ? item->AsElement()->AsXULButton()
+ : nullptr;
+ if (button) {
+ buttonEl = item;
+ }
+ }
+ }
+ return Relation(mDoc, buttonEl);
+ }
+ }
+ return Relation();
+ }
+
+ case RelationType::CONTAINING_DOCUMENT:
+ return Relation(mDoc);
+
+ case RelationType::CONTAINING_TAB_PANE: {
+ nsCOMPtr<nsIDocShell> docShell = nsCoreUtils::GetDocShellFor(GetNode());
+ if (docShell) {
+ // Walk up the parent chain without crossing the boundary at which item
+ // types change, preventing us from walking up out of tab content.
+ nsCOMPtr<nsIDocShellTreeItem> root;
+ docShell->GetInProcessSameTypeRootTreeItem(getter_AddRefs(root));
+ if (root) {
+ // If the item type is typeContent, we assume we are in browser tab
+ // content. Note, this includes content such as about:addons,
+ // for consistency.
+ if (root->ItemType() == nsIDocShellTreeItem::typeContent) {
+ return Relation(nsAccUtils::GetDocAccessibleFor(root));
+ }
+ }
+ }
+ return Relation();
+ }
+
+ case RelationType::CONTAINING_APPLICATION:
+ return Relation(ApplicationAcc());
+
+ case RelationType::DETAILS:
+ return Relation(
+ new IDRefsIterator(mDoc, mContent, nsGkAtoms::aria_details));
+
+ case RelationType::DETAILS_FOR:
+ return Relation(
+ new RelatedAccIterator(mDoc, mContent, nsGkAtoms::aria_details));
+
+ case RelationType::ERRORMSG:
+ return Relation(
+ new IDRefsIterator(mDoc, mContent, nsGkAtoms::aria_errormessage));
+
+ case RelationType::ERRORMSG_FOR:
+ return Relation(
+ new RelatedAccIterator(mDoc, mContent, nsGkAtoms::aria_errormessage));
+
+ default:
+ return Relation();
+ }
+}
+
+void LocalAccessible::GetNativeInterface(void** aNativeAccessible) {}
+
+void LocalAccessible::DoCommand(nsIContent* aContent,
+ uint32_t aActionIndex) const {
+ class Runnable final : public mozilla::Runnable {
+ public:
+ Runnable(const LocalAccessible* aAcc, nsIContent* aContent, uint32_t aIdx)
+ : mozilla::Runnable("Runnable"),
+ mAcc(aAcc),
+ mContent(aContent),
+ mIdx(aIdx) {}
+
+ // XXX Cannot mark as MOZ_CAN_RUN_SCRIPT because the base class change
+ // requires too big changes across a lot of modules.
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD Run() override {
+ if (mAcc) {
+ MOZ_KnownLive(mAcc)->DispatchClickEvent(MOZ_KnownLive(mContent), mIdx);
+ }
+ return NS_OK;
+ }
+
+ void Revoke() {
+ mAcc = nullptr;
+ mContent = nullptr;
+ }
+
+ private:
+ RefPtr<const LocalAccessible> mAcc;
+ nsCOMPtr<nsIContent> mContent;
+ uint32_t mIdx;
+ };
+
+ nsIContent* content = aContent ? aContent : mContent.get();
+ nsCOMPtr<nsIRunnable> runnable = new Runnable(this, content, aActionIndex);
+ NS_DispatchToMainThread(runnable);
+}
+
+void LocalAccessible::DispatchClickEvent(nsIContent* aContent,
+ uint32_t aActionIndex) const {
+ if (IsDefunct()) return;
+
+ RefPtr<PresShell> presShell = mDoc->PresShellPtr();
+
+ // Scroll into view.
+ presShell->ScrollContentIntoView(aContent, ScrollAxis(), ScrollAxis(),
+ ScrollFlags::ScrollOverflowHidden);
+
+ AutoWeakFrame frame = aContent->GetPrimaryFrame();
+ if (!frame) return;
+
+ // Compute x and y coordinates.
+ nsPoint point;
+ nsCOMPtr<nsIWidget> widget = frame->GetNearestWidget(point);
+ if (!widget) return;
+
+ nsSize size = frame->GetSize();
+
+ RefPtr<nsPresContext> presContext = presShell->GetPresContext();
+ int32_t x = presContext->AppUnitsToDevPixels(point.x + size.width / 2);
+ int32_t y = presContext->AppUnitsToDevPixels(point.y + size.height / 2);
+
+ // Simulate a touch interaction by dispatching touch events with mouse events.
+ nsCoreUtils::DispatchTouchEvent(eTouchStart, x, y, aContent, frame, presShell,
+ widget);
+ nsCoreUtils::DispatchMouseEvent(eMouseDown, x, y, aContent, frame, presShell,
+ widget);
+ nsCoreUtils::DispatchTouchEvent(eTouchEnd, x, y, aContent, frame, presShell,
+ widget);
+ nsCoreUtils::DispatchMouseEvent(eMouseUp, x, y, aContent, frame, presShell,
+ widget);
+}
+
+void LocalAccessible::ScrollToPoint(uint32_t aCoordinateType, int32_t aX,
+ int32_t aY) {
+ nsIFrame* frame = GetFrame();
+ if (!frame) return;
+
+ LayoutDeviceIntPoint coords =
+ nsAccUtils::ConvertToScreenCoords(aX, aY, aCoordinateType, this);
+
+ nsIFrame* parentFrame = frame;
+ while ((parentFrame = parentFrame->GetParent())) {
+ nsCoreUtils::ScrollFrameToPoint(parentFrame, frame, coords);
+ }
+}
+
+void LocalAccessible::AppendTextTo(nsAString& aText, uint32_t aStartOffset,
+ uint32_t aLength) {
+ // Return text representation of non-text accessible within hypertext
+ // accessible. Text accessible overrides this method to return enclosed text.
+ if (aStartOffset != 0 || aLength == 0) return;
+
+ MOZ_ASSERT(mParent,
+ "Called on accessible unbound from tree. Result can be wrong.");
+ nsIFrame* frame = GetFrame();
+ // We handle something becoming display: none async, which means we won't have
+ // a frame when we're queuing text removed events. Thus, it's important that
+ // we produce text here even if there's no frame. Otherwise, we won't fire a
+ // text removed event at all, which might leave client caches (e.g. NVDA
+ // virtual buffers) with dead nodes.
+ if (IsHTMLBr() || (frame && frame->IsBrFrame())) {
+ aText += kForcedNewLineChar;
+ } else if (mParent && nsAccUtils::MustPrune(mParent)) {
+ // Expose the embedded object accessible as imaginary embedded object
+ // character if its parent hypertext accessible doesn't expose children to
+ // AT.
+ aText += kImaginaryEmbeddedObjectChar;
+ } else {
+ aText += kEmbeddedObjectChar;
+ }
+}
+
+void LocalAccessible::Shutdown() {
+ // Mark the accessible as defunct, invalidate the child count and pointers to
+ // other accessibles, also make sure none of its children point to this
+ // parent
+ mStateFlags |= eIsDefunct;
+
+ int32_t childCount = mChildren.Length();
+ for (int32_t childIdx = 0; childIdx < childCount; childIdx++) {
+ mChildren.ElementAt(childIdx)->UnbindFromParent();
+ }
+ mChildren.Clear();
+
+ mEmbeddedObjCollector = nullptr;
+
+ if (mParent) mParent->RemoveChild(this);
+
+ mContent = nullptr;
+ mDoc = nullptr;
+ if (SelectionMgr() && SelectionMgr()->AccessibleWithCaret(nullptr) == this) {
+ SelectionMgr()->ResetCaretOffset();
+ }
+}
+
+// LocalAccessible protected
+void LocalAccessible::ARIAName(nsString& aName) const {
+ // aria-labelledby now takes precedence over aria-label
+ nsresult rv = nsTextEquivUtils::GetTextEquivFromIDRefs(
+ this, nsGkAtoms::aria_labelledby, aName);
+ if (NS_SUCCEEDED(rv)) {
+ aName.CompressWhitespace();
+ }
+
+ if (aName.IsEmpty() && mContent->IsElement() &&
+ nsAccUtils::GetARIAAttr(mContent->AsElement(), nsGkAtoms::aria_label,
+ aName)) {
+ aName.CompressWhitespace();
+ }
+}
+
+// LocalAccessible protected
+void LocalAccessible::ARIADescription(nsString& aDescription) const {
+ // aria-describedby takes precedence over aria-description
+ nsresult rv = nsTextEquivUtils::GetTextEquivFromIDRefs(
+ this, nsGkAtoms::aria_describedby, aDescription);
+ if (NS_SUCCEEDED(rv)) {
+ aDescription.CompressWhitespace();
+ }
+
+ if (aDescription.IsEmpty() && mContent->IsElement() &&
+ nsAccUtils::GetARIAAttr(mContent->AsElement(),
+ nsGkAtoms::aria_description, aDescription)) {
+ aDescription.CompressWhitespace();
+ }
+}
+
+// LocalAccessible protected
+ENameValueFlag LocalAccessible::NativeName(nsString& aName) const {
+ if (mContent->IsHTMLElement()) {
+ LocalAccessible* label = nullptr;
+ HTMLLabelIterator iter(Document(), this);
+ while ((label = iter.Next())) {
+ nsTextEquivUtils::AppendTextEquivFromContent(this, label->GetContent(),
+ &aName);
+ aName.CompressWhitespace();
+ }
+
+ if (!aName.IsEmpty()) return eNameOK;
+
+ NameFromAssociatedXULLabel(mDoc, mContent, aName);
+ if (!aName.IsEmpty()) {
+ return eNameOK;
+ }
+
+ nsTextEquivUtils::GetNameFromSubtree(this, aName);
+ return aName.IsEmpty() ? eNameOK : eNameFromSubtree;
+ }
+
+ if (mContent->IsXULElement()) {
+ XULElmName(mDoc, mContent, aName);
+ if (!aName.IsEmpty()) return eNameOK;
+
+ nsTextEquivUtils::GetNameFromSubtree(this, aName);
+ return aName.IsEmpty() ? eNameOK : eNameFromSubtree;
+ }
+
+ if (mContent->IsSVGElement()) {
+ // If user agents need to choose among multiple 'desc' or 'title'
+ // elements for processing, the user agent shall choose the first one.
+ for (nsIContent* childElm = mContent->GetFirstChild(); childElm;
+ childElm = childElm->GetNextSibling()) {
+ if (childElm->IsSVGElement(nsGkAtoms::title)) {
+ nsTextEquivUtils::AppendTextEquivFromContent(this, childElm, &aName);
+ return eNameOK;
+ }
+ }
+ }
+
+ return eNameOK;
+}
+
+// LocalAccessible protected
+void LocalAccessible::NativeDescription(nsString& aDescription) const {
+ bool isXUL = mContent->IsXULElement();
+ if (isXUL) {
+ // Try XUL <description control="[id]">description text</description>
+ XULDescriptionIterator iter(Document(), mContent);
+ LocalAccessible* descr = nullptr;
+ while ((descr = iter.Next())) {
+ nsTextEquivUtils::AppendTextEquivFromContent(this, descr->GetContent(),
+ &aDescription);
+ }
+ }
+}
+
+// LocalAccessible protected
+void LocalAccessible::BindToParent(LocalAccessible* aParent,
+ uint32_t aIndexInParent) {
+ MOZ_ASSERT(aParent, "This method isn't used to set null parent");
+ MOZ_ASSERT(!mParent, "The child was expected to be moved");
+
+#ifdef A11Y_LOG
+ if (mParent) {
+ logging::TreeInfo("BindToParent: stealing accessible", 0, "old parent",
+ mParent, "new parent", aParent, "child", this, nullptr);
+ }
+#endif
+
+ mParent = aParent;
+ mIndexInParent = aIndexInParent;
+
+ if (mParent->HasNameDependent() || mParent->IsXULListItem() ||
+ RelationByType(RelationType::LABEL_FOR).Next() ||
+ nsTextEquivUtils::HasNameRule(mParent, eNameFromSubtreeRule)) {
+ mContextFlags |= eHasNameDependent;
+ } else {
+ mContextFlags &= ~eHasNameDependent;
+ }
+ if (mParent->HasDescriptionDependent() ||
+ RelationByType(RelationType::DESCRIPTION_FOR).Next()) {
+ mContextFlags |= eHasDescriptionDependent;
+ } else {
+ mContextFlags &= ~eHasDescriptionDependent;
+ }
+
+ // Add name/description dependent flags for dependent content once
+ // a name/description provider is added to doc.
+ Relation rel = RelationByType(RelationType::LABELLED_BY);
+ LocalAccessible* relTarget = nullptr;
+ while ((relTarget = rel.LocalNext())) {
+ if (!relTarget->HasNameDependent()) {
+ relTarget->ModifySubtreeContextFlags(eHasNameDependent, true);
+ }
+ }
+
+ rel = RelationByType(RelationType::DESCRIBED_BY);
+ while ((relTarget = rel.LocalNext())) {
+ if (!relTarget->HasDescriptionDependent()) {
+ relTarget->ModifySubtreeContextFlags(eHasDescriptionDependent, true);
+ }
+ }
+
+ mContextFlags |=
+ static_cast<uint32_t>((mParent->IsAlert() || mParent->IsInsideAlert())) &
+ eInsideAlert;
+
+ if (TableCellAccessible* cell = AsTableCell()) {
+ if (StaticPrefs::accessibility_cache_enabled_AtStartup()) {
+ CachedTableAccessible::Invalidate(this);
+ } else if (Role() == roles::COLUMNHEADER) {
+ // A new column header is being added. Invalidate the table's header
+ // cache.
+ TableAccessible* table = cell->Table();
+ if (table) {
+ table->GetHeaderCache().Clear();
+ }
+ }
+ }
+}
+
+// LocalAccessible protected
+void LocalAccessible::UnbindFromParent() {
+ // We do this here to handle document shutdown and an Accessible being moved.
+ // We do this for subtree removal in DocAccessible::UncacheChildrenInSubtree.
+ if (StaticPrefs::accessibility_cache_enabled_AtStartup() &&
+ (IsTable() || IsTableCell())) {
+ CachedTableAccessible::Invalidate(this);
+ }
+
+ mParent = nullptr;
+ mIndexInParent = -1;
+ mIndexOfEmbeddedChild = -1;
+
+ delete mGroupInfo;
+ mGroupInfo = nullptr;
+ mContextFlags &= ~eHasNameDependent & ~eInsideAlert;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// LocalAccessible public methods
+
+RootAccessible* LocalAccessible::RootAccessible() const {
+ nsCOMPtr<nsIDocShell> docShell = nsCoreUtils::GetDocShellFor(GetNode());
+ NS_ASSERTION(docShell, "No docshell for mContent");
+ if (!docShell) {
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIDocShellTreeItem> root;
+ docShell->GetInProcessRootTreeItem(getter_AddRefs(root));
+ NS_ASSERTION(root, "No root content tree item");
+ if (!root) {
+ return nullptr;
+ }
+
+ DocAccessible* docAcc = nsAccUtils::GetDocAccessibleFor(root);
+ return docAcc ? docAcc->AsRoot() : nullptr;
+}
+
+nsIFrame* LocalAccessible::GetFrame() const {
+ return mContent ? mContent->GetPrimaryFrame() : nullptr;
+}
+
+nsINode* LocalAccessible::GetNode() const { return mContent; }
+
+dom::Element* LocalAccessible::Elm() const {
+ return dom::Element::FromNodeOrNull(mContent);
+}
+
+void LocalAccessible::Language(nsAString& aLanguage) {
+ aLanguage.Truncate();
+
+ if (!mDoc) return;
+
+ nsCoreUtils::GetLanguageFor(mContent, nullptr, aLanguage);
+ if (aLanguage.IsEmpty()) { // Nothing found, so use document's language
+ mDoc->DocumentNode()->GetHeaderData(nsGkAtoms::headerContentLanguage,
+ aLanguage);
+ }
+}
+
+bool LocalAccessible::InsertChildAt(uint32_t aIndex, LocalAccessible* aChild) {
+ if (!aChild) return false;
+
+ if (aIndex == mChildren.Length()) {
+ // XXX(Bug 1631371) Check if this should use a fallible operation as it
+ // pretended earlier.
+ mChildren.AppendElement(aChild);
+ } else {
+ // XXX(Bug 1631371) Check if this should use a fallible operation as it
+ // pretended earlier.
+ mChildren.InsertElementAt(aIndex, aChild);
+
+ MOZ_ASSERT(mStateFlags & eKidsMutating, "Illicit children change");
+
+ for (uint32_t idx = aIndex + 1; idx < mChildren.Length(); idx++) {
+ mChildren[idx]->mIndexInParent = idx;
+ }
+ }
+
+ if (aChild->IsText()) {
+ mStateFlags |= eHasTextKids;
+ }
+
+ aChild->BindToParent(this, aIndex);
+ return true;
+}
+
+bool LocalAccessible::RemoveChild(LocalAccessible* aChild) {
+ MOZ_DIAGNOSTIC_ASSERT(aChild, "No child was given");
+ MOZ_DIAGNOSTIC_ASSERT(aChild->mParent, "No parent");
+ MOZ_DIAGNOSTIC_ASSERT(aChild->mParent == this, "Wrong parent");
+ MOZ_DIAGNOSTIC_ASSERT(aChild->mIndexInParent != -1,
+ "Unbound child was given");
+ MOZ_DIAGNOSTIC_ASSERT((mStateFlags & eKidsMutating) || aChild->IsDefunct() ||
+ aChild->IsDoc() || IsApplication(),
+ "Illicit children change");
+
+ int32_t index = static_cast<uint32_t>(aChild->mIndexInParent);
+ if (mChildren.SafeElementAt(index) != aChild) {
+ MOZ_ASSERT_UNREACHABLE("A wrong child index");
+ index = mChildren.IndexOf(aChild);
+ if (index == -1) {
+ MOZ_ASSERT_UNREACHABLE("No child was found");
+ return false;
+ }
+ }
+
+ aChild->UnbindFromParent();
+ mChildren.RemoveElementAt(index);
+
+ for (uint32_t idx = index; idx < mChildren.Length(); idx++) {
+ mChildren[idx]->mIndexInParent = idx;
+ }
+
+ return true;
+}
+
+void LocalAccessible::RelocateChild(uint32_t aNewIndex,
+ LocalAccessible* aChild) {
+ MOZ_DIAGNOSTIC_ASSERT(aChild, "No child was given");
+ MOZ_DIAGNOSTIC_ASSERT(aChild->mParent == this,
+ "A child from different subtree was given");
+ MOZ_DIAGNOSTIC_ASSERT(aChild->mIndexInParent != -1,
+ "Unbound child was given");
+ MOZ_DIAGNOSTIC_ASSERT(
+ aChild->mParent->LocalChildAt(aChild->mIndexInParent) == aChild,
+ "Wrong index in parent");
+ MOZ_DIAGNOSTIC_ASSERT(
+ static_cast<uint32_t>(aChild->mIndexInParent) != aNewIndex,
+ "No move, same index");
+ MOZ_DIAGNOSTIC_ASSERT(aNewIndex <= mChildren.Length(),
+ "Wrong new index was given");
+
+ RefPtr<AccHideEvent> hideEvent = new AccHideEvent(aChild, false);
+ if (mDoc->Controller()->QueueMutationEvent(hideEvent)) {
+ aChild->SetHideEventTarget(true);
+ }
+
+ mEmbeddedObjCollector = nullptr;
+ mChildren.RemoveElementAt(aChild->mIndexInParent);
+
+ uint32_t startIdx = aNewIndex, endIdx = aChild->mIndexInParent;
+
+ // If the child is moved after its current position.
+ if (static_cast<uint32_t>(aChild->mIndexInParent) < aNewIndex) {
+ startIdx = aChild->mIndexInParent;
+ if (aNewIndex == mChildren.Length() + 1) {
+ // The child is moved to the end.
+ mChildren.AppendElement(aChild);
+ endIdx = mChildren.Length() - 1;
+ } else {
+ mChildren.InsertElementAt(aNewIndex - 1, aChild);
+ endIdx = aNewIndex;
+ }
+ } else {
+ // The child is moved prior its current position.
+ mChildren.InsertElementAt(aNewIndex, aChild);
+ }
+
+ for (uint32_t idx = startIdx; idx <= endIdx; idx++) {
+ mChildren[idx]->mIndexInParent = idx;
+ mChildren[idx]->mIndexOfEmbeddedChild = -1;
+ }
+
+ for (uint32_t idx = 0; idx < mChildren.Length(); idx++) {
+ mChildren[idx]->mStateFlags |= eGroupInfoDirty;
+ }
+
+ RefPtr<AccShowEvent> showEvent = new AccShowEvent(aChild);
+ DebugOnly<bool> added = mDoc->Controller()->QueueMutationEvent(showEvent);
+ MOZ_ASSERT(added);
+ aChild->SetShowEventTarget(true);
+}
+
+LocalAccessible* LocalAccessible::LocalChildAt(uint32_t aIndex) const {
+ LocalAccessible* child = mChildren.SafeElementAt(aIndex, nullptr);
+ if (!child) return nullptr;
+
+#ifdef DEBUG
+ LocalAccessible* realParent = child->mParent;
+ NS_ASSERTION(!realParent || realParent == this,
+ "Two accessibles have the same first child accessible!");
+#endif
+
+ return child;
+}
+
+uint32_t LocalAccessible::ChildCount() const { return mChildren.Length(); }
+
+int32_t LocalAccessible::IndexInParent() const { return mIndexInParent; }
+
+uint32_t LocalAccessible::EmbeddedChildCount() {
+ if (mStateFlags & eHasTextKids) {
+ if (!mEmbeddedObjCollector) {
+ mEmbeddedObjCollector.reset(new EmbeddedObjCollector(this));
+ }
+ return mEmbeddedObjCollector->Count();
+ }
+
+ return ChildCount();
+}
+
+LocalAccessible* LocalAccessible::EmbeddedChildAt(uint32_t aIndex) {
+ if (mStateFlags & eHasTextKids) {
+ if (!mEmbeddedObjCollector) {
+ mEmbeddedObjCollector.reset(new EmbeddedObjCollector(this));
+ }
+ return mEmbeddedObjCollector.get()
+ ? mEmbeddedObjCollector->GetAccessibleAt(aIndex)
+ : nullptr;
+ }
+
+ return LocalChildAt(aIndex);
+}
+
+int32_t LocalAccessible::IndexOfEmbeddedChild(Accessible* aChild) {
+ MOZ_ASSERT(aChild->IsLocal());
+ if (mStateFlags & eHasTextKids) {
+ if (!mEmbeddedObjCollector) {
+ mEmbeddedObjCollector.reset(new EmbeddedObjCollector(this));
+ }
+ return mEmbeddedObjCollector.get()
+ ? mEmbeddedObjCollector->GetIndexAt(aChild->AsLocal())
+ : -1;
+ }
+
+ return GetIndexOf(aChild->AsLocal());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// HyperLinkAccessible methods
+
+bool LocalAccessible::IsLink() const {
+ // Every embedded accessible within hypertext accessible implements
+ // hyperlink interface.
+ return mParent && mParent->IsHyperText() && !IsText();
+}
+
+uint32_t LocalAccessible::AnchorCount() {
+ MOZ_ASSERT(IsLink(), "AnchorCount is called on not hyper link!");
+ return 1;
+}
+
+LocalAccessible* LocalAccessible::AnchorAt(uint32_t aAnchorIndex) {
+ MOZ_ASSERT(IsLink(), "GetAnchor is called on not hyper link!");
+ return aAnchorIndex == 0 ? this : nullptr;
+}
+
+already_AddRefed<nsIURI> LocalAccessible::AnchorURIAt(
+ uint32_t aAnchorIndex) const {
+ MOZ_ASSERT(IsLink(), "AnchorURIAt is called on not hyper link!");
+ return nullptr;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// SelectAccessible
+
+void LocalAccessible::SelectedItems(nsTArray<Accessible*>* aItems) {
+ AccIterator iter(this, filters::GetSelected);
+ LocalAccessible* selected = nullptr;
+ while ((selected = iter.Next())) aItems->AppendElement(selected);
+}
+
+uint32_t LocalAccessible::SelectedItemCount() {
+ uint32_t count = 0;
+ AccIterator iter(this, filters::GetSelected);
+ LocalAccessible* selected = nullptr;
+ while ((selected = iter.Next())) ++count;
+
+ return count;
+}
+
+Accessible* LocalAccessible::GetSelectedItem(uint32_t aIndex) {
+ AccIterator iter(this, filters::GetSelected);
+ LocalAccessible* selected = nullptr;
+
+ uint32_t index = 0;
+ while ((selected = iter.Next()) && index < aIndex) index++;
+
+ return selected;
+}
+
+bool LocalAccessible::IsItemSelected(uint32_t aIndex) {
+ uint32_t index = 0;
+ AccIterator iter(this, filters::GetSelectable);
+ LocalAccessible* selected = nullptr;
+ while ((selected = iter.Next()) && index < aIndex) index++;
+
+ return selected && selected->State() & states::SELECTED;
+}
+
+bool LocalAccessible::AddItemToSelection(uint32_t aIndex) {
+ uint32_t index = 0;
+ AccIterator iter(this, filters::GetSelectable);
+ LocalAccessible* selected = nullptr;
+ while ((selected = iter.Next()) && index < aIndex) index++;
+
+ if (selected) selected->SetSelected(true);
+
+ return static_cast<bool>(selected);
+}
+
+bool LocalAccessible::RemoveItemFromSelection(uint32_t aIndex) {
+ uint32_t index = 0;
+ AccIterator iter(this, filters::GetSelectable);
+ LocalAccessible* selected = nullptr;
+ while ((selected = iter.Next()) && index < aIndex) index++;
+
+ if (selected) selected->SetSelected(false);
+
+ return static_cast<bool>(selected);
+}
+
+bool LocalAccessible::SelectAll() {
+ bool success = false;
+ LocalAccessible* selectable = nullptr;
+
+ AccIterator iter(this, filters::GetSelectable);
+ while ((selectable = iter.Next())) {
+ success = true;
+ selectable->SetSelected(true);
+ }
+ return success;
+}
+
+bool LocalAccessible::UnselectAll() {
+ bool success = false;
+ LocalAccessible* selected = nullptr;
+
+ AccIterator iter(this, filters::GetSelected);
+ while ((selected = iter.Next())) {
+ success = true;
+ selected->SetSelected(false);
+ }
+ return success;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Widgets
+
+bool LocalAccessible::IsWidget() const { return false; }
+
+bool LocalAccessible::IsActiveWidget() const {
+ if (FocusMgr()->HasDOMFocus(mContent)) return true;
+
+ // If text entry of combobox widget has a focus then the combobox widget is
+ // active.
+ const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
+ if (roleMapEntry && roleMapEntry->Is(nsGkAtoms::combobox)) {
+ uint32_t childCount = ChildCount();
+ for (uint32_t idx = 0; idx < childCount; idx++) {
+ LocalAccessible* child = mChildren.ElementAt(idx);
+ if (child->Role() == roles::ENTRY) {
+ return FocusMgr()->HasDOMFocus(child->GetContent());
+ }
+ }
+ }
+
+ return false;
+}
+
+bool LocalAccessible::AreItemsOperable() const {
+ return HasOwnContent() && mContent->IsElement() &&
+ mContent->AsElement()->HasAttr(nsGkAtoms::aria_activedescendant);
+}
+
+LocalAccessible* LocalAccessible::CurrentItem() const {
+ // Check for aria-activedescendant, which changes which element has focus.
+ // For activedescendant, the ARIA spec does not require that the user agent
+ // checks whether pointed node is actually a DOM descendant of the element
+ // with the aria-activedescendant attribute.
+ nsAutoString id;
+ if (HasOwnContent() && mContent->IsElement() &&
+ mContent->AsElement()->GetAttr(nsGkAtoms::aria_activedescendant, id)) {
+ dom::Element* activeDescendantElm = IDRefsIterator::GetElem(mContent, id);
+ if (activeDescendantElm) {
+ if (mContent->IsInclusiveDescendantOf(activeDescendantElm)) {
+ // Don't want a cyclical descendant relationship. That would be bad.
+ return nullptr;
+ }
+
+ DocAccessible* document = Document();
+ if (document) return document->GetAccessible(activeDescendantElm);
+ }
+ }
+ return nullptr;
+}
+
+void LocalAccessible::SetCurrentItem(const LocalAccessible* aItem) {
+ nsAtom* id = aItem->GetContent()->GetID();
+ if (id) {
+ nsAutoString idStr;
+ id->ToString(idStr);
+ mContent->AsElement()->SetAttr(
+ kNameSpaceID_None, nsGkAtoms::aria_activedescendant, idStr, true);
+ }
+}
+
+LocalAccessible* LocalAccessible::ContainerWidget() const {
+ if (HasARIARole() && mContent->HasID()) {
+ for (LocalAccessible* parent = LocalParent(); parent;
+ parent = parent->LocalParent()) {
+ nsIContent* parentContent = parent->GetContent();
+ if (parentContent && parentContent->IsElement() &&
+ parentContent->AsElement()->HasAttr(
+ nsGkAtoms::aria_activedescendant)) {
+ return parent;
+ }
+
+ // Don't cross DOM document boundaries.
+ if (parent->IsDoc()) break;
+ }
+ }
+ return nullptr;
+}
+
+bool LocalAccessible::IsActiveDescendant(LocalAccessible** aWidget) const {
+ if (!HasOwnContent() || !mContent->HasID()) {
+ return false;
+ }
+
+ dom::DocumentOrShadowRoot* docOrShadowRoot =
+ mContent->GetUncomposedDocOrConnectedShadowRoot();
+ if (!docOrShadowRoot) {
+ return false;
+ }
+
+ nsAutoCString selector;
+ selector.AppendPrintf(
+ "[aria-activedescendant=\"%s\"]",
+ NS_ConvertUTF16toUTF8(mContent->GetID()->GetUTF16String()).get());
+ IgnoredErrorResult er;
+
+ dom::Element* widgetElm =
+ docOrShadowRoot->AsNode().QuerySelector(selector, er);
+
+ if (!widgetElm || er.Failed()) {
+ return false;
+ }
+
+ if (widgetElm->IsInclusiveDescendantOf(mContent)) {
+ // Don't want a cyclical descendant relationship. That would be bad.
+ return false;
+ }
+
+ LocalAccessible* widget = mDoc->GetAccessible(widgetElm);
+
+ if (aWidget) {
+ *aWidget = widget;
+ }
+
+ return !!widget;
+}
+
+void LocalAccessible::Announce(const nsAString& aAnnouncement,
+ uint16_t aPriority) {
+ RefPtr<AccAnnouncementEvent> event =
+ new AccAnnouncementEvent(this, aAnnouncement, aPriority);
+ nsEventShell::FireEvent(event);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// LocalAccessible protected methods
+
+void LocalAccessible::LastRelease() {
+ // First cleanup if needed...
+ if (mDoc) {
+ Shutdown();
+ NS_ASSERTION(!mDoc,
+ "A Shutdown() impl forgot to call its parent's Shutdown?");
+ }
+ // ... then die.
+ delete this;
+}
+
+LocalAccessible* LocalAccessible::GetSiblingAtOffset(int32_t aOffset,
+ nsresult* aError) const {
+ if (!mParent || mIndexInParent == -1) {
+ if (aError) *aError = NS_ERROR_UNEXPECTED;
+
+ return nullptr;
+ }
+
+ if (aError &&
+ mIndexInParent + aOffset >= static_cast<int32_t>(mParent->ChildCount())) {
+ *aError = NS_OK; // fail peacefully
+ return nullptr;
+ }
+
+ LocalAccessible* child = mParent->LocalChildAt(mIndexInParent + aOffset);
+ if (aError && !child) *aError = NS_ERROR_UNEXPECTED;
+
+ return child;
+}
+
+void LocalAccessible::ModifySubtreeContextFlags(uint32_t aContextFlags,
+ bool aAdd) {
+ Pivot pivot(this);
+ LocalAccInSameDocRule rule;
+ for (Accessible* anchor = this; anchor; anchor = pivot.Next(anchor, rule)) {
+ MOZ_ASSERT(anchor->IsLocal());
+ LocalAccessible* acc = anchor->AsLocal();
+ if (aAdd) {
+ acc->mContextFlags |= aContextFlags;
+ } else {
+ acc->mContextFlags &= ~aContextFlags;
+ }
+ }
+}
+
+double LocalAccessible::AttrNumericValue(nsAtom* aAttr) const {
+ const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
+ if (!roleMapEntry || roleMapEntry->valueRule == eNoValue) {
+ return UnspecifiedNaN<double>();
+ }
+
+ nsAutoString attrValue;
+ if (!mContent->IsElement() ||
+ !nsAccUtils::GetARIAAttr(mContent->AsElement(), aAttr, attrValue)) {
+ return UnspecifiedNaN<double>();
+ }
+
+ nsresult error = NS_OK;
+ double value = attrValue.ToDouble(&error);
+ return NS_FAILED(error) ? UnspecifiedNaN<double>() : value;
+}
+
+uint32_t LocalAccessible::GetActionRule() const {
+ if (!HasOwnContent() || (InteractiveState() & states::UNAVAILABLE)) {
+ return eNoAction;
+ }
+
+ // Return "click" action on elements that have an attached popup menu.
+ if (mContent->IsXULElement()) {
+ if (mContent->AsElement()->HasAttr(kNameSpaceID_None, nsGkAtoms::popup)) {
+ return eClickAction;
+ }
+ }
+
+ // Has registered 'click' event handler.
+ bool isOnclick = nsCoreUtils::HasClickListener(mContent);
+
+ if (isOnclick) return eClickAction;
+
+ // Get an action based on ARIA role.
+ const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
+ if (roleMapEntry && roleMapEntry->actionRule != eNoAction) {
+ return roleMapEntry->actionRule;
+ }
+
+ // Get an action based on ARIA attribute.
+ if (nsAccUtils::HasDefinedARIAToken(mContent, nsGkAtoms::aria_expanded)) {
+ return eExpandAction;
+ }
+
+ return eNoAction;
+}
+
+AccGroupInfo* LocalAccessible::GetGroupInfo() const {
+ if (mGroupInfo && !(mStateFlags & eGroupInfoDirty)) {
+ return mGroupInfo;
+ }
+
+ return nullptr;
+}
+
+AccGroupInfo* LocalAccessible::GetOrCreateGroupInfo() {
+ if (mGroupInfo) {
+ if (mStateFlags & eGroupInfoDirty) {
+ mGroupInfo->Update();
+ mStateFlags &= ~eGroupInfoDirty;
+ }
+
+ return mGroupInfo;
+ }
+
+ mGroupInfo = AccGroupInfo::CreateGroupInfo(this);
+ mStateFlags &= ~eGroupInfoDirty;
+ return mGroupInfo;
+}
+
+void LocalAccessible::SendCache(uint64_t aCacheDomain,
+ CacheUpdateType aUpdateType) {
+ if (!StaticPrefs::accessibility_cache_enabled_AtStartup()) {
+ return;
+ }
+
+ if (!IPCAccessibilityActive() || !Document()) {
+ return;
+ }
+
+ DocAccessibleChild* ipcDoc = mDoc->IPCDoc();
+ if (!ipcDoc) {
+ // This means DocAccessible::DoInitialUpdate hasn't been called yet, which
+ // means the a11y tree hasn't been built yet. Therefore, this should only
+ // be possible if this is a DocAccessible.
+ MOZ_ASSERT(IsDoc(), "Called on a non-DocAccessible but IPCDoc is null");
+ return;
+ }
+
+ RefPtr<AccAttributes> fields =
+ BundleFieldsForCache(aCacheDomain, aUpdateType);
+ if (!fields->Count()) {
+ return;
+ }
+ nsTArray<CacheData> data;
+ data.AppendElement(CacheData(ID(), fields));
+ ipcDoc->SendCache(aUpdateType, data, false);
+
+ if (profiler_thread_is_being_profiled_for_markers()) {
+ nsAutoCString updateTypeStr;
+ if (aUpdateType == CacheUpdateType::Initial) {
+ updateTypeStr = "Initial";
+ } else if (aUpdateType == CacheUpdateType::Update) {
+ updateTypeStr = "Update";
+ } else {
+ updateTypeStr = "Other";
+ }
+ PROFILER_MARKER_TEXT("LocalAccessible::SendCache", A11Y, {}, updateTypeStr);
+ }
+}
+
+already_AddRefed<AccAttributes> LocalAccessible::BundleFieldsForCache(
+ uint64_t aCacheDomain, CacheUpdateType aUpdateType) {
+ RefPtr<AccAttributes> fields = new AccAttributes();
+
+ // Caching name for text leaf Accessibles is redundant, since their name is
+ // always their text. Text gets handled below.
+ if (aCacheDomain & CacheDomain::NameAndDescription && !IsText()) {
+ nsString name;
+ int32_t nameFlag = Name(name);
+ if (nameFlag != eNameOK) {
+ fields->SetAttribute(nsGkAtoms::explicit_name, nameFlag);
+ } else if (aUpdateType == CacheUpdateType::Update) {
+ fields->SetAttribute(nsGkAtoms::explicit_name, DeleteEntry());
+ }
+
+ if (!name.IsEmpty()) {
+ fields->SetAttribute(nsGkAtoms::name, std::move(name));
+ } else if (aUpdateType == CacheUpdateType::Update) {
+ fields->SetAttribute(nsGkAtoms::name, DeleteEntry());
+ }
+
+ nsString description;
+ Description(description);
+ if (!description.IsEmpty()) {
+ fields->SetAttribute(nsGkAtoms::description, std::move(description));
+ } else if (aUpdateType == CacheUpdateType::Update) {
+ fields->SetAttribute(nsGkAtoms::description, DeleteEntry());
+ }
+ }
+
+ if (aCacheDomain & CacheDomain::Value) {
+ // We cache the text value in 3 cases:
+ // 1. Accessible is an HTML input type that holds a number.
+ // 2. Accessible has a numeric value and an aria-valuetext.
+ // 3. Accessible is an HTML input type that holds text.
+ // 4. Accessible is a link, in which case value is the target URL.
+ // ... for all other cases we divine the value remotely.
+ bool cacheValueText = false;
+ if (HasNumericValue()) {
+ fields->SetAttribute(nsGkAtoms::value, CurValue());
+ fields->SetAttribute(nsGkAtoms::max, MaxValue());
+ fields->SetAttribute(nsGkAtoms::min, MinValue());
+ fields->SetAttribute(nsGkAtoms::step, Step());
+ cacheValueText = NativeHasNumericValue() ||
+ (mContent->IsElement() &&
+ nsAccUtils::HasARIAAttr(mContent->AsElement(),
+ nsGkAtoms::aria_valuetext));
+ } else {
+ cacheValueText = IsTextField() || IsHTMLLink();
+ }
+
+ if (cacheValueText) {
+ nsString value;
+ Value(value);
+ if (!value.IsEmpty()) {
+ fields->SetAttribute(nsGkAtoms::aria_valuetext, std::move(value));
+ } else if (aUpdateType == CacheUpdateType::Update) {
+ fields->SetAttribute(nsGkAtoms::aria_valuetext, DeleteEntry());
+ }
+ }
+ }
+
+ if (aCacheDomain & CacheDomain::Viewport && IsDoc()) {
+ // Construct the viewport cache for this document. This cache domain will
+ // only be requested after we finish painting.
+ DocAccessible* doc = AsDoc();
+ PresShell* presShell = doc->PresShellPtr();
+
+ if (nsIFrame* rootFrame = presShell->GetRootFrame()) {
+ nsTArray<nsIFrame*> frames;
+ nsIScrollableFrame* sf = presShell->GetRootScrollFrameAsScrollable();
+ nsRect scrollPort = sf ? sf->GetScrollPortRect() : rootFrame->GetRect();
+
+ nsLayoutUtils::GetFramesForArea(
+ RelativeTo{rootFrame}, scrollPort, frames,
+ {{// We only care about visible content for hittesting.
+ nsLayoutUtils::FrameForPointOption::OnlyVisible,
+ // This flag ensures the display lists are built, even if
+ // the page hasn't finished loading.
+ nsLayoutUtils::FrameForPointOption::IgnorePaintSuppression,
+ // Each doc should have its own viewport cache, so we can
+ // ignore cross-doc content as an optimization.
+ nsLayoutUtils::FrameForPointOption::IgnoreCrossDoc}});
+
+ nsTHashSet<LocalAccessible*> inViewAccs;
+ nsTArray<uint64_t> viewportCache;
+ // Layout considers table rows fully occluded by their containing cells.
+ // This means they don't have their own display list items, and they won't
+ // show up in the list returned from GetFramesForArea. To prevent table
+ // rows from appearing offscreen, we manually add any rows for which we
+ // have on-screen cells.
+ LocalAccessible* prevParentRow = nullptr;
+ for (nsIFrame* frame : frames) {
+ nsIContent* content = frame->GetContent();
+ if (!content) {
+ continue;
+ }
+
+ LocalAccessible* acc = doc->GetAccessible(content);
+ // The document should always be present at the end of the list, so
+ // including it is unnecessary and wasteful. We skip the document here
+ // and handle it as a fallback when hit testing.
+ if (!acc || acc == mDoc) {
+ continue;
+ }
+
+ if (acc->IsTextLeaf() && nsAccUtils::MustPrune(acc->LocalParent())) {
+ acc = acc->LocalParent();
+ }
+ if (acc->IsTableCell()) {
+ LocalAccessible* parent = acc->LocalParent();
+ if (parent && parent->IsTableRow() && parent != prevParentRow) {
+ // If we've entered a new row since the last cell we saw, add the
+ // previous parent row to our viewport cache here to maintain
+ // hittesting order. Keep track of the current parent row.
+ if (prevParentRow && inViewAccs.EnsureInserted(prevParentRow)) {
+ viewportCache.AppendElement(prevParentRow->ID());
+ }
+ prevParentRow = parent;
+ }
+ } else if (acc->IsTable()) {
+ // If we've encountered a table, we know we've already
+ // handled all of this table's content (because we're traversing
+ // in hittesting order). Add our table's final row to the viewport
+ // cache before adding the table itself. Reset our marker for the next
+ // table.
+ if (prevParentRow && inViewAccs.EnsureInserted(prevParentRow)) {
+ viewportCache.AppendElement(prevParentRow->ID());
+ }
+ prevParentRow = nullptr;
+ } else if (acc->IsImageMap()) {
+ // Layout doesn't walk image maps, so we do that
+ // manually here. We do this before adding the map itself
+ // so the children come earlier in the hittesting order.
+ for (uint32_t i = 0; i < acc->ChildCount(); i++) {
+ LocalAccessible* child = acc->LocalChildAt(i);
+ MOZ_ASSERT(child);
+ if (inViewAccs.EnsureInserted(child)) {
+ MOZ_ASSERT(!child->IsDoc());
+ viewportCache.AppendElement(child->ID());
+ }
+ }
+ } else if (acc->IsHTMLCombobox()) {
+ // Layout doesn't consider combobox lists (or their
+ // currently selected items) to be onscreen, but we do.
+ // Add those things manually here.
+ HTMLComboboxAccessible* combobox =
+ static_cast<HTMLComboboxAccessible*>(acc);
+ HTMLComboboxListAccessible* list = combobox->List();
+ LocalAccessible* currItem = combobox->SelectedOption();
+ // Preserve hittesting order by adding the item, then
+ // the list, and finally the combobox itself.
+ if (currItem && inViewAccs.EnsureInserted(currItem)) {
+ viewportCache.AppendElement(currItem->ID());
+ }
+ if (list && inViewAccs.EnsureInserted(list)) {
+ viewportCache.AppendElement(list->ID());
+ }
+ }
+
+ if (inViewAccs.EnsureInserted(acc)) {
+ MOZ_ASSERT(!acc->IsDoc());
+ viewportCache.AppendElement(acc->ID());
+ }
+ }
+
+ if (viewportCache.Length()) {
+ fields->SetAttribute(nsGkAtoms::viewport, std::move(viewportCache));
+ }
+ }
+ }
+
+ bool boundsChanged = false;
+ if (aCacheDomain & CacheDomain::Bounds) {
+ nsRect newBoundsRect = ParentRelativeBounds();
+
+ // 1. Layout might notify us of a possible bounds change when the bounds
+ // haven't really changed. Therefore, we cache the last bounds we sent
+ // and don't send an update if they haven't changed.
+ // 2. For an initial cache push, we ignore 1) and always send the bounds.
+ // This handles the case where this LocalAccessible was moved (but not
+ // re-created). In that case, we will have cached bounds, but we currently
+ // do an initial cache push.
+ MOZ_ASSERT(aUpdateType == CacheUpdateType::Initial || mBounds.isSome(),
+ "Incremental cache push but mBounds is not set!");
+
+ if (OuterDocAccessible* doc = AsOuterDoc()) {
+ if (nsIFrame* docFrame = doc->GetFrame()) {
+ const nsMargin& newOffset = docFrame->GetUsedBorderAndPadding();
+ Maybe<nsMargin> currOffset = doc->GetCrossDocOffset();
+ if (!currOffset || *currOffset != newOffset) {
+ // OOP iframe docs can't compute their position within their
+ // cross-proc parent, so we have to manually cache that offset
+ // on the parent (outer doc) itself. For simplicity and consistency,
+ // we do this here for both OOP and in-process iframes. For in-process
+ // iframes, this also avoids the need to push a cache update for the
+ // embedded document when the iframe changes its padding, gets
+ // re-created, etc. Similar to bounds, we maintain a local cache and a
+ // remote cache to avoid sending redundant updates.
+ doc->SetCrossDocOffset(newOffset);
+ nsTArray<int32_t> offsetArray(2);
+ offsetArray.AppendElement(newOffset.Side(eSideLeft)); // X offset
+ offsetArray.AppendElement(newOffset.Side(eSideTop)); // Y offset
+ fields->SetAttribute(nsGkAtoms::crossorigin, std::move(offsetArray));
+ }
+ }
+ }
+
+ boundsChanged = aUpdateType == CacheUpdateType::Initial ||
+ !newBoundsRect.IsEqualEdges(mBounds.value());
+ if (boundsChanged) {
+ mBounds = Some(newBoundsRect);
+
+ nsTArray<int32_t> boundsArray(4);
+
+ boundsArray.AppendElement(newBoundsRect.x);
+ boundsArray.AppendElement(newBoundsRect.y);
+ boundsArray.AppendElement(newBoundsRect.width);
+ boundsArray.AppendElement(newBoundsRect.height);
+
+ fields->SetAttribute(nsGkAtoms::relativeBounds, std::move(boundsArray));
+ }
+ }
+
+ if (aCacheDomain & CacheDomain::Text) {
+ if (!HasChildren()) {
+ // We only cache text and line offsets on leaf Accessibles.
+ // Only text Accessibles can have actual text.
+ if (IsText()) {
+ nsString text;
+ AppendTextTo(text);
+ fields->SetAttribute(nsGkAtoms::text, std::move(text));
+ TextLeafPoint point(this, 0);
+ RefPtr<AccAttributes> attrs = point.GetTextAttributesLocalAcc(
+ /* aIncludeDefaults */ false);
+ fields->SetAttribute(nsGkAtoms::style, std::move(attrs));
+ }
+ }
+ if (HyperTextAccessible* ht = AsHyperText()) {
+ RefPtr<AccAttributes> attrs = ht->DefaultTextAttributes();
+ fields->SetAttribute(nsGkAtoms::style, std::move(attrs));
+ }
+ }
+
+ // If text changes, we must also update spelling errors.
+ if (aCacheDomain & (CacheDomain::Spelling | CacheDomain::Text) &&
+ IsTextLeaf()) {
+ auto spellingErrors = TextLeafPoint::GetSpellingErrorOffsets(this);
+ if (!spellingErrors.IsEmpty()) {
+ fields->SetAttribute(nsGkAtoms::spelling, std::move(spellingErrors));
+ } else if (aUpdateType == CacheUpdateType::Update) {
+ fields->SetAttribute(nsGkAtoms::spelling, DeleteEntry());
+ }
+ }
+
+ nsIFrame* frame = GetFrame();
+ if (aCacheDomain & (CacheDomain::Text | CacheDomain::Bounds) &&
+ !HasChildren()) {
+ // We cache line start offsets for both text and non-text leaf Accessibles
+ // because non-text leaf Accessibles can still start a line.
+ TextLeafPoint lineStart =
+ TextLeafPoint(this, 0).FindNextLineStartSameLocalAcc(
+ /* aIncludeOrigin */ true);
+ int32_t lineStartOffset = lineStart ? lineStart.mOffset : -1;
+ // We push line starts and text bounds in two cases:
+ // 1. Text or bounds changed, which means it's very likely that line starts
+ // and text bounds changed too.
+ // 2. CacheDomain::Bounds was requested (indicating that the frame was
+ // reflowed) but the bounds didn't actually change. This can happen when
+ // the spanned text is non-rectangular. For example, an Accessible might
+ // cover two characters on one line and a single character on another line.
+ // An insertion in a previous text node might cause it to shift such that it
+ // now covers a single character on the first line and two characters on the
+ // second line. Its bounding rect will be the same both before and after the
+ // insertion. In this case, we use the first line start to determine whether
+ // there was a change. This should be safe because the text didn't change in
+ // this Accessible, so if the first line start doesn't shift, none of them
+ // should shift.
+ if (aCacheDomain & CacheDomain::Text || boundsChanged ||
+ mFirstLineStart != lineStartOffset) {
+ mFirstLineStart = lineStartOffset;
+ nsTArray<int32_t> lineStarts;
+ for (; lineStart;
+ lineStart = lineStart.FindNextLineStartSameLocalAcc(false)) {
+ lineStarts.AppendElement(lineStart.mOffset);
+ }
+ if (!lineStarts.IsEmpty()) {
+ fields->SetAttribute(nsGkAtoms::line, std::move(lineStarts));
+ } else if (aUpdateType == CacheUpdateType::Update) {
+ fields->SetAttribute(nsGkAtoms::line, DeleteEntry());
+ }
+
+ if (frame && frame->IsTextFrame()) {
+ nsTArray<int32_t> charData;
+
+ if (nsTextFrame* currTextFrame = do_QueryFrame(frame)) {
+ nsTextFrame* prevTextFrame = currTextFrame;
+ nsRect frameRect = currTextFrame->GetRect();
+ nsIFrame* nearestAccAncestorFrame =
+ LocalParent() ? LocalParent()->GetFrame() : nullptr;
+ while (currTextFrame) {
+ nsRect contRect = currTextFrame->GetRect();
+ if (prevTextFrame->GetParent() != currTextFrame->GetParent() &&
+ nearestAccAncestorFrame) {
+ // Continuations can span multiple frame tree subtrees,
+ // particularly when multiline text is nested within both block
+ // and inline elements. In addition to using the position of this
+ // continuation to offset our char rects, we'll need to offset
+ // this continuation from the continuations that occurred before
+ // it. We don't know how many there are or what subtrees they're
+ // in, so we use a transform here. This also ensures our offset is
+ // accurate even if the intervening inline elements are not
+ // present in the a11y tree.
+ contRect = frameRect;
+ nsLayoutUtils::TransformRect(currTextFrame,
+ nearestAccAncestorFrame, contRect);
+ }
+ nsTArray<nsRect> charBounds;
+ currTextFrame->GetCharacterRectsInRange(
+ currTextFrame->GetContentOffset(),
+ currTextFrame->GetContentEnd(), charBounds);
+ for (const nsRect& charRect : charBounds) {
+ // We expect each char rect to be relative to the text leaf
+ // acc this text lives in. Unfortunately, GetCharacterRectsInRange
+ // returns rects relative to their continuation. Add the
+ // continuation's relative position here to make our final
+ // rect relative to the text leaf acc. Continuation rects include
+ // the padding of their parent text frame, so we compute the
+ // relative offset here instead of using `contRect`'s coordinates
+ // outright.
+ int computedX = charRect.x + (contRect.x - frameRect.x);
+ int computedY = charRect.y + (contRect.y - frameRect.y);
+ charData.AppendElement(computedX);
+ charData.AppendElement(computedY);
+ charData.AppendElement(charRect.width);
+ charData.AppendElement(charRect.height);
+ }
+ prevTextFrame = currTextFrame;
+ currTextFrame = currTextFrame->GetNextContinuation();
+ }
+ }
+ if (charData.Length()) {
+ fields->SetAttribute(nsGkAtoms::characterData, std::move(charData));
+ }
+ }
+ }
+ }
+
+ if (aCacheDomain & CacheDomain::TransformMatrix) {
+ bool transformed = false;
+ if (frame && frame->IsTransformed()) {
+ // We need to find a frame to make our transform relative to.
+ // It's important this frame have a corresponding accessible,
+ // because this transform is applied while walking the accessibility
+ // tree (in the parent process), not the frame tree.
+ nsIFrame* boundingFrame = FindNearestAccessibleAncestorFrame();
+ // This matrix is only valid when applied to CSSPixel points/rects
+ // in the coordinate space of `frame`. It also includes the translation
+ // to the parent space.
+ gfx::Matrix4x4Flagged mtx = nsLayoutUtils::GetTransformToAncestor(
+ RelativeTo{frame}, RelativeTo{boundingFrame}, nsIFrame::IN_CSS_UNITS);
+ // We might get back the identity matrix. This can happen if there is no
+ // actual transform. For example, if an element has
+ // will-change: transform, nsIFrame::IsTransformed will return true, but
+ // this doesn't necessarily mean there is a transform right now.
+ // Applying the identity matrix is effectively a no-op, so there's no
+ // point caching it.
+ transformed = !mtx.IsIdentity();
+ if (transformed) {
+ UniquePtr<gfx::Matrix4x4> ptr =
+ MakeUnique<gfx::Matrix4x4>(mtx.GetMatrix());
+ fields->SetAttribute(nsGkAtoms::transform, std::move(ptr));
+ }
+ }
+ if (!transformed && aUpdateType == CacheUpdateType::Update) {
+ // Otherwise, if we're bundling a transform update but this
+ // frame isn't transformed (or doesn't exist), we need
+ // to send a DeleteEntry() to remove any
+ // transform that was previously cached for this frame.
+ fields->SetAttribute(nsGkAtoms::transform, DeleteEntry());
+ }
+ }
+
+ if (aCacheDomain & CacheDomain::ScrollPosition) {
+ nsPoint scrollPosition;
+ std::tie(scrollPosition, std::ignore) = mDoc->ComputeScrollData(this);
+ if (scrollPosition.x || scrollPosition.y) {
+ nsTArray<int32_t> positionArr(2);
+ positionArr.AppendElement(scrollPosition.x);
+ positionArr.AppendElement(scrollPosition.y);
+ fields->SetAttribute(nsGkAtoms::scrollPosition, std::move(positionArr));
+ } else if (aUpdateType == CacheUpdateType::Update) {
+ fields->SetAttribute(nsGkAtoms::scrollPosition, DeleteEntry());
+ }
+ }
+
+ if (aCacheDomain & CacheDomain::DOMNodeID && mContent) {
+ nsAtom* id = mContent->GetID();
+ if (id) {
+ fields->SetAttribute(nsGkAtoms::id, id);
+ } else if (aUpdateType == CacheUpdateType::Update) {
+ fields->SetAttribute(nsGkAtoms::id, DeleteEntry());
+ }
+ }
+
+ // State is only included in the initial push. Thereafter, cached state is
+ // updated via events.
+ if (aCacheDomain & CacheDomain::State) {
+ if (aUpdateType == CacheUpdateType::Initial) {
+ // Most states are updated using state change events, so we only send
+ // these for the initial cache push.
+ uint64_t state = State();
+ // Exclude states which must be calculated by RemoteAccessible.
+ state &= ~kRemoteCalculatedStates;
+ fields->SetAttribute(nsGkAtoms::state, state);
+ }
+ // If aria-selected isn't specified, there may be no SELECTED state.
+ // However, aria-selected can be implicit in some cases when an item is
+ // focused. We don't want to do this if aria-selected is explicitly
+ // set to "false", so we need to differentiate between false and unset.
+ if (auto ariaSelected = ARIASelected()) {
+ fields->SetAttribute(nsGkAtoms::aria_selected, *ariaSelected);
+ } else if (aUpdateType == CacheUpdateType::Update) {
+ fields->SetAttribute(nsGkAtoms::aria_selected, DeleteEntry()); // Unset.
+ }
+ }
+
+ if (aCacheDomain & CacheDomain::GroupInfo && mContent) {
+ for (nsAtom* attr : {nsGkAtoms::aria_level, nsGkAtoms::aria_setsize,
+ nsGkAtoms::aria_posinset}) {
+ int32_t value = 0;
+ if (nsCoreUtils::GetUIntAttr(mContent, attr, &value)) {
+ fields->SetAttribute(attr, value);
+ } else if (aUpdateType == CacheUpdateType::Update) {
+ fields->SetAttribute(attr, DeleteEntry());
+ }
+ }
+ }
+
+ if (aCacheDomain & CacheDomain::Actions) {
+ if (HasPrimaryAction()) {
+ // Here we cache the primary action.
+ nsAutoString actionName;
+ ActionNameAt(0, actionName);
+ RefPtr<nsAtom> actionAtom = NS_Atomize(actionName);
+ fields->SetAttribute(nsGkAtoms::action, actionAtom);
+ } else if (aUpdateType == CacheUpdateType::Update) {
+ fields->SetAttribute(nsGkAtoms::action, DeleteEntry());
+ }
+
+ if (ImageAccessible* imgAcc = AsImage()) {
+ // Here we cache the showlongdesc action.
+ if (imgAcc->HasLongDesc()) {
+ fields->SetAttribute(nsGkAtoms::longdesc, true);
+ } else if (aUpdateType == CacheUpdateType::Update) {
+ fields->SetAttribute(nsGkAtoms::longdesc, DeleteEntry());
+ }
+ }
+
+ KeyBinding accessKey = AccessKey();
+ if (!accessKey.IsEmpty()) {
+ fields->SetAttribute(nsGkAtoms::accesskey, accessKey.Serialize());
+ } else if (aUpdateType == CacheUpdateType::Update) {
+ fields->SetAttribute(nsGkAtoms::accesskey, DeleteEntry());
+ }
+ }
+
+ if (aCacheDomain & CacheDomain::Style) {
+ if (RefPtr<nsAtom> display = DisplayStyle()) {
+ fields->SetAttribute(nsGkAtoms::display, display);
+ }
+
+ float opacity = Opacity();
+ if (opacity != 1.0f) {
+ fields->SetAttribute(nsGkAtoms::opacity, opacity);
+ } else if (aUpdateType == CacheUpdateType::Update) {
+ fields->SetAttribute(nsGkAtoms::opacity, DeleteEntry());
+ }
+ }
+
+ if (aCacheDomain & CacheDomain::Table) {
+ if (TableAccessible* table = AsTable()) {
+ if (table->IsProbablyLayoutTable()) {
+ fields->SetAttribute(nsGkAtoms::layout_guess, true);
+ } else if (aUpdateType == CacheUpdateType::Update) {
+ fields->SetAttribute(nsGkAtoms::layout_guess, DeleteEntry());
+ }
+ } else if (TableCellAccessible* cell = AsTableCell()) {
+ // For HTML table cells, we must use the HTMLTableCellAccessible
+ // GetRow/ColExtent methods rather than using the DOM attributes directly.
+ // This is because of things like rowspan="0" which depend on knowing
+ // about thead, tbody, etc., which is info we don't have in the a11y tree.
+ int32_t value = static_cast<int32_t>(cell->RowExtent());
+ MOZ_ASSERT(value > 0);
+ if (value > 1) {
+ fields->SetAttribute(nsGkAtoms::rowspan, value);
+ } else if (aUpdateType == CacheUpdateType::Update) {
+ fields->SetAttribute(nsGkAtoms::rowspan, DeleteEntry());
+ }
+ value = static_cast<int32_t>(cell->ColExtent());
+ MOZ_ASSERT(value > 0);
+ if (value > 1) {
+ fields->SetAttribute(nsGkAtoms::colspan, value);
+ } else if (aUpdateType == CacheUpdateType::Update) {
+ fields->SetAttribute(nsGkAtoms::colspan, DeleteEntry());
+ }
+ if (mContent->AsElement()->HasAttr(kNameSpaceID_None,
+ nsGkAtoms::headers)) {
+ nsTArray<uint64_t> headers;
+ IDRefsIterator iter(mDoc, mContent, nsGkAtoms::headers);
+ while (LocalAccessible* cell = iter.Next()) {
+ if (cell->IsTableCell()) {
+ headers.AppendElement(cell->ID());
+ }
+ }
+ fields->SetAttribute(nsGkAtoms::headers, std::move(headers));
+ } else if (aUpdateType == CacheUpdateType::Update) {
+ fields->SetAttribute(nsGkAtoms::headers, DeleteEntry());
+ }
+ }
+ }
+
+ if (aCacheDomain & CacheDomain::ARIA && mContent && mContent->IsElement()) {
+ // We use a nested AccAttributes to make cache updates simpler. Rather than
+ // managing individual removals, we just replace or remove the entire set of
+ // ARIA attributes.
+ RefPtr<AccAttributes> ariaAttrs;
+ aria::AttrIterator attrIt(mContent);
+ while (attrIt.Next()) {
+ if (!ariaAttrs) {
+ ariaAttrs = new AccAttributes();
+ }
+ attrIt.ExposeAttr(ariaAttrs);
+ }
+ if (ariaAttrs) {
+ fields->SetAttribute(nsGkAtoms::aria, std::move(ariaAttrs));
+ } else if (aUpdateType == CacheUpdateType::Update) {
+ fields->SetAttribute(nsGkAtoms::aria, DeleteEntry());
+ }
+ }
+
+ if (aCacheDomain & CacheDomain::Relations && mContent) {
+ if (IsHTMLRadioButton() ||
+ (mContent->IsElement() &&
+ mContent->AsElement()->IsHTMLElement(nsGkAtoms::a))) {
+ // HTML radio buttons with the same name should be grouped
+ // and returned together when their MEMBER_OF relation is
+ // requested. Computing LINKS_TO also requires we cache `name` on
+ // anchor elements.
+ nsString name;
+ mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::name, name);
+ if (!name.IsEmpty()) {
+ fields->SetAttribute(nsGkAtoms::attributeName, std::move(name));
+ } else if (aUpdateType != CacheUpdateType::Initial) {
+ // It's possible we used to have a name and it's since been
+ // removed. Send a delete entry.
+ fields->SetAttribute(nsGkAtoms::attributeName, DeleteEntry());
+ }
+ }
+
+ for (auto const& data : kRelationTypeAtoms) {
+ nsTArray<uint64_t> ids;
+ nsStaticAtom* const relAtom = data.mAtom;
+
+ Relation rel;
+ if (data.mType == RelationType::LABEL_FOR) {
+ // Labels are a special case -- we need to validate that the target of
+ // their `for` attribute is in fact labelable. DOM checks this when we
+ // call GetControl(). If a label contains an element we will return it
+ // here.
+ if (dom::HTMLLabelElement* labelEl =
+ dom::HTMLLabelElement::FromNode(mContent)) {
+ rel.AppendTarget(mDoc, labelEl->GetControl());
+ }
+ } else {
+ // We use an IDRefsIterator here instead of calling RelationByType
+ // directly because we only want to cache explicit relations. Implicit
+ // relations will be computed and stored separately in the parent
+ // process.
+ rel.AppendIter(new IDRefsIterator(mDoc, mContent, relAtom));
+ }
+
+ while (LocalAccessible* acc = rel.LocalNext()) {
+ ids.AppendElement(acc->ID());
+ }
+ if (ids.Length()) {
+ fields->SetAttribute(relAtom, std::move(ids));
+ } else if (aUpdateType == CacheUpdateType::Update) {
+ fields->SetAttribute(relAtom, DeleteEntry());
+ }
+ }
+ }
+
+#if defined(XP_WIN)
+ if (aCacheDomain & CacheDomain::InnerHTML && HasOwnContent() &&
+ mContent->IsMathMLElement(nsGkAtoms::math)) {
+ nsString innerHTML;
+ mContent->AsElement()->GetInnerHTML(innerHTML, IgnoreErrors());
+ fields->SetAttribute(nsGkAtoms::html, std::move(innerHTML));
+ }
+#endif // defined(XP_WIN)
+
+ if (aUpdateType == CacheUpdateType::Initial) {
+ // Add fields which never change and thus only need to be included in the
+ // initial cache push.
+ if (mContent && mContent->IsElement()) {
+ fields->SetAttribute(nsGkAtoms::tag, mContent->NodeInfo()->NameAtom());
+
+ dom::Element* el = mContent->AsElement();
+ if (IsTextField() || IsDateTimeField()) {
+ // Cache text input types. Accessible is recreated if this changes,
+ // so it is considered immutable.
+ if (const nsAttrValue* attr = el->GetParsedAttr(nsGkAtoms::type)) {
+ RefPtr<nsAtom> inputType = attr->GetAsAtom();
+ if (inputType) {
+ fields->SetAttribute(nsGkAtoms::textInputType, inputType);
+ }
+ }
+ }
+
+ // Changing the role attribute currently re-creates the Accessible, so
+ // it's immutable in the cache.
+ if (const nsRoleMapEntry* roleMap = ARIARoleMap()) {
+ // Most of the time, the role attribute is a single, known role. We
+ // already send the map index, so we don't need to double up.
+ if (!nsAccUtils::ARIAAttrValueIs(el, nsGkAtoms::role, roleMap->roleAtom,
+ eIgnoreCase)) {
+ // Multiple roles or unknown roles are rare, so just send them as a
+ // string.
+ nsAutoString role;
+ nsAccUtils::GetARIAAttr(el, nsGkAtoms::role, role);
+ fields->SetAttribute(nsGkAtoms::role, std::move(role));
+ }
+ }
+ }
+
+ if (frame) {
+ // Note our frame's current computed style so we can track style changes
+ // later on.
+ mOldComputedStyle = frame->Style();
+ if (frame->IsTransformed()) {
+ mStateFlags |= eOldFrameHasValidTransformStyle;
+ } else {
+ mStateFlags &= ~eOldFrameHasValidTransformStyle;
+ }
+ }
+
+ if (IsDoc()) {
+ if (PresShell* presShell = AsDoc()->PresShellPtr()) {
+ // Send the initial resolution of the document. When this changes, we
+ // will ne notified via nsAS::NotifyOfResolutionChange
+ float resolution = presShell->GetResolution();
+ fields->SetAttribute(nsGkAtoms::resolution, resolution);
+ int32_t appUnitsPerDevPixel =
+ presShell->GetPresContext()->AppUnitsPerDevPixel();
+ fields->SetAttribute(nsGkAtoms::_moz_device_pixel_ratio,
+ appUnitsPerDevPixel);
+ }
+ }
+ }
+
+ if ((aCacheDomain & (CacheDomain::Text | CacheDomain::ScrollPosition) ||
+ boundsChanged) &&
+ mDoc) {
+ mDoc->SetViewportCacheDirty(true);
+ }
+
+ return fields.forget();
+}
+
+void LocalAccessible::MaybeQueueCacheUpdateForStyleChanges() {
+ // mOldComputedStyle might be null if the initial cache hasn't been sent yet.
+ // In that case, there is nothing to do here.
+ if (!IPCAccessibilityActive() ||
+ !StaticPrefs::accessibility_cache_enabled_AtStartup() ||
+ !mOldComputedStyle) {
+ return;
+ }
+
+ if (nsIFrame* frame = GetFrame()) {
+ const ComputedStyle* newStyle = frame->Style();
+
+ nsAutoCString oldDisplay, newDisplay;
+ mOldComputedStyle->GetComputedPropertyValue(eCSSProperty_display,
+ oldDisplay);
+ newStyle->GetComputedPropertyValue(eCSSProperty_display, newDisplay);
+
+ nsAutoCString oldOpacity, newOpacity;
+ mOldComputedStyle->GetComputedPropertyValue(eCSSProperty_opacity,
+ oldOpacity);
+ newStyle->GetComputedPropertyValue(eCSSProperty_opacity, newOpacity);
+
+ if (oldDisplay != newDisplay || oldOpacity != newOpacity) {
+ // CacheDomain::Style covers both display and opacity, so if
+ // either property has changed, send an update for the entire domain.
+ mDoc->QueueCacheUpdate(this, CacheDomain::Style);
+ }
+
+ bool newHasValidTransformStyle =
+ newStyle->StyleDisplay()->HasTransform(frame);
+ bool oldHasValidTransformStyle =
+ (mStateFlags & eOldFrameHasValidTransformStyle) != 0;
+
+ // We should send a transform update if we're adding or
+ // removing transform styling altogether.
+ bool sendTransformUpdate =
+ newHasValidTransformStyle || oldHasValidTransformStyle;
+
+ if (newHasValidTransformStyle && oldHasValidTransformStyle) {
+ // If we continue to have transform styling, verify
+ // our transform has actually changed.
+ nsChangeHint transformHint =
+ newStyle->StyleDisplay()->CalcTransformPropertyDifference(
+ *mOldComputedStyle->StyleDisplay());
+ // If this hint exists, it implies we found a property difference
+ sendTransformUpdate = !!transformHint;
+ }
+
+ if (sendTransformUpdate) {
+ // If our transform matrix has changed, it's possible our
+ // viewport cache has also changed.
+ mDoc->SetViewportCacheDirty(true);
+ // Queuing a cache update for the TransformMatrix domain doesn't
+ // necessarily mean we'll send the matrix itself, we may
+ // send a DeleteEntry() instead. See BundleFieldsForCache for
+ // more information.
+ mDoc->QueueCacheUpdate(this, CacheDomain::TransformMatrix);
+ }
+
+ if (newStyle->StyleDisplay()->IsPositionedStyle()) {
+ // We normally rely on reflow to know when bounds might have changed.
+ // However, changing the CSS left, top, etc. properties doesn't always
+ // cause reflow.
+ for (auto prop : {eCSSProperty_left, eCSSProperty_right, eCSSProperty_top,
+ eCSSProperty_bottom}) {
+ nsAutoCString oldVal, newVal;
+ mOldComputedStyle->GetComputedPropertyValue(prop, oldVal);
+ newStyle->GetComputedPropertyValue(prop, newVal);
+ if (oldVal != newVal) {
+ mDoc->QueueCacheUpdate(this, CacheDomain::Bounds);
+ break;
+ }
+ }
+ }
+
+ mOldComputedStyle = newStyle;
+ if (newHasValidTransformStyle) {
+ mStateFlags |= eOldFrameHasValidTransformStyle;
+ } else {
+ mStateFlags &= ~eOldFrameHasValidTransformStyle;
+ }
+ }
+}
+
+nsAtom* LocalAccessible::TagName() const {
+ return mContent && mContent->IsElement() ? mContent->NodeInfo()->NameAtom()
+ : nullptr;
+}
+
+already_AddRefed<nsAtom> LocalAccessible::DisplayStyle() const {
+ if (dom::Element* elm = Elm()) {
+ if (elm->IsHTMLElement(nsGkAtoms::area)) {
+ // This is an image map area. CSS is irrelevant here. Furthermore, we
+ // won't be able to get the computed style if the map is unslotted in a
+ // shadow host.
+ return nullptr;
+ }
+ StyleInfo info(elm);
+ return info.Display();
+ }
+ return nullptr;
+}
+
+float LocalAccessible::Opacity() const {
+ if (nsIFrame* frame = GetFrame()) {
+ return frame->StyleEffects()->mOpacity;
+ }
+
+ return 1.0f;
+}
+
+void LocalAccessible::DOMNodeID(nsString& aID) const {
+ aID.Truncate();
+ if (mContent) {
+ if (nsAtom* id = mContent->GetID()) {
+ id->ToString(aID);
+ }
+ }
+}
+
+void LocalAccessible::LiveRegionAttributes(nsAString* aLive,
+ nsAString* aRelevant,
+ Maybe<bool>* aAtomic,
+ nsAString* aBusy) const {
+ dom::Element* el = Elm();
+ if (!el) {
+ return;
+ }
+ if (aLive) {
+ nsAccUtils::GetARIAAttr(el, nsGkAtoms::aria_live, *aLive);
+ }
+ if (aRelevant) {
+ nsAccUtils::GetARIAAttr(el, nsGkAtoms::aria_relevant, *aRelevant);
+ }
+ if (aAtomic) {
+ // XXX We ignore aria-atomic="false", but this probably doesn't conform to
+ // the spec.
+ if (nsAccUtils::ARIAAttrValueIs(el, nsGkAtoms::aria_atomic,
+ nsGkAtoms::_true, eCaseMatters)) {
+ *aAtomic = Some(true);
+ }
+ }
+ if (aBusy) {
+ nsAccUtils::GetARIAAttr(el, nsGkAtoms::aria_busy, *aBusy);
+ }
+}
+
+Maybe<bool> LocalAccessible::ARIASelected() const {
+ if (dom::Element* el = Elm()) {
+ nsStaticAtom* atom =
+ nsAccUtils::NormalizeARIAToken(el, nsGkAtoms::aria_selected);
+ if (atom == nsGkAtoms::_true) {
+ return Some(true);
+ }
+ if (atom == nsGkAtoms::_false) {
+ return Some(false);
+ }
+ }
+ return Nothing();
+}
+
+void LocalAccessible::StaticAsserts() const {
+ static_assert(
+ eLastStateFlag <= (1 << kStateFlagsBits) - 1,
+ "LocalAccessible::mStateFlags was oversized by eLastStateFlag!");
+ static_assert(
+ eLastContextFlag <= (1 << kContextFlagsBits) - 1,
+ "LocalAccessible::mContextFlags was oversized by eLastContextFlag!");
+}
+
+TableAccessibleBase* LocalAccessible::AsTableBase() {
+ if (StaticPrefs::accessibility_cache_enabled_AtStartup() && IsTable() &&
+ !mContent->IsXULElement()) {
+ // This isn't strictly related to caching, but this new table implementation
+ // is being developed to make caching feasible. We put it behind this pref
+ // to make it easy to test while it's still under development.
+ return CachedTableAccessible::GetFrom(this);
+ }
+ return AsTable();
+}
+
+TableCellAccessibleBase* LocalAccessible::AsTableCellBase() {
+ if (StaticPrefs::accessibility_cache_enabled_AtStartup() && IsTableCell() &&
+ !mContent->IsXULElement()) {
+ // This isn't strictly related to caching, but this new table implementation
+ // is being developed to make caching feasible. We put it behind this pref
+ // to make it easy to test while it's still under development.
+ return CachedTableCellAccessible::GetFrom(this);
+ }
+ return AsTableCell();
+}
+
+Maybe<int32_t> LocalAccessible::GetIntARIAAttr(nsAtom* aAttrName) const {
+ if (mContent) {
+ int32_t val;
+ if (nsCoreUtils::GetUIntAttr(mContent, aAttrName, &val)) {
+ return Some(val);
+ }
+ // XXX Handle attributes that allow -1; e.g. aria-row/colcount.
+ }
+ return Nothing();
+}
diff --git a/accessible/generic/LocalAccessible.h b/accessible/generic/LocalAccessible.h
new file mode 100644
index 0000000000..173cb41cab
--- /dev/null
+++ b/accessible/generic/LocalAccessible.h
@@ -0,0 +1,1066 @@
+/* -*- 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/. */
+
+#ifndef _LocalAccessible_H_
+#define _LocalAccessible_H_
+
+#include "mozilla/ComputedStyle.h"
+#include "mozilla/a11y/Accessible.h"
+#include "mozilla/a11y/AccTypes.h"
+#include "mozilla/a11y/RelationType.h"
+#include "mozilla/a11y/States.h"
+
+#include "mozilla/UniquePtr.h"
+
+#include "nsIContent.h"
+#include "nsTArray.h"
+#include "nsRefPtrHashtable.h"
+#include "nsRect.h"
+
+struct nsRoleMapEntry;
+
+class nsIFrame;
+
+class nsAttrValue;
+
+namespace mozilla::dom {
+class Element;
+}
+
+namespace mozilla {
+namespace a11y {
+
+class LocalAccessible;
+class AccAttributes;
+class AccEvent;
+class AccGroupInfo;
+class ApplicationAccessible;
+class CacheData;
+class DocAccessible;
+class EmbeddedObjCollector;
+class EventTree;
+class HTMLImageMapAccessible;
+class HTMLLIAccessible;
+class HTMLLinkAccessible;
+class HyperTextAccessible;
+class HyperTextAccessibleBase;
+class ImageAccessible;
+class KeyBinding;
+class OuterDocAccessible;
+class RemoteAccessible;
+class Relation;
+class RootAccessible;
+class TableAccessible;
+class TableAccessibleBase;
+class TableCellAccessible;
+class TableCellAccessibleBase;
+class TextLeafAccessible;
+class XULLabelAccessible;
+class XULTreeAccessible;
+
+enum class CacheUpdateType;
+
+#ifdef A11Y_LOG
+namespace logging {
+typedef const char* (*GetTreePrefix)(void* aData, LocalAccessible*);
+void Tree(const char* aTitle, const char* aMsgText, LocalAccessible* aRoot,
+ GetTreePrefix aPrefixFunc, void* GetTreePrefixData);
+void TreeSize(const char* aTitle, const char* aMsgText, LocalAccessible* aRoot);
+}; // namespace logging
+#endif
+
+typedef nsRefPtrHashtable<nsPtrHashKey<const void>, LocalAccessible>
+ AccessibleHashtable;
+
+#define NS_ACCESSIBLE_IMPL_IID \
+ { /* 133c8bf4-4913-4355-bd50-426bd1d6e1ad */ \
+ 0x133c8bf4, 0x4913, 0x4355, { \
+ 0xbd, 0x50, 0x42, 0x6b, 0xd1, 0xd6, 0xe1, 0xad \
+ } \
+ }
+
+/**
+ * An accessibility tree node that originated in mDoc's content process.
+ */
+class LocalAccessible : public nsISupports, public Accessible {
+ public:
+ LocalAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS(LocalAccessible)
+
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_ACCESSIBLE_IMPL_IID)
+
+ //////////////////////////////////////////////////////////////////////////////
+ // Public methods
+
+ /**
+ * Return the document accessible for this accessible.
+ */
+ DocAccessible* Document() const { return mDoc; }
+
+ /**
+ * Return the root document accessible for this accessible.
+ */
+ a11y::RootAccessible* RootAccessible() const;
+
+ /**
+ * Return frame for this accessible.
+ * Note that this will return null for display: contents. Also,
+ * DocAccessible::GetFrame can return null if the frame tree hasn't been
+ * created yet.
+ */
+ virtual nsIFrame* GetFrame() const;
+
+ /**
+ * Return DOM node associated with the accessible.
+ */
+ virtual nsINode* GetNode() const;
+
+ nsIContent* GetContent() const { return mContent; }
+ dom::Element* Elm() const;
+
+ /**
+ * Return node type information of DOM node associated with the accessible.
+ */
+ bool IsContent() const { return GetNode() && GetNode()->IsContent(); }
+
+ /**
+ * Return the unique identifier of the accessible.
+ * ID() should be preferred, but this method still exists because many
+ * LocalAccessible callers expect a void*.
+ */
+ void* UniqueID() { return static_cast<void*>(this); }
+
+ virtual uint64_t ID() const override {
+ return IsDoc() ? 0 : reinterpret_cast<uintptr_t>(this);
+ }
+
+ /**
+ * Return language associated with the accessible.
+ */
+ void Language(nsAString& aLocale);
+
+ /**
+ * Get the description of this accessible.
+ */
+ virtual void Description(nsString& aDescription) const override;
+
+ /**
+ * Get the value of this accessible.
+ */
+ virtual void Value(nsString& aValue) const override;
+
+ /**
+ * Get help string for the accessible.
+ */
+ void Help(nsString& aHelp) const { aHelp.Truncate(); }
+
+ /**
+ * Get the name of this accessible.
+ */
+ virtual ENameValueFlag Name(nsString& aName) const override;
+
+ /**
+ * Maps ARIA state attributes to state of accessible. Note the given state
+ * argument should hold states for accessible before you pass it into this
+ * method.
+ *
+ * @param [in/out] where to fill the states into.
+ */
+ virtual void ApplyARIAState(uint64_t* aState) const;
+
+ /**
+ * Return enumerated accessible role (see constants in Role.h).
+ */
+ virtual mozilla::a11y::role Role() const override;
+
+ /**
+ * Return accessible role specified by ARIA (see constants in
+ * roles).
+ */
+ mozilla::a11y::role ARIARole();
+
+ /**
+ * Returns enumerated accessible role from native markup (see constants in
+ * Role.h). Doesn't take into account ARIA roles.
+ */
+ virtual mozilla::a11y::role NativeRole() const;
+
+ virtual uint64_t State() override;
+
+ /**
+ * Return interactive states present on the accessible
+ * (@see NativeInteractiveState).
+ */
+ uint64_t InteractiveState() const {
+ uint64_t state = NativeInteractiveState();
+ ApplyARIAState(&state);
+ return state;
+ }
+
+ /**
+ * Return link states present on the accessible.
+ */
+ uint64_t LinkState() const {
+ uint64_t state = NativeLinkState();
+ ApplyARIAState(&state);
+ return state;
+ }
+
+ /**
+ * Return the states of accessible, not taking into account ARIA states.
+ * Use State() to get complete set of states.
+ */
+ virtual uint64_t NativeState() const;
+
+ /**
+ * Return native interactice state (unavailable, focusable or selectable).
+ */
+ virtual uint64_t NativeInteractiveState() const;
+
+ /**
+ * Return native link states present on the accessible.
+ */
+ virtual uint64_t NativeLinkState() const;
+
+ /**
+ * Return bit set of invisible and offscreen states.
+ */
+ uint64_t VisibilityState() const;
+
+ /**
+ * Return true if native unavailable state present.
+ */
+ virtual bool NativelyUnavailable() const;
+
+ virtual already_AddRefed<AccAttributes> Attributes() override;
+
+ /**
+ * Return direct or deepest child at the given point.
+ *
+ * @param aX [in] x coordinate relative screen
+ * @param aY [in] y coordinate relative screen
+ * @param aWhichChild [in] flag points if deepest or direct child
+ * should be returned
+ */
+ virtual LocalAccessible* LocalChildAtPoint(int32_t aX, int32_t aY,
+ EWhichChildAtPoint aWhichChild);
+
+ /**
+ * Similar to LocalChildAtPoint but crosses process boundaries.
+ */
+ virtual Accessible* ChildAtPoint(int32_t aX, int32_t aY,
+ EWhichChildAtPoint aWhichChild) override;
+
+ virtual Relation RelationByType(RelationType aType) const override;
+
+ //////////////////////////////////////////////////////////////////////////////
+ // Initializing methods
+
+ /**
+ * Shutdown this accessible object.
+ */
+ virtual void Shutdown();
+
+ /**
+ * Set the ARIA role map entry for a new accessible.
+ */
+ void SetRoleMapEntry(const nsRoleMapEntry* aRoleMapEntry);
+
+ /**
+ * Append/insert/remove a child. Return true if operation was successful.
+ */
+ bool AppendChild(LocalAccessible* aChild) {
+ return InsertChildAt(mChildren.Length(), aChild);
+ }
+ virtual bool InsertChildAt(uint32_t aIndex, LocalAccessible* aChild);
+
+ /**
+ * Inserts a child after given sibling. If the child cannot be inserted,
+ * then the child is unbound from the document, and false is returned. Make
+ * sure to null out any references on the child object as it may be destroyed.
+ */
+ bool InsertAfter(LocalAccessible* aNewChild, LocalAccessible* aRefChild);
+
+ virtual bool RemoveChild(LocalAccessible* aChild);
+
+ /**
+ * Reallocates the child within its parent.
+ */
+ virtual void RelocateChild(uint32_t aNewIndex, LocalAccessible* aChild);
+
+ // Accessible hierarchy method overrides
+
+ virtual Accessible* Parent() const override { return LocalParent(); }
+
+ virtual Accessible* ChildAt(uint32_t aIndex) const override {
+ return LocalChildAt(aIndex);
+ }
+
+ virtual Accessible* NextSibling() const override {
+ return LocalNextSibling();
+ }
+
+ virtual Accessible* PrevSibling() const override {
+ return LocalPrevSibling();
+ }
+
+ //////////////////////////////////////////////////////////////////////////////
+ // LocalAccessible tree traverse methods
+
+ /**
+ * Return parent accessible.
+ */
+ LocalAccessible* LocalParent() const { return mParent; }
+
+ /**
+ * Return child accessible at the given index.
+ */
+ virtual LocalAccessible* LocalChildAt(uint32_t aIndex) const;
+
+ /**
+ * Return child accessible count.
+ */
+ virtual uint32_t ChildCount() const override;
+
+ /**
+ * Return index of the given child accessible.
+ */
+ int32_t GetIndexOf(const LocalAccessible* aChild) const {
+ return (aChild->mParent != this) ? -1 : aChild->IndexInParent();
+ }
+
+ /**
+ * Return index in parent accessible.
+ */
+ virtual int32_t IndexInParent() const override;
+
+ /**
+ * Return first/last/next/previous sibling of the accessible.
+ */
+ inline LocalAccessible* LocalNextSibling() const {
+ return GetSiblingAtOffset(1);
+ }
+ inline LocalAccessible* LocalPrevSibling() const {
+ return GetSiblingAtOffset(-1);
+ }
+ inline LocalAccessible* LocalFirstChild() const { return LocalChildAt(0); }
+ inline LocalAccessible* LocalLastChild() const {
+ uint32_t childCount = ChildCount();
+ return childCount != 0 ? LocalChildAt(childCount - 1) : nullptr;
+ }
+
+ virtual uint32_t EmbeddedChildCount() override;
+
+ /**
+ * Return embedded accessible child at the given index.
+ */
+ virtual LocalAccessible* EmbeddedChildAt(uint32_t aIndex) override;
+
+ virtual int32_t IndexOfEmbeddedChild(Accessible* aChild) override;
+
+ /**
+ * Return number of content children/content child at index. The content
+ * child is created from markup in contrast to it's never constructed by its
+ * parent accessible (like treeitem accessibles for XUL trees).
+ */
+ uint32_t ContentChildCount() const { return mChildren.Length(); }
+ LocalAccessible* ContentChildAt(uint32_t aIndex) const {
+ return mChildren.ElementAt(aIndex);
+ }
+
+ /**
+ * Return true if the accessible is attached to tree.
+ */
+ bool IsBoundToParent() const { return !!mParent; }
+
+ //////////////////////////////////////////////////////////////////////////////
+ // Miscellaneous methods
+
+ /**
+ * Handle accessible event, i.e. process it, notifies observers and fires
+ * platform specific event.
+ */
+ virtual nsresult HandleAccEvent(AccEvent* aAccEvent);
+
+ /**
+ * Return true if the accessible is an acceptable child.
+ */
+ virtual bool IsAcceptableChild(nsIContent* aEl) const {
+ return aEl &&
+ !aEl->IsAnyOfHTMLElements(nsGkAtoms::option, nsGkAtoms::optgroup);
+ }
+
+ virtual void AppendTextTo(nsAString& aText, uint32_t aStartOffset = 0,
+ uint32_t aLength = UINT32_MAX) override;
+
+ virtual nsRect BoundsInAppUnits() const override;
+
+ virtual LayoutDeviceIntRect Bounds() const override;
+
+ /**
+ * Return boundaries rect relative the bounding frame.
+ */
+ virtual nsRect RelativeBounds(nsIFrame** aRelativeFrame) const;
+
+ /**
+ * Return boundaries rect relative to the frame of the parent accessible.
+ * The returned bounds are the same regardless of whether the parent is
+ * scrolled. This means the scroll position must be later subtracted to
+ * calculate absolute coordinates.
+ */
+ virtual nsRect ParentRelativeBounds();
+
+ /**
+ * Selects the accessible within its container if applicable.
+ */
+ virtual void SetSelected(bool aSelect) override;
+
+ /**
+ * Select the accessible within its container.
+ */
+ virtual void TakeSelection() override;
+
+ /**
+ * Focus the accessible.
+ */
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY virtual void TakeFocus() const override;
+
+ MOZ_CAN_RUN_SCRIPT
+ virtual void ScrollTo(uint32_t aHow) const override;
+
+ /**
+ * Scroll the accessible to the given point.
+ */
+ void ScrollToPoint(uint32_t aCoordinateType, int32_t aX, int32_t aY);
+
+ /**
+ * Get a pointer to accessibility interface for this node, which is specific
+ * to the OS/accessibility toolkit we're running on.
+ */
+ virtual void GetNativeInterface(void** aNativeAccessible);
+
+ virtual Maybe<int32_t> GetIntARIAAttr(nsAtom* aAttrName) const override;
+
+ //////////////////////////////////////////////////////////////////////////////
+ // Downcasting and types
+
+ inline bool IsAbbreviation() const {
+ return mContent &&
+ mContent->IsAnyOfHTMLElements(nsGkAtoms::abbr, nsGkAtoms::acronym);
+ }
+
+ ApplicationAccessible* AsApplication();
+
+ DocAccessible* AsDoc();
+
+ HyperTextAccessible* AsHyperText();
+ virtual HyperTextAccessibleBase* AsHyperTextBase() override;
+
+ HTMLLIAccessible* AsHTMLListItem();
+
+ HTMLLinkAccessible* AsHTMLLink();
+
+ ImageAccessible* AsImage();
+
+ HTMLImageMapAccessible* AsImageMap();
+
+ OuterDocAccessible* AsOuterDoc();
+
+ a11y::RootAccessible* AsRoot();
+
+ bool IsSearchbox() const;
+
+ virtual TableAccessible* AsTable() { return nullptr; }
+
+ virtual TableCellAccessible* AsTableCell() { return nullptr; }
+ const TableCellAccessible* AsTableCell() const {
+ return const_cast<LocalAccessible*>(this)->AsTableCell();
+ }
+
+ virtual TableAccessibleBase* AsTableBase() override;
+ virtual TableCellAccessibleBase* AsTableCellBase() override;
+
+ TextLeafAccessible* AsTextLeaf();
+
+ XULLabelAccessible* AsXULLabel();
+
+ XULTreeAccessible* AsXULTree();
+
+ //////////////////////////////////////////////////////////////////////////////
+ // ActionAccessible
+
+ virtual bool HasPrimaryAction() const override;
+
+ virtual uint8_t ActionCount() const override;
+
+ virtual void ActionNameAt(uint8_t aIndex, nsAString& aName) override;
+
+ virtual bool DoAction(uint8_t aIndex) const override;
+
+ virtual KeyBinding AccessKey() const override;
+
+ /**
+ * Return global keyboard shortcut for default action, such as Ctrl+O for
+ * Open file menuitem.
+ */
+ virtual KeyBinding KeyboardShortcut() const;
+
+ //////////////////////////////////////////////////////////////////////////////
+ // HyperLinkAccessible (any embedded object in text can implement HyperLink,
+ // which helps determine where it is located within containing text).
+
+ /**
+ * Return true if the accessible is hyper link accessible.
+ */
+ virtual bool IsLink() const override;
+
+ /**
+ * Return true if the link is valid (e. g. points to a valid URL).
+ */
+ inline bool IsLinkValid() {
+ MOZ_ASSERT(IsLink(), "IsLinkValid is called on not hyper link!");
+
+ // XXX In order to implement this we would need to follow every link
+ // Perhaps we can get information about invalid links from the cache
+ // In the mean time authors can use role="link" aria-invalid="true"
+ // to force it for links they internally know to be invalid
+ return (0 == (State() & mozilla::a11y::states::INVALID));
+ }
+
+ /**
+ * Return the number of anchors within the link.
+ */
+ virtual uint32_t AnchorCount();
+
+ /**
+ * Returns an anchor accessible at the given index.
+ */
+ virtual LocalAccessible* AnchorAt(uint32_t aAnchorIndex);
+
+ /**
+ * Returns an anchor URI at the given index.
+ */
+ virtual already_AddRefed<nsIURI> AnchorURIAt(uint32_t aAnchorIndex) const;
+
+ //////////////////////////////////////////////////////////////////////////////
+ // SelectAccessible
+
+ /**
+ * Return an array of selected items.
+ */
+ virtual void SelectedItems(nsTArray<Accessible*>* aItems) override;
+
+ /**
+ * Return the number of selected items.
+ */
+ virtual uint32_t SelectedItemCount() override;
+
+ /**
+ * Return selected item at the given index.
+ */
+ virtual Accessible* GetSelectedItem(uint32_t aIndex) override;
+
+ /**
+ * Determine if item at the given index is selected.
+ */
+ virtual bool IsItemSelected(uint32_t aIndex) override;
+
+ /**
+ * Add item at the given index the selection. Return true if success.
+ */
+ virtual bool AddItemToSelection(uint32_t aIndex) override;
+
+ /**
+ * Remove item at the given index from the selection. Return if success.
+ */
+ virtual bool RemoveItemFromSelection(uint32_t aIndex) override;
+
+ /**
+ * Select all items. Return true if success.
+ */
+ virtual bool SelectAll() override;
+
+ /**
+ * Unselect all items. Return true if success.
+ */
+ virtual bool UnselectAll() override;
+
+ //////////////////////////////////////////////////////////////////////////////
+ // Value (numeric value interface)
+
+ virtual double MaxValue() const override;
+ virtual double MinValue() const override;
+ virtual double CurValue() const override;
+ virtual double Step() const override;
+ virtual bool SetCurValue(double aValue);
+
+ //////////////////////////////////////////////////////////////////////////////
+ // Widgets
+
+ /**
+ * Return true if accessible is a widget, i.e. control or accessible that
+ * manages its items. Note, being a widget the accessible may be a part of
+ * composite widget.
+ */
+ virtual bool IsWidget() const;
+
+ /**
+ * Return true if the widget is active, i.e. has a focus within it.
+ */
+ virtual bool IsActiveWidget() const;
+
+ /**
+ * Return true if the widget has items and items are operable by user and
+ * can be activated.
+ */
+ virtual bool AreItemsOperable() const;
+
+ /**
+ * Return the current item of the widget, i.e. an item that has or will have
+ * keyboard focus when widget gets active.
+ */
+ virtual LocalAccessible* CurrentItem() const;
+
+ /**
+ * Set the current item of the widget.
+ */
+ virtual void SetCurrentItem(const LocalAccessible* aItem);
+
+ /**
+ * Return container widget this accessible belongs to.
+ */
+ virtual LocalAccessible* ContainerWidget() const;
+
+ bool IsActiveDescendant(LocalAccessible** aWidget = nullptr) const;
+
+ /**
+ * Return true if the accessible is defunct.
+ */
+ bool IsDefunct() const;
+
+ /**
+ * Return false if the accessible is no longer in the document.
+ */
+ bool IsInDocument() const { return !(mStateFlags & eIsNotInDocument); }
+
+ /**
+ * Return true if the accessible should be contained by document node map.
+ */
+ bool IsNodeMapEntry() const {
+ return HasOwnContent() && !(mStateFlags & eNotNodeMapEntry);
+ }
+
+ /**
+ * Return true if the accessible has associated DOM content.
+ */
+ bool HasOwnContent() const {
+ return mContent && !(mStateFlags & eSharedNode);
+ }
+
+ /**
+ * Return true if native markup has a numeric value.
+ */
+ bool NativeHasNumericValue() const;
+
+ /**
+ * Return true if ARIA specifies support for a numeric value.
+ */
+ bool ARIAHasNumericValue() const;
+
+ /**
+ * Return true if the accessible has a numeric value.
+ */
+ virtual bool HasNumericValue() const override;
+
+ /**
+ * Return true if the accessible state change is processed by handling proper
+ * DOM UI event, if otherwise then false. For example, CheckboxAccessible
+ * created for HTML:input@type="checkbox" will process
+ * nsIDocumentObserver::ElementStateChanged instead of 'CheckboxStateChange'
+ * event.
+ */
+ bool NeedsDOMUIEvent() const { return !(mStateFlags & eIgnoreDOMUIEvent); }
+
+ /**
+ * Get/set repositioned bit indicating that the accessible was moved in
+ * the accessible tree, i.e. the accessible tree structure differs from DOM.
+ */
+ bool IsRelocated() const { return mStateFlags & eRelocated; }
+ void SetRelocated(bool aRelocated) {
+ if (aRelocated) {
+ mStateFlags |= eRelocated;
+ } else {
+ mStateFlags &= ~eRelocated;
+ }
+ }
+
+ /**
+ * Return true if the accessible allows accessible children from subtree of
+ * a DOM element of this accessible.
+ */
+ bool KidsFromDOM() const { return !(mStateFlags & eNoKidsFromDOM); }
+
+ /**
+ * Return true if this accessible has a parent, relation or ancestor with a
+ * relation whose name depends on this accessible.
+ */
+ bool HasNameDependent() const { return mContextFlags & eHasNameDependent; }
+
+ /**
+ * Return true if this accessible has a parent, relation or ancestor with a
+ * relation whose description depends on this accessible.
+ */
+ bool HasDescriptionDependent() const {
+ return mContextFlags & eHasDescriptionDependent;
+ }
+
+ /**
+ * Return true if the element is inside an alert.
+ */
+ bool IsInsideAlert() const { return mContextFlags & eInsideAlert; }
+
+ /**
+ * Return true if there is a pending reorder event for this accessible.
+ */
+ bool ReorderEventTarget() const { return mReorderEventTarget; }
+
+ /**
+ * Return true if there is a pending show event for this accessible.
+ */
+ bool ShowEventTarget() const { return mShowEventTarget; }
+
+ /**
+ * Return true if there is a pending hide event for this accessible.
+ */
+ bool HideEventTarget() const { return mHideEventTarget; }
+
+ /**
+ * Set if there is a pending reorder event for this accessible.
+ */
+ void SetReorderEventTarget(bool aTarget) { mReorderEventTarget = aTarget; }
+
+ /**
+ * Set if this accessible is a show event target.
+ */
+ void SetShowEventTarget(bool aTarget) { mShowEventTarget = aTarget; }
+
+ /**
+ * Set if this accessible is a hide event target.
+ */
+ void SetHideEventTarget(bool aTarget) { mHideEventTarget = aTarget; }
+
+ void Announce(const nsAString& aAnnouncement, uint16_t aPriority);
+
+ virtual bool IsRemote() const override { return false; }
+
+ already_AddRefed<AccAttributes> BundleFieldsForCache(
+ uint64_t aCacheDomain, CacheUpdateType aUpdateType);
+
+ /**
+ * Push fields to cache.
+ * aCacheDomain - describes which fields to bundle and ultimately send
+ * aUpdate - describes whether this is an initial or subsequent update
+ */
+ void SendCache(uint64_t aCacheDomain, CacheUpdateType aUpdate);
+
+ void MaybeQueueCacheUpdateForStyleChanges();
+
+ virtual nsAtom* TagName() const override;
+
+ virtual already_AddRefed<nsAtom> DisplayStyle() const override;
+
+ virtual float Opacity() const override;
+
+ virtual void DOMNodeID(nsString& aID) const override;
+
+ virtual void LiveRegionAttributes(nsAString* aLive, nsAString* aRelevant,
+ Maybe<bool>* aAtomic,
+ nsAString* aBusy) const override;
+
+ virtual Maybe<bool> ARIASelected() const override;
+
+ protected:
+ virtual ~LocalAccessible();
+
+ /**
+ * Return the accessible name provided by native markup. It doesn't take
+ * into account ARIA markup used to specify the name.
+ */
+ virtual mozilla::a11y::ENameValueFlag NativeName(nsString& aName) const;
+
+ /**
+ * Return the accessible description provided by native markup. It doesn't
+ * take into account ARIA markup used to specify the description.
+ */
+ void NativeDescription(nsString& aDescription) const;
+
+ /**
+ * Return object attributes provided by native markup. It doesn't take into
+ * account ARIA.
+ */
+ virtual already_AddRefed<AccAttributes> NativeAttributes();
+
+ /**
+ * The given attribute has the potential of changing the accessible's state.
+ * This is used to capture the state before the attribute change and compare
+ * it with the state after.
+ */
+ virtual bool AttributeChangesState(nsAtom* aAttribute);
+
+ /**
+ * Notify accessible that a DOM attribute on its associated content has
+ * changed. This allows the accessible to update its state and emit any
+ * relevant events.
+ */
+ virtual void DOMAttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute,
+ int32_t aModType,
+ const nsAttrValue* aOldValue,
+ uint64_t aOldState);
+
+ //////////////////////////////////////////////////////////////////////////////
+ // Initializing, cache and tree traverse methods
+
+ /**
+ * Destroy the object.
+ */
+ void LastRelease();
+
+ /**
+ * Set accessible parent and index in parent.
+ */
+ void BindToParent(LocalAccessible* aParent, uint32_t aIndexInParent);
+ void UnbindFromParent();
+
+ /**
+ * Return sibling accessible at the given offset.
+ */
+ virtual LocalAccessible* GetSiblingAtOffset(int32_t aOffset,
+ nsresult* aError = nullptr) const;
+
+ void ModifySubtreeContextFlags(uint32_t aContextFlags, bool aAdd);
+
+ /**
+ * Flags used to describe the state of this accessible.
+ */
+ enum StateFlags {
+ eIsDefunct = 1 << 0, // accessible is defunct
+ eIsNotInDocument = 1 << 1, // accessible is not in document
+ eSharedNode = 1 << 2, // accessible shares DOM node from another accessible
+ eNotNodeMapEntry = 1 << 3, // accessible shouldn't be in document node map
+ eGroupInfoDirty = 1 << 4, // accessible needs to update group info
+ eKidsMutating = 1 << 5, // subtree is being mutated
+ eIgnoreDOMUIEvent = 1 << 6, // don't process DOM UI events for a11y events
+ eRelocated = 1 << 7, // accessible was moved in tree
+ eNoKidsFromDOM = 1 << 8, // accessible doesn't allow children from DOM
+ eHasTextKids = 1 << 9, // accessible have a text leaf in children
+ eOldFrameHasValidTransformStyle =
+ 1 << 10, // frame prior to most recent style change both has transform
+ // styling and supports transforms
+
+ eLastStateFlag = eOldFrameHasValidTransformStyle
+ };
+
+ /**
+ * Flags used for contextual information about the accessible.
+ */
+ enum ContextFlags {
+ eHasNameDependent = 1 << 0, // See HasNameDependent().
+ eInsideAlert = 1 << 1,
+ eHasDescriptionDependent = 1 << 2, // See HasDescriptionDependent().
+
+ eLastContextFlag = eHasDescriptionDependent
+ };
+
+ protected:
+ //////////////////////////////////////////////////////////////////////////////
+ // Miscellaneous helpers
+
+ /**
+ * Return ARIA role (helper method).
+ */
+ mozilla::a11y::role ARIATransformRole(mozilla::a11y::role aRole) const;
+
+ //////////////////////////////////////////////////////////////////////////////
+ // Name helpers
+
+ /**
+ * Returns the accessible name specified by ARIA.
+ */
+ void ARIAName(nsString& aName) const;
+
+ /**
+ * Returns the accessible description specified by ARIA.
+ */
+ void ARIADescription(nsString& aDescription) const;
+
+ /**
+ * Returns the accessible name specified for this control using XUL
+ * <label control="id" ...>.
+ */
+ static void NameFromAssociatedXULLabel(DocAccessible* aDocument,
+ nsIContent* aElm, nsString& aName);
+
+ /**
+ * Return the name for XUL element.
+ */
+ static void XULElmName(DocAccessible* aDocument, nsIContent* aElm,
+ nsString& aName);
+
+ // helper method to verify frames
+ static nsresult GetFullKeyName(const nsAString& aModifierName,
+ const nsAString& aKeyName,
+ nsAString& aStringOut);
+
+ //////////////////////////////////////////////////////////////////////////////
+ // Action helpers
+
+ /**
+ * Prepares click action that will be invoked in timeout.
+ *
+ * @note DoCommand() prepares an action in timeout because when action
+ * command opens a modal dialog/window, it won't return until the
+ * dialog/window is closed. If executing action command directly in
+ * nsIAccessible::DoAction() method, it will block AT tools (e.g. GOK) that
+ * invoke action of mozilla accessibles direclty (see bug 277888 for
+ * details).
+ *
+ * @param aContent [in, optional] element to click
+ * @param aActionIndex [in, optional] index of accessible action
+ */
+ void DoCommand(nsIContent* aContent = nullptr,
+ uint32_t aActionIndex = 0) const;
+
+ /**
+ * Dispatch click event.
+ */
+ MOZ_CAN_RUN_SCRIPT
+ virtual void DispatchClickEvent(nsIContent* aContent,
+ uint32_t aActionIndex) const;
+
+ //////////////////////////////////////////////////////////////////////////////
+ // Helpers
+
+ /**
+ * Get the container node for an atomic region, defined by aria-atomic="true"
+ * @return the container node
+ */
+ nsIContent* GetAtomicRegion() const;
+
+ /**
+ * Return numeric value of the given ARIA attribute, NaN if not applicable.
+ *
+ * @param aARIAProperty [in] the ARIA property we're using
+ * @return a numeric value
+ */
+ double AttrNumericValue(nsAtom* aARIAAttr) const;
+
+ /**
+ * Return the action rule based on ARIA enum constants EActionRule
+ * (see ARIAMap.h). Used by ActionCount() and ActionNameAt().
+ */
+ uint32_t GetActionRule() const;
+
+ virtual AccGroupInfo* GetGroupInfo() const override;
+
+ virtual AccGroupInfo* GetOrCreateGroupInfo() override;
+
+ virtual void ARIAGroupPosition(int32_t* aLevel, int32_t* aSetSize,
+ int32_t* aPosInSet) const override;
+
+ // Data Members
+ // mContent can be null in a DocAccessible if the document has no body or
+ // root element.
+ nsCOMPtr<nsIContent> mContent;
+ RefPtr<DocAccessible> mDoc;
+
+ LocalAccessible* mParent;
+ nsTArray<LocalAccessible*> mChildren;
+ int32_t mIndexInParent;
+
+ // These are used to determine whether to send cache updates.
+ Maybe<nsRect> mBounds;
+ int32_t mFirstLineStart;
+
+ /**
+ * Maintain a reference to the ComputedStyle of our frame so we can
+ * send cache updates when style changes are observed.
+ *
+ * This RefPtr is initialised in BundleFieldsForCache to the ComputedStyle
+ * for our initial frame.
+ * Style changes are observed in one of two ways:
+ * 1. Style changes on the same frame are observed in
+ * nsIFrame::DidSetComputedStyle.
+ * 2. Style changes for reconstructed frames are handled in
+ * DocAccessible::PruneOrInsertSubtree.
+ * In both cases, we call into MaybeQueueCacheUpdateForStyleChanges. There, we
+ * compare a11y-relevant properties in mOldComputedStyle with the current
+ * ComputedStyle fetched from GetFrame()->Style(). Finally, we send cache
+ * updates for attributes affected by the style change and update
+ * mOldComputedStyle to the style of our current frame.
+ */
+ RefPtr<const ComputedStyle> mOldComputedStyle;
+
+ static const uint8_t kStateFlagsBits = 11;
+ static const uint8_t kContextFlagsBits = 3;
+
+ /**
+ * Keep in sync with StateFlags, ContextFlags, and AccTypes.
+ */
+ mutable uint32_t mStateFlags : kStateFlagsBits;
+ uint32_t mContextFlags : kContextFlagsBits;
+ uint32_t mReorderEventTarget : 1;
+ uint32_t mShowEventTarget : 1;
+ uint32_t mHideEventTarget : 1;
+
+ void StaticAsserts() const;
+
+#ifdef A11Y_LOG
+ friend void logging::Tree(const char* aTitle, const char* aMsgText,
+ LocalAccessible* aRoot,
+ logging::GetTreePrefix aPrefixFunc,
+ void* aGetTreePrefixData);
+ friend void logging::TreeSize(const char* aTitle, const char* aMsgText,
+ LocalAccessible* aRoot);
+#endif
+ friend class DocAccessible;
+ friend class xpcAccessible;
+ friend class TreeMutation;
+
+ UniquePtr<mozilla::a11y::EmbeddedObjCollector> mEmbeddedObjCollector;
+ int32_t mIndexOfEmbeddedChild;
+
+ friend class EmbeddedObjCollector;
+
+ mutable AccGroupInfo* mGroupInfo;
+ friend class AccGroupInfo;
+
+ private:
+ LocalAccessible() = delete;
+ LocalAccessible(const LocalAccessible&) = delete;
+ LocalAccessible& operator=(const LocalAccessible&) = delete;
+
+ /**
+ * Traverses the accessible's parent chain in search of an accessible with
+ * a frame. Returns the frame when found. Includes special handling for
+ * OOP iframe docs and tab documents.
+ */
+ nsIFrame* FindNearestAccessibleAncestorFrame();
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(LocalAccessible, NS_ACCESSIBLE_IMPL_IID)
+
+////////////////////////////////////////////////////////////////////////////////
+// LocalAccessible downcasting method
+
+inline LocalAccessible* Accessible::AsLocal() {
+ return IsLocal() ? static_cast<LocalAccessible*>(this) : nullptr;
+}
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/generic/OuterDocAccessible.cpp b/accessible/generic/OuterDocAccessible.cpp
new file mode 100644
index 0000000000..3c03551e98
--- /dev/null
+++ b/accessible/generic/OuterDocAccessible.cpp
@@ -0,0 +1,236 @@
+/* -*- 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 "OuterDocAccessible.h"
+
+#include "LocalAccessible-inl.h"
+#include "nsAccUtils.h"
+#include "DocAccessible-inl.h"
+#include "mozilla/a11y/DocAccessibleChild.h"
+#include "mozilla/a11y/DocAccessibleParent.h"
+#include "mozilla/dom/BrowserBridgeChild.h"
+#include "mozilla/dom/BrowserParent.h"
+#include "Role.h"
+#include "States.h"
+
+#ifdef A11Y_LOG
+# include "Logging.h"
+#endif
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+////////////////////////////////////////////////////////////////////////////////
+// OuterDocAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+OuterDocAccessible::OuterDocAccessible(nsIContent* aContent,
+ DocAccessible* aDoc)
+ : AccessibleWrap(aContent, aDoc) {
+ mType = eOuterDocType;
+
+#ifdef XP_WIN
+ if (DocAccessibleParent* remoteDoc = RemoteChildDoc()) {
+ remoteDoc->SendParentCOMProxy(this);
+ }
+#endif
+
+ if (IPCAccessibilityActive()) {
+ auto bridge = dom::BrowserBridgeChild::GetFrom(aContent);
+ if (bridge) {
+ // This is an iframe which will be rendered in another process.
+ SendEmbedderAccessible(bridge);
+ }
+ }
+
+ // Request document accessible for the content document to make sure it's
+ // created. It will appended to outerdoc accessible children asynchronously.
+ dom::Document* outerDoc = mContent->GetUncomposedDoc();
+ if (outerDoc) {
+ dom::Document* innerDoc = outerDoc->GetSubDocumentFor(mContent);
+ if (innerDoc) GetAccService()->GetDocAccessible(innerDoc);
+ }
+}
+
+OuterDocAccessible::~OuterDocAccessible() {}
+
+void OuterDocAccessible::SendEmbedderAccessible(
+ dom::BrowserBridgeChild* aBridge) {
+ MOZ_ASSERT(mDoc);
+ DocAccessibleChild* ipcDoc = mDoc->IPCDoc();
+ if (ipcDoc) {
+ uint64_t id = reinterpret_cast<uintptr_t>(UniqueID());
+#if defined(XP_WIN)
+ ipcDoc->SetEmbedderOnBridge(aBridge, id);
+#else
+ aBridge->SetEmbedderAccessible(ipcDoc, id);
+#endif
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// LocalAccessible public (DON'T add methods here)
+
+role OuterDocAccessible::NativeRole() const { return roles::INTERNAL_FRAME; }
+
+LocalAccessible* OuterDocAccessible::LocalChildAtPoint(
+ int32_t aX, int32_t aY, EWhichChildAtPoint aWhichChild) {
+ LayoutDeviceIntRect docRect = Bounds();
+ if (!docRect.Contains(aX, aY)) return nullptr;
+
+ // Always return the inner doc as direct child accessible unless bounds
+ // outside of it.
+ LocalAccessible* child = LocalChildAt(0);
+ NS_ENSURE_TRUE(child, nullptr);
+
+ if (aWhichChild == Accessible::EWhichChildAtPoint::DeepestChild) {
+ return child->LocalChildAtPoint(
+ aX, aY, Accessible::EWhichChildAtPoint::DeepestChild);
+ }
+ return child;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// LocalAccessible public
+
+void OuterDocAccessible::Shutdown() {
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eDocDestroy)) logging::OuterDocDestroy(this);
+#endif
+
+ if (auto* bridge = dom::BrowserBridgeChild::GetFrom(mContent)) {
+ uint64_t id = reinterpret_cast<uintptr_t>(UniqueID());
+ if (bridge->GetEmbedderAccessibleID() == id) {
+ // We were the last embedder accessible sent via PBrowserBridge; i.e. a
+ // new embedder accessible hasn't been created yet for this iframe. Clear
+ // the embedder accessible on PBrowserBridge.
+ bridge->SetEmbedderAccessible(nullptr, 0);
+ }
+ }
+
+ LocalAccessible* child = mChildren.SafeElementAt(0, nullptr);
+ if (child) {
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eDocDestroy)) {
+ logging::DocDestroy("outerdoc's child document rebind is scheduled",
+ child->AsDoc()->DocumentNode());
+ }
+#endif
+ RemoveChild(child);
+
+ // XXX: sometimes outerdoc accessible is shutdown because of layout style
+ // change however the presshell of underlying document isn't destroyed and
+ // the document doesn't get pagehide events. Schedule a document rebind
+ // to its parent document. Otherwise a document accessible may be lost if
+ // its outerdoc has being recreated (see bug 862863 for details).
+ if (!mDoc->IsDefunct()) {
+ MOZ_ASSERT(!child->IsDefunct(),
+ "Attempt to reattach shutdown document accessible");
+ if (!child->IsDefunct()) {
+ mDoc->BindChildDocument(child->AsDoc());
+ }
+ }
+ }
+
+ AccessibleWrap::Shutdown();
+}
+
+bool OuterDocAccessible::InsertChildAt(uint32_t aIdx,
+ LocalAccessible* aAccessible) {
+ MOZ_RELEASE_ASSERT(aAccessible->IsDoc(),
+ "OuterDocAccessible can have a document child only!");
+
+ // We keep showing the old document for a bit after creating the new one,
+ // and while building the new DOM and frame tree. That's done on purpose
+ // to avoid weird flashes of default background color.
+ // The old viewer will be destroyed after the new one is created.
+ // For a11y, it should be safe to shut down the old document now.
+ if (mChildren.Length()) mChildren[0]->Shutdown();
+
+ if (!AccessibleWrap::InsertChildAt(0, aAccessible)) return false;
+
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eDocCreate)) {
+ logging::DocCreate("append document to outerdoc",
+ aAccessible->AsDoc()->DocumentNode());
+ logging::Address("outerdoc", this);
+ }
+#endif
+
+ return true;
+}
+
+bool OuterDocAccessible::RemoveChild(LocalAccessible* aAccessible) {
+ LocalAccessible* child = mChildren.SafeElementAt(0, nullptr);
+ MOZ_ASSERT(child == aAccessible, "Wrong child to remove!");
+ if (child != aAccessible) {
+ return false;
+ }
+
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eDocDestroy)) {
+ logging::DocDestroy("remove document from outerdoc",
+ child->AsDoc()->DocumentNode(), child->AsDoc());
+ logging::Address("outerdoc", this);
+ }
+#endif
+
+ bool wasRemoved = AccessibleWrap::RemoveChild(child);
+
+ NS_ASSERTION(!mChildren.Length(),
+ "This child document of outerdoc accessible wasn't removed!");
+
+ return wasRemoved;
+}
+
+bool OuterDocAccessible::IsAcceptableChild(nsIContent* aEl) const {
+ // outer document accessible doesn't not participate in ordinal tree
+ // mutations.
+ return false;
+}
+
+// Accessible
+
+uint32_t OuterDocAccessible::ChildCount() const {
+ uint32_t result = mChildren.Length();
+ if (!result && RemoteChildDoc()) {
+ result = 1;
+ }
+ return result;
+}
+
+Accessible* OuterDocAccessible::ChildAt(uint32_t aIndex) const {
+ LocalAccessible* result = LocalChildAt(aIndex);
+ if (result || aIndex) {
+ return result;
+ }
+
+ return RemoteChildDoc();
+}
+
+Accessible* OuterDocAccessible::ChildAtPoint(int32_t aX, int32_t aY,
+ EWhichChildAtPoint aWhichChild) {
+ LayoutDeviceIntRect docRect = Bounds();
+ if (!docRect.Contains(aX, aY)) return nullptr;
+
+ // Always return the inner doc as direct child accessible unless bounds
+ // outside of it.
+ Accessible* child = ChildAt(0);
+ NS_ENSURE_TRUE(child, nullptr);
+
+ if (aWhichChild == EWhichChildAtPoint::DeepestChild) {
+ return child->ChildAtPoint(aX, aY, EWhichChildAtPoint::DeepestChild);
+ }
+ return child;
+}
+
+DocAccessibleParent* OuterDocAccessible::RemoteChildDoc() const {
+ dom::BrowserParent* tab = dom::BrowserParent::GetFrom(GetContent());
+ if (!tab) {
+ return nullptr;
+ }
+
+ return tab->GetTopLevelDocAccessible();
+}
diff --git a/accessible/generic/OuterDocAccessible.h b/accessible/generic/OuterDocAccessible.h
new file mode 100644
index 0000000000..6c16d69124
--- /dev/null
+++ b/accessible/generic/OuterDocAccessible.h
@@ -0,0 +1,80 @@
+/* -*- 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/. */
+
+#ifndef MOZILLA_A11Y_OUTERDOCACCESSIBLE_H_
+#define MOZILLA_A11Y_OUTERDOCACCESSIBLE_H_
+
+#include "AccessibleWrap.h"
+
+namespace mozilla {
+
+namespace dom {
+class BrowserBridgeChild;
+}
+
+namespace a11y {
+class DocAccessibleParent;
+
+/**
+ * Used for <browser>, <frame>, <iframe>, <page> or editor> elements.
+ *
+ * In these variable names, "outer" relates to the OuterDocAccessible as
+ * opposed to the DocAccessibleWrap which is "inner". The outer node is
+ * a something like tags listed above, whereas the inner node corresponds to
+ * the inner document root.
+ */
+
+class OuterDocAccessible final : public AccessibleWrap {
+ public:
+ OuterDocAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(OuterDocAccessible, AccessibleWrap)
+
+ DocAccessibleParent* RemoteChildDoc() const;
+
+ /**
+ * For iframes in a content process which will be rendered in another content
+ * process, tell the parent process about this OuterDocAccessible
+ * so it can link the trees together when the embedded document is added.
+ * Note that an OuterDocAccessible can be created before the
+ * BrowserBridgeChild or vice versa. Therefore, this must be conditionally
+ * called when either of these is created.
+ */
+ void SendEmbedderAccessible(dom::BrowserBridgeChild* aBridge);
+
+ Maybe<nsMargin> GetCrossDocOffset() { return mCrossDocOffset; }
+
+ void SetCrossDocOffset(nsMargin aMargin) { mCrossDocOffset = Some(aMargin); }
+
+ // LocalAccessible
+ virtual void Shutdown() override;
+ virtual mozilla::a11y::role NativeRole() const override;
+ virtual LocalAccessible* LocalChildAtPoint(
+ int32_t aX, int32_t aY, EWhichChildAtPoint aWhichChild) override;
+
+ virtual bool InsertChildAt(uint32_t aIdx, LocalAccessible* aChild) override;
+ virtual bool RemoveChild(LocalAccessible* aAccessible) override;
+ virtual bool IsAcceptableChild(nsIContent* aEl) const override;
+
+ virtual uint32_t ChildCount() const override;
+
+ // Accessible
+ virtual Accessible* ChildAt(uint32_t aIndex) const override;
+ virtual Accessible* ChildAtPoint(int32_t aX, int32_t aY,
+ EWhichChildAtPoint aWhichChild) override;
+
+ protected:
+ virtual ~OuterDocAccessible() override;
+ Maybe<nsMargin> mCrossDocOffset;
+};
+
+inline OuterDocAccessible* LocalAccessible::AsOuterDoc() {
+ return IsOuterDoc() ? static_cast<OuterDocAccessible*>(this) : nullptr;
+}
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/generic/RootAccessible.cpp b/accessible/generic/RootAccessible.cpp
new file mode 100644
index 0000000000..b41e348912
--- /dev/null
+++ b/accessible/generic/RootAccessible.cpp
@@ -0,0 +1,709 @@
+/* -*- 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 "RootAccessible.h"
+
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/PresShell.h" // for nsAccUtils::GetDocAccessibleFor()
+#include "nsXULPopupManager.h"
+
+#define CreateEvent CreateEventA
+
+#include "LocalAccessible-inl.h"
+#include "DocAccessible-inl.h"
+#include "mozilla/a11y/DocAccessibleParent.h"
+#include "nsAccessibilityService.h"
+#include "nsAccUtils.h"
+#include "nsCoreUtils.h"
+#include "nsEventShell.h"
+#include "Relation.h"
+#include "Role.h"
+#include "States.h"
+#include "XULTreeAccessible.h"
+
+#include "mozilla/dom/BindingUtils.h"
+#include "mozilla/dom/CustomEvent.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/ScriptSettings.h"
+#include "mozilla/dom/BrowserHost.h"
+
+#include "nsIDocShellTreeOwner.h"
+#include "mozilla/dom/Event.h"
+#include "mozilla/dom/EventTarget.h"
+#include "nsIDOMXULMultSelectCntrlEl.h"
+#include "mozilla/dom/Document.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsIPropertyBag2.h"
+#include "nsPIDOMWindow.h"
+#include "nsIWebBrowserChrome.h"
+#include "nsReadableUtils.h"
+#include "nsFocusManager.h"
+#include "nsGlobalWindow.h"
+
+#include "nsIAppWindow.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+using namespace mozilla::dom;
+
+////////////////////////////////////////////////////////////////////////////////
+// nsISupports
+
+NS_IMPL_ISUPPORTS_INHERITED(RootAccessible, DocAccessible, nsIDOMEventListener)
+
+////////////////////////////////////////////////////////////////////////////////
+// Constructor/destructor
+
+RootAccessible::RootAccessible(Document* aDocument, PresShell* aPresShell)
+ : DocAccessibleWrap(aDocument, aPresShell) {
+ mType = eRootType;
+}
+
+RootAccessible::~RootAccessible() {}
+
+////////////////////////////////////////////////////////////////////////////////
+// LocalAccessible
+
+ENameValueFlag RootAccessible::Name(nsString& aName) const {
+ aName.Truncate();
+
+ if (ARIARoleMap()) {
+ LocalAccessible::Name(aName);
+ if (!aName.IsEmpty()) return eNameOK;
+ }
+
+ mDocumentNode->GetTitle(aName);
+ return eNameOK;
+}
+
+// RootAccessible protected member
+uint32_t RootAccessible::GetChromeFlags() const {
+ // Return the flag set for the top level window as defined
+ // by nsIWebBrowserChrome::CHROME_WINDOW_[FLAGNAME]
+ // Not simple: nsIAppWindow is not just a QI from nsIDOMWindow
+ nsCOMPtr<nsIDocShell> docShell = nsCoreUtils::GetDocShellFor(mDocumentNode);
+ NS_ENSURE_TRUE(docShell, 0);
+ nsCOMPtr<nsIDocShellTreeOwner> treeOwner;
+ docShell->GetTreeOwner(getter_AddRefs(treeOwner));
+ NS_ENSURE_TRUE(treeOwner, 0);
+ nsCOMPtr<nsIAppWindow> appWin(do_GetInterface(treeOwner));
+ if (!appWin) {
+ return 0;
+ }
+ uint32_t chromeFlags;
+ appWin->GetChromeFlags(&chromeFlags);
+ return chromeFlags;
+}
+
+uint64_t RootAccessible::NativeState() const {
+ uint64_t state = DocAccessibleWrap::NativeState();
+ if (state & states::DEFUNCT) return state;
+
+ uint32_t chromeFlags = GetChromeFlags();
+ if (chromeFlags & nsIWebBrowserChrome::CHROME_WINDOW_RESIZE) {
+ state |= states::SIZEABLE;
+ }
+ // If it has a titlebar it's movable
+ // XXX unless it's minimized or maximized, but not sure
+ // how to detect that
+ if (chromeFlags & nsIWebBrowserChrome::CHROME_TITLEBAR) {
+ state |= states::MOVEABLE;
+ }
+ if (chromeFlags & nsIWebBrowserChrome::CHROME_MODAL) state |= states::MODAL;
+
+ nsFocusManager* fm = nsFocusManager::GetFocusManager();
+ if (fm && fm->GetActiveWindow() == mDocumentNode->GetWindow()) {
+ state |= states::ACTIVE;
+ }
+
+ return state;
+}
+
+const char* const kEventTypes[] = {
+#ifdef DEBUG_DRAGDROPSTART
+ // Capture mouse over events and fire fake DRAGDROPSTART event to simplify
+ // debugging a11y objects with event viewers.
+ "mouseover",
+#endif
+ // Fired when list or tree selection changes.
+ "select",
+ // Fired when value changes immediately, wether or not focused changed.
+ "ValueChange", "AlertActive", "TreeRowCountChanged", "TreeInvalidated",
+ // add ourself as a OpenStateChange listener (custom event fired in
+ // tree.xml)
+ "OpenStateChange",
+ // add ourself as a CheckboxStateChange listener (custom event fired in
+ // HTMLInputElement.cpp)
+ "CheckboxStateChange",
+ // add ourself as a RadioStateChange Listener (custom event fired in in
+ // HTMLInputElement.cpp & radio.js)
+ "RadioStateChange", "popupshown", "popuphiding", "DOMMenuInactive",
+ "DOMMenuItemActive", "DOMMenuItemInactive", "DOMMenuBarActive",
+ "DOMMenuBarInactive", "scroll", "DOMTitleChanged"};
+
+nsresult RootAccessible::AddEventListeners() {
+ // EventTarget interface allows to register event listeners to
+ // receive untrusted events (synthetic events generated by untrusted code).
+ // For example, XBL bindings implementations for elements that are hosted in
+ // non chrome document fire untrusted events.
+ // We must use the window's parent target in order to receive events from
+ // iframes and shadow DOM; e.g. ValueChange events from a <select> in an
+ // iframe or shadow DOM. The root document itself doesn't receive these.
+ nsPIDOMWindowOuter* window = mDocumentNode->GetWindow();
+ nsCOMPtr<EventTarget> nstarget = window ? window->GetParentTarget() : nullptr;
+
+ if (nstarget) {
+ for (const char *const *e = kEventTypes, *const *e_end =
+ ArrayEnd(kEventTypes);
+ e < e_end; ++e) {
+ nsresult rv = nstarget->AddEventListener(NS_ConvertASCIItoUTF16(*e), this,
+ true, true);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ return DocAccessible::AddEventListeners();
+}
+
+nsresult RootAccessible::RemoveEventListeners() {
+ nsPIDOMWindowOuter* window = mDocumentNode->GetWindow();
+ nsCOMPtr<EventTarget> target = window ? window->GetParentTarget() : nullptr;
+ if (target) {
+ for (const char *const *e = kEventTypes, *const *e_end =
+ ArrayEnd(kEventTypes);
+ e < e_end; ++e) {
+ target->RemoveEventListener(NS_ConvertASCIItoUTF16(*e), this, true);
+ }
+ }
+
+ // Do this before removing clearing caret accessible, so that it can use
+ // shutdown the caret accessible's selection listener
+ DocAccessible::RemoveEventListeners();
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// public
+
+void RootAccessible::DocumentActivated(DocAccessible* aDocument) {}
+
+////////////////////////////////////////////////////////////////////////////////
+// nsIDOMEventListener
+
+NS_IMETHODIMP
+RootAccessible::HandleEvent(Event* aDOMEvent) {
+ MOZ_ASSERT(aDOMEvent);
+ if (IsDefunct()) {
+ // Even though we've been shut down, RemoveEventListeners might not have
+ // removed the event handlers on the window's parent target if GetWindow
+ // returned null, so we might still get events here in this case. We should
+ // just ignore these events.
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsINode> origTargetNode =
+ do_QueryInterface(aDOMEvent->GetOriginalTarget());
+ if (!origTargetNode) return NS_OK;
+
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eDOMEvents)) {
+ nsAutoString eventType;
+ aDOMEvent->GetType(eventType);
+ logging::DOMEvent("handled", origTargetNode, eventType);
+ }
+#endif
+
+ DocAccessible* document =
+ GetAccService()->GetDocAccessible(origTargetNode->OwnerDoc());
+
+ if (document) {
+ nsAutoString eventType;
+ aDOMEvent->GetType(eventType);
+ if (eventType.EqualsLiteral("scroll")) {
+ // We don't put this in the notification queue for 2 reasons:
+ // 1. We will flood the queue with repetitive events.
+ // 2. Since this doesn't necessarily touch layout, we are not
+ // guaranteed to have a WillRefresh tick any time soon.
+ document->HandleScroll(origTargetNode);
+ } else {
+ // Root accessible exists longer than any of its descendant documents so
+ // that we are guaranteed notification is processed before root accessible
+ // is destroyed.
+ // For shadow DOM, GetOriginalTarget on the Event returns null if we
+ // process the event async, so we must pass the target node as well.
+ document->HandleNotification<RootAccessible, Event, nsINode>(
+ this, &RootAccessible::ProcessDOMEvent, aDOMEvent, origTargetNode);
+ }
+ }
+
+ return NS_OK;
+}
+
+// RootAccessible protected
+void RootAccessible::ProcessDOMEvent(Event* aDOMEvent, nsINode* aTarget) {
+ MOZ_ASSERT(aDOMEvent);
+ MOZ_ASSERT(aTarget);
+
+ nsAutoString eventType;
+ aDOMEvent->GetType(eventType);
+
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eDOMEvents)) {
+ logging::DOMEvent("processed", aTarget, eventType);
+ }
+#endif
+
+ if (eventType.EqualsLiteral("popuphiding")) {
+ HandlePopupHidingEvent(aTarget);
+ return;
+ }
+
+ DocAccessible* targetDocument =
+ GetAccService()->GetDocAccessible(aTarget->OwnerDoc());
+ if (!targetDocument) {
+ // Document has ceased to exist.
+ return;
+ }
+
+ if (eventType.EqualsLiteral("popupshown") &&
+ aTarget->IsAnyOfXULElements(nsGkAtoms::tooltip, nsGkAtoms::panel)) {
+ targetDocument->ContentInserted(aTarget->AsContent(),
+ aTarget->GetNextSibling());
+ return;
+ }
+
+ LocalAccessible* accessible =
+ targetDocument->GetAccessibleOrContainer(aTarget);
+ if (!accessible) return;
+
+ if (accessible->IsDoc() && eventType.EqualsLiteral("DOMTitleChanged")) {
+ targetDocument->FireDelayedEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE,
+ accessible);
+ return;
+ }
+
+ XULTreeAccessible* treeAcc = accessible->AsXULTree();
+ if (treeAcc) {
+ if (eventType.EqualsLiteral("TreeRowCountChanged")) {
+ HandleTreeRowCountChangedEvent(aDOMEvent, treeAcc);
+ return;
+ }
+
+ if (eventType.EqualsLiteral("TreeInvalidated")) {
+ HandleTreeInvalidatedEvent(aDOMEvent, treeAcc);
+ return;
+ }
+ }
+
+ if (eventType.EqualsLiteral("RadioStateChange")) {
+ uint64_t state = accessible->State();
+ bool isEnabled = (state & (states::CHECKED | states::SELECTED)) != 0;
+
+ if (accessible->NeedsDOMUIEvent()) {
+ RefPtr<AccEvent> accEvent =
+ new AccStateChangeEvent(accessible, states::CHECKED, isEnabled);
+ nsEventShell::FireEvent(accEvent);
+ }
+
+ if (isEnabled) {
+ FocusMgr()->ActiveItemChanged(accessible);
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eFocus)) {
+ logging::ActiveItemChangeCausedBy("RadioStateChange", accessible);
+ }
+#endif
+ }
+
+ return;
+ }
+
+ if (eventType.EqualsLiteral("CheckboxStateChange")) {
+ if (accessible->NeedsDOMUIEvent()) {
+ uint64_t state = accessible->State();
+ bool isEnabled = !!(state & states::CHECKED);
+
+ RefPtr<AccEvent> accEvent =
+ new AccStateChangeEvent(accessible, states::CHECKED, isEnabled);
+ nsEventShell::FireEvent(accEvent);
+ }
+ return;
+ }
+
+ LocalAccessible* treeItemAcc = nullptr;
+ // If it's a tree element, need the currently selected item.
+ if (treeAcc) {
+ treeItemAcc = accessible->CurrentItem();
+ if (treeItemAcc) accessible = treeItemAcc;
+ }
+
+ if (treeItemAcc && eventType.EqualsLiteral("OpenStateChange")) {
+ uint64_t state = accessible->State();
+ bool isEnabled = (state & states::EXPANDED) != 0;
+
+ RefPtr<AccEvent> accEvent =
+ new AccStateChangeEvent(accessible, states::EXPANDED, isEnabled);
+ nsEventShell::FireEvent(accEvent);
+ return;
+ }
+
+ nsINode* targetNode = accessible->GetNode();
+ if (treeItemAcc && eventType.EqualsLiteral("select")) {
+ // XXX: We shouldn't be based on DOM select event which doesn't provide us
+ // any context info. We should integrate into nsTreeSelection instead.
+ // If multiselect tree, we should fire selectionadd or selection removed
+ if (FocusMgr()->HasDOMFocus(targetNode)) {
+ nsCOMPtr<nsIDOMXULMultiSelectControlElement> multiSel =
+ targetNode->AsElement()->AsXULMultiSelectControl();
+ if (!multiSel) {
+ // This shouldn't be possible. All XUL trees should have
+ // nsIDOMXULMultiSelectControlElement, and the tree is focused, so it
+ // shouldn't be dying. Nevertheless, this sometimes happens in the wild
+ // (bug 1597043).
+ MOZ_ASSERT_UNREACHABLE(
+ "XUL tree doesn't have nsIDOMXULMultiSelectControlElement");
+ return;
+ }
+ nsAutoString selType;
+ multiSel->GetSelType(selType);
+ if (selType.IsEmpty() || !selType.EqualsLiteral("single")) {
+ // XXX: We need to fire EVENT_SELECTION_ADD and EVENT_SELECTION_REMOVE
+ // for each tree item. Perhaps each tree item will need to cache its
+ // selection state and fire an event after a DOM "select" event when
+ // that state changes. XULTreeAccessible::UpdateTreeSelection();
+ nsEventShell::FireEvent(nsIAccessibleEvent::EVENT_SELECTION_WITHIN,
+ accessible);
+ return;
+ }
+
+ RefPtr<AccSelChangeEvent> selChangeEvent = new AccSelChangeEvent(
+ treeAcc, treeItemAcc, AccSelChangeEvent::eSelectionAdd);
+ nsEventShell::FireEvent(selChangeEvent);
+ return;
+ }
+ } else if (eventType.EqualsLiteral("AlertActive")) {
+ nsEventShell::FireEvent(nsIAccessibleEvent::EVENT_ALERT, accessible);
+ } else if (eventType.EqualsLiteral("popupshown")) {
+ HandlePopupShownEvent(accessible);
+ } else if (eventType.EqualsLiteral("DOMMenuInactive")) {
+ if (accessible->Role() == roles::MENUPOPUP) {
+ nsEventShell::FireEvent(nsIAccessibleEvent::EVENT_MENUPOPUP_END,
+ accessible);
+ }
+ if (auto* focus = FocusMgr()->FocusedLocalAccessible()) {
+ // Intentionally use the content tree, because Linux strips menupopups
+ // from the a11y tree so accessible might be an arbitrary ancestor.
+ if (focus->GetContent() &&
+ focus->GetContent()->IsShadowIncludingInclusiveDescendantOf(
+ aTarget)) {
+ // Move the focus to the topmost menu active content if any. The
+ // menu item in the parent menu will not fire a DOMMenuItemActive
+ // event if it's already active.
+ LocalAccessible* newActiveAccessible = nullptr;
+ if (auto* pm = nsXULPopupManager::GetInstance()) {
+ if (auto* content = pm->GetTopActiveMenuItemContent()) {
+ newActiveAccessible =
+ accessible->Document()->GetAccessible(content);
+ }
+ }
+ FocusMgr()->ActiveItemChanged(newActiveAccessible);
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eFocus)) {
+ logging::ActiveItemChangeCausedBy("DOMMenuInactive",
+ newActiveAccessible);
+ }
+#endif
+ }
+ }
+ } else if (eventType.EqualsLiteral("DOMMenuItemActive")) {
+ RefPtr<AccEvent> event =
+ new AccStateChangeEvent(accessible, states::ACTIVE, true);
+ nsEventShell::FireEvent(event);
+ FocusMgr()->ActiveItemChanged(accessible);
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eFocus)) {
+ logging::ActiveItemChangeCausedBy("DOMMenuItemActive", accessible);
+ }
+#endif
+ } else if (eventType.EqualsLiteral("DOMMenuItemInactive")) {
+ RefPtr<AccEvent> event =
+ new AccStateChangeEvent(accessible, states::ACTIVE, false);
+ nsEventShell::FireEvent(event);
+
+ // Process DOMMenuItemInactive event for autocomplete only because this is
+ // unique widget that may acquire focus from autocomplete popup while popup
+ // stays open and has no active item. In case of XUL tree autocomplete
+ // popup this event is fired for tree accessible.
+ LocalAccessible* widget =
+ accessible->IsWidget() ? accessible : accessible->ContainerWidget();
+ if (widget && widget->IsAutoCompletePopup()) {
+ FocusMgr()->ActiveItemChanged(nullptr);
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eFocus)) {
+ logging::ActiveItemChangeCausedBy("DOMMenuItemInactive", accessible);
+ }
+#endif
+ }
+ } else if (eventType.EqualsLiteral(
+ "DOMMenuBarActive")) { // Always from user input
+ nsEventShell::FireEvent(nsIAccessibleEvent::EVENT_MENU_START, accessible,
+ eFromUserInput);
+
+ // Notify of active item change when menubar gets active and if it has
+ // current item. This is a case of mouseover (set current menuitem) and
+ // mouse click (activate the menubar). If menubar doesn't have current item
+ // (can be a case of menubar activation from keyboard) then ignore this
+ // notification because later we'll receive DOMMenuItemActive event after
+ // current menuitem is set.
+ LocalAccessible* activeItem = accessible->CurrentItem();
+ if (activeItem) {
+ FocusMgr()->ActiveItemChanged(activeItem);
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eFocus)) {
+ logging::ActiveItemChangeCausedBy("DOMMenuBarActive", accessible);
+ }
+#endif
+ }
+ } else if (eventType.EqualsLiteral(
+ "DOMMenuBarInactive")) { // Always from user input
+ nsEventShell::FireEvent(nsIAccessibleEvent::EVENT_MENU_END, accessible,
+ eFromUserInput);
+
+ FocusMgr()->ActiveItemChanged(nullptr);
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eFocus)) {
+ logging::ActiveItemChangeCausedBy("DOMMenuBarInactive", accessible);
+ }
+#endif
+ } else if (accessible->NeedsDOMUIEvent() &&
+ eventType.EqualsLiteral("ValueChange")) {
+ uint32_t event = accessible->HasNumericValue()
+ ? nsIAccessibleEvent::EVENT_VALUE_CHANGE
+ : nsIAccessibleEvent::EVENT_TEXT_VALUE_CHANGE;
+ targetDocument->FireDelayedEvent(event, accessible);
+ }
+#ifdef DEBUG_DRAGDROPSTART
+ else if (eventType.EqualsLiteral("mouseover")) {
+ nsEventShell::FireEvent(nsIAccessibleEvent::EVENT_DRAGDROP_START,
+ accessible);
+ }
+#endif
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// LocalAccessible
+
+void RootAccessible::Shutdown() {
+ // Called manually or by LocalAccessible::LastRelease()
+ if (HasShutdown()) {
+ return;
+ }
+ DocAccessibleWrap::Shutdown();
+}
+
+Relation RootAccessible::RelationByType(RelationType aType) const {
+ if (!mDocumentNode || aType != RelationType::EMBEDS) {
+ return DocAccessibleWrap::RelationByType(aType);
+ }
+
+ if (RemoteAccessible* remoteDoc = GetPrimaryRemoteTopLevelContentDoc()) {
+ return Relation(remoteDoc);
+ }
+
+ if (nsIDocShell* docShell = mDocumentNode->GetDocShell()) {
+ nsCOMPtr<nsIDocShellTreeOwner> owner;
+ docShell->GetTreeOwner(getter_AddRefs(owner));
+ if (owner) {
+ nsCOMPtr<nsIDocShellTreeItem> contentShell;
+ owner->GetPrimaryContentShell(getter_AddRefs(contentShell));
+ if (contentShell) {
+ return Relation(nsAccUtils::GetDocAccessibleFor(contentShell));
+ }
+ }
+ }
+
+ return Relation();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Protected members
+
+void RootAccessible::HandlePopupShownEvent(LocalAccessible* aAccessible) {
+ roles::Role role = aAccessible->Role();
+
+ if (role == roles::MENUPOPUP) {
+ // Don't fire menupopup events for combobox and autocomplete lists.
+ nsEventShell::FireEvent(nsIAccessibleEvent::EVENT_MENUPOPUP_START,
+ aAccessible);
+ return;
+ }
+
+ if (role == roles::COMBOBOX_LIST) {
+ // Fire expanded state change event for comboboxes and autocompeletes.
+ LocalAccessible* combobox = aAccessible->LocalParent();
+ if (!combobox) return;
+
+ if (combobox->IsCombobox()) {
+ RefPtr<AccEvent> event =
+ new AccStateChangeEvent(combobox, states::EXPANDED, true);
+ nsEventShell::FireEvent(event);
+ }
+
+ // If aria-activedescendant is present, redirect focus.
+ // This is needed for parent process <select> dropdowns, which use a
+ // menulist containing div elements instead of XUL menuitems. XUL menuitems
+ // fire DOMMenuItemActive events from layout instead.
+ MOZ_ASSERT(aAccessible->Elm());
+ if (aAccessible->Elm()->HasAttr(nsGkAtoms::aria_activedescendant)) {
+ LocalAccessible* activeDescendant = aAccessible->CurrentItem();
+ if (activeDescendant) {
+ FocusMgr()->ActiveItemChanged(activeDescendant, false);
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eFocus)) {
+ logging::ActiveItemChangeCausedBy("ARIA activedescendant on popup",
+ activeDescendant);
+ }
+#endif
+ }
+ }
+ }
+}
+
+void RootAccessible::HandlePopupHidingEvent(nsINode* aPopupNode) {
+ DocAccessible* document = nsAccUtils::GetDocAccessibleFor(aPopupNode);
+ if (!document) {
+ return;
+ }
+
+ if (aPopupNode->IsAnyOfXULElements(nsGkAtoms::tooltip, nsGkAtoms::panel)) {
+ document->ContentRemoved(aPopupNode->AsContent());
+ return;
+ }
+
+ // Get popup accessible. There are cases when popup element isn't accessible
+ // but an underlying widget is and behaves like popup, an example is
+ // autocomplete popups.
+ LocalAccessible* popup = document->GetAccessible(aPopupNode);
+ if (!popup) {
+ LocalAccessible* popupContainer =
+ document->GetContainerAccessible(aPopupNode);
+ if (!popupContainer) {
+ return;
+ }
+
+ uint32_t childCount = popupContainer->ChildCount();
+ for (uint32_t idx = 0; idx < childCount; idx++) {
+ LocalAccessible* child = popupContainer->LocalChildAt(idx);
+ if (child->IsAutoCompletePopup()) {
+ popup = child;
+ break;
+ }
+ }
+
+ // No popup no events. Focus is managed by DOM. This is a case for
+ // menupopups of menus on Linux since there are no accessible for popups.
+ if (!popup) {
+ return;
+ }
+ }
+
+ // In case of autocompletes and comboboxes fire state change event for
+ // expanded state. Note, HTML form autocomplete isn't a subject of state
+ // change event because they aren't autocompletes strictly speaking.
+
+ // HTML select is target of popuphidding event. Otherwise get container
+ // widget. No container widget means this is either tooltip or menupopup.
+ // No events in the former case.
+ LocalAccessible* widget = nullptr;
+ if (popup->IsCombobox()) {
+ widget = popup;
+ } else {
+ widget = popup->ContainerWidget();
+ if (!widget) {
+ if (!popup->IsMenuPopup()) {
+ return;
+ }
+ widget = popup;
+ }
+ }
+
+ // Fire expanded state change event.
+ if (widget->IsCombobox()) {
+ RefPtr<AccEvent> event =
+ new AccStateChangeEvent(widget, states::EXPANDED, false);
+ document->FireDelayedEvent(event);
+ }
+}
+
+static void GetPropertyBagFromEvent(Event* aEvent,
+ nsIPropertyBag2** aPropertyBag) {
+ *aPropertyBag = nullptr;
+
+ CustomEvent* customEvent = aEvent->AsCustomEvent();
+ if (!customEvent) return;
+
+ AutoJSAPI jsapi;
+ if (!jsapi.Init(customEvent->GetParentObject())) return;
+
+ JSContext* cx = jsapi.cx();
+ JS::Rooted<JS::Value> detail(cx);
+ customEvent->GetDetail(cx, &detail);
+ if (!detail.isObject()) return;
+
+ JS::Rooted<JSObject*> detailObj(cx, &detail.toObject());
+
+ nsresult rv;
+ nsCOMPtr<nsIPropertyBag2> propBag;
+ rv = UnwrapArg<nsIPropertyBag2>(cx, detailObj, getter_AddRefs(propBag));
+ if (NS_FAILED(rv)) return;
+
+ propBag.forget(aPropertyBag);
+}
+
+void RootAccessible::HandleTreeRowCountChangedEvent(
+ Event* aEvent, XULTreeAccessible* aAccessible) {
+ nsCOMPtr<nsIPropertyBag2> propBag;
+ GetPropertyBagFromEvent(aEvent, getter_AddRefs(propBag));
+ if (!propBag) return;
+
+ nsresult rv;
+ int32_t index, count;
+ rv = propBag->GetPropertyAsInt32(u"index"_ns, &index);
+ if (NS_FAILED(rv)) return;
+
+ rv = propBag->GetPropertyAsInt32(u"count"_ns, &count);
+ if (NS_FAILED(rv)) return;
+
+ aAccessible->InvalidateCache(index, count);
+}
+
+void RootAccessible::HandleTreeInvalidatedEvent(
+ Event* aEvent, XULTreeAccessible* aAccessible) {
+ nsCOMPtr<nsIPropertyBag2> propBag;
+ GetPropertyBagFromEvent(aEvent, getter_AddRefs(propBag));
+ if (!propBag) return;
+
+ int32_t startRow = 0, endRow = -1, startCol = 0, endCol = -1;
+ propBag->GetPropertyAsInt32(u"startrow"_ns, &startRow);
+ propBag->GetPropertyAsInt32(u"endrow"_ns, &endRow);
+ propBag->GetPropertyAsInt32(u"startcolumn"_ns, &startCol);
+ propBag->GetPropertyAsInt32(u"endcolumn"_ns, &endCol);
+
+ aAccessible->TreeViewInvalidated(startRow, endRow, startCol, endCol);
+}
+
+RemoteAccessible* RootAccessible::GetPrimaryRemoteTopLevelContentDoc() const {
+ nsCOMPtr<nsIDocShellTreeOwner> owner;
+ mDocumentNode->GetDocShell()->GetTreeOwner(getter_AddRefs(owner));
+ NS_ENSURE_TRUE(owner, nullptr);
+
+ nsCOMPtr<nsIRemoteTab> remoteTab;
+ owner->GetPrimaryRemoteTab(getter_AddRefs(remoteTab));
+ if (!remoteTab) {
+ return nullptr;
+ }
+
+ auto tab = static_cast<dom::BrowserHost*>(remoteTab.get());
+ return tab->GetTopLevelDocAccessible();
+}
diff --git a/accessible/generic/RootAccessible.h b/accessible/generic/RootAccessible.h
new file mode 100644
index 0000000000..b1e7e42fdb
--- /dev/null
+++ b/accessible/generic/RootAccessible.h
@@ -0,0 +1,93 @@
+/* -*- 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/. */
+
+#ifndef mozilla_a11y_RootAccessible_h__
+#define mozilla_a11y_RootAccessible_h__
+
+#include "HyperTextAccessible.h"
+#include "DocAccessibleWrap.h"
+
+#include "nsIDOMEventListener.h"
+
+namespace mozilla {
+
+class PresShell;
+
+namespace a11y {
+
+/**
+ * The node at a root of the accessibility tree. This node originated in the
+ * current process. If this is the parent process, RootAccessible is the
+ * Accessible for the top-level window. If this is a content process,
+ * RootAccessible is a top-level content document in this process, which is
+ * either a tab document or an out-of-process iframe.
+ */
+class RootAccessible : public DocAccessibleWrap, public nsIDOMEventListener {
+ NS_DECL_ISUPPORTS_INHERITED
+
+ public:
+ RootAccessible(dom::Document* aDocument, PresShell* aPresShell);
+
+ // nsIDOMEventListener
+ NS_DECL_NSIDOMEVENTLISTENER
+
+ // LocalAccessible
+ virtual void Shutdown() override;
+ virtual mozilla::a11y::ENameValueFlag Name(nsString& aName) const override;
+ virtual Relation RelationByType(RelationType aType) const override;
+ virtual uint64_t NativeState() const override;
+
+ // RootAccessible
+
+ /**
+ * Notify that the sub document presshell was activated.
+ */
+ virtual void DocumentActivated(DocAccessible* aDocument);
+
+ /**
+ * Return the primary remote top level document if any.
+ */
+ RemoteAccessible* GetPrimaryRemoteTopLevelContentDoc() const;
+
+ protected:
+ virtual ~RootAccessible();
+
+ /**
+ * Add/remove DOM event listeners.
+ */
+ virtual nsresult AddEventListeners() override;
+ virtual nsresult RemoveEventListeners() override;
+
+ /**
+ * Process the DOM event.
+ */
+ void ProcessDOMEvent(dom::Event* aDOMEvent, nsINode* aTarget);
+
+ /**
+ * Process "popupshown" event. Used by HandleEvent().
+ */
+ void HandlePopupShownEvent(LocalAccessible* aAccessible);
+
+ /*
+ * Process "popuphiding" event. Used by HandleEvent().
+ */
+ void HandlePopupHidingEvent(nsINode* aNode);
+
+ void HandleTreeRowCountChangedEvent(dom::Event* aEvent,
+ XULTreeAccessible* aAccessible);
+ void HandleTreeInvalidatedEvent(dom::Event* aEvent,
+ XULTreeAccessible* aAccessible);
+
+ uint32_t GetChromeFlags() const;
+};
+
+inline RootAccessible* LocalAccessible::AsRoot() {
+ return IsRoot() ? static_cast<mozilla::a11y::RootAccessible*>(this) : nullptr;
+}
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/generic/TableAccessible.cpp b/accessible/generic/TableAccessible.cpp
new file mode 100644
index 0000000000..9bcbfdcc2f
--- /dev/null
+++ b/accessible/generic/TableAccessible.cpp
@@ -0,0 +1,317 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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 "TableAccessible.h"
+
+#include "LocalAccessible-inl.h"
+#include "AccIterator.h"
+
+#include "nsTableCellFrame.h"
+#include "nsTableWrapperFrame.h"
+#include "TableCellAccessible.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+bool TableAccessible::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
+
+ LocalAccessible* thisacc = AsAccessible();
+
+ MOZ_ASSERT(!thisacc->IsDefunct(), "Table accessible should not be defunct");
+
+ // Need to see all elements while document is being edited.
+ if (thisacc->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 (thisacc->HasARIARole()) {
+ RETURN_LAYOUT_ANSWER(false, "Has role attribute");
+ }
+
+ dom::Element* el = thisacc->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 = thisacc->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 =
+ thisacc->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 =
+ thisacc->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->GetXULBorder(border);
+ 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 = thisacc->ChildCount();
+ nscolor rowColor = 0;
+ nscolor prevRowColor;
+ for (auto childIdx = 0U; childIdx < childCount; childIdx++) {
+ LocalAccessible* child = thisacc->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 = thisacc->Document()->GetFrame();
+ nsSize documentSize = documentFrame->GetSize();
+ if (documentSize.width > 0) {
+ nsSize tableSize = thisacc->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 (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");
+}
+
+LocalAccessible* TableAccessible::RowAt(int32_t aRow) {
+ int32_t rowIdx = aRow;
+
+ AccIterator rowIter(this->AsAccessible(), filters::GetRow);
+
+ LocalAccessible* row = rowIter.Next();
+ while (rowIdx != 0 && (row = rowIter.Next())) {
+ rowIdx--;
+ }
+
+ return row;
+}
+
+LocalAccessible* TableAccessible::CellInRowAt(LocalAccessible* aRow,
+ int32_t aColumn) {
+ int32_t colIdx = aColumn;
+
+ AccIterator cellIter(aRow, filters::GetCell);
+ LocalAccessible* cell = nullptr;
+
+ while (colIdx >= 0 && (cell = cellIter.Next())) {
+ MOZ_ASSERT(cell->IsTableCell(), "No table or grid cell!");
+ colIdx -= cell->AsTableCell()->ColExtent();
+ }
+
+ return cell;
+}
+
+int32_t TableAccessible::ColIndexAt(uint32_t aCellIdx) {
+ uint32_t colCount = ColCount();
+ if (colCount < 1 || aCellIdx >= colCount * RowCount()) {
+ return -1; // Error: column count is 0 or index out of bounds.
+ }
+
+ return aCellIdx % colCount;
+}
+
+int32_t TableAccessible::RowIndexAt(uint32_t aCellIdx) {
+ uint32_t colCount = ColCount();
+ if (colCount < 1 || aCellIdx >= colCount * RowCount()) {
+ return -1; // Error: column count is 0 or index out of bounds.
+ }
+
+ return aCellIdx / colCount;
+}
+
+void TableAccessible::RowAndColIndicesAt(uint32_t aCellIdx, int32_t* aRowIdx,
+ int32_t* aColIdx) {
+ uint32_t colCount = ColCount();
+ if (colCount < 1 || aCellIdx >= colCount * RowCount()) {
+ *aRowIdx = -1;
+ *aColIdx = -1;
+ return; // Error: column count is 0 or index out of bounds.
+ }
+
+ *aRowIdx = aCellIdx / colCount;
+ *aColIdx = aCellIdx % colCount;
+}
diff --git a/accessible/generic/TableAccessible.h b/accessible/generic/TableAccessible.h
new file mode 100644
index 0000000000..e2cd51de70
--- /dev/null
+++ b/accessible/generic/TableAccessible.h
@@ -0,0 +1,72 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef TABLE_ACCESSIBLE_H
+#define TABLE_ACCESSIBLE_H
+
+#include "LocalAccessible.h"
+#include "mozilla/a11y/TableAccessibleBase.h"
+#include "mozilla/a11y/TableCellAccessibleBase.h"
+#include "nsPointerHashKeys.h"
+#include "nsRefPtrHashtable.h"
+
+namespace mozilla {
+namespace a11y {
+
+/**
+ * Base class for LocalAccessible table implementations.
+ */
+class TableAccessible : public TableAccessibleBase {
+ public:
+ virtual LocalAccessible* Caption() const override { return nullptr; }
+
+ virtual LocalAccessible* CellAt(uint32_t aRowIdx, uint32_t aColIdx) override {
+ return nullptr;
+ }
+
+ virtual int32_t CellIndexAt(uint32_t aRowIdx, uint32_t aColIdx) override {
+ return ColCount() * aRowIdx + aColIdx;
+ }
+
+ virtual int32_t ColIndexAt(uint32_t aCellIdx) override;
+ virtual int32_t RowIndexAt(uint32_t aCellIdx) override;
+ virtual void RowAndColIndicesAt(uint32_t aCellIdx, int32_t* aRowIdx,
+ int32_t* aColIdx) override;
+ virtual bool IsProbablyLayoutTable() override;
+ virtual LocalAccessible* AsAccessible() override = 0;
+
+ using HeaderCache =
+ nsRefPtrHashtable<nsPtrHashKey<const TableCellAccessibleBase>,
+ LocalAccessible>;
+
+ /**
+ * Get the header cache, which maps a TableCellAccessible to its previous
+ * header.
+ * Although this data is only used in TableCellAccessible, it is stored on
+ * TableAccessible so the cache can be easily invalidated when the table
+ * is mutated.
+ */
+ HeaderCache& GetHeaderCache() { return mHeaderCache; }
+
+ protected:
+ /**
+ * Return row accessible at the given row index.
+ */
+ LocalAccessible* RowAt(int32_t aRow);
+
+ /**
+ * Return cell accessible at the given column index in the row.
+ */
+ LocalAccessible* CellInRowAt(LocalAccessible* aRow, int32_t aColumn);
+
+ private:
+ HeaderCache mHeaderCache;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/generic/TableCellAccessible.cpp b/accessible/generic/TableCellAccessible.cpp
new file mode 100644
index 0000000000..c962e2103c
--- /dev/null
+++ b/accessible/generic/TableCellAccessible.cpp
@@ -0,0 +1,165 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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 "TableCellAccessible.h"
+
+#include "LocalAccessible-inl.h"
+#include "TableAccessible.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+void TableCellAccessible::RowHeaderCells(nsTArray<Accessible*>* aCells) {
+ uint32_t rowIdx = RowIdx(), colIdx = ColIdx();
+ TableAccessible* table = Table();
+ if (!table) return;
+
+ // Move to the left to find row header cells
+ for (uint32_t curColIdx = colIdx - 1; curColIdx < colIdx; curColIdx--) {
+ LocalAccessible* cell = table->CellAt(rowIdx, curColIdx);
+ if (!cell) continue;
+
+ // CellAt should always return a TableCellAccessible (XXX Bug 587529)
+ TableCellAccessible* tableCell = cell->AsTableCell();
+ NS_ASSERTION(tableCell, "cell should be a table cell!");
+ if (!tableCell) continue;
+
+ // Avoid addding cells multiple times, if this cell spans more columns
+ // we'll get it later.
+ if (tableCell->ColIdx() == curColIdx && cell->Role() == roles::ROWHEADER) {
+ aCells->AppendElement(cell);
+ }
+ }
+}
+
+LocalAccessible* TableCellAccessible::PrevColHeader() {
+ TableAccessible* table = Table();
+ if (!table) {
+ return nullptr;
+ }
+
+ TableAccessible::HeaderCache& cache = table->GetHeaderCache();
+ bool inCache = false;
+ LocalAccessible* cachedHeader = cache.GetWeak(this, &inCache);
+ if (inCache) {
+ // Cached but null means we know there is no previous column header.
+ // if defunct, the cell was removed, so behave as if there is no cached
+ // value.
+ if (!cachedHeader || !cachedHeader->IsDefunct()) {
+ return cachedHeader;
+ }
+ }
+
+ uint32_t rowIdx = RowIdx(), colIdx = ColIdx();
+ for (uint32_t curRowIdx = rowIdx - 1; curRowIdx < rowIdx; curRowIdx--) {
+ LocalAccessible* cell = table->CellAt(curRowIdx, colIdx);
+ if (!cell) {
+ continue;
+ }
+ // CellAt should always return a TableCellAccessible (XXX Bug 587529)
+ TableCellAccessible* tableCell = cell->AsTableCell();
+ MOZ_ASSERT(tableCell, "cell should be a table cell!");
+ if (!tableCell) {
+ continue;
+ }
+
+ // Check whether the previous table cell has a cached value.
+ cachedHeader = cache.GetWeak(tableCell, &inCache);
+ if (
+ // We check the cache first because even though we might not use it,
+ // it's faster than the other conditions.
+ inCache &&
+ // Only use the cached value if:
+ // 1. cell is a table cell which is not a column header. In that case,
+ // cell is the previous header and cachedHeader is the one before that.
+ // We will return cell later.
+ cell->Role() != roles::COLUMNHEADER &&
+ // 2. cell starts in this column. If it starts in a previous column and
+ // extends into this one, its header will be for the starting column,
+ // which is wrong for this cell.
+ // ColExtent is faster than ColIdx, so check that first.
+ (tableCell->ColExtent() == 1 || tableCell->ColIdx() == colIdx)) {
+ if (!cachedHeader || !cachedHeader->IsDefunct()) {
+ // Cache it for this cell.
+ cache.InsertOrUpdate(this, RefPtr<LocalAccessible>(cachedHeader));
+ return cachedHeader;
+ }
+ }
+
+ // Avoid addding cells multiple times, if this cell spans more rows
+ // we'll get it later.
+ if (cell->Role() != roles::COLUMNHEADER ||
+ tableCell->RowIdx() != curRowIdx) {
+ continue;
+ }
+
+ // Cache the header we found.
+ cache.InsertOrUpdate(this, RefPtr<LocalAccessible>(cell));
+ return cell;
+ }
+
+ // There's no header, so cache that fact.
+ cache.InsertOrUpdate(this, RefPtr<LocalAccessible>(nullptr));
+ return nullptr;
+}
+
+void TableCellAccessible::ColHeaderCells(nsTArray<Accessible*>* aCells) {
+ for (LocalAccessible* cell = PrevColHeader(); cell;
+ cell = cell->AsTableCell()->PrevColHeader()) {
+ aCells->AppendElement(cell);
+ }
+}
+
+a11y::role TableCellAccessible::GetHeaderCellRole(
+ const LocalAccessible* aAcc) const {
+ if (!aAcc || !aAcc->GetContent() || !aAcc->GetContent()->IsElement()) {
+ return roles::NOTHING;
+ }
+
+ MOZ_ASSERT(aAcc->IsTableCell());
+
+ // Check value of @scope attribute.
+ static mozilla::dom::Element::AttrValuesArray scopeValues[] = {
+ nsGkAtoms::col, nsGkAtoms::colgroup, nsGkAtoms::row, nsGkAtoms::rowgroup,
+ nullptr};
+ int32_t valueIdx = aAcc->GetContent()->AsElement()->FindAttrValueIn(
+ kNameSpaceID_None, nsGkAtoms::scope, scopeValues, eCaseMatters);
+
+ switch (valueIdx) {
+ case 0:
+ case 1:
+ return roles::COLUMNHEADER;
+ case 2:
+ case 3:
+ return roles::ROWHEADER;
+ }
+
+ TableAccessible* table = Table();
+ if (!table) {
+ return roles::NOTHING;
+ }
+
+ // If the cell next to this one is not a header cell then assume this cell is
+ // a row header for it.
+ uint32_t rowIdx = RowIdx(), colIdx = ColIdx();
+ LocalAccessible* cell = table->CellAt(rowIdx, colIdx + ColExtent());
+ if (cell && !nsCoreUtils::IsHTMLTableHeader(cell->GetContent())) {
+ return roles::ROWHEADER;
+ }
+
+ // If the cell below this one is not a header cell then assume this cell is
+ // a column header for it.
+ uint32_t rowExtent = RowExtent();
+ cell = table->CellAt(rowIdx + rowExtent, colIdx);
+ if (cell && !nsCoreUtils::IsHTMLTableHeader(cell->GetContent())) {
+ return roles::COLUMNHEADER;
+ }
+
+ // Otherwise if this cell is surrounded by header cells only then make a guess
+ // based on its cell spanning. In other words if it is row spanned then assume
+ // it's a row header, otherwise it's a column header.
+ return rowExtent > 1 ? roles::ROWHEADER : roles::COLUMNHEADER;
+}
diff --git a/accessible/generic/TableCellAccessible.h b/accessible/generic/TableCellAccessible.h
new file mode 100644
index 0000000000..3da12dfadb
--- /dev/null
+++ b/accessible/generic/TableCellAccessible.h
@@ -0,0 +1,40 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_a11y_TableCellAccessible_h__
+#define mozilla_a11y_TableCellAccessible_h__
+
+#include "mozilla/a11y/TableCellAccessibleBase.h"
+#include "TableAccessible.h"
+
+namespace mozilla {
+namespace a11y {
+
+class LocalAccessible;
+
+/**
+ * Base class for LocalAccessible table cell implementations.
+ */
+class TableCellAccessible : public TableCellAccessibleBase {
+ public:
+ virtual TableAccessible* Table() const override = 0;
+ virtual void ColHeaderCells(nsTArray<Accessible*>* aCells) override;
+ virtual void RowHeaderCells(nsTArray<Accessible*>* aCells) override;
+
+ protected:
+ // Get the proper role for the given header cell accessible. The given acc
+ // must be either an ARIA grid cell accessible for a th element or a true
+ // table header cell accessible for the result to be valid.
+ a11y::role GetHeaderCellRole(const LocalAccessible* aAcc) const;
+
+ private:
+ LocalAccessible* PrevColHeader();
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif // mozilla_a11y_TableCellAccessible_h__
diff --git a/accessible/generic/TextLeafAccessible.cpp b/accessible/generic/TextLeafAccessible.cpp
new file mode 100644
index 0000000000..1f7c2ff13a
--- /dev/null
+++ b/accessible/generic/TextLeafAccessible.cpp
@@ -0,0 +1,44 @@
+/* -*- 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 "TextLeafAccessible.h"
+
+#include "nsAccUtils.h"
+#include "DocAccessible.h"
+#include "Role.h"
+
+using namespace mozilla::a11y;
+
+////////////////////////////////////////////////////////////////////////////////
+// TextLeafAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+TextLeafAccessible::TextLeafAccessible(nsIContent* aContent,
+ DocAccessible* aDoc)
+ : LinkableAccessible(aContent, aDoc) {
+ mType = eTextLeafType;
+ mGenericTypes |= eText;
+ mStateFlags |= eNoKidsFromDOM;
+}
+
+TextLeafAccessible::~TextLeafAccessible() {}
+
+role TextLeafAccessible::NativeRole() const {
+ nsIFrame* frame = GetFrame();
+ if (frame && frame->IsGeneratedContentFrame()) return roles::STATICTEXT;
+
+ return roles::TEXT_LEAF;
+}
+
+void TextLeafAccessible::AppendTextTo(nsAString& aText, uint32_t aStartOffset,
+ uint32_t aLength) {
+ aText.Append(Substring(mText, aStartOffset, aLength));
+}
+
+ENameValueFlag TextLeafAccessible::Name(nsString& aName) const {
+ // Text node, ARIA can't be used.
+ aName = mText;
+ return eNameOK;
+}
diff --git a/accessible/generic/TextLeafAccessible.h b/accessible/generic/TextLeafAccessible.h
new file mode 100644
index 0000000000..65d59a2aad
--- /dev/null
+++ b/accessible/generic/TextLeafAccessible.h
@@ -0,0 +1,46 @@
+/* -*- 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/. */
+
+#ifndef mozilla_a11y_TextLeafAccessible_h__
+#define mozilla_a11y_TextLeafAccessible_h__
+
+#include "BaseAccessibles.h"
+
+namespace mozilla {
+namespace a11y {
+
+/**
+ * Generic class used for text nodes.
+ */
+class TextLeafAccessible : public LinkableAccessible {
+ public:
+ TextLeafAccessible(nsIContent* aContent, DocAccessible* aDoc);
+ virtual ~TextLeafAccessible();
+
+ // LocalAccessible
+ virtual mozilla::a11y::role NativeRole() const override;
+ virtual void AppendTextTo(nsAString& aText, uint32_t aStartOffset = 0,
+ uint32_t aLength = UINT32_MAX) override;
+ virtual ENameValueFlag Name(nsString& aName) const override;
+
+ // TextLeafAccessible
+ void SetText(const nsAString& aText) { mText = aText; }
+ const nsString& Text() const { return mText; }
+
+ protected:
+ nsString mText;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+// LocalAccessible downcast method
+
+inline TextLeafAccessible* LocalAccessible::AsTextLeaf() {
+ return IsTextLeaf() ? static_cast<TextLeafAccessible*>(this) : nullptr;
+}
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/generic/moz.build b/accessible/generic/moz.build
new file mode 100644
index 0000000000..ae4b28e6ba
--- /dev/null
+++ b/accessible/generic/moz.build
@@ -0,0 +1,74 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+EXPORTS.mozilla.a11y += [
+ "DocAccessible.h",
+ "HyperTextAccessible.h",
+ "LocalAccessible.h",
+ "OuterDocAccessible.h",
+]
+
+UNIFIED_SOURCES += [
+ "ApplicationAccessible.cpp",
+ "ARIAGridAccessible.cpp",
+ "BaseAccessibles.cpp",
+ "DocAccessible.cpp",
+ "FormControlAccessible.cpp",
+ "HyperTextAccessible.cpp",
+ "ImageAccessible.cpp",
+ "LocalAccessible.cpp",
+ "OuterDocAccessible.cpp",
+ "RootAccessible.cpp",
+ "TableAccessible.cpp",
+ "TableCellAccessible.cpp",
+ "TextLeafAccessible.cpp",
+]
+
+LOCAL_INCLUDES += [
+ "/accessible/base",
+ "/accessible/html",
+ "/accessible/xpcom",
+ "/accessible/xul",
+ "/dom/base",
+ "/dom/xul",
+ "/layout/generic",
+ "/layout/xul",
+]
+
+if CONFIG["OS_ARCH"] == "WINNT":
+ LOCAL_INCLUDES += [
+ "/accessible/ipc/win",
+ ]
+else:
+ LOCAL_INCLUDES += [
+ "/accessible/ipc/other",
+ ]
+
+if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk":
+ LOCAL_INCLUDES += [
+ "/accessible/atk",
+ ]
+elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "windows":
+ LOCAL_INCLUDES += [
+ "/accessible/windows/ia2",
+ "/accessible/windows/msaa",
+ ]
+elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa":
+ LOCAL_INCLUDES += [
+ "/accessible/mac",
+ ]
+elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "android":
+ LOCAL_INCLUDES += [
+ "/accessible/android",
+ ]
+else:
+ LOCAL_INCLUDES += [
+ "/accessible/other",
+ ]
+
+FINAL_LIBRARY = "xul"
+
+include("/ipc/chromium/chromium-config.mozbuild")