diff options
author | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2021-09-03 13:32:17 +0200 |
---|---|---|
committer | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2021-10-01 14:31:55 +0200 |
commit | 21ba0c5d4bf8fba15dddd97cd693bad2358b77fd (patch) | |
tree | 91be119f694044dfc1ff9fdc054459e925de9df0 /chromium/third_party/blink/renderer/modules/accessibility | |
parent | 03c549e0392f92c02536d3f86d5e1d8dfa3435ac (diff) | |
download | qtwebengine-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')
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()); |