blob: 26b106831eaa0fcebb0e852c68d842e3a93970c9 [file] [log] [blame]
// Copyright 2017 The Chromium Authors
// 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_relation_cache.h"
#include "base/memory/ptr_util.h"
#include "base/notreached.h"
#include "third_party/blink/renderer/core/dom/dom_node_ids.h"
#include "third_party/blink/renderer/core/dom/shadow_including_tree_order_traversal.h"
#include "third_party/blink/renderer/core/html/custom/element_internals.h"
#include "third_party/blink/renderer/core/html/forms/html_label_element.h"
#include "third_party/blink/renderer/core/html/forms/html_opt_group_element.h"
#include "third_party/blink/renderer/core/html/forms/html_option_element.h"
#include "third_party/blink/renderer/core/html/html_area_element.h"
#include "third_party/blink/renderer/core/html/html_body_element.h"
#include "third_party/blink/renderer/core/html/html_br_element.h"
#include "third_party/blink/renderer/core/layout/layout_box.h"
#include "third_party/blink/renderer/modules/accessibility/ax_node_object.h"
#include "third_party/blink/renderer/modules/accessibility/ax_object-inl.h"
#include "third_party/blink/renderer/platform/wtf/std_lib_extras.h"
#include "ui/accessibility/ax_common.h"
namespace blink {
namespace {
void IdsFromAttribute(const Element& element,
Vector<AtomicString>& ids,
const QualifiedName& attr_name) {
SpaceSplitString split_ids(AXObject::AriaAttribute(element, attr_name));
ids.AppendRange(split_ids.begin(), split_ids.end());
}
} // namespace
AXRelationCache::AXRelationCache(AXObjectCacheImpl* object_cache)
: object_cache_(object_cache) {}
AXRelationCache::~AXRelationCache() = default;
void AXRelationCache::Init() {
// Init the relation cache with elements already present.
// Normally, these relations would be cached when the node is first attached,
// via AXObjectCacheImpl::NodeIsConnected().
// The initial scan must include both flat traversal and node traversal,
// othrwise some connected elements can be missed.
DoInitialDocumentScan(object_cache_->GetDocument());
if (Document* popup_doc = object_cache_->GetPopupDocumentIfShowing()) {
DoInitialDocumentScan(*popup_doc);
}
#if AX_FAIL_FAST_BUILD()
is_initialized_ = true;
#endif
}
void AXRelationCache::DoInitialDocumentScan(Document& document) {
#if DCHECK_IS_ON()
DCHECK(document.Lifecycle().GetState() >= DocumentLifecycle::kLayoutClean)
<< "Unclean document at lifecycle " << document.Lifecycle().ToString();
#endif
// TODO(crbug.com/1473733) Address flaw that all DOM ids are being cached
// together regardless of their TreeScope, which can lead to conflicts.
// Traverse all connected nodes in the document, via both DOM and shadow DOM.
for (Node& node :
ShadowIncludingTreeOrderTraversal::DescendantsOf(document)) {
if (Element* element = DynamicTo<Element>(node)) {
// Cache relations that do not require an AXObject.
CacheRelations(*element);
// Caching aria-owns requires creating target AXObjects.
// TODO(crbug.com/41469336): Support aria-owns relations set via
// explicitly set attr-elements on element or element internals.
if (AXObject::HasAriaAttribute(*element, html_names::kAriaOwnsAttr)) {
owner_axids_to_update_.insert(element->GetDomNodeId());
}
}
}
}
void AXRelationCache::CacheRelations(Element& element) {
DOMNodeId node_id = element.GetDomNodeId();
#if AX_FAIL_FAST_BUILD()
// Register that the relations for this element have been cached, to
// help enforce that relations are never missed.
CHECK(node_id);
processed_elements_.insert(node_id);
#endif
UpdateRegisteredIdAttribute(element, node_id);
// Register aria-owns.
UpdateReverseOwnsRelations(element);
// Register <label for>.
// TODO(crbug.com/41469336): Track reverse relations set via explicitly set
// attr-elements for htmlFor, when/if this is supported.
const auto& for_id = element.FastGetAttribute(html_names::kForAttr);
if (!for_id.empty()) {
all_previously_seen_label_target_ids_.insert(for_id);
}
// Register aria-labelledby, aria-describedby relations.
UpdateReverseTextRelations(element);
// Register aria-activedescendant.
UpdateReverseActiveDescendantRelations(element);
// Register aria-controls, aria-details, aria-errormessage, aria-flowto, and
// aria-actions.
UpdateReverseOtherRelations(element);
}
#if AX_FAIL_FAST_BUILD()
void AXRelationCache::CheckRelationsCached(Element& element) {
if (!is_initialized_) {
return;
}
CheckElementWasProcessed(element);
// Check aria-owns.
Vector<AtomicString> owns_ids;
HeapVector<Member<Element>> owns_elements;
GetRelationTargets(element, html_names::kAriaOwnsAttr, owns_ids,
owns_elements);
for (const auto& owns_id : owns_ids) {
CHECK(aria_owns_id_map_.Contains(owns_id))
<< element << " with aria-owns=" << owns_id
<< " and DOMNodeId=" << DOMNodeIds::ExistingIdForNode(&element)
<< " should already be in cache.";
}
for (const Member<Element>& owns_element : owns_elements) {
DOMNodeId owns_dom_node_id =
DOMNodeIds::ExistingIdForNode(owns_element.Get());
CHECK(owns_dom_node_id && aria_owns_node_map_.Contains(owns_dom_node_id))
<< element << " with ariaOwnsElements including " << owns_element
<< " and DOMNodeId=" << DOMNodeIds::ExistingIdForNode(&element)
<< " should already be in cache.";
}
// Check <label for>.
// TODO(crbug.com/41469336): Track reverse relations set via explicitly set
// attr-elements for htmlFor, when/if this is supported.
if (IsA<HTMLLabelElement>(element)) {
const auto& for_id = element.FastGetAttribute(html_names::kForAttr);
if (!for_id.empty()) {
CHECK(all_previously_seen_label_target_ids_.Contains(for_id))
<< element << " <label for=" << for_id
<< " with DOMNodeId=" << DOMNodeIds::ExistingIdForNode(&element)
<< " should already be in cache.";
}
}
// Check aria-labelledby, aria-describedby.
for (const auto& [attribute, filter] : GetTextRelationAttributes()) {
Vector<AtomicString> text_relation_ids;
HeapVector<Member<Element>> text_relation_elements;
GetRelationTargets(element, attribute, text_relation_ids,
text_relation_elements);
for (const auto& text_relation_id : text_relation_ids) {
CHECK(aria_text_relations_id_map_.Contains(text_relation_id))
<< element << " with " << attribute << "=" << text_relation_id
<< " and DOMNodeId=" << DOMNodeIds::ExistingIdForNode(&element)
<< " should already be in cache.";
}
for (const Member<Element>& text_relation_element :
text_relation_elements) {
DOMNodeId text_relation_dom_node_id =
DOMNodeIds::ExistingIdForNode(text_relation_element.Get());
CHECK(text_relation_dom_node_id &&
aria_text_relations_node_map_.Contains(text_relation_dom_node_id))
<< element << " with " << attribute
<< "-associated elements including " << text_relation_element
<< " and DOMNodeId=" << DOMNodeIds::ExistingIdForNode(&element)
<< " should already be in cache.";
}
}
// Check aria-activedescendant.
const AtomicString& activedescendant_id =
AXObject::AriaAttribute(element, html_names::kAriaActivedescendantAttr);
if (!activedescendant_id.empty()) {
CHECK(aria_activedescendant_id_map_.Contains(activedescendant_id))
<< element << " with aria-activedescendant=" << activedescendant_id
<< " and DOMNodeId=" << DOMNodeIds::ExistingIdForNode(&element)
<< " should already be in cache.";
} else {
HeapVector<Member<Element>> activedescendant_elements;
GetExplicitlySetElementsForAttr(element,
html_names::kAriaActivedescendantAttr,
activedescendant_elements);
if (!activedescendant_elements.empty()) {
Member<Element>& active_descendant_element = activedescendant_elements[0];
DOMNodeId active_descendant_dom_node_id =
DOMNodeIds::ExistingIdForNode(active_descendant_element);
CHECK(active_descendant_dom_node_id &&
aria_activedescendant_node_map_.Contains(
active_descendant_dom_node_id))
<< element << " with ariaActiveDescendantElement "
<< active_descendant_element
<< " and DOMNodeId=" << DOMNodeIds::ExistingIdForNode(&element)
<< " should already be in cache.";
}
}
}
void AXRelationCache::CheckElementWasProcessed(Element& element) {
DOMNodeId node_id = DOMNodeIds::ExistingIdForNode(&element);
if (node_id && processed_elements_.Contains(node_id)) {
return;
}
// Find first ancestor that was not processed.
Node* ancestor = &element;
if (element.GetDocument().IsFlatTreeTraversalForbidden()) {
DVLOG(1) << "Note: flat tree traversal forbidden.";
} else {
while (true) {
Node* next_ancestor = FlatTreeTraversal::Parent(*ancestor);
if (!next_ancestor) {
break;
}
if (!IsA<Element>(next_ancestor)) {
break;
}
node_id = DOMNodeIds::ExistingIdForNode(next_ancestor);
if (node_id && processed_elements_.Contains(node_id)) {
// next_ancestor was not processed, therefore ancestor is the
// top unprocessed node.
break;
}
ancestor = next_ancestor;
}
}
AXObject* obj = Get(ancestor);
NOTREACHED()
<< "The following element was attached to the document, but "
"UpdateCacheAfterNodeIsAttached() was never called with it, and it "
"did not exist when the cache was first initialized:"
<< "\n* Element: " << ancestor
<< "\n* LayoutObject: " << ancestor->GetLayoutObject()
<< "\n* AXObject: " << obj << "\n"
<< (obj && obj->ParentObjectIncludedInTree()
? obj->ParentObjectIncludedInTree()->GetAXTreeForThis()
: "");
}
#endif // AX_FAIL_FAST_BUILD()
void AXRelationCache::ProcessUpdatesWithCleanLayout() {
HashSet<DOMNodeId> old_owner_axids_to_update;
old_owner_axids_to_update.swap(owner_axids_to_update_);
for (DOMNodeId aria_owner_axid : old_owner_axids_to_update) {
AXObject* obj = ObjectFromAXID(aria_owner_axid);
if (obj) {
UpdateAriaOwnsWithCleanLayout(obj);
}
}
owner_axids_to_update_.clear();
}
bool AXRelationCache::IsDirty() const {
return !owner_axids_to_update_.empty();
}
bool AXRelationCache::IsAriaOwned(const AXObject* child, bool check) const {
if (!child)
return false;
DCHECK(!child->IsDetached()) << "Child was detached: " << child;
bool is_owned =
aria_owned_child_to_owner_mapping_.Contains(child->AXObjectID());
if (is_owned) {
return true;
}
if (!check) {
return false;
}
// Ensure that unowned objects have the expected parent.
AXObject* parent = child->ParentObjectIfPresent();
if (parent && parent->GetElement() && child->GetElement() &&
!child->GetElement()->IsPseudoElement()) {
Node* natural_parent = AXObject::GetParentNodeForComputeParent(
*object_cache_, child->GetElement());
if (parent->GetNode() != natural_parent) {
std::ostringstream msg;
msg << "Unowned child should have natural parent:"
<< "\n* Child: " << child << "\n* Actual parent: " << parent
<< "\n* Natural ax parent: " << object_cache_->Get(natural_parent)
<< "\n* Natural dom parent: " << natural_parent << " #"
<< natural_parent->GetDomNodeId() << "\n* Owners to update:";
for (AXID id : owner_axids_to_update_) {
msg << " " << id;
}
DUMP_WILL_BE_CHECK(false) << msg.str();
}
}
return false;
}
AXObject* AXRelationCache::GetAriaOwnedParent(const AXObject* child) const {
// Child IDs may still be present in owning parents whose list of children
// have been marked as requiring an update, but have not been updated yet.
HashMap<AXID, AXID>::const_iterator iter =
aria_owned_child_to_owner_mapping_.find(child->AXObjectID());
if (iter == aria_owned_child_to_owner_mapping_.end())
return nullptr;
return ObjectFromAXID(iter->value);
}
AXObject* AXRelationCache::ValidatedAriaOwner(const AXObject* child) {
if (!child->GetNode()) {
return nullptr;
}
AXObject* owner = GetAriaOwnedParent(child);
if (!owner || IsValidOwnsRelation(owner, *child->GetNode())) {
return owner;
}
RemoveOwnedRelation(child->AXObjectID());
return nullptr;
}
void AXRelationCache::GetExplicitlySetElementsForAttr(
const Element& source,
const QualifiedName& attr_name,
HeapVector<Member<Element>>& target_elements) {
if (auto* explicitly_set_elements =
source.GetExplicitlySetElementsForAttr(attr_name);
explicitly_set_elements) {
for (const WeakMember<Element>& element : *explicitly_set_elements) {
target_elements.push_back(element);
}
return;
}
const ElementInternals* element_internals = source.GetElementInternals();
if (!element_internals) {
return;
}
const FrozenArray<Element>* element_internals_attr_elements =
element_internals->GetElementArrayAttribute(attr_name);
if (!element_internals_attr_elements) {
return;
}
target_elements = element_internals_attr_elements->AsVector();
}
void AXRelationCache::GetRelationTargets(
const Element& source,
const QualifiedName& attr_name,
Vector<AtomicString>& target_ids,
HeapVector<Member<Element>>& target_elements) {
const AtomicString& ids = AXObject::AriaAttribute(source, attr_name);
if (!ids.empty()) {
// If the attribute is set to an ID list string, the IDs are the primary key
// for the relation.
IdsFromAttribute(source, target_ids, attr_name);
return;
}
GetExplicitlySetElementsForAttr(source, attr_name, target_elements);
}
void AXRelationCache::UpdateReverseRelations(
Element& source,
const QualifiedName& attr_name,
TargetIdToSourceNodeMap& id_map,
TargetNodeToSourceNodeMap& node_map) {
Vector<AtomicString> target_ids;
HeapVector<Member<Element>> target_elements;
GetRelationTargets(source, attr_name, target_ids, target_elements);
UpdateReverseIdAttributeRelations(id_map, &source, target_ids);
Vector<DOMNodeId> target_nodes;
for (const Member<Element>& element : target_elements) {
target_nodes.push_back(element->GetDomNodeId());
}
UpdateReverseElementAttributeRelations(node_map, &source, target_nodes);
}
void AXRelationCache::GetSingleRelationTarget(const Element& source,
const QualifiedName& attr_name,
AtomicString& target_id,
Element** element) {
const AtomicString& id = AXObject::AriaAttribute(source, attr_name);
if (!id.empty()) {
target_id = id;
return;
}
HeapVector<Member<Element>> target_elements;
GetExplicitlySetElementsForAttr(source, attr_name, target_elements);
if (target_elements.empty()) {
return;
}
DCHECK_EQ(target_elements.size(), 1u);
*element = target_elements.at(0).Get();
}
void AXRelationCache::UpdateReverseSingleRelation(
Element& source,
const QualifiedName& attr_name,
TargetIdToSourceNodeMap& id_map,
TargetNodeToSourceNodeMap& node_map) {
AtomicString target_id;
Element* target_element = nullptr;
GetSingleRelationTarget(source, attr_name, target_id, &target_element);
if (!target_id.empty()) {
UpdateReverseIdAttributeRelations(id_map, &source, {target_id});
return;
}
if (!target_element) {
return;
}
Vector<DOMNodeId> target_nodes;
target_nodes.push_back(target_element->GetDomNodeId());
UpdateReverseElementAttributeRelations(node_map, &source, target_nodes);
}
// Update reverse relation map, where source is related to target_ids.
void AXRelationCache::UpdateReverseIdAttributeRelations(
TargetIdToSourceNodeMap& id_map,
Node* source,
const Vector<AtomicString>& target_ids) {
// Add entries to reverse map.
for (const AtomicString& target_id : target_ids) {
auto result = id_map.insert(target_id, HashSet<DOMNodeId>());
result.stored_value->value.insert(source->GetDomNodeId());
}
}
// Update reverse relation map, where source is related to
// target_elements.
void AXRelationCache::UpdateReverseElementAttributeRelations(
TargetNodeToSourceNodeMap& node_map,
Node* source,
const Vector<DOMNodeId>& target_nodes) {
// Add entries to reverse map.
for (const DOMNodeId& target_node : target_nodes) {
auto result = node_map.insert(target_node, HashSet<DOMNodeId>());
result.stored_value->value.insert(source->GetDomNodeId());
}
}
base::span<std::pair<QualifiedName, Element::TinyBloomFilter>>
AXRelationCache::GetTextRelationAttributes() {
// Avoid issues with commas within the type name in DEFINE_STATIC_LOCAL().
using QualifiedNameArray =
std::array<std::pair<QualifiedName, Element::TinyBloomFilter>, 3>;
DEFINE_STATIC_LOCAL(
QualifiedNameArray, text_attributes,
({{html_names::kAriaLabelledbyAttr,
Element::FilterForAttribute(html_names::kAriaLabelledbyAttr)},
{html_names::kAriaLabeledbyAttr,
Element::FilterForAttribute(html_names::kAriaLabeledbyAttr)},
{html_names::kAriaDescribedbyAttr,
Element::FilterForAttribute(html_names::kAriaDescribedbyAttr)}}));
return text_attributes;
}
void AXRelationCache::UpdateReverseTextRelations(Element& source) {
for (const auto& [attribute, filter] : GetTextRelationAttributes()) {
if (source.CouldMatchFilter(filter) || source.GetElementInternals()) {
UpdateReverseTextRelations(source, attribute);
}
}
}
void AXRelationCache::UpdateReverseTextRelations(
Element& source,
const QualifiedName& attr_name) {
Vector<AtomicString> id_vector;
HeapVector<Member<Element>> target_elements;
GetRelationTargets(source, attr_name, id_vector, target_elements);
UpdateReverseIdAttributeTextRelations(source, id_vector);
UpdateReverseElementAttributeTextRelations(source, target_elements);
}
void AXRelationCache::UpdateReverseIdAttributeTextRelations(
Element& source,
const Vector<AtomicString>& target_ids) {
if (target_ids.empty()) {
return;
}
Vector<AtomicString> new_target_ids;
for (const AtomicString& id : target_ids) {
if (aria_text_relations_id_map_.Contains(id)) {
continue;
}
new_target_ids.push_back(id);
}
// Update the target ids so that the point back to the relation source node.
UpdateReverseIdAttributeRelations(aria_text_relations_id_map_, &source,
target_ids);
// Mark all of the new text relation targets dirty.
TreeScope& scope = source.GetTreeScope();
for (const AtomicString& id : new_target_ids) {
MarkNewRelationTargetDirty(scope.getElementById(id));
}
}
void AXRelationCache::UpdateReverseElementAttributeTextRelations(
Element& source,
const HeapVector<Member<Element>>& target_elements) {
if (target_elements.empty()) {
return;
}
Vector<DOMNodeId> target_nodes;
HeapVector<Member<Element>> new_target_elements;
for (const Member<Element>& element : target_elements) {
DOMNodeId dom_node_id = element->GetDomNodeId();
target_nodes.push_back(dom_node_id);
if (!aria_text_relations_node_map_.Contains(element->GetDomNodeId())) {
new_target_elements.push_back(element);
}
}
// Update the target nodes so that they point back to the relation source
// node.
UpdateReverseElementAttributeRelations(aria_text_relations_node_map_, &source,
target_nodes);
// Mark all of the new text relation targets dirty.
for (const Member<Element>& element : new_target_elements) {
MarkNewRelationTargetDirty(element.Get());
}
}
void AXRelationCache::UpdateReverseActiveDescendantRelations(Element& source) {
if (source.CouldHaveAttribute(html_names::kAriaActivedescendantAttr) ||
source.GetElementInternals()) {
UpdateReverseSingleRelation(source, html_names::kAriaActivedescendantAttr,
aria_activedescendant_id_map_,
aria_activedescendant_node_map_);
}
}
void AXRelationCache::UpdateReverseOwnsRelations(Element& source) {
if (source.CouldHaveAttribute(html_names::kAriaOwnsAttr) ||
source.GetElementInternals()) {
UpdateReverseRelations(source, html_names::kAriaOwnsAttr, aria_owns_id_map_,
aria_owns_node_map_);
}
}
base::span<std::pair<QualifiedName, Element::TinyBloomFilter>>
AXRelationCache::GetOtherRelationAttributes() {
// Avoid issues with commas within the type name in DEFINE_STATIC_LOCAL().
using QualifiedNameArray =
std::array<std::pair<QualifiedName, Element::TinyBloomFilter>, 5>;
DEFINE_STATIC_LOCAL(
QualifiedNameArray, attributes,
({{html_names::kAriaControlsAttr,
Element::FilterForAttribute(html_names::kAriaControlsAttr)},
{html_names::kAriaDetailsAttr,
Element::FilterForAttribute(html_names::kAriaDetailsAttr)},
{html_names::kAriaErrormessageAttr,
Element::FilterForAttribute(html_names::kAriaErrormessageAttr)},
{html_names::kAriaFlowtoAttr,
Element::FilterForAttribute(html_names::kAriaFlowtoAttr)},
{html_names::kAriaActionsAttr,
Element::FilterForAttribute(html_names::kAriaActionsAttr)}}));
return attributes;
}
void AXRelationCache::UpdateReverseOtherRelations(Element& source) {
for (const auto& [attribute, filter] : GetOtherRelationAttributes()) {
if (source.CouldMatchFilter(filter) || source.GetElementInternals()) {
UpdateReverseRelations(source, attribute, aria_other_relations_id_map_,
aria_other_relations_node_map_);
}
}
}
void AXRelationCache::MarkNewRelationTargetDirty(Node* target) {
// Mark root of label dirty so that we can change inclusion states as
// necessary (label subtrees are included in the tree even if hidden).
if (object_cache_->lifecycle().StateAllowsImmediateTreeUpdates()) {
// WHen the relation cache is first initialized, we are already in
// processing deferred events, and must manually invalidate the
// cached values (is_used_for_label_or_description may have changed).
if (AXObject* ax_target = Get(target)) {
ax_target->InvalidateCachedValues(
TreeUpdateReason::kNewRelationTargetDirty);
}
// Must use clean layout method.
object_cache_->MarkElementDirtyWithCleanLayout(target);
} else {
// This will automatically invalidate the cached values of the target.
object_cache_->MarkElementDirty(target);
}
}
// ContainsCycle() should:
// * Return true when a cycle is an authoring error, but not an error in Blink.
// * CHECK failure when Blink should have caught this error earlier ... we
// should have never gotten into this state.
//
// For example, if a web page specifies that grandchild owns it's grandparent,
// what should happen is the ContainsCycle will start at the grandchild and go
// up, finding that it's grandparent is already in the ancestor chain, and
// return false, thus disallowing the relation. However, if on the way to the
// root, it discovers that any other two objects are repeated in the ancestor
// chain, this is unexpected, and results in the CHECK() failure.
static bool ContainsCycle(AXObject* owner, Node& child_node) {
if (FlatTreeTraversal::IsDescendantOf(*owner->GetNode(), child_node)) {
// A DOM descendant cannot own its ancestor.
return true;
}
HashSet<AXID> visited;
// Walk up the parents of the owner object, make sure that this child
// doesn't appear there, as that would create a cycle.
for (AXObject* ancestor = owner; ancestor;
ancestor = ancestor->ParentObject()) {
if (ancestor->GetNode() == &child_node) {
return true;
}
CHECK(visited.insert(ancestor->AXObjectID()).is_new_entry)
<< "Cycle in unexpected place:\n"
<< "* Owner = " << owner << "* Child = " << child_node;
}
return false;
}
bool AXRelationCache::IsValidOwnsRelation(AXObject* owner,
Node& child_node) const {
if (!IsValidOwner(owner)) {
return false;
}
if (!IsValidOwnedChild(child_node)) {
return false;
}
// If this child is already aria-owned by a different owner, continue.
// It's an author error if this happens and we don't worry about which of
// the two owners wins ownership, as long as only one of them does.
if (AXObject* child = object_cache_->Get(&child_node)) {
if (IsAriaOwned(child) && GetAriaOwnedParent(child) != owner) {
return false;
}
}
// You can't own yourself or an ancestor!
if (ContainsCycle(owner, child_node)) {
return false;
}
return true;
}
// static
bool AXRelationCache::IsValidOwner(AXObject* owner) {
if (!owner->GetNode()) {
NOTREACHED() << "Cannot use aria-owns without a node on both ends";
}
// Can't have element children.
// <br> is special in that it is allowed to have inline textbox children,
// but no element children.
if (!owner->CanHaveChildren() || IsA<HTMLBRElement>(owner->GetNode())) {
return false;
}
// 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;
// A frame/iframe/fencedframe can only parent a document.
if (AXObject::IsFrame(owner->GetNode()))
return false;
// Images can only use <img usemap> to "own" <area> children.
// This requires special parenting logic, and aria-owns is prevented here in
// order to keep things from getting too complex.
if (owner->RoleValue() == ax::mojom::blink::Role::kImage)
return false;
// 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;
// Problematic for cycles, and does not solve a known use case.
// Easiest to omit the possibility.
if (owner->IsAriaHidden())
return false;
return true;
}
// static
bool AXRelationCache::IsValidOwnedChild(Node& child_node) {
Element* child_element = DynamicTo<Element>(child_node);
if (!child_element) {
return false;
}
// Require a layout object, in order to avoid strange situations where
// a node tries to parent an AXObject that cannot exist because its node
// cannot partake in layout tree building (e.g. unused fallback content of a
// media element). This is the simplest way to avoid many types of abnormal
// situations, and there's no known use case for pairing aria-owns with
// invisible content.
if (!child_node.GetLayoutObject()) {
return false;
}
// An area can't be owned, only parented by <img usemap>.
if (IsA<HTMLAreaElement>(child_node)) {
return false;
}
// <select> options can only be children of AXMenuListPopup or AXListBox.
if (IsA<HTMLOptionElement>(child_node) ||
IsA<HTMLOptGroupElement>(child_node)) {
return false;
}
// aria-hidden is problematic for cycles, and does not solve a known use case.
// Easiest to omit the possibility.
if (AXObject::IsAriaAttributeTrue(*child_element,
html_names::kAriaHiddenAttr)) {
return false;
}
return true;
}
void AXRelationCache::UnmapOwnedChildrenWithCleanLayout(
const AXObject* owner,
const Vector<AXID>& removed_child_ids,
Vector<AXID>& unparented_child_ids) {
DCHECK(owner);
DCHECK(!owner->IsDetached());
for (AXID removed_child_id : removed_child_ids) {
// Find the AXObject for the child that this owner no longer owns.
AXObject* removed_child = ObjectFromAXID(removed_child_id);
// It's possible that this child has already been owned by some other
// owner, in which case we don't need to do anything other than marking
// the original parent dirty.
if (removed_child && GetAriaOwnedParent(removed_child) != owner) {
ChildrenChangedWithCleanLayout(removed_child->ParentObjectIfPresent());
continue;
}
// Remove it from the child -> owner mapping so it's not owned by this
// owner anymore.
aria_owned_child_to_owner_mapping_.erase(removed_child_id);
if (removed_child) {
// Return the unparented children so their parent can be restored after
// all aria-owns changes are complete.
unparented_child_ids.push_back(removed_child_id);
}
}
}
void AXRelationCache::MapOwnedChildrenWithCleanLayout(
const AXObject* owner,
const Vector<AXID>& child_ids) {
DCHECK(owner);
DCHECK(!owner->IsDetached());
for (AXID added_child_id : child_ids) {
AXObject* added_child = ObjectFromAXID(added_child_id);
DCHECK(added_child);
DCHECK(!added_child->IsDetached());
// Invalidating ensures that cached "included in tree" state is recomputed
// on objects with changed ownership -- owned children must always be
// included in the tree.
added_child->InvalidateCachedValues(TreeUpdateReason::kUpdateAriaOwns);
// Add this child to the mapping from child to owner.
aria_owned_child_to_owner_mapping_.Set(added_child_id, owner->AXObjectID());
// 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->ParentObjectIfPresent();
if (original_parent != owner) {
if (original_parent) {
added_child->DetachFromParent();
}
added_child->SetParent(const_cast<AXObject*>(owner));
if (original_parent) {
ChildrenChangedWithCleanLayout(original_parent);
// Reparenting detection requires the parent of the original parent to
// be reserialized.
// This change prevents several DumpAccessibilityEventsTest failures:
// - AccessibilityEventsSubtreeReparentedViaAriaOwns/linux
// - AccessibilityEventsSubtreeReparentedViaAriaOwns2/linux
// TODO(crbug.com/1299031) Find out why this is necessary.
object_cache_->MarkAXObjectDirtyWithCleanLayout(
original_parent->ParentObject());
}
}
// Now that the child is owned, it's "included in tree" state must be
// recomputed because owned children are always included in the tree.
added_child->UpdateCachedAttributeValuesIfNeeded(false);
// If the added child had a change in an inherited state because of the new
// owner, that state needs to propagate into the subtree. Remove its
// descendants so they are re-added with the correct cached states.
// The new states would also be propagted in FinalizeTree(), but this is
// safer for certain situations such as the aria-owns + aria-hidden state,
// where the aria-hidden state could be invalidated late in the cycle due
// to focus changes.
if (added_child->ChildrenNeedToUpdateCachedValues()) {
object_cache_->RemoveSubtree(added_child->GetNode(),
/*remove_root*/ false);
}
}
}
void AXRelationCache::UpdateAriaOwnsFromAttrAssociatedElementsWithCleanLayout(
AXObject* owner,
const GCedHeapVector<Member<Element>>& attr_associated_elements,
HeapVector<Member<AXObject>>& validated_owned_children_result,
bool force) {
CHECK(!object_cache_->IsFrozen());
// 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.
Vector<DOMNodeId> owned_dom_node_ids;
for (const auto& element : attr_associated_elements) {
CHECK(element);
if (!IsValidOwnsRelation(const_cast<AXObject*>(owner), *element)) {
continue;
}
AXObject* child = GetOrCreate(element, owner);
if (!child) {
return;
}
owned_dom_node_ids.push_back(element->GetDomNodeId());
validated_owned_children_result.push_back(child);
}
// Track reverse relations for future tree updates.
UpdateReverseElementAttributeRelations(aria_owns_node_map_, owner->GetNode(),
owned_dom_node_ids);
// Update the internal mappings of owned children.
UpdateAriaOwnerToChildrenMappingWithCleanLayout(
owner, validated_owned_children_result, force);
}
void AXRelationCache::ValidatedAriaOwnedChildren(
const AXObject* owner,
HeapVector<Member<AXObject>>& validated_owned_children_result) {
if (!aria_owner_to_children_mapping_.Contains(owner->AXObjectID()))
return;
Vector<AXID> current_child_axids =
aria_owner_to_children_mapping_.at(owner->AXObjectID());
for (AXID child_id : current_child_axids) {
AXObject* child = ObjectFromAXID(child_id);
if (!child) {
RemoveOwnedRelation(child_id);
} else if (ValidatedAriaOwner(child) == owner) {
validated_owned_children_result.push_back(child);
DCHECK(IsAriaOwned(child))
<< "Owned child not in owned child map:"
<< "\n* Owner = " << owner << "\n* Child = " << child;
}
}
}
void AXRelationCache::UpdateAriaOwnsWithCleanLayout(AXObject* owner,
bool force) {
CHECK(!object_cache_->IsFrozen());
DCHECK(owner);
Element* element = owner->GetElement();
if (!element)
return;
DCHECK(!element->GetDocument().NeedsLayoutTreeUpdateForNode(*element));
// 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) {
// Make sure that the owner's children are updated even in the case where
// aria-owns is empty, or the object is not a valid owner. This protects
// from ending up with a previous owner containing invalid children.
ChildrenChangedWithCleanLayout(owner);
return;
}
HeapVector<Member<AXObject>> owned_children;
// We first check if the element has an explicitly set aria-owns association.
// 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.
if (!is_valid_owner) {
DCHECK(force) << "Should not reach here except when an AXObject was "
"invalidated and is being refreshed: "
<< owner;
} else if (element && element->HasExplicitlySetAttrAssociatedElements(
html_names::kAriaOwnsAttr)) {
// TODO (crbug.com/41469336): Also check ElementInternals here.
UpdateAriaOwnsFromAttrAssociatedElementsWithCleanLayout(
owner, *element->GetAttrAssociatedElements(html_names::kAriaOwnsAttr),
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
// update the maps and |validated_owned_children_result| based on that.
//
// Figure out the children that are owned by this object and are in the
// tree.
TreeScope& scope = element->GetTreeScope();
SpaceSplitString owned_id_vector(
AXObject::AriaAttribute(*element, html_names::kAriaOwnsAttr));
HeapVector<Member<Element>> valid_owned_child_elements;
for (AtomicString id_name : owned_id_vector) {
Element* child_element = scope.getElementById(id_name);
if (!child_element ||
!IsValidOwnsRelation(const_cast<AXObject*>(owner), *child_element)) {
continue;
}
AXID future_child_axid = child_element->GetDomNodeId();
HashMap<AXID, AXID>::const_iterator iter =
aria_owned_child_to_owner_mapping_.find(future_child_axid);
bool has_previous_owner =
iter != aria_owned_child_to_owner_mapping_.end();
if (has_previous_owner && owner->AXObjectID() != iter->value) {
// Already has a different aria-owns parent.
continue;
}
// Preemptively add the child to owner mapping to satisfy checks
// that this child is owned, and therefore does not need to be added by
// any other node who's subtree is eagerly updated during the
// GetOrCreate() call, as this call recursively fills out subtrees.
aria_owned_child_to_owner_mapping_.Set(future_child_axid,
owner->AXObjectID());
if (!has_previous_owner) {
// Force UpdateAriaOwnerToChildrenMappingWithCleanLayout() to map
// the new owner.
force = true;
}
valid_owned_child_elements.emplace_back(child_element);
}
for (Element* child_element : valid_owned_child_elements) {
AXObject* child = GetOrCreate(child_element, owner);
if (child) {
owned_children.push_back(child);
}
}
}
// Update the internal validated mapping of owned children. This will
// fire an event if the mapping has changed.
UpdateAriaOwnerToChildrenMappingWithCleanLayout(owner, owned_children, force);
}
void AXRelationCache::UpdateAriaOwnerToChildrenMappingWithCleanLayout(
AXObject* owner,
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());
}
// Compare this to the current list of owned children, and exit early if
// there are no changes.
Vector<AXID> previously_owned_child_ids;
auto it = aria_owner_to_children_mapping_.find(owner->AXObjectID());
if (it != aria_owner_to_children_mapping_.end()) {
previously_owned_child_ids = it->value;
}
// 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 (previously_owned_child_ids == validated_owned_child_axids &&
(!force || previously_owned_child_ids.empty())) {
ChildrenChangedWithCleanLayout(owner);
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
// children and add the new list of owned children.
Vector<AXID> unparented_child_ids;
UnmapOwnedChildrenWithCleanLayout(owner, previously_owned_child_ids,
unparented_child_ids);
MapOwnedChildrenWithCleanLayout(owner, validated_owned_child_axids);
#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->ComputeIsIgnoredButIncludedInTree())
<< "Owned child not in tree: " << child
<< "\nRecompute included in tree: "
<< child->ComputeIsIgnoredButIncludedInTree();
}
#endif
// Finally, update the mapping from the owner to the list of child IDs.
if (validated_owned_child_axids.empty()) {
aria_owner_to_children_mapping_.erase(owner->AXObjectID());
} else {
aria_owner_to_children_mapping_.Set(owner->AXObjectID(),
validated_owned_child_axids);
}
// Ensure that objects that have lost their parent have one, or that their
// subtree is pruned if there is no available parent.
for (AXID unparented_child_id : unparented_child_ids) {
if (validated_owned_child_axids.Contains(unparented_child_id)) {
continue;
}
// Recompute the real parent and cache it.
if (AXObject* ax_unparented = ObjectFromAXID(unparented_child_id)) {
// Invalidating ensures that cached "included in tree" state is recomputed
// on objects with changed ownership -- owned children must always be
// included in the tree.
ax_unparented->InvalidateCachedValues(TreeUpdateReason::kUpdateAriaOwns);
// Find the unparented child's new parent, and reparent it to that
// back to its real parent in the tree by finding its current parent,
// marking that dirty and detaching from that parent.
AXObject* original_parent = ax_unparented->ParentObjectIfPresent();
// Recompute the real parent .
ax_unparented->DetachFromParent();
MaybeRestoreParentOfOwnedChild(unparented_child_id);
// Mark everything dirty so that the serializer sees all changes.
ChildrenChangedWithCleanLayout(original_parent);
ChildrenChangedWithCleanLayout(ax_unparented->ParentObjectIfPresent());
if (!ax_unparented->IsDetached()) {
object_cache_->MarkAXObjectDirtyWithCleanLayout(ax_unparented);
}
}
}
ChildrenChangedWithCleanLayout(owner);
}
bool AXRelationCache::MayHaveHTMLLabelViaForAttribute(
const HTMLElement& labelable) {
const AtomicString& id = labelable.GetIdAttribute();
if (id.empty())
return false;
return all_previously_seen_label_target_ids_.Contains(id);
}
bool AXRelationCache::IsARIALabelOrDescription(Element& element) {
// Labels and descriptions set by ariaLabelledByElements,
// ariaDescribedByElements.
if (aria_text_relations_node_map_.find(element.GetDomNodeId()) !=
aria_text_relations_node_map_.end()) {
return true;
}
// Labels and descriptions set by aria-labelledby, aria-describedby.
const AtomicString& id_value = element.GetIdAttribute();
if (id_value.IsNull()) {
return false;
}
bool found_in_id_mapping = aria_text_relations_id_map_.find(id_value) !=
aria_text_relations_id_map_.end();
return found_in_id_mapping;
}
// Fill source_objects with AXObjects for relations pointing to target.
void AXRelationCache::GetRelationSourcesById(
const AtomicString& target_id_attr,
TargetIdToSourceNodeMap& id_map,
HeapVector<Member<AXObject>>& source_objects) {
if (target_id_attr == g_null_atom) {
return;
}
auto it = id_map.find(target_id_attr);
if (it == id_map.end()) {
return;
}
for (DOMNodeId source_node : it->value) {
AXObject* source_object = Get(DOMNodeIds::NodeForId(source_node));
if (source_object)
source_objects.push_back(source_object);
}
}
void AXRelationCache::GetRelationSourcesByElementReference(
const DOMNodeId target_node,
TargetNodeToSourceNodeMap& node_map,
HeapVector<Member<AXObject>>& source_objects) {
auto it = node_map.find(target_node);
if (it == node_map.end()) {
return;
}
for (const DOMNodeId& source_node : it->value) {
AXObject* source_object = Get(DOMNodeIds::NodeForId(source_node));
if (source_object) {
source_objects.push_back(source_object);
}
}
}
AXObject* AXRelationCache::GetOrCreateAriaOwnerFor(Node* node, AXObject* obj) {
CHECK(object_cache_->lifecycle().StateAllowsImmediateTreeUpdates());
Element* element = DynamicTo<Element>(node);
if (!element) {
return nullptr;
}
#if DCHECK_IS_ON()
if (obj)
DCHECK(!obj->IsDetached());
AXObject* obj_for_node = object_cache_->Get(node);
DCHECK(!obj || obj_for_node == obj)
<< "Object and node did not match:"
<< "\n* node = " << node << "\n* obj = " << obj
<< "\n* obj_for_node = " << obj_for_node;
#endif
// Look for any new aria-owns relations.
// Schedule an update on any potential new owner.
HeapVector<Member<AXObject>> related_sources;
GetRelationSourcesById(element->GetIdAttribute(), aria_owns_id_map_,
related_sources);
GetRelationSourcesByElementReference(element->GetDomNodeId(),
aria_owns_node_map_, related_sources);
// First check for an existing aria-owns relation to the related AXObject.
AXObject* ax_new_owner = nullptr;
for (AXObject* related : related_sources) {
if (related) {
// Ensure that the candidate owner updates its children in its validity
// as an owner is changing.
owner_axids_to_update_.insert(related->AXObjectID());
object_cache_->MarkAXObjectDirtyWithCleanLayout(related);
related->SetNeedsToUpdateChildren();
if (IsValidOwnsRelation(related, *node)) {
if (!ax_new_owner) {
ax_new_owner = related;
}
owner_axids_to_update_.insert(related->AXObjectID());
}
}
}
// Schedule an update on any previous owner. This owner takes priority over
// any new owners.
AXObject* related_target = obj ? obj : Get(node);
if (related_target && IsAriaOwned(related_target)) {
AXObject* ax_previous_owner = ValidatedAriaOwner(related_target);
if (ax_previous_owner) {
owner_axids_to_update_.insert(ax_previous_owner->AXObjectID());
return ax_previous_owner;
}
}
// Only the first aria-owns relation can be used.
return ax_new_owner;
}
void AXRelationCache::UpdateRelatedTree(Node* node, AXObject* obj) {
// This can happen if MarkAXObjectDirtyWithCleanLayout is
// called and then UpdateRelatedTree is called on the same object,
// e.g. in TextChangedWithCleanLayout.
if (obj && obj->IsDetached()) {
return;
}
if (GetOrCreateAriaOwnerFor(node, obj)) {
// Ensure the aria-owns relation is processed, which in turn ensures that
// both the owner and owned child exist, and that the parent-child
// relations are correctly set on each.
ProcessUpdatesWithCleanLayout();
}
// Update names and descriptions.
UpdateRelatedText(node);
}
void AXRelationCache::UpdateRelatedTreeAfterChange(Element& element) {
// aria-activedescendant requires special handling, because additional events
// may be fired when it changes.
// Check whether aria-activedescendant on the focused object points to
// `element`. If so, fire activedescendantchanged event now. This is only for
// ARIA active descendants, not in a native control like a listbox, which
// has its own initial active descendant handling.
MarkOldAndNewRelationSourcesDirty(element, aria_activedescendant_id_map_,
aria_activedescendant_node_map_);
Element* focused_element = element.GetDocument().FocusedElement();
if (AXObject* ax_focus = Get(focused_element)) {
if (AXObject::ElementFromAttributeOrInternals(
focused_element, html_names::kAriaActivedescendantAttr) ==
&element) {
ax_focus->HandleActiveDescendantChanged();
}
}
// aria-labelledby and aria-describedby.
// Additional processing occurs in UpdateRelatedTree() when any node within
// the label or description subtree changes.
MarkOldAndNewRelationSourcesDirty(element, aria_text_relations_id_map_,
aria_text_relations_node_map_);
// aria-controls, aria-details, aria-errormessage, aria-flowto, and
// aria-actions.
MarkOldAndNewRelationSourcesDirty(element, aria_other_relations_id_map_,
aria_other_relations_node_map_);
UpdateReverseOtherRelations(element);
// Finally, update the registered id attribute for this element.
UpdateRegisteredIdAttribute(element, element.GetDomNodeId());
}
void AXRelationCache::UpdateRegisteredIdAttribute(Element& element,
DOMNodeId node_id) {
const auto& id_attr = element.GetIdAttribute();
if (id_attr == g_null_atom) {
registered_id_attributes_.erase(node_id);
} else {
registered_id_attributes_.Set(node_id, id_attr);
}
}
void AXRelationCache::UpdateRelatedText(Node* node) {
// Shortcut: used cached value to determine whether this node contributes to
// a name or description. Return early if not.
AXObject* obj = Get(node);
if (!obj || !obj->IsUsedForLabelOrDescription()) {
// Nothing to do, as this node is not part of a label or description.
return;
}
// Walk up ancestor chain from node and refresh text of any related content.
while ((obj = obj->ParentObjectIncludedInTree()) != nullptr &&
obj->IsUsedForLabelOrDescription()) {
Element* ancestor_element = obj->GetElement();
if (!ancestor_element) {
// Can occur in the CSS column case.
continue;
}
// Reverse relations via aria-labelledby, aria-describedby, aria-owns.
HeapVector<Member<AXObject>> related_sources;
GetRelationSourcesById(ancestor_element->GetIdAttribute(),
aria_text_relations_id_map_, related_sources);
GetRelationSourcesByElementReference(ancestor_element->GetDomNodeId(),
aria_text_relations_node_map_,
related_sources);
for (AXObject* related : related_sources) {
if (related && related->IsIncludedInTree() &&
!related->NeedsToUpdateChildren()) {
object_cache_->MarkAXObjectDirtyWithCleanLayout(related);
}
}
// Ancestors that may derive their accessible name from descendant content
// should also handle text changed events when descendant content changes.
if ((!obj->IsIgnored() || obj->CanSetFocusAttribute()) &&
obj->SupportsNameFromContents(/*recursive=*/false) &&
!obj->NeedsToUpdateChildren()) {
object_cache_->MarkAXObjectDirtyWithCleanLayout(obj);
}
// Forward relation via <label for="[id]">.
if (HTMLLabelElement* label =
DynamicTo<HTMLLabelElement>(ancestor_element)) {
object_cache_->MarkElementDirtyWithCleanLayout(LabelChanged(*label));
}
}
}
void AXRelationCache::MarkOldAndNewRelationSourcesDirty(
Element& element,
TargetIdToSourceNodeMap& id_map,
TargetNodeToSourceNodeMap& node_map) {
HeapVector<Member<AXObject>> related_sources;
const AtomicString& id_attr = element.GetIdAttribute();
GetRelationSourcesById(id_attr, id_map, related_sources);
const DOMNodeId dom_node_id = element.GetDomNodeId();
GetRelationSourcesByElementReference(dom_node_id, node_map, related_sources);
// If id attribute changed, also mark old relation source dirty, and update
// the map that points from the id attribute to the node id
auto iter = registered_id_attributes_.find(element.GetDomNodeId());
if (iter != registered_id_attributes_.end()) {
const AtomicString& old_id_attr = iter->value;
if (old_id_attr != id_attr) {
GetRelationSourcesById(old_id_attr, id_map, related_sources);
}
}
for (AXObject* related : related_sources) {
object_cache_->MarkAXObjectDirtyWithCleanLayout(related);
}
}
void AXRelationCache::UpdateCSSAnchorFor(Node* positioned_node) {
// Remove existing mapping.
AXID positioned_id = positioned_node->GetDomNodeId();
if (positioned_obj_to_anchor_mapping_.Contains(positioned_id)) {
AXID prev_anchor = positioned_obj_to_anchor_mapping_.at(positioned_id);
anchor_to_positioned_obj_mapping_.erase(prev_anchor);
positioned_obj_to_anchor_mapping_.erase(positioned_id);
object_cache_->MarkAXObjectDirtyWithCleanLayout(
ObjectFromAXID(prev_anchor));
}
LayoutBox* layout_box =
DynamicTo<LayoutBox>(positioned_node->GetLayoutObject());
if (!layout_box) {
return;
}
Element* anchor = layout_box->AccessibilityAnchor();
if (!anchor) {
return;
}
// AccessibilityAnchor() only returns an anchor if there is one anchor, so
// the map is only updated when there is a 1:1 anchor to positioned element
// mapping.
AXID anchor_id = anchor->GetDomNodeId();
anchor_to_positioned_obj_mapping_.Set(anchor_id, positioned_id);
positioned_obj_to_anchor_mapping_.Set(positioned_id, anchor_id);
object_cache_->MarkElementDirtyWithCleanLayout(anchor);
}
AXObject* AXRelationCache::GetPositionedObjectForAnchor(
const AXObject* anchor) {
HashMap<AXID, AXID>::const_iterator iter =
anchor_to_positioned_obj_mapping_.find(anchor->AXObjectID());
if (iter == anchor_to_positioned_obj_mapping_.end()) {
return nullptr;
}
return ObjectFromAXID(iter->value);
}
AXObject* AXRelationCache::GetAnchorForPositionedObject(
const AXObject* positioned_obj) {
HashMap<AXID, AXID>::const_iterator iter =
positioned_obj_to_anchor_mapping_.find(positioned_obj->AXObjectID());
if (iter == positioned_obj_to_anchor_mapping_.end()) {
return nullptr;
}
return ObjectFromAXID(iter->value);
}
void AXRelationCache::RemoveAXID(AXID obj_id) {
// Need to remove from maps.
// There are maps from children to their owners, and owners to their children.
// In addition, the removed id may be an owner, or be owned, or both.
// |obj_id| owned others:
if (aria_owner_to_children_mapping_.Contains(obj_id)) {
// |obj_id| no longer owns anything.
Vector<AXID> child_axids = aria_owner_to_children_mapping_.at(obj_id);
aria_owned_child_to_owner_mapping_.RemoveAll(child_axids);
// Owned children are no longer owned by |obj_id|
aria_owner_to_children_mapping_.erase(obj_id);
// When removing nodes in AXObjectCacheImpl::Dispose we do not need to
// reparent (that could anyway fail trying to attach to an already removed
// node.
// TODO(jdapena@igalia.com): explore if we can skip all processing of the
// mappings in AXRelationCache in dispose case.
if (!object_cache_->IsDisposing()) {
for (const auto& child_axid : child_axids) {
if (AXObject* owned_child = ObjectFromAXID(child_axid)) {
owned_child->DetachFromParent();
CHECK(object_cache_->lifecycle().StateAllowsReparentingAXObjects())
<< "Removing owned child at a bad time, which leads to "
"parentless objects at a bad time: "
<< owned_child;
}
MaybeRestoreParentOfOwnedChild(child_axid);
}
}
registered_id_attributes_.erase(obj_id);
}
// Another id owned |obj_id|:
RemoveOwnedRelation(obj_id);
}
void AXRelationCache::RemoveOwnedRelation(AXID obj_id) {
// Another id owned |obj_id|.
if (aria_owned_child_to_owner_mapping_.Contains(obj_id)) {
CHECK(object_cache_->lifecycle().StateAllowsReparentingAXObjects());
// Previous owner no longer relevant to this child.
// Also, remove |obj_id| from previous owner's owned child list:
AXID owner_id = aria_owned_child_to_owner_mapping_.Take(obj_id);
if (aria_owner_to_children_mapping_.Contains(owner_id)) {
const Vector<AXID>& owners_owned_children =
aria_owner_to_children_mapping_.at(owner_id);
for (wtf_size_t index = 0; index < owners_owned_children.size();
index++) {
if (owners_owned_children[index] == obj_id) {
aria_owner_to_children_mapping_.at(owner_id).EraseAt(index);
break;
}
}
} else {
// TODO(crbug.com/437579600) This is not a situation we expect, but it
// also shouldn't cause a renderer crash. Once we have fixed the
// underlying issue and verified that this dump does not exist in
// telemetry, we should upgrade this to a NOTREACHED or remove the
// `Contains(owner_id)` check above.
DUMP_WILL_BE_NOTREACHED() << "Inconsistent aria-owns mapping: owner "
<< owner_id << " not found";
}
if (AXObject* owner = ObjectFromAXID(owner_id)) {
// The child is removed, so the owner needs to make sure its maps
// are updated because it could point to something new or even back to the
// same child if it's recreated, because it still has aria-owns markup.
// The next call AXRelationCache::ProcessUpdatesWithCleanLayout()
// will refresh this owner before the tree is frozen.
owner_axids_to_update_.insert(owner_id);
if (object_cache_->lifecycle().StateAllowsImmediateTreeUpdates()) {
// Currently in CommitAXUpdates(). Changing the children of the owner
// here could interfere with the execution of RemoveSubtree().
object_cache_->MarkAXObjectDirtyWithCleanLayout(owner);
} else {
object_cache_->ChildrenChanged(owner);
}
}
if (AXObject* owned_child = ObjectFromAXID(obj_id)) {
owned_child->DetachFromParent();
}
}
}
AXObject* AXRelationCache::ObjectFromAXID(AXID axid) const {
return object_cache_->ObjectFromAXID(axid);
}
AXObject* AXRelationCache::Get(Node* node) {
return object_cache_->Get(node);
}
AXObject* AXRelationCache::GetOrCreate(Node* node, const AXObject* owner) {
return object_cache_->GetOrCreate(node, const_cast<AXObject*>(owner));
}
void AXRelationCache::ChildrenChangedWithCleanLayout(AXObject* object) {
if (!object) {
return;
}
object->ChildrenChangedWithCleanLayout();
object_cache_->MarkAXObjectDirtyWithCleanLayout(object);
}
Node* AXRelationCache::LabelChanged(HTMLLabelElement& label) {
const auto& id = label.FastGetAttribute(html_names::kForAttr);
if (id.empty()) {
return nullptr;
}
all_previously_seen_label_target_ids_.insert(id);
return label.Control();
}
void AXRelationCache::MaybeRestoreParentOfOwnedChild(AXID removed_child_axid) {
// This works because AXIDs are equal to the DOMNodeID for their DOM nodes.
if (Node* child_node = DOMNodeIds::NodeForId(removed_child_axid)) {
object_cache_->RestoreParentOrPrune(child_node);
// Handle case where there were multiple elements aria-owns=|child|,
// by making sure they are updated in the next round, in case one of them
// can now own it because of the removal the old_parent.
HeapVector<Member<AXObject>> other_potential_owners;
if (Element* child_element = DynamicTo<Element>(child_node)) {
GetRelationSourcesById(child_element->GetIdAttribute(), aria_owns_id_map_,
other_potential_owners);
for (AXObject* other_potential_owner : other_potential_owners) {
owner_axids_to_update_.insert(other_potential_owner->AXObjectID());
}
}
}
}
} // namespace blink