// Copyright 2014 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "ui/accessibility/platform/ax_platform_node_base.h" #include #include #include #include #include #include #include #include "base/no_destructor.h" #include "base/numerics/checked_math.h" #include "base/strings/string_number_conversions.h" #include "base/strings/string_util.h" #include "base/strings/utf_string_conversions.h" #include "build/build_config.h" #include "third_party/skia/include/core/SkColor.h" #include "ui/accessibility/ax_action_data.h" #include "ui/accessibility/ax_enums.mojom.h" #include "ui/accessibility/ax_node_data.h" #include "ui/accessibility/ax_role_properties.h" #include "ui/accessibility/ax_selection.h" #include "ui/accessibility/ax_tree_data.h" #include "ui/accessibility/platform/ax_platform_node_delegate.h" #include "ui/accessibility/platform/compute_attributes.h" #include "ui/gfx/geometry/rect_conversions.h" namespace ui { namespace { using OnNotifyEventCallbackMap = std::map; OnNotifyEventCallbackMap& GetOnNotifyEventCallbackMap() { static base::NoDestructor on_notify_event_for_testing; return *on_notify_event_for_testing; } // Check for descendant comment, using limited depth first search. bool FindDescendantRoleWithMaxDepth(const AXPlatformNodeBase* node, ax::mojom::Role descendant_role, size_t max_depth, size_t max_children_to_check) { if (node->GetRole() == descendant_role) return true; if (max_depth <= 1) return false; size_t num_children_to_check = std::min(node->GetChildCount(), max_children_to_check); for (size_t index = 0; index < num_children_to_check; index++) { auto* child = static_cast( AXPlatformNode::FromNativeViewAccessible(node->ChildAtIndex(index))); if (child && FindDescendantRoleWithMaxDepth(child, descendant_role, max_depth - 1, max_children_to_check)) { return true; } } return false; } } // namespace const char16_t AXPlatformNodeBase::kEmbeddedCharacter = u'\xfffc'; // Map from each AXPlatformNode's unique id to its instance. using UniqueIdMap = std::unordered_map; base::LazyInstance::Leaky g_unique_id_map = LAZY_INSTANCE_INITIALIZER; // TODO(fxbug.dev/91030): Remove the !BUILDFLAG(IS_FUCHSIA) condition once // fuchsia has native accessibility. #if !BUILDFLAG_INTERNAL_HAS_NATIVE_ACCESSIBILITY() && !BUILDFLAG(IS_FUCHSIA) // static AXPlatformNode* AXPlatformNode::Create(AXPlatformNodeDelegate* delegate) { AXPlatformNodeBase* node = new AXPlatformNodeBase(); node->Init(delegate); return node; } #endif // static AXPlatformNode* AXPlatformNodeBase::GetFromUniqueId(int32_t unique_id) { UniqueIdMap* unique_ids = g_unique_id_map.Pointer(); auto iter = unique_ids->find(unique_id); if (iter != unique_ids->end()) return iter->second; return nullptr; } // static size_t AXPlatformNodeBase::GetInstanceCountForTesting() { return g_unique_id_map.Get().size(); } // static void AXPlatformNodeBase::SetOnNotifyEventCallbackForTesting( ax::mojom::Event event_type, base::RepeatingClosure callback) { OnNotifyEventCallbackMap& callback_map = GetOnNotifyEventCallbackMap(); callback_map[event_type] = std::move(callback); } AXPlatformNodeBase::AXPlatformNodeBase() = default; AXPlatformNodeBase::~AXPlatformNodeBase() = default; void AXPlatformNodeBase::Init(AXPlatformNodeDelegate* delegate) { delegate_ = delegate; // This must be called after assigning our delegate. g_unique_id_map.Get()[GetUniqueId()] = this; } const AXNodeData& AXPlatformNodeBase::GetData() const { static const base::NoDestructor empty_data; if (delegate_) return delegate_->GetData(); return *empty_data; } gfx::NativeViewAccessible AXPlatformNodeBase::GetFocus() const { if (delegate_) return delegate_->GetFocus(); return nullptr; } gfx::NativeViewAccessible AXPlatformNodeBase::GetParent() const { if (delegate_) return delegate_->GetParent(); return nullptr; } AXPlatformNodeBase* AXPlatformNodeBase::GetPlatformParent() const { if (delegate_) return FromNativeViewAccessible(delegate_->GetParent()); return nullptr; } AXPlatformNodeBase* AXPlatformNodeBase::GetPlatformTextFieldAncestor() const { if (delegate_) return FromNativeViewAccessible(delegate_->GetTextFieldAncestor()); return nullptr; } size_t AXPlatformNodeBase::GetChildCount() const { if (delegate_) return delegate_->GetChildCount(); return 0; } gfx::NativeViewAccessible AXPlatformNodeBase::ChildAtIndex(size_t index) const { if (delegate_) return delegate_->ChildAtIndex(index); return nullptr; } std::string AXPlatformNodeBase::GetName() const { if (delegate_) { std::string name = delegate_->GetName(); // Compute extra name based on the image annotation (generated alt text) // results. std::string extra_text; ax::mojom::ImageAnnotationStatus status = GetData().GetImageAnnotationStatus(); switch (status) { case ax::mojom::ImageAnnotationStatus::kEligibleForAnnotation: case ax::mojom::ImageAnnotationStatus::kAnnotationPending: case ax::mojom::ImageAnnotationStatus::kAnnotationEmpty: case ax::mojom::ImageAnnotationStatus::kAnnotationAdult: case ax::mojom::ImageAnnotationStatus::kAnnotationProcessFailed: extra_text = base::UTF16ToUTF8( delegate_->GetLocalizedStringForImageAnnotationStatus(status)); break; case ax::mojom::ImageAnnotationStatus::kAnnotationSucceeded: extra_text = GetStringAttribute(ax::mojom::StringAttribute::kImageAnnotation); break; case ax::mojom::ImageAnnotationStatus::kNone: case ax::mojom::ImageAnnotationStatus::kWillNotAnnotateDueToScheme: case ax::mojom::ImageAnnotationStatus::kIneligibleForAnnotation: case ax::mojom::ImageAnnotationStatus::kSilentlyEligibleForAnnotation: break; } if (!extra_text.empty()) { if (!name.empty()) name += ". "; name += extra_text; } DCHECK(base::IsStringUTF8AllowingNoncharacters(name)) << "Invalid UTF8"; return name; } return std::string(); } absl::optional AXPlatformNodeBase::GetIndexInParent() { AXPlatformNodeBase* parent = FromNativeViewAccessible(GetParent()); if (!parent) return absl::nullopt; // If this is the webview, it is not in the child in the list of its parent's // child. // TODO(jkim): Check if we could remove this after making WebView ignored. if (delegate_ && delegate_->GetNativeViewAccessible() != GetNativeViewAccessible()) { return absl::nullopt; } size_t child_count = parent->GetChildCount(); if (child_count == 0) { // |child_count| could be 0 if the parent is IsLeaf. DCHECK(parent->IsLeaf()); return absl::nullopt; } // Ask the delegate for the index in parent, and return it if it's plausible. // // Delegates are allowed to not implement this (ViewsAXPlatformNodeDelegate // returns -1). Also, delegates may not know the correct answer if this // node is the root of a tree that's embedded in another tree, in which // case the delegate should return -1 and we'll compute it. auto index = delegate_ ? delegate_->GetIndexInParent() : absl::nullopt; if (index.has_value() && index.value() < child_count) return index; // Otherwise, search the parent's children. gfx::NativeViewAccessible current = GetNativeViewAccessible(); for (size_t i = 0; i < child_count; i++) { if (parent->ChildAtIndex(i) == current) return i; } // If the parent has a modal dialog, it doesn't count other children. if (parent->delegate_ && parent->delegate_->HasModalDialog()) return absl::nullopt; NOTREACHED() << "Unable to find the child in the list of its parent's children."; return absl::nullopt; } base::stack AXPlatformNodeBase::GetAncestors() { base::stack ancestors; gfx::NativeViewAccessible current_node = GetNativeViewAccessible(); while (current_node) { ancestors.push(current_node); current_node = FromNativeViewAccessible(current_node)->GetParent(); } return ancestors; } absl::optional AXPlatformNodeBase::CompareTo(AXPlatformNodeBase& other) { // We define two node's relative positions in the following way: // 1. this->CompareTo(other) == 0: // - |this| and |other| are the same node. // 2. this->CompareTo(other) < 0: // - |this| is an ancestor of |other|. // - |this|'s first uncommon ancestor comes before |other|'s first uncommon // ancestor. The first uncommon ancestor is defined as the immediate child // of the lowest common anestor of the two nodes. The first uncommon // ancestor of |this| and |other| share the same parent (i.e. lowest common // ancestor), so we can just compare the first uncommon ancestors' child // indices to determine their relative positions. // 3. this->CompareTo(other) == nullopt: // - |this| and |other| are not comparable. E.g. they do not have a common // ancestor. // // Another way to look at the nodes' relative positions/logical orders is that // they are equivalent to pre-order traversal of the tree. If we pre-order // traverse from the root, the node that we visited earlier is always going to // be before (logically less) the node we visit later. if (this == &other) return absl::optional(0); // Compute the ancestor stacks of both positions and traverse them from the // top most ancestor down, so we can discover the first uncommon ancestors. // The first uncommon ancestor is the immediate child of the lowest common // ancestor. gfx::NativeViewAccessible common_ancestor = nullptr; base::stack our_ancestors = GetAncestors(); base::stack other_ancestors = other.GetAncestors(); // Start at the root and traverse down. Keep going until the |this|'s ancestor // chain and |other|'s ancestor chain disagree. The last node before they // disagree is the lowest common ancestor. while (!our_ancestors.empty() && !other_ancestors.empty() && our_ancestors.top() == other_ancestors.top()) { common_ancestor = our_ancestors.top(); our_ancestors.pop(); other_ancestors.pop(); } // Nodes do not have a common ancestor, they are not comparable. if (!common_ancestor) return absl::nullopt; // Compute the logical order when the common ancestor is |this| or |other|. auto* common_ancestor_platform_node = FromNativeViewAccessible(common_ancestor); if (common_ancestor_platform_node == this) return absl::optional(-1); if (common_ancestor_platform_node == &other) return absl::optional(1); // Compute the logical order of |this| and |other| by using their first // uncommon ancestors. if (!our_ancestors.empty() && !other_ancestors.empty()) { absl::optional this_index_in_parent = FromNativeViewAccessible(our_ancestors.top())->GetIndexInParent(); absl::optional other_index_in_parent = FromNativeViewAccessible(other_ancestors.top())->GetIndexInParent(); if (!this_index_in_parent || !other_index_in_parent) return absl::nullopt; int this_uncommon_ancestor_index = this_index_in_parent.value(); int other_uncommon_ancestor_index = other_index_in_parent.value(); DCHECK_NE(this_uncommon_ancestor_index, other_uncommon_ancestor_index) << "Deepest uncommon ancestors should truly be uncommon, i.e. not " "the same."; return absl::optional(this_uncommon_ancestor_index - other_uncommon_ancestor_index); } return absl::nullopt; } AXNodeID AXPlatformNodeBase::GetNodeId() const { if (!delegate_) return kInvalidAXNodeID; return delegate_->GetData().id; } AXPlatformNodeBase* AXPlatformNodeBase::GetActiveDescendant() const { if (!delegate_) return nullptr; AXNodeID active_descendant_id; AXPlatformNodeBase* active_descendant = nullptr; if (GetIntAttribute(ax::mojom::IntAttribute::kActivedescendantId, &active_descendant_id)) { active_descendant = static_cast( delegate_->GetFromNodeID(active_descendant_id)); } if (GetRole() == ax::mojom::Role::kComboBoxSelect) { AXPlatformNodeBase* child = GetFirstChild(); if (child && child->GetRole() == ax::mojom::Role::kMenuListPopup && !child->IsInvisibleOrIgnored()) { // The active descendant is found on the menu list popup, i.e. on the // actual list and not on the button that opens it. // If there is no active descendant, focus should stay on the button so // that Windows screen readers would enable their virtual cursor. // Do not expose an activedescendant in a hidden/collapsed list, as // screen readers expect the focus event to go to the button itself. // Note that the AX hierarchy in this case is strange -- the active // option is the only visible option, and is inside an invisible list. if (child->GetIntAttribute(ax::mojom::IntAttribute::kActivedescendantId, &active_descendant_id)) { active_descendant = static_cast( child->delegate_->GetFromNodeID(active_descendant_id)); } } } if (active_descendant && !active_descendant->IsInvisibleOrIgnored()) return active_descendant; return nullptr; } // AXPlatformNode overrides. void AXPlatformNodeBase::Destroy() { g_unique_id_map.Get().erase(GetUniqueId()); AXPlatformNode::Destroy(); delegate_ = nullptr; Dispose(); } void AXPlatformNodeBase::Dispose() { delete this; } gfx::NativeViewAccessible AXPlatformNodeBase::GetNativeViewAccessible() { return nullptr; } void AXPlatformNodeBase::NotifyAccessibilityEvent(ax::mojom::Event event_type) { OnNotifyEventCallbackMap& callback_map = GetOnNotifyEventCallbackMap(); if (callback_map.find(event_type) != callback_map.end() && callback_map[event_type]) { callback_map[event_type].Run(); } } #if BUILDFLAG(IS_APPLE) void AXPlatformNodeBase::AnnounceText(const std::u16string& text) {} #endif AXPlatformNodeDelegate* AXPlatformNodeBase::GetDelegate() const { return delegate_; } bool AXPlatformNodeBase::IsDescendantOf(AXPlatformNode* ancestor) const { if (!ancestor) return false; if (this == ancestor) return true; AXPlatformNodeBase* parent = FromNativeViewAccessible(GetParent()); if (!parent) return false; return parent->IsDescendantOf(ancestor); } AXPlatformNodeBase::AXPlatformNodeChildIterator AXPlatformNodeBase::AXPlatformNodeChildrenBegin() const { return AXPlatformNodeChildIterator(this, GetFirstChild()); } AXPlatformNodeBase::AXPlatformNodeChildIterator AXPlatformNodeBase::AXPlatformNodeChildrenEnd() const { return AXPlatformNodeChildIterator(this, nullptr); } // Helpers. AXPlatformNodeBase* AXPlatformNodeBase::GetPreviousSibling() const { if (!delegate_) return nullptr; return FromNativeViewAccessible(delegate_->GetPreviousSibling()); } AXPlatformNodeBase* AXPlatformNodeBase::GetNextSibling() const { if (!delegate_) return nullptr; return FromNativeViewAccessible(delegate_->GetNextSibling()); } AXPlatformNodeBase* AXPlatformNodeBase::GetFirstChild() const { if (!delegate_) return nullptr; return FromNativeViewAccessible(delegate_->GetFirstChild()); } AXPlatformNodeBase* AXPlatformNodeBase::GetLastChild() const { if (!delegate_) return nullptr; return FromNativeViewAccessible(delegate_->GetLastChild()); } bool AXPlatformNodeBase::IsDescendant(AXPlatformNodeBase* node) { if (!delegate_) return false; if (!node) return false; if (node == this) return true; gfx::NativeViewAccessible native_parent = node->GetParent(); if (!native_parent) return false; AXPlatformNodeBase* parent = FromNativeViewAccessible(native_parent); return IsDescendant(parent); } ax::mojom::Role AXPlatformNodeBase::GetRole() const { if (!delegate_) return ax::mojom::Role::kUnknown; return delegate_->GetRole(); } bool AXPlatformNodeBase::HasBoolAttribute( ax::mojom::BoolAttribute attribute) const { if (!delegate_) return false; return delegate_->HasBoolAttribute(attribute); } bool AXPlatformNodeBase::GetBoolAttribute( ax::mojom::BoolAttribute attribute) const { if (!delegate_) return false; return delegate_->GetBoolAttribute(attribute); } bool AXPlatformNodeBase::GetBoolAttribute(ax::mojom::BoolAttribute attribute, bool* value) const { if (!delegate_) return false; return delegate_->GetBoolAttribute(attribute, value); } bool AXPlatformNodeBase::HasFloatAttribute( ax::mojom::FloatAttribute attribute) const { if (!delegate_) return false; return delegate_->HasFloatAttribute(attribute); } float AXPlatformNodeBase::GetFloatAttribute( ax::mojom::FloatAttribute attribute) const { if (!delegate_) return 0.0f; return delegate_->GetFloatAttribute(attribute); } bool AXPlatformNodeBase::GetFloatAttribute(ax::mojom::FloatAttribute attribute, float* value) const { if (!delegate_) return false; return delegate_->GetFloatAttribute(attribute, value); } const std::vector>& AXPlatformNodeBase::GetIntAttributes() const { static const base::NoDestructor< const std::vector>> empty_data; if (!delegate_) return *empty_data; return delegate_->GetIntAttributes(); } bool AXPlatformNodeBase::HasIntAttribute( ax::mojom::IntAttribute attribute) const { if (!delegate_) return false; return delegate_->HasIntAttribute(attribute); } int AXPlatformNodeBase::GetIntAttribute( ax::mojom::IntAttribute attribute) const { if (!delegate_) return 0; return delegate_->GetIntAttribute(attribute); } bool AXPlatformNodeBase::GetIntAttribute(ax::mojom::IntAttribute attribute, int* value) const { if (!delegate_) return false; return delegate_->GetIntAttribute(attribute, value); } const std::vector>& AXPlatformNodeBase::GetStringAttributes() const { static const base::NoDestructor< const std::vector>> empty_data; if (!delegate_) return *empty_data; return delegate_->GetStringAttributes(); } bool AXPlatformNodeBase::HasStringAttribute( ax::mojom::StringAttribute attribute) const { if (!delegate_) return false; return delegate_->HasStringAttribute(attribute); } const std::string& AXPlatformNodeBase::GetStringAttribute( ax::mojom::StringAttribute attribute) const { if (!delegate_) return base::EmptyString(); return delegate_->GetStringAttribute(attribute); } bool AXPlatformNodeBase::GetStringAttribute( ax::mojom::StringAttribute attribute, std::string* value) const { if (!delegate_) return false; return delegate_->GetStringAttribute(attribute, value); } std::u16string AXPlatformNodeBase::GetString16Attribute( ax::mojom::StringAttribute attribute) const { if (!delegate_) return std::u16string(); return delegate_->GetString16Attribute(attribute); } bool AXPlatformNodeBase::GetString16Attribute( ax::mojom::StringAttribute attribute, std::u16string* value) const { if (!delegate_) return false; return delegate_->GetString16Attribute(attribute, value); } bool AXPlatformNodeBase::HasInheritedStringAttribute( ax::mojom::StringAttribute attribute) const { const AXPlatformNodeBase* current_node = this; do { if (!current_node->delegate_) { return false; } if (current_node->HasStringAttribute(attribute)) { return true; } current_node = FromNativeViewAccessible(current_node->GetParent()); } while (current_node); return false; } const std::string& AXPlatformNodeBase::GetInheritedStringAttribute( ax::mojom::StringAttribute attribute) const { // TODO(nektar): Switch to using `AXNode::GetInheritedStringAttribute` after // it has been modified to cross tree boundaries. const AXPlatformNodeBase* current_node = this; do { if (!current_node->delegate_) { return base::EmptyString(); } if (current_node->HasStringAttribute(attribute)) { return current_node->GetStringAttribute(attribute); } current_node = FromNativeViewAccessible(current_node->GetParent()); } while (current_node); return base::EmptyString(); } bool AXPlatformNodeBase::GetInheritedStringAttribute( ax::mojom::StringAttribute attribute, std::string* value) const { // TODO(nektar): Switch to using `AXNode::GetInheritedStringAttribute` after // it has been modified to cross tree boundaries. const AXPlatformNodeBase* current_node = this; do { if (!current_node->delegate_) { return false; } if (current_node->GetStringAttribute(attribute, value)) { return true; } current_node = FromNativeViewAccessible(current_node->GetParent()); } while (current_node); return false; } std::u16string AXPlatformNodeBase::GetInheritedString16Attribute( ax::mojom::StringAttribute attribute) const { // TODO(nektar): Switch to using `AXNode::GetInheritedString16Attribute` after // it has been modified to cross tree boundaries. return base::UTF8ToUTF16(GetInheritedStringAttribute(attribute)); } bool AXPlatformNodeBase::GetInheritedString16Attribute( ax::mojom::StringAttribute attribute, std::u16string* value) const { // TODO(nektar): Switch to using `AXNode::GetInheritedString16Attribute` after // it has been modified to cross tree boundaries. std::string value_utf8; if (!GetInheritedStringAttribute(attribute, &value_utf8)) return false; *value = base::UTF8ToUTF16(value_utf8); return true; } const std::vector>>& AXPlatformNodeBase::GetIntListAttributes() const { static const base::NoDestructor>>> empty_data; if (!delegate_) return *empty_data; return delegate_->GetIntListAttributes(); } bool AXPlatformNodeBase::HasIntListAttribute( ax::mojom::IntListAttribute attribute) const { if (!delegate_) return false; return delegate_->HasIntListAttribute(attribute); } const std::vector& AXPlatformNodeBase::GetIntListAttribute( ax::mojom::IntListAttribute attribute) const { static const base::NoDestructor> empty_data; if (!delegate_) return *empty_data; return delegate_->GetIntListAttribute(attribute); } bool AXPlatformNodeBase::GetIntListAttribute( ax::mojom::IntListAttribute attribute, std::vector* value) const { if (!delegate_) return false; return delegate_->GetIntListAttribute(attribute, value); } bool AXPlatformNodeBase::HasStringListAttribute( ax::mojom::StringListAttribute attribute) const { if (!delegate_) return false; return delegate_->HasStringListAttribute(attribute); } const std::vector& AXPlatformNodeBase::GetStringListAttribute( ax::mojom::StringListAttribute attribute) const { static const base::NoDestructor> empty_data; if (!delegate_) return *empty_data; return delegate_->GetStringListAttribute(attribute); } bool AXPlatformNodeBase::GetStringListAttribute( ax::mojom::StringListAttribute attribute, std::vector* value) const { if (!delegate_) return false; return delegate_->GetStringListAttribute(attribute, value); } bool AXPlatformNodeBase::HasHtmlAttribute(const char* attribute) const { if (!delegate_) return false; return delegate_->HasHtmlAttribute(attribute); } const base::StringPairs& AXPlatformNodeBase::GetHtmlAttributes() const { static const base::NoDestructor empty_data; if (!delegate_) return *empty_data; return delegate_->GetHtmlAttributes(); } bool AXPlatformNodeBase::GetHtmlAttribute(const char* attribute, std::string* value) const { if (!delegate_) return false; return delegate_->GetHtmlAttribute(attribute, value); } bool AXPlatformNodeBase::GetHtmlAttribute(const char* attribute, std::u16string* value) const { if (!delegate_) return false; return delegate_->GetHtmlAttribute(attribute, value); } AXTextAttributes AXPlatformNodeBase::GetTextAttributes() const { if (!delegate_) return AXTextAttributes(); return delegate_->GetTextAttributes(); } bool AXPlatformNodeBase::HasState(ax::mojom::State state) const { if (!delegate_) return false; return delegate_->HasState(state); } ax::mojom::State AXPlatformNodeBase::GetState() const { if (!delegate_) return ax::mojom::State::kNone; return delegate_->GetState(); } bool AXPlatformNodeBase::HasAction(ax::mojom::Action action) const { if (!delegate_) return false; return delegate_->HasAction(action); } bool AXPlatformNodeBase::HasTextStyle(ax::mojom::TextStyle text_style) const { if (!delegate_) return false; return delegate_->HasTextStyle(text_style); } ax::mojom::NameFrom AXPlatformNodeBase::GetNameFrom() const { if (!delegate_) return ax::mojom::NameFrom::kNone; return delegate_->GetNameFrom(); } bool AXPlatformNodeBase::HasNameFromOtherElement() const { ax::mojom::NameFrom nameFrom = GetNameFrom(); return nameFrom == ax::mojom::NameFrom::kCaption || nameFrom == ax::mojom::NameFrom::kRelatedElement; } // static AXPlatformNodeBase* AXPlatformNodeBase::FromNativeViewAccessible( gfx::NativeViewAccessible accessible) { return static_cast( AXPlatformNode::FromNativeViewAccessible(accessible)); } bool AXPlatformNodeBase::SetHypertextSelection(int start_offset, int end_offset) { if (!delegate_) return false; return delegate_->SetHypertextSelection(start_offset, end_offset); } bool AXPlatformNodeBase::IsPlatformDocument() const { return delegate_ && delegate_->IsPlatformDocument(); } bool AXPlatformNodeBase::IsStructuredAnnotation() const { // The node represents a structured annotation if it can trace back to a // target node that is being annotated. std::set reverse_relations = GetDelegate()->GetReverseRelations( ax::mojom::IntListAttribute::kDetailsIds); return !reverse_relations.empty(); } bool AXPlatformNodeBase::IsSelectionItemSupported() const { switch (GetRole()) { // An ARIA 1.1+ role of "cell", or a role of "row" inside // an ARIA 1.1 role of "table", should not be selectable. // ARIA "table" is not interactable, ARIA "grid" is. case ax::mojom::Role::kCell: case ax::mojom::Role::kColumnHeader: case ax::mojom::Role::kRow: case ax::mojom::Role::kRowHeader: { // An ARIA grid subwidget is only selectable if explicitly marked as // selected (or not) with the aria-selected property. if (!HasBoolAttribute(ax::mojom::BoolAttribute::kSelected)) return false; AXPlatformNodeBase* table = GetTable(); if (!table) return false; return table->GetRole() == ax::mojom::Role::kGrid || table->GetRole() == ax::mojom::Role::kTreeGrid; } // https://www.w3.org/TR/core-aam-1.1/#mapping_state-property_table // SelectionItem.IsSelected is exposed when aria-checked is True or False, // for 'radio' and 'menuitemradio' roles. case ax::mojom::Role::kRadioButton: case ax::mojom::Role::kMenuItemRadio: { if (GetData().GetCheckedState() == ax::mojom::CheckedState::kTrue || GetData().GetCheckedState() == ax::mojom::CheckedState::kFalse) return true; return false; } // https://www.w3.org/TR/wai-aria-1.1/#aria-selected // SelectionItem.IsSelected is exposed when aria-select is True or False. case ax::mojom::Role::kListBoxOption: case ax::mojom::Role::kListItem: case ax::mojom::Role::kMenuListOption: case ax::mojom::Role::kTab: case ax::mojom::Role::kTreeItem: return HasBoolAttribute(ax::mojom::BoolAttribute::kSelected); default: return false; } } bool AXPlatformNodeBase::IsTextField() const { return GetData().IsTextField(); } bool AXPlatformNodeBase::IsAtomicTextField() const { return GetData().IsAtomicTextField(); } bool AXPlatformNodeBase::IsNonAtomicTextField() const { return GetData().IsNonAtomicTextField(); } bool AXPlatformNodeBase::IsText() const { return delegate_ && delegate_->IsText(); } std::u16string AXPlatformNodeBase::GetHypertext() const { if (!delegate_) return std::u16string(); // Hypertext of platform leaves, which internally are composite objects, are // represented with the text content of the internal composite object. These // don't exist on non-web content. if (IsChildOfLeaf()) return GetTextContentUTF16(); if (hypertext_.needs_update) UpdateComputedHypertext(); return hypertext_.hypertext; } std::u16string AXPlatformNodeBase::GetTextContentUTF16() const { if (!delegate_) return std::u16string(); return delegate_->GetTextContentUTF16(); } std::u16string AXPlatformNodeBase::GetRoleDescriptionFromImageAnnotationStatusOrFromAttribute() const { if (GetRole() == ax::mojom::Role::kImage && (GetData().GetImageAnnotationStatus() == ax::mojom::ImageAnnotationStatus::kEligibleForAnnotation || GetData().GetImageAnnotationStatus() == ax::mojom::ImageAnnotationStatus::kSilentlyEligibleForAnnotation)) { return GetDelegate()->GetLocalizedRoleDescriptionForUnlabeledImage(); } return GetString16Attribute(ax::mojom::StringAttribute::kRoleDescription); } std::u16string AXPlatformNodeBase::GetRoleDescription() const { std::u16string role_description = GetRoleDescriptionFromImageAnnotationStatusOrFromAttribute(); if (!role_description.empty()) { return role_description; } return GetDelegate()->GetLocalizedStringForRoleDescription(); } bool AXPlatformNodeBase::IsImageWithMap() const { DCHECK_EQ(GetRole(), ax::mojom::Role::kImage) << "Only call IsImageWithMap() on an image"; return GetChildCount(); } AXPlatformNodeBase* AXPlatformNodeBase::GetSelectionContainer() const { if (!delegate_) return nullptr; return FromNativeViewAccessible(delegate_->GetSelectionContainer()); } AXPlatformNodeBase* AXPlatformNodeBase::GetTable() const { if (!delegate_) return nullptr; return FromNativeViewAccessible(delegate_->GetTableAncestor()); } AXPlatformNodeBase* AXPlatformNodeBase::GetTableCaption() const { if (!delegate_) return nullptr; AXPlatformNodeBase* table = GetTable(); if (!table) return nullptr; DCHECK(table->delegate_); return static_cast(table->delegate_->GetTableCaption()); } AXPlatformNodeBase* AXPlatformNodeBase::GetTableCell(int index) const { if (!delegate_) return nullptr; if (!IsTableLike(GetRole()) && !IsCellOrTableHeader(GetRole())) return nullptr; AXPlatformNodeBase* table = GetTable(); if (!table) return nullptr; DCHECK(table->delegate_); absl::optional cell_id = table->delegate_->CellIndexToId(index); if (!cell_id) return nullptr; return static_cast( table->delegate_->GetFromNodeID(*cell_id)); } AXPlatformNodeBase* AXPlatformNodeBase::GetTableCell(int row, int column) const { if (!IsTableLike(GetRole()) && !IsCellOrTableHeader(GetRole())) return nullptr; AXPlatformNodeBase* table = GetTable(); if (!table || !GetTableRowCount() || !GetTableColumnCount()) return nullptr; if (row < 0 || row >= *GetTableRowCount() || column < 0 || column >= *GetTableColumnCount()) { return nullptr; } DCHECK(table->delegate_); absl::optional cell_id = table->delegate_->GetCellId(row, column); if (!cell_id) return nullptr; return static_cast( table->delegate_->GetFromNodeID(*cell_id)); } absl::optional AXPlatformNodeBase::GetTableCellIndex() const { if (!delegate_) return absl::nullopt; return delegate_->GetTableCellIndex(); } absl::optional AXPlatformNodeBase::GetTableColumn() const { if (!delegate_) return absl::nullopt; return delegate_->GetTableCellColIndex(); } absl::optional AXPlatformNodeBase::GetTableColumnCount() const { if (!delegate_) return absl::nullopt; AXPlatformNodeBase* table = GetTable(); if (!table) return absl::nullopt; DCHECK(table->delegate_); return table->delegate_->GetTableColCount(); } absl::optional AXPlatformNodeBase::GetTableAriaColumnCount() const { if (!delegate_) return absl::nullopt; AXPlatformNodeBase* table = GetTable(); if (!table) return absl::nullopt; DCHECK(table->delegate_); return table->delegate_->GetTableAriaColCount(); } absl::optional AXPlatformNodeBase::GetTableColumnSpan() const { if (!delegate_) return absl::nullopt; return delegate_->GetTableCellColSpan(); } absl::optional AXPlatformNodeBase::GetTableRow() const { if (!delegate_) return absl::nullopt; if (delegate_->IsTableRow()) return delegate_->GetTableRowRowIndex(); if (delegate_->IsTableCellOrHeader()) return delegate_->GetTableCellRowIndex(); return absl::nullopt; } absl::optional AXPlatformNodeBase::GetTableRowCount() const { if (!delegate_) return absl::nullopt; AXPlatformNodeBase* table = GetTable(); if (!table) return absl::nullopt; DCHECK(table->delegate_); return table->delegate_->GetTableRowCount(); } absl::optional AXPlatformNodeBase::GetTableAriaRowCount() const { if (!delegate_) return absl::nullopt; AXPlatformNodeBase* table = GetTable(); if (!table) return absl::nullopt; DCHECK(table->delegate_); return table->delegate_->GetTableAriaRowCount(); } absl::optional AXPlatformNodeBase::GetTableRowSpan() const { if (!delegate_) return absl::nullopt; return delegate_->GetTableCellRowSpan(); } absl::optional AXPlatformNodeBase::GetFontSizeInPoints() const { float font_size; // Attribute has no default value. if (GetFloatAttribute(ax::mojom::FloatAttribute::kFontSize, &font_size)) { // The IA2 Spec requires the value to be in pt, not in pixels. // There are 72 points per inch. // We assume that there are 96 pixels per inch on a standard display. // TODO(nektar): Figure out the current value of pixels per inch. float points = font_size * 72.0 / 96.0; // Round to the nearest 0.5 points. points = std::round(points * 2.0) / 2.0; return points; } return absl::nullopt; } bool AXPlatformNodeBase::HasVisibleCaretOrSelection() const { return delegate_ && delegate_->HasVisibleCaretOrSelection(); } bool AXPlatformNodeBase::IsLeaf() const { return delegate_ && delegate_->IsLeaf(); } bool AXPlatformNodeBase::IsChildOfLeaf() const { return delegate_ && delegate_->IsChildOfLeaf(); } bool AXPlatformNodeBase::IsInvisibleOrIgnored() const { if (!GetData().IsInvisibleOrIgnored()) return false; if (HasState(ax::mojom::State::kFocusable)) return !IsFocused(); return !HasVisibleCaretOrSelection(); } bool AXPlatformNodeBase::IsFocused() const { return delegate_ && FromNativeViewAccessible(delegate_->GetFocus()) == this; } bool AXPlatformNodeBase::IsScrollable() const { return (HasIntAttribute(ax::mojom::IntAttribute::kScrollXMin) && HasIntAttribute(ax::mojom::IntAttribute::kScrollXMax) && HasIntAttribute(ax::mojom::IntAttribute::kScrollX)) || (HasIntAttribute(ax::mojom::IntAttribute::kScrollYMin) && HasIntAttribute(ax::mojom::IntAttribute::kScrollYMax) && HasIntAttribute(ax::mojom::IntAttribute::kScrollY)); } bool AXPlatformNodeBase::IsHorizontallyScrollable() const { DCHECK_GE(GetIntAttribute(ax::mojom::IntAttribute::kScrollXMin), 0) << "Pixel sizes should be non-negative."; DCHECK_GE(GetIntAttribute(ax::mojom::IntAttribute::kScrollXMax), 0) << "Pixel sizes should be non-negative."; return IsScrollable() && GetIntAttribute(ax::mojom::IntAttribute::kScrollXMin) < GetIntAttribute(ax::mojom::IntAttribute::kScrollXMax); } bool AXPlatformNodeBase::IsVerticallyScrollable() const { DCHECK_GE(GetIntAttribute(ax::mojom::IntAttribute::kScrollYMin), 0) << "Pixel sizes should be non-negative."; DCHECK_GE(GetIntAttribute(ax::mojom::IntAttribute::kScrollYMax), 0) << "Pixel sizes should be non-negative."; return IsScrollable() && GetIntAttribute(ax::mojom::IntAttribute::kScrollYMin) < GetIntAttribute(ax::mojom::IntAttribute::kScrollYMax); } std::u16string AXPlatformNodeBase::GetValueForControl() const { if (!delegate_) return std::u16string(); return delegate_->GetValueForControl(); } void AXPlatformNodeBase::ComputeAttributes(PlatformAttributeList* attributes) { DCHECK(delegate_) << "Many attributes need to be retrieved from our " "AXPlatformNodeDelegate."; // Expose some HTML and ARIA attributes in the IAccessible2 attributes string // "display", "tag", and "xml-roles" have somewhat unusual names for // historical reasons. Aside from that virtually every ARIA attribute // is exposed in a really straightforward way, i.e. "aria-foo" is exposed // as "foo". AddAttributeToList(ax::mojom::StringAttribute::kDisplay, "display", attributes); AddAttributeToList(ax::mojom::StringAttribute::kHtmlTag, "tag", attributes); AddAttributeToList(ax::mojom::StringAttribute::kRole, "xml-roles", attributes); AddAttributeToList(ax::mojom::StringAttribute::kPlaceholder, "placeholder", attributes); AddAttributeToList(ax::mojom::StringAttribute::kAutoComplete, "autocomplete", attributes); if (!HasStringAttribute(ax::mojom::StringAttribute::kAutoComplete) && HasState(ax::mojom::State::kAutofillAvailable)) { AddAttributeToList("autocomplete", "list", attributes); } std::u16string role_description = GetRoleDescriptionFromImageAnnotationStatusOrFromAttribute(); if (!role_description.empty() || HasStringAttribute(ax::mojom::StringAttribute::kRoleDescription)) { AddAttributeToList("roledescription", base::UTF16ToUTF8(role_description), attributes); } // Expose description-from and description. int desc_from; if (GetIntAttribute(ax::mojom::IntAttribute::kDescriptionFrom, &desc_from)) { std::string from; switch (static_cast(desc_from)) { case ax::mojom::DescriptionFrom::kAriaDescription: // Descriptions are exposed via each platform's usual description field. // Also, only aria-description is exposed via the "description" object // attribute, in order to match Firefox. AddAttributeToList(ax::mojom::StringAttribute::kDescription, "description", attributes); from = "aria-description"; break; case ax::mojom::DescriptionFrom::kButtonLabel: from = "button-label"; break; case ax::mojom::DescriptionFrom::kRelatedElement: // aria-describedby=tooltip is mapped to "tooltip". from = IsDescribedByTooltip() ? "tooltip" : "aria-describedby"; break; case ax::mojom::DescriptionFrom::kRubyAnnotation: from = "ruby-annotation"; break; case ax::mojom::DescriptionFrom::kSummary: from = "summary"; break; case ax::mojom::DescriptionFrom::kSvgDescElement: from = "svg-desc-element"; break; case ax::mojom::DescriptionFrom::kTableCaption: from = "table-caption"; break; case ax::mojom::DescriptionFrom::kTitle: case ax::mojom::DescriptionFrom::kPopupElement: // The following types of markup are mapped to "tooltip": // * The title attribute. // * A related popup=hint related via popuptoggletarget / // popupshowtarget / popuphidetarget. // * A tooltip related via aria-describedby (see kRelatedElement above). from = "tooltip"; break; case ax::mojom::DescriptionFrom::kNone: case ax::mojom::DescriptionFrom::kAttributeExplicitlyEmpty: NOTREACHED(); } DCHECK(!from.empty()); AddAttributeToList("description-from", from, attributes); } AddAttributeToList(ax::mojom::StringAttribute::kAriaBrailleLabel, "braillelabel", attributes); AddAttributeToList(ax::mojom::StringAttribute::kAriaBrailleRoleDescription, "brailleroledescription", attributes); AddAttributeToList(ax::mojom::StringAttribute::kKeyShortcuts, "keyshortcuts", attributes); AddAttributeToList(ax::mojom::IntAttribute::kHierarchicalLevel, "level", attributes); AddAttributeToList(ax::mojom::IntAttribute::kSetSize, "setsize", attributes); AddAttributeToList(ax::mojom::IntAttribute::kPosInSet, "posinset", attributes); if (IsPlatformCheckable()) AddAttributeToList("checkable", "true", attributes); if (IsInvisibleOrIgnored()) // Note: NVDA prefers this over INVISIBLE state. AddAttributeToList("hidden", "true", attributes); // Expose live region attributes. AddAttributeToList(ax::mojom::StringAttribute::kLiveStatus, "live", attributes); AddAttributeToList(ax::mojom::StringAttribute::kLiveRelevant, "relevant", attributes); AddAttributeToList(ax::mojom::BoolAttribute::kLiveAtomic, "atomic", attributes); // Busy is usually associated with live regions but can occur anywhere: AddAttributeToList(ax::mojom::BoolAttribute::kBusy, "busy", attributes); // Expose container live region attributes. AddAttributeToList(ax::mojom::StringAttribute::kContainerLiveStatus, "container-live", attributes); AddAttributeToList(ax::mojom::StringAttribute::kContainerLiveRelevant, "container-relevant", attributes); AddAttributeToList(ax::mojom::BoolAttribute::kContainerLiveAtomic, "container-atomic", attributes); AddAttributeToList(ax::mojom::BoolAttribute::kContainerLiveBusy, "container-busy", attributes); // Expose the non-standard explicit-name IA2 attribute. int name_from; if (GetIntAttribute(ax::mojom::IntAttribute::kNameFrom, &name_from) && name_from != static_cast(ax::mojom::NameFrom::kContents)) { AddAttributeToList("explicit-name", "true", attributes); } // Expose the aria-haspopup attribute. int32_t has_popup; if (GetIntAttribute(ax::mojom::IntAttribute::kHasPopup, &has_popup)) { switch (static_cast(has_popup)) { case ax::mojom::HasPopup::kFalse: break; case ax::mojom::HasPopup::kTrue: AddAttributeToList("haspopup", "true", attributes); break; case ax::mojom::HasPopup::kMenu: AddAttributeToList("haspopup", "menu", attributes); break; case ax::mojom::HasPopup::kListbox: AddAttributeToList("haspopup", "listbox", attributes); break; case ax::mojom::HasPopup::kTree: AddAttributeToList("haspopup", "tree", attributes); break; case ax::mojom::HasPopup::kGrid: AddAttributeToList("haspopup", "grid", attributes); break; case ax::mojom::HasPopup::kDialog: AddAttributeToList("haspopup", "dialog", attributes); break; } } else if (HasState(ax::mojom::State::kAutofillAvailable)) { AddAttributeToList("haspopup", "menu", attributes); } // Expose the aria-current attribute. int32_t aria_current_state; if (GetIntAttribute(ax::mojom::IntAttribute::kAriaCurrentState, &aria_current_state)) { switch (static_cast(aria_current_state)) { case ax::mojom::AriaCurrentState::kNone: break; case ax::mojom::AriaCurrentState::kFalse: AddAttributeToList("current", "false", attributes); break; case ax::mojom::AriaCurrentState::kTrue: AddAttributeToList("current", "true", attributes); break; case ax::mojom::AriaCurrentState::kPage: AddAttributeToList("current", "page", attributes); break; case ax::mojom::AriaCurrentState::kStep: AddAttributeToList("current", "step", attributes); break; case ax::mojom::AriaCurrentState::kLocation: AddAttributeToList("current", "location", attributes); break; case ax::mojom::AriaCurrentState::kDate: AddAttributeToList("current", "date", attributes); break; case ax::mojom::AriaCurrentState::kTime: AddAttributeToList("current", "time", attributes); break; } } // Expose table cell index. if (IsCellOrTableHeader(GetRole())) { absl::optional index = delegate_->GetTableCellIndex(); if (index) { std::string str_index(base::NumberToString(*index)); AddAttributeToList("table-cell-index", str_index, attributes); } } if (GetRole() == ax::mojom::Role::kLayoutTable) AddAttributeToList("layout-guess", "true", attributes); // Expose aria-colcount and aria-rowcount in a table, grid or treegrid if they // are different from its physical dimensions. if (IsTableLike(GetRole()) && (delegate_->GetTableAriaRowCount() != delegate_->GetTableRowCount() || delegate_->GetTableAriaColCount() != delegate_->GetTableColCount())) { AddAttributeToList(ax::mojom::IntAttribute::kAriaColumnCount, "colcount", attributes); AddAttributeToList(ax::mojom::IntAttribute::kAriaRowCount, "rowcount", attributes); } if (IsCellOrTableHeader(GetRole()) || IsTableRow(GetRole())) { // Expose aria-colindex and aria-rowindex in a cell or row only if they are // different from the table's physical coordinates. // Note: aria-col/rowindex is 1 based where as table's physical coordinates // are 0 based, so we subtract aria-col/rowindex by 1 to compare with // table's physical coordinates. absl::optional aria_rowindex = delegate_->GetTableCellAriaRowIndex(); absl::optional physical_rowindex = delegate_->GetTableCellRowIndex(); absl::optional aria_colindex = delegate_->GetTableCellAriaColIndex(); absl::optional physical_colindex = delegate_->GetTableCellColIndex(); if (aria_rowindex && physical_rowindex && aria_rowindex.value() - 1 != physical_rowindex.value()) { AddAttributeToList(ax::mojom::IntAttribute::kAriaCellRowIndex, "rowindex", attributes); } if (!IsTableRow(GetRole()) && aria_colindex && physical_colindex && aria_colindex.value() - 1 != physical_colindex.value()) { AddAttributeToList(ax::mojom::IntAttribute::kAriaCellColumnIndex, "colindex", attributes); } // Experimental: expose aria-rowtext / aria-coltext. Not standardized // yet, but obscure enough that it's safe to expose. // http://crbug.com/791634 for (const auto& attribute_value : GetHtmlAttributes()) { const std::string& attr = attribute_value.first; const std::string& value = attribute_value.second; if (attr == "aria-coltext") { AddAttributeToList("coltext", value, attributes); } if (attr == "aria-rowtext") { AddAttributeToList("rowtext", value, attributes); } } } // Expose row or column header sort direction. int32_t sort_direction; if (IsTableHeader(GetRole()) && GetIntAttribute(ax::mojom::IntAttribute::kSortDirection, &sort_direction)) { switch (static_cast(sort_direction)) { case ax::mojom::SortDirection::kNone: break; case ax::mojom::SortDirection::kUnsorted: AddAttributeToList("sort", "none", attributes); break; case ax::mojom::SortDirection::kAscending: AddAttributeToList("sort", "ascending", attributes); break; case ax::mojom::SortDirection::kDescending: AddAttributeToList("sort", "descending", attributes); break; case ax::mojom::SortDirection::kOther: AddAttributeToList("sort", "other", attributes); break; } } if (IsCellOrTableHeader(GetRole())) { // Expose colspan attribute. std::string colspan; if (GetHtmlAttribute("aria-colspan", &colspan)) { AddAttributeToList("colspan", colspan, attributes); } // Expose rowspan attribute. std::string rowspan; if (GetHtmlAttribute("aria-rowspan", &rowspan)) { AddAttributeToList("rowspan", rowspan, attributes); } } // Expose the value of a progress bar, slider, scroll bar or