summaryrefslogtreecommitdiff
path: root/chromium/third_party/blink/renderer/modules/accessibility
diff options
context:
space:
mode:
authorAllan Sandfeld Jensen <allan.jensen@qt.io>2021-09-03 13:32:17 +0200
committerAllan Sandfeld Jensen <allan.jensen@qt.io>2021-10-01 14:31:55 +0200
commit21ba0c5d4bf8fba15dddd97cd693bad2358b77fd (patch)
tree91be119f694044dfc1ff9fdc054459e925de9df0 /chromium/third_party/blink/renderer/modules/accessibility
parent03c549e0392f92c02536d3f86d5e1d8dfa3435ac (diff)
downloadqtwebengine-chromium-21ba0c5d4bf8fba15dddd97cd693bad2358b77fd.tar.gz
BASELINE: Update Chromium to 92.0.4515.166
Change-Id: I42a050486714e9e54fc271f2a8939223a02ae364
Diffstat (limited to 'chromium/third_party/blink/renderer/modules/accessibility')
-rw-r--r--chromium/third_party/blink/renderer/modules/accessibility/BUILD.gn2
-rw-r--r--chromium/third_party/blink/renderer/modules/accessibility/accessibility_object_model_test.cc2
-rw-r--r--chromium/third_party/blink/renderer/modules/accessibility/ax_enums.h2
-rw-r--r--chromium/third_party/blink/renderer/modules/accessibility/ax_image_map_link.cc20
-rw-r--r--chromium/third_party/blink/renderer/modules/accessibility/ax_image_map_link.h3
-rw-r--r--chromium/third_party/blink/renderer/modules/accessibility/ax_inline_text_box.cc10
-rw-r--r--chromium/third_party/blink/renderer/modules/accessibility/ax_inline_text_box.h1
-rw-r--r--chromium/third_party/blink/renderer/modules/accessibility/ax_layout_object.cc211
-rw-r--r--chromium/third_party/blink/renderer/modules/accessibility/ax_layout_object.h6
-rw-r--r--chromium/third_party/blink/renderer/modules/accessibility/ax_layout_object_test.cc12
-rw-r--r--chromium/third_party/blink/renderer/modules/accessibility/ax_list_box_option.cc16
-rw-r--r--chromium/third_party/blink/renderer/modules/accessibility/ax_list_box_option.h1
-rw-r--r--chromium/third_party/blink/renderer/modules/accessibility/ax_media_control.cc36
-rw-r--r--chromium/third_party/blink/renderer/modules/accessibility/ax_media_control.h31
-rw-r--r--chromium/third_party/blink/renderer/modules/accessibility/ax_media_element.cc2
-rw-r--r--chromium/third_party/blink/renderer/modules/accessibility/ax_menu_list.cc19
-rw-r--r--chromium/third_party/blink/renderer/modules/accessibility/ax_menu_list.h2
-rw-r--r--chromium/third_party/blink/renderer/modules/accessibility/ax_menu_list_option.cc65
-rw-r--r--chromium/third_party/blink/renderer/modules/accessibility/ax_menu_list_option.h7
-rw-r--r--chromium/third_party/blink/renderer/modules/accessibility/ax_menu_list_popup.cc3
-rw-r--r--chromium/third_party/blink/renderer/modules/accessibility/ax_mock_object.cc6
-rw-r--r--chromium/third_party/blink/renderer/modules/accessibility/ax_mock_object.h1
-rw-r--r--chromium/third_party/blink/renderer/modules/accessibility/ax_node_object.cc797
-rw-r--r--chromium/third_party/blink/renderer/modules/accessibility/ax_node_object.h36
-rw-r--r--chromium/third_party/blink/renderer/modules/accessibility/ax_object.cc1386
-rw-r--r--chromium/third_party/blink/renderer/modules/accessibility/ax_object.h223
-rw-r--r--chromium/third_party/blink/renderer/modules/accessibility/ax_object_cache_impl.cc799
-rw-r--r--chromium/third_party/blink/renderer/modules/accessibility/ax_object_cache_impl.h44
-rw-r--r--chromium/third_party/blink/renderer/modules/accessibility/ax_object_cache_test.cc3
-rw-r--r--chromium/third_party/blink/renderer/modules/accessibility/ax_object_test.cc206
-rw-r--r--chromium/third_party/blink/renderer/modules/accessibility/ax_position.cc12
-rw-r--r--chromium/third_party/blink/renderer/modules/accessibility/ax_position.h1
-rw-r--r--chromium/third_party/blink/renderer/modules/accessibility/ax_range.h1
-rw-r--r--chromium/third_party/blink/renderer/modules/accessibility/ax_relation_cache.cc112
-rw-r--r--chromium/third_party/blink/renderer/modules/accessibility/ax_relation_cache.h12
-rw-r--r--chromium/third_party/blink/renderer/modules/accessibility/ax_selection.cc16
-rw-r--r--chromium/third_party/blink/renderer/modules/accessibility/ax_selection.h5
-rw-r--r--chromium/third_party/blink/renderer/modules/accessibility/ax_sparse_attribute_setter.cc18
-rw-r--r--chromium/third_party/blink/renderer/modules/accessibility/ax_virtual_object.cc4
-rw-r--r--chromium/third_party/blink/renderer/modules/accessibility/ax_virtual_object.h2
-rw-r--r--chromium/third_party/blink/renderer/modules/accessibility/inspector_accessibility_agent.cc57
-rw-r--r--chromium/third_party/blink/renderer/modules/accessibility/inspector_type_builder_helper.cc4
-rw-r--r--chromium/third_party/blink/renderer/modules/accessibility/inspector_type_builder_helper.h2
-rw-r--r--chromium/third_party/blink/renderer/modules/accessibility/testing/accessibility_test.cc2
44 files changed, 2467 insertions, 1733 deletions
diff --git a/chromium/third_party/blink/renderer/modules/accessibility/BUILD.gn b/chromium/third_party/blink/renderer/modules/accessibility/BUILD.gn
index e7dabc4d991..507e6b44e08 100644
--- a/chromium/third_party/blink/renderer/modules/accessibility/BUILD.gn
+++ b/chromium/third_party/blink/renderer/modules/accessibility/BUILD.gn
@@ -18,6 +18,8 @@ blink_modules_sources("accessibility") {
"ax_list_box.h",
"ax_list_box_option.cc",
"ax_list_box_option.h",
+ "ax_media_control.cc",
+ "ax_media_control.h",
"ax_media_element.cc",
"ax_media_element.h",
"ax_menu_list.cc",
diff --git a/chromium/third_party/blink/renderer/modules/accessibility/accessibility_object_model_test.cc b/chromium/third_party/blink/renderer/modules/accessibility/accessibility_object_model_test.cc
index c06f240c14a..8ba9a008a97 100644
--- a/chromium/third_party/blink/renderer/modules/accessibility/accessibility_object_model_test.cc
+++ b/chromium/third_party/blink/renderer/modules/accessibility/accessibility_object_model_test.cc
@@ -144,7 +144,7 @@ TEST_F(AccessibilityObjectModelTest, AOMPropertiesCanBeCleared) {
// Null the AOM properties.
button->accessibleNode()->setRole(g_null_atom);
button->accessibleNode()->setLabel(g_null_atom);
- button->accessibleNode()->setDisabled(base::nullopt);
+ button->accessibleNode()->setDisabled(absl::nullopt);
GetDocument().View()->UpdateLifecycleToLayoutClean(
DocumentUpdateReason::kTest);
diff --git a/chromium/third_party/blink/renderer/modules/accessibility/ax_enums.h b/chromium/third_party/blink/renderer/modules/accessibility/ax_enums.h
index 560a7c20792..df71eed1255 100644
--- a/chromium/third_party/blink/renderer/modules/accessibility/ax_enums.h
+++ b/chromium/third_party/blink/renderer/modules/accessibility/ax_enums.h
@@ -105,14 +105,12 @@ enum AXTextFromNativeHTML {
enum AXIgnoredReason {
kAXActiveModalDialog,
kAXAriaModalDialog,
- kAXAncestorIsLeafNode,
kAXAriaHiddenElement,
kAXAriaHiddenSubtree,
kAXEmptyAlt,
kAXEmptyText,
kAXInertElement,
kAXInertSubtree,
- kAXInheritsPresentation,
kAXLabelContainer,
kAXLabelFor,
kAXNotRendered,
diff --git a/chromium/third_party/blink/renderer/modules/accessibility/ax_image_map_link.cc b/chromium/third_party/blink/renderer/modules/accessibility/ax_image_map_link.cc
index 57cfe31a62b..9ee0a87f11a 100644
--- a/chromium/third_party/blink/renderer/modules/accessibility/ax_image_map_link.cc
+++ b/chromium/third_party/blink/renderer/modules/accessibility/ax_image_map_link.cc
@@ -30,6 +30,7 @@
#include "third_party/blink/renderer/core/aom/accessible_node.h"
#include "third_party/blink/renderer/core/dom/element_traversal.h"
+#include "third_party/blink/renderer/core/html/html_image_element.h"
#include "third_party/blink/renderer/modules/accessibility/ax_layout_object.h"
#include "third_party/blink/renderer/modules/accessibility/ax_object_cache_impl.h"
#include "third_party/blink/renderer/platform/graphics/path.h"
@@ -50,14 +51,17 @@ HTMLMapElement* AXImageMapLink::MapElement() const {
return Traversal<HTMLMapElement>::FirstAncestor(*area);
}
-AXObject* AXImageMapLink::ComputeParentImpl() const {
- if (MapElement()) {
- AXObject* ax_parent =
- AXObjectCache().GetOrCreate(MapElement()->GetLayoutObject());
- if (ax_parent)
- return ax_parent;
- }
- return AXNodeObject::ComputeParentImpl();
+// static
+AXObject* AXImageMapLink::GetAXObjectForImageMap(AXObjectCacheImpl& cache,
+ Node* area) {
+ DCHECK(area);
+ DCHECK(IsA<HTMLAreaElement>(area));
+
+ HTMLMapElement* map = Traversal<HTMLMapElement>::FirstAncestor(*area);
+ if (!map)
+ return nullptr;
+
+ return cache.GetOrCreate(static_cast<Node*>(map->ImageElement()));
}
ax::mojom::blink::Role AXImageMapLink::NativeRoleIgnoringAria() const {
diff --git a/chromium/third_party/blink/renderer/modules/accessibility/ax_image_map_link.h b/chromium/third_party/blink/renderer/modules/accessibility/ax_image_map_link.h
index e52e50154e8..c325cc031d4 100644
--- a/chromium/third_party/blink/renderer/modules/accessibility/ax_image_map_link.h
+++ b/chromium/third_party/blink/renderer/modules/accessibility/ax_image_map_link.h
@@ -63,7 +63,8 @@ class AXImageMapLink final : public AXNodeObject {
Element* ActionElement() const override;
KURL Url() const override;
bool IsLinked() const override { return true; }
- AXObject* ComputeParentImpl() const override;
+ // For an <area>, return an <img> that should be used as its parent, or null.
+ static AXObject* GetAXObjectForImageMap(AXObjectCacheImpl& cache, Node* area);
void GetRelativeBounds(AXObject** out_container,
FloatRect& out_bounds_in_container,
SkMatrix44& out_container_transform,
diff --git a/chromium/third_party/blink/renderer/modules/accessibility/ax_inline_text_box.cc b/chromium/third_party/blink/renderer/modules/accessibility/ax_inline_text_box.cc
index f49bc9f3ae2..f6f95831d8f 100644
--- a/chromium/third_party/blink/renderer/modules/accessibility/ax_inline_text_box.cc
+++ b/chromium/third_party/blink/renderer/modules/accessibility/ax_inline_text_box.cc
@@ -33,7 +33,7 @@
#include <utility>
#include "base/numerics/clamped_math.h"
-#include "base/optional.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
#include "third_party/blink/renderer/core/editing/markers/document_marker_controller.h"
#include "third_party/blink/renderer/core/editing/position.h"
@@ -270,7 +270,7 @@ void AXInlineTextBox::SerializeMarkerAttributes(
std::vector<int32_t> marker_ends;
// First use ARIA markers for spelling/grammar if available.
- base::Optional<DocumentMarker::MarkerType> aria_marker_type =
+ absl::optional<DocumentMarker::MarkerType> aria_marker_type =
GetAriaSpellingOrGrammarMarker();
if (aria_marker_type) {
marker_types.push_back(ToAXMarkerType(aria_marker_type.value()));
@@ -360,6 +360,8 @@ void AXInlineTextBox::Init(AXObject* parent) {
DCHECK(parent);
DCHECK(ui::CanHaveInlineTextBoxChildren(parent->RoleValue()))
<< "Unexpected parent of inline text box: " << parent->RoleValue();
+ DCHECK(parent->CanHaveChildren())
+ << "Parent cannot have children: " << parent->ToString(true, true);
SetParent(parent);
UpdateCachedAttributeValuesIfNeeded(false);
}
@@ -390,4 +392,8 @@ int AXInlineTextBox::TextLength() const {
return int{inline_text_box_->Len()};
}
+void AXInlineTextBox::ClearChildren() const {
+ // An AXInlineTextBox has no children to clear.
+}
+
} // namespace blink
diff --git a/chromium/third_party/blink/renderer/modules/accessibility/ax_inline_text_box.h b/chromium/third_party/blink/renderer/modules/accessibility/ax_inline_text_box.h
index b81811bc79b..98d17ed1906 100644
--- a/chromium/third_party/blink/renderer/modules/accessibility/ax_inline_text_box.h
+++ b/chromium/third_party/blink/renderer/modules/accessibility/ax_inline_text_box.h
@@ -69,6 +69,7 @@ class AXInlineTextBox final : public AXObject {
NOTREACHED();
return ax::mojom::blink::Role::kInlineTextBox;
}
+ void ClearChildren() const override;
protected:
void Init(AXObject* parent) override;
diff --git a/chromium/third_party/blink/renderer/modules/accessibility/ax_layout_object.cc b/chromium/third_party/blink/renderer/modules/accessibility/ax_layout_object.cc
index 9b38973a76e..454c0502df3 100644
--- a/chromium/third_party/blink/renderer/modules/accessibility/ax_layout_object.cc
+++ b/chromium/third_party/blink/renderer/modules/accessibility/ax_layout_object.cc
@@ -57,6 +57,7 @@
#include "third_party/blink/renderer/core/html/html_table_cell_element.h"
#include "third_party/blink/renderer/core/html/html_table_col_element.h"
#include "third_party/blink/renderer/core/html/html_table_element.h"
+#include "third_party/blink/renderer/core/html/media/html_media_element.h"
#include "third_party/blink/renderer/core/html/shadow/shadow_element_names.h"
#include "third_party/blink/renderer/core/input_type_names.h"
#include "third_party/blink/renderer/core/layout/api/line_layout_api_shim.h"
@@ -94,6 +95,7 @@
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/text/platform_locale.h"
#include "third_party/blink/renderer/platform/wtf/std_lib_extras.h"
+#include "ui/accessibility/ax_role_properties.h"
namespace blink {
@@ -112,6 +114,10 @@ AXLayoutObject::~AXLayoutObject() {
DCHECK(IsDetached());
}
+LayoutObject* AXLayoutObject::GetLayoutObject() const {
+ return layout_object_;
+}
+
bool IsProgrammaticallyScrollable(LayoutBox* box) {
if (!box->IsScrollContainer())
return false;
@@ -161,15 +167,46 @@ static bool IsImageOrAltText(LayoutObject* layout_object, Node* node) {
return false;
}
+static bool ShouldIgnoreListItem(Node* node) {
+ DCHECK(node);
+
+ // http://www.w3.org/TR/wai-aria/complete#presentation
+ // A list item is presentational if its parent is a native list but
+ // it has an explicit ARIA role set on it that's anything other than "list".
+ Element* parent = FlatTreeTraversal::ParentElement(*node);
+ if (!parent)
+ return false;
+
+ if (IsA<HTMLMenuElement>(*parent) || IsA<HTMLUListElement>(*parent) ||
+ IsA<HTMLOListElement>(*parent)) {
+ AtomicString role = AccessibleNode::GetPropertyOrARIAAttribute(
+ parent, AOMStringProperty::kRole);
+ if (!role.IsEmpty() && role != "list")
+ return true;
+ }
+ return false;
+}
+
ax::mojom::blink::Role AXLayoutObject::RoleFromLayoutObjectOrNode() const {
DCHECK(layout_object_);
Node* node = GetNode(); // Can be null in the case of pseudo content.
- if (layout_object_->IsListItemIncludingNG() || IsA<HTMLLIElement>(node))
+ if (IsA<HTMLLIElement>(node)) {
+ if (ShouldIgnoreListItem(node))
+ return ax::mojom::blink::Role::kNone;
return ax::mojom::blink::Role::kListItem;
- if (layout_object_->IsListMarkerIncludingAll())
+ }
+
+ if (layout_object_->IsListMarkerIncludingAll()) {
+ Node* list_item = layout_object_->GeneratingNode();
+ if (list_item && ShouldIgnoreListItem(list_item))
+ return ax::mojom::blink::Role::kNone;
return ax::mojom::blink::Role::kListMarker;
+ }
+
+ if (layout_object_->IsListItemIncludingNG())
+ return ax::mojom::blink::Role::kListItem;
if (layout_object_->IsBR())
return ax::mojom::blink::Role::kLineBreak;
if (layout_object_->IsText())
@@ -272,89 +309,6 @@ static bool IsLinkable(const AXObject& object) {
object.GetLayoutObject()->IsText();
}
-// Requires layoutObject to be present because it relies on style
-// user-modify. Don't move this logic to AXNodeObject.
-bool AXLayoutObject::IsEditable() const {
- if (IsDetached())
- return false;
-
- const Node* node = GetNodeOrContainingBlockNode();
- if (!node)
- return false;
-
- const auto* elem = DynamicTo<Element>(node);
- if (!elem)
- elem = FlatTreeTraversal::ParentElement(*node);
- if (GetLayoutObject()->IsTextControlIncludingNG())
- return true;
-
- // Contrary to Firefox, we mark editable all auto-generated content, such as
- // list bullets and soft line breaks, that are contained within an editable
- // container.
- if (HasEditableStyle(*node))
- return true;
-
- if (IsWebArea()) {
- Document& document = GetLayoutObject()->GetDocument();
- HTMLElement* body = document.body();
- if (body && HasEditableStyle(*body)) {
- // A web area is editable if the body is contenteditable, unless the body
- // or an ancestor of the body is aria-hidden. The following avoids
- // GetOrCreate() on the body so that IsEditable() can be called when
- // layout is not clean. Check current object for AriaHiddenRoot(), and
- // manually check the <html> and <body> elements directly.
- bool is_null = true;
- if (AriaHiddenRoot() ||
- AccessibleNode::GetPropertyOrARIAAttribute(
- body, AOMBooleanProperty::kHidden, is_null) ||
- AccessibleNode::GetPropertyOrARIAAttribute(
- body->parentElement(), AOMBooleanProperty::kHidden, is_null)) {
- return false;
- }
- return true;
- }
-
- return HasEditableStyle(document);
- }
-
- return AXNodeObject::IsEditable();
-}
-
-// Requires layoutObject to be present because it relies on style
-// user-modify. Don't move this logic to AXNodeObject.
-// Returns true for a contenteditable or any descendant of it.
-bool AXLayoutObject::IsRichlyEditable() const {
- if (IsDetached())
- return false;
-
- const Node* node = GetNodeOrContainingBlockNode();
- if (!node)
- return false;
-
- const Element* elem = DynamicTo<Element>(node);
- if (!elem)
- elem = FlatTreeTraversal::ParentElement(*node);
-
- // Contrary to Firefox, we mark richly editable all auto-generated content,
- // such as list bullets and soft line breaks, that are contained within a
- // richly editable container.
- if (HasRichlyEditableStyle(*node))
- return true;
-
- if (IsWebArea()) {
- Document& document = layout_object_->GetDocument();
- HTMLElement* body = document.body();
- if (body && HasRichlyEditableStyle(*body)) {
- AXObject* ax_body = AXObjectCache().GetOrCreate(body);
- return ax_body && ax_body != ax_body->AriaHiddenRoot();
- }
-
- return HasRichlyEditableStyle(document);
- }
-
- return AXNodeObject::IsRichlyEditable();
-}
-
bool AXLayoutObject::IsLineBreakingObject() const {
if (IsDetached())
return false;
@@ -364,6 +318,11 @@ bool AXLayoutObject::IsLineBreakingObject() const {
if (IsPresentational())
return false;
+ // Without this condition, LayoutNG reports list markers as line breaking
+ // objects (legacy layout does not).
+ if (RoleValue() == ax::mojom::blink::Role::kListMarker)
+ return false;
+
const LayoutObject* layout_object = GetLayoutObject();
if (layout_object->IsBR() || layout_object->IsLayoutBlock() ||
layout_object->IsTableSection() || layout_object->IsAnonymousBlock() ||
@@ -515,6 +474,14 @@ bool AXLayoutObject::ComputeAccessibilityIsIgnored(
if (layout_object_->IsLayoutEmbeddedContent())
return false;
+ if (node && node->IsInUserAgentShadowRoot()) {
+ if (auto* containing_media_element =
+ DynamicTo<HTMLMediaElement>(node->OwnerShadowHost())) {
+ if (!containing_media_element->ShouldShowControls())
+ return true;
+ }
+ }
+
// Make sure renderers with layers stay in the tree.
if (GetLayoutObject() && GetLayoutObject()->HasLayer() && node &&
node->hasChildren()) {
@@ -559,11 +526,24 @@ bool AXLayoutObject::ComputeAccessibilityIsIgnored(
ignored_reasons->push_back(IgnoredReason(kAXPresentational));
return true;
}
+ // Ignore text inside of an ignored <label>.
+ // To save processing, only walk up the ignored objects.
+ // This means that other interesting objects inside the <label> will
+ // cause the text to be unignored.
+ AXObject* ancestor = ParentObject();
+ while (ancestor && ancestor->AccessibilityIsIgnored()) {
+ if (ancestor->RoleValue() == ax::mojom::blink::Role::kLabelText) {
+ if (ignored_reasons)
+ ignored_reasons->push_back(IgnoredReason(kAXPresentational));
+ return true;
+ }
+ ancestor = ancestor->ParentObject();
+ }
return false;
}
// FIXME(aboxhall): may need to move?
- base::Optional<String> alt_text = GetCSSAltText(node);
+ absl::optional<String> alt_text = GetCSSAltText(node);
if (alt_text)
return alt_text->IsEmpty();
@@ -761,7 +741,7 @@ static AXObject* NextOnLineInternalNG(const AXObject& ax_object) {
if (cursor)
break;
- // No cursor found: will try get cursor from first layout child.
+ // No cursor found: will try getting the cursor from the last layout child.
// This can happen on an inline element.
LayoutObject* layout_child = layout_object->SlowLastChild();
if (!layout_child)
@@ -794,7 +774,23 @@ static AXObject* NextOnLineInternalNG(const AXObject& ax_object) {
}
// Fallback: Use AX parent's next on line.
- return ax_object.ParentObject()->NextOnLine();
+ AXObject* ax_parent = ax_object.ParentObject();
+ AXObject* ax_result = ax_parent->NextOnLine();
+ if (!ax_result)
+ return nullptr;
+
+#if DCHECK_IS_ON()
+ if (!ax_object.AXObjectCache().IsAriaOwned(&ax_object)) {
+ DCHECK_NE(ax_result->ParentObject(), &ax_object)
+ << "NextOnLine() must not point to a child of the current object. "
+ "Because inline objects without try to return a result from their "
+ "parents, using a descendant can cause a previous position to be "
+ "reused, which appears as a loop in the nextOnLine data, and "
+ "can cause an infinite loop in consumers of the nextOnLine data";
+ }
+#endif
+
+ return ax_result;
}
AXObject* AXLayoutObject::NextOnLine() const {
@@ -927,8 +923,24 @@ static AXObject* PreviousOnLineInlineNG(const AXObject& ax_object) {
return nullptr;
}
- // Fallback: Use AX parent's next on line.
- return ax_object.ParentObject()->PreviousOnLine();
+ // Fallback: Use AX parent's previous on line.
+ AXObject* ax_parent = ax_object.ParentObject();
+ AXObject* ax_result = ax_parent->PreviousOnLine();
+ if (!ax_result)
+ return nullptr;
+
+#if DCHECK_IS_ON()
+ if (!ax_object.AXObjectCache().IsAriaOwned(&ax_object)) {
+ DCHECK_NE(ax_result->ParentObject(), &ax_object)
+ << "PreviousOnLine() must not point to a child of the current object. "
+ "Because inline objects without try to return a result from their "
+ "parents, using a descendant can cause a previous position to be "
+ "reused, which appears as a loop in the previousOnLine data, and "
+ "can cause an infinite loop in consumers of the previousOnLine data";
+ }
+#endif
+
+ return ax_result;
}
AXObject* AXLayoutObject::PreviousOnLine() const {
@@ -1015,7 +1027,7 @@ String AXLayoutObject::TextAlternative(bool recursive,
AXRelatedObjectVector* related_objects,
NameSources* name_sources) const {
if (layout_object_) {
- base::Optional<String> text_alternative = GetCSSAltText(GetNode());
+ absl::optional<String> text_alternative = GetCSSAltText(GetNode());
bool found_text_alternative = false;
if (text_alternative) {
if (name_sources) {
@@ -1133,17 +1145,18 @@ AXObject* AXLayoutObject::AccessibilityHitTest(const IntPoint& point) const {
// Allow the element to perform any hit-testing it might need to do to reach
// non-layout children.
result = result->ElementAccessibilityHitTest(point);
- if (result && result->AccessibilityIsIgnored()) {
+
+ while (result && result->AccessibilityIsIgnored()) {
// If this element is the label of a control, a hit test should return the
- // control.
- if (auto* ax_object = DynamicTo<AXLayoutObject>(result)) {
- AXObject* control_object =
- ax_object->CorrespondingControlAXObjectForLabelElement();
- if (control_object && control_object->NameFromLabelElement())
- return control_object;
+ // control. The label is ignored because it's already reflected in the name.
+ if (auto* label = DynamicTo<HTMLLabelElement>(result->GetNode())) {
+ if (HTMLElement* control = label->control()) {
+ if (AXObject* ax_control = AXObjectCache().GetOrCreate(control))
+ return ax_control;
+ }
}
- result = result->ParentObjectUnignored();
+ result = result->ParentObject();
}
return result;
diff --git a/chromium/third_party/blink/renderer/modules/accessibility/ax_layout_object.h b/chromium/third_party/blink/renderer/modules/accessibility/ax_layout_object.h
index 2edb48cf73c..9b376d41ccf 100644
--- a/chromium/third_party/blink/renderer/modules/accessibility/ax_layout_object.h
+++ b/chromium/third_party/blink/renderer/modules/accessibility/ax_layout_object.h
@@ -47,8 +47,8 @@ class MODULES_EXPORT AXLayoutObject : public AXNodeObject {
AXLayoutObject(LayoutObject*, AXObjectCacheImpl&);
~AXLayoutObject() override;
- // Public, overridden from AXObject.
- LayoutObject* GetLayoutObject() const final { return layout_object_; }
+ // AXObject overrides:
+ LayoutObject* GetLayoutObject() const final;
ScrollableArea* GetScrollableAreaIfScrollable() const final;
// If this is an anonymous node, returns the node of its containing layout
@@ -70,8 +70,6 @@ class MODULES_EXPORT AXLayoutObject : public AXNodeObject {
bool IsAXLayoutObject() const final;
// Check object role or purpose.
- bool IsEditable() const override;
- bool IsRichlyEditable() const override;
bool IsLineBreakingObject() const override;
bool IsLinked() const override;
bool IsOffScreen() const override;
diff --git a/chromium/third_party/blink/renderer/modules/accessibility/ax_layout_object_test.cc b/chromium/third_party/blink/renderer/modules/accessibility/ax_layout_object_test.cc
index d8e63dfbc4d..ef26ab34184 100644
--- a/chromium/third_party/blink/renderer/modules/accessibility/ax_layout_object_test.cc
+++ b/chromium/third_party/blink/renderer/modules/accessibility/ax_layout_object_test.cc
@@ -22,7 +22,7 @@ class AXLayoutObjectTest : public AccessibilityTest {
}
};
-TEST_F(AXLayoutObjectTest, IsEditableInsideListmarker) {
+TEST_F(AXLayoutObjectTest, IsNotEditableInsideListmarker) {
SetBodyInnerHTML("<div contenteditable><li id=t>ab");
// The layout tree is:
// LayoutNGBlockFlow {DIV} at (0,0) size 784x20
@@ -44,11 +44,11 @@ TEST_F(AXLayoutObjectTest, IsEditableInsideListmarker) {
const AXObject* ax_list_marker = GetAXObject(&list_marker);
ASSERT_NE(nullptr, ax_list_marker);
EXPECT_TRUE(IsA<AXLayoutObject>(ax_list_item));
- EXPECT_TRUE(ax_list_marker->IsEditable());
- EXPECT_TRUE(ax_list_marker->IsRichlyEditable());
+ EXPECT_FALSE(ax_list_marker->IsEditable());
+ EXPECT_FALSE(ax_list_marker->IsRichlyEditable());
}
-TEST_F(AXLayoutObjectTest, IsEditableOutsideListmarker) {
+TEST_F(AXLayoutObjectTest, IsNotEditableOutsideListmarker) {
SetBodyInnerHTML("<ol contenteditable><li id=t>ab");
// THe layout tree is:
// LayoutNGBlockFlow {OL} at (0,0) size 784x20
@@ -70,8 +70,8 @@ TEST_F(AXLayoutObjectTest, IsEditableOutsideListmarker) {
const AXObject* ax_list_marker = GetAXObject(&list_marker);
ASSERT_NE(nullptr, ax_list_marker);
EXPECT_TRUE(IsA<AXLayoutObject>(ax_list_item));
- EXPECT_TRUE(ax_list_marker->IsEditable());
- EXPECT_TRUE(ax_list_marker->IsRichlyEditable());
+ EXPECT_FALSE(ax_list_marker->IsEditable());
+ EXPECT_FALSE(ax_list_marker->IsRichlyEditable());
}
TEST_F(AXLayoutObjectTest, GetValueForControlWithTextTransform) {
diff --git a/chromium/third_party/blink/renderer/modules/accessibility/ax_list_box_option.cc b/chromium/third_party/blink/renderer/modules/accessibility/ax_list_box_option.cc
index c8de06e4808..d2924bba469 100644
--- a/chromium/third_party/blink/renderer/modules/accessibility/ax_list_box_option.cc
+++ b/chromium/third_party/blink/renderer/modules/accessibility/ax_list_box_option.cc
@@ -46,22 +46,6 @@ ax::mojom::blink::Role AXListBoxOption::NativeRoleIgnoringAria() const {
return ax::mojom::blink::Role::kListBoxOption;
}
-bool AXListBoxOption::IsParentPresentationalRole() const {
- LayoutObject* parent_layout_object = GetLayoutObject()->Parent();
- if (!parent_layout_object)
- return false;
-
- AXObject* parent = AXObjectCache().GetOrCreate(parent_layout_object);
- if (!parent)
- return false;
-
- if (IsListBox(parent_layout_object) &&
- parent->HasInheritedPresentationalRole())
- return true;
-
- return false;
-}
-
AccessibilitySelectedState AXListBoxOption::IsSelected() const {
if (!GetNode() || !CanSetSelectedAttribute())
return kSelectedStateUndefined;
diff --git a/chromium/third_party/blink/renderer/modules/accessibility/ax_list_box_option.h b/chromium/third_party/blink/renderer/modules/accessibility/ax_list_box_option.h
index d1e555d3a55..e4bb885433f 100644
--- a/chromium/third_party/blink/renderer/modules/accessibility/ax_list_box_option.h
+++ b/chromium/third_party/blink/renderer/modules/accessibility/ax_list_box_option.h
@@ -61,7 +61,6 @@ class AXListBoxOption final : public AXLayoutObject {
bool ComputeAccessibilityIsIgnored(IgnoredReasons* = nullptr) const override;
HTMLSelectElement* ListBoxOptionParentNode() const;
- bool IsParentPresentationalRole() const;
DISALLOW_COPY_AND_ASSIGN(AXListBoxOption);
};
diff --git a/chromium/third_party/blink/renderer/modules/accessibility/ax_media_control.cc b/chromium/third_party/blink/renderer/modules/accessibility/ax_media_control.cc
new file mode 100644
index 00000000000..9fc9ebe4a0b
--- /dev/null
+++ b/chromium/third_party/blink/renderer/modules/accessibility/ax_media_control.cc
@@ -0,0 +1,36 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/modules/accessibility/ax_media_control.h"
+
+#include "third_party/blink/renderer/core/layout/layout_object.h"
+#include "third_party/blink/renderer/modules/media_controls/elements/media_control_elements_helper.h"
+
+namespace blink {
+
+// static
+AXObject* AccessibilityMediaControl::Create(
+ LayoutObject* layout_object,
+ AXObjectCacheImpl& ax_object_cache) {
+ DCHECK(layout_object->GetNode());
+ return MakeGarbageCollected<AccessibilityMediaControl>(layout_object,
+ ax_object_cache);
+}
+
+AccessibilityMediaControl::AccessibilityMediaControl(
+ LayoutObject* layout_object,
+ AXObjectCacheImpl& ax_object_cache)
+ : AXLayoutObject(layout_object, ax_object_cache) {}
+
+bool AccessibilityMediaControl::InternalSetAccessibilityFocusAction() {
+ MediaControlElementsHelper::NotifyMediaControlAccessibleFocus(GetElement());
+ return true;
+}
+
+bool AccessibilityMediaControl::InternalClearAccessibilityFocusAction() {
+ MediaControlElementsHelper::NotifyMediaControlAccessibleBlur(GetElement());
+ return true;
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/modules/accessibility/ax_media_control.h b/chromium/third_party/blink/renderer/modules/accessibility/ax_media_control.h
new file mode 100644
index 00000000000..f8e63171dd8
--- /dev/null
+++ b/chromium/third_party/blink/renderer/modules/accessibility/ax_media_control.h
@@ -0,0 +1,31 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_MODULES_ACCESSIBILITY_AX_MEDIA_CONTROL_H_
+#define THIRD_PARTY_BLINK_RENDERER_MODULES_ACCESSIBILITY_AX_MEDIA_CONTROL_H_
+
+#include "third_party/blink/renderer/modules/accessibility/ax_layout_object.h"
+
+namespace blink {
+
+class AXObjectCacheImpl;
+
+class AccessibilityMediaControl : public AXLayoutObject {
+ public:
+ static AXObject* Create(LayoutObject*, AXObjectCacheImpl&);
+
+ AccessibilityMediaControl(LayoutObject*, AXObjectCacheImpl&);
+ AccessibilityMediaControl(const AccessibilityMediaControl&) = delete;
+ AccessibilityMediaControl& operator=(const AccessibilityMediaControl&) =
+ delete;
+ ~AccessibilityMediaControl() override = default;
+
+ // AXLayoutObject:
+ bool InternalSetAccessibilityFocusAction() override;
+ bool InternalClearAccessibilityFocusAction() override;
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_MODULES_ACCESSIBILITY_AX_MEDIA_CONTROL_H_
diff --git a/chromium/third_party/blink/renderer/modules/accessibility/ax_media_element.cc b/chromium/third_party/blink/renderer/modules/accessibility/ax_media_element.cc
index e2268f6907d..a339d726a08 100644
--- a/chromium/third_party/blink/renderer/modules/accessibility/ax_media_element.cc
+++ b/chromium/third_party/blink/renderer/modules/accessibility/ax_media_element.cc
@@ -46,7 +46,7 @@ String AccessibilityMediaElement::TextAlternative(
}
bool AccessibilityMediaElement::CanHaveChildren() const {
- return HasControls();
+ return true;
}
bool AccessibilityMediaElement::ComputeAccessibilityIsIgnored(
diff --git a/chromium/third_party/blink/renderer/modules/accessibility/ax_menu_list.cc b/chromium/third_party/blink/renderer/modules/accessibility/ax_menu_list.cc
index 02e05fcbddb..8dddcb56c54 100644
--- a/chromium/third_party/blink/renderer/modules/accessibility/ax_menu_list.cc
+++ b/chromium/third_party/blink/renderer/modules/accessibility/ax_menu_list.cc
@@ -108,7 +108,18 @@ void AXMenuList::AddChildren() {
DCHECK(children_dirty_);
children_dirty_ = false;
- // Ensure mock AXMenuListPopup exists.
+ AXObject* ax_popup_child = GetOrCreateMockPopupChild();
+
+ // Update mock AXMenuListPopup children.
+ ax_popup_child->SetNeedsToUpdateChildren();
+ ax_popup_child->UpdateChildrenIfNecessary();
+}
+
+AXObject* AXMenuList::GetOrCreateMockPopupChild() {
+ if (IsDetached())
+ return nullptr;
+
+ // Ensure mock AXMenuListPopup exists as first and only child.
if (children_.IsEmpty()) {
AXObjectCacheImpl& cache = AXObjectCache();
AXObject* popup =
@@ -118,10 +129,8 @@ void AXMenuList::AddChildren() {
DCHECK(popup->CachedParentObject());
children_.push_back(popup);
}
-
- // Update mock AXMenuListPopup children.
- children_[0]->SetNeedsToUpdateChildren();
- children_[0]->UpdateChildrenIfNecessary();
+ DCHECK_EQ(children_.size(), 1U);
+ return children_[0];
}
bool AXMenuList::IsCollapsed() const {
diff --git a/chromium/third_party/blink/renderer/modules/accessibility/ax_menu_list.h b/chromium/third_party/blink/renderer/modules/accessibility/ax_menu_list.h
index 5c99a2f1575..b29757c9aad 100644
--- a/chromium/third_party/blink/renderer/modules/accessibility/ax_menu_list.h
+++ b/chromium/third_party/blink/renderer/modules/accessibility/ax_menu_list.h
@@ -46,6 +46,8 @@ class AXMenuList final : public AXLayoutObject {
void DidShowPopup();
void DidHidePopup();
+ AXObject* GetOrCreateMockPopupChild();
+
private:
friend class AXMenuListOption;
diff --git a/chromium/third_party/blink/renderer/modules/accessibility/ax_menu_list_option.cc b/chromium/third_party/blink/renderer/modules/accessibility/ax_menu_list_option.cc
index 6d56cefaa54..bcd8b2447dd 100644
--- a/chromium/third_party/blink/renderer/modules/accessibility/ax_menu_list_option.cc
+++ b/chromium/third_party/blink/renderer/modules/accessibility/ax_menu_list_option.cc
@@ -43,45 +43,32 @@ Element* AXMenuListOption::ActionElement() const {
return GetElement();
}
-AXObject* AXMenuListOption::ComputeParentImpl() const {
- Node* node = GetNode();
- if (!node) {
- NOTREACHED();
+// Return a parent if this is an <option> for an AXMenuList, otherwise null.
+// Returns null means that a parent will be computed from the DOM.
+// static
+AXObject* AXMenuListOption::ComputeParentAXMenuPopupFor(
+ AXObjectCacheImpl& cache,
+ HTMLOptionElement* option) {
+ // Note: In a <select> size=1, AXObjects are not created for <optgroup>'s.
+ DCHECK(option);
+
+ HTMLSelectElement* select = option->OwnerSelectElement();
+ if (!select || !select->UsesMenuList()) {
+ // If it's an <option> that is not inside of a menulist, we want it to
+ // return to the caller and use the default logic.
return nullptr;
}
- auto* select = To<HTMLOptionElement>(node)->OwnerSelectElement();
- if (!select) {
- NOTREACHED();
- return nullptr;
+ // If there is a <select> ancestor, return the popup for it, if rendered.
+ if (AXObject* select_ax_object = cache.GetOrCreate(select)) {
+ if (auto* menu_list = DynamicTo<AXMenuList>(select_ax_object))
+ return menu_list->GetOrCreateMockPopupChild();
}
- AXObject* select_ax_object = AXObjectCache().GetOrCreate(select);
- if (!select_ax_object) {
- NOTREACHED();
- return nullptr;
- }
-
- // This happens if the <select> is not rendered. Return it and move on.
- auto* menu_list = DynamicTo<AXMenuList>(select_ax_object);
- if (!menu_list)
- return select_ax_object;
-
- // In order to return the popup, which is a mock object, we need to grab
- // the AXMenuList itself, and get its only child.
- if (menu_list->NeedsToUpdateChildren())
- menu_list->UpdateChildrenIfNecessary();
-
- const auto& child_objects = menu_list->ChildrenIncludingIgnored();
- if (child_objects.IsEmpty())
- return nullptr;
- DCHECK_EQ(child_objects.size(), 1UL)
- << "A menulist must have a single popup child";
- DCHECK(IsA<AXMenuListPopup>(child_objects[0].Get()));
- To<AXMenuListPopup>(child_objects[0].Get())->UpdateChildrenIfNecessary();
-
- // Return the popup child, which is the parent of this AXMenuListOption.
- return child_objects[0];
+ // Otherwise, just return an AXObject for the parent node.
+ // This could be the <select> if it was not rendered.
+ // Or, any parent node if the <option> was not inside an AXMenuList.
+ return cache.GetOrCreate(select);
}
bool AXMenuListOption::IsVisible() const {
@@ -220,14 +207,4 @@ String AXMenuListOption::TextAlternative(bool recursive,
return text_alternative;
}
-HTMLSelectElement* AXMenuListOption::ParentSelectNode() const {
- if (!GetNode())
- return nullptr;
-
- if (auto* option = DynamicTo<HTMLOptionElement>(GetNode()))
- return option->OwnerSelectElement();
-
- return nullptr;
-}
-
} // namespace blink
diff --git a/chromium/third_party/blink/renderer/modules/accessibility/ax_menu_list_option.h b/chromium/third_party/blink/renderer/modules/accessibility/ax_menu_list_option.h
index aa6ae5c1da5..74d30ee4f5d 100644
--- a/chromium/third_party/blink/renderer/modules/accessibility/ax_menu_list_option.h
+++ b/chromium/third_party/blink/renderer/modules/accessibility/ax_menu_list_option.h
@@ -39,11 +39,15 @@ class AXMenuListOption final : public AXNodeObject {
AXMenuListOption(HTMLOptionElement*, AXObjectCacheImpl&);
~AXMenuListOption() override = default;
+ // For an <option>/<optgroup>, return an AXObject* for its popup, if any,
+ // otherwise return null.
+ static AXObject* ComputeParentAXMenuPopupFor(AXObjectCacheImpl& cache,
+ HTMLOptionElement* option);
+
private:
bool IsMenuListOption() const override { return true; }
bool CanHaveChildren() const override { return false; }
- AXObject* ComputeParentImpl() const override;
Element* ActionElement() const override;
bool IsVisible() const override;
@@ -63,7 +67,6 @@ class AXMenuListOption final : public AXNodeObject {
AXRelatedObjectVector*,
NameSources*) const override;
bool ComputeAccessibilityIsIgnored(IgnoredReasons* = nullptr) const override;
- HTMLSelectElement* ParentSelectNode() const;
DISALLOW_COPY_AND_ASSIGN(AXMenuListOption);
};
diff --git a/chromium/third_party/blink/renderer/modules/accessibility/ax_menu_list_popup.cc b/chromium/third_party/blink/renderer/modules/accessibility/ax_menu_list_popup.cc
index d01181a1736..10cd1b53341 100644
--- a/chromium/third_party/blink/renderer/modules/accessibility/ax_menu_list_popup.cc
+++ b/chromium/third_party/blink/renderer/modules/accessibility/ax_menu_list_popup.cc
@@ -67,8 +67,7 @@ bool AXMenuListPopup::ComputeAccessibilityIsIgnored(
AXMenuListOption* AXMenuListPopup::MenuListOptionAXObject(
HTMLElement* element) {
DCHECK(element);
- if (!IsA<HTMLOptionElement>(*element))
- return nullptr;
+ DCHECK(IsA<HTMLOptionElement>(*element));
AXObject* ax_object = AXObjectCache().GetOrCreate(element, this);
diff --git a/chromium/third_party/blink/renderer/modules/accessibility/ax_mock_object.cc b/chromium/third_party/blink/renderer/modules/accessibility/ax_mock_object.cc
index 28f7d5c3192..1b70b6d2366 100644
--- a/chromium/third_party/blink/renderer/modules/accessibility/ax_mock_object.cc
+++ b/chromium/third_party/blink/renderer/modules/accessibility/ax_mock_object.cc
@@ -43,12 +43,6 @@ Document* AXMockObject::GetDocument() const {
return ParentObject() ? ParentObject()->GetDocument() : nullptr;
}
-AXObject* AXMockObject::ComputeParentImpl() const {
- NOTREACHED()
- << "Mock objects are explicitly parented until their parent is detached";
- return nullptr;
-}
-
ax::mojom::blink::Role AXMockObject::NativeRoleIgnoringAria() const {
NOTREACHED();
return ax::mojom::blink::Role::kUnknown;
diff --git a/chromium/third_party/blink/renderer/modules/accessibility/ax_mock_object.h b/chromium/third_party/blink/renderer/modules/accessibility/ax_mock_object.h
index dce69a8f87c..a03d6af5eb6 100644
--- a/chromium/third_party/blink/renderer/modules/accessibility/ax_mock_object.h
+++ b/chromium/third_party/blink/renderer/modules/accessibility/ax_mock_object.h
@@ -50,7 +50,6 @@ class MODULES_EXPORT AXMockObject : public AXObject {
AXRestriction Restriction() const override { return kRestrictionNone; }
bool IsMockObject() const final { return true; }
Document* GetDocument() const override;
- AXObject* ComputeParentImpl() const override;
ax::mojom::blink::Role NativeRoleIgnoringAria() const override;
private:
diff --git a/chromium/third_party/blink/renderer/modules/accessibility/ax_node_object.cc b/chromium/third_party/blink/renderer/modules/accessibility/ax_node_object.cc
index 1e25b8ccf37..4455cbe96ab 100644
--- a/chromium/third_party/blink/renderer/modules/accessibility/ax_node_object.cc
+++ b/chromium/third_party/blink/renderer/modules/accessibility/ax_node_object.cc
@@ -33,7 +33,7 @@
#include <algorithm>
-#include "base/optional.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
#include "third_party/blink/public/common/input/web_keyboard_event.h"
#include "third_party/blink/public/strings/grit/blink_strings.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_image_bitmap_options.h"
@@ -48,11 +48,11 @@
#include "third_party/blink/renderer/core/dom/qualified_name.h"
#include "third_party/blink/renderer/core/dom/shadow_root.h"
#include "third_party/blink/renderer/core/dom/text.h"
-#include "third_party/blink/renderer/core/editing/editing_utilities.h"
#include "third_party/blink/renderer/core/editing/markers/document_marker_controller.h"
#include "third_party/blink/renderer/core/editing/position.h"
#include "third_party/blink/renderer/core/events/event_util.h"
#include "third_party/blink/renderer/core/events/keyboard_event.h"
+#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/local_frame_view.h"
#include "third_party/blink/renderer/core/frame/settings.h"
#include "third_party/blink/renderer/core/html/canvas/html_canvas_element.h"
@@ -114,7 +114,6 @@
#include "third_party/blink/renderer/modules/accessibility/ax_position.h"
#include "third_party/blink/renderer/modules/accessibility/ax_range.h"
#include "third_party/blink/renderer/modules/accessibility/ax_relation_cache.h"
-#include "third_party/blink/renderer/modules/media_controls/elements/media_control_elements_helper.h"
#include "third_party/blink/renderer/platform/graphics/image_data_buffer.h"
#include "third_party/blink/renderer/platform/keyboard_codes.h"
#include "third_party/blink/renderer/platform/text/platform_locale.h"
@@ -323,25 +322,11 @@ AXObject* AXNodeObject::ActiveDescendant() {
AXObjectInclusion AXNodeObject::ShouldIncludeBasedOnSemantics(
IgnoredReasons* ignored_reasons) const {
- // If this element is within a parent that cannot have children, it should not
- // be exposed.
- if (IsDescendantOfLeafNode()) {
- if (ignored_reasons)
- ignored_reasons->push_back(
- IgnoredReason(kAXAncestorIsLeafNode, LeafNodeAncestor()));
- return kIgnoreObject;
- }
+ DCHECK(GetDocument());
- if (HasInheritedPresentationalRole()) {
- if (ignored_reasons) {
- const AXObject* inherits_from = InheritsPresentationalRoleFrom();
- if (inherits_from == this) {
- ignored_reasons->push_back(IgnoredReason(kAXPresentational));
- } else {
- ignored_reasons->push_back(
- IgnoredReason(kAXInheritsPresentation, inherits_from));
- }
- }
+ if (IsPresentational()) {
+ if (ignored_reasons)
+ ignored_reasons->push_back(IgnoredReason(kAXPresentational));
return kIgnoreObject;
}
@@ -349,37 +334,23 @@ AXObjectInclusion AXNodeObject::ShouldIncludeBasedOnSemantics(
// their contents as the contents are not focusable (portals do not currently
// support input events). Portals do use their contents to compute a default
// accessible name.
- if (GetDocument() && GetDocument()->GetPage() &&
- GetDocument()->GetPage()->InsidePortal()) {
+ if (GetDocument()->GetPage() && GetDocument()->GetPage()->InsidePortal())
return kIgnoreObject;
+
+ Node* node = GetNode();
+ if (!node) {
+ // Nodeless pseudo element images are included, even if they don't have CSS
+ // alt text. This can allow auto alt to be applied to them.
+ if (IsImage())
+ return kIncludeObject;
+ return kDefaultBehavior;
}
if (IsTableLikeRole() || IsTableRowLikeRole() || IsTableCellLikeRole())
return kIncludeObject;
- // Ignore labels that are already referenced by a control but are not set to
- // be focusable.
- AXObject* control_ax_object = CorrespondingControlAXObjectForLabelElement();
- if (control_ax_object && control_ax_object->IsCheckboxOrRadio() &&
- control_ax_object->NameFromLabelElement() &&
- AccessibleNode::GetPropertyOrARIAAttribute(
- LabelElementContainer(), AOMStringProperty::kRole) == g_null_atom) {
- AXObject* label_ax_object = CorrespondingLabelAXObject();
- // If the label is set to be focusable, we should expose it.
- if (label_ax_object && label_ax_object->CanSetFocusAttribute())
- return kIncludeObject;
-
- if (ignored_reasons) {
- if (label_ax_object && label_ax_object != this)
- ignored_reasons->push_back(
- IgnoredReason(kAXLabelContainer, label_ax_object));
-
- ignored_reasons->push_back(IgnoredReason(kAXLabelFor, control_ax_object));
- }
- return kIgnoreObject;
- }
-
- if (GetNode() && !IsA<HTMLBodyElement>(GetNode()) && CanSetFocusAttribute())
+ // All focusable elements except the <body> are included.
+ if (!IsA<HTMLBodyElement>(node) && CanSetFocusAttribute())
return kIncludeObject;
if (IsLink() || IsInPageLinkTarget())
@@ -397,8 +368,8 @@ AXObjectInclusion AXNodeObject::ShouldIncludeBasedOnSemantics(
// Header and footer tags may also be exposed as landmark roles but not
// always.
- if (GetNode() && (GetNode()->HasTagName(html_names::kHeaderTag) ||
- GetNode()->HasTagName(html_names::kFooterTag)))
+ if (node->HasTagName(html_names::kHeaderTag) ||
+ node->HasTagName(html_names::kFooterTag))
return kIncludeObject;
// All controls are accessible.
@@ -412,15 +383,10 @@ AXObjectInclusion AXNodeObject::ShouldIncludeBasedOnSemantics(
// Anything with CSS alt should be included.
// Note: this is duplicated from AXLayoutObject because CSS alt text may apply
// to both Elements and pseudo-elements.
- base::Optional<String> alt_text = GetCSSAltText(GetNode());
+ absl::optional<String> alt_text = GetCSSAltText(GetNode());
if (alt_text && !alt_text->IsEmpty())
return kIncludeObject;
- // Don't ignore labels, because they serve as TitleUIElements.
- Node* node = GetNode();
- if (IsA<HTMLLabelElement>(node))
- return kIncludeObject;
-
// Don't ignored legends, because JAWS uses them to determine redundant text.
if (IsA<HTMLLegendElement>(node))
return kIncludeObject;
@@ -472,46 +438,54 @@ AXObjectInclusion AXNodeObject::ShouldIncludeBasedOnSemantics(
return kIgnoreObject;
}
- // If this element has aria attributes on it, it should not be ignored.
- if (HasGlobalARIAAttribute())
+ // Using the title or accessibility description (so we
+ // check if there's some kind of accessible name for the element)
+ // to decide an element's visibility is not as definitive as
+ // previous checks, so this should remain as one of the last.
+ if (HasAriaAttribute() || !GetAttribute(kTitleAttr).IsEmpty())
return kIncludeObject;
- bool has_non_empty_alt_attribute = !GetAttribute(kAltAttr).IsEmpty();
if (IsImage()) {
- if (has_non_empty_alt_attribute || GetAttribute(kAltAttr).IsNull())
+ String alt = GetAttribute(kAltAttr);
+ // A null alt attribute means the attribute is not present. We assume this
+ // is a mistake, and expose the image so that it can be repaired.
+ // In contrast, alt="" is treated as intentional markup to ignore the image.
+ if (!alt.IsEmpty() || alt.IsNull())
return kIncludeObject;
- else if (ignored_reasons)
+ if (ignored_reasons)
ignored_reasons->push_back(IgnoredReason(kAXEmptyAlt));
return kIgnoreObject;
}
- // Using the title or accessibility description (so we
- // check if there's some kind of accessible name for the element)
- // to decide an element's visibility is not as definitive as
- // previous checks, so this should remain as one of the last.
- //
- // These checks are simplified in the interest of execution speed;
- // for example, any element having an alt attribute will make it
- // not ignored, rather than just images.
- if (HasAriaAttribute() || !GetAttribute(kTitleAttr).IsEmpty() ||
- has_non_empty_alt_attribute)
- return kIncludeObject;
// <span> tags are inline tags and not meant to convey information if they
// have no other ARIA information on them. If we don't ignore them, they may
// emit signals expected to come from their parent.
- if (node && IsA<HTMLSpanElement>(node)) {
+ if (IsA<HTMLSpanElement>(node)) {
if (ignored_reasons)
ignored_reasons->push_back(IgnoredReason(kAXUninteresting));
return kIgnoreObject;
}
+ // Ignore labels that are already used to name a control.
+ // See IsRedundantLabel() for more commentary.
+ if (HTMLLabelElement* label = DynamicTo<HTMLLabelElement>(node)) {
+ if (IsRedundantLabel(label)) {
+ if (ignored_reasons) {
+ ignored_reasons->push_back(
+ IgnoredReason(kAXLabelFor, AXObjectCache().Get(label->control())));
+ }
+ return kIgnoreObject;
+ }
+ return kIncludeObject;
+ }
+
return kDefaultBehavior;
}
-base::Optional<String> AXNodeObject::GetCSSAltText(const Node* node) {
+absl::optional<String> AXNodeObject::GetCSSAltText(const Node* node) {
if (!node || !node->GetComputedStyle() ||
node->GetComputedStyle()->ContentBehavesAsNormal()) {
- return base::nullopt;
+ return absl::nullopt;
}
const ComputedStyle* style = node->GetComputedStyle();
@@ -521,7 +495,7 @@ base::Optional<String> AXNodeObject::GetCSSAltText(const Node* node) {
if (content_data->IsAltText())
return To<AltTextContentData>(content_data)->GetText();
}
- return base::nullopt;
+ return absl::nullopt;
}
// If the content property is used on a non-pseudo element, match the
@@ -533,7 +507,7 @@ base::Optional<String> AXNodeObject::GetCSSAltText(const Node* node) {
return To<AltTextContentData>(content_data->Next())->GetText();
}
- return base::nullopt;
+ return absl::nullopt;
}
bool AXNodeObject::ComputeAccessibilityIsIgnored(
@@ -593,69 +567,6 @@ bool AXNodeObject::ComputeAccessibilityIsIgnored(
return true;
}
-static bool IsListElement(Node* node) {
- return IsA<HTMLUListElement>(*node) || IsA<HTMLOListElement>(*node) ||
- IsA<HTMLDListElement>(*node);
-}
-
-static bool IsRequiredOwnedElement(AXObject* parent,
- ax::mojom::blink::Role current_role,
- HTMLElement* current_element) {
- Node* parent_node = parent->GetNode();
- auto* parent_html_element = DynamicTo<HTMLElement>(parent_node);
- if (!parent_html_element)
- return false;
-
- if (current_role == ax::mojom::blink::Role::kListItem)
- return IsListElement(parent_node);
- if (current_role == ax::mojom::blink::Role::kListMarker)
- return IsA<HTMLLIElement>(*parent_node);
-
- if (!current_element)
- return false;
- if (IsA<HTMLTableCellElement>(*current_element))
- return IsA<HTMLTableRowElement>(*parent_node);
- if (IsA<HTMLTableRowElement>(*current_element))
- return IsA<HTMLTableSectionElement>(parent_html_element);
-
- // In case of ListboxRole and its child, ListBoxOptionRole, inheritance of
- // presentation role is handled in AXListBoxOption because ListBoxOption Role
- // doesn't have any child.
- // If it's just ignored because of presentation, we can't see any AX tree
- // related to ListBoxOption.
- return false;
-}
-
-const AXObject* AXNodeObject::InheritsPresentationalRoleFrom() const {
- // ARIA states if an item can get focus, it should not be presentational.
- if (CanSetFocusAttribute())
- return nullptr;
-
- if (IsPresentational())
- return this;
-
- // http://www.w3.org/TR/wai-aria/complete#presentation
- // ARIA spec says that the user agent MUST apply an inherited role of
- // presentation to any owned elements that do not have an explicit role.
- if (AriaRoleAttribute() != ax::mojom::blink::Role::kUnknown)
- return nullptr;
-
- AXObject* parent = ParentObject();
- if (!parent)
- return nullptr;
-
- auto* element = DynamicTo<HTMLElement>(GetNode());
- if (!parent->HasInheritedPresentationalRole())
- return nullptr;
-
- // ARIA spec says that when a parent object is presentational and this object
- // is a required owned element of that parent, then this object is also
- // presentational.
- if (IsRequiredOwnedElement(parent, RoleValue(), element))
- return parent;
- return nullptr;
-}
-
// There should only be one banner/contentInfo per page. If header/footer are
// being used within an article, aside, nave, section, blockquote, details,
// fieldset, figure, td, or main, then it should not be exposed as whole
@@ -1033,8 +944,11 @@ ax::mojom::blink::Role AXNodeObject::NativeRoleIgnoringAria() const {
if (GetNode()->HasTagName(html_names::kPreTag))
return ax::mojom::blink::Role::kPre;
- if (GetNode()->HasTagName(html_names::kSectionTag))
- return ax::mojom::blink::Role::kSection;
+ if (GetNode()->HasTagName(html_names::kSectionTag)) {
+ // Treat a named <section> as role="region".
+ return IsNameFromAuthorAttribute() ? ax::mojom::blink::Role::kRegion
+ : ax::mojom::blink::Role::kSection;
+ }
if (GetNode()->HasTagName(html_names::kAddressTag))
return RoleFromLayoutObjectOrNode();
@@ -1131,50 +1045,6 @@ void AXNodeObject::AccessibilityChildrenFromAOMProperty(
}
}
-bool AXNodeObject::IsMultiline() const {
- Node* node = this->GetNode();
- if (!node)
- return false;
-
- const ax::mojom::blink::Role role = RoleValue();
- const bool is_edit_box = role == ax::mojom::blink::Role::kSearchBox ||
- role == ax::mojom::blink::Role::kTextField;
- if (!IsEditable() && !is_edit_box)
- return false; // Doesn't support multiline.
-
- // Supports aria-multiline, so check for attribute.
- bool is_multiline = false;
- if (HasAOMPropertyOrARIAAttribute(AOMBooleanProperty::kMultiline,
- is_multiline)) {
- return is_multiline;
- }
-
- // Default for <textarea> is true.
- if (IsA<HTMLTextAreaElement>(*node))
- return true;
-
- // Default for other edit boxes is false, including for ARIA, says CORE-AAM.
- if (is_edit_box)
- return false;
-
- // If root of contenteditable area and no ARIA role of textbox/searchbox used,
- // default to multiline=true which is what the default behavior is.
- return HasContentEditableAttributeSet();
-}
-
-// This only returns true if this is the element that actually has the
-// contentEditable attribute set, unlike node->hasEditableStyle() which will
-// also return true if an ancestor is editable.
-bool AXNodeObject::HasContentEditableAttributeSet() const {
- const AtomicString& content_editable_value =
- GetAttribute(html_names::kContenteditableAttr);
- if (content_editable_value.IsNull())
- return false;
- // Both "true" (case-insensitive) and the empty string count as true.
- return content_editable_value.IsEmpty() ||
- EqualIgnoringASCIICase(content_editable_value, "true");
-}
-
static Element* SiblingWithAriaRole(String role, Node* node) {
Node* parent = LayoutTreeBuilderTraversal::Parent(*node);
if (!parent)
@@ -1203,7 +1073,7 @@ Element* AXNodeObject::MenuItemElementForMenu() const {
}
Element* AXNodeObject::MouseButtonListener() const {
- Node* node = this->GetNode();
+ Node* node = GetNode();
if (!node)
return nullptr;
@@ -1223,28 +1093,29 @@ Element* AXNodeObject::MouseButtonListener() const {
return nullptr;
}
-void AXNodeObject::Init(AXObject* parent_if_known) {
+void AXNodeObject::Init(AXObject* parent) {
#if DCHECK_IS_ON()
DCHECK(!initialized_);
initialized_ = true;
#endif
- AXObject::Init(parent_if_known);
+ AXObject::Init(parent);
DCHECK(role_ == native_role_ || role_ == aria_role_)
<< "Role must be either the cached native role or cached aria role: "
<< "\n* Final role: " << role_ << "\n* Native role: " << native_role_
<< "\n* Aria role: " << aria_role_ << "\n* Node: " << GetNode();
- DCHECK(node_ ||
- (GetLayoutObject() &&
- AXObjectCacheImpl::IsPseudoElementDescendant(*GetLayoutObject())))
+ DCHECK(node_ || (GetLayoutObject() &&
+ AXObjectCacheImpl::IsRelevantPseudoElementDescendant(
+ *GetLayoutObject())))
<< "Nodeless AXNodeObject can only exist inside a pseudo element: "
<< GetLayoutObject();
}
void AXNodeObject::Detach() {
-#if DCHECK_IS_ON()
- DCHECK(!is_adding_children_) << "Cannot Detach |this| during AddChildren()";
+#if defined(AX_FAIL_FAST_BUILD)
+ SANITIZER_CHECK(!is_adding_children_)
+ << "Cannot detach |this| during AddChildren(): " << GetNode();
#endif
AXObject::Detach();
node_ = nullptr;
@@ -1255,7 +1126,7 @@ bool AXNodeObject::IsAXNodeObject() const {
}
bool AXNodeObject::IsControl() const {
- Node* node = this->GetNode();
+ Node* node = GetNode();
if (!node)
return false;
@@ -1264,15 +1135,6 @@ bool AXNodeObject::IsControl() const {
AXObject::IsARIAControl(AriaRoleAttribute()));
}
-bool AXNodeObject::IsControllingVideoElement() const {
- Node* node = this->GetNode();
- if (!node)
- return true;
-
- return IsA<HTMLVideoElement>(
- MediaControlElementsHelper::ToParentMediaElement(node));
-}
-
bool AXNodeObject::IsAutofillAvailable() const {
// Autofill state is stored in AXObjectCache.
WebAXAutofillState state = AXObjectCache().GetAutofillState(AXObjectID());
@@ -1294,27 +1156,12 @@ bool AXNodeObject::IsDefault() const {
return GetElement()->MatchesDefaultPseudoClass();
}
-bool AXNodeObject::ComputeIsEditableRoot() const {
- Node* node = GetNode();
- if (!node)
- return false;
- if (IsNativeTextField())
- return true;
- if (IsRootEditableElement(*node)) {
- // Editable roots created by the user agent are handled by
- // |IsNativeTextField| above.
- ShadowRoot* root = node->ContainingShadowRoot();
- return !root || !root->IsUserAgent();
- }
- return false;
-}
-
bool AXNodeObject::IsFieldset() const {
return IsA<HTMLFieldSetElement>(GetNode());
}
bool AXNodeObject::IsHovered() const {
- if (Node* node = this->GetNode())
+ if (Node* node = GetNode())
return node->IsHovered();
return false;
}
@@ -1384,7 +1231,7 @@ bool AXNodeObject::IsMultiSelectable() const {
}
bool AXNodeObject::IsNativeImage() const {
- Node* node = this->GetNode();
+ Node* node = GetNode();
if (!node)
return false;
@@ -1408,22 +1255,6 @@ bool AXNodeObject::IsProgressIndicator() const {
return RoleValue() == ax::mojom::blink::Role::kProgressIndicator;
}
-bool AXNodeObject::IsRichlyEditable() const {
- // This check is necessary to support the richlyEditable and editable states
- // in canvas fallback, for contenteditable elements.
- // TODO(accessiblity) Support on descendants of the fallback element that
- // has contenteditable set.
- return HasContentEditableAttributeSet();
-}
-
-bool AXNodeObject::IsEditable() const {
- if (IsNativeTextField())
- return true;
-
- // Support editable states in canvas fallback content.
- return AXNodeObject::IsRichlyEditable();
-}
-
bool AXNodeObject::IsSlider() const {
return RoleValue() == ax::mojom::blink::Role::kSlider;
}
@@ -1698,13 +1529,13 @@ bool AXNodeObject::IsRequired() const {
bool AXNodeObject::CanvasHasFallbackContent() const {
if (IsDetached())
return false;
- Node* node = this->GetNode();
+ Node* node = GetNode();
return IsA<HTMLCanvasElement>(node) && node->hasChildren();
}
int AXNodeObject::HeadingLevel() const {
// headings can be in block flow and non-block flow
- Node* node = this->GetNode();
+ Node* node = GetNode();
if (!node)
return 0;
@@ -1808,7 +1639,7 @@ String AXNodeObject::AutoComplete() const {
WebAXAutofillState::kAutocompleteAvailable)
return "list";
- if (IsNativeTextField() || IsARIATextField()) {
+ if (IsAtomicTextField() || IsARIATextField()) {
const AtomicString& aria_auto_complete =
GetAOMPropertyOrARIAAttribute(AOMStringProperty::kAutocomplete)
.LowerASCII();
@@ -1841,7 +1672,7 @@ void AXNodeObject::SerializeMarkerAttributes(ui::AXNodeData* node_data) const {
std::vector<int32_t> marker_ends;
// First use ARIA markers for spelling/grammar if available.
- base::Optional<DocumentMarker::MarkerType> aria_marker_type =
+ absl::optional<DocumentMarker::MarkerType> aria_marker_type =
GetAriaSpellingOrGrammarMarker();
if (aria_marker_type) {
AXRange range = AXRange::RangeOfContents(*this);
@@ -2164,13 +1995,13 @@ String AXNodeObject::ImageDataUrl(const IntSize& max_size) const {
ImageBitmap* image_bitmap = nullptr;
if (auto* image = DynamicTo<HTMLImageElement>(node)) {
image_bitmap = MakeGarbageCollected<ImageBitmap>(
- image, base::Optional<IntRect>(), options);
+ image, absl::optional<IntRect>(), options);
} else if (auto* canvas = DynamicTo<HTMLCanvasElement>(node)) {
image_bitmap = MakeGarbageCollected<ImageBitmap>(
- canvas, base::Optional<IntRect>(), options);
+ canvas, absl::optional<IntRect>(), options);
} else if (auto* video = DynamicTo<HTMLVideoElement>(node)) {
image_bitmap = MakeGarbageCollected<ImageBitmap>(
- video, base::Optional<IntRect>(), options);
+ video, absl::optional<IntRect>(), options);
}
if (!image_bitmap)
return String();
@@ -2654,7 +2485,7 @@ String AXNodeObject::GetValueForControl() const {
return select_element->InnerElement().innerText();
}
- if (IsNativeTextField()) {
+ if (IsAtomicTextField()) {
// This is an "<input type=text>" or a "<textarea>": We should not simply
// return the "value" attribute because it might be sanitized in some input
// control types, e.g. email fields. If we do that, then "selectionStart"
@@ -2739,7 +2570,7 @@ String AXNodeObject::GetValueForControl() const {
}
String AXNodeObject::SlowGetValueForControlIncludingContentEditable() const {
- if (IsNonNativeTextField()) {
+ if (IsNonAtomicTextField()) {
Element* element = GetElement();
return element ? element->GetInnerTextWithoutUpdate() : String();
}
@@ -2750,25 +2581,6 @@ ax::mojom::blink::Role AXNodeObject::AriaRoleAttribute() const {
return aria_role_;
}
-bool AXNodeObject::HasAriaAttribute() const {
- Element* element = GetElement();
- if (!element)
- return false;
-
- // Explicit ARIA role should be considered an aria attribute.
- if (AriaRoleAttribute() != ax::mojom::blink::Role::kUnknown)
- return true;
-
- AttributeCollection attributes = element->AttributesWithoutUpdate();
- for (const Attribute& attr : attributes) {
- // Attributes cache their uppercase names.
- if (attr.GetName().LocalNameUpper().StartsWith("ARIA-"))
- return true;
- }
-
- return false;
-}
-
void AXNodeObject::AriaDescribedbyElements(AXObjectVector& describedby) const {
AccessibilityChildrenFromAOMProperty(AOMRelationListProperty::kDescribedBy,
describedby);
@@ -2809,7 +2621,8 @@ void AXNodeObject::Dropeffects(
return;
Vector<String> str_dropeffects;
- TokenVectorFromAttribute(str_dropeffects, html_names::kAriaDropeffectAttr);
+ TokenVectorFromAttribute(GetElement(), str_dropeffects,
+ html_names::kAriaDropeffectAttr);
if (str_dropeffects.IsEmpty()) {
dropeffects.push_back(ax::mojom::blink::Dropeffect::kNone);
@@ -2850,8 +2663,9 @@ ax::mojom::blink::HasPopup AXNodeObject::HasPopup() const {
// ARIA 1.1 default value of haspopup for combobox is "listbox".
if (RoleValue() == ax::mojom::blink::Role::kComboBoxMenuButton ||
- RoleValue() == ax::mojom::blink::Role::kTextFieldWithComboBox)
+ RoleValue() == ax::mojom::blink::Role::kTextFieldWithComboBox) {
return ax::mojom::blink::HasPopup::kListbox;
+ }
if (AXObjectCache().GetAutofillState(AXObjectID()) !=
WebAXAutofillState::kNoSuggestions) {
@@ -2861,6 +2675,47 @@ ax::mojom::blink::HasPopup AXNodeObject::HasPopup() const {
return AXObject::HasPopup();
}
+bool AXNodeObject::IsEditableRoot() const {
+ const Node* node = GetNode();
+ if (IsDetached() || !node)
+ return false;
+#if DCHECK_IS_ON() // Required in order to get Lifecycle().ToString()
+ DCHECK(GetDocument());
+ DCHECK_GE(GetDocument()->Lifecycle().GetState(),
+ DocumentLifecycle::kStyleClean)
+ << "Unclean document style at lifecycle state "
+ << GetDocument()->Lifecycle().ToString();
+#endif // DCHECK_IS_ON()
+
+ // The DOM inside native text fields is an implementation detail that should
+ // not be exposed to platform accessibility APIs.
+ if (EnclosingTextControl(node))
+ return false;
+
+ if (IsRootEditableElement(*node))
+ return true;
+
+ // Catches the case where a contenteditable is inside another contenteditable.
+ // This is especially important when the two nested contenteditables have
+ // different attributes, e.g. "true" vs. "plaintext-only".
+ if (HasContentEditableAttributeSet())
+ return true;
+
+ return false;
+}
+
+bool AXNodeObject::HasContentEditableAttributeSet() const {
+ if (IsDetached() || !GetNode())
+ return false;
+
+ const auto* html_element = DynamicTo<HTMLElement>(GetNode());
+ if (!html_element)
+ return false;
+
+ String normalized_value = html_element->contentEditable();
+ return normalized_value == "true" || normalized_value == "plaintext-only";
+}
+
// Returns the nearest block-level LayoutBlockFlow ancestor
static LayoutBlockFlow* NonInlineBlockFlow(LayoutObject* object) {
LayoutObject* current = object;
@@ -3125,8 +2980,8 @@ static bool ShouldInsertSpaceBetweenObjectsIfNeeded(
String AXNodeObject::TextFromDescendants(AXObjectSet& visited,
bool recursive) const {
- if (!CanHaveChildren() && recursive)
- return String();
+ if (!CanHaveChildren())
+ return recursive ? String() : GetElement()->innerText();
StringBuilder accumulated_text;
AXObject* previous = nullptr;
@@ -3159,26 +3014,21 @@ String AXNodeObject::TextFromDescendants(AXObjectSet& visited,
constexpr size_t kMaxDescendantsForTextAlternativeComputation = 100;
if (visited.size() > kMaxDescendantsForTextAlternativeComputation + 1)
break; // Need to add 1 because the root naming node is in the list.
- // If a child is a continuation, we should ignore attributes like
- // hidden and presentational. See LAYOUT TREE WALKING ALGORITHM in
- // ax_layout_object.cc for more information on continuations.
- bool is_continuation = child->GetLayoutObject() &&
- child->GetLayoutObject()->IsElementContinuation();
// Don't recurse into children that are explicitly hidden.
// Note that we don't call IsInertOrAriaHidden because that would return
// true if any ancestor is hidden, but we need to be able to compute the
// accessible name of object inside hidden subtrees (for example, if
// aria-labelledby points to an object that's hidden).
- if (!is_continuation &&
- (child->AOMPropertyOrARIAAttributeIsTrue(AOMBooleanProperty::kHidden) ||
- child->IsHiddenForTextAlternativeCalculation()))
+ if (child->AOMPropertyOrARIAAttributeIsTrue(AOMBooleanProperty::kHidden) ||
+ child->IsHiddenForTextAlternativeCalculation()) {
continue;
+ }
ax::mojom::blink::NameFrom child_name_from =
ax::mojom::blink::NameFrom::kUninitialized;
String result;
- if (!is_continuation && child->IsPresentational()) {
+ if (child->IsPresentational()) {
result = child->TextFromDescendants(visited, true);
} else {
result =
@@ -3214,48 +3064,65 @@ String AXNodeObject::TextFromDescendants(AXObjectSet& visited,
return accumulated_text.ToString();
}
-bool AXNodeObject::NameFromLabelElement() const {
- // This unfortunately duplicates a bit of logic from textAlternative and
- // nativeTextAlternative, but it's necessary because nameFromLabelElement
- // needs to be called from computeAccessibilityIsIgnored, which isn't allowed
- // to call axObjectCache->getOrCreate.
+// static
+bool AXNodeObject::IsNameFromLabelElement(HTMLElement* control) {
+ // This duplicates some logic from TextAlternative()/NativeTextAlternative(),
+ // but is necessary because IsNameFromLabelElement() needs to be called from
+ // ComputeAccessibilityIsIgnored(), which isn't allowed to call
+ // AXObjectCache().GetOrCreate() in random places in the tree.
- if (!GetNode() && !GetLayoutObject())
+ if (!control)
return false;
- // Step 2A from: http://www.w3.org/TR/accname-aam-1.1
- if (IsHiddenForTextAlternativeCalculation())
+ // aria-label and aria-labelledby take precedence over <label>.
+ if (IsNameFromAriaAttribute(control))
return false;
- // Step 2B from: http://www.w3.org/TR/accname-aam-1.1
- // Try both spellings, but prefer aria-labelledby, which is the official spec.
- const QualifiedName& attr =
- HasAttribute(html_names::kAriaLabeledbyAttr) &&
- !HasAttribute(html_names::kAriaLabelledbyAttr)
- ? html_names::kAriaLabeledbyAttr
- : html_names::kAriaLabelledbyAttr;
- HeapVector<Member<Element>> elements_from_attribute;
- Vector<String> ids;
- ElementsFromAttribute(elements_from_attribute, attr, ids);
- if (elements_from_attribute.size() > 0)
+ // <label> will be used. It contains the control or points via <label for>.
+ // Based on https://www.w3.org/TR/html-aam-1.0
+ // 5.1/5.5 Text inputs, Other labelable Elements
+ auto* labels = control->labels();
+ return labels && labels->length();
+}
+
+// static
+bool AXNodeObject::IsRedundantLabel(HTMLLabelElement* label) {
+ // Determine if label is redundant:
+ // - Labelling a checkbox or radio.
+ // - Text was already used to name the checkbox/radio.
+ // - No interesting content in the label (focusable or semantically useful)
+ // TODO(accessibility) Consider moving this logic to the browser side.
+ // TODO(accessibility) Consider this for more controls, such as textboxes.
+ // There isn't a clear history why this is only done for checkboxes, and not
+ // other controls such as textboxes. It may be because the checkbox/radio
+ // itself is small, so this makes a nicely sized combined click target. Most
+ // ATs do not already have features to combine labels and controls, e.g.
+ // removing redundant announcements caused by having text and named controls
+ // as separate objects.
+ HTMLInputElement* input = DynamicTo<HTMLInputElement>(label->control());
+ if (!input)
return false;
- // Step 2C from: http://www.w3.org/TR/accname-aam-1.1
- const AtomicString& aria_label =
- GetAOMPropertyOrARIAAttribute(AOMStringProperty::kLabel);
- if (!aria_label.IsEmpty())
+ if (!input->IsCheckable())
return false;
- // Based on
- // http://rawgit.com/w3c/aria/master/html-aam/html-aam.html#accessible-name-and-description-calculation
- // 5.1/5.5 Text inputs, Other labelable Elements
- auto* html_element = DynamicTo<HTMLElement>(GetNode());
- if (html_element && html_element->IsLabelable()) {
- if (html_element->labels() && html_element->labels()->length() > 0)
- return true;
- }
+ if (!IsNameFromLabelElement(input))
+ return false;
- return false;
+ DCHECK_NE(input->labels()->length(), 0U);
+
+ // Look for any first child element that is not the input control itself.
+ // This could be important semantically.
+ Element* first_child = ElementTraversal::FirstChild(*label);
+ if (!first_child)
+ return true; // No element children.
+
+ if (first_child != input)
+ return false; // Has an element child that is not the input control.
+
+ // The first child was the input control.
+ // If there's another child, then it won't be the input control.
+ return ElementTraversal::NextSibling(*first_child) == nullptr;
}
void AXNodeObject::GetRelativeBounds(AXObject** out_container,
@@ -3477,19 +3344,31 @@ int AXNodeObject::TextOffsetInFormattingContext(int offset) const {
// Inline text boxes.
//
-void AXNodeObject::LoadInlineTextBoxes() {
- if (!GetLayoutObject())
- return;
-
- if (GetLayoutObject()->IsText()) {
- ClearChildren();
- AddInlineTextBoxChildren(true);
- children_dirty_ = false; // Avoid adding these children twice.
+void AXNodeObject::LoadInlineTextBoxesRecursive() {
+ if (ui::CanHaveInlineTextBoxChildren(RoleValue())) {
+ if (children_.IsEmpty()) {
+ // We only need to add inline textbox children if they aren't present.
+ // Although some platforms (e.g. Android), load inline text boxes
+ // on subtrees that may later be stale, once they are stale, the old
+ // inline text boxes are cleared because SetNeedsToUpdateChildren()
+ // calls ClearChildren().
+ AddInlineTextBoxChildren(/*force*/ true);
+ children_dirty_ = false; // Avoid adding these children twice.
+ }
return;
}
for (const auto& child : ChildrenIncludingIgnored()) {
- child->LoadInlineTextBoxes();
+ // TODO(https://crbug.com/1200244) Downgrade these to DCHECKs.
+ CHECK(child) << "Child has been destroyed before attempted use, parent is: "
+ << ToString(true, true);
+ CHECK(!child->IsDetached())
+ << "Child has been detached before attempted use, parent is: "
+ << ToString(true, true);
+ CHECK(!IsDetached()) << "Parent was detached while attempting to load "
+ "child text boxes, parent is: "
+ << ToString(true, true);
+ child->LoadInlineTextBoxesRecursive();
}
}
@@ -3605,11 +3484,12 @@ void AXNodeObject::AddImageMapChildren() {
// parent that its children have changed.
if (AXObject* ax_preexisting = AXObjectCache().Get(first_area)) {
if (AXObject* ax_previous_parent = ax_preexisting->CachedParentObject()) {
- DCHECK_NE(ax_previous_parent, this);
- DCHECK(ax_previous_parent->GetNode());
- AXObjectCache().ChildrenChangedWithCleanLayout(
- ax_previous_parent->GetNode(), ax_previous_parent);
+ if (ax_previous_parent != this) {
+ DCHECK(ax_previous_parent->GetNode());
+ AXObjectCache().ChildrenChangedWithCleanLayout(
+ ax_previous_parent->GetNode(), ax_previous_parent);
ax_previous_parent->ClearChildren();
+ }
}
}
@@ -3672,14 +3552,20 @@ bool AXNodeObject::CanAddLayoutChild(LayoutObject& child) {
void AXNodeObject::AddLayoutChildren() {
// Children are added this way only for pseudo-element subtrees.
// See AXObject::ShouldUseLayoutObjectTraversalForChildren().
- DCHECK(GetLayoutObject());
+ if (!GetLayoutObject()) {
+ DCHECK(GetNode());
+ DCHECK(GetNode()->IsPseudoElement());
+ return; // Can't add children for hidden or display-locked pseudo elements.
+ }
LayoutObject* child = GetLayoutObject()->SlowFirstChild();
while (child) {
- DCHECK(AXObjectCacheImpl::IsPseudoElementDescendant(*child));
if (CanAddLayoutChild(*child)) {
CHECK_NO_OTHER_PARENT_FOR(child);
// All added pseudo element desecendants are included in the tree.
- AddChildAndCheckIncluded(AXObjectCache().GetOrCreate(child, this));
+ if (AXObject* ax_child = AXObjectCache().GetOrCreate(child, this)) {
+ DCHECK(AXObjectCacheImpl::IsRelevantPseudoElementDescendant(*child));
+ AddChildAndCheckIncluded(ax_child);
+ }
}
child = child->NextSibling();
}
@@ -3715,13 +3601,13 @@ void AXNodeObject::AddOwnedChildren() {
AXObjectCache().GetAriaOwnedChildren(this, owned_children);
DCHECK(owned_children.size() == 0 || AXRelationCache::IsValidOwner(this))
- << "This object is not allowed to use aria-owns, but is: "
+ << "This object is not allowed to use aria-owns, but it is.\n"
<< ToString(true, true);
// Always include owned children.
for (const auto& owned_child : owned_children) {
DCHECK(AXRelationCache::IsValidOwnedChild(owned_child))
- << "This object is not allowed to be owned, but is: "
+ << "This object is not allowed to be owned, but it is.\n"
<< owned_child->ToString(true, true);
AddChildAndCheckIncluded(owned_child, true);
}
@@ -3738,7 +3624,7 @@ void AXNodeObject::AddChildrenImpl() {
if (!CanHaveChildren()) {
NOTREACHED()
- << "Should not reach AddChildren() if CanHaveChildren() is false "
+ << "Should not reach AddChildren() if CanHaveChildren() is false.\n"
<< ToString(true, true);
return;
}
@@ -3782,27 +3668,31 @@ void AXNodeObject::AddChildrenImpl() {
void AXNodeObject::AddChildren() {
#if DCHECK_IS_ON()
DCHECK(!IsDetached());
- DCHECK(!is_adding_children_) << " Reentering method on " << GetNode();
- base::AutoReset<bool> reentrancy_protector(&is_adding_children_, true);
// If the need to add more children in addition to existing children arises,
// childrenChanged should have been called, which leads to children_dirty_
// being true, then UpdateChildrenIfNecessary() clears the children before
// calling AddChildren().
- DCHECK_EQ(children_.size(), 0U)
+ DCHECK(children_.IsEmpty())
<< "\nParent still has " << children_.size() << " children before adding:"
<< "\nParent is " << ToString(true, true) << "\nFirst child is "
<< children_[0]->ToString(true, true);
#endif
+#if defined(AX_FAIL_FAST_BUILD)
+ SANITIZER_CHECK(!is_adding_children_)
+ << " Reentering method on " << GetNode();
+ base::AutoReset<bool> reentrancy_protector(&is_adding_children_, true);
+#endif
+
AddChildrenImpl();
children_dirty_ = false;
#if DCHECK_IS_ON()
// All added children must be attached.
for (const auto& child : children_) {
- DCHECK(!child->IsDetached())
- << "A brand new child was detached: " << child->ToString(true, true)
- << "\n ... of parent " << ToString(true, true);
+ DCHECK(!child->IsDetached()) << "A brand new child was detached.\n"
+ << child->ToString(true, true)
+ << "\n ... of parent " << ToString(true, true);
}
#endif
}
@@ -3846,14 +3736,14 @@ void AXNodeObject::AddNodeChild(Node* node) {
#if DCHECK_IS_ON()
void AXNodeObject::CheckValidChild(AXObject* child) {
- DCHECK(!child->IsDetached())
- << "Cannot add a detached child: " << child->ToString(true, true);
+ DCHECK(!child->IsDetached()) << "Cannot add a detached child.\n"
+ << child->ToString(true, true);
Node* child_node = child->GetNode();
// An HTML image can only have area children.
DCHECK(!IsA<HTMLImageElement>(GetNode()) || IsA<HTMLAreaElement>(child_node))
- << "Image elements can only have area children, had "
+ << "Image elements can only have area children, but this one has:\n"
<< child->ToString(true, true);
// <area> children should only be added via AddImageMapChildren(), as the
@@ -3941,9 +3831,6 @@ void AXNodeObject::InsertChild(AXObject* child,
<< "Owned elements must be in tree: " << child->ToString(true, true)
<< "\nRecompute included in tree: "
<< child->ComputeAccessibilityIsIgnoredButIncludedInTree();
- // Child is ignored and not in the tree.
- // Recompute the child's children now as we skip over the ignored object.
- child->SetNeedsToUpdateChildren();
// Get the ignored child's children and add to children of ancestor
// included in tree. This will recurse if necessary, skipping levels of
@@ -3952,13 +3839,16 @@ void AXNodeObject::InsertChild(AXObject* child,
wtf_size_t length = children.size();
int new_index = index;
for (wtf_size_t i = 0; i < length; ++i) {
+ if (children[i]->IsDetached()) {
+ CHECK(false) << "Cannot add a detached child: "
+ << "\n* Child: " << children[i]->ToString(true, true)
+ << "\n* Parent: " << child->ToString(true, true)
+ << "\n* Grandparent: " << ToString(true, true);
+ }
// If the child was owned, it will be added elsewhere as a direct
// child of the object owning it.
- if (!AXObjectCache().IsAriaOwned(children[i])) {
- DCHECK(!children[i]->IsDetached()) << "Cannot add a detached child: "
- << children[i]->ToString(true, true);
+ if (!AXObjectCache().IsAriaOwned(children[i]))
children_.insert(new_index++, children[i]);
- }
}
} else {
children_.insert(index, child);
@@ -3966,12 +3856,7 @@ void AXNodeObject::InsertChild(AXObject* child,
}
bool AXNodeObject::CanHaveChildren() const {
- // If this is an AXLayoutObject, then it's okay if this object
- // doesn't have a node - there are some layoutObjects that don't have
- // associated nodes, like scroll areas and css-generated text.
- if (!GetNode() && !IsAXLayoutObject())
- return false;
-
+ DCHECK(!IsDetached());
DCHECK(!IsA<HTMLMapElement>(GetNode()));
// Placeholder gets exposed as an attribute on the input accessibility node,
@@ -3983,6 +3868,18 @@ bool AXNodeObject::CanHaveChildren() const {
return false;
}
+ if (IsA<HTMLBRElement>(GetNode()) &&
+ (!GetLayoutObject() || !GetLayoutObject()->IsBR())) {
+ // A <br> element that is not treated as a line break could occur when the
+ // <br> element has DOM children. A <br> does not usually have DOM children,
+ // but there is nothing preventing a script from creating this situation.
+ // This anomalous child content is not rendered, and therefore AXObjects
+ // should not be created for the children. Enforcing that <br>s to only have
+ // children when they are line breaks also helps create consistency: any AX
+ // child of a <br> will always be an AXInlineTextBox.
+ return false;
+ }
+
switch (native_role_) {
case ax::mojom::blink::Role::kCheckBox:
case ax::mojom::blink::Role::kListBoxOption:
@@ -3993,19 +3890,19 @@ bool AXNodeObject::CanHaveChildren() const {
case ax::mojom::blink::Role::kProgressIndicator:
case ax::mojom::blink::Role::kRadioButton:
case ax::mojom::blink::Role::kScrollBar:
- // case ax::mojom::blink::Role::kSearchBox:
case ax::mojom::blink::Role::kSlider:
case ax::mojom::blink::Role::kSplitter:
case ax::mojom::blink::Role::kSwitch:
case ax::mojom::blink::Role::kTab:
- // case ax::mojom::blink::Role::kTextField:
case ax::mojom::blink::Role::kToggleButton:
return false;
case ax::mojom::blink::Role::kPopUpButton:
return true;
case ax::mojom::blink::Role::kLineBreak:
case ax::mojom::blink::Role::kStaticText:
- return AXObjectCache().InlineTextBoxAccessibilityEnabled();
+ // Note: these can have AXInlineTextBox children, but when adding them, we
+ // also check AXObjectCache().InlineTextBoxAccessibilityEnabled().
+ return true;
case ax::mojom::blink::Role::kImage:
// Can turn into an image map if gains children later.
return GetNode() && GetNode()->IsLink();
@@ -4013,13 +3910,11 @@ bool AXNodeObject::CanHaveChildren() const {
break;
}
- // Allow plain text controls to expose any children they might have, complying
+ // Allow native text fields to expose any children they might have, complying
// with browser-side expectations that editable controls have children
// containing the actual text content.
- if (blink::EnclosingTextControl(GetNode()) ||
- GetAttribute(html_names::kContenteditableAttr) == "plaintext-only") {
+ if (blink::EnclosingTextControl(GetNode()))
return true;
- }
switch (AriaRoleAttribute()) {
case ax::mojom::blink::Role::kImage:
@@ -4048,8 +3943,13 @@ bool AXNodeObject::CanHaveChildren() const {
// otherwise the subtree is exposed. The ChildrenPresentational rule
// is thus useful for authoring/verification tools but does not break
// complex widget implementations.
+ // Similarly, when content is inside a contenteditable, it does not make
+ // sense to hide it, since the user can interact with it.
+ // TODO(accessibility) Does it make sense to hide any of this content even
+ // in non-editable content?
Element* element = GetElement();
- return element && !element->HasOneTextChild();
+ return element &&
+ (HasEditableStyle(*element) || !element->HasOneTextChild());
}
default:
break;
@@ -4078,7 +3978,7 @@ double AXNodeObject::EstimatedLoadingProgress() const {
//
Element* AXNodeObject::ActionElement() const {
- Node* node = this->GetNode();
+ Node* node = GetNode();
if (!node)
return nullptr;
@@ -4094,7 +3994,7 @@ Element* AXNodeObject::ActionElement() const {
}
Element* AXNodeObject::AnchorElement() const {
- Node* node = this->GetNode();
+ Node* node = GetNode();
if (!node)
return nullptr;
@@ -4123,6 +4023,19 @@ Document* AXNodeObject::GetDocument() const {
return &GetNode()->GetDocument();
}
+Node* AXNodeObject::GetNode() const {
+ if (IsDetached()) {
+ DCHECK(!node_);
+ return nullptr;
+ }
+
+ DCHECK(!GetLayoutObject() || GetLayoutObject()->GetNode() == node_)
+ << "If there is an associated layout object, its node should match the "
+ "associated node of this accessibility object.\n"
+ << ToString(true, true);
+ return node_;
+}
+
// TODO(chrishall): consider merging this with AXObject::Language in followup.
AtomicString AXNodeObject::Language() const {
if (!GetNode())
@@ -4186,48 +4099,6 @@ const AtomicString& AXNodeObject::GetInternalsAttribute(
return element.EnsureElementInternals().FastGetAttribute(attribute);
}
-AXObject* AXNodeObject::CorrespondingControlAXObjectForLabelElement() const {
- HTMLLabelElement* label_element = LabelElementContainer();
- if (!label_element)
- return nullptr;
-
- HTMLElement* corresponding_control = label_element->control();
- if (!corresponding_control)
- return nullptr;
-
- // Make sure the corresponding control isn't a descendant of this label
- // that's in the middle of being destroyed.
- if (corresponding_control->GetLayoutObject() &&
- !corresponding_control->GetLayoutObject()->Parent())
- return nullptr;
-
- return AXObjectCache().GetOrCreate(corresponding_control);
-}
-
-AXObject* AXNodeObject::CorrespondingLabelAXObject() const {
- HTMLLabelElement* label_element = LabelElementContainer();
- if (!label_element)
- return nullptr;
-
- return AXObjectCache().GetOrCreate(label_element);
-}
-
-HTMLLabelElement* AXNodeObject::LabelElementContainer() const {
- if (!GetNode())
- return nullptr;
-
- // the control element should not be considered part of the label
- if (IsControl())
- return nullptr;
-
- // the link element should not be considered part of the label
- if (IsLink())
- return nullptr;
-
- // find if this has a ancestor that is a label
- return Traversal<HTMLLabelElement>::FirstAncestorOrSelf(*GetNode());
-}
-
bool AXNodeObject::OnNativeFocusAction() {
// Checking if node is focusable in a native focus action requires that we
// have updated style and layout tree, since the focus check relies on the
@@ -4321,11 +4192,11 @@ bool AXNodeObject::OnNativeSetSequentialFocusNavigationStartingPointAction() {
return true;
}
-void AXNodeObject::ChildrenChanged() {
+void AXNodeObject::ChildrenChangedWithCleanLayout() {
if (!GetNode() && !GetLayoutObject())
return;
- DCHECK(!IsDetached()) << "Avoid ChildrenChanged() on detached node: "
+ DCHECK(!IsDetached()) << "Don't call on detached node: "
<< ToString(true, true);
// When children changed on a <map> that means we need to forward the
@@ -4337,45 +4208,37 @@ void AXNodeObject::ChildrenChanged() {
if (image_element) {
AXObject* ax_image = AXObjectCache().Get(image_element);
if (ax_image) {
- ax_image->ChildrenChanged();
+ ax_image->ChildrenChangedWithCleanLayout();
return;
}
}
}
- // Always update current object, in case it wasn't included in the tree but
- // now is. In that case, the LastKnownIsIncludedInTreeValue() won't have been
- // updated yet, so we can't use that. Unfortunately, this is not a safe time
- // to get the current included in tree value, therefore, we'll play it safe
- // and update the children in two places sometimes.
+ // Always invalidate |children_| even if it was invalidated before, because
+ // now layout is clean.
SetNeedsToUpdateChildren();
- // If this node is not in the tree, update the children of the first ancesor
- // that is included in the tree.
+ // If this object is not included in the tree, then our parent needs to
+ // recompute its included-in-the-tree children vector. (And if our parent
+ // isn't included in the tree either, it will recursively update its parent
+ // and so on.)
+ //
+ // The first ancestor that's included in the tree will
+ // be the one that actually fires the ChildrenChanged
+ // event notification.
if (!LastKnownIsIncludedInTreeValue()) {
- // The first object (this or ancestor) that is included in the tree is the
- // one whose children may have changed.
- // Can be null, e.g. if <title> contents change
- if (AXObject* node_to_update = ParentObjectIncludedInTree())
- node_to_update->SetNeedsToUpdateChildren();
- }
-
- // If this node's children are not part of the accessibility tree then
- // skip notification and walking up the ancestors.
- // Cases where this happens:
- // - an ancestor has only presentational children, or
- // - this or an ancestor is a leaf node
- // Uses |cached_is_descendant_of_leaf_node_| to avoid updating cached
- // attributes for eachc change via | UpdateCachedAttributeValuesIfNeeded()|.
- if (!CanHaveChildren() || LastKnownIsDescendantOfLeafNode())
- return;
+ if (AXObject* ax_parent = CachedParentObject()) {
+ ax_parent->ChildrenChangedWithCleanLayout();
+ return;
+ }
+ }
- // TODO(aleventhal) Consider removing.
- if (IsDetached()) {
- NOTREACHED() << "None of the above calls should be able to detach |this|: "
- << ToString(true, true);
+ // TODO(accessibility) Move this up.
+ if (!CanHaveChildren())
return;
- }
+
+ DCHECK(!IsDetached()) << "None of the above should be able to detach |this|: "
+ << ToString(true, true);
AXObjectCache().PostNotification(this,
ax::mojom::blink::Event::kChildrenChanged);
@@ -4431,7 +4294,7 @@ void AXNodeObject::SelectionChanged() {
void AXNodeObject::HandleAriaExpandedChanged() {
// Find if a parent of this object should handle aria-expanded changes.
- AXObject* container_parent = this->ParentObject();
+ AXObject* container_parent = ParentObject();
while (container_parent) {
bool found_parent = false;
@@ -4993,7 +4856,7 @@ String AXNodeObject::NativeTextAlternative(
// Document.
if (IsWebArea()) {
- Document* document = this->GetDocument();
+ Document* document = GetDocument();
if (document) {
name_from = ax::mojom::blink::NameFrom::kAttribute;
if (name_sources) {
@@ -5029,30 +4892,13 @@ String AXNodeObject::NativeTextAlternative(
if (name_sources) {
name_sources->push_back(NameSource(*found_text_alternative));
- name_sources->back().type = name_from;
- name_sources->back().native_source = kAXTextFromNativeHTMLTitleElement;
- }
-
- Element* title_element = document->TitleElement();
- AXObject* title_ax_object = AXObjectCache().GetOrCreate(
- title_element, AXObjectCache().Get(document));
- if (title_ax_object) {
- if (related_objects) {
- local_related_objects.push_back(
- MakeGarbageCollected<NameSourceRelatedObject>(title_ax_object,
- text_alternative));
- *related_objects = local_related_objects;
- local_related_objects.clear();
- }
-
- if (name_sources) {
- NameSource& source = name_sources->back();
- source.related_objects = *related_objects;
- source.text = text_alternative;
- *found_text_alternative = true;
- } else {
- return text_alternative;
- }
+ NameSource& source = name_sources->back();
+ source.type = name_from;
+ source.native_source = kAXTextFromNativeHTMLTitleElement;
+ source.text = text_alternative;
+ *found_text_alternative = true;
+ } else {
+ return text_alternative;
}
}
}
@@ -5138,9 +4984,8 @@ String AXNodeObject::Description(
Vector<String> ids;
HeapVector<Member<Element>> elements_from_attribute;
- ElementsFromAttribute(elements_from_attribute,
- html_names::kAriaDescribedbyAttr, ids);
- if (!elements_from_attribute.IsEmpty()) {
+ if (ElementsFromAttribute(element, elements_from_attribute,
+ html_names::kAriaDescribedbyAttr, ids)) {
// TODO(meredithl): Determine description sources when |aria_describedby| is
// the empty string, in order to make devtools work with attr-associated
// elements.
@@ -5155,7 +5000,7 @@ String AXNodeObject::Description(
for (auto& element : elements_from_attribute)
ids.push_back(element->GetIdAttribute());
- TokenVectorFromAttribute(ids, html_names::kAriaDescribedbyAttr);
+ TokenVectorFromAttribute(element, ids, html_names::kAriaDescribedbyAttr);
AXObjectCache().UpdateReverseRelations(this, ids);
if (!description.IsNull()) {
@@ -5178,7 +5023,7 @@ String AXNodeObject::Description(
const AtomicString& aria_desc =
GetAOMPropertyOrARIAAttribute(AOMStringProperty::kDescription);
if (!aria_desc.IsNull()) {
- description_from = ax::mojom::blink::DescriptionFrom::kAttribute;
+ description_from = ax::mojom::blink::DescriptionFrom::kAriaDescription;
description = aria_desc;
if (description_sources) {
found_description = true;
@@ -5190,10 +5035,10 @@ String AXNodeObject::Description(
const auto* input_element = DynamicTo<HTMLInputElement>(GetNode());
- // value, 5.2.2 from: http://rawgit.com/w3c/aria/master/html-aam/html-aam.html
+ // value, 5.2.2 from: https://www.w3.org/TR/html-aam-1.0/
if (name_from != ax::mojom::blink::NameFrom::kValue && input_element &&
input_element->IsTextButton()) {
- description_from = ax::mojom::blink::DescriptionFrom::kAttribute;
+ description_from = ax::mojom::blink::DescriptionFrom::kButtonLabel;
if (description_sources) {
description_sources->push_back(
DescriptionSource(found_description, kValueAttr));
@@ -5213,7 +5058,7 @@ String AXNodeObject::Description(
}
if (RoleValue() == ax::mojom::blink::Role::kRuby) {
- description_from = ax::mojom::blink::DescriptionFrom::kRelatedElement;
+ description_from = ax::mojom::blink::DescriptionFrom::kRubyAnnotation;
if (description_sources) {
description_sources->push_back(DescriptionSource(found_description));
description_sources->back().type = description_from;
@@ -5249,11 +5094,10 @@ String AXNodeObject::Description(
}
}
- // table caption, 5.9.2 from:
- // http://rawgit.com/w3c/aria/master/html-aam/html-aam.html
+ // table caption, 5.9.2 from: https://www.w3.org/TR/html-aam-1.0/
auto* table_element = DynamicTo<HTMLTableElement>(element);
if (name_from != ax::mojom::blink::NameFrom::kCaption && table_element) {
- description_from = ax::mojom::blink::DescriptionFrom::kRelatedElement;
+ description_from = ax::mojom::blink::DescriptionFrom::kTableCaption;
if (description_sources) {
description_sources->push_back(DescriptionSource(found_description));
description_sources->back().type = description_from;
@@ -5285,11 +5129,10 @@ String AXNodeObject::Description(
}
}
- // summary, 5.6.2 from:
- // http://rawgit.com/w3c/aria/master/html-aam/html-aam.html
+ // summary, 5.8.2 from: https://www.w3.org/TR/html-aam-1.0/
if (name_from != ax::mojom::blink::NameFrom::kContents &&
IsA<HTMLSummaryElement>(GetNode())) {
- description_from = ax::mojom::blink::DescriptionFrom::kContents;
+ description_from = ax::mojom::blink::DescriptionFrom::kSummary;
if (description_sources) {
description_sources->push_back(DescriptionSource(found_description));
description_sources->back().type = description_from;
@@ -5308,8 +5151,7 @@ String AXNodeObject::Description(
}
}
- // title attribute, from:
- // http://rawgit.com/w3c/aria/master/html-aam/html-aam.html
+ // title attribute, from: https://www.w3.org/TR/html-aam-1.0/
if (name_from != ax::mojom::blink::NameFrom::kTitle) {
description_from = ax::mojom::blink::DescriptionFrom::kTitle;
if (description_sources) {
@@ -5329,9 +5171,14 @@ String AXNodeObject::Description(
}
}
- description_from = ax::mojom::blink::DescriptionFrom::kUninitialized;
+ description_from = ax::mojom::blink::DescriptionFrom::kNone;
if (found_description) {
+ DCHECK(description_sources)
+ << "Should only reach here if description_sources are tracked";
+ // Use the first non-null description.
+ // TODO(accessibility) Why do we need to check superceded if that will
+ // always be the first one?
for (DescriptionSource& description_source : *description_sources) {
if (!description_source.text.IsNull() && !description_source.superseded) {
description_from = description_source.type;
diff --git a/chromium/third_party/blink/renderer/modules/accessibility/ax_node_object.h b/chromium/third_party/blink/renderer/modules/accessibility/ax_node_object.h
index 7d1154bffea..48ef377b405 100644
--- a/chromium/third_party/blink/renderer/modules/accessibility/ax_node_object.h
+++ b/chromium/third_party/blink/renderer/modules/accessibility/ax_node_object.h
@@ -29,6 +29,7 @@
#ifndef THIRD_PARTY_BLINK_RENDERER_MODULES_ACCESSIBILITY_AX_NODE_OBJECT_H_
#define THIRD_PARTY_BLINK_RENDERER_MODULES_ACCESSIBILITY_AX_NODE_OBJECT_H_
+#include "base/dcheck_is_on.h"
#include "base/macros.h"
#include "third_party/blink/renderer/core/editing/markers/document_marker.h"
#include "third_party/blink/renderer/modules/accessibility/ax_object.h"
@@ -38,6 +39,7 @@ namespace blink {
class AXObjectCacheImpl;
class Element;
+class HTMLElement;
class HTMLLabelElement;
class Node;
@@ -59,11 +61,10 @@ class MODULES_EXPORT AXNodeObject : public AXObject {
// The ARIA role, not taking the native role into account.
ax::mojom::blink::Role aria_role_;
- static base::Optional<String> GetCSSAltText(const Node*);
+ static absl::optional<String> GetCSSAltText(const Node*);
AXObjectInclusion ShouldIncludeBasedOnSemantics(
IgnoredReasons* = nullptr) const;
bool ComputeAccessibilityIsIgnored(IgnoredReasons* = nullptr) const override;
- const AXObject* InheritsPresentationalRoleFrom() const override;
ax::mojom::blink::Role DetermineTableSectionRole() const;
ax::mojom::blink::Role DetermineTableCellRole() const;
ax::mojom::blink::Role DetermineTableRowRole() const;
@@ -78,26 +79,19 @@ class MODULES_EXPORT AXNodeObject : public AXObject {
Element* MenuItemElementForMenu() const;
Element* MouseButtonListener() const;
- AXObject* CorrespondingControlAXObjectForLabelElement() const;
- AXObject* CorrespondingLabelAXObject() const;
- HTMLLabelElement* LabelElementContainer() const;
+ HTMLElement* CorrespondingControlForLabelElement() const;
//
// Overridden from AXObject.
//
- void Init(AXObject* parent_if_known) override;
+ void Init(AXObject* parent) override;
void Detach() override;
bool IsAXNodeObject() const final;
// Check object role or purpose.
bool IsAutofillAvailable() const override;
- bool IsControllingVideoElement() const;
bool IsDefault() const final;
- bool IsMultiline() const override;
- bool IsEditable() const override;
- bool ComputeIsEditableRoot() const override;
- bool HasContentEditableAttributeSet() const override;
bool IsFieldset() const final;
bool IsHovered() const final;
bool IsImageButton() const;
@@ -108,7 +102,6 @@ class MODULES_EXPORT AXNodeObject : public AXObject {
bool IsNativeImage() const final;
bool IsOffScreen() const override;
bool IsProgressIndicator() const override;
- bool IsRichlyEditable() const override;
bool IsSlider() const override;
bool IsSpinButton() const override;
bool IsNativeSlider() const override;
@@ -182,7 +175,6 @@ class MODULES_EXPORT AXNodeObject : public AXObject {
// ARIA attributes.
ax::mojom::blink::Role AriaRoleAttribute() const final;
- bool HasAriaAttribute() const override;
void AriaDescribedbyElements(AXObjectVector&) const override;
void AriaOwnsElements(AXObjectVector&) const override;
bool SupportsARIADragging() const override;
@@ -190,6 +182,8 @@ class MODULES_EXPORT AXNodeObject : public AXObject {
Vector<ax::mojom::blink::Dropeffect>& dropeffects) const override;
ax::mojom::blink::HasPopup HasPopup() const override;
+ bool IsEditableRoot() const override;
+ bool HasContentEditableAttributeSet() const override;
// Modify or take an action on an object.
bool OnNativeSetValueAction(const String&) override;
@@ -212,7 +206,6 @@ class MODULES_EXPORT AXNodeObject : public AXObject {
AXRelatedObjectVector*) const override;
String Placeholder(ax::mojom::blink::NameFrom) const override;
String Title(ax::mojom::blink::NameFrom) const override;
- bool NameFromLabelElement() const override;
// Location
void GetRelativeBounds(AXObject** out_container,
@@ -241,7 +234,7 @@ class MODULES_EXPORT AXNodeObject : public AXObject {
Element* ActionElement() const override;
Element* AnchorElement() const override;
Document* GetDocument() const override;
- Node* GetNode() const override { return node_; }
+ Node* GetNode() const final;
// DOM and layout tree access.
AtomicString Language() const override;
@@ -255,7 +248,7 @@ class MODULES_EXPORT AXNodeObject : public AXObject {
bool OnNativeSetSequentialFocusNavigationStartingPointAction() final;
// Notifications that this object may have changed.
- void ChildrenChanged() override;
+ void ChildrenChangedWithCleanLayout() override;
void SelectionChanged() final;
void HandleAriaExpandedChanged() override;
void HandleActiveDescendantChanged() override;
@@ -273,7 +266,7 @@ class MODULES_EXPORT AXNodeObject : public AXObject {
HeapVector<Member<AXObject>>& owned_children) const;
// Inline text boxes.
- void LoadInlineTextBoxes() override;
+ void LoadInlineTextBoxesRecursive() override;
//
// Layout object specific methods.
@@ -288,8 +281,6 @@ class MODULES_EXPORT AXNodeObject : public AXObject {
const AtomicString& GetInternalsAttribute(Element&,
const QualifiedName&) const;
- Member<Node> node_;
-
bool IsNativeCheckboxInMixedState() const;
String NativeTextAlternative(AXObjectSet& visited,
ax::mojom::blink::NameFrom&,
@@ -307,6 +298,8 @@ class MODULES_EXPORT AXNodeObject : public AXObject {
void AddNodeChildren();
void AddLayoutChildren();
bool CanAddLayoutChild(LayoutObject& child);
+ // Add inline textbox children, if either force == true or
+ // AXObjectCache().InlineTextBoxAccessibilityEnabled().
void AddInlineTextBoxChildren(bool force = false);
void AddImageMapChildren();
void AddPopupChildren();
@@ -321,6 +314,11 @@ class MODULES_EXPORT AXNodeObject : public AXObject {
ax::mojom::blink::Dropeffect ParseDropeffect(String& dropeffect) const;
+ static bool IsNameFromLabelElement(HTMLElement* control);
+ static bool IsRedundantLabel(HTMLLabelElement* label);
+
+ Member<Node> node_;
+
DISALLOW_COPY_AND_ASSIGN(AXNodeObject);
};
diff --git a/chromium/third_party/blink/renderer/modules/accessibility/ax_object.cc b/chromium/third_party/blink/renderer/modules/accessibility/ax_object.cc
index d09d11af243..fa0a78a291f 100644
--- a/chromium/third_party/blink/renderer/modules/accessibility/ax_object.cc
+++ b/chromium/third_party/blink/renderer/modules/accessibility/ax_object.cc
@@ -29,6 +29,7 @@
#include "third_party/blink/renderer/modules/accessibility/ax_object.h"
#include <algorithm>
+#include <ostream>
#include "base/strings/string_util.h"
#include "build/build_config.h"
#include "third_party/blink/public/common/input/web_menu_source_type.h"
@@ -41,6 +42,7 @@
#include "third_party/blink/renderer/core/dom/events/simulated_click_options.h"
#include "third_party/blink/renderer/core/dom/focus_params.h"
#include "third_party/blink/renderer/core/dom/node_computed_style.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/local_frame_view.h"
#include "third_party/blink/renderer/core/frame/settings.h"
@@ -48,11 +50,14 @@
#include "third_party/blink/renderer/core/html/custom/element_internals.h"
#include "third_party/blink/renderer/core/html/forms/html_input_element.h"
#include "third_party/blink/renderer/core/html/forms/html_select_element.h"
+#include "third_party/blink/renderer/core/html/forms/html_text_area_element.h"
#include "third_party/blink/renderer/core/html/forms/text_control_element.h"
#include "third_party/blink/renderer/core/html/html_dialog_element.h"
+#include "third_party/blink/renderer/core/html/html_element.h"
#include "third_party/blink/renderer/core/html/html_frame_owner_element.h"
#include "third_party/blink/renderer/core/html/html_head_element.h"
#include "third_party/blink/renderer/core/html/html_script_element.h"
+#include "third_party/blink/renderer/core/html/html_slot_element.h"
#include "third_party/blink/renderer/core/html/html_style_element.h"
#include "third_party/blink/renderer/core/html/html_table_cell_element.h"
#include "third_party/blink/renderer/core/html/html_table_element.h"
@@ -73,6 +78,7 @@
#include "third_party/blink/renderer/core/svg/svg_element.h"
#include "third_party/blink/renderer/core/svg/svg_g_element.h"
#include "third_party/blink/renderer/core/svg/svg_style_element.h"
+#include "third_party/blink/renderer/modules/accessibility/ax_image_map_link.h"
#include "third_party/blink/renderer/modules/accessibility/ax_menu_list.h"
#include "third_party/blink/renderer/modules/accessibility/ax_menu_list_option.h"
#include "third_party/blink/renderer/modules/accessibility/ax_menu_list_popup.h"
@@ -109,8 +115,6 @@ String IgnoredReasonName(AXIgnoredReason reason) {
return "activeModalDialog";
case kAXAriaModalDialog:
return "activeAriaModalDialog";
- case kAXAncestorIsLeafNode:
- return "ancestorIsLeafNode";
case kAXAriaHiddenElement:
return "ariaHiddenElement";
case kAXAriaHiddenSubtree:
@@ -123,8 +127,6 @@ String IgnoredReasonName(AXIgnoredReason reason) {
return "inertElement";
case kAXInertSubtree:
return "inertSubtree";
- case kAXInheritsPresentation:
- return "inheritsPresentation";
case kAXLabelContainer:
return "labelContainer";
case kAXLabelFor:
@@ -159,7 +161,15 @@ String GetIgnoredReasonsDebugString(AXObject::IgnoredReasons& reasons) {
#endif
-String GetElementString(Element* element) {
+String GetNodeString(Node* node) {
+ if (node->IsTextNode()) {
+ String string_builder = "\"";
+ string_builder = string_builder + node->nodeValue();
+ string_builder = string_builder + "\"";
+ return string_builder;
+ }
+
+ Element* element = DynamicTo<Element>(node);
if (!element)
return "<null>";
@@ -183,14 +193,18 @@ Node* GetParentNodeForComputeParent(Node* node) {
if (!node)
return nullptr;
- // Prefer LayoutTreeBuilderTraversal::Parent(), which handles pseudo content.
- Node* parent = LayoutTreeBuilderTraversal::Parent(*node);
- if (parent)
- return parent;
-
- // Unfortunately, LayoutTreeBuilderTraversal::Parent() can return nullptr for
- // a text node, such as inside a text area. Fall back on DOM parentNode().
- return node->parentNode();
+ // Use LayoutTreeBuilderTraversal::Parent(), which handles pseudo content.
+ // This can return nullptr for a node that is never visited by
+ // LayoutTreeBuilderTraversal's child traversal. For example, while an element
+ // can be appended as a <textarea>'s child, it is never visited by
+ // LayoutTreeBuilderTraversal's child traversal. Therefore, returning null in
+ // this case is appropriate, because that child content is not attached to any
+ // parent as far as rendering or accessibility are concerned.
+ // Whenever null is returned from this function, then a parent cannot be
+ // computed, and when a parent is not provided or computed, the accessible
+ // object will not be created.
+ // TODO(aleventhal) Remove this method / inline, if proven to be this simple.
+ return LayoutTreeBuilderTraversal::Parent(*node);
}
#if DCHECK_IS_ON()
@@ -202,7 +216,6 @@ bool IsValidRole(ax::mojom::blink::Role role) {
case ax::mojom::blink::Role::kColumn:
case ax::mojom::blink::Role::kDesktop:
case ax::mojom::blink::Role::kKeyboard:
- case ax::mojom::blink::Role::kIgnored:
case ax::mojom::blink::Role::kImeCandidate:
case ax::mojom::blink::Role::kListGrid:
case ax::mojom::blink::Role::kPane:
@@ -227,6 +240,9 @@ struct RoleHashTraits : HashTraits<ax::mojom::blink::Role> {
}
};
+constexpr wtf_size_t kNumRoles =
+ static_cast<wtf_size_t>(ax::mojom::blink::Role::kMaxValue) + 1;
+
using ARIARoleMap = HashMap<String,
ax::mojom::blink::Role,
CaseFoldingHash,
@@ -234,12 +250,16 @@ using ARIARoleMap = HashMap<String,
RoleHashTraits>;
struct RoleEntry {
- const char* aria_role;
- ax::mojom::blink::Role webcore_role;
+ const char* role_name;
+ ax::mojom::blink::Role role;
};
// Mapping of ARIA role name to internal role name.
-const RoleEntry kRoles[] = {
+// This is used for the following:
+// 1. Map from an ARIA role to the internal role when building tree.
+// 2. Map from an internal role to an ARIA role name, for debugging, the
+// xml-roles object attribute and element.computedRole.
+const RoleEntry kAriaRoles[] = {
{"alert", ax::mojom::blink::Role::kAlert},
{"alertdialog", ax::mojom::blink::Role::kAlertDialog},
{"application", ax::mojom::blink::Role::kApplication},
@@ -342,16 +362,17 @@ const RoleEntry kRoles[] = {
{"mark", ax::mojom::blink::Role::kMark},
{"meter", ax::mojom::blink::Role::kMeter},
{"navigation", ax::mojom::blink::Role::kNavigation},
+ // role="presentation" is the same as role="none".
+ {"presentation", ax::mojom::blink::Role::kNone},
+ // role="none" is listed after role="presentation", so that it is the
+ // canonical name in devtools and tests.
{"none", ax::mojom::blink::Role::kNone},
{"note", ax::mojom::blink::Role::kNote},
{"option", ax::mojom::blink::Role::kListBoxOption},
{"paragraph", ax::mojom::blink::Role::kParagraph},
- {"presentation", ax::mojom::blink::Role::kPresentational},
{"progressbar", ax::mojom::blink::Role::kProgressIndicator},
{"radio", ax::mojom::blink::Role::kRadioButton},
{"radiogroup", ax::mojom::blink::Role::kRadioGroup},
- // TODO(accessibility) region should only be mapped
- // if name present. See http://crbug.com/840819.
{"region", ax::mojom::blink::Role::kRegion},
{"row", ax::mojom::blink::Role::kRow},
{"rowgroup", ax::mojom::blink::Role::kRowGroup},
@@ -371,7 +392,6 @@ const RoleEntry kRoles[] = {
{"tablist", ax::mojom::blink::Role::kTabList},
{"tabpanel", ax::mojom::blink::Role::kTabPanel},
{"term", ax::mojom::blink::Role::kTerm},
- {"text", ax::mojom::blink::Role::kStaticText},
{"textbox", ax::mojom::blink::Role::kTextField},
{"time", ax::mojom::blink::Role::kTime},
{"timer", ax::mojom::blink::Role::kTimer},
@@ -381,316 +401,49 @@ const RoleEntry kRoles[] = {
{"treegrid", ax::mojom::blink::Role::kTreeGrid},
{"treeitem", ax::mojom::blink::Role::kTreeItem}};
-struct InternalRoleEntry {
- ax::mojom::blink::Role webcore_role;
- const char* internal_role_name;
-};
-
-const InternalRoleEntry kInternalRoles[] = {
- {ax::mojom::blink::Role::kNone, "None"},
- {ax::mojom::blink::Role::kAbbr, "Abbr"},
- {ax::mojom::blink::Role::kAlertDialog, "AlertDialog"},
- {ax::mojom::blink::Role::kAlert, "Alert"},
- {ax::mojom::blink::Role::kAnchor, "Anchor"},
- {ax::mojom::blink::Role::kComment, "Comment"},
- {ax::mojom::blink::Role::kApplication, "Application"},
- {ax::mojom::blink::Role::kArticle, "Article"},
- {ax::mojom::blink::Role::kAudio, "Audio"},
- {ax::mojom::blink::Role::kBanner, "Banner"},
- {ax::mojom::blink::Role::kBlockquote, "Blockquote"},
- {ax::mojom::blink::Role::kButton, "Button"},
- {ax::mojom::blink::Role::kCanvas, "Canvas"},
- {ax::mojom::blink::Role::kCaption, "Caption"},
- {ax::mojom::blink::Role::kCaret, "Caret"},
- {ax::mojom::blink::Role::kCell, "Cell"},
- {ax::mojom::blink::Role::kCheckBox, "CheckBox"},
- {ax::mojom::blink::Role::kClient, "Client"},
- {ax::mojom::blink::Role::kCode, "Code"},
- {ax::mojom::blink::Role::kColorWell, "ColorWell"},
- {ax::mojom::blink::Role::kColumnHeader, "ColumnHeader"},
- {ax::mojom::blink::Role::kColumn, "Column"},
- {ax::mojom::blink::Role::kComboBoxGrouping, "ComboBox"},
- {ax::mojom::blink::Role::kComboBoxMenuButton, "ComboBox"},
- {ax::mojom::blink::Role::kComplementary, "Complementary"},
- {ax::mojom::blink::Role::kContentDeletion, "ContentDeletion"},
- {ax::mojom::blink::Role::kContentInsertion, "ContentInsertion"},
- {ax::mojom::blink::Role::kContentInfo, "ContentInfo"},
- {ax::mojom::blink::Role::kDate, "Date"},
- {ax::mojom::blink::Role::kDateTime, "DateTime"},
- {ax::mojom::blink::Role::kDefinition, "Definition"},
- {ax::mojom::blink::Role::kDescriptionListDetail, "DescriptionListDetail"},
- {ax::mojom::blink::Role::kDescriptionList, "DescriptionList"},
- {ax::mojom::blink::Role::kDescriptionListTerm, "DescriptionListTerm"},
- {ax::mojom::blink::Role::kDesktop, "Desktop"},
- {ax::mojom::blink::Role::kDetails, "Details"},
- {ax::mojom::blink::Role::kDialog, "Dialog"},
- {ax::mojom::blink::Role::kDirectory, "Directory"},
- {ax::mojom::blink::Role::kDisclosureTriangle, "DisclosureTriangle"},
- // --------------------------------------------------------------
- // DPub Roles:
- // https://www.w3.org/TR/dpub-aam-1.0/#mapping_role_table
- {ax::mojom::blink::Role::kDocAbstract, "DocAbstract"},
- {ax::mojom::blink::Role::kDocAcknowledgments, "DocAcknowledgments"},
- {ax::mojom::blink::Role::kDocAfterword, "DocAfterword"},
- {ax::mojom::blink::Role::kDocAppendix, "DocAppendix"},
- {ax::mojom::blink::Role::kDocBackLink, "DocBackLink"},
- {ax::mojom::blink::Role::kDocBiblioEntry, "DocBiblioentry"},
- {ax::mojom::blink::Role::kDocBibliography, "DocBibliography"},
- {ax::mojom::blink::Role::kDocBiblioRef, "DocBiblioref"},
- {ax::mojom::blink::Role::kDocChapter, "DocChapter"},
- {ax::mojom::blink::Role::kDocColophon, "DocColophon"},
- {ax::mojom::blink::Role::kDocConclusion, "DocConclusion"},
- {ax::mojom::blink::Role::kDocCover, "DocCover"},
- {ax::mojom::blink::Role::kDocCredit, "DocCredit"},
- {ax::mojom::blink::Role::kDocCredits, "DocCredits"},
- {ax::mojom::blink::Role::kDocDedication, "DocDedication"},
- {ax::mojom::blink::Role::kDocEndnote, "DocEndnote"},
- {ax::mojom::blink::Role::kDocEndnotes, "DocEndnotes"},
- {ax::mojom::blink::Role::kDocEpigraph, "DocEpigraph"},
- {ax::mojom::blink::Role::kDocEpilogue, "DocEpilogue"},
- {ax::mojom::blink::Role::kDocErrata, "DocErrata"},
- {ax::mojom::blink::Role::kDocExample, "DocExample"},
- {ax::mojom::blink::Role::kDocFootnote, "DocFootnote"},
- {ax::mojom::blink::Role::kDocForeword, "DocForeword"},
- {ax::mojom::blink::Role::kDocGlossary, "DocGlossary"},
- {ax::mojom::blink::Role::kDocGlossRef, "DocGlossref"},
- {ax::mojom::blink::Role::kDocIndex, "DocIndex"},
- {ax::mojom::blink::Role::kDocIntroduction, "DocIntroduction"},
- {ax::mojom::blink::Role::kDocNoteRef, "DocNoteref"},
- {ax::mojom::blink::Role::kDocNotice, "DocNotice"},
- {ax::mojom::blink::Role::kDocPageBreak, "DocPagebreak"},
- {ax::mojom::blink::Role::kDocPageFooter, "DocPageFooter"},
- {ax::mojom::blink::Role::kDocPageHeader, "DocPageHeader"},
- {ax::mojom::blink::Role::kDocPageList, "DocPagelist"},
- {ax::mojom::blink::Role::kDocPart, "DocPart"},
- {ax::mojom::blink::Role::kDocPreface, "DocPreface"},
- {ax::mojom::blink::Role::kDocPrologue, "DocPrologue"},
- {ax::mojom::blink::Role::kDocPullquote, "DocPullquote"},
- {ax::mojom::blink::Role::kDocQna, "DocQna"},
- {ax::mojom::blink::Role::kDocSubtitle, "DocSubtitle"},
- {ax::mojom::blink::Role::kDocTip, "DocTip"},
- {ax::mojom::blink::Role::kDocToc, "DocToc"},
- // End DPub roles.
- // --------------------------------------------------------------
- {ax::mojom::blink::Role::kDocument, "Document"},
- {ax::mojom::blink::Role::kEmbeddedObject, "EmbeddedObject"},
- {ax::mojom::blink::Role::kEmphasis, "Emphasis"},
- {ax::mojom::blink::Role::kFeed, "feed"},
- {ax::mojom::blink::Role::kFigcaption, "Figcaption"},
- {ax::mojom::blink::Role::kFigure, "Figure"},
- {ax::mojom::blink::Role::kFooter, "Footer"},
- {ax::mojom::blink::Role::kFooterAsNonLandmark, "FooterAsNonLandmark"},
- {ax::mojom::blink::Role::kForm, "Form"},
- {ax::mojom::blink::Role::kGenericContainer, "GenericContainer"},
- // --------------------------------------------------------------
- // ARIA Graphics module roles:
- // https://rawgit.com/w3c/graphics-aam/master/#mapping_role_table
- {ax::mojom::blink::Role::kGraphicsDocument, "GraphicsDocument"},
- {ax::mojom::blink::Role::kGraphicsObject, "GraphicsObject"},
- {ax::mojom::blink::Role::kGraphicsSymbol, "GraphicsSymbol"},
- // End ARIA Graphics module roles.
- // --------------------------------------------------------------
- {ax::mojom::blink::Role::kGrid, "Grid"},
- {ax::mojom::blink::Role::kGroup, "Group"},
- {ax::mojom::blink::Role::kHeader, "Header"},
- {ax::mojom::blink::Role::kHeaderAsNonLandmark, "HeaderAsNonLandmark"},
- {ax::mojom::blink::Role::kHeading, "Heading"},
- {ax::mojom::blink::Role::kIframePresentational, "IframePresentational"},
- {ax::mojom::blink::Role::kIframe, "Iframe"},
- {ax::mojom::blink::Role::kIgnored, "Ignored"},
- {ax::mojom::blink::Role::kImage, "Image"},
- {ax::mojom::blink::Role::kImeCandidate, "ImeCandidate"},
- {ax::mojom::blink::Role::kInlineTextBox, "InlineTextBox"},
- {ax::mojom::blink::Role::kInputTime, "InputTime"},
- {ax::mojom::blink::Role::kKeyboard, "Keyboard"},
- {ax::mojom::blink::Role::kLabelText, "Label"},
- {ax::mojom::blink::Role::kLayoutTable, "LayoutTable"},
- {ax::mojom::blink::Role::kLayoutTableCell, "LayoutCellTable"},
- {ax::mojom::blink::Role::kLayoutTableRow, "LayoutRowTable"},
- {ax::mojom::blink::Role::kLegend, "Legend"},
- {ax::mojom::blink::Role::kLink, "Link"},
- {ax::mojom::blink::Role::kLineBreak, "LineBreak"},
- {ax::mojom::blink::Role::kListBox, "ListBox"},
- {ax::mojom::blink::Role::kListBoxOption, "ListBoxOption"},
- {ax::mojom::blink::Role::kListGrid, "ListGrid"},
- {ax::mojom::blink::Role::kListItem, "ListItem"},
- {ax::mojom::blink::Role::kListMarker, "ListMarker"},
- {ax::mojom::blink::Role::kList, "List"},
- {ax::mojom::blink::Role::kLog, "Log"},
- {ax::mojom::blink::Role::kMain, "Main"},
- {ax::mojom::blink::Role::kMark, "Mark"},
- {ax::mojom::blink::Role::kMarquee, "Marquee"},
- {ax::mojom::blink::Role::kMath, "Math"},
- {ax::mojom::blink::Role::kMenuBar, "MenuBar"},
- {ax::mojom::blink::Role::kMenuItem, "MenuItem"},
- {ax::mojom::blink::Role::kMenuItemCheckBox, "MenuItemCheckBox"},
- {ax::mojom::blink::Role::kMenuItemRadio, "MenuItemRadio"},
- {ax::mojom::blink::Role::kMenuListOption, "MenuListOption"},
- {ax::mojom::blink::Role::kMenuListPopup, "MenuListPopup"},
- {ax::mojom::blink::Role::kMenu, "Menu"},
- {ax::mojom::blink::Role::kMeter, "Meter"},
- {ax::mojom::blink::Role::kNavigation, "Navigation"},
- {ax::mojom::blink::Role::kNote, "Note"},
- {ax::mojom::blink::Role::kPane, "Pane"},
- {ax::mojom::blink::Role::kParagraph, "Paragraph"},
- {ax::mojom::blink::Role::kPdfActionableHighlight, "PdfActionableHighlight"},
- {ax::mojom::blink::Role::kPdfRoot, "PdfRoot"},
- {ax::mojom::blink::Role::kPluginObject, "PluginObject"},
- {ax::mojom::blink::Role::kPopUpButton, "PopUpButton"},
- {ax::mojom::blink::Role::kPortal, "Portal"},
- {ax::mojom::blink::Role::kPre, "Pre"},
- {ax::mojom::blink::Role::kPresentational, "Presentational"},
- {ax::mojom::blink::Role::kProgressIndicator, "ProgressIndicator"},
- {ax::mojom::blink::Role::kRadioButton, "RadioButton"},
- {ax::mojom::blink::Role::kRadioGroup, "RadioGroup"},
- {ax::mojom::blink::Role::kRegion, "Region"},
- {ax::mojom::blink::Role::kRootWebArea, "WebArea"},
- {ax::mojom::blink::Role::kRow, "Row"},
- {ax::mojom::blink::Role::kRowGroup, "RowGroup"},
- {ax::mojom::blink::Role::kRowHeader, "RowHeader"},
- {ax::mojom::blink::Role::kRuby, "Ruby"},
- {ax::mojom::blink::Role::kRubyAnnotation, "RubyAnnotation"},
- {ax::mojom::blink::Role::kSection, "Section"},
- {ax::mojom::blink::Role::kSvgRoot, "SVGRoot"},
- {ax::mojom::blink::Role::kScrollBar, "ScrollBar"},
- {ax::mojom::blink::Role::kScrollView, "ScrollView"},
- {ax::mojom::blink::Role::kSearch, "Search"},
- {ax::mojom::blink::Role::kSearchBox, "SearchBox"},
- {ax::mojom::blink::Role::kSlider, "Slider"},
- {ax::mojom::blink::Role::kSpinButton, "SpinButton"},
- {ax::mojom::blink::Role::kSplitter, "Splitter"},
- {ax::mojom::blink::Role::kStaticText, "StaticText"},
- {ax::mojom::blink::Role::kStatus, "Status"},
- {ax::mojom::blink::Role::kStrong, "Strong"},
- {ax::mojom::blink::Role::kSuggestion, "Suggestion"},
- {ax::mojom::blink::Role::kSwitch, "Switch"},
- {ax::mojom::blink::Role::kTab, "Tab"},
- {ax::mojom::blink::Role::kTabList, "TabList"},
- {ax::mojom::blink::Role::kTabPanel, "TabPanel"},
- {ax::mojom::blink::Role::kTable, "Table"},
- {ax::mojom::blink::Role::kTableHeaderContainer, "TableHeaderContainer"},
- {ax::mojom::blink::Role::kTerm, "Term"},
- {ax::mojom::blink::Role::kTextField, "TextField"},
- {ax::mojom::blink::Role::kTextFieldWithComboBox, "ComboBox"},
- {ax::mojom::blink::Role::kTime, "Time"},
- {ax::mojom::blink::Role::kTimer, "Timer"},
- {ax::mojom::blink::Role::kTitleBar, "TitleBar"},
- {ax::mojom::blink::Role::kToggleButton, "ToggleButton"},
- {ax::mojom::blink::Role::kToolbar, "Toolbar"},
- {ax::mojom::blink::Role::kTreeGrid, "TreeGrid"},
- {ax::mojom::blink::Role::kTreeItem, "TreeItem"},
- {ax::mojom::blink::Role::kTree, "Tree"},
- {ax::mojom::blink::Role::kTooltip, "UserInterfaceTooltip"},
- {ax::mojom::blink::Role::kUnknown, "Unknown"},
- {ax::mojom::blink::Role::kVideo, "Video"},
- {ax::mojom::blink::Role::kWebView, "WebView"},
- {ax::mojom::blink::Role::kWindow, "Window"}};
-
-static_assert(base::size(kInternalRoles) ==
- static_cast<size_t>(ax::mojom::blink::Role::kMaxValue) + 1,
- "Not all internal roles have an entry in internalRoles array");
-
-// Roles which we need to map in the other direction
+// More friendly names for debugging. These are roles which don't map from
+// the ARIA role name to the internal role when building the tree, but when
+// debugging, we want to show the ARIA role name, since it is close in meaning.
const RoleEntry kReverseRoles[] = {
{"banner", ax::mojom::blink::Role::kHeader},
{"button", ax::mojom::blink::Role::kToggleButton},
{"combobox", ax::mojom::blink::Role::kPopUpButton},
{"contentinfo", ax::mojom::blink::Role::kFooter},
{"menuitem", ax::mojom::blink::Role::kMenuListOption},
- {"progressbar", ax::mojom::blink::Role::kMeter},
- {"region", ax::mojom::blink::Role::kSection},
- {"textbox", ax::mojom::blink::Role::kTextField},
{"combobox", ax::mojom::blink::Role::kComboBoxMenuButton},
{"combobox", ax::mojom::blink::Role::kTextFieldWithComboBox}};
static ARIARoleMap* CreateARIARoleMap() {
ARIARoleMap* role_map = new ARIARoleMap;
- for (size_t i = 0; i < base::size(kRoles); ++i)
- role_map->Set(String(kRoles[i].aria_role), kRoles[i].webcore_role);
+ for (auto aria_role : kAriaRoles)
+ role_map->Set(String(aria_role.role_name), aria_role.role);
return role_map;
}
-static Vector<AtomicString>* CreateRoleNameVector() {
- Vector<AtomicString>* role_name_vector =
- new Vector<AtomicString>(base::size(kInternalRoles));
- for (wtf_size_t i = 0; i < base::size(kInternalRoles); i++)
- (*role_name_vector)[i] = g_null_atom;
+// The role name vector contains only ARIA roles, and no internal roles.
+static Vector<AtomicString>* CreateARIARoleNameVector() {
+ Vector<AtomicString>* role_name_vector = new Vector<AtomicString>(kNumRoles);
+ role_name_vector->Fill(g_null_atom, kNumRoles);
- for (wtf_size_t i = 0; i < base::size(kRoles); ++i) {
- (*role_name_vector)[static_cast<wtf_size_t>(kRoles[i].webcore_role)] =
- AtomicString(kRoles[i].aria_role);
+ for (auto aria_role : kAriaRoles) {
+ (*role_name_vector)[static_cast<wtf_size_t>(aria_role.role)] =
+ AtomicString(aria_role.role_name);
}
- for (wtf_size_t i = 0; i < base::size(kReverseRoles); ++i) {
- (*role_name_vector)[static_cast<wtf_size_t>(
- kReverseRoles[i].webcore_role)] =
- AtomicString(kReverseRoles[i].aria_role);
+ for (auto reverse_role : kReverseRoles) {
+ (*role_name_vector)[static_cast<wtf_size_t>(reverse_role.role)] =
+ AtomicString(reverse_role.role_name);
}
return role_name_vector;
}
-static Vector<AtomicString>* CreateInternalRoleNameVector() {
- Vector<AtomicString>* internal_role_name_vector =
- new Vector<AtomicString>(base::size(kInternalRoles));
- for (wtf_size_t i = 0; i < base::size(kInternalRoles); i++) {
- (*internal_role_name_vector)[static_cast<wtf_size_t>(
- kInternalRoles[i].webcore_role)] =
- AtomicString(kInternalRoles[i].internal_role_name);
- }
-
- return internal_role_name_vector;
-}
-
HTMLDialogElement* GetActiveDialogElement(Node* node) {
return node->GetDocument().ActiveModalDialog();
}
-// TODO(dmazzoni): replace this with a call to RoleName().
-std::string GetEquivalentAriaRoleString(const ax::mojom::blink::Role role) {
- switch (role) {
- case ax::mojom::blink::Role::kArticle:
- return "article";
- case ax::mojom::blink::Role::kBanner:
- return "banner";
- case ax::mojom::blink::Role::kButton:
- return "button";
- case ax::mojom::blink::Role::kComplementary:
- return "complementary";
- case ax::mojom::blink::Role::kFigure:
- return "figure";
- case ax::mojom::blink::Role::kFooter:
- return "contentinfo";
- case ax::mojom::blink::Role::kHeader:
- return "banner";
- case ax::mojom::blink::Role::kHeading:
- return "heading";
- case ax::mojom::blink::Role::kImage:
- return "img";
- case ax::mojom::blink::Role::kMain:
- return "main";
- case ax::mojom::blink::Role::kNavigation:
- return "navigation";
- case ax::mojom::blink::Role::kRadioButton:
- return "radio";
- case ax::mojom::blink::Role::kRegion:
- return "region";
- case ax::mojom::blink::Role::kSection:
- // A <section> element uses the 'region' ARIA role mapping.
- return "region";
- case ax::mojom::blink::Role::kSlider:
- return "slider";
- case ax::mojom::blink::Role::kTime:
- return "time";
- default:
- break;
- }
-
- return std::string();
-}
-
} // namespace
int32_t ToAXMarkerType(DocumentMarker::MarkerType marker_type) {
@@ -720,6 +473,8 @@ int32_t ToAXMarkerType(DocumentMarker::MarkerType marker_type) {
return static_cast<int32_t>(result);
}
+// static
+bool AXObject::is_loading_inline_boxes_ = false;
unsigned AXObject::number_of_live_ax_objects_ = 0;
AXObject::AXObject(AXObjectCacheImpl& ax_object_cache)
@@ -731,10 +486,7 @@ AXObject::AXObject(AXObjectCacheImpl& ax_object_cache)
cached_is_ignored_(false),
cached_is_ignored_but_included_in_tree_(false),
cached_is_inert_or_aria_hidden_(false),
- cached_is_descendant_of_leaf_node_(false),
cached_is_descendant_of_disabled_node_(false),
- cached_has_inherited_presentational_role_(false),
- cached_is_editable_root_(false),
cached_live_region_root_(nullptr),
cached_aria_column_index_(0),
cached_aria_row_index_(0),
@@ -747,9 +499,12 @@ AXObject::~AXObject() {
--number_of_live_ax_objects_;
}
-void AXObject::Init(AXObject* parent_if_known) {
+void AXObject::Init(AXObject* parent) {
#if DCHECK_IS_ON()
- DCHECK(!parent_);
+ DCHECK(!parent_) << "Should not already have a cached parent:"
+ << "\n* Child = " << GetNode() << " / " << GetLayoutObject()
+ << "\n* Parent = " << parent_->ToString(true, true)
+ << "\n* Equal to passed-in parent? " << (parent == parent_);
DCHECK(!is_initializing_);
base::AutoReset<bool> reentrancy_protector(&is_initializing_, true);
#endif // DCHECK_IS_ON()
@@ -766,24 +521,42 @@ void AXObject::Init(AXObject* parent_if_known) {
// Determine the parent as soon as possible.
// Every AXObject must have a parent unless it's the root.
- SetParent(parent_if_known ? parent_if_known : ComputeParent());
- DCHECK(parent_ || IsA<Document>(GetNode()))
+ SetParent(parent);
+ DCHECK(parent_ || IsRoot())
<< "The following node should have a parent: " << GetNode();
- SetNeedsToUpdateChildren(); // Should be called after role_ is set.
+ // The parent cannot have children. This object must be destroyed.
+ DCHECK(!parent_ || parent_->CanHaveChildren())
+ << "Tried to set a parent that cannot have children:"
+ << "\n* Parent = " << parent_->ToString(true, true)
+ << "\n* Child = " << ToString(true, true);
+
+ // This is one after the role_ is computed, because the role is used to
+ // determine whether an AXObject can have children.
+ children_dirty_ = CanHaveChildren();
+
+ // Ensure that the aria-owns relationship is set before attempting
+ // to update cached attribute values.
+ if (GetNode())
+ AXObjectCache().MaybeNewRelationTarget(*GetNode(), this);
+
UpdateCachedAttributeValuesIfNeeded(false);
}
void AXObject::Detach() {
-#if DCHECK_IS_ON()
- // Only mock objects can end up being detached twice, because their owner
- // may have needed to detach them when they were detached, but couldn't
- // remove them from the object cache yet.
+ // Prevents LastKnown*() methods from returning the wrong values.
+ cached_is_ignored_ = true;
+ cached_is_ignored_but_included_in_tree_ = false;
+
if (IsDetached()) {
+ // Only mock objects can end up being detached twice, because their owner
+ // may have needed to detach them when they were detached, but couldn't
+ // remove them from the object cache yet.
DCHECK(IsMockObject()) << "Object detached twice: " << RoleValue();
return;
}
- DCHECK(!is_adding_children_) << ToString(true, true);
+
+#if DCHECK_IS_ON()
DCHECK(ax_object_cache_);
DCHECK(!ax_object_cache_->IsFrozen())
<< "Do not detach children while the tree is frozen, in order to avoid "
@@ -791,6 +564,15 @@ void AXObject::Detach() {
"accessibility properties.";
#endif
+#if defined(AX_FAIL_FAST_BUILD)
+ SANITIZER_CHECK(!is_adding_children_) << ToString(true, true);
+#endif
+
+ CHECK(!is_loading_inline_boxes_)
+ << "Should not be attempting to detach object while in the middle of "
+ "recursively loading inline text boxes: "
+ << ToString(true, true);
+
// Clear any children and call DetachFromParent() on them so that
// no children are left with dangling pointers to their parent.
ClearChildren();
@@ -804,12 +586,35 @@ bool AXObject::IsDetached() const {
return !ax_object_cache_;
}
-void AXObject::SetParent(AXObject* new_parent) {
- DCHECK(new_parent || IsA<Document>(GetNode()))
- << "Parent cannot be null, except at the root, was null at " << GetNode()
- << " " << GetLayoutObject();
+bool AXObject::IsRoot() const {
+ return GetNode() && GetNode() == &AXObjectCache().GetDocument();
+}
+void AXObject::SetParent(AXObject* new_parent) const {
#if DCHECK_IS_ON()
+ if (!new_parent && !IsRoot()) {
+ std::ostringstream message;
+ message << "Parent cannot be null, except at the root. "
+ "Parent chain from DOM, starting at |this|:";
+ int count = 0;
+ for (Node* node = GetNode(); node;
+ node = GetParentNodeForComputeParent(node)) {
+ message << "\n"
+ << (++count) << ". " << node
+ << "\n LayoutObject=" << node->GetLayoutObject();
+ if (AXObject* obj = AXObjectCache().Get(node))
+ message << "\n " << obj->ToString(true, true);
+ }
+ NOTREACHED() << message.str();
+ }
+
+ if (new_parent) {
+ DCHECK(!new_parent->IsDetached())
+ << "Cannot set parent to a detached object:"
+ << "\n* Child: " << ToString(true, true)
+ << "\n* New parent: " << new_parent->ToString(true, true);
+ }
+
// Check to ensure that if the parent is changing from a previous parent,
// that |this| is not still a child of that one.
// This is similar to the IsParentUnignoredOf() check in
@@ -832,99 +637,177 @@ void AXObject::SetParent(AXObject* new_parent) {
parent_ = new_parent;
}
+bool AXObject::IsMissingParent() const {
+ if (!parent_)
+ return !IsRoot();
+
+ if (parent_->IsDetached())
+ return true;
+
+ return false;
+}
+
+void AXObject::RepairMissingParent() const {
+ DCHECK(IsMissingParent());
+
+ SetParent(ComputeParent());
+}
+
// In many cases, ComputeParent() is not called, because the parent adding
-// the child will pass itself into AXObject::Init() via parent_if_known.
+// the parent adding the child will pass itself into AXObjectCacheImpl.
// ComputeParent() is still necessary because some parts of the code,
// especially web tests, result in AXObjects being created in the middle of
// the tree before their parents are created.
// TODO(accessibility) Consider forcing all ax objects to be created from
// the top down, eliminating the need for ComputeParent().
AXObject* AXObject::ComputeParent() const {
- DCHECK(!IsDetached());
+#if defined(AX_FAIL_FAST_BUILD)
+ SANITIZER_CHECK(!IsDetached());
- DCHECK(!parent_ || parent_->IsDetached())
- << "Should use cached parent unless it's detached, and should not "
- "attempt to recompute it, occurred on "
- << GetNode();
+ SANITIZER_CHECK(!IsVirtualObject())
+ << "A virtual object must have a parent, and cannot exist without one. "
+ "The parent is set when the object is constructed.";
- if (!GetNode() && !GetLayoutObject()) {
- NOTREACHED() << "Can't compute parent on AXObjects without a backing Node "
- "or LayoutObject. Objects without those must set the "
- "parent in Init(), |this| = "
- << RoleValue();
- return nullptr;
- }
+ SANITIZER_CHECK(!IsMockObject())
+ << "A mock object must have a parent, and cannot exist without one. "
+ "The parent is set when the object is constructed.";
- return ComputeParentImpl();
-}
+ SANITIZER_CHECK(GetNode() || GetLayoutObject())
+ << "Can't compute parent on AXObjects without a backing Node "
+ "or LayoutObject. Objects without those must set the "
+ "parent in Init(), |this| = "
+ << RoleValue();
+#endif
-AXObject* AXObject::ComputeParentImpl() const {
- DCHECK(!IsDetached());
+ AXObject* ax_parent =
+ AXObjectCache().IsAriaOwned(this)
+ ? AXObjectCache().GetAriaOwnedParent(this)
+ : ComputeNonARIAParent(AXObjectCache(), GetNode(), GetLayoutObject());
- if (AXObjectCache().IsAriaOwned(this))
- return AXObjectCache().GetAriaOwnedParent(this);
+ CHECK(!ax_parent || !ax_parent->IsDetached())
+ << "Computed parent should never be detached:"
+ << "\n* Child: " << GetNode()
+ << "\n* Parent: " << ax_parent->ToString(true, true);
- Node* current_node = GetNode();
+ return ax_parent;
+}
- // A WebArea's parent should be the page popup owner, if any, otherwise null.
- if (IsA<Document>(current_node)) {
- LocalFrame* frame = GetLayoutObject()->GetFrame();
- return AXObjectCache().GetOrCreate(frame->PagePopupOwner());
+// static
+bool AXObject::CanComputeAsNaturalParent(Node* node) {
+ // A <select> menulist that will use AXMenuList is not allowed.
+ if (AXObjectCacheImpl::UseAXMenuList()) {
+ if (auto* select = DynamicTo<HTMLSelectElement>(node)) {
+ if (select->UsesMenuList())
+ return false;
+ }
}
- if (IsVirtualObject()) {
- NOTREACHED()
- << "A virtual object must have a parent, and cannot exist without one. "
- "The parent is set when the object is constructed.";
- return nullptr;
+ // A <br> can only support AXInlineTextBox children, which is never the result
+ // of a parent computation (the parent of the children is set at Init()).
+ if (IsA<HTMLBRElement>(node))
+ return false;
+
+ // Image map parent-child relationships (from image to area) must be retrieved
+ // manually via AXImageMapLink::GetAXObjectForImageMap().
+ if (IsA<HTMLMapElement>(node) || IsA<HTMLAreaElement>(node) ||
+ IsA<HTMLImageElement>(node)) {
+ return false;
}
- // If no node, or a pseudo element, use the layout parent.
+ return true;
+}
+
+// static
+AXObject* AXObject::ComputeNonARIAParent(AXObjectCacheImpl& cache,
+ Node* current_node,
+ LayoutObject* current_layout_obj) {
+ DCHECK(current_node || current_layout_obj)
+ << "Can't compute parent without a backing Node "
+ "or LayoutObject.";
+
+ // If no node, use the layout parent.
if (!current_node) {
- LayoutObject* current_layout_obj = GetLayoutObject();
- if (!current_layout_obj) {
- NOTREACHED()
- << "Can't compute parent on AXObjects without a backing Node "
- "or LayoutObject. Objects without those must set the "
- "parent in Init(), |this| = "
- << RoleValue();
- return nullptr;
- }
- // If no DOM node and no parent, this must be an anonymous layout object.
+ // If no DOM node, this is an anonymous layout object.
DCHECK(current_layout_obj->IsAnonymous());
+ // In accessibility, this only occurs for descendants of pseudo elements.
+ DCHECK(AXObjectCacheImpl::IsRelevantPseudoElementDescendant(
+ *current_layout_obj))
+ << "Attempt to get AX parent for irrelevant anonymous layout object: "
+ << current_layout_obj;
LayoutObject* parent_layout_obj = current_layout_obj->Parent();
if (!parent_layout_obj)
return nullptr;
- if (AXObject* ax_parent = AXObjectCache().GetOrCreate(parent_layout_obj)) {
- DCHECK(!ax_parent->IsDetached());
- return ax_parent;
- }
- // Switch to using DOM nodes. The only cases that should occur do not have
- // chains of multiple parents without DOM nodes.
Node* parent_node = parent_layout_obj->GetNode();
- DCHECK(parent_node) << "Computing an accessible parent from the layout "
- "parent did not yield an accessible object nor a "
- "DOM node to walk up from, current_layout_obj = "
- << current_layout_obj;
- if (!parent_node)
+ if (!CanComputeAsNaturalParent(parent_node))
return nullptr;
+ if (AXObject* ax_parent = cache.GetOrCreate(parent_layout_obj)) {
+ DCHECK(!ax_parent->IsDetached());
+ DCHECK(ax_parent->ShouldUseLayoutObjectTraversalForChildren())
+ << "Do not compute a parent that cannot have this as a child.";
+ return ax_parent->CanHaveChildren() ? ax_parent : nullptr;
+ }
+ return nullptr;
}
DCHECK(current_node->isConnected())
<< "Should not call ComputeParent() with disconnected node: "
<< current_node;
- while (true) {
- current_node = GetParentNodeForComputeParent(current_node);
- if (!current_node)
- break;
- AXObject* ax_parent = AXObjectCache().GetOrCreate(current_node);
- if (ax_parent) {
- DCHECK(!ax_parent->IsDetached());
- return ax_parent;
+ // A WebArea's parent should be the page popup owner, if any, otherwise null.
+ if (auto* document = DynamicTo<Document>(current_node)) {
+ LocalFrame* frame = document->GetFrame();
+ DCHECK(frame);
+ return cache.GetOrCreate(frame->PagePopupOwner());
+ }
+
+ // For <option> in <select size=1>, return the popup.
+ if (AXObjectCacheImpl::UseAXMenuList()) {
+ if (auto* option = DynamicTo<HTMLOptionElement>(current_node)) {
+ if (AXObject* ax_select =
+ AXMenuListOption::ComputeParentAXMenuPopupFor(cache, option)) {
+ return ax_select;
+ }
+ }
+ }
+
+ // For <area>, return the image it is a child link of.
+ if (IsA<HTMLAreaElement>(current_node)) {
+ if (AXObject* ax_image =
+ AXImageMapLink::GetAXObjectForImageMap(cache, current_node)) {
+ return ax_image;
}
}
+ Node* parent_node = GetParentNodeForComputeParent(current_node);
+ if (!parent_node) {
+ // This occurs when a DOM child isn't visited by LayoutTreeBuilderTraversal,
+ // such as an element child of a <textarea>, which only supports plain text.
+ return nullptr;
+ }
+
+ // When the flag to use AXMenuList in on, a menu list is only allowed to
+ // parent an AXMenuListPopup, which is added as a child on creation. No other
+ // children are allowed, and nullptr is returned for anything else where the
+ // parent would be AXMenuList.
+ if (AXObjectCacheImpl::ShouldCreateAXMenuListFor(
+ parent_node->GetLayoutObject())) {
+ return nullptr;
+ }
+
+ if (!CanComputeAsNaturalParent(parent_node))
+ return nullptr;
+
+ if (AXObject* ax_parent = cache.GetOrCreate(parent_node)) {
+ DCHECK(!ax_parent->IsDetached());
+ // If the parent can't have children, then return null so that the caller
+ // knows that it is not a relevant natural parent, as it is a leaf.
+ return ax_parent->CanHaveChildren() ? ax_parent : nullptr;
+ }
+
+ // Could not create AXObject for |parent_node|, therefore there is no relevant
+ // natural parent. For example, the AXObject that would have been created
+ // would have been a descendant of a leaf, or otherwise an illegal child of a
+ // specialized object.
return nullptr;
}
@@ -935,6 +818,8 @@ void AXObject::EnsureCorrectParentComputation() {
DCHECK(!parent_->IsDetached());
+ DCHECK(parent_->CanHaveChildren());
+
// Don't check the computed parent if the cached parent is a mock object.
// It is expected that a computed parent could never be a mock object,
// which has no backing DOM node or layout object, and therefore cannot be
@@ -958,24 +843,28 @@ void AXObject::EnsureCorrectParentComputation() {
if (GetNode() && GetNode()->IsPseudoElement())
return;
- // Verify that the algorithm in ComputeParentImpl() provides same results as
- // parents that init their children with themselves as the parent_if_known.
- // Inconsistency indicates a problem could potentially exist where a child's
- // parent does not include the child in its children.
- DCHECK(ComputeParentImpl())
- << "Computed parent was null for " << this << ", expected " << parent_;
- DCHECK_EQ(ComputeParentImpl(), parent_)
+ // Verify that the algorithm in ComputeParent() provides same results as
+ // parents that init their children with themselves as the parent.
+ // Inconsistency indicates a problem could potentially exist where a child's
+ // parent does not include the child in its children.
+#if DCHECK_IS_ON()
+ AXObject* computed_parent = ComputeParent();
+
+ DCHECK(computed_parent) << "Computed parent was null for " << this
+ << ", expected " << parent_;
+ DCHECK_EQ(computed_parent, parent_)
<< "\n**** ComputeParent should have provided the same result as "
"the known parent.\n**** Computed parent layout object was "
- << ComputeParentImpl()->GetLayoutObject()
+ << computed_parent->GetLayoutObject()
<< "\n**** Actual parent's layout object was "
<< parent_->GetLayoutObject() << "\n**** Child was " << this;
+#endif
}
#endif
const AtomicString& AXObject::GetAOMPropertyOrARIAAttribute(
AOMStringProperty property) const {
- Element* element = this->GetElement();
+ Element* element = GetElement();
if (!element)
return g_null_atom;
@@ -984,7 +873,7 @@ const AtomicString& AXObject::GetAOMPropertyOrARIAAttribute(
Element* AXObject::GetAOMPropertyOrARIAAttribute(
AOMRelationProperty property) const {
- Element* element = this->GetElement();
+ Element* element = GetElement();
if (!element)
return nullptr;
@@ -993,7 +882,7 @@ Element* AXObject::GetAOMPropertyOrARIAAttribute(
bool AXObject::HasAOMProperty(AOMRelationListProperty property,
HeapVector<Member<Element>>& result) const {
- Element* element = this->GetElement();
+ Element* element = GetElement();
if (!element)
return false;
@@ -1003,7 +892,7 @@ bool AXObject::HasAOMProperty(AOMRelationListProperty property,
bool AXObject::HasAOMPropertyOrARIAAttribute(
AOMRelationListProperty property,
HeapVector<Member<Element>>& result) const {
- Element* element = this->GetElement();
+ Element* element = GetElement();
if (!element)
return false;
@@ -1012,7 +901,7 @@ bool AXObject::HasAOMPropertyOrARIAAttribute(
bool AXObject::HasAOMPropertyOrARIAAttribute(AOMBooleanProperty property,
bool& result) const {
- Element* element = this->GetElement();
+ Element* element = GetElement();
if (!element)
return false;
@@ -1040,7 +929,7 @@ bool AXObject::AOMPropertyOrARIAAttributeIsFalse(
bool AXObject::HasAOMPropertyOrARIAAttribute(AOMUIntProperty property,
uint32_t& result) const {
- Element* element = this->GetElement();
+ Element* element = GetElement();
if (!element)
return false;
@@ -1052,7 +941,7 @@ bool AXObject::HasAOMPropertyOrARIAAttribute(AOMUIntProperty property,
bool AXObject::HasAOMPropertyOrARIAAttribute(AOMIntProperty property,
int32_t& result) const {
- Element* element = this->GetElement();
+ Element* element = GetElement();
if (!element)
return false;
@@ -1064,7 +953,7 @@ bool AXObject::HasAOMPropertyOrARIAAttribute(AOMIntProperty property,
bool AXObject::HasAOMPropertyOrARIAAttribute(AOMFloatProperty property,
float& result) const {
- Element* element = this->GetElement();
+ Element* element = GetElement();
if (!element)
return false;
@@ -1076,7 +965,7 @@ bool AXObject::HasAOMPropertyOrARIAAttribute(AOMFloatProperty property,
bool AXObject::HasAOMPropertyOrARIAAttribute(AOMStringProperty property,
AtomicString& result) const {
- Element* element = this->GetElement();
+ Element* element = GetElement();
if (!element)
return false;
@@ -1118,7 +1007,7 @@ void AXObject::Serialize(ui::AXNodeData* node_data,
node_data->AddState(ax::mojom::blink::State::kEditable);
if (IsEditableRoot()) {
node_data->AddBoolAttribute(
- ax::mojom::blink::BoolAttribute::kEditableRoot, true);
+ ax::mojom::blink::BoolAttribute::kContentEditableRoot, true);
}
if (IsRichlyEditable())
node_data->AddState(ax::mojom::blink::State::kRichlyEditable);
@@ -1227,7 +1116,7 @@ void AXObject::SerializeUnignoredAttributes(ui::AXNodeData* node_data,
if (auto* html_frame_owner_element =
DynamicTo<HTMLFrameOwnerElement>(GetElement())) {
if (Frame* child_frame = html_frame_owner_element->ContentFrame()) {
- base::Optional<base::UnguessableToken> child_token =
+ absl::optional<base::UnguessableToken> child_token =
child_frame->GetEmbeddingToken();
if (child_token && !(IsDetached() || ChildCountIncludingIgnored())) {
ui::AXTreeID child_tree_id =
@@ -1259,6 +1148,14 @@ void AXObject::SerializeUnignoredAttributes(ui::AXNodeData* node_data,
SerializeTableAttributes(node_data);
}
+ if (accessibility_mode.has_mode(ui::AXMode::kScreenReader)) {
+ // Whether it has ARIA attributes at all.
+ if (HasAriaAttribute()) {
+ node_data->AddBoolAttribute(
+ ax::mojom::blink::BoolAttribute::kHasAriaAttribute, true);
+ }
+ }
+
if (accessibility_mode.has_mode(ui::AXMode::kPDF)) {
// Return early. None of the following attributes are needed for PDFs.
return;
@@ -1295,21 +1192,7 @@ void AXObject::SerializeUnignoredAttributes(ui::AXNodeData* node_data,
SerializeSparseAttributes(node_data);
if (Element* element = GetElement()) {
- if (const AtomicString& aria_role =
- GetAOMPropertyOrARIAAttribute(AOMStringProperty::kRole)) {
- TruncateAndAddStringAttribute(node_data,
- ax::mojom::blink::StringAttribute::kRole,
- aria_role.Utf8());
- } else {
- std::string role_str = GetEquivalentAriaRoleString(RoleValue());
- if (!role_str.empty()) {
- TruncateAndAddStringAttribute(node_data,
- ax::mojom::blink::StringAttribute::kRole,
- GetEquivalentAriaRoleString(RoleValue()));
- }
- }
-
- if (IsNativeTextField()) {
+ if (IsAtomicTextField()) {
// Selection offsets are only used for plain text controls, (input of a
// text field type, and textarea). Rich editable areas, such as
// contenteditables, use AXTreeData.
@@ -1424,7 +1307,7 @@ void AXObject::SerializeScrollAttributes(ui::AXNodeData* node_data) {
}
void AXObject::SerializeElementAttributes(ui::AXNodeData* node_data) {
- Element* element = this->GetElement();
+ Element* element = GetElement();
if (!element)
return;
@@ -1440,10 +1323,11 @@ void AXObject::SerializeElementAttributes(ui::AXNodeData* node_data) {
TruncateAndAddStringAttribute(
node_data, ax::mojom::blink::StringAttribute::kRole, aria_role.Utf8());
} else {
- std::string role = GetEquivalentAriaRoleString(RoleValue());
- if (!role.empty()) {
- TruncateAndAddStringAttribute(
- node_data, ax::mojom::blink::StringAttribute::kRole, role);
+ const AtomicString& role_str = GetEquivalentAriaRoleName(RoleValue());
+ if (role_str != g_null_atom) {
+ TruncateAndAddStringAttribute(node_data,
+ ax::mojom::blink::StringAttribute::kRole,
+ role_str.Ascii());
}
}
}
@@ -1740,7 +1624,7 @@ bool AXObject::IsAnchor() const {
}
bool AXObject::IsARIATextField() const {
- if (IsNativeTextField())
+ if (IsAtomicTextField())
return false; // Native role supercedes the ARIA one.
return AriaRoleAttribute() == ax::mojom::blink::Role::kTextField ||
AriaRoleAttribute() == ax::mojom::blink::Role::kSearchBox ||
@@ -1755,19 +1639,6 @@ bool AXObject::IsCanvas() const {
return RoleValue() == ax::mojom::blink::Role::kCanvas;
}
-bool AXObject::IsCheckboxOrRadio() const {
- switch (RoleValue()) {
- case ax::mojom::blink::Role::kCheckBox:
- case ax::mojom::blink::Role::kMenuItemCheckBox:
- case ax::mojom::blink::Role::kMenuItemRadio:
- case ax::mojom::blink::Role::kRadioButton:
- return true;
- default:
- break;
- }
- return false;
-}
-
bool AXObject::IsColorWell() const {
return RoleValue() == ax::mojom::blink::Role::kColorWell;
}
@@ -1863,7 +1734,7 @@ ax::mojom::blink::CheckedState AXObject::CheckedState() const {
// Native checked state
if (role != ax::mojom::blink::Role::kToggleButton) {
- const Node* node = this->GetNode();
+ const Node* node = GetNode();
if (!node)
return ax::mojom::blink::CheckedState::kNone;
@@ -1957,15 +1828,15 @@ bool AXObject::IsNativeSpinButton() const {
return false;
}
-bool AXObject::IsNativeTextField() const {
+bool AXObject::IsAtomicTextField() const {
return blink::IsTextControl(GetNode());
}
-bool AXObject::IsNonNativeTextField() const {
+bool AXObject::IsNonAtomicTextField() const {
// Consivably, an <input type=text> or a <textarea> might also have the
// contenteditable attribute applied. In such cases, the <input> or <textarea>
// tags should supercede.
- if (IsNativeTextField())
+ if (IsAtomicTextField())
return false;
return HasContentEditableAttributeSet() || IsARIATextField();
}
@@ -2027,7 +1898,7 @@ bool AXObject::IsTabItem() const {
bool AXObject::IsTextField() const {
if (IsDetached())
return false;
- return IsNativeTextField() || IsNonNativeTextField();
+ return IsAtomicTextField() || IsNonAtomicTextField();
}
bool AXObject::IsAutofillAvailable() const {
@@ -2140,12 +2011,10 @@ void AXObject::UpdateCachedAttributeValuesIfNeeded(
DocumentLifecycle::kAfterPerformLayout)
<< "Unclean document at lifecycle "
<< GetDocument()->Lifecycle().ToString();
-#endif
+#endif // DCHECK_IS_ON()
- // TODO(accessibility) Every AXObject must have a parent except the root.
- // Sometimes the parent is detached and a new parent isn't yet reattached.
- if (!parent_)
- parent_ = ComputeParent();
+ if (IsMissingParent())
+ RepairMissingParent();
cached_is_hidden_via_style = ComputeIsHiddenViaStyle();
@@ -2160,10 +2029,7 @@ void AXObject::UpdateCachedAttributeValuesIfNeeded(
SetNeedsToUpdateChildren();
cached_is_inert_or_aria_hidden_ = is_inert_or_aria_hidden;
}
- cached_is_descendant_of_leaf_node_ = !!LeafNodeAncestor();
cached_is_descendant_of_disabled_node_ = !!DisabledAncestor();
- cached_has_inherited_presentational_role_ =
- !!InheritsPresentationalRoleFrom();
bool is_ignored = ComputeAccessibilityIsIgnored();
bool is_ignored_but_included_in_tree =
@@ -2178,7 +2044,8 @@ void AXObject::UpdateCachedAttributeValuesIfNeeded(
DisplayLockUtilities::NearestLockedExclusiveAncestor(*GetNode())) {
DCHECK(!cached_is_ignored_but_included_in_tree_)
<< "Display locked text should not be included in the tree (subject to "
- "future rule change)";
+ "future rule change): "
+ << ToString(true, true);
}
#endif
bool included_in_tree_changed = false;
@@ -2207,7 +2074,6 @@ void AXObject::UpdateCachedAttributeValuesIfNeeded(
cached_is_ignored_ = is_ignored;
cached_is_ignored_but_included_in_tree_ = is_ignored_but_included_in_tree;
- cached_is_editable_root_ = ComputeIsEditableRoot();
// Compute live region root, which can be from any ARIA live value, including
// "off", or from an automatic ARIA live value, e.g. from role="status".
// TODO(dmazzoni): remove this const_cast.
@@ -2236,9 +2102,10 @@ void AXObject::UpdateCachedAttributeValuesIfNeeded(
// "not handle a signal and call ChilldrenChanged() earlier."
// << "\nChild: " << ToString(true)
// << "\nParent: " << parent->ToString(true);
- // Defer this ChildrenChanged(), otherwise it can cause reentry into
+ // Defers a ChildrenChanged() on the first included ancestor.
+ // Must defer it, otherwise it can cause reentry into
// UpdateCachedAttributeValuesIfNeeded() on |this|.
- AXObjectCache().ChildrenChanged(parent);
+ AXObjectCache().ChildrenChangedOnAncestorOf(const_cast<AXObject*>(this));
}
}
@@ -2371,22 +2238,6 @@ bool AXObject::IsVisible() const {
return !IsInertOrAriaHidden() && !IsHiddenViaStyle();
}
-bool AXObject::IsDescendantOfLeafNode() const {
- UpdateCachedAttributeValuesIfNeeded();
- return cached_is_descendant_of_leaf_node_;
-}
-
-AXObject* AXObject::LeafNodeAncestor() const {
- if (AXObject* parent = ParentObject()) {
- if (!parent->CanHaveChildren())
- return parent;
-
- return parent->LeafNodeAncestor();
- }
-
- return nullptr;
-}
-
const AXObject* AXObject::AriaHiddenRoot() const {
for (const AXObject* object = this; object; object = object->ParentObject()) {
if (object->AOMPropertyOrARIAAttributeIsTrue(AOMBooleanProperty::kHidden))
@@ -2554,28 +2405,81 @@ bool AXObject::ComputeAccessibilityIsIgnoredButIncludedInTree() const {
return true;
}
- // Include all pseudo element content. Any anonymous subtree is included
- // from above, in the condition where there is no node.
- if (node->IsPseudoElement())
+ // Allow the browser side ax tree to access "visibility: [hidden|collapse]"
+ // and "display: none" nodes. This is useful for APIs that return the node
+ // referenced by aria-labeledby and aria-describedby.
+ // An element must have an id attribute or it cannot be referenced by
+ // aria-labelledby or aria-describedby.
+ if (RuntimeEnabledFeatures::AccessibilityExposeDisplayNoneEnabled()) {
+ if (Element* element = GetElement()) {
+ if (element->FastHasAttribute(html_names::kIdAttr) &&
+ IsHiddenViaStyle()) {
+ return true;
+ }
+ }
+ } else if (GetLayoutObject()) {
+ if (GetLayoutObject()->Style()->Visibility() != EVisibility::kVisible)
+ return true;
+ }
+
+ // Allow the browser side ax tree to access "aria-hidden" nodes.
+ // This is useful for APIs that return the node referenced by
+ // aria-labeledby and aria-describedby.
+ if (GetLayoutObject() && IsAriaHidden())
return true;
+ // Custom elements and their children are included in the tree.
// <slot>s and their children are included in the tree.
- // TODO(accessibility) Consider including all shadow content; however, this
- // can actually be a lot of nodes inside of a web component, e.g. svg.
- if (IsA<HTMLSlotElement>(node))
+ // This checks to see if this a child one of those.
+ if (Node* parent_node = LayoutTreeBuilderTraversal::Parent(*node)) {
+ if (parent_node->IsCustomElement() || IsA<HTMLSlotElement>(parent_node))
+ return true;
+ }
+
+ Element* element = GetElement();
+ if (!element)
+ return false;
+
+ // Custom elements and their children are included in the tree.
+ if (element->IsCustomElement())
+ return true;
+
+ // <slot>s and their children are included in the tree.
+ // Detailed explanation:
+ // <slot> elements are placeholders marking locations in a shadow tree where
+ // users of a web component can insert their own custom nodes. Inserted nodes
+ // (also known as distributed nodes) become children of their respective slots
+ // in the accessibility tree. In other words, the accessibility tree mirrors
+ // the flattened DOM tree or the layout tree, not the original DOM tree.
+ // Distributed nodes still maintain their parent relations and computed style
+ // information with their original location in the DOM. Therefore, we need to
+ // ensure that in the accessibility tree no remnant information from the
+ // unflattened DOM tree remains, such as the cached parent.
+ if (IsA<HTMLSlotElement>(element))
return true;
- if (CachedParentObject() &&
- IsA<HTMLSlotElement>(CachedParentObject()->GetNode()))
+
+ // Include all pseudo element content. Any anonymous subtree is included
+ // from above, in the condition where there is no node.
+ if (element->IsPseudoElement())
return true;
- if (GetElement() && GetElement()->IsCustomElement())
+ // Include all parents of ::before/::after/::marker pseudo elements to help
+ // ClearChildren() find all children, and assist naming computation.
+ // It is unnecessary to include a rule for other types of pseudo elements:
+ // Specifically, ::first-letter/::backdrop are not visited by
+ // LayoutTreeBuilderTraversal, and cannot be in the tree, therefore do not add
+ // a special rule to include their parents.
+ if (element->GetPseudoElement(kPseudoIdBefore) ||
+ element->GetPseudoElement(kPseudoIdAfter) ||
+ element->GetPseudoElement(kPseudoIdMarker)) {
return true;
+ }
// Use a flag to control whether or not the <html> element is included
// in the accessibility tree. Either way it's always marked as "ignored",
// but eventually we want to always include it in the tree to simplify
// some logic.
- if (IsA<HTMLHtmlElement>(node))
+ if (IsA<HTMLHtmlElement>(element))
return RuntimeEnabledFeatures::AccessibilityExposeHTMLElementEnabled();
// Keep the internal accessibility tree consistent for videos which lack
@@ -2590,41 +2494,24 @@ bool AXObject::ComputeAccessibilityIsIgnoredButIncludedInTree() const {
if (IsLineBreakingObject())
return true;
- // Allow the browser side ax tree to access "visibility: [hidden|collapse]"
- // and "display: none" nodes. This is useful for APIs that return the node
- // referenced by aria-labeledby and aria-describedby.
- // An element must have an id attribute or it cannot be referenced by
- // aria-labelledby or aria-describedby.
- if (RuntimeEnabledFeatures::AccessibilityExposeDisplayNoneEnabled()) {
- if (Element* element = GetElement()) {
- if (element->FastHasAttribute(html_names::kIdAttr) &&
- IsHiddenViaStyle()) {
- return true;
- }
- }
- } else if (GetLayoutObject()) {
- if (GetLayoutObject()->Style()->Visibility() != EVisibility::kVisible)
- return true;
- }
-
- // Allow the browser side ax tree to access "aria-hidden" nodes.
- // This is useful for APIs that return the node referenced by
- // aria-labeledby and aria-describedby.
- if (GetLayoutObject() && IsAriaHidden())
- return true;
-
// Preserve SVG grouping elements.
- if (IsA<SVGGElement>(node))
+ if (IsA<SVGGElement>(element))
return true;
// Keep table-related elements in the tree, because it's too easy for them
// to in and out of being ignored based on their ancestry, as their role
// can depend on several levels up in the hierarchy.
- if (IsA<HTMLTableElement>(node) || IsA<HTMLTableSectionElement>(node) ||
- IsA<HTMLTableRowElement>(node) || IsA<HTMLTableCellElement>(node)) {
+ if (IsA<HTMLTableElement>(element) || IsA<HTMLTableSectionElement>(element) ||
+ IsA<HTMLTableRowElement>(element) || IsA<HTMLTableCellElement>(element)) {
return true;
}
+ // Ensure clean teardown of AXMenuList.
+ if (auto* option = DynamicTo<HTMLOptionElement>(element)) {
+ if (option->OwnerSelectElement())
+ return true;
+ }
+
// Preserve nodes with language attributes.
if (HasAttribute(html_names::kLangAttr))
return true;
@@ -2632,16 +2519,16 @@ bool AXObject::ComputeAccessibilityIsIgnoredButIncludedInTree() const {
return false;
}
-const AXObject* AXObject::GetNativeTextControlAncestor(
+const AXObject* AXObject::GetAtomicTextFieldAncestor(
int max_levels_to_check) const {
- if (IsNativeTextField())
+ if (IsAtomicTextField())
return this;
if (max_levels_to_check == 0)
return nullptr;
if (AXObject* parent = ParentObject())
- return parent->GetNativeTextControlAncestor(max_levels_to_check - 1);
+ return parent->GetAtomicTextFieldAncestor(max_levels_to_check - 1);
return nullptr;
}
@@ -2667,10 +2554,15 @@ const AXObject* AXObject::DatetimeAncestor(int max_levels_to_check) const {
}
bool AXObject::LastKnownIsIgnoredValue() const {
+ DCHECK(cached_is_ignored_ || !IsDetached())
+ << "A detached object should always indicate that it is ignored so that "
+ "it won't ever accidentally be included in the tree.";
return cached_is_ignored_;
}
bool AXObject::LastKnownIsIgnoredButIncludedInTreeValue() const {
+ DCHECK(!cached_is_ignored_but_included_in_tree_ || !IsDetached())
+ << "A detached object should never be included in the tree.";
return cached_is_ignored_but_included_in_tree_;
}
@@ -2685,11 +2577,6 @@ ax::mojom::blink::Role AXObject::DetermineAccessibilityRole() {
return NativeRoleIgnoringAria();
}
-bool AXObject::HasInheritedPresentationalRole() const {
- UpdateCachedAttributeValuesIfNeeded();
- return cached_has_inherited_presentational_role_;
-}
-
bool AXObject::CanSetValueAttribute() const {
switch (RoleValue()) {
case ax::mojom::blink::Role::kColorWell:
@@ -3163,7 +3050,7 @@ String AXObject::AriaTextAlternative(bool recursive,
if (element) {
HeapVector<Member<Element>> elements_from_attribute;
Vector<String> ids;
- ElementsFromAttribute(elements_from_attribute, attr, ids);
+ ElementsFromAttribute(element, elements_from_attribute, attr, ids);
const AtomicString& aria_labelledby = GetAttribute(attr);
@@ -3257,13 +3144,14 @@ String AXObject::TextFromElements(
return accumulated_text.ToString();
}
-void AXObject::TokenVectorFromAttribute(Vector<String>& tokens,
- const QualifiedName& attribute) const {
- Node* node = this->GetNode();
- if (!node || !node->IsElementNode())
+// static
+void AXObject::TokenVectorFromAttribute(Element* element,
+ Vector<String>& tokens,
+ const QualifiedName& attribute) {
+ if (!element)
return;
- String attribute_value = GetAttribute(attribute).GetString();
+ String attribute_value = element->FastGetAttribute(attribute).GetString();
if (attribute_value.IsEmpty())
return;
@@ -3271,47 +3159,84 @@ void AXObject::TokenVectorFromAttribute(Vector<String>& tokens,
attribute_value.Split(' ', tokens);
}
-void AXObject::ElementsFromAttribute(HeapVector<Member<Element>>& elements,
+// static
+bool AXObject::ElementsFromAttribute(Element* from,
+ HeapVector<Member<Element>>& elements,
const QualifiedName& attribute,
- Vector<String>& ids) const {
+ Vector<String>& ids) {
+ if (!from)
+ return false;
+
// We compute the attr-associated elements, which are either explicitly set
// element references set via the IDL, or computed from the content attribute.
- TokenVectorFromAttribute(ids, attribute);
- Element* element = GetElement();
- if (!element)
- return;
+ TokenVectorFromAttribute(from, ids, attribute);
- base::Optional<HeapVector<Member<Element>>> attr_associated_elements =
- element->GetElementArrayAttribute(attribute);
+ absl::optional<HeapVector<Member<Element>>> attr_associated_elements =
+ from->GetElementArrayAttribute(attribute);
if (!attr_associated_elements)
- return;
+ return false;
for (const auto& element : attr_associated_elements.value())
elements.push_back(element);
+
+ return elements.size();
}
-void AXObject::AriaLabelledbyElementVector(
+// static
+bool AXObject::AriaLabelledbyElementVector(
+ Element* from,
HeapVector<Member<Element>>& elements,
- Vector<String>& ids) const {
+ Vector<String>& ids) {
// Try both spellings, but prefer aria-labelledby, which is the official spec.
- ElementsFromAttribute(elements, html_names::kAriaLabelledbyAttr, ids);
- if (!ids.size())
- ElementsFromAttribute(elements, html_names::kAriaLabeledbyAttr, ids);
+ if (ElementsFromAttribute(from, elements, html_names::kAriaLabelledbyAttr,
+ ids)) {
+ return true;
+ }
+
+ return ElementsFromAttribute(from, elements, html_names::kAriaLabeledbyAttr,
+ ids);
+}
+
+// static
+bool AXObject::IsNameFromAriaAttribute(Element* element) {
+ // TODO(accessibility) Make this work for virtual nodes.
+
+ if (!element)
+ return false;
+
+ HeapVector<Member<Element>> elements_from_attribute;
+ Vector<String> ids;
+ if (AriaLabelledbyElementVector(element, elements_from_attribute, ids))
+ return true;
+
+ const AtomicString& aria_label = AccessibleNode::GetPropertyOrARIAAttribute(
+ element, AOMStringProperty::kLabel);
+ if (!aria_label.IsEmpty())
+ return true;
+
+ return false;
+}
+
+bool AXObject::IsNameFromAuthorAttribute() const {
+ return IsNameFromAriaAttribute(GetElement()) ||
+ HasAttribute(html_names::kTitleAttr);
}
String AXObject::TextFromAriaLabelledby(AXObjectSet& visited,
AXRelatedObjectVector* related_objects,
Vector<String>& ids) const {
HeapVector<Member<Element>> elements;
- AriaLabelledbyElementVector(elements, ids);
+ AriaLabelledbyElementVector(GetElement(), elements, ids);
return TextFromElements(true, visited, elements, related_objects);
}
String AXObject::TextFromAriaDescribedby(AXRelatedObjectVector* related_objects,
Vector<String>& ids) const {
AXObjectSet visited;
+
HeapVector<Member<Element>> elements;
- ElementsFromAttribute(elements, html_names::kAriaDescribedbyAttr, ids);
+ ElementsFromAttribute(GetElement(), elements,
+ html_names::kAriaDescribedbyAttr, ids);
return TextFromElements(true, visited, elements, related_objects);
}
@@ -3321,7 +3246,12 @@ AccessibilityOrientation AXObject::Orientation() const {
return kAccessibilityOrientationUndefined;
}
-void AXObject::LoadInlineTextBoxes() {}
+void AXObject::LoadInlineTextBoxes() {
+ base::AutoReset<bool> reentrancy_protector(&is_loading_inline_boxes_, true);
+ LoadInlineTextBoxesRecursive();
+}
+
+void AXObject::LoadInlineTextBoxesRecursive() {}
AXObject* AXObject::NextOnLine() const {
return nullptr;
@@ -3331,7 +3261,7 @@ AXObject* AXObject::PreviousOnLine() const {
return nullptr;
}
-base::Optional<const DocumentMarker::MarkerType>
+absl::optional<const DocumentMarker::MarkerType>
AXObject::GetAriaSpellingOrGrammarMarker() const {
AtomicString aria_invalid_value;
const AncestorsIterator iter = std::find_if(
@@ -3343,12 +3273,12 @@ AXObject::GetAriaSpellingOrGrammarMarker() const {
});
if (iter == UnignoredAncestorsEnd())
- return base::nullopt;
+ return absl::nullopt;
if (EqualIgnoringASCIICase(aria_invalid_value, "spelling"))
return DocumentMarker::kSpelling;
if (EqualIgnoringASCIICase(aria_invalid_value, "grammar"))
return DocumentMarker::kGrammar;
- return base::nullopt;
+ return absl::nullopt;
}
void AXObject::TextCharacterOffsets(Vector<int>&) const {}
@@ -3357,7 +3287,7 @@ void AXObject::GetWordBoundaries(Vector<int>& word_starts,
Vector<int>& word_ends) const {}
int AXObject::TextLength() const {
- if (IsNativeTextField())
+ if (IsAtomicTextField())
return GetValueForControl().length();
return 0;
}
@@ -3461,76 +3391,60 @@ bool AXObject::SupportsARIAExpanded() const {
}
}
-bool IsGlobalARIAAttribute(const AtomicString& name) {
- if (!name.StartsWith("ARIA"))
- return false;
- if (name.StartsWith("ARIA-ATOMIC"))
- return true;
- if (name.StartsWith("ARIA-BUSY"))
- return true;
- if (name.StartsWith("ARIA-CONTROLS"))
- return true;
- if (name.StartsWith("ARIA-CURRENT"))
- return true;
- if (name.StartsWith("ARIA-DESCRIBEDBY"))
- return true;
- if (name.StartsWith("ARIA-DETAILS"))
- return true;
- if (name.StartsWith("ARIA-DISABLED"))
- return true;
- if (name.StartsWith("ARIA-DROPEFFECT"))
- return true;
- if (name.StartsWith("ARIA-ERRORMESSAGE"))
- return true;
- if (name.StartsWith("ARIA-FLOWTO"))
- return true;
- if (name.StartsWith("ARIA-GRABBED"))
- return true;
- if (name.StartsWith("ARIA-HASPOPUP"))
- return true;
- if (name.StartsWith("ARIA-HIDDEN"))
- return true;
- if (name.StartsWith("ARIA-INVALID"))
- return true;
- if (name.StartsWith("ARIA-KEYSHORTCUTS"))
- return true;
- if (name.StartsWith("ARIA-LABEL"))
- return true;
- if (name.StartsWith("ARIA-LABELEDBY"))
- return true;
- if (name.StartsWith("ARIA-LABELLEDBY"))
- return true;
- if (name.StartsWith("ARIA-LIVE"))
- return true;
- if (name.StartsWith("ARIA-OWNS"))
- return true;
- if (name.StartsWith("ARIA-RELEVANT"))
- return true;
- if (name.StartsWith("ARIA-ROLEDESCRIPTION"))
- return true;
- return false;
-}
-
-bool AXObject::HasGlobalARIAAttribute() const {
+bool DoesUndoRolePresentation(const AtomicString& name) {
+ // This is the list of global ARIA properties that force
+ // role="presentation"/"none" to be exposed, and does not contain ARIA
+ // properties who's global status is being deprecated.
+ // clang-format off
+ DEFINE_STATIC_LOCAL(
+ HashSet<AtomicString>, aria_global_properties,
+ ({
+ "ARIA-ATOMIC",
+ // TODO(accessibility/ARIA 1.3) Add (and test in aria-global.html)
+ // "ARIA-BRAILLEROLEDESCRIPTION",
+ "ARIA-BUSY",
+ "ARIA-CONTROLS",
+ "ARIA-CURRENT",
+ "ARIA-DESCRIBEDBY",
+ "ARIA-DESCRIPTION",
+ "ARIA-DETAILS",
+ "ARIA-DROPEFFECT",
+ "ARIA-FLOWTO",
+ "ARIA-GRABBED",
+ "ARIA-HIDDEN", // For aria-hidden=false.
+ "ARIA-KEYSHORTCUTS",
+ "ARIA-LIVE",
+ "ARIA-OWNS",
+ "ARIA-RELEVANT",
+ "ARIA-ROLEDESCRIPTION"
+ }));
+ // clang-format on
+
+ return aria_global_properties.Contains(name);
+}
+
+bool AXObject::HasAriaAttribute(bool does_undo_role_presentation) const {
auto* element = GetElement();
if (!element)
return false;
+ // A role is considered an ARIA attribute.
+ if (!does_undo_role_presentation &&
+ AriaRoleAttribute() != ax::mojom::blink::Role::kUnknown) {
+ return true;
+ }
+
+ // Check for any attribute that begins with "aria-".
AttributeCollection attributes = element->AttributesWithoutUpdate();
for (const Attribute& attr : attributes) {
// Attributes cache their uppercase names.
auto name = attr.GetName().LocalNameUpper();
- if (IsGlobalARIAAttribute(name))
- return true;
- }
- if (!element->DidAttachInternals())
- return false;
- const auto& internals_attributes =
- element->EnsureElementInternals().GetAttributes();
- for (const QualifiedName& attr : internals_attributes.Keys()) {
- if (IsGlobalARIAAttribute(attr.LocalNameUpper()))
- return true;
+ if (name.StartsWith("ARIA-")) {
+ if (!does_undo_role_presentation || DoesUndoRolePresentation(name))
+ return true;
+ }
}
+
return false;
}
@@ -3637,13 +3551,28 @@ ax::mojom::blink::Role AXObject::AriaRoleAttribute() const {
return ax::mojom::blink::Role::kUnknown;
}
-ax::mojom::blink::Role AXObject::DetermineAriaRoleAttribute() const {
+ax::mojom::blink::Role AXObject::RawAriaRole() const {
const AtomicString& aria_role =
GetAOMPropertyOrARIAAttribute(AOMStringProperty::kRole);
if (aria_role.IsNull() || aria_role.IsEmpty())
return ax::mojom::blink::Role::kUnknown;
+ return AriaRoleStringToRoleEnum(aria_role);
+}
- ax::mojom::blink::Role role = AriaRoleToWebCoreRole(aria_role);
+ax::mojom::blink::Role AXObject::DetermineAriaRoleAttribute() const {
+ ax::mojom::blink::Role role = RawAriaRole();
+
+ if (role == ax::mojom::blink::Role::kRegion && !IsNameFromAuthorAttribute() &&
+ !HasAttribute(html_names::kAriaRoledescriptionAttr)) {
+ // Nameless ARIA regions fall back on the native element's role.
+ // We only check aria-label/aria-labelledby because those are the only
+ // allowed ways to name an ARIA region.
+ // TODO(accessibility) The aria-roledescription logic is required, otherwise
+ // ChromeVox will ignore the aria-roledescription. It only speaks the role
+ // description on certain roles, and ignores it on the generic role.
+ // See also https://github.com/w3c/aria/issues/1463.
+ return ax::mojom::blink::Role::kUnknown;
+ }
// ARIA states if an item can get focus, it should not be presentational.
// It also states user agents should ignore the presentational role if
@@ -3652,9 +3581,13 @@ ax::mojom::blink::Role AXObject::DetermineAriaRoleAttribute() const {
if (IsA<HTMLIFrameElement>(*GetNode()) || IsA<HTMLFrameElement>(*GetNode()))
return ax::mojom::blink::Role::kIframePresentational;
if ((GetElement() && GetElement()->SupportsFocus()) ||
- HasGlobalARIAAttribute()) {
- // If we return an unknown role, then the native HTML role would be used
- // instead.
+ HasAriaAttribute(true /* does_undo_role_presentation */)) {
+ // Must be exposed with a role if focusable or has a global ARIA property
+ // that is allowed in this context. See
+ // https://w3c.github.io/aria/#presentation for more information about the
+ // conditions upon which elements with role="none"/"presentation" must be
+ // included in the tree. Return Role::kUnknown, so that the native HTML
+ // role is used instead.
return ax::mojom::blink::Role::kUnknown;
}
}
@@ -3671,7 +3604,7 @@ ax::mojom::blink::Role AXObject::DetermineAriaRoleAttribute() const {
// ax::mojom::blink::Role::kComboBoxMenuButton:
// <div tabindex=0 role="combobox">Select</div>
if (role == ax::mojom::blink::Role::kComboBoxGrouping) {
- if (IsNativeTextField())
+ if (IsAtomicTextField())
role = ax::mojom::blink::Role::kTextFieldWithComboBox;
else if (GetElement() && GetElement()->SupportsFocus())
role = ax::mojom::blink::Role::kComboBoxMenuButton;
@@ -3680,9 +3613,71 @@ ax::mojom::blink::Role AXObject::DetermineAriaRoleAttribute() const {
return role;
}
+ax::mojom::blink::HasPopup AXObject::HasPopup() const {
+ return ax::mojom::blink::HasPopup::kFalse;
+}
+
+bool AXObject::IsEditable() const {
+ const Node* node = GetNode();
+ if (IsDetached() || !node)
+ return false;
+#if DCHECK_IS_ON() // Required in order to get Lifecycle().ToString()
+ DCHECK(GetDocument());
+ DCHECK_GE(GetDocument()->Lifecycle().GetState(),
+ DocumentLifecycle::kStyleClean)
+ << "Unclean document style at lifecycle state "
+ << GetDocument()->Lifecycle().ToString();
+#endif // DCHECK_IS_ON()
+
+ if (HasEditableStyle(*node))
+ return true;
+
+ // For the purposes of accessibility, atomic text fields i.e. input and
+ // textarea are editable because the user can potentially enter text in them.
+ if (IsAtomicTextField())
+ return true;
+
+ return false;
+}
+
bool AXObject::IsEditableRoot() const {
- UpdateCachedAttributeValuesIfNeeded();
- return cached_is_editable_root_;
+ return false;
+}
+
+bool AXObject::HasContentEditableAttributeSet() const {
+ return false;
+}
+
+bool AXObject::IsMultiline() const {
+ if (IsDetached() || !GetNode() || !IsTextField())
+ return false;
+
+ bool is_multiline = false;
+ if (HasAOMPropertyOrARIAAttribute(AOMBooleanProperty::kMultiline,
+ is_multiline)) {
+ return is_multiline;
+ }
+
+ return IsA<HTMLTextAreaElement>(*GetNode()) ||
+ HasContentEditableAttributeSet();
+}
+
+bool AXObject::IsRichlyEditable() const {
+ const Node* node = GetNode();
+ if (IsDetached() || !node)
+ return false;
+#if DCHECK_IS_ON() // Required in order to get Lifecycle().ToString()
+ DCHECK(GetDocument());
+ DCHECK_GE(GetDocument()->Lifecycle().GetState(),
+ DocumentLifecycle::kStyleClean)
+ << "Unclean document style at lifecycle state "
+ << GetDocument()->Lifecycle().ToString();
+#endif // DCHECK_IS_ON()
+
+ if (HasRichlyEditableStyle(*node))
+ return true;
+
+ return false;
}
AXObject* AXObject::LiveRegionRoot() const {
@@ -4106,13 +4101,11 @@ AXObject* AXObject::ParentObject() const {
// detached, but the children still exist. One example of this is when
// a <select size="1"> changes to <select size="2">, where the
// Role::kMenuListPopup is detached.
- if (!parent_) {
+ if (IsMissingParent()) {
DCHECK(!IsVirtualObject())
<< "A virtual object must have a parent, and cannot exist without one. "
"The parent is set when the object is constructed.";
- parent_ = ComputeParent();
- DCHECK(parent_ || IsA<Document>(GetNode()))
- << "The following node should have a parent: " << GetNode();
+ RepairMissingParent();
}
return parent_;
@@ -4151,23 +4144,30 @@ AXObject* AXObject::ContainerWidget() const {
return ancestor;
}
-// Only use layout object traversal for pseudo elements and their descendants.
+// Determine which traversal approach is used to get children of an object.
bool AXObject::ShouldUseLayoutObjectTraversalForChildren() const {
- if (!GetLayoutObject())
- return false;
+ // There are two types of traversal used to find AXObjects:
+ // 1. LayoutTreeBuilderTraversal, which takes FlatTreeTraversal and adds
+ // pseudo elements on top of that. This is the usual case. However, while this
+ // can add pseudo elements it cannot add important content descendants such as
+ // text and images. For this, LayoutObject traversal (#2) is required.
+ // 2. LayoutObject traversal, which just uses the children of a LayoutObject.
+
+ // Therefore, if the object is a pseudo element or pseudo element descendant,
+ // use LayoutObject traversal (#2) to find the children.
+ if (GetNode() && GetNode()->IsPseudoElement())
+ return true;
// If no node, this is an anonymous layout object. The only way this can be
// reached is inside a pseudo element subtree.
- if (!GetNode()) {
+ if (!GetNode() && GetLayoutObject()) {
DCHECK(GetLayoutObject()->IsAnonymous());
- DCHECK(AXObjectCacheImpl::IsPseudoElementDescendant(*GetLayoutObject()));
+ DCHECK(AXObjectCacheImpl::IsRelevantPseudoElementDescendant(
+ *GetLayoutObject()));
return true;
}
- // The only other case for using layout builder traversal is for a pseudo
- // element, such as ::before. Pseudo element child text and images are not
- // visibited by LayoutBuilderTraversal.
- return GetNode()->IsPseudoElement();
+ return false;
}
void AXObject::UpdateChildrenIfNecessary() {
@@ -4230,15 +4230,22 @@ void AXObject::ClearChildren() const {
// AccessibilityExposeIgnoredNodes().
// Loop through AXObject children.
-
-#if DCHECK_IS_ON()
- DCHECK(!is_adding_children_)
- << "Should not be attempting to clear children while in the middle of "
- "adding children on parent: "
+#if defined(AX_FAIL_FAST_BUILD)
+ CHECK(!is_adding_children_)
+ << "Should not attempt to simultaneously add and clear children on: "
<< ToString(true, true);
#endif
+ CHECK(!is_loading_inline_boxes_) << "Should not attempt to clear children "
+ "while loading inline text boxes: "
+ << ToString(true, true);
+
for (const auto& child : children_) {
+ // Check parent first, as the child might be several levels down if there
+ // are unincluded nodes in between, in which case the cached parent will
+ // also be a descendant (unlike children_, parent_ does not skip levels).
+ // Another case where the parent is not the same is when the child has been
+ // reparented using aria-owns.
if (child->CachedParentObject() == this)
child->DetachFromParent();
}
@@ -4248,6 +4255,12 @@ void AXObject::ClearChildren() const {
if (!GetNode())
return;
+ if (GetDocument()->IsFlatTreeTraversalForbidden()) {
+ // Cannot use layout tree builder traversal now, will have to rely on
+ // RepairParent() at a later point.
+ return;
+ }
+
// <slot> content is always included in the tree, so there is no need to
// iterate through the nodes. This also protects us against slot use "after
// poison", where attempts to access assigned nodes triggers a DCHECK.
@@ -4262,24 +4275,37 @@ void AXObject::ClearChildren() const {
// information with their original location in the DOM. Therefore, we need to
// ensure that in the accessibility tree no remnant information from the
// unflattened DOM tree remains, such as the cached parent.
- if (IsA<HTMLSlotElement>(GetNode()))
+
+ // TODO(crbug.com/1209216): Figure out why removing this causes a
+ // use-after-poison and possibly replace it with a better check.
+ HTMLSlotElement* slot = DynamicTo<HTMLSlotElement>(GetNode());
+ if (slot && slot->SupportsAssignment())
return;
+ // Detach children that were not cleared from first loop.
+ // These must have been an unincluded node who's parent is this,
+ // although it may now be included since the children were last updated.
for (Node* child_node = LayoutTreeBuilderTraversal::FirstChild(*GetNode());
child_node;
child_node = LayoutTreeBuilderTraversal::NextSibling(*child_node)) {
+ // Get the child object that should be detached from this parent.
AXObject* ax_child_from_node = AXObjectCache().Get(child_node);
if (ax_child_from_node &&
ax_child_from_node->CachedParentObject() == this) {
- // Child was not cleared from first loop.
- // It must have been an unincluded node who's parent is this,
- // although it may now be included since the children were last updated.
// Check current parent first. It may be owned by another node.
ax_child_from_node->DetachFromParent();
}
}
}
+Node* AXObject::GetNode() const {
+ return nullptr;
+}
+
+LayoutObject* AXObject::GetLayoutObject() const {
+ return nullptr;
+}
+
Element* AXObject::GetElement() const {
return DynamicTo<Element>(GetNode());
}
@@ -4976,11 +5002,11 @@ bool AXObject::RequestShowContextMenuAction() {
return OnNativeShowContextMenuAction();
}
-bool AXObject::InternalClearAccessibilityFocusAction() {
+bool AXObject::InternalSetAccessibilityFocusAction() {
return false;
}
-bool AXObject::InternalSetAccessibilityFocusAction() {
+bool AXObject::InternalClearAccessibilityFocusAction() {
return false;
}
@@ -5137,15 +5163,15 @@ bool AXObject::HasARIAOwns(Element* element) {
const AtomicString& aria_owns =
element->FastGetAttribute(html_names::kAriaOwnsAttr);
- // TODO: do we need to check !AriaOwnsElements.empty() ? Is that fundamentally
- // different from HasExplicitlySetAttrAssociatedElements()? And is an element
- // even necessary in the case of virtual nodes?
+ // TODO(accessibility): do we need to check !AriaOwnsElements.empty() ? Is
+ // that fundamentally different from HasExplicitlySetAttrAssociatedElements()?
+ // And is an element even necessary in the case of virtual nodes?
return !aria_owns.IsEmpty() ||
element->HasExplicitlySetAttrAssociatedElements(
html_names::kAriaOwnsAttr);
}
-ax::mojom::blink::Role AXObject::AriaRoleToWebCoreRole(const String& value) {
+ax::mojom::blink::Role AXObject::AriaRoleStringToRoleEnum(const String& value) {
DCHECK(!value.IsEmpty());
static const ARIARoleMap* role_map = CreateARIARoleMap();
@@ -5345,7 +5371,6 @@ bool AXObject::SupportsNameFromContents(bool recursive) const {
case ax::mojom::blink::Role::kNone:
case ax::mojom::blink::Role::kParagraph:
case ax::mojom::blink::Role::kPre:
- case ax::mojom::blink::Role::kPresentational:
case ax::mojom::blink::Role::kRegion:
// Spec says we should always expose the name on rows,
// but for performance reasons we only do it
@@ -5416,7 +5441,6 @@ bool AXObject::SupportsNameFromContents(bool recursive) const {
case ax::mojom::blink::Role::kColumn:
case ax::mojom::blink::Role::kDesktop:
case ax::mojom::blink::Role::kKeyboard:
- case ax::mojom::blink::Role::kIgnored:
case ax::mojom::blink::Role::kImeCandidate:
case ax::mojom::blink::Role::kListGrid:
case ax::mojom::blink::Role::kPane:
@@ -5465,18 +5489,62 @@ ax::mojom::blink::Role AXObject::ButtonRoleType() const {
}
// static
-const AtomicString& AXObject::RoleName(ax::mojom::blink::Role role) {
- static const Vector<AtomicString>* role_name_vector = CreateRoleNameVector();
+const AtomicString& AXObject::ARIARoleName(ax::mojom::blink::Role role) {
+ static const Vector<AtomicString>* aria_role_name_vector =
+ CreateARIARoleNameVector();
- return role_name_vector->at(static_cast<wtf_size_t>(role));
+ return aria_role_name_vector->at(static_cast<wtf_size_t>(role));
}
// static
-const AtomicString& AXObject::InternalRoleName(ax::mojom::blink::Role role) {
- static const Vector<AtomicString>* internal_role_name_vector =
- CreateInternalRoleNameVector();
+const AtomicString& AXObject::GetEquivalentAriaRoleName(
+ const ax::mojom::blink::Role role) {
+ // TODO(accessibilty) Why are some roles listed here and not others?
+ switch (role) {
+ case ax::mojom::blink::Role::kArticle:
+ case ax::mojom::blink::Role::kBanner:
+ case ax::mojom::blink::Role::kButton:
+ case ax::mojom::blink::Role::kComplementary:
+ case ax::mojom::blink::Role::kFigure:
+ case ax::mojom::blink::Role::kFooter:
+ case ax::mojom::blink::Role::kHeader:
+ case ax::mojom::blink::Role::kHeading:
+ case ax::mojom::blink::Role::kImage:
+ case ax::mojom::blink::Role::kMain:
+ case ax::mojom::blink::Role::kNavigation:
+ case ax::mojom::blink::Role::kRadioButton:
+ case ax::mojom::blink::Role::kRegion:
+ case ax::mojom::blink::Role::kSlider:
+ case ax::mojom::blink::Role::kTime:
+ return ARIARoleName(role);
+ default:
+ return g_null_atom;
+ }
+}
+
+const String AXObject::InternalRoleName(ax::mojom::blink::Role role) {
+ std::ostringstream role_name;
+ role_name << role;
+ // Convert from std::ostringstream to std::string, while removing "k" prefix.
+ // For example, kStaticText becomes StaticText.
+ // Many conversions, but this isn't used in performance-sensitive code.
+ std::string role_name_std = role_name.str().substr(1, std::string::npos);
+ String role_name_wtf_string = role_name_std.c_str();
+ return role_name_wtf_string;
+}
+
+// static
+const String AXObject::RoleName(ax::mojom::blink::Role role,
+ bool* is_internal) {
+ if (is_internal)
+ *is_internal = false;
+ if (const auto& role_name = ARIARoleName(role))
+ return role_name.GetString();
+
+ if (is_internal)
+ *is_internal = true;
- return internal_role_name_vector->at(static_cast<wtf_size_t>(role));
+ return InternalRoleName(role);
}
// static
@@ -5525,8 +5593,7 @@ String AXObject::ToString(bool verbose, bool cached_values_only) const {
// Build a friendly name for debugging the object.
// If verbose, build a longer name name in the form of:
// CheckBox axid#28 <input.someClass#cbox1> name="checkbox"
- String string_builder =
- AXObject::InternalRoleName(RoleValue()).GetString().EncodeForDebugging();
+ String string_builder = InternalRoleName(RoleValue()).EncodeForDebugging();
if (IsDetached())
string_builder = string_builder + " (detached)";
@@ -5534,8 +5601,8 @@ String AXObject::ToString(bool verbose, bool cached_values_only) const {
if (verbose) {
string_builder = string_builder + " axid#" + String::Number(AXObjectID());
// Add useful HTML element info, like <div.myClass#myId>.
- if (GetElement())
- string_builder = string_builder + " " + GetElementString(GetElement());
+ if (GetNode())
+ string_builder = string_builder + " " + GetNodeString(GetNode());
// Add properties of interest that often contribute to errors:
if (HasARIAOwns(GetElement())) {
@@ -5588,23 +5655,28 @@ String AXObject::ToString(bool verbose, bool cached_values_only) const {
string_builder = string_builder + " isDisplayLocked";
}
}
- if (const AXObject* aria_hidden_root = AriaHiddenRoot()) {
- string_builder = string_builder + " ariaHiddenRoot";
- if (aria_hidden_root != this) {
- string_builder =
- string_builder + GetElementString(aria_hidden_root->GetElement());
+ if (cached_values_only) {
+ if (cached_is_inert_or_aria_hidden_ && GetNode() && !GetNode()->IsInert())
+ string_builder = string_builder + " ariaHidden";
+ } else {
+ if (const AXObject* aria_hidden_root = AriaHiddenRoot()) {
+ string_builder = string_builder + " ariaHiddenRoot";
+ if (aria_hidden_root != this) {
+ string_builder =
+ string_builder + GetNodeString(aria_hidden_root->GetNode());
+ }
}
}
- if (GetDocument() && GetDocument()->Lifecycle().GetState() <
- DocumentLifecycle::kLayoutClean) {
- string_builder = string_builder + " styleInfoUnavailable";
- } else if (IsHiddenViaStyle()) {
+ if (cached_values_only ? cached_is_hidden_via_style : IsHiddenViaStyle())
string_builder = string_builder + " isHiddenViaCSS";
- }
if (GetNode() && GetNode()->IsInert())
string_builder = string_builder + " isInert";
- if (NeedsToUpdateChildren())
+ if (NeedsToUpdateChildren()) {
string_builder = string_builder + " needsToUpdateChildren";
+ } else if (!children_.IsEmpty()) {
+ string_builder = string_builder + " #children=";
+ string_builder = string_builder + String::Number(children_.size());
+ }
if (!GetLayoutObject())
string_builder = string_builder + " missingLayout";
diff --git a/chromium/third_party/blink/renderer/modules/accessibility/ax_object.h b/chromium/third_party/blink/renderer/modules/accessibility/ax_object.h
index 9aab1d10171..7de1c54b612 100644
--- a/chromium/third_party/blink/renderer/modules/accessibility/ax_object.h
+++ b/chromium/third_party/blink/renderer/modules/accessibility/ax_object.h
@@ -33,8 +33,9 @@
#include <ostream>
#include <utility>
+#include "base/dcheck_is_on.h"
#include "base/macros.h"
-#include "base/optional.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
#include "third_party/blink/public/web/web_ax_enums.h"
#include "third_party/blink/renderer/core/accessibility/axid.h"
#include "third_party/blink/renderer/core/dom/element.h"
@@ -51,6 +52,7 @@
#include "third_party/blink/renderer/platform/weborigin/kurl.h"
#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
#include "third_party/blink/renderer/platform/wtf/vector.h"
+#include "ui/accessibility/ax_common.h"
#include "ui/accessibility/ax_enums.mojom-blink.h"
#include "ui/accessibility/ax_mode.h"
@@ -141,7 +143,7 @@ class DescriptionSource {
bool superseded = false;
bool invalid = false;
ax::mojom::blink::DescriptionFrom type =
- ax::mojom::blink::DescriptionFrom::kUninitialized;
+ ax::mojom::blink::DescriptionFrom::kNone;
const QualifiedName& attribute;
AtomicString attribute_value;
AXTextFromNativeHTML native_source = kAXTextFromNativeHTMLUninitialized;
@@ -343,6 +345,9 @@ class MODULES_EXPORT AXObject : public GarbageCollected<AXObject> {
#if DCHECK_IS_ON()
bool is_initializing_ = false;
mutable bool is_updating_cached_values_ = false;
+#endif
+
+#if defined(AX_FAIL_FAST_BUILD)
bool is_adding_children_ = false;
#endif
@@ -353,10 +358,12 @@ class MODULES_EXPORT AXObject : public GarbageCollected<AXObject> {
static unsigned NumberOfLiveAXObjects() { return number_of_live_ax_objects_; }
// After constructing an AXObject, it must be given a
- // unique ID, then added to AXObjectCacheImpl, and finally init() must
+ // unique ID, then added to AXObjectCacheImpl, and finally Init() must
// be called last.
void SetAXObjectID(AXID ax_object_id) { id_ = ax_object_id; }
- virtual void Init(AXObject* parent_if_known);
+ // Initialize the object and set the |parent|, which can only be null for the
+ // root of the tree.
+ virtual void Init(AXObject* parent);
// When the corresponding WebCore object that this AXObject
// wraps is deleted, it must be detached.
@@ -401,7 +408,9 @@ class MODULES_EXPORT AXObject : public GarbageCollected<AXObject> {
AtomicString& result) const;
virtual AccessibleNode* GetAccessibleNode() const;
- void TokenVectorFromAttribute(Vector<String>&, const QualifiedName&) const;
+ static void TokenVectorFromAttribute(Element* element,
+ Vector<String>&,
+ const QualifiedName&);
// Serialize the properties of this node into |node_data|.
//
@@ -438,7 +447,6 @@ class MODULES_EXPORT AXObject : public GarbageCollected<AXObject> {
bool IsButton() const;
bool IsCanvas() const;
- bool IsCheckboxOrRadio() const;
bool IsColorWell() const;
virtual bool IsControl() const;
virtual bool IsDefault() const;
@@ -457,12 +465,12 @@ class MODULES_EXPORT AXObject : public GarbageCollected<AXObject> {
// Returns true if this object is an input element of a text field type, such
// as type="text" or type="tel", or a textarea.
- bool IsNativeTextField() const;
+ bool IsAtomicTextField() const;
// Returns true if this object is not an <input> or a <textarea>, and is
- // either a contenteditable, or has role=textbox role=searchbox or
- // role=combobox.
- bool IsNonNativeTextField() const;
+ // either a contenteditable, or has the CSS user-modify style set to something
+ // editable.
+ bool IsNonAtomicTextField() const;
// Returns true if this object is a text field that is used for entering
// passwords, i.e. <input type=password>.
@@ -551,21 +559,14 @@ class MODULES_EXPORT AXObject : public GarbageCollected<AXObject> {
const AXObject* AriaHiddenRoot() const;
bool ComputeIsInertOrAriaHidden(IgnoredReasons* = nullptr) const;
bool IsBlockedByAriaModalDialog(IgnoredReasons* = nullptr) const;
- bool IsDescendantOfLeafNode() const;
- bool LastKnownIsDescendantOfLeafNode() const {
- return cached_is_descendant_of_leaf_node_;
- }
- AXObject* LeafNodeAncestor() const;
bool IsDescendantOfDisabledNode() const;
bool ComputeAccessibilityIsIgnoredButIncludedInTree() const;
- const AXObject* GetNativeTextControlAncestor(
- int max_levels_to_check = 3) const;
+ const AXObject* GetAtomicTextFieldAncestor(int max_levels_to_check = 3) const;
const AXObject* DatetimeAncestor(int max_levels_to_check = 3) const;
const AXObject* DisabledAncestor() const;
bool LastKnownIsIgnoredValue() const;
bool LastKnownIsIgnoredButIncludedInTreeValue() const;
bool LastKnownIsIncludedInTreeValue() const;
- bool HasInheritedPresentationalRole() const;
bool CanBeActiveDescendant() const;
// Some objects, such as table header containers, could be the children of
// more than one object but have only one primary parent.
@@ -637,13 +638,6 @@ class MODULES_EXPORT AXObject : public GarbageCollected<AXObject> {
// This is a simpler high-level interface to |name| used by Inspector.
String ComputedName() const;
- // Internal function used to determine whether the result of calling |GetName|
- // on this object would return text that came from the an HTML label element
- // or not. This is intended to be faster than calling |GetName| or
- // |TextAlternative|, and without side effects (it won't call
- // AXObjectCache->GetOrCreate).
- virtual bool NameFromLabelElement() const { return false; }
-
// Internal function used to determine whether the element supports deriving
// its accessible name from its descendants. The result of calling |GetName|
// may be derived by other means even when this returns true.
@@ -716,7 +710,8 @@ class MODULES_EXPORT AXObject : public GarbageCollected<AXObject> {
// Load inline text boxes for just this node, even if
// settings->inlineTextBoxAccessibilityEnabled() is false.
- virtual void LoadInlineTextBoxes();
+ void LoadInlineTextBoxes();
+ virtual void LoadInlineTextBoxesRecursive();
// Walk the AXObjects on the same line.
virtual AXObject* NextOnLine() const;
@@ -727,7 +722,7 @@ class MODULES_EXPORT AXObject : public GarbageCollected<AXObject> {
// of this attribute. As an optimization, goes up until the deepest line
// breaking object which, in most cases, is the paragraph containing this
// object.
- base::Optional<const DocumentMarker::MarkerType>
+ absl::optional<const DocumentMarker::MarkerType>
GetAriaSpellingOrGrammarMarker() const;
// For all inline text objects: Returns the horizontal pixel offset of each
@@ -740,13 +735,13 @@ class MODULES_EXPORT AXObject : public GarbageCollected<AXObject> {
virtual void GetWordBoundaries(Vector<int>& word_starts,
Vector<int>& word_ends) const;
- // For all inline text fields and native text fields: Returns the length of
- // the inline's text or the field's value respectively.
+ // For all inline text boxes and atomic text fields: Returns the length of the
+ // inline's text or the field's value respectively.
virtual int TextLength() const;
// Supported on layout inline, layout text, layout replaced, and layout block
// flow, provided that they are at inline-level, i.e. "display=inline" or
- // "display=inline-block". Also supported on native text fields. For all other
+ // "display=inline-block". Also supported on atomic text fields. For all other
// object types, returns |offset|.
//
// For layout inline, text, replaced, and block flow: Translates the given
@@ -761,12 +756,14 @@ class MODULES_EXPORT AXObject : public GarbageCollected<AXObject> {
// in the DOM, from the start of the layout inline's deepest block flow
// ancestor, e.g. the beginning of the paragraph in which the span is found.
//
- // For native text fields: Simply returns |offset|, because native text fields
+ // For atomic text fields: Simply returns |offset|, because atomic text fields
// have no collapsed white space and so no translation from a DOM to an
- // accessible text offset is necessary.
+ // accessible text offset is necessary. An atomic text field does not expose
+ // its internal implementation to assistive software, appearing as a single
+ // leaf node in the accessibility tree. It includes <input> and <textarea>.
virtual int TextOffsetInFormattingContext(int offset) const;
- // For all inline text boxes and native text fields. For all other object
+ // For all inline text boxes and atomic text fields. For all other object
// types, returns |offset|.
//
// For inline text boxes: Translates the given character offset to the
@@ -778,7 +775,7 @@ class MODULES_EXPORT AXObject : public GarbageCollected<AXObject> {
// characters, excluding any collapsed white space found in the DOM, from the
// start of the inline text box's static text parent.
//
- // For native text fields: Simply returns |offset|, because native text fields
+ // For atomic text fields: Simply returns |offset|, because atomic text fields
// have no collapsed white space and so no translation from a DOM to an
// accessible text offset is necessary.
virtual int TextOffsetInContainer(int offset) const;
@@ -820,26 +817,53 @@ class MODULES_EXPORT AXObject : public GarbageCollected<AXObject> {
// ARIA attributes.
virtual ax::mojom::blink::Role DetermineAccessibilityRole();
+ // Determine the ARIA role purely based on the role attribute, when no
+ // additional rules or limitations on role usage are applied.
+ ax::mojom::blink::Role RawAriaRole() const;
+ // Determine the ARIA role after post-processing on the raw ARIA role.
ax::mojom::blink::Role DetermineAriaRoleAttribute() const;
virtual ax::mojom::blink::Role AriaRoleAttribute() const;
- virtual bool HasAriaAttribute() const { return false; }
+ bool HasAriaAttribute(bool does_undo_role_presentation = false) const;
virtual AXObject* ActiveDescendant() { return nullptr; }
virtual String AutoComplete() const { return String(); }
virtual void AriaOwnsElements(AXObjectVector& owns) const {}
virtual void AriaDescribedbyElements(AXObjectVector&) const {}
virtual AXObject* ErrorMessage() const { return nullptr; }
- virtual ax::mojom::blink::HasPopup HasPopup() const {
- return ax::mojom::blink::HasPopup::kFalse;
- }
- virtual bool IsEditable() const { return false; }
- bool IsEditableRoot() const;
- virtual bool ComputeIsEditableRoot() const { return false; }
- virtual bool HasContentEditableAttributeSet() const { return false; }
- virtual bool IsMultiline() const { return false; }
- virtual bool IsRichlyEditable() const { return false; }
+
+ // Determines whether this object has an associated popup menu, list, or grid,
+ // such as in the case of an ARIA combobox or when the browser offers an
+ // autocomplete suggestion.
+ virtual ax::mojom::blink::HasPopup HasPopup() const;
+
+ // Returns true if this object is within or at the root of an editable region,
+ // such as a contenteditable. Also, returns true if this object is an atomic
+ // text field, i.e. an input or a textarea. Note that individual subtrees
+ // within an editable region could be made non-editable via e.g.
+ // contenteditable="false".
+ bool IsEditable() const;
+
+ // Returns true if this object is at the root of an editable region, such as a
+ // contenteditable. Does not return true if this object is an atomic text
+ // field, i.e. an input or a textarea.
+ //
+ // https://w3c.github.io/editing/execCommand.html#editing-host
+ virtual bool IsEditableRoot() const;
+
+ // Returns true if this object has contenteditable="true" or
+ // contenteditable="plaintext-only".
+ virtual bool HasContentEditableAttributeSet() const;
+
+ // Returns true if the user can enter multiple lines of text inside this
+ // editable region. By default, textareas and content editables can accept
+ // multiple lines of text.
+ bool IsMultiline() const;
+
+ // Same as `IsEditable()` but returns whether the region accepts rich text
+ // as well.
+ bool IsRichlyEditable() const;
+
bool AriaCheckedIsPresent() const;
bool AriaPressedIsPresent() const;
- bool HasGlobalARIAAttribute() const;
bool SupportsARIAExpanded() const;
virtual bool SupportsARIADragging() const { return false; }
virtual void Dropeffects(
@@ -1082,9 +1106,15 @@ class MODULES_EXPORT AXObject : public GarbageCollected<AXObject> {
// including nodes that might not be in the tree.
AXObject* CachedParentObject() const { return parent_; }
+ // Get the current unignored children without refreshing them, even if
+ // children_dirty_ aka NeedsToUpdateChildren() is true.
+ const AXObjectVector& CachedChildrenIncludingIgnored() const {
+ return children_;
+ }
+
// Sets the parent AXObject directly. If the parent of this object is known,
// this can be faster than using ComputeParent().
- void SetParent(AXObject* new_parent);
+ void SetParent(AXObject* new_parent) const;
// If parent was not initialized during AddChildren() it can be computed by
// walking the DOM (or layout for nodeless aka anonymous layout object).
@@ -1092,8 +1122,33 @@ class MODULES_EXPORT AXObject : public GarbageCollected<AXObject> {
// an attached parent_ is already cached, and that it is possible to compute
// the parent. It calls ComputeParentImpl() for the actual work.
AXObject* ComputeParent() const;
- // Subclasses override ComputeParentImpl() to change parent computation.
- virtual AXObject* ComputeParentImpl() const;
+
+ // Can this node be used to compute the natural parent of an object?
+ // These are objects that can have some children, but the children are
+ // only of a certain type or from another part of the tree, and therefore
+ // the parent-child relationships are not natural and must be handled
+ // specially. For example, a <select> may be an innapropriate natural parent
+ // for all of its child nodes as determined by LayoutTreeBuilderTraversal,
+ // such as an <optgroup> or <div> in the shadow DOM, because an AXMenuList, if
+ // used, only allows <option>/AXMenuListOption children.
+ static bool CanComputeAsNaturalParent(Node*);
+
+ // Compute the AXObject parent for the given node or layout_object.
+ // The layout object is only necessary if the node is null, which is the case
+ // only for pseudo elements. ** Does not take aria-owns into account. **
+ static AXObject* ComputeNonARIAParent(AXObjectCacheImpl& cache,
+ Node* node,
+ LayoutObject* layout_object = nullptr);
+
+ // Returns true if |parent_| is null and not at the root.
+ bool IsMissingParent() const;
+
+ // Compute a missing parent, and ask it to update children.
+ // Must only be called if IsMissingParent() is true.
+ void RepairMissingParent() const;
+
+ // Is this the root of this object hierarchy.
+ bool IsRoot() const;
#if DCHECK_IS_ON()
// When the parent on children during AddChildren(), take the opportunity to
@@ -1131,10 +1186,25 @@ class MODULES_EXPORT AXObject : public GarbageCollected<AXObject> {
virtual double EstimatedLoadingProgress() const { return 0; }
virtual AXObject* RootScroller() const;
+ //
// DOM and layout tree access.
- virtual Node* GetNode() const { return nullptr; }
- Element* GetElement() const; // Same as GetNode, if it's an Element.
- virtual LayoutObject* GetLayoutObject() const { return nullptr; }
+ //
+
+ // Returns the associated DOM node or, if an associated layout object is
+ // present, the node of the associated layout object.
+ //
+ // If this object is associated with generated content, or a list marker,
+ // returns a pseudoelement. It does not return the node that generated the
+ // content or the list marker.
+ virtual Node* GetNode() const;
+
+ // Returns the associated layout object if any.
+ virtual LayoutObject* GetLayoutObject() const;
+
+ // Returns the same as `AXObject::GetNode()` if the node is an Element,
+ // otherwise returns nullptr.
+ Element* GetElement() const;
+
virtual Document* GetDocument() const = 0;
LocalFrameView* DocumentFrameView() const;
virtual Element* AnchorElement() const { return nullptr; }
@@ -1221,8 +1291,8 @@ class MODULES_EXPORT AXObject : public GarbageCollected<AXObject> {
// to keep track of nodes that gain or lose accessibility focus, but
// this isn't exposed to the open web so they're explicitly marked as
// internal so it's clear that these should not dispatch DOM events.
- bool InternalClearAccessibilityFocusAction();
- bool InternalSetAccessibilityFocusAction();
+ virtual bool InternalSetAccessibilityFocusAction();
+ virtual bool InternalClearAccessibilityFocusAction();
// Native implementations of actions that aren't handled by AOM
// event listeners. These all return true if handled.
@@ -1242,7 +1312,7 @@ class MODULES_EXPORT AXObject : public GarbageCollected<AXObject> {
bool OnNativeShowContextMenuAction();
// Notifications that this object may have changed.
- virtual void ChildrenChanged() {}
+ virtual void ChildrenChangedWithCleanLayout() {}
virtual void HandleActiveDescendantChanged() {}
virtual void HandleAutofillStateChanged(WebAXAutofillState) {}
virtual void HandleAriaExpandedChanged() {}
@@ -1255,9 +1325,24 @@ class MODULES_EXPORT AXObject : public GarbageCollected<AXObject> {
static bool HasARIAOwns(Element* element);
// Is this a widget that requires container widget.
bool IsSubWidget() const;
- static ax::mojom::blink::Role AriaRoleToWebCoreRole(const String&);
- static const AtomicString& RoleName(ax::mojom::blink::Role);
- static const AtomicString& InternalRoleName(ax::mojom::blink::Role);
+ static ax::mojom::blink::Role AriaRoleStringToRoleEnum(const String&);
+
+ // Return the equivalent ARIA name for an enumerated role, or g_null_atom.
+ static const AtomicString& ARIARoleName(ax::mojom::blink::Role);
+
+ // For a native role get the equivalent ARIA role for use in the xml-roles
+ // object attribute.
+ static const AtomicString& GetEquivalentAriaRoleName(ax::mojom::blink::Role);
+
+ // Return the equivalent internal role name as a string.
+ static const String InternalRoleName(ax::mojom::blink::Role);
+
+ // Return a role name, preferring the ARIA over the internal name.
+ // Optional boolean out param |*is_internal| will be false if the role matches
+ // an ARIA role, and true if an internal role name is used (no ARIA mapping).
+ static const String RoleName(ax::mojom::blink::Role,
+ bool* is_internal = nullptr);
+
static void AccessibleNodeListToElementVector(const AccessibleNodeList&,
HeapVector<Member<Element>>&);
@@ -1319,19 +1404,22 @@ class MODULES_EXPORT AXObject : public GarbageCollected<AXObject> {
AXObjectSet& visited,
HeapVector<Member<Element>>& elements,
AXRelatedObjectVector* related_objects) const;
- void ElementsFromAttribute(HeapVector<Member<Element>>& elements,
- const QualifiedName&,
- Vector<String>& ids) const;
- void AriaLabelledbyElementVector(HeapVector<Member<Element>>& elements,
- Vector<String>& ids) const;
+ static bool ElementsFromAttribute(Element* from,
+ HeapVector<Member<Element>>& elements,
+ const QualifiedName&,
+ Vector<String>& ids);
+ static bool AriaLabelledbyElementVector(Element* from,
+ HeapVector<Member<Element>>& elements,
+ Vector<String>& ids);
+ // Return true if the ame is from @aria-label / @aria-labelledby.
+ static bool IsNameFromAriaAttribute(Element* element);
+ // Return true if the name is from @aria-label / @aria-labelledby / @title.
+ bool IsNameFromAuthorAttribute() const;
String TextFromAriaLabelledby(AXObjectSet& visited,
AXRelatedObjectVector* related_objects,
Vector<String>& ids) const;
String TextFromAriaDescribedby(AXRelatedObjectVector* related_objects,
Vector<String>& ids) const;
- virtual const AXObject* InheritsPresentationalRoleFrom() const {
- return nullptr;
- }
ax::mojom::blink::Role ButtonRoleType() const;
@@ -1376,10 +1464,7 @@ class MODULES_EXPORT AXObject : public GarbageCollected<AXObject> {
mutable bool cached_is_ignored_but_included_in_tree_ : 1;
mutable bool cached_is_inert_or_aria_hidden_ : 1;
mutable bool cached_is_hidden_via_style : 1;
- mutable bool cached_is_descendant_of_leaf_node_ : 1;
mutable bool cached_is_descendant_of_disabled_node_ : 1;
- mutable bool cached_has_inherited_presentational_role_ : 1;
- mutable bool cached_is_editable_root_ : 1;
mutable Member<AXObject> cached_live_region_root_;
mutable int cached_aria_column_index_;
mutable int cached_aria_row_index_;
@@ -1420,6 +1505,8 @@ class MODULES_EXPORT AXObject : public GarbageCollected<AXObject> {
const std::string& value,
uint32_t max_len = kMaxStringAttributeLength) const;
+ static bool is_loading_inline_boxes_;
+
static unsigned number_of_live_ax_objects_;
DISALLOW_COPY_AND_ASSIGN(AXObject);
diff --git a/chromium/third_party/blink/renderer/modules/accessibility/ax_object_cache_impl.cc b/chromium/third_party/blink/renderer/modules/accessibility/ax_object_cache_impl.cc
index 1290169dade..0c7b9b8d088 100644
--- a/chromium/third_party/blink/renderer/modules/accessibility/ax_object_cache_impl.cc
+++ b/chromium/third_party/blink/renderer/modules/accessibility/ax_object_cache_impl.cc
@@ -43,6 +43,7 @@
#include "third_party/blink/renderer/core/display_lock/display_lock_utilities.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/document_lifecycle.h"
+#include "third_party/blink/renderer/core/dom/slot_assignment_engine.h"
#include "third_party/blink/renderer/core/editing/editing_utilities.h"
#include "third_party/blink/renderer/core/events/event_util.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
@@ -82,6 +83,7 @@
#include "third_party/blink/renderer/modules/accessibility/ax_layout_object.h"
#include "third_party/blink/renderer/modules/accessibility/ax_list_box.h"
#include "third_party/blink/renderer/modules/accessibility/ax_list_box_option.h"
+#include "third_party/blink/renderer/modules/accessibility/ax_media_control.h"
#include "third_party/blink/renderer/modules/accessibility/ax_media_element.h"
#include "third_party/blink/renderer/modules/accessibility/ax_menu_list.h"
#include "third_party/blink/renderer/modules/accessibility/ax_menu_list_option.h"
@@ -91,10 +93,9 @@
#include "third_party/blink/renderer/modules/accessibility/ax_slider.h"
#include "third_party/blink/renderer/modules/accessibility/ax_validation_message.h"
#include "third_party/blink/renderer/modules/accessibility/ax_virtual_object.h"
-#include "third_party/blink/renderer/modules/media_controls/elements/media_control_elements_helper.h"
#include "third_party/blink/renderer/modules/permissions/permission_utils.h"
#include "third_party/blink/renderer/platform/instrumentation/tracing/trace_event.h"
-#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
+#include "ui/accessibility/ax_common.h"
#include "ui/accessibility/ax_enums.mojom-blink.h"
#include "ui/accessibility/ax_event.h"
#include "ui/accessibility/ax_role_properties.h"
@@ -126,6 +127,25 @@ Node* GetClosestNodeForLayoutObject(const LayoutObject* layout_object) {
return node ? node : GetClosestNodeForLayoutObject(layout_object->Parent());
}
+// Return true if display locked or inside slot recalc, false otherwise.
+// Also returns false if not a safe time to perform the check.
+bool IsDisplayLocked(const Node* node) {
+ if (!node)
+ return false;
+ // The NearestLockedExclusiveAncestor() function will attempt to do
+ // a flat tree traversal of ancestors. If we're in a flat tree traversal
+ // forbidden scope, return false. Additionally, flat tree traversal
+ // might call AssignedSlot, so if we're in a slot assignment recalc
+ // forbidden scope, return false.
+ if (node->GetDocument().IsFlatTreeTraversalForbidden() ||
+ node->GetDocument()
+ .GetSlotAssignmentEngine()
+ .HasPendingSlotAssignmentRecalc()) {
+ return false; // Cannot safely perform this check now.
+ }
+ return DisplayLockUtilities::NearestLockedExclusiveAncestor(*node);
+}
+
bool IsActive(Document& document) {
return document.IsActive() && !document.IsDetached();
}
@@ -136,7 +156,28 @@ bool HasAriaCellRole(Element* elem) {
if (role_str.IsEmpty())
return false;
- return ui::IsCellOrTableHeader(AXObject::AriaRoleToWebCoreRole(role_str));
+ return ui::IsCellOrTableHeader(AXObject::AriaRoleStringToRoleEnum(role_str));
+}
+
+// How deep can role="presentation" propagate from this node (inclusive)?
+// For example, propagates from table->tbody->tr->td (4).
+// Limiting the depth is an optimization that keeps recursion under control.
+int RolePresentationPropagationDepth(Node* node) {
+ // Check for list markup.
+ if (IsA<HTMLMenuElement>(node) || IsA<HTMLUListElement>(node) ||
+ IsA<HTMLOListElement>(node)) {
+ return 2;
+ }
+
+ // Check for <table>
+ if (IsA<HTMLTableElement>(node))
+ return 4; // table section, table row, table cells,
+
+ // Check for display: table CSS.
+ if (node->GetLayoutObject() && node->GetLayoutObject()->IsTable())
+ return 4;
+
+ return 0;
}
// Return true if whitespace is not necessary to keep adjacent_node separate
@@ -203,8 +244,8 @@ bool CanIgnoreSpaceNextTo(LayoutObject* layout_object,
if (!child && elem) {
// No children of inline element. Check adjacent sibling in same direction.
Node* adjacent_node =
- is_after ? FlatTreeTraversal::NextSkippingChildren(*elem)
- : FlatTreeTraversal::PreviousAbsoluteSibling(*elem);
+ is_after ? NodeTraversal::NextIncludingPseudoSkippingChildren(*elem)
+ : NodeTraversal::PreviousAbsoluteSiblingIncludingPseudo(*elem);
return adjacent_node &&
CanIgnoreSpaceNextTo(adjacent_node->GetLayoutObject(), is_after,
++counter);
@@ -213,8 +254,10 @@ bool CanIgnoreSpaceNextTo(LayoutObject* layout_object,
}
bool IsTextRelevantForAccessibility(const LayoutText& layout_text) {
- DCHECK(layout_text.Parent());
- Node* node = layout_text.GetNode();
+ if (!layout_text.Parent())
+ return false;
+
+ const Node* node = layout_text.GetNode();
DCHECK(node); // Anonymous text is processed earlier, doesn't reach here.
// Ignore empty text.
@@ -228,12 +271,15 @@ bool IsTextRelevantForAccessibility(const LayoutText& layout_text) {
// Will now look at sibling nodes. We need the closest element to the
// whitespace markup-wise, e.g. tag1 in these examples:
// [whitespace] <tag1><tag2>x</tag2></tag1>
- // <span>[whitespace]</span> <tag1><tag2>x</tag2></tag1>
- Node* prev_node = FlatTreeTraversal::PreviousAbsoluteSibling(*node);
+ // <span>[whitespace]</span> <tag1><tag2>x</tag2></tag1>.
+ // Do not use LayoutTreeBuilderTraversal or FlatTreeTraversal as this may need
+ // to be called during slot assignment, when flat tree traversal is forbidden.
+ Node* prev_node =
+ NodeTraversal::PreviousAbsoluteSiblingIncludingPseudo(*node);
if (!prev_node)
return false;
- Node* next_node = FlatTreeTraversal::NextSkippingChildren(*node);
+ Node* next_node = NodeTraversal::NextIncludingPseudoSkippingChildren(*node);
if (!next_node)
return false;
@@ -253,39 +299,50 @@ bool IsTextRelevantForAccessibility(const LayoutText& layout_text) {
bool IsShadowContentRelevantForAccessibility(const Node* node) {
DCHECK(node->ContainingShadowRoot());
- // All author shadow content is relevant.
- if (!node->IsInUserAgentShadowRoot())
- return true;
-
- // All non-slot user agent shadow nodes are relevant.
- const HTMLSlotElement* slot_element = DynamicTo<HTMLSlotElement>(node);
- if (!slot_element)
- return true;
-
- // All empty slots are irrelevant.
- if (!LayoutTreeBuilderTraversal::FirstChild(*slot_element))
- return false;
+ // Don't use non-<option> descendants of an AXMenuList.
// If the UseAXMenuList flag is on, we use a specialized class AXMenuList
// for handling the user-agent shadow DOM exposed by a <select> element.
- if (AXObjectCacheImpl::UseAXMenuList()) {
- // Don't use any shadow root descendants, because AXMenuList already
- // handles adding descendants and these would be redundant.
- // DOM traversal is still necessary for the <canvas> case.
+ // That class adds a mock AXMenuListPopup, which adds AXMenuListOption
+ // children for <option> descendants only.
+ if (AXObjectCacheImpl::UseAXMenuList() && node->IsInUserAgentShadowRoot() &&
+ !IsA<HTMLOptionElement>(node)) {
+ // Find any ancestor <select> if it is present.
Node* host = node->OwnerShadowHost();
auto* select_element = DynamicTo<HTMLSelectElement>(host);
if (!select_element) {
+ // An <optgroup> can be a shadow host too -- look for it's owner <select>.
if (auto* opt_group_element = DynamicTo<HTMLOptGroupElement>(host))
select_element = opt_group_element->OwnerSelectElement();
}
-
if (select_element) {
- return !select_element->UsesMenuList() ||
- select_element->IsInCanvasSubtree();
+ if (!select_element->GetLayoutObject())
+ return select_element->IsInCanvasSubtree();
+ // Non-option: only create AXObject if not inside an AXMenuList.
+ return !AXObjectCacheImpl::ShouldCreateAXMenuListFor(
+ select_element->GetLayoutObject());
}
}
- return true;
+ // Outside of AXMenuList descendants, all other non-slot user agent shadow
+ // nodes are relevant.
+ const HTMLSlotElement* slot_element = DynamicTo<HTMLSlotElement>(node);
+ if (!slot_element)
+ return true;
+
+ // Slots are relevant if they have content.
+ // However, this can only be checked during safe times.
+ // During other times we must assume that the <slot> is relevant.
+ // TODO(accessibility) Consider removing this rule, but it will require
+ // a different way of dealing with these PDF test failures:
+ // https://chromium-review.googlesource.com/c/chromium/src/+/2965317
+ // For some reason the iframe tests hang, waiting for content to change. In
+ // other words, returning true here causes some tree updates not to occur.
+ return node->GetDocument().IsFlatTreeTraversalForbidden() ||
+ node->GetDocument()
+ .GetSlotAssignmentEngine()
+ .HasPendingSlotAssignmentRecalc() ||
+ LayoutTreeBuilderTraversal::FirstChild(*slot_element);
}
bool IsLayoutObjectRelevantForAccessibility(const LayoutObject& layout_object) {
@@ -293,24 +350,9 @@ bool IsLayoutObjectRelevantForAccessibility(const LayoutObject& layout_object) {
// Anonymous means there is no DOM node, and it's been inserted by the
// layout engine within the tree. An example is an anonymous block that is
// inserted as a parent of an inline where there are block siblings.
-
- // Visible anonymous content (text, image, layout quotes) is relevant.
- if (!layout_object.CanHaveChildren()) {
- if (layout_object.IsText())
- return !To<LayoutText>(layout_object).HasEmptyText();
- return true;
- }
-
- // Anonymous containers are not relevant, unless inside a pseudo element.
- // Allowing anonymous pseudo elements ensures that all visible descendant
- // pseudo content will be reached, despite only being able to walk layout
- // inside of pseudo content.
- return AXObjectCacheImpl::IsPseudoElementDescendant(layout_object);
+ return AXObjectCacheImpl::IsRelevantPseudoElementDescendant(layout_object);
}
- if (layout_object.IsText())
- return IsTextRelevantForAccessibility(To<LayoutText>(layout_object));
-
Node* node = layout_object.GetNode();
DCHECK(node) << "Non-anonymous layout objects always have a node";
@@ -318,14 +360,35 @@ bool IsLayoutObjectRelevantForAccessibility(const LayoutObject& layout_object) {
!IsShadowContentRelevantForAccessibility(node)) {
return false;
}
+
+ if (layout_object.IsText())
+ return IsTextRelevantForAccessibility(To<LayoutText>(layout_object));
+
// Menu list option and HTML area elements are indexed by DOM node, never by
// layout object.
if (AXObjectCacheImpl::ShouldCreateAXMenuListOptionFor(node))
return false;
+ // TODO(accessibility) Refactor so that the following rules are not repeated
+ // in IsNodeRelevantForAccessibility().
+
if (IsA<HTMLAreaElement>(node))
return false;
+ if (node->IsPseudoElement())
+ return AXObjectCacheImpl::IsRelevantPseudoElement(*node);
+
+ // <optgroup> is irrelevant inside of a <select> menulist.
+ if (auto* opt_group = DynamicTo<HTMLOptGroupElement>(node)) {
+ if (auto* select = opt_group->OwnerSelectElement())
+ return !select->UsesMenuList();
+ }
+
+ // An HTML <title> does not require an AXObject: the document's name is
+ // retrieved directly via the inner text.
+ if (IsA<HTMLTitleElement>(node))
+ return node->IsSVGElement();
+
return true;
}
@@ -353,6 +416,11 @@ bool IsNodeRelevantForAccessibility(const Node* node,
if (node->IsDocumentNode())
return true;
+ if (node->ContainingShadowRoot() &&
+ !IsShadowContentRelevantForAccessibility(node)) {
+ return false;
+ }
+
if (node->IsTextNode()) {
// Layout has more info available to determine if whitespace is relevant.
// If display-locked, layout object may be missing or stale:
@@ -383,12 +451,6 @@ bool IsNodeRelevantForAccessibility(const Node* node,
return !To<Text>(node)->ContainsOnlyWhitespaceOrEmpty();
}
- // Node also not relevant -- truncate subtree here.
- if (node->ContainingShadowRoot() &&
- !IsShadowContentRelevantForAccessibility(node)) {
- return false;
- }
-
if (!node->IsElementNode())
return false; // Only documents, elements and text nodes get ax objects.
@@ -400,6 +462,15 @@ bool IsNodeRelevantForAccessibility(const Node* node,
if (IsA<HTMLMapElement>(node))
return false; // Contains children for an img, but is not its own object.
+ if (node->IsPseudoElement())
+ return AXObjectCacheImpl::IsRelevantPseudoElement(*node);
+
+ // <optgroup> is irrelevant inside of a <select> menulist.
+ if (auto* opt_group = DynamicTo<HTMLOptGroupElement>(node)) {
+ if (auto* select = opt_group->OwnerSelectElement())
+ return !select->UsesMenuList();
+ }
+
// When there is a layout object, the element is known to be visible, so
// consider it relevant and return early. Checking the layout object is only
// useful when display locking (content-visibility) is not used.
@@ -408,6 +479,11 @@ bool IsNodeRelevantForAccessibility(const Node* node,
return true;
}
+ // An HTML <title> does not require an AXObject: the document's name is
+ // retrieved directly via the inner text.
+ if (IsA<HTMLTitleElement>(node))
+ return node->IsSVGElement();
+
// The node is either hidden or display locked:
// Do not consider <head>/<style>/<script> relevant in these cases.
if (IsA<HTMLHeadElement>(node))
@@ -431,9 +507,10 @@ bool IsNodeRelevantForAccessibility(const Node* node,
// <style> element.
if (parent_ax_known)
return true; // No need to check inside if the parent exists.
- // Objects inside <head> are irrelevant, except <title> (collects title text).
+
+ // Objects inside <head> are irrelevant.
if (Traversal<HTMLHeadElement>::FirstAncestor(*node))
- return IsA<HTMLTitleElement>(node);
+ return false;
// Objects inside a <style> are irrelevant.
if (Traversal<HTMLStyleElement>::FirstAncestor(*node))
return false;
@@ -454,12 +531,15 @@ bool IsNodeRelevantForAccessibility(const Node* node,
bool AXObjectCacheImpl::use_ax_menu_list_ = false;
// static
-AXObjectCache* AXObjectCacheImpl::Create(Document& document) {
- return MakeGarbageCollected<AXObjectCacheImpl>(document);
+AXObjectCache* AXObjectCacheImpl::Create(Document& document,
+ const ui::AXMode& ax_mode) {
+ return MakeGarbageCollected<AXObjectCacheImpl>(document, ax_mode);
}
-AXObjectCacheImpl::AXObjectCacheImpl(Document& document)
+AXObjectCacheImpl::AXObjectCacheImpl(Document& document,
+ const ui::AXMode& ax_mode)
: document_(document),
+ ax_mode_(ax_mode),
modification_count_(0),
validation_message_axid_(0),
active_aria_modal_dialog_(nullptr),
@@ -538,10 +618,8 @@ AXObject* AXObjectCacheImpl::GetOrCreateFocusedObjectFromNode(Node* node) {
// popup. Ensure the popup document has a clean layout before trying to
// create an AXObject from a node in it.
if (node->GetDocument().View()) {
- node->GetDocument()
- .View()
- ->UpdateLifecycleToCompositingCleanPlusScrolling(
- DocumentUpdateReason::kAccessibility);
+ node->GetDocument().View()->UpdateAllLifecyclePhasesExceptPaint(
+ DocumentUpdateReason::kAccessibility);
}
}
@@ -557,9 +635,16 @@ AXObject* AXObjectCacheImpl::GetOrCreateFocusedObjectFromNode(Node* node) {
return obj;
}
-
AXObject* AXObjectCacheImpl::FocusedObject() {
- return GetOrCreateFocusedObjectFromNode(this->FocusedElement());
+ return GetOrCreateFocusedObjectFromNode(FocusedElement());
+}
+
+const ui::AXMode& AXObjectCacheImpl::GetAXMode() {
+ return ax_mode_;
+}
+
+void AXObjectCacheImpl::SetAXMode(const ui::AXMode& ax_mode) {
+ ax_mode_ = ax_mode;
}
AXObject* AXObjectCacheImpl::Get(const LayoutObject* layout_object) {
@@ -574,7 +659,7 @@ AXObject* AXObjectCacheImpl::Get(const LayoutObject* layout_object) {
if (!ax_id)
return node ? Get(node) : nullptr;
- if ((node && DisplayLockUtilities::NearestLockedExclusiveAncestor(*node)) ||
+ if (IsDisplayLocked(node) ||
!IsLayoutObjectRelevantForAccessibility(*layout_object)) {
// Change from AXLayoutObject -> AXNodeObject.
// We previously saved the node in the cache with its layout object,
@@ -631,20 +716,23 @@ AXObject* AXObjectCacheImpl::Get(const Node* node) {
// objects that are no longer relevant.
Invalidate(layout_id);
} else {
- // Layout object is irrelevant, but node object is still relevant.
+ // Layout object is irrelevant, but node object can still be relevant.
+ if (!node_id) {
+ DCHECK(layout_id); // One of of node_id, layout_id is non-zero.
+ Invalidate(layout_id);
+ return nullptr;
+ }
layout_object = nullptr;
layout_id = 0;
}
}
- if (layout_id &&
- DisplayLockUtilities::NearestLockedExclusiveAncestor(*node)) {
+ if (layout_id && IsDisplayLocked(node)) {
// Change from AXLayoutObject -> AXNodeObject.
// The node is in a display locked subtree, but we've previously put it in
// the cache with its layout object.
Invalidate(layout_id);
- } else if (layout_object && node_id && !layout_id &&
- !DisplayLockUtilities::NearestLockedExclusiveAncestor(*node)) {
+ } else if (layout_object && node_id && !layout_id && !IsDisplayLocked(node)) {
// Change from AXNodeObject -> AXLayoutObject.
// Has a layout object but no layout_id, meaning that when the AXObject was
// originally created only for Node*, the LayoutObject* didn't exist yet.
@@ -758,6 +846,9 @@ AXObject* AXObjectCacheImpl::CreateFromRenderer(LayoutObject* layout_object) {
if (node && node->IsMediaElement())
return AccessibilityMediaElement::Create(layout_object, *this);
+ if (node && node->IsMediaControlElement())
+ return AccessibilityMediaControl::Create(layout_object, *this);
+
if (IsA<HTMLOptionElement>(node))
return MakeGarbageCollected<AXListBoxOption>(layout_object, *this);
@@ -767,22 +858,21 @@ AXObject* AXObjectCacheImpl::CreateFromRenderer(LayoutObject* layout_object) {
return MakeGarbageCollected<AXSlider>(layout_object, *this);
}
- if (layout_object->IsBoxModelObject()) {
- auto* css_box = To<LayoutBoxModelObject>(layout_object);
- if (auto* select_element = DynamicTo<HTMLSelectElement>(node)) {
- if (select_element->UsesMenuList()) {
- if (use_ax_menu_list_)
- return MakeGarbageCollected<AXMenuList>(css_box, *this);
- } else {
- return MakeGarbageCollected<AXListBox>(css_box, *this);
+ if (auto* select_element = DynamicTo<HTMLSelectElement>(node)) {
+ if (select_element->UsesMenuList()) {
+ if (use_ax_menu_list_) {
+ DCHECK(ShouldCreateAXMenuListFor(layout_object));
+ return MakeGarbageCollected<AXMenuList>(layout_object, *this);
}
+ } else {
+ return MakeGarbageCollected<AXListBox>(layout_object, *this);
}
+ }
- // progress bar
- if (css_box->IsProgress()) {
- return MakeGarbageCollected<AXProgressIndicator>(
- To<LayoutProgress>(css_box), *this);
- }
+ // progress bar
+ if (layout_object->IsProgress()) {
+ return MakeGarbageCollected<AXProgressIndicator>(
+ To<LayoutProgress>(layout_object), *this);
}
return MakeGarbageCollected<AXLayoutObject>(layout_object, *this);
@@ -795,22 +885,70 @@ bool AXObjectCacheImpl::ShouldCreateAXMenuListOptionFor(const Node* node) {
auto* option_element = DynamicTo<HTMLOptionElement>(node);
if (!option_element)
return false;
- const HTMLSelectElement* select = option_element->OwnerSelectElement();
- if (!select || !select->UsesMenuList())
+
+ if (auto* select = option_element->OwnerSelectElement())
+ return ShouldCreateAXMenuListFor(select->GetLayoutObject());
+
+ return false;
+}
+
+// static
+bool AXObjectCacheImpl::ShouldCreateAXMenuListFor(LayoutObject* layout_object) {
+ if (!layout_object)
+ return false;
+
+ if (!AXObjectCacheImpl::UseAXMenuList())
return false;
- return select->GetLayoutObject() && AXObjectCacheImpl::UseAXMenuList();
+
+ if (auto* select = DynamicTo<HTMLSelectElement>(layout_object->GetNode()))
+ return select->UsesMenuList();
+
+ return false;
}
// static
-bool AXObjectCacheImpl::IsPseudoElementDescendant(
+bool AXObjectCacheImpl::IsRelevantPseudoElement(const Node& node) {
+ DCHECK(node.IsPseudoElement());
+ if (!node.GetLayoutObject())
+ return false;
+
+ // ::before, ::after and ::marker are relevant.
+ // Allowing these pseudo elements ensures that all visible descendant
+ // pseudo content will be reached, despite only being able to walk layout
+ // inside of pseudo content.
+ // However, AXObjects aren't created for ::first-letter subtrees. The text
+ // of ::first-letter is already available in the child text node of the
+ // element that the CSS ::first letter applied to.
+ if (node.IsMarkerPseudoElement() || node.IsBeforePseudoElement() ||
+ node.IsAfterPseudoElement()) {
+ return true;
+ }
+
+ DCHECK(node.IsFirstLetterPseudoElement())
+ << "The only remaining type that should reach here.";
+
+ if (LayoutObject* layout_parent = node.GetLayoutObject()->Parent()) {
+ if (Node* layout_parent_node = layout_parent->GetNode()) {
+ if (layout_parent_node->IsPseudoElement())
+ return IsRelevantPseudoElement(*layout_parent_node);
+ }
+ }
+
+ return false;
+}
+
+// static
+bool AXObjectCacheImpl::IsRelevantPseudoElementDescendant(
const LayoutObject& layout_object) {
+ if (layout_object.IsText() && To<LayoutText>(layout_object).HasEmptyText())
+ return false;
const LayoutObject* ancestor = &layout_object;
while (true) {
ancestor = ancestor->Parent();
if (!ancestor)
return false;
if (ancestor->IsPseudoElement())
- return true;
+ return IsRelevantPseudoElement(*ancestor->GetNode());
if (!ancestor->IsAnonymous())
return false;
}
@@ -842,11 +980,13 @@ AXObject* AXObjectCacheImpl::GetOrCreate(AccessibleNode* accessible_node,
<< "A virtual object must have a parent, and cannot exist without one. "
"The parent is set when the object is constructed.";
+ if (!parent->CanHaveChildren())
+ return nullptr;
+
AXObject* new_obj =
MakeGarbageCollected<AXVirtualObject>(*this, accessible_node);
const AXID ax_id = AssociateAXID(new_obj);
accessible_node_mapping_.Set(accessible_node, ax_id);
-
new_obj->Init(parent);
return new_obj;
}
@@ -879,13 +1019,14 @@ AXObject* AXObjectCacheImpl::CreateAndInit(Node* node,
AXObject* parent_if_known,
AXID use_axid) {
DCHECK(node);
+ DCHECK(!parent_if_known || parent_if_known->CanHaveChildren());
// If the node has a layout object, prefer using that as the primary key for
// the AXObject, with the exception of the HTMLAreaElement and nodes within
// a locked subtree, which are created based on its node.
LayoutObject* layout_object = node->GetLayoutObject();
if (layout_object && IsLayoutObjectRelevantForAccessibility(*layout_object) &&
- !DisplayLockUtilities::NearestLockedExclusiveAncestor(*node)) {
+ !IsDisplayLocked(node)) {
return CreateAndInit(layout_object, parent_if_known, use_axid);
}
@@ -900,6 +1041,8 @@ AXObject* AXObjectCacheImpl::CreateAndInit(Node* node,
DCHECK(document->Lifecycle().GetState() >=
DocumentLifecycle::kAfterPerformLayout)
<< "Unclean document at lifecycle " << document->Lifecycle().ToString();
+ DCHECK_NE(node, document_)
+ << "The document's AXObject is backed by its layout object.";
#endif // DCHECK_IS_ON()
// Return null if inside a shadow tree of something that can't have children,
@@ -912,6 +1055,34 @@ AXObject* AXObjectCacheImpl::CreateAndInit(Node* node,
return nullptr;
}
+#if DCHECK_IS_ON()
+ if (!IsA<HTMLOptionElement>(node) && node->IsInUserAgentShadowRoot()) {
+ if (Node* owner_shadow_host = node->OwnerShadowHost()) {
+ DCHECK(!AXObjectCacheImpl::ShouldCreateAXMenuListFor(
+ owner_shadow_host->GetLayoutObject()))
+ << "DOM descendants of an AXMenuList should not be added to the AX "
+ "hierarchy, except for the AXMenuListOption children added in "
+ "AXMenuListPopup. An attempt was made to create an AXObject for: "
+ << node;
+ }
+ }
+#endif
+
+ AXObject* parent = parent_if_known
+ ? parent_if_known
+ : AXObject::ComputeNonARIAParent(*this, node);
+ // An AXObject backed only by a DOM node must have a parent, because it's
+ // never the root, which will always have a layout object.
+ if (!parent)
+ return nullptr;
+
+ DCHECK(parent->CanHaveChildren());
+
+ // One of the above calls could have already created the planned object via a
+ // recursive call to GetOrCreate(). If so, just return that object.
+ if (node_object_mapping_.at(node))
+ return Get(node);
+
AXObject* new_obj = CreateFromNode(node);
// Will crash later if we have two objects for the same node.
@@ -921,7 +1092,7 @@ AXObject* AXObjectCacheImpl::CreateAndInit(Node* node,
const AXID ax_id = AssociateAXID(new_obj, use_axid);
DCHECK(!HashTraits<AXID>::IsDeletedValue(ax_id));
node_object_mapping_.Set(node, ax_id);
- new_obj->Init(parent_if_known);
+ new_obj->Init(parent);
MaybeNewRelationTarget(*node, new_obj);
return new_obj;
@@ -952,8 +1123,8 @@ AXObject* AXObjectCacheImpl::CreateAndInit(LayoutObject* layout_object,
DCHECK(document->Lifecycle().GetState() >=
DocumentLifecycle::kAfterPerformLayout)
<< "Unclean document at lifecycle " << document->Lifecycle().ToString();
+ DCHECK(!parent_if_known || parent_if_known->CanHaveChildren());
#endif // DCHECK_IS_ON()
-
if (!IsLayoutObjectRelevantForAccessibility(*layout_object))
return nullptr;
@@ -972,6 +1143,20 @@ AXObject* AXObjectCacheImpl::CreateAndInit(LayoutObject* layout_object,
return nullptr;
}
+#if DCHECK_IS_ON()
+ if (node && !IsA<HTMLOptionElement>(node) &&
+ node->IsInUserAgentShadowRoot()) {
+ if (Node* owner_shadow_host = node->OwnerShadowHost()) {
+ DCHECK(!AXObjectCacheImpl::ShouldCreateAXMenuListFor(
+ owner_shadow_host->GetLayoutObject()))
+ << "DOM descendants of an AXMenuList should not be added to the AX "
+ "hierarchy, except for the AXMenuListOption children added in "
+ "AXMenuListPopup. An attempt was made to create an AXObject for: "
+ << node;
+ }
+ }
+#endif
+
// Prefer creating AXNodeObjects over AXLayoutObjects in locked subtrees
// (e.g. content-visibility: auto), even if a LayoutObject is available,
// because the LayoutObject is not guaranteed to be up-to-date (it might come
@@ -994,15 +1179,38 @@ AXObject* AXObjectCacheImpl::CreateAndInit(LayoutObject* layout_object,
return CreateAndInit(node, parent_if_known, use_axid);
}
+ AXObject* parent = parent_if_known ? parent_if_known
+ : AXObject::ComputeNonARIAParent(
+ *this, node, layout_object);
+ if (node == document_)
+ DCHECK(!parent);
+ else if (!parent)
+ return nullptr;
+ else
+ DCHECK(parent->CanHaveChildren());
+
+ // One of the above calls could have already created the planned object via a
+ // recursive call to GetOrCreate(). If so, just return that object.
+ // Example: parent calls Init() => ComputeAccessibilityIsIgnored() =>
+ // CanSetFocusAttribute() => CanBeActiveDescendant() =>
+ // IsARIAControlledByTextboxWithActiveDescendant() => GetOrCreate().
+ if (layout_object_mapping_.at(layout_object)) {
+ AXObject* result = Get(layout_object);
+ DCHECK(result) << "Missing cached AXObject for " << layout_object;
+ return result;
+ }
+
AXObject* new_obj = CreateFromRenderer(layout_object);
+ DCHECK(new_obj) << "Could not create AXObject for " << layout_object;
+
// Will crash later if we have two objects for the same layoutObject.
DCHECK(!layout_object_mapping_.at(layout_object))
<< "Already have an AXObject for " << layout_object;
const AXID axid = AssociateAXID(new_obj, use_axid);
layout_object_mapping_.Set(layout_object, axid);
- new_obj->Init(parent_if_known);
+ new_obj->Init(parent);
if (node) // There may not be a node, e.g. for an anonymous block.
MaybeNewRelationTarget(*node, new_obj);
@@ -1059,6 +1267,7 @@ AXObject* AXObjectCacheImpl::GetOrCreate(AbstractInlineTextBox* inline_text_box,
AXObject* AXObjectCacheImpl::CreateAndInit(ax::mojom::blink::Role role,
AXObject* parent) {
DCHECK(parent);
+ DCHECK(parent->CanHaveChildren());
AXObject* obj = nullptr;
switch (role) {
@@ -1079,15 +1288,18 @@ AXObject* AXObjectCacheImpl::CreateAndInit(ax::mojom::blink::Role role,
return obj;
}
-void AXObjectCacheImpl::RemoveAXObjectsInLayoutSubtree(AXObject* subtree) {
- if (!subtree)
+void AXObjectCacheImpl::RemoveAXObjectsInLayoutSubtree(AXObject* subtree,
+ int depth) {
+ if (!subtree || depth <= 0)
return;
+ depth--;
+
LayoutObject* layout_object = subtree->GetLayoutObject();
if (layout_object) {
LayoutObject* layout_child = layout_object->SlowFirstChild();
while (layout_child) {
- RemoveAXObjectsInLayoutSubtree(Get(layout_child));
+ RemoveAXObjectsInLayoutSubtree(Get(layout_child), depth);
layout_child = layout_child->NextSibling();
}
}
@@ -1107,6 +1319,13 @@ void AXObjectCacheImpl::Remove(AXObject* object) {
Remove(object->AXObjectID());
}
+// This is safe to call even if there isn't a current mapping.
+// This is called by other Remove() methods, called by Blink for DOM and layout
+// changes, iterating over all removed content in the subtree:
+// - When a DOM subtree is removed, it is called with the root node first, and
+// then descending down into the subtree.
+// - When layout for a subtree is detached, it is called on layout objects,
+// starting with leaves and moving upward, ending with the subtree root.
void AXObjectCacheImpl::Remove(AXID ax_id) {
if (!ax_id)
return;
@@ -1116,9 +1335,9 @@ void AXObjectCacheImpl::Remove(AXID ax_id) {
if (!obj)
return;
- ChildrenChanged(obj->CachedParentObject());
-
+ ChildrenChangedOnAncestorOf(obj);
obj->Detach();
+
RemoveAXID(obj);
// Finally, remove the object.
@@ -1135,18 +1354,23 @@ void AXObjectCacheImpl::Remove(AccessibleNode* accessible_node) {
return;
AXID ax_id = accessible_node_mapping_.at(accessible_node);
- Remove(ax_id);
accessible_node_mapping_.erase(accessible_node);
+
+ Remove(ax_id);
}
-void AXObjectCacheImpl::Remove(LayoutObject* layout_object) {
+bool AXObjectCacheImpl::Remove(LayoutObject* layout_object) {
if (!layout_object)
- return;
+ return false;
AXID ax_id = layout_object_mapping_.at(layout_object);
+ if (!ax_id)
+ return false;
- Remove(ax_id);
layout_object_mapping_.erase(layout_object);
+ Remove(ax_id);
+
+ return true;
}
void AXObjectCacheImpl::Remove(Node* node) {
@@ -1155,11 +1379,10 @@ void AXObjectCacheImpl::Remove(Node* node) {
// This is all safe even if we didn't have a mapping.
AXID ax_id = node_object_mapping_.at(node);
- Remove(ax_id);
node_object_mapping_.erase(node);
- if (node->GetLayoutObject())
- Remove(node->GetLayoutObject());
+ if (!Remove(node->GetLayoutObject()))
+ Remove(ax_id);
}
void AXObjectCacheImpl::Remove(AbstractInlineTextBox* inline_text_box) {
@@ -1167,8 +1390,9 @@ void AXObjectCacheImpl::Remove(AbstractInlineTextBox* inline_text_box) {
return;
AXID ax_id = inline_text_box_object_mapping_.at(inline_text_box);
- Remove(ax_id);
inline_text_box_object_mapping_.erase(inline_text_box);
+
+ Remove(ax_id);
}
AXID AXObjectCacheImpl::GenerateAXID() const {
@@ -1286,15 +1510,20 @@ void AXObjectCacheImpl::DeferTreeUpdateInternal(base::OnceClosure callback,
}
#if DCHECK_IS_ON()
- DCHECK(!tree_update_document->GetPage()->Animator().IsServicingAnimations() ||
- (tree_update_document->Lifecycle().GetState() <
- DocumentLifecycle::kInAccessibility ||
- tree_update_document->Lifecycle().StateAllowsDetach()))
- << "DeferTreeUpdateInternal should only be outside of the lifecycle or "
- "before the accessibility state:"
- << "\n* IsServicingAnimations: "
- << tree_update_document->GetPage()->Animator().IsServicingAnimations()
- << "\n* Lifecycle: " << tree_update_document->Lifecycle().ToString();
+ // TODO(accessibility) Restore this check. Currently must be removed because a
+ // loop in ProcessDeferredAccessibilityEvents() is allowed to queue deferred
+ // ChildrenChanged() events and process them.
+ // DCHECK(!tree_update_document->GetPage()->Animator().IsServicingAnimations()
+ // ||
+ // (tree_update_document->Lifecycle().GetState() <
+ // DocumentLifecycle::kInAccessibility ||
+ // tree_update_document->Lifecycle().StateAllowsDetach()))
+ // << "DeferTreeUpdateInternal should only be outside of the lifecycle or
+ // "
+ // "before the accessibility state:"
+ // << "\n* IsServicingAnimations: "
+ // << tree_update_document->GetPage()->Animator().IsServicingAnimations()
+ // << "\n* Lifecycle: " << tree_update_document->Lifecycle().ToString();
#endif
tree_update_callback_queue_.push_back(MakeGarbageCollected<TreeUpdateParams>(
@@ -1510,7 +1739,8 @@ void AXObjectCacheImpl::TextChangedWithCleanLayout(
#endif // DCHECK_IS_ON()
if (obj) {
- if (obj->RoleValue() == ax::mojom::blink::Role::kStaticText) {
+ if (obj->RoleValue() == ax::mojom::blink::Role::kStaticText &&
+ obj->LastKnownIsIncludedInTreeValue()) {
Settings* settings = GetSettings();
if (settings && settings->GetInlineTextBoxAccessibilityEnabled()) {
// Update inline text box children.
@@ -1544,12 +1774,16 @@ void AXObjectCacheImpl::FocusableChangedWithCleanLayout(Element* element) {
if (obj->AriaHiddenRoot()) {
// Elements that are hidden but focusable are not ignored. Therefore, if a
// hidden element's focusable state changes, it's ignored state must be
- // recomputed.
- ChildrenChangedWithCleanLayout(element->parentNode());
+ // recomputed. It may be newly included in the tree, which means the
+ // parents must be updated.
+ // TODO(accessibility) Is this necessary? We have other places in the code
+ // that automatically do a children changed on parents of nodes whose
+ // ignored or included states change.
+ ChildrenChangedWithCleanLayout(obj->CachedParentObject());
}
// Refresh the focusable state and State::kIgnored on the exposed object.
- MarkAXObjectDirty(obj, false);
+ MarkAXObjectDirtyWithCleanLayout(obj, false);
}
void AXObjectCacheImpl::DocumentTitleChanged() {
@@ -1567,6 +1801,41 @@ void AXObjectCacheImpl::UpdateCacheAfterNodeIsAttached(Node* node) {
&AXObjectCacheImpl::UpdateCacheAfterNodeIsAttachedWithCleanLayout, node);
}
+bool AXObjectCacheImpl::IsStillInTree(AXObject* obj) {
+ // Return an AXObject for the node if the AXObject is still in the tree.
+ // If there is a viable included parent, that means it's still in the tree.
+ // Otherwise, repair missing parent, or prune the object if no viable parent
+ // can be found. For example, through CSS changes, an ancestor became an
+ // image, which is always a leaf; therefore, no descendants are "in the tree".
+
+ if (!obj)
+ return false;
+
+ if (obj->IsMissingParent()) {
+ // Parent is missing. Attempt to repair it with a viable recomputed parent.
+ AXObject* ax_parent = obj->ComputeParent();
+ if (!IsStillInTree(ax_parent)) {
+ // Parent is unrepairable, meaning that this AXObject can no longer be
+ // attached to the tree and is no longer viable. Prune it now.
+ Remove(obj);
+ return false;
+ }
+ obj->SetParent(ax_parent);
+ return true;
+ }
+
+ if (!obj->LastKnownIsIncludedInTreeValue()) {
+ // Current object was not included in the tree, therefore, recursively
+ // keep checking up a level until a viable included parent is found.
+ if (!IsStillInTree(obj->CachedParentObject())) {
+ Remove(obj);
+ return false;
+ }
+ }
+
+ return true;
+}
+
void AXObjectCacheImpl::UpdateCacheAfterNodeIsAttachedWithCleanLayout(
Node* node) {
if (!node || !node->isConnected())
@@ -1601,7 +1870,8 @@ void AXObjectCacheImpl::UpdateCacheAfterNodeIsAttachedWithCleanLayout(
// descendants of the attached node, thus ChildrenChangedWithCleanLayout()
// must be called. It handles ignored logic, ensuring that the first ancestor
// that should have this as a child will be updated.
- ChildrenChangedWithCleanLayout(LayoutTreeBuilderTraversal::Parent(*node));
+ ChildrenChangedWithCleanLayout(
+ Get(LayoutTreeBuilderTraversal::Parent(*node)));
}
void AXObjectCacheImpl::DidInsertChildrenOfNode(Node* node) {
@@ -1618,30 +1888,85 @@ void AXObjectCacheImpl::DidInsertChildrenOfNode(Node* node) {
}
}
-void AXObjectCacheImpl::ChildrenChanged(const AXObject* obj) {
- ChildrenChanged(const_cast<AXObject*>(obj));
-}
+// Note: do not call this when a child is becoming newly included, because
+// it will return early if |obj| was last known to be unincluded.
+void AXObjectCacheImpl::ChildrenChangedOnAncestorOf(AXObject* obj) {
+ DCHECK(obj);
+ DCHECK(!obj->IsDetached());
-void AXObjectCacheImpl::ChildrenChanged(AXObject* obj) {
- if (!obj)
+ // If |obj| is not included, and it has no included descendants, then there is
+ // nothing in any ancestor's cached children that needs clearing. This rule
+ // improves performance when removing an entire subtree of unincluded nodes.
+ // For example, if a <div id="root" style="display:none"> will be
+ // included because it is a potential relation target. If unincluded
+ // descendants change, no ChildrenChanged() processing is necessary, because
+ // #root has no children.
+ if (!obj->LastKnownIsIncludedInTreeValue() &&
+ obj->CachedChildrenIncludingIgnored().IsEmpty()) {
return;
+ }
- Node* node = obj->GetNode();
- if (node && !nodes_with_pending_children_changed_.insert(node).is_new_entry)
- return;
+ // Clear children of ancestors in order to ensure this detached object is not
+ // cached an ancestor's list of children:
+ // Any ancestor up to the first included ancestor can contain the now-detached
+ // child in it's cached children, and therefore must update children.
+ ChildrenChanged(obj->CachedParentObject());
+}
- DeferTreeUpdate(&AXObjectCacheImpl::ChildrenChangedWithCleanLayout, obj);
+void AXObjectCacheImpl::ChildrenChangedWithCleanLayout(AXObject* obj) {
+ if (AXObject* ax_ancestor_for_notification = InvalidateChildren(obj)) {
+ ChildrenChangedWithCleanLayout(ax_ancestor_for_notification->GetNode(),
+ ax_ancestor_for_notification);
+ }
}
-void AXObjectCacheImpl::ChildrenChanged(Node* node) {
- if (!node)
- return;
+void AXObjectCacheImpl::ChildrenChanged(AXObject* obj) {
+ if (AXObject* ax_ancestor_for_notification = InvalidateChildren(obj)) {
+ DeferTreeUpdate(&AXObjectCacheImpl::ChildrenChangedWithCleanLayout,
+ ax_ancestor_for_notification);
+ }
+}
+AXObject* AXObjectCacheImpl::InvalidateChildren(AXObject* obj) {
+ if (!obj)
+ return nullptr;
+
+ // Clear children of ancestors in order to ensure this detached object is not
+ // cached an ancestor's list of children:
+ // Any ancestor up to the first included ancestor can contain the now-detached
+ // child in it's cached children, and therefore must update children.
+ AXObject* ancestor = obj;
+ while (ancestor && !ancestor->LastKnownIsIncludedInTreeValue()) {
+ if (ancestor->NeedsToUpdateChildren() || ancestor->IsDetached())
+ return nullptr; // Processing has already occurred for this ancestor.
+ ancestor->SetNeedsToUpdateChildren();
+ ancestor = ancestor->CachedParentObject();
+ }
+
+ // Only process ChildrenChanged() events on the included ancestor. This allows
+ // deduping of ChildrenChanged() occurrences within the same subtree.
+ // For example, if a subtree has unincluded children, but included
+ // grandchildren have changed, only the root children changed needs to be
+ // processed.
+ if (!ancestor)
+ return nullptr;
// Don't enqueue a deferred event on the same node more than once.
- if (!nodes_with_pending_children_changed_.insert(node).is_new_entry)
- return;
+ if (ancestor->GetNode() &&
+ !nodes_with_pending_children_changed_.insert(ancestor->GetNode())
+ .is_new_entry) {
+ return nullptr;
+ }
+
+ // Return ancestor to fire children changed notification on.
+ DCHECK(ancestor->LastKnownIsIncludedInTreeValue())
+ << "ChildrenChanged() must only be called on included nodes: "
+ << ancestor->ToString(true, true);
- DeferTreeUpdate(&AXObjectCacheImpl::ChildrenChangedWithCleanLayout, node);
+ return ancestor;
+}
+
+void AXObjectCacheImpl::ChildrenChanged(Node* node) {
+ ChildrenChanged(Get(node));
}
void AXObjectCacheImpl::ChildrenChanged(const LayoutObject* layout_object) {
@@ -1654,15 +1979,10 @@ void AXObjectCacheImpl::ChildrenChanged(const LayoutObject* layout_object) {
// Update using nearest node (walking ancestors if necessary).
Node* node = GetClosestNodeForLayoutObject(layout_object);
-
if (!node)
return;
- // Don't enqueue a deferred event on the same node more than once.
- if (!nodes_with_pending_children_changed_.insert(node).is_new_entry)
- return;
-
- DeferTreeUpdate(&AXObjectCacheImpl::ChildrenChangedWithCleanLayout, node);
+ ChildrenChanged(Get(node));
if (!layout_object->IsAnonymous())
return;
@@ -1691,18 +2011,12 @@ void AXObjectCacheImpl::ChildrenChanged(const LayoutObject* layout_object) {
for (Node* child = LayoutTreeBuilderTraversal::FirstChild(*node); child;
child = LayoutTreeBuilderTraversal::NextSibling(*child)) {
- DeferTreeUpdate(&AXObjectCacheImpl::ChildrenChangedWithCleanLayout, child);
+ ChildrenChanged(Get(child));
}
}
void AXObjectCacheImpl::ChildrenChanged(AccessibleNode* accessible_node) {
- if (!accessible_node)
- return;
-
- AXObject* object = Get(accessible_node);
- if (!object)
- return;
- DeferTreeUpdate(&AXObjectCacheImpl::ChildrenChangedWithCleanLayout, object);
+ ChildrenChanged(Get(accessible_node));
}
void AXObjectCacheImpl::ChildrenChangedWithCleanLayout(Node* node) {
@@ -1742,8 +2056,11 @@ void AXObjectCacheImpl::ChildrenChangedWithCleanLayout(Node* optional_node,
<< "Unclean document at lifecycle " << document->Lifecycle().ToString();
#endif // DCHECK_IS_ON()
- if (obj)
- obj->ChildrenChanged();
+ if (obj) {
+ if (!IsStillInTree(obj))
+ return; // Object is no longer in tree, and therefore not viable.
+ obj->ChildrenChangedWithCleanLayout();
+ }
if (optional_node)
relation_cache_->UpdateRelatedTree(optional_node, obj);
@@ -1758,23 +2075,41 @@ void AXObjectCacheImpl::ProcessDeferredAccessibilityEvents(Document& document) {
return;
}
- // Destroy and recreate any objects which are no longer valid, for example
- // they used AXNodeObject and now must be an AXLayoutObject, or vice-versa.
- // Also fires children changed on the parent of these nodes.
- ProcessInvalidatedObjects(document);
-
- // Call the queued callback methods that do processing which must occur when
- // layout is clean. These callbacks are stored in tree_update_callback_queue_,
- // and have names like FooBarredWithCleanLayout().
- ProcessCleanLayoutCallbacks(document);
+ SCOPED_UMA_HISTOGRAM_TIMER(
+ "Accessibility.Performance.ProcessDeferredAccessibilityEvents");
- // Changes to ids or aria-owns may have resulted in queued up relation
- // cache work; do that now.
- relation_cache_->ProcessUpdatesWithCleanLayout();
+#if DCHECK_IS_ON()
+ int loop_counter = 0;
+#endif
- // Perform this step a second time, to refresh any new invalidated objects
- // from the previous deferred processing steps.
- ProcessInvalidatedObjects(document);
+ do {
+ // Destroy and recreate any objects which are no longer valid, for example
+ // they used AXNodeObject and now must be an AXLayoutObject, or vice-versa.
+ // Also fires children changed on the parent of these nodes.
+ ProcessInvalidatedObjects(document);
+
+ // Call the queued callback methods that do processing which must occur when
+ // layout is clean. These callbacks are stored in
+ // tree_update_callback_queue_, and have names like
+ // FooBarredWithCleanLayout().
+ ProcessCleanLayoutCallbacks(document);
+
+ // Changes to ids or aria-owns may have resulted in queued up relation
+ // cache work; do that now.
+ relation_cache_->ProcessUpdatesWithCleanLayout();
+
+ // Keep going if there are more ids to invalidate or children changes to
+ // process from previous steps. For examople, a display locked
+ // (content-visibility:auto) element could be invalidated as it is scrolled
+ // in or out of view, causing Invalidate() to add it to invalidated_ids_.
+ // As ProcessInvalidatedObjects() refreshes the objectt and calls
+ // ChildrenChanged() on the parent, more objects may be invalidated, or
+ // more objects may have children changed called on them.
+#if DCHECK_IS_ON()
+ DCHECK_LE(++loop_counter, 100) << "Probable infinite loop detected.";
+#endif
+ } while (!nodes_with_pending_children_changed_.IsEmpty() ||
+ !invalidated_ids_.IsEmpty());
// Send events to RenderAccessibilityImpl, which serializes them and then
// sends the serialized events and dirty objects to the browser process.
@@ -1798,12 +2133,12 @@ void AXObjectCacheImpl::EmbeddingTokenChanged(HTMLFrameOwnerElement* element) {
void AXObjectCacheImpl::ProcessInvalidatedObjects(Document& document) {
HashSet<AXID> wrong_document_invalidated_ids;
HashSet<AXID> old_invalidated_ids;
- HashSet<AXID> pending_children_changed_ids;
// Create a new object with the same AXID as the old one.
// Currently only supported for objects with a backing node.
// Returns the new object.
auto refresh = [this](AXObject* current) {
+ DCHECK(current);
Node* node = current->GetNode();
DCHECK(node) << "Refresh() is currently only supported for objects "
"with a backing node.";
@@ -1817,7 +2152,10 @@ void AXObjectCacheImpl::ProcessInvalidatedObjects(Document& document) {
DCHECK(!layout_object_mapping_.at(node->GetLayoutObject()))
<< node << " " << node->GetLayoutObject();
}
+
+ ChildrenChangedOnAncestorOf(current);
current->Detach();
+
// TODO(accessibility) We don't use the return value, can we use .erase()
// and it will still make sure that the object is cleaned up?
objects_.Take(retained_axid);
@@ -1827,15 +2165,25 @@ void AXObjectCacheImpl::ProcessInvalidatedObjects(Document& document) {
// it could be handled in RoleChangedWithCleanLayout(), and the cached
// parent could be used.
AXObject* new_object = CreateAndInit(node, nullptr, retained_axid);
- if (!new_object)
- RemoveAXID(current); // Failed to create, so remove object completely.
+ if (new_object) {
+ // Any owned objects need to reset their parent_ to point to the
+ // new object.
+ if (AXObject::HasARIAOwns(DynamicTo<Element>(node)) &&
+ AXRelationCache::IsValidOwner(new_object)) {
+ relation_cache_->UpdateAriaOwnsWithCleanLayout(new_object, true);
+ }
+ } else {
+ // Failed to create, so remove object completely.
+ RemoveAXID(current);
+ }
+
return new_object;
};
while (!invalidated_ids_.IsEmpty()) {
- // ChildrenChanged() below may invalidate more objects. This outer loop
- // ensures all newly invalid objects are caught and refreshed before the
- // function returns.
+ // ChildrenChanged() calls from below work may invalidate more objects. This
+ // outer loop ensures all newly invalid objects are caught and refreshed
+ // before the function returns.
old_invalidated_ids.swap(invalidated_ids_);
for (AXID ax_id : old_invalidated_ids) {
AXObject* object = ObjectFromAXID(ax_id);
@@ -1849,8 +2197,10 @@ void AXObjectCacheImpl::ProcessInvalidatedObjects(Document& document) {
continue;
}
+#if defined(AX_FAIL_FAST_BUILD)
bool did_use_layout_object_traversal =
object->ShouldUseLayoutObjectTraversalForChildren();
+#endif
// Invalidate children on the first available non-detached parent that is
// included in the tree. Sometimes a cached parent is detached because
@@ -1865,43 +2215,39 @@ void AXObjectCacheImpl::ProcessInvalidatedObjects(Document& document) {
// refreshing and initializing the new object can occur (a parent is
// required).
candidate_parent = parent->ComputeParent();
- parent->SetParent(candidate_parent);
+ if (candidate_parent)
+ parent->SetParent(candidate_parent);
}
- if (!candidate_parent)
+ parent = candidate_parent;
+ if (!parent)
break; // No higher candidate parent found, will invalidate |parent|.
- parent = candidate_parent;
// Queue up a ChildrenChanged() call for this parent.
- pending_children_changed_ids.insert(parent->AXObjectID());
if (parent->LastKnownIsIncludedInTreeValue())
break; // Stop here (otherwise continue to higher ancestor).
}
+ if (!parent) {
+ // If no parent is possible, prune from the tree.
+ Remove(object);
+ continue;
+ }
+
AXObject* new_object = refresh(object);
MarkAXObjectDirtyWithCleanLayout(new_object, false);
- // Children might change because child traversal style changed.
- if (new_object &&
- new_object->ShouldUseLayoutObjectTraversalForChildren() !=
- did_use_layout_object_traversal) {
- // TODO(accessibility) Need test for this.
- DCHECK(!HashTraits<AXID>::IsDeletedValue(ax_id));
- pending_children_changed_ids.insert(ax_id);
- }
- }
- // Update parents' children.
- for (AXID parent_id : pending_children_changed_ids) {
- AXObject* parent = ObjectFromAXID(parent_id);
- if (parent && !parent->NeedsToUpdateChildren()) {
- // Invalidate the parent's children.
- ChildrenChangedWithCleanLayout(parent->GetNode(), parent);
- // Update children now.
- parent->UpdateChildrenIfNecessary();
- }
+#if defined(AX_FAIL_FAST_BUILD)
+ SANITIZER_CHECK(!new_object ||
+ new_object->ShouldUseLayoutObjectTraversalForChildren() ==
+ did_use_layout_object_traversal)
+ << "This should no longer be possible, an object only uses layout "
+ "object traversal if it is part of a pseudo element subtree, "
+ "and that never changes: "
+ << new_object->ToString(true, true);
+#endif
}
old_invalidated_ids.clear();
- pending_children_changed_ids.clear();
}
// Invalidate these objects when their document is clean.
invalidated_ids_.swap(wrong_document_invalidated_ids);
@@ -2115,7 +2461,7 @@ void AXObjectCacheImpl::FireAXEventImmediately(
const bool is_in_tree = obj->LastKnownIsIncludedInTreeValue();
if (is_ignored != was_ignored || was_in_tree != is_in_tree)
- ChildrenChangedWithCleanLayout(nullptr, obj->CachedParentObject());
+ ChildrenChangedWithCleanLayout(obj->CachedParentObject());
}
}
@@ -2256,7 +2602,7 @@ void AXObjectCacheImpl::HandleNodeGainedFocusWithCleanLayout(Node* node) {
// This should only occur when focus goes into a popup document. The main
// document has an updated layout, but the popup does not.
DCHECK_NE(document_, node->GetDocument());
- node->GetDocument().View()->UpdateLifecycleToCompositingCleanPlusScrolling(
+ node->GetDocument().View()->UpdateAllLifecyclePhasesExceptPaint(
DocumentUpdateReason::kAccessibility);
}
@@ -2309,6 +2655,25 @@ void AXObjectCacheImpl::HandleActiveDescendantChangedWithCleanLayout(
obj->HandleActiveDescendantChanged();
}
+// A <section> or role=region uses the region role if and only if it has a name.
+void AXObjectCacheImpl::SectionOrRegionRoleMaybeChanged(Element* element) {
+ AXObject* ax_object = Get(element);
+ if (!ax_object)
+ return;
+
+ // Require <section> or role="region" markup.
+ if (!element->HasTagName(html_names::kSectionTag) &&
+ ax_object->RawAriaRole() != ax::mojom::blink::Role::kRegion) {
+ return;
+ }
+
+ // If role would stay the same, do nothing.
+ if (ax_object->RoleValue() == ax_object->DetermineAccessibilityRole())
+ return;
+
+ Invalidate(ax_object->AXObjectID());
+}
+
// Be as safe as possible about changes that could alter the accessibility role,
// as this may require a different subclass of AXObject.
// Role changes are disallowed by the spec but we must handle it gracefully, see
@@ -2325,16 +2690,23 @@ void AXObjectCacheImpl::HandleRoleChangeWithCleanLayout(Node* node) {
// a new one needs to be created in its place. We destroy the current
// AXObject in this method and call ChildrenChangeWithCleanLayout() on the
// parent so that future updates to its children will create the alert.
- ChildrenChangedWithCleanLayout(nullptr, obj->CachedParentObject());
- LayoutObject* layout_object = node->GetLayoutObject();
- if (layout_object && layout_object->IsTable()) {
- // If role changes on a table, invalidate the entire table subtree as many
- // objects may suddenly need to change, because presentation is inherited
- // from the table to rows and cells.
- RemoveAXObjectsInLayoutSubtree(obj);
+ ChildrenChangedWithCleanLayout(obj->CachedParentObject());
+ if (int depth = RolePresentationPropagationDepth(node)) {
+ // If role changes on a table, menu, or list invalidate the subtree of
+ // objects that may require a specific parent role in order to keep their
+ // role. For example, rows and cells require a table ancestor, and list
+ // items require a parent list (must be direct DOM parent).
+ RemoveAXObjectsInLayoutSubtree(obj, depth);
} else {
- Remove(node);
+ // The children of this thing need to detach from parent.
+ Remove(obj);
}
+ // The aria-owns relation may have changed if the role changed,
+ // because some roles allow aria-owns and others don't.
+ // In addition, any owned objects need to reset their parent_ to point
+ // to the new object.
+ if (AXObject* new_object = GetOrCreate(node))
+ relation_cache_->UpdateAriaOwnsWithCleanLayout(new_object, true);
}
}
@@ -2386,8 +2758,8 @@ void AXObjectCacheImpl::HandleAriaHiddenChangedWithCleanLayout(Node* node) {
// Invalidate the subtree because aria-hidden affects the
// accessibility ignored state for the entire subtree.
- MarkAXObjectDirty(obj, /*subtree=*/true);
- ChildrenChangedWithCleanLayout(node->parentNode());
+ MarkAXObjectDirtyWithCleanLayout(obj, /*subtree=*/true);
+ ChildrenChangedWithCleanLayout(obj->CachedParentObject());
}
void AXObjectCacheImpl::HandleAttributeChanged(const QualifiedName& attr_name,
@@ -2413,9 +2785,11 @@ void AXObjectCacheImpl::HandleAttributeChangedWithCleanLayout(
if (!obj->IsTextField())
HandleRoleChangeWithCleanLayout(element);
}
- } else if (attr_name == html_names::kAltAttr ||
- attr_name == html_names::kTitleAttr) {
+ } else if (attr_name == html_names::kAltAttr) {
+ TextChangedWithCleanLayout(element);
+ } else if (attr_name == html_names::kTitleAttr) {
TextChangedWithCleanLayout(element);
+ SectionOrRegionRoleMaybeChanged(element);
} else if (attr_name == html_names::kForAttr &&
IsA<HTMLLabelElement>(*element)) {
LabelChangedWithCleanLayout(element);
@@ -2452,6 +2826,7 @@ void AXObjectCacheImpl::HandleAttributeChangedWithCleanLayout(
attr_name == html_names::kAriaLabeledbyAttr ||
attr_name == html_names::kAriaLabelledbyAttr) {
TextChangedWithCleanLayout(element);
+ SectionOrRegionRoleMaybeChanged(element);
} else if (attr_name == html_names::kAriaDescriptionAttr ||
attr_name == html_names::kAriaDescribedbyAttr) {
TextChangedWithCleanLayout(element);
@@ -2691,7 +3066,7 @@ Settings* AXObjectCacheImpl::GetSettings() {
}
bool AXObjectCacheImpl::InlineTextBoxAccessibilityEnabled() {
- Settings* settings = this->GetSettings();
+ Settings* settings = GetSettings();
if (!settings)
return false;
return settings->GetInlineTextBoxAccessibilityEnabled();
@@ -2878,7 +3253,7 @@ void AXObjectCacheImpl::HandleFocusedUIElementChanged(
UpdateActiveAriaModalDialog(new_focused_element);
DeferTreeUpdate(&AXObjectCacheImpl::HandleNodeGainedFocusWithCleanLayout,
- this->FocusedElement());
+ FocusedElement());
}
// Check if the focused node is inside an active aria-modal dialog. If so, we
@@ -3138,8 +3513,8 @@ const AtomicString& AXObjectCacheImpl::ComputedRoleForNode(Node* node) {
AXObject* obj = GetOrCreate(node);
if (!obj)
- return AXObject::RoleName(ax::mojom::Role::kUnknown);
- return AXObject::RoleName(obj->RoleValue());
+ return AXObject::ARIARoleName(ax::mojom::blink::Role::kUnknown);
+ return AXObject::ARIARoleName(obj->RoleValue());
}
String AXObjectCacheImpl::ComputedNameForNode(Node* node) {
diff --git a/chromium/third_party/blink/renderer/modules/accessibility/ax_object_cache_impl.h b/chromium/third_party/blink/renderer/modules/accessibility/ax_object_cache_impl.h
index 0bb4cfec7e8..8db06a4ca38 100644
--- a/chromium/third_party/blink/renderer/modules/accessibility/ax_object_cache_impl.h
+++ b/chromium/third_party/blink/renderer/modules/accessibility/ax_object_cache_impl.h
@@ -32,6 +32,7 @@
#include <memory>
#include <utility>
+#include "base/dcheck_is_on.h"
#include "base/gtest_prod_util.h"
#include "base/macros.h"
#include "third_party/blink/public/mojom/permissions/permission.mojom-blink.h"
@@ -51,6 +52,7 @@
#include "third_party/blink/renderer/platform/wtf/hash_set.h"
#include "third_party/blink/renderer/platform/wtf/vector.h"
#include "ui/accessibility/ax_enums.mojom-blink-forward.h"
+#include "ui/accessibility/ax_mode.h"
namespace blink {
@@ -64,15 +66,18 @@ class MODULES_EXPORT AXObjectCacheImpl
: public AXObjectCacheBase,
public mojom::blink::PermissionObserver {
public:
- static AXObjectCache* Create(Document&);
+ static AXObjectCache* Create(Document&, const ui::AXMode&);
- explicit AXObjectCacheImpl(Document&);
+ AXObjectCacheImpl(Document&, const ui::AXMode&);
~AXObjectCacheImpl() override;
void Trace(Visitor*) const override;
Document& GetDocument() { return *document_; }
AXObject* FocusedObject();
+ const ui::AXMode& GetAXMode() override;
+ void SetAXMode(const ui::AXMode&) override;
+
void Dispose() override;
void Freeze() override { is_frozen_ = true; }
@@ -94,7 +99,7 @@ class MODULES_EXPORT AXObjectCacheImpl
void UpdateReverseRelations(const AXObject* relation_source,
const Vector<String>& target_ids);
void ChildrenChanged(AXObject*);
- void ChildrenChanged(const AXObject*);
+ void ChildrenChangedWithCleanLayout(AXObject*);
void ChildrenChanged(Node*) override;
void ChildrenChanged(const LayoutObject*) override;
void ChildrenChanged(AccessibleNode*) override;
@@ -106,11 +111,18 @@ class MODULES_EXPORT AXObjectCacheImpl
void ImageLoaded(const LayoutObject*) override;
void Remove(AccessibleNode*) override;
- void Remove(LayoutObject*) override;
+ // Returns false if no associated AXObject exists in the cache.
+ bool Remove(LayoutObject*) override;
void Remove(Node*) override;
void Remove(AbstractInlineTextBox*) override;
void Remove(AXObject*); // Calls more specific Remove methods as necessary.
+ // For any ancestor that could contain the passed-in AXObject* in their cached
+ // children, clear their children and set needs to update children on them.
+ // In addition, ChildrenChanged() on an included ancestor that might contain
+ // this child, if one exists.
+ void ChildrenChangedOnAncestorOf(AXObject*);
+
const Element* RootAXEditableElement(const Node*) override;
// Called when aspects of the style (e.g. color, alignment) change.
@@ -205,9 +217,18 @@ class MODULES_EXPORT AXObjectCacheImpl
AXObject* Get(AccessibleNode*);
AXObject* Get(AbstractInlineTextBox*);
- AXObject* Get(const Node*) override;
+ // Get an AXObject* backed by the passed-in DOM node or the node's layout
+ // object, whichever is available.
+ // If it no longer the correct type of AXObject (AXNodeObject/AXLayoutObject),
+ // will Invalidate() the AXObject so that it is refreshed with a new object
+ // when safe to do so.
+ AXObject* Get(const Node*);
AXObject* Get(const LayoutObject*);
+ // Return true if the object is still part of the tree, meaning that ancestors
+ // exist or can be repaired all the way to the root.
+ bool IsStillInTree(AXObject*);
+
AXObject* FirstAccessibleObjectFromNode(const Node*);
void ChildrenChangedWithCleanLayout(Node* optional_node_for_relation_update,
@@ -218,6 +239,7 @@ class MODULES_EXPORT AXObjectCacheImpl
void MaybeNewRelationTarget(Node& node, AXObject* obj);
void HandleActiveDescendantChangedWithCleanLayout(Node*);
+ void SectionOrRegionRoleMaybeChanged(Element* element);
void HandleRoleChangeWithCleanLayout(Node*);
void HandleAriaHiddenChangedWithCleanLayout(Node*);
void HandleAriaExpandedChangeWithCleanLayout(Node*);
@@ -311,8 +333,11 @@ class MODULES_EXPORT AXObjectCacheImpl
AXObject* GetActiveAriaModalDialog() const;
static bool UseAXMenuList() { return use_ax_menu_list_; }
+ static bool ShouldCreateAXMenuListFor(LayoutObject* layout_object);
static bool ShouldCreateAXMenuListOptionFor(const Node*);
- static bool IsPseudoElementDescendant(const LayoutObject& layout_object);
+ static bool IsRelevantPseudoElement(const Node& node);
+ static bool IsRelevantPseudoElementDescendant(
+ const LayoutObject& layout_object);
#if DCHECK_IS_ON()
bool HasBeenDisposed() { return has_been_disposed_; }
@@ -424,7 +449,12 @@ class MODULES_EXPORT AXObjectCacheImpl
void MarkAXSubtreeDirtyWithCleanLayout(AXObject*);
void MarkElementDirtyWithCleanLayout(const Node*, bool subtree);
+ // Helper that clears children up to the first included ancestor and returns
+ // the ancestor if a children changed notification should be fired on it.
+ AXObject* InvalidateChildren(AXObject* obj);
+
Member<Document> document_;
+ ui::AXMode ax_mode_;
HeapHashMap<AXID, Member<AXObject>> objects_;
// LayoutObject and AbstractInlineTextBox are not on the Oilpan heap so we
// do not use HeapHashMap for those mappings.
@@ -500,7 +530,7 @@ class MODULES_EXPORT AXObjectCacheImpl
void ContainingTableRowsOrColsMaybeChanged(Node*);
// Must be called an entire subtree of accessible objects are no longer valid.
- void RemoveAXObjectsInLayoutSubtree(AXObject* subtree);
+ void RemoveAXObjectsInLayoutSubtree(AXObject* subtree, int depth);
// Object for HTML validation alerts. Created at most once per object cache.
AXObject* GetOrCreateValidationMessageObject();
diff --git a/chromium/third_party/blink/renderer/modules/accessibility/ax_object_cache_test.cc b/chromium/third_party/blink/renderer/modules/accessibility/ax_object_cache_test.cc
index 545bce61715..c27a732ac6c 100644
--- a/chromium/third_party/blink/renderer/modules/accessibility/ax_object_cache_test.cc
+++ b/chromium/third_party/blink/renderer/modules/accessibility/ax_object_cache_test.cc
@@ -63,8 +63,7 @@ class MockAXObject : public AXObject {
: AXObject(ax_object_cache) {}
static unsigned num_children_changed_calls_;
- void ChildrenChanged() final { num_children_changed_calls_++; }
- AXObject* ComputeParentImpl() const final { return nullptr; }
+ void ChildrenChangedWithCleanLayout() final { num_children_changed_calls_++; }
Document* GetDocument() const final { return &AXObjectCache().GetDocument(); }
void AddChildren() final {}
ax::mojom::blink::Role NativeRoleIgnoringAria() const override {
diff --git a/chromium/third_party/blink/renderer/modules/accessibility/ax_object_test.cc b/chromium/third_party/blink/renderer/modules/accessibility/ax_object_test.cc
index 5a649c79d54..191831a1f97 100644
--- a/chromium/third_party/blink/renderer/modules/accessibility/ax_object_test.cc
+++ b/chromium/third_party/blink/renderer/modules/accessibility/ax_object_test.cc
@@ -4,6 +4,8 @@
#include "third_party/blink/renderer/modules/accessibility/ax_object.h"
+#include <memory>
+
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/renderer/modules/accessibility/ax_object_cache_impl.h"
#include "third_party/blink/renderer/modules/accessibility/testing/accessibility_test.h"
@@ -40,6 +42,206 @@ TEST_F(AccessibilityTest, IsAncestorOf) {
EXPECT_FALSE(button->IsAncestorOf(*root));
}
+TEST_F(AccessibilityTest, IsEditableInTextField) {
+ SetBodyInnerHTML(R"HTML(
+ <input type="text" id="input" value="Test">
+ <textarea id="textarea">
+ Test
+ </textarea>)HTML");
+
+ const AXObject* root = GetAXRootObject();
+ ASSERT_NE(nullptr, root);
+ const AXObject* input = GetAXObjectByElementId("input");
+ ASSERT_NE(nullptr, input);
+ const AXObject* input_text =
+ input->FirstChildIncludingIgnored()->UnignoredChildAt(0);
+ ASSERT_NE(nullptr, input_text);
+ ASSERT_EQ(ax::mojom::blink::Role::kStaticText, input_text->RoleValue());
+ const AXObject* textarea = GetAXObjectByElementId("textarea");
+ ASSERT_NE(nullptr, textarea);
+ const AXObject* textarea_text =
+ textarea->FirstChildIncludingIgnored()->UnignoredChildAt(0);
+ ASSERT_NE(nullptr, textarea_text);
+ ASSERT_EQ(ax::mojom::blink::Role::kStaticText, textarea_text->RoleValue());
+
+ EXPECT_FALSE(root->IsEditable());
+ EXPECT_TRUE(input->IsEditable());
+ EXPECT_TRUE(input_text->IsEditable());
+ EXPECT_TRUE(textarea->IsEditable());
+ EXPECT_TRUE(textarea_text->IsEditable());
+
+ EXPECT_FALSE(root->IsEditableRoot());
+ EXPECT_FALSE(input->IsEditableRoot());
+ EXPECT_FALSE(input_text->IsEditableRoot());
+ EXPECT_FALSE(textarea->IsEditableRoot());
+ EXPECT_FALSE(textarea_text->IsEditableRoot());
+
+ EXPECT_FALSE(root->HasContentEditableAttributeSet());
+ EXPECT_FALSE(input->HasContentEditableAttributeSet());
+ EXPECT_FALSE(input_text->HasContentEditableAttributeSet());
+ EXPECT_FALSE(textarea->HasContentEditableAttributeSet());
+ EXPECT_FALSE(textarea_text->HasContentEditableAttributeSet());
+
+ EXPECT_FALSE(root->IsMultiline());
+ EXPECT_FALSE(input->IsMultiline());
+ EXPECT_FALSE(input_text->IsMultiline());
+ EXPECT_TRUE(textarea->IsMultiline());
+ EXPECT_FALSE(textarea_text->IsMultiline());
+
+ EXPECT_FALSE(root->IsRichlyEditable());
+ EXPECT_FALSE(input->IsRichlyEditable());
+ EXPECT_FALSE(input_text->IsRichlyEditable());
+ EXPECT_FALSE(textarea->IsRichlyEditable());
+ EXPECT_FALSE(textarea_text->IsRichlyEditable());
+}
+
+TEST_F(AccessibilityTest, IsEditableInContentEditable) {
+ // On purpose, also add the textbox role to ensure that it won't affect the
+ // contenteditable state.
+ SetBodyInnerHTML(R"HTML(
+ <div role="textbox" contenteditable="true" id="outerContenteditable">
+ Test
+ <div contenteditable="plaintext-only" id="innerContenteditable">
+ Test
+ </div>
+ </div>)HTML");
+
+ const AXObject* root = GetAXRootObject();
+ ASSERT_NE(nullptr, root);
+ const AXObject* outer_contenteditable =
+ GetAXObjectByElementId("outerContenteditable");
+ ASSERT_NE(nullptr, outer_contenteditable);
+ const AXObject* outer_contenteditable_text =
+ outer_contenteditable->UnignoredChildAt(0);
+ ASSERT_NE(nullptr, outer_contenteditable_text);
+ ASSERT_EQ(ax::mojom::blink::Role::kStaticText,
+ outer_contenteditable_text->RoleValue());
+ const AXObject* inner_contenteditable =
+ GetAXObjectByElementId("innerContenteditable");
+ ASSERT_NE(nullptr, inner_contenteditable);
+ const AXObject* inner_contenteditable_text =
+ inner_contenteditable->UnignoredChildAt(0);
+ ASSERT_NE(nullptr, inner_contenteditable_text);
+ ASSERT_EQ(ax::mojom::blink::Role::kStaticText,
+ inner_contenteditable_text->RoleValue());
+
+ EXPECT_FALSE(root->IsEditable());
+ EXPECT_TRUE(outer_contenteditable->IsEditable());
+ EXPECT_TRUE(outer_contenteditable_text->IsEditable());
+ EXPECT_TRUE(inner_contenteditable->IsEditable());
+ EXPECT_TRUE(inner_contenteditable_text->IsEditable());
+
+ EXPECT_FALSE(root->IsEditableRoot());
+ EXPECT_TRUE(outer_contenteditable->IsEditableRoot());
+ EXPECT_FALSE(outer_contenteditable_text->IsEditableRoot());
+ EXPECT_TRUE(inner_contenteditable->IsEditableRoot());
+ EXPECT_FALSE(inner_contenteditable_text->IsEditableRoot());
+
+ EXPECT_FALSE(root->HasContentEditableAttributeSet());
+ EXPECT_TRUE(outer_contenteditable->HasContentEditableAttributeSet());
+ EXPECT_FALSE(outer_contenteditable_text->HasContentEditableAttributeSet());
+ EXPECT_TRUE(inner_contenteditable->HasContentEditableAttributeSet());
+ EXPECT_FALSE(inner_contenteditable_text->HasContentEditableAttributeSet());
+
+ EXPECT_FALSE(root->IsMultiline());
+ EXPECT_TRUE(outer_contenteditable->IsMultiline());
+ EXPECT_FALSE(outer_contenteditable_text->IsMultiline());
+ EXPECT_TRUE(inner_contenteditable->IsMultiline());
+ EXPECT_FALSE(inner_contenteditable_text->IsMultiline());
+
+ EXPECT_FALSE(root->IsRichlyEditable());
+ EXPECT_TRUE(outer_contenteditable->IsRichlyEditable());
+ EXPECT_TRUE(outer_contenteditable_text->IsRichlyEditable());
+ // contenteditable="plaintext-only".
+ EXPECT_FALSE(inner_contenteditable->IsRichlyEditable());
+ EXPECT_FALSE(inner_contenteditable_text->IsRichlyEditable());
+}
+
+TEST_F(AccessibilityTest, IsEditableInCanvasFallback) {
+ SetBodyInnerHTML(R"HTML(
+ <canvas id="canvas" width="300" height="300">
+ <input id="input" value="Test">
+ <div contenteditable="true" id="outerContenteditable">
+ Test
+ <div contenteditable="plaintext-only" id="innerContenteditable">
+ Test
+ </div>
+ </div>
+ </canvas>)HTML");
+
+ const AXObject* root = GetAXRootObject();
+ ASSERT_NE(nullptr, root);
+ const AXObject* canvas = GetAXObjectByElementId("canvas");
+ ASSERT_NE(nullptr, canvas);
+ const AXObject* input = GetAXObjectByElementId("input");
+ ASSERT_NE(nullptr, input);
+ const AXObject* input_text =
+ input->FirstChildIncludingIgnored()->UnignoredChildAt(0);
+ ASSERT_NE(nullptr, input_text);
+ ASSERT_EQ(ax::mojom::blink::Role::kStaticText, input_text->RoleValue());
+ const AXObject* outer_contenteditable =
+ GetAXObjectByElementId("outerContenteditable");
+ ASSERT_NE(nullptr, outer_contenteditable);
+ const AXObject* outer_contenteditable_text =
+ outer_contenteditable->UnignoredChildAt(0);
+ ASSERT_NE(nullptr, outer_contenteditable_text);
+ ASSERT_EQ(ax::mojom::blink::Role::kStaticText,
+ outer_contenteditable_text->RoleValue());
+ const AXObject* inner_contenteditable =
+ GetAXObjectByElementId("innerContenteditable");
+ ASSERT_NE(nullptr, inner_contenteditable);
+ const AXObject* inner_contenteditable_text =
+ inner_contenteditable->UnignoredChildAt(0);
+ ASSERT_NE(nullptr, inner_contenteditable_text);
+ ASSERT_EQ(ax::mojom::blink::Role::kStaticText,
+ inner_contenteditable_text->RoleValue());
+
+ EXPECT_FALSE(root->IsEditable());
+ EXPECT_FALSE(canvas->IsEditable());
+ EXPECT_TRUE(input->IsEditable());
+ EXPECT_TRUE(input_text->IsEditable());
+ EXPECT_TRUE(outer_contenteditable->IsEditable());
+ EXPECT_TRUE(outer_contenteditable_text->IsEditable());
+ EXPECT_TRUE(inner_contenteditable->IsEditable());
+ EXPECT_TRUE(inner_contenteditable_text->IsEditable());
+
+ EXPECT_FALSE(root->IsEditableRoot());
+ EXPECT_FALSE(canvas->IsEditableRoot());
+ EXPECT_FALSE(input->IsEditableRoot());
+ EXPECT_FALSE(input_text->IsEditableRoot());
+ EXPECT_TRUE(outer_contenteditable->IsEditableRoot());
+ EXPECT_FALSE(outer_contenteditable_text->IsEditableRoot());
+ EXPECT_TRUE(inner_contenteditable->IsEditableRoot());
+ EXPECT_FALSE(inner_contenteditable_text->IsEditableRoot());
+
+ EXPECT_FALSE(root->HasContentEditableAttributeSet());
+ EXPECT_FALSE(canvas->HasContentEditableAttributeSet());
+ EXPECT_FALSE(input->HasContentEditableAttributeSet());
+ EXPECT_FALSE(input_text->HasContentEditableAttributeSet());
+ EXPECT_TRUE(outer_contenteditable->HasContentEditableAttributeSet());
+ EXPECT_FALSE(outer_contenteditable_text->HasContentEditableAttributeSet());
+ EXPECT_TRUE(inner_contenteditable->HasContentEditableAttributeSet());
+ EXPECT_FALSE(inner_contenteditable_text->HasContentEditableAttributeSet());
+
+ EXPECT_FALSE(root->IsMultiline());
+ EXPECT_FALSE(canvas->IsMultiline());
+ EXPECT_FALSE(input->IsMultiline());
+ EXPECT_FALSE(input_text->IsMultiline());
+ EXPECT_TRUE(outer_contenteditable->IsMultiline());
+ EXPECT_FALSE(outer_contenteditable_text->IsMultiline());
+ EXPECT_TRUE(inner_contenteditable->IsMultiline());
+ EXPECT_FALSE(inner_contenteditable_text->IsMultiline());
+
+ EXPECT_FALSE(root->IsRichlyEditable());
+ EXPECT_FALSE(canvas->IsRichlyEditable());
+ EXPECT_FALSE(input->IsRichlyEditable());
+ EXPECT_FALSE(input_text->IsRichlyEditable());
+ EXPECT_TRUE(outer_contenteditable->IsRichlyEditable());
+ EXPECT_TRUE(outer_contenteditable_text->IsRichlyEditable());
+ EXPECT_FALSE(inner_contenteditable->IsRichlyEditable());
+ EXPECT_FALSE(inner_contenteditable_text->IsRichlyEditable());
+}
+
TEST_F(AccessibilityTest, DetachedIsIgnored) {
SetBodyInnerHTML(R"HTML(<button id="button">button</button>)HTML");
@@ -752,7 +954,7 @@ TEST_F(AccessibilityTest, InitRelationCacheLabelFor) {
// Now recreate an AXContext, simulating what happens if accessibility
// is enabled after the document is loaded.
- ax_context_.reset(new AXContext(GetDocument()));
+ ax_context_ = std::make_unique<AXContext>(GetDocument());
const AXObject* root = GetAXRootObject();
ASSERT_NE(nullptr, root);
@@ -778,7 +980,7 @@ TEST_F(AccessibilityTest, InitRelationCacheAriaOwns) {
// Now recreate an AXContext, simulating what happens if accessibility
// is enabled after the document is loaded.
- ax_context_.reset(new AXContext(GetDocument()));
+ ax_context_ = std::make_unique<AXContext>(GetDocument());
const AXObject* root = GetAXRootObject();
ASSERT_NE(nullptr, root);
diff --git a/chromium/third_party/blink/renderer/modules/accessibility/ax_position.cc b/chromium/third_party/blink/renderer/modules/accessibility/ax_position.cc
index a21a4eef62f..6f89a2c4936 100644
--- a/chromium/third_party/blink/renderer/modules/accessibility/ax_position.cc
+++ b/chromium/third_party/blink/renderer/modules/accessibility/ax_position.cc
@@ -85,7 +85,7 @@ const AXPosition AXPosition::CreateFirstPositionInObject(
if (container.IsDetached())
return {};
- if (container.IsTextObject() || container.IsNativeTextField()) {
+ if (container.IsTextObject() || container.IsAtomicTextField()) {
AXPosition position(container);
position.text_offset_or_child_index_ = 0;
#if DCHECK_IS_ON()
@@ -120,7 +120,7 @@ const AXPosition AXPosition::CreateLastPositionInObject(
if (container.IsDetached())
return {};
- if (container.IsTextObject() || container.IsNativeTextField()) {
+ if (container.IsTextObject() || container.IsAtomicTextField()) {
AXPosition position(container);
position.text_offset_or_child_index_ = position.MaxTextOffset();
#if DCHECK_IS_ON()
@@ -420,7 +420,7 @@ int AXPosition::MaxTextOffset() const {
// TODO(nektar): Make AXObject::TextLength() public and use throughout this
// method.
- if (container_object_->IsNativeTextField())
+ if (container_object_->IsAtomicTextField())
return container_object_->GetValueForControl().length();
const Node* container_node = container_object_->GetNode();
@@ -552,7 +552,7 @@ bool AXPosition::IsTextPosition() const {
if (!container_object_)
return false;
return container_object_->IsTextObject() ||
- container_object_->IsNativeTextField();
+ container_object_->IsAtomicTextField();
}
const AXPosition AXPosition::CreateNextPosition() const {
@@ -614,7 +614,7 @@ const AXPosition AXPosition::CreatePreviousPosition() const {
const AXObject* last_child =
container_object_->LastChildIncludingIgnored();
// Dont skip over any intervening text.
- if (last_child->IsTextObject() || last_child->IsNativeTextField()) {
+ if (last_child->IsTextObject() || last_child->IsAtomicTextField()) {
return CreatePositionAfterObject(
*last_child, AXPositionAdjustmentBehavior::kMoveLeft);
}
@@ -636,7 +636,7 @@ const AXPosition AXPosition::CreatePreviousPosition() const {
// Dont skip over any intervening text.
if (object_before_position->IsTextObject() ||
- object_before_position->IsNativeTextField()) {
+ object_before_position->IsAtomicTextField()) {
return CreatePositionAfterObject(*object_before_position,
AXPositionAdjustmentBehavior::kMoveLeft);
}
diff --git a/chromium/third_party/blink/renderer/modules/accessibility/ax_position.h b/chromium/third_party/blink/renderer/modules/accessibility/ax_position.h
index fd0174e6ccf..62ea3c6c7f5 100644
--- a/chromium/third_party/blink/renderer/modules/accessibility/ax_position.h
+++ b/chromium/third_party/blink/renderer/modules/accessibility/ax_position.h
@@ -9,6 +9,7 @@
#include <ostream>
+#include "base/dcheck_is_on.h"
#include "base/logging.h"
#include "third_party/blink/renderer/core/editing/forward.h"
#include "third_party/blink/renderer/core/editing/text_affinity.h"
diff --git a/chromium/third_party/blink/renderer/modules/accessibility/ax_range.h b/chromium/third_party/blink/renderer/modules/accessibility/ax_range.h
index 43682e15d25..b60265ba651 100644
--- a/chromium/third_party/blink/renderer/modules/accessibility/ax_range.h
+++ b/chromium/third_party/blink/renderer/modules/accessibility/ax_range.h
@@ -9,6 +9,7 @@
#include <ostream>
+#include "base/dcheck_is_on.h"
#include "base/logging.h"
#include "third_party/blink/renderer/modules/accessibility/ax_position.h"
#include "third_party/blink/renderer/modules/modules_export.h"
diff --git a/chromium/third_party/blink/renderer/modules/accessibility/ax_relation_cache.cc b/chromium/third_party/blink/renderer/modules/accessibility/ax_relation_cache.cc
index 1fb5906708f..15c8d1950af 100644
--- a/chromium/third_party/blink/renderer/modules/accessibility/ax_relation_cache.cc
+++ b/chromium/third_party/blink/renderer/modules/accessibility/ax_relation_cache.cc
@@ -7,6 +7,7 @@
#include "base/memory/ptr_util.h"
#include "third_party/blink/renderer/core/dom/element_traversal.h"
#include "third_party/blink/renderer/core/html/forms/html_label_element.h"
+#include "ui/accessibility/ax_common.h"
namespace blink {
@@ -56,8 +57,11 @@ void AXRelationCache::ProcessUpdatesWithCleanLayout() {
}
bool AXRelationCache::IsAriaOwned(const AXObject* child) const {
- return child &&
- aria_owned_child_to_owner_mapping_.Contains(child->AXObjectID());
+ if (!child)
+ return false;
+ DCHECK(!child->IsDetached())
+ << "Child was detached: " << child->ToString(true, true);
+ return aria_owned_child_to_owner_mapping_.Contains(child->AXObjectID());
}
AXObject* AXRelationCache::GetAriaOwnedParent(const AXObject* child) const {
@@ -71,6 +75,7 @@ AXObject* AXRelationCache::GetAriaOwnedParent(const AXObject* child) const {
}
// Update reverse relation map, where relation_source is related to target_ids.
+// TODO Support when HasExplicitlySetAttrAssociatedElement() == true.
void AXRelationCache::UpdateReverseRelations(const AXObject* relation_source,
const Vector<String>& target_ids) {
AXID relation_source_axid = relation_source->AXObjectID();
@@ -121,10 +126,10 @@ bool AXRelationCache::IsValidOwner(AXObject* owner) {
if (!owner->CanHaveChildren())
return false;
- // An aria-owns is disallowed on editable roots, such as <input>, <textarea>
- // and content editables, otherwise the result would be unworkable and totally
- // unexpected on the browser side.
- if (owner->IsEditableRoot())
+ // An aria-owns is disallowed on editable roots and atomic text fields, such
+ // as <input>, <textarea> and content editables, otherwise the result would be
+ // unworkable and totally unexpected on the browser side.
+ if (owner->IsTextField())
return false;
// Images can only use <img usemap> to "own" <area> children.
@@ -133,8 +138,9 @@ bool AXRelationCache::IsValidOwner(AXObject* owner) {
if (owner->RoleValue() == ax::mojom::blink::Role::kImage)
return false;
- // Similarly, do not allow <area> to own another object.
- if (owner->IsImageMapLink())
+ // Many types of nodes cannot be used as parent in normal situations.
+ // These rules also apply to allowing aria-owns.
+ if (!AXObject::CanComputeAsNaturalParent(owner->GetNode()))
return false;
return true;
@@ -177,7 +183,11 @@ void AXRelationCache::UnmapOwnedChildren(const AXObject* owner,
removed_child->DetachFromParent();
// Recompute the real parent and cache it.
AXObject* real_parent = removed_child->ParentObject();
- ChildrenChanged(real_parent);
+ SANITIZER_CHECK(real_parent) << "No parent to restore for object with "
+ "unmapped aria-owns, child is: "
+ << removed_child->ToString(true, true);
+ if (real_parent)
+ ChildrenChanged(real_parent);
}
}
}
@@ -192,17 +202,21 @@ void AXRelationCache::MapOwnedChildren(const AXObject* owner,
// Now detach the object from its original parent and call childrenChanged
// on the original parent so that it can recompute its list of children.
- AXObject* original_parent = added_child->ParentObject();
- added_child->DetachFromParent();
- added_child->SetParent(const_cast<AXObject*>(owner));
- ChildrenChanged(original_parent);
+ AXObject* original_parent = added_child->CachedParentObject();
+ if (original_parent != owner) {
+ added_child->DetachFromParent();
+ added_child->SetParent(const_cast<AXObject*>(owner));
+ if (original_parent)
+ ChildrenChanged(original_parent);
+ }
}
}
void AXRelationCache::UpdateAriaOwnsFromAttrAssociatedElementsWithCleanLayout(
AXObject* owner,
const HeapVector<Member<Element>>& attr_associated_elements,
- HeapVector<Member<AXObject>>& validated_owned_children_result) {
+ HeapVector<Member<AXObject>>& validated_owned_children_result,
+ bool force) {
// attr-associated elements have already had their scope validated, but they
// need to be further validated to determine if they introduce a cycle or are
// already owned by another element.
@@ -221,7 +235,7 @@ void AXRelationCache::UpdateAriaOwnsFromAttrAssociatedElementsWithCleanLayout(
validated_owned_children_result.push_back(child);
} else if (child) {
// Invalid owns relation: repair the parent that was set above.
- child->SetParent(child->ComputeParentImpl());
+ child->SetParent(child->ComputeParent());
}
}
@@ -230,7 +244,7 @@ void AXRelationCache::UpdateAriaOwnsFromAttrAssociatedElementsWithCleanLayout(
// Update the internal mappings of owned children.
UpdateAriaOwnerToChildrenMappingWithCleanLayout(
- owner, validated_owned_children_result);
+ owner, validated_owned_children_result, force);
}
void AXRelationCache::GetAriaOwnedChildren(
@@ -249,30 +263,38 @@ void AXRelationCache::GetAriaOwnedChildren(
}
}
-void AXRelationCache::UpdateAriaOwnsWithCleanLayout(AXObject* owner) {
+void AXRelationCache::UpdateAriaOwnsWithCleanLayout(AXObject* owner,
+ bool force) {
+ DCHECK(owner);
Element* element = owner->GetElement();
if (!element)
return;
DCHECK(!element->GetDocument().NeedsLayoutTreeUpdateForNode(*element));
- Vector<String> owned_id_vector;
- owner->TokenVectorFromAttribute(owned_id_vector, html_names::kAriaOwnsAttr);
+ // A refresh can occur even if not a valid owner, because the old object
+ // that |owner| is replacing may have previously been a valid owner. In this
+ // case, the old owned child mappings will need to be removed.
+ bool is_valid_owner = IsValidOwner(owner);
+ if (!force && !is_valid_owner)
+ return;
- // Track reverse relations for future tree updates.
- UpdateReverseRelations(owner, owned_id_vector);
+ HeapVector<Member<AXObject>> owned_children;
// We first check if the element has an explicitly set aria-owns association.
- // Explicitly set elements are validated on setting time (that they are in a
- // valid scope etc). The content attribute can contain ids that are not
+ // Explicitly set elements are validated when they are read (that they are in
+ // a valid scope etc). The content attribute can contain ids that are not
// legally ownable.
- HeapVector<Member<AXObject>> owned_children;
- if (element && element->HasExplicitlySetAttrAssociatedElements(
- html_names::kAriaOwnsAttr)) {
+ if (!is_valid_owner) {
+ DCHECK(force) << "Should not reach here except when an AXObject was "
+ "invalidated and is being refreshed: "
+ << owner->ToString(true, true);
+ } else if (element && element->HasExplicitlySetAttrAssociatedElements(
+ html_names::kAriaOwnsAttr)) {
UpdateAriaOwnsFromAttrAssociatedElementsWithCleanLayout(
owner,
element->GetElementArrayAttribute(html_names::kAriaOwnsAttr).value(),
- owned_children);
+ owned_children, force);
} else {
// Figure out the ids that actually correspond to children that exist
// and that we can legally own (not cyclical, not already owned, etc.) and
@@ -281,7 +303,11 @@ void AXRelationCache::UpdateAriaOwnsWithCleanLayout(AXObject* owner) {
// Figure out the children that are owned by this object and are in the
// tree.
TreeScope& scope = element->GetTreeScope();
- Vector<AXID> validated_owned_child_axids;
+ Vector<String> owned_id_vector;
+ owner->TokenVectorFromAttribute(element, owned_id_vector,
+ html_names::kAriaOwnsAttr);
+ // Track reverse relations for future tree updates.
+ UpdateReverseRelations(owner, owned_id_vector);
for (const String& id_name : owned_id_vector) {
Element* child_element = scope.getElementById(AtomicString(id_name));
// Pass in owner parent assuming that the owns relationship will be valid.
@@ -292,19 +318,24 @@ void AXRelationCache::UpdateAriaOwnsWithCleanLayout(AXObject* owner) {
owned_children.push_back(child);
} else if (child) {
// Invalid owns relation: repair the parent that was set above.
- child->SetParent(child->ComputeParentImpl());
+ child->SetParent(child->ComputeParent());
}
}
}
// Update the internal validated mapping of owned children. This will
// fire an event if the mapping has changed.
- UpdateAriaOwnerToChildrenMappingWithCleanLayout(owner, owned_children);
+ UpdateAriaOwnerToChildrenMappingWithCleanLayout(owner, owned_children, force);
}
void AXRelationCache::UpdateAriaOwnerToChildrenMappingWithCleanLayout(
AXObject* owner,
- HeapVector<Member<AXObject>>& validated_owned_children_result) {
+ HeapVector<Member<AXObject>>& validated_owned_children_result,
+ bool force) {
+ DCHECK(owner);
+ if (!owner->CanHaveChildren())
+ return;
+
Vector<AXID> validated_owned_child_axids;
for (auto& child : validated_owned_children_result)
validated_owned_child_axids.push_back(child->AXObjectID());
@@ -313,8 +344,13 @@ void AXRelationCache::UpdateAriaOwnerToChildrenMappingWithCleanLayout(
// there are no changes.
Vector<AXID> current_child_axids =
aria_owner_to_children_mapping_.at(owner->AXObjectID());
- if (current_child_axids == validated_owned_child_axids)
+
+ // Only force the refresh if there was or will be owned children; otherwise,
+ // there is nothing to refresh even for a new AXObject replacing an old owner.
+ if (current_child_axids == validated_owned_child_axids &&
+ (!force || current_child_axids.IsEmpty())) {
return;
+ }
// The list of owned children has changed. Even if they were just reordered,
// to be safe and handle all cases we remove all of the current owned
@@ -325,6 +361,7 @@ void AXRelationCache::UpdateAriaOwnerToChildrenMappingWithCleanLayout(
#if DCHECK_IS_ON()
// Owned children must be in tree to avoid serialization issues.
for (AXObject* child : validated_owned_children_result) {
+ DCHECK(IsAriaOwned(child));
DCHECK(child->AccessibilityIsIncludedInTree())
<< "Owned child not in tree: " << child->ToString(true, false)
<< "\nRecompute included in tree: "
@@ -333,11 +370,14 @@ void AXRelationCache::UpdateAriaOwnerToChildrenMappingWithCleanLayout(
#endif
// Finally, update the mapping from the owner to the list of child IDs.
- aria_owner_to_children_mapping_.Set(owner->AXObjectID(),
- validated_owned_child_axids);
+ if (validated_owned_child_axids.IsEmpty()) {
+ aria_owner_to_children_mapping_.erase(owner->AXObjectID());
+ } else {
+ aria_owner_to_children_mapping_.Set(owner->AXObjectID(),
+ validated_owned_child_axids);
+ }
ChildrenChanged(owner);
- owner->UpdateChildrenIfNecessary();
}
bool AXRelationCache::MayHaveHTMLLabelViaForAttribute(
@@ -475,7 +515,7 @@ AXObject* AXRelationCache::GetOrCreate(Node* node, const AXObject* owner) {
}
void AXRelationCache::ChildrenChanged(AXObject* object) {
- object->ChildrenChanged();
+ object->ChildrenChangedWithCleanLayout();
}
void AXRelationCache::LabelChanged(Node* node) {
diff --git a/chromium/third_party/blink/renderer/modules/accessibility/ax_relation_cache.h b/chromium/third_party/blink/renderer/modules/accessibility/ax_relation_cache.h
index 0d9bb159121..4240ead9f65 100644
--- a/chromium/third_party/blink/renderer/modules/accessibility/ax_relation_cache.h
+++ b/chromium/third_party/blink/renderer/modules/accessibility/ax_relation_cache.h
@@ -80,7 +80,9 @@ class AXRelationCache {
// calls ChildrenChanged on all affected nodes (old and new parents).
// This affects the tree, which is why it should only be called at a
// specific time in the lifecycle.
- void UpdateAriaOwnsWithCleanLayout(AXObject* owner);
+ // Pass |force=true| when the mappings must be updated even though the
+ // owned ids have not changed, e.g. when an object has been refreshed.
+ void UpdateAriaOwnsWithCleanLayout(AXObject* owner, bool force = false);
static bool IsValidOwner(AXObject* owner);
static bool IsValidOwnedChild(AXObject* child);
@@ -94,7 +96,8 @@ class AXRelationCache {
void UpdateAriaOwnsFromAttrAssociatedElementsWithCleanLayout(
AXObject* owner,
const HeapVector<Member<Element>>& attr_associated_elements,
- HeapVector<Member<AXObject>>& owned_children);
+ HeapVector<Member<AXObject>>& owned_children,
+ bool force);
// If any object is related to this object via <label for>, aria-owns,
// aria-describedby or aria-labeledby, update the text for the related object.
@@ -109,7 +112,8 @@ class AXRelationCache {
// either the content attribute or the attr associated elements.
void UpdateAriaOwnerToChildrenMappingWithCleanLayout(
AXObject* owner,
- HeapVector<Member<AXObject>>& validated_owned_children_result);
+ HeapVector<Member<AXObject>>& validated_owned_children_result,
+ bool force);
// Whether the document has been scanned for initial relationships
// first or not.
@@ -164,4 +168,4 @@ class AXRelationCache {
} // namespace blink
-#endif
+#endif // THIRD_PARTY_BLINK_RENDERER_MODULES_ACCESSIBILITY_AX_RELATION_CACHE_H_
diff --git a/chromium/third_party/blink/renderer/modules/accessibility/ax_selection.cc b/chromium/third_party/blink/renderer/modules/accessibility/ax_selection.cc
index 614423b9bcb..2b0c51e6fd7 100644
--- a/chromium/third_party/blink/renderer/modules/accessibility/ax_selection.cc
+++ b/chromium/third_party/blink/renderer/modules/accessibility/ax_selection.cc
@@ -264,18 +264,18 @@ bool AXSelection::IsValid() const {
// boundaries, replaced elements, CSS user-select, etc.
//
- if (base_.IsTextPosition() && base_.ContainerObject()->IsNativeTextField() &&
+ if (base_.IsTextPosition() && base_.ContainerObject()->IsAtomicTextField() &&
!(base_.ContainerObject() == extent_.ContainerObject() &&
extent_.IsTextPosition() &&
- extent_.ContainerObject()->IsNativeTextField())) {
+ extent_.ContainerObject()->IsAtomicTextField())) {
return false;
}
if (extent_.IsTextPosition() &&
- extent_.ContainerObject()->IsNativeTextField() &&
+ extent_.ContainerObject()->IsAtomicTextField() &&
!(base_.ContainerObject() == extent_.ContainerObject() &&
base_.IsTextPosition() &&
- base_.ContainerObject()->IsNativeTextField())) {
+ base_.ContainerObject()->IsAtomicTextField())) {
return false;
}
@@ -353,12 +353,12 @@ bool AXSelection::Select(const AXSelectionBehavior selection_behavior) {
return false;
}
- base::Optional<AXSelection::TextControlSelection> text_control_selection =
+ absl::optional<AXSelection::TextControlSelection> text_control_selection =
AsTextControlSelection();
if (text_control_selection.has_value()) {
DCHECK_LE(text_control_selection->start, text_control_selection->end);
TextControlElement& text_control = ToTextControl(
- *base_.ContainerObject()->GetNativeTextControlAncestor()->GetNode());
+ *base_.ContainerObject()->GetAtomicTextFieldAncestor()->GetNode());
if (!text_control.SetSelectionRange(text_control_selection->start,
text_control_selection->end,
text_control_selection->direction)) {
@@ -427,7 +427,7 @@ String AXSelection::ToString() const {
return "AXSelection from " + Base().ToString() + " to " + Extent().ToString();
}
-base::Optional<AXSelection::TextControlSelection>
+absl::optional<AXSelection::TextControlSelection>
AXSelection::AsTextControlSelection() const {
if (!IsValid() || !base_.IsTextPosition() || !extent_.IsTextPosition() ||
base_.ContainerObject() != extent_.ContainerObject()) {
@@ -435,7 +435,7 @@ AXSelection::AsTextControlSelection() const {
}
const AXObject* text_control =
- base_.ContainerObject()->GetNativeTextControlAncestor();
+ base_.ContainerObject()->GetAtomicTextFieldAncestor();
if (!text_control)
return {};
diff --git a/chromium/third_party/blink/renderer/modules/accessibility/ax_selection.h b/chromium/third_party/blink/renderer/modules/accessibility/ax_selection.h
index 64abe6dc9f2..e6c3f828842 100644
--- a/chromium/third_party/blink/renderer/modules/accessibility/ax_selection.h
+++ b/chromium/third_party/blink/renderer/modules/accessibility/ax_selection.h
@@ -9,8 +9,9 @@
#include <ostream>
+#include "base/dcheck_is_on.h"
#include "base/logging.h"
-#include "base/optional.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/editing/forward.h"
#include "third_party/blink/renderer/core/html/forms/text_control_element.h"
@@ -101,7 +102,7 @@ class MODULES_EXPORT AXSelection final {
// Determines whether this selection is targeted to the contents of a text
// field, and returns the start and end text offsets, as well as its
// direction. |start| should always be less than equal to |end|.
- base::Optional<TextControlSelection> AsTextControlSelection() const;
+ absl::optional<TextControlSelection> AsTextControlSelection() const;
// The |AXPosition| where the selection starts.
AXPosition base_;
diff --git a/chromium/third_party/blink/renderer/modules/accessibility/ax_sparse_attribute_setter.cc b/chromium/third_party/blink/renderer/modules/accessibility/ax_sparse_attribute_setter.cc
index 4fa047eea78..af587c98697 100644
--- a/chromium/third_party/blink/renderer/modules/accessibility/ax_sparse_attribute_setter.cc
+++ b/chromium/third_party/blink/renderer/modules/accessibility/ax_sparse_attribute_setter.cc
@@ -3,8 +3,10 @@
// found in the LICENSE file.
#include "third_party/blink/renderer/modules/accessibility/ax_sparse_attribute_setter.h"
+#include "third_party/blink/public/mojom/web_feature/web_feature.mojom-blink.h"
#include "third_party/blink/renderer/core/dom/qualified_name.h"
#include "third_party/blink/renderer/modules/accessibility/ax_object_cache_impl.h"
+#include "third_party/blink/renderer/platform/instrumentation/use_counter.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
#include "third_party/blink/renderer/platform/wtf/functional.h"
@@ -21,6 +23,18 @@ void SetBoolAttribute(ax::mojom::blink::BoolAttribute attribute,
AXObject* object,
ui::AXNodeData* node_data,
const AtomicString& value) {
+ // Don't set kTouchPassthrough unless the feature is enabled in this
+ // context.
+ if (attribute == ax::mojom::blink::BoolAttribute::kTouchPassthrough) {
+ auto* context = object->AXObjectCache().GetDocument().GetExecutionContext();
+ if (RuntimeEnabledFeatures::AccessibilityAriaTouchPassthroughEnabled(
+ context)) {
+ UseCounter::Count(context, WebFeature::kAccessibilityTouchPassthroughSet);
+ } else {
+ return;
+ }
+ }
+
// ARIA booleans are true if not "false" and not specifically undefined.
bool is_true = !AccessibleNode::IsUndefinedAttrValue(value) &&
!EqualIgnoringASCIICase(value, "false");
@@ -68,9 +82,9 @@ void SetIntListAttribute(ax::mojom::blink::IntListAttribute attribute,
Element* element = object->GetElement();
if (!element)
return;
- base::Optional<HeapVector<Member<Element>>> attr_associated_elements =
+ absl::optional<HeapVector<Member<Element>>> attr_associated_elements =
element->GetElementArrayAttribute(qualified_name);
- if (!attr_associated_elements)
+ if (!attr_associated_elements || attr_associated_elements.value().IsEmpty())
return;
std::vector<int32_t> ax_ids;
diff --git a/chromium/third_party/blink/renderer/modules/accessibility/ax_virtual_object.cc b/chromium/third_party/blink/renderer/modules/accessibility/ax_virtual_object.cc
index 89dc1f93de8..1de1df6b6b6 100644
--- a/chromium/third_party/blink/renderer/modules/accessibility/ax_virtual_object.cc
+++ b/chromium/third_party/blink/renderer/modules/accessibility/ax_virtual_object.cc
@@ -58,7 +58,7 @@ void AXVirtualObject::AddChildren() {
}
}
-void AXVirtualObject::ChildrenChanged() {
+void AXVirtualObject::ChildrenChangedWithCleanLayout() {
ClearChildren();
AXObjectCache().PostNotification(this, ax::mojom::Event::kChildrenChanged);
}
@@ -76,7 +76,7 @@ bool AXVirtualObject::HasAOMPropertyOrARIAAttribute(AOMBooleanProperty property,
if (!accessible_node_)
return false;
- base::Optional<bool> property_value = accessible_node_->GetProperty(property);
+ absl::optional<bool> property_value = accessible_node_->GetProperty(property);
result = property_value.value_or(false);
return property_value.has_value();
}
diff --git a/chromium/third_party/blink/renderer/modules/accessibility/ax_virtual_object.h b/chromium/third_party/blink/renderer/modules/accessibility/ax_virtual_object.h
index a5c01f64902..3a914ef8e0f 100644
--- a/chromium/third_party/blink/renderer/modules/accessibility/ax_virtual_object.h
+++ b/chromium/third_party/blink/renderer/modules/accessibility/ax_virtual_object.h
@@ -23,7 +23,7 @@ class MODULES_EXPORT AXVirtualObject : public AXObject {
void Detach() override;
bool IsVirtualObject() const override { return true; }
void AddChildren() override;
- void ChildrenChanged() override;
+ void ChildrenChangedWithCleanLayout() override;
const AtomicString& GetAOMPropertyOrARIAAttribute(
AOMStringProperty) const override;
bool HasAOMPropertyOrARIAAttribute(AOMBooleanProperty,
diff --git a/chromium/third_party/blink/renderer/modules/accessibility/inspector_accessibility_agent.cc b/chromium/third_party/blink/renderer/modules/accessibility/inspector_accessibility_agent.cc
index 00cfd35fe52..4e1c4987559 100644
--- a/chromium/third_party/blink/renderer/modules/accessibility/inspector_accessibility_agent.cc
+++ b/chromium/third_party/blink/renderer/modules/accessibility/inspector_accessibility_agent.cc
@@ -502,15 +502,11 @@ void FillSparseAttributes(AXObject& ax_object,
}
std::unique_ptr<AXValue> CreateRoleNameValue(ax::mojom::Role role) {
- AtomicString role_name = AXObject::RoleName(role);
- std::unique_ptr<AXValue> role_name_value;
- if (!role_name.IsNull()) {
- role_name_value = CreateValue(role_name, AXValueTypeEnum::Role);
- } else {
- role_name_value = CreateValue(AXObject::InternalRoleName(role),
- AXValueTypeEnum::InternalRole);
- }
- return role_name_value;
+ bool is_internal = false;
+ const String& role_name = AXObject::RoleName(role, &is_internal);
+ const auto& value_type =
+ is_internal ? AXValueTypeEnum::InternalRole : AXValueTypeEnum::Role;
+ return CreateValue(role_name, value_type);
}
} // namespace
@@ -586,18 +582,10 @@ void InspectorAccessibilityAgent::AddAncestors(
std::unique_ptr<protocol::Array<AXNode>>& nodes,
AXObjectCacheImpl& cache) const {
AXObject* ancestor = &first_ancestor;
- AXObject* child = inspected_ax_object;
while (ancestor) {
std::unique_ptr<AXNode> parent_node_object = BuildProtocolAXObject(
*ancestor, inspected_ax_object, true, nodes, cache);
- auto child_ids = std::make_unique<protocol::Array<AXNodeId>>();
- if (child)
- child_ids->emplace_back(String::Number(child->AXObjectID()));
- else
- child_ids->emplace_back(String::Number(kIDForInspectedNodeWithNoAXNode));
- parent_node_object->setChildIds(std::move(child_ids));
nodes->emplace_back(std::move(parent_node_object));
- child = ancestor;
ancestor = ancestor->ParentObjectUnignored();
}
}
@@ -617,7 +605,7 @@ std::unique_ptr<AXNode> InspectorAccessibilityAgent::BuildObjectForIgnoredNode(
.setNodeId(String::Number(ax_id))
.setIgnored(true)
.build();
- ax::mojom::Role role = ax::mojom::Role::kIgnored;
+ ax::mojom::blink::Role role = ax::mojom::blink::Role::kNone;
ignored_node_object->setRole(CreateRoleNameValue(role));
if (ax_object && ax_object->IsAXLayoutObject()) {
@@ -674,8 +662,27 @@ void InspectorAccessibilityAgent::PopulateDOMNodeAncestors(
if (!parent_ax_object)
return;
- // Populate parent and ancestors.
- AddAncestors(*parent_ax_object, nullptr, nodes, cache);
+ std::unique_ptr<AXNode> parent_node_object =
+ BuildProtocolAXObject(*parent_ax_object, nullptr, true, nodes, cache);
+ auto child_ids = std::make_unique<protocol::Array<AXNodeId>>();
+ auto* existing_child_ids = parent_node_object->getChildIds(nullptr);
+
+ // put the Ignored node first regardless of DOM structure
+ child_ids->insert(child_ids->begin(),
+ String::Number(kIDForInspectedNodeWithNoAXNode));
+ if (existing_child_ids) {
+ for (auto id : *existing_child_ids)
+ child_ids->push_back(id);
+ }
+
+ parent_node_object->setChildIds(std::move(child_ids));
+ nodes->emplace_back(std::move(parent_node_object));
+
+ parent_ax_object = parent_ax_object->ParentObjectUnignored();
+ if (parent_ax_object) {
+ // Populate ancestors.
+ AddAncestors(*parent_ax_object, nullptr, nodes, cache);
+ }
}
std::unique_ptr<AXNode> InspectorAccessibilityAgent::BuildProtocolAXObject(
@@ -901,14 +908,18 @@ void InspectorAccessibilityAgent::AddChildren(
for (unsigned i = 0; i < children.size(); i++) {
AXObject& child_ax_object = *children[i].Get();
child_ids->emplace_back(String::Number(child_ax_object.AXObjectID()));
+
if (&child_ax_object == inspected_ax_object)
continue;
+
if (&ax_object != inspected_ax_object) {
if (!inspected_ax_object)
continue;
- if (&ax_object != inspected_ax_object->ParentObjectUnignored() &&
- ax_object.GetNode())
+
+ if (ax_object.ParentObject() != inspected_ax_object ||
+ ax_object.GetNode()) {
continue;
+ }
}
// Only add children of inspected node (or un-inspectable children of
@@ -958,7 +969,7 @@ Response InspectorAccessibilityAgent::queryAXTree(
auto sought_role = ax::mojom::blink::Role::kUnknown;
if (role.isJust())
- sought_role = AXObject::AriaRoleToWebCoreRole(role.fromJust());
+ sought_role = AXObject::AriaRoleStringToRoleEnum(role.fromJust());
const String sought_name = accessible_name.fromMaybe("");
HeapVector<Member<AXObject>> reachable;
diff --git a/chromium/third_party/blink/renderer/modules/accessibility/inspector_type_builder_helper.cc b/chromium/third_party/blink/renderer/modules/accessibility/inspector_type_builder_helper.cc
index 2808716d118..167ff1330e8 100644
--- a/chromium/third_party/blink/renderer/modules/accessibility/inspector_type_builder_helper.cc
+++ b/chromium/third_party/blink/renderer/modules/accessibility/inspector_type_builder_helper.cc
@@ -24,8 +24,6 @@ String IgnoredReasonName(AXIgnoredReason reason) {
return "activeModalDialog";
case kAXAriaModalDialog:
return "activeAriaModalDialog";
- case kAXAncestorIsLeafNode:
- return "ancestorIsLeafNode";
case kAXAriaHiddenElement:
return "ariaHiddenElement";
case kAXAriaHiddenSubtree:
@@ -38,8 +36,6 @@ String IgnoredReasonName(AXIgnoredReason reason) {
return "inertElement";
case kAXInertSubtree:
return "inertSubtree";
- case kAXInheritsPresentation:
- return "inheritsPresentation";
case kAXLabelContainer:
return "labelContainer";
case kAXLabelFor:
diff --git a/chromium/third_party/blink/renderer/modules/accessibility/inspector_type_builder_helper.h b/chromium/third_party/blink/renderer/modules/accessibility/inspector_type_builder_helper.h
index d0b5b50d910..a73e1d02d35 100644
--- a/chromium/third_party/blink/renderer/modules/accessibility/inspector_type_builder_helper.h
+++ b/chromium/third_party/blink/renderer/modules/accessibility/inspector_type_builder_helper.h
@@ -47,4 +47,4 @@ std::unique_ptr<AXValueSource> CreateValueSource(NameSource&);
} // namespace blink
-#endif // InspectorAccessibilityAgent_h
+#endif // THIRD_PARTY_BLINK_RENDERER_MODULES_ACCESSIBILITY_INSPECTOR_TYPE_BUILDER_HELPER_H_
diff --git a/chromium/third_party/blink/renderer/modules/accessibility/testing/accessibility_test.cc b/chromium/third_party/blink/renderer/modules/accessibility/testing/accessibility_test.cc
index 3e99a2d2a5b..b8ca5fe7c65 100644
--- a/chromium/third_party/blink/renderer/modules/accessibility/testing/accessibility_test.cc
+++ b/chromium/third_party/blink/renderer/modules/accessibility/testing/accessibility_test.cc
@@ -24,7 +24,7 @@ void AccessibilityTest::SetUp() {
AXObjectCacheImpl& AccessibilityTest::GetAXObjectCache() const {
DCHECK(GetDocument().View());
- GetDocument().View()->UpdateLifecycleToCompositingCleanPlusScrolling(
+ GetDocument().View()->UpdateAllLifecyclePhasesExceptPaint(
DocumentUpdateReason::kAccessibility);
auto* ax_object_cache =
To<AXObjectCacheImpl>(GetDocument().ExistingAXObjectCache());