summaryrefslogtreecommitdiff
path: root/chromium/third_party/blink/renderer/modules/accessibility/ax_object_cache_impl.cc
diff options
context:
space:
mode:
authorAllan Sandfeld Jensen <allan.jensen@qt.io>2021-09-03 13:32:17 +0200
committerAllan Sandfeld Jensen <allan.jensen@qt.io>2021-10-01 14:31:55 +0200
commit21ba0c5d4bf8fba15dddd97cd693bad2358b77fd (patch)
tree91be119f694044dfc1ff9fdc054459e925de9df0 /chromium/third_party/blink/renderer/modules/accessibility/ax_object_cache_impl.cc
parent03c549e0392f92c02536d3f86d5e1d8dfa3435ac (diff)
downloadqtwebengine-chromium-21ba0c5d4bf8fba15dddd97cd693bad2358b77fd.tar.gz
BASELINE: Update Chromium to 92.0.4515.166
Change-Id: I42a050486714e9e54fc271f2a8939223a02ae364
Diffstat (limited to 'chromium/third_party/blink/renderer/modules/accessibility/ax_object_cache_impl.cc')
-rw-r--r--chromium/third_party/blink/renderer/modules/accessibility/ax_object_cache_impl.cc799
1 files changed, 587 insertions, 212 deletions
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) {