* Copyright (C) 2012, Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
* its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "third_party/blink/renderer/modules/accessibility/ax_node_object.h"
#include <math.h>
#include <algorithm>
#include <array>
#include <memory>
#include <optional>
#include <queue>
#include "base/auto_reset.h"
#include "base/containers/contains.h"
#include "base/containers/fixed_flat_set.h"
#include "base/metrics/histogram_functions.h"
#include "base/numerics/safe_conversions.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/input/web_keyboard_event.h"
#include "third_party/blink/public/mojom/frame/user_activation_notification_type.mojom-blink.h"
#include "third_party/blink/public/strings/grit/blink_strings.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_image_bitmap_options.h"
#include "third_party/blink/renderer/core/accessibility/ax_utilities_generated.h"
#include "third_party/blink/renderer/core/css/counter_style_map.h"
#include "third_party/blink/renderer/core/css/css_resolution_units.h"
#include "third_party/blink/renderer/core/css/properties/longhands.h"
#include "third_party/blink/renderer/core/display_lock/display_lock_utilities.h"
#include "third_party/blink/renderer/core/dom/flat_tree_traversal.h"
#include "third_party/blink/renderer/core/dom/focus_params.h"
#include "third_party/blink/renderer/core/dom/focusgroup_flags.h"
#include "third_party/blink/renderer/core/dom/layout_tree_builder_traversal.h"
#include "third_party/blink/renderer/core/dom/node_traversal.h"
#include "third_party/blink/renderer/core/dom/qualified_name.h"
#include "third_party/blink/renderer/core/dom/range.h"
#include "third_party/blink/renderer/core/dom/scroll_marker_group_pseudo_element.h"
#include "third_party/blink/renderer/core/dom/scroll_marker_pseudo_element.h"
#include "third_party/blink/renderer/core/dom/shadow_root.h"
#include "third_party/blink/renderer/core/dom/text.h"
#include "third_party/blink/renderer/core/editing/editing_utilities.h"
#include "third_party/blink/renderer/core/editing/markers/custom_highlight_marker.h"
#include "third_party/blink/renderer/core/editing/markers/document_marker_controller.h"
#include "third_party/blink/renderer/core/editing/position.h"
#include "third_party/blink/renderer/core/events/event_util.h"
#include "third_party/blink/renderer/core/events/keyboard_event.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/local_frame_view.h"
#include "third_party/blink/renderer/core/frame/settings.h"
#include "third_party/blink/renderer/core/highlight/highlight.h"
#include "third_party/blink/renderer/core/html/canvas/html_canvas_element.h"
#include "third_party/blink/renderer/core/html/canvas/image_data.h"
#include "third_party/blink/renderer/core/html/custom/element_internals.h"
#include "third_party/blink/renderer/core/html/fenced_frame/html_fenced_frame_element.h"
#include "third_party/blink/renderer/core/html/forms/html_button_element.h"
#include "third_party/blink/renderer/core/html/forms/html_data_list_element.h"
#include "third_party/blink/renderer/core/html/forms/html_field_set_element.h"
#include "third_party/blink/renderer/core/html/forms/html_form_control_element.h"
#include "third_party/blink/renderer/core/html/forms/html_form_element.h"
#include "third_party/blink/renderer/core/html/forms/html_input_element.h"
#include "third_party/blink/renderer/core/html/forms/html_label_element.h"
#include "third_party/blink/renderer/core/html/forms/html_legend_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/forms/html_output_element.h"
#include "third_party/blink/renderer/core/html/forms/html_select_element.h"
#include "third_party/blink/renderer/core/html/forms/html_text_area_element.h"
#include "third_party/blink/renderer/core/html/forms/labels_node_list.h"
#include "third_party/blink/renderer/core/html/forms/radio_input_type.h"
#include "third_party/blink/renderer/core/html/forms/text_control_element.h"
#include "third_party/blink/renderer/core/html/html_anchor_element.h"
#include "third_party/blink/renderer/core/html/html_body_element.h"
#include "third_party/blink/renderer/core/html/html_details_element.h"
#include "third_party/blink/renderer/core/html/html_dialog_element.h"
#include "third_party/blink/renderer/core/html/html_directory_element.h"
#include "third_party/blink/renderer/core/html/html_div_element.h"
#include "third_party/blink/renderer/core/html/html_dlist_element.h"
#include "third_party/blink/renderer/core/html/html_element.h"
#include "third_party/blink/renderer/core/html/html_embed_element.h"
#include "third_party/blink/renderer/core/html/html_frame_element_base.h"
#include "third_party/blink/renderer/core/html/html_hr_element.h"
#include "third_party/blink/renderer/core/html/html_html_element.h"
#include "third_party/blink/renderer/core/html/html_image_element.h"
#include "third_party/blink/renderer/core/html/html_li_element.h"
#include "third_party/blink/renderer/core/html/html_map_element.h"
#include "third_party/blink/renderer/core/html/html_menu_bar_element.h"
#include "third_party/blink/renderer/core/html/html_menu_element.h"
#include "third_party/blink/renderer/core/html/html_menu_item_element.h"
#include "third_party/blink/renderer/core/html/html_menu_list_element.h"
#include "third_party/blink/renderer/core/html/html_meter_element.h"
#include "third_party/blink/renderer/core/html/html_olist_element.h"
#include "third_party/blink/renderer/core/html/html_paragraph_element.h"
#include "third_party/blink/renderer/core/html/html_permission_element.h"
#include "third_party/blink/renderer/core/html/html_plugin_element.h"
#include "third_party/blink/renderer/core/html/html_progress_element.h"
#include "third_party/blink/renderer/core/html/html_slot_element.h"
#include "third_party/blink/renderer/core/html/html_span_element.h"
#include "third_party/blink/renderer/core/html/html_summary_element.h"
#include "third_party/blink/renderer/core/html/html_table_caption_element.h"
#include "third_party/blink/renderer/core/html/html_table_cell_element.h"
#include "third_party/blink/renderer/core/html/html_table_col_element.h"
#include "third_party/blink/renderer/core/html/html_table_element.h"
#include "third_party/blink/renderer/core/html/html_table_row_element.h"
#include "third_party/blink/renderer/core/html/html_table_section_element.h"
#include "third_party/blink/renderer/core/html/html_time_element.h"
#include "third_party/blink/renderer/core/html/html_ulist_element.h"
#include "third_party/blink/renderer/core/html/media/html_audio_element.h"
#include "third_party/blink/renderer/core/html/media/html_media_element.h"
#include "third_party/blink/renderer/core/html/media/html_video_element.h"
#include "third_party/blink/renderer/core/html/parser/html_parser_idioms.h"
#include "third_party/blink/renderer/core/html/shadow/shadow_element_names.h"
#include "third_party/blink/renderer/core/html_names.h"
#include "third_party/blink/renderer/core/imagebitmap/image_bitmap.h"
#include "third_party/blink/renderer/core/input_type_names.h"
#include "third_party/blink/renderer/core/layout/hit_test_location.h"
#include "third_party/blink/renderer/core/layout/hit_test_result.h"
#include "third_party/blink/renderer/core/layout/inline/abstract_inline_text_box.h"
#include "third_party/blink/renderer/core/layout/inline/inline_cursor.h"
#include "third_party/blink/renderer/core/layout/inline/inline_node.h"
#include "third_party/blink/renderer/core/layout/inline/offset_mapping.h"
#include "third_party/blink/renderer/core/layout/layout_block_flow.h"
#include "third_party/blink/renderer/core/layout/layout_box_model_object.h"
#include "third_party/blink/renderer/core/layout/layout_html_canvas.h"
#include "third_party/blink/renderer/core/layout/layout_inline.h"
#include "third_party/blink/renderer/core/layout/layout_object.h"
#include "third_party/blink/renderer/core/layout/layout_view.h"
#include "third_party/blink/renderer/core/layout/table/layout_table.h"
#include "third_party/blink/renderer/core/layout/table/layout_table_cell.h"
#include "third_party/blink/renderer/core/layout/table/layout_table_row.h"
#include "third_party/blink/renderer/core/layout/table/layout_table_section.h"
#include "third_party/blink/renderer/core/loader/progress_tracker.h"
#include "third_party/blink/renderer/core/mathml/mathml_element.h"
#include "third_party/blink/renderer/core/mathml_names.h"
#include "third_party/blink/renderer/core/navigation_api/navigation_api.h"
#include "third_party/blink/renderer/core/page/focus_controller.h"
#include "third_party/blink/renderer/core/page/page.h"
#include "third_party/blink/renderer/core/style/computed_style_base_constants.h"
#include "third_party/blink/renderer/core/style/computed_style_constants.h"
#include "third_party/blink/renderer/core/svg/svg_a_element.h"
#include "third_party/blink/renderer/core/svg/svg_desc_element.h"
#include "third_party/blink/renderer/core/svg/svg_element.h"
#include "third_party/blink/renderer/core/svg/svg_foreign_object_element.h"
#include "third_party/blink/renderer/core/svg/svg_g_element.h"
#include "third_party/blink/renderer/core/svg/svg_svg_element.h"
#include "third_party/blink/renderer/core/svg/svg_symbol_element.h"
#include "third_party/blink/renderer/core/svg/svg_text_element.h"
#include "third_party/blink/renderer/core/svg/svg_title_element.h"
#include "third_party/blink/renderer/core/svg/svg_use_element.h"
#include "third_party/blink/renderer/core/xlink_names.h"
#include "third_party/blink/renderer/modules/accessibility/ax_block_flow_iterator.h"
#include "third_party/blink/renderer/modules/accessibility/ax_image_map_link.h"
#include "third_party/blink/renderer/modules/accessibility/ax_inline_text_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/modules/accessibility/ax_object_cache_impl.h"
#include "third_party/blink/renderer/modules/accessibility/ax_position.h"
#include "third_party/blink/renderer/modules/accessibility/ax_range.h"
#include "third_party/blink/renderer/modules/accessibility/ax_relation_cache.h"
#include "third_party/blink/renderer/platform/graphics/image_data_buffer.h"
#include "third_party/blink/renderer/platform/keyboard_codes.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
#include "third_party/blink/renderer/platform/text/platform_locale.h"
#include "third_party/blink/renderer/platform/text/text_direction.h"
#include "third_party/blink/renderer/platform/weborigin/kurl.h"
#include "third_party/blink/renderer/platform/wtf/casting.h"
#include "third_party/blink/renderer/platform/wtf/text/strcat.h"
#include "third_party/blink/renderer/platform/wtf/text/string_builder.h"
#include "third_party/skia/include/core/SkImage.h"
#include "ui/accessibility/accessibility_features.h"
#include "ui/accessibility/ax_common.h"
#include "ui/accessibility/ax_role_properties.h"
#include "ui/events/keycodes/dom/dom_code.h"
#include "ui/events/keycodes/dom/keycode_converter.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/rect_f.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/geometry/size_f.h"
#include "ui/gfx/geometry/transform.h"
#include "ui/strings/grit/ax_strings.h"
namespace blink {
namespace {
const ScrollMarkerPseudoElement* GetScrollMarker(const Node* node) {
auto* element = DynamicTo<Element>(node);
if (!element) {
return nullptr;
}
return DynamicTo<ScrollMarkerPseudoElement>(
element->GetPseudoElement(kPseudoIdScrollMarker));
}
bool IsTabsModeScrollMarker(const ScrollMarkerPseudoElement& scroll_marker) {
ScrollMarkerGroupPseudoElement* scroll_marker_group =
scroll_marker.ScrollMarkerGroup();
if (!scroll_marker_group) {
return false;
}
return scroll_marker_group->ScrollMarkerGroupMode() ==
ScrollMarkerGroup::ScrollMarkerMode::kTabs;
}
bool IsOriginatingElementForScrollMarkerInTabsMode(const Node* node) {
const ScrollMarkerPseudoElement* scroll_marker = GetScrollMarker(node);
return scroll_marker && IsTabsModeScrollMarker(*scroll_marker);
}
bool IsOriginatingElementForInactiveScrollMarkerInTabsMode(const Node* node) {
const ScrollMarkerPseudoElement* scroll_marker = GetScrollMarker(node);
return scroll_marker && !scroll_marker->IsSelected() &&
IsTabsModeScrollMarker(*scroll_marker);
}
bool ShouldUseLayoutNG(const blink::LayoutObject& layout_object) {
return layout_object.IsInline() &&
layout_object.IsInLayoutNGInlineFormattingContext();
}
bool IsPotentialInPageLinkTarget(blink::Node& node) {
auto* element = blink::DynamicTo<blink::Element>(&node);
if (!element) {
return blink::IsA<blink::Document>(node);
}
if (element->ContainingShadowRoot())
return false;
if (element->IsSVGElement())
return false;
if (auto* anchor = blink::DynamicTo<blink::HTMLAnchorElement>(element)) {
if (anchor->HasName())
return true;
}
if (element->HasID() && !blink::IsA<blink::HTMLLabelElement>(element) &&
!blink::IsA<blink::HTMLOptGroupElement>(element)) {
return true;
}
return false;
}
bool IsLinkable(const blink::AXObject& object) {
if (!object.GetLayoutObject()) {
return false;
}
return object.IsLink() || object.IsImage() ||
object.GetLayoutObject()->IsText();
}
bool IsImageOrAltText(blink::LayoutObject* layout_object, blink::Node* node) {
DCHECK(layout_object);
if (layout_object->IsImage()) {
return true;
}
if (IsA<blink::HTMLImageElement>(node)) {
return true;
}
auto* html_input_element = DynamicTo<blink::HTMLInputElement>(node);
return html_input_element && html_input_element->HasFallbackContent();
}
bool ShouldIgnoreListItem(blink::Node* node) {
DCHECK(node);
blink::Element* parent = blink::FlatTreeTraversal::ParentElement(*node);
if (!parent) {
return false;
}
if (IsA<blink::HTMLMenuElement>(*parent) ||
IsA<blink::HTMLUListElement>(*parent) ||
IsA<blink::HTMLOListElement>(*parent)) {
AtomicString role =
blink::AXObject::AriaAttribute(*parent, blink::html_names::kRoleAttr);
if (!role.empty() && role != "list" && role != "directory") {
return true;
}
}
return false;
}
bool IsNeutralWithinTable(blink::AXObject* obj) {
if (!obj)
return false;
ax::mojom::blink::Role role = obj->RoleValue();
return role == ax::mojom::blink::Role::kGroup ||
role == ax::mojom::blink::Role::kGenericContainer ||
role == ax::mojom::blink::Role::kRowGroup;
}
blink::AXObject* GetDOMTableAXAncestor(blink::Node* node,
blink::AXObjectCacheImpl& cache) {
if (!node)
return nullptr;
while (true) {
node = blink::LayoutTreeBuilderTraversal::Parent(*node);
if (!node)
return nullptr;
blink::AXObject* ax_object = cache.Get(node);
if (ax_object && !IsNeutralWithinTable(ax_object))
return ax_object;
}
}
blink::LayoutTableSection* FirstTableSection(
blink::LayoutObject* maybe_table,
blink::LayoutTable** table_out = nullptr) {
if (auto* table = DynamicTo<blink::LayoutTable>(maybe_table)) {
if (table->GetNode()) {
if (table_out) {
*table_out = table;
}
return table->FirstSection();
}
}
if (table_out) {
*table_out = nullptr;
}
return nullptr;
}
enum class AXAction {
kActionIncrement = 0,
kActionDecrement,
};
blink::KeyboardEvent* CreateKeyboardEvent(
blink::LocalDOMWindow* local_dom_window,
blink::WebInputEvent::Type type,
AXAction action,
blink::AccessibilityOrientation orientation,
ax::mojom::blink::WritingDirection text_direction) {
blink::WebKeyboardEvent key(type,
blink::WebInputEvent::Modifiers::kNoModifiers,
base::TimeTicks::Now());
if (action == AXAction::kActionIncrement) {
if (orientation == blink::kAccessibilityOrientationVertical) {
key.dom_key = ui::DomKey::ARROW_UP;
key.dom_code = static_cast<int>(ui::DomCode::ARROW_UP);
key.native_key_code = key.windows_key_code = blink::VKEY_UP;
} else if (text_direction == ax::mojom::blink::WritingDirection::kRtl) {
key.dom_key = ui::DomKey::ARROW_LEFT;
key.dom_code = static_cast<int>(ui::DomCode::ARROW_LEFT);
key.native_key_code = key.windows_key_code = blink::VKEY_LEFT;
} else {
key.dom_key = ui::DomKey::ARROW_RIGHT;
key.dom_code = static_cast<int>(ui::DomCode::ARROW_RIGHT);
key.native_key_code = key.windows_key_code = blink::VKEY_RIGHT;
}
} else if (action == AXAction::kActionDecrement) {
if (orientation == blink::kAccessibilityOrientationVertical) {
key.dom_key = ui::DomKey::ARROW_DOWN;
key.dom_code = static_cast<int>(ui::DomCode::ARROW_DOWN);
key.native_key_code = key.windows_key_code = blink::VKEY_DOWN;
} else if (text_direction == ax::mojom::blink::WritingDirection::kRtl) {
key.dom_key = ui::DomKey::ARROW_RIGHT;
key.dom_code = static_cast<int>(ui::DomCode::ARROW_RIGHT);
key.native_key_code = key.windows_key_code = blink::VKEY_RIGHT;
} else {
key.dom_key = ui::DomKey::ARROW_LEFT;
key.dom_code = static_cast<int>(ui::DomCode::ARROW_LEFT);
key.native_key_code = key.windows_key_code = blink::VKEY_LEFT;
}
}
return blink::KeyboardEvent::Create(key, local_dom_window, true);
}
unsigned TextStyleFlag(ax::mojom::blink::TextStyle text_style_enum) {
return static_cast<unsigned>(1 << static_cast<int>(text_style_enum));
}
ax::mojom::blink::TextDecorationStyle
TextDecorationStyleToAXTextDecorationStyle(
const blink::ETextDecorationStyle text_decoration_style) {
switch (text_decoration_style) {
case blink::ETextDecorationStyle::kDashed:
return ax::mojom::blink::TextDecorationStyle::kDashed;
case blink::ETextDecorationStyle::kSolid:
return ax::mojom::blink::TextDecorationStyle::kSolid;
case blink::ETextDecorationStyle::kDotted:
return ax::mojom::blink::TextDecorationStyle::kDotted;
case blink::ETextDecorationStyle::kDouble:
return ax::mojom::blink::TextDecorationStyle::kDouble;
case blink::ETextDecorationStyle::kWavy:
return ax::mojom::blink::TextDecorationStyle::kWavy;
}
NOTREACHED();
}
String GetTitle(blink::Element* element) {
if (!element)
return String();
if (blink::SVGElement* svg_element =
blink::DynamicTo<blink::SVGElement>(element)) {
if (svg_element->InUseShadowTree()) {
String title = GetTitle(svg_element->OwnerShadowHost());
if (!title.empty())
return title;
}
if (auto* title_element =
blink::Traversal<blink::SVGTitleElement>::FirstChild(*element)) {
return title_element->GetInnerTextWithoutUpdate();
}
return String();
}
return element->title();
}
bool HasLayoutText(const blink::AXObject* obj) {
#if DCHECK_IS_ON()
DCHECK(obj->GetDocument()->Lifecycle().GetState() >=
blink::DocumentLifecycle::kLayoutClean)
<< "Unclean document at lifecycle "
<< obj->GetDocument()->Lifecycle().ToString();
#endif
if (!obj->GetLayoutObject()) {
return false;
}
if (blink::DisplayLockUtilities::LockedAncestorPreventingPaint(
*obj->GetLayoutObject())) {
return false;
}
if (!obj->GetLayoutObject()->IsText()) {
return false;
}
if (obj->GetLayoutObject()->NeedsLayout()) {
DCHECK(false) << "LayoutText needed layout but was not display locked: "
<< obj;
return false;
}
return true;
}
const LayoutObject* GetListMarker(const LayoutObject& layout_object,
const AXObject* parent) {
if (layout_object.IsLayoutOutsideListMarker()) {
return &layout_object;
}
if (parent && parent->RoleValue() == ax::mojom::blink::Role::kNone &&
parent->GetLayoutObject() &&
parent->GetLayoutObject()->IsLayoutOutsideListMarker()) {
return parent->GetLayoutObject();
}
return nullptr;
}
bool ElementHasAnyAriaRelation(Element& element) {
if (element.HasAnyExplicitlySetAttrAssociatedElements()) {
return true;
}
const auto& idref_attrs = GetAriaIdrefAttributes();
for (const QualifiedName* attr : idref_attrs) {
if (AXObject::HasAriaAttribute(element, *attr)) {
return true;
}
}
const auto& idref_list_attrs = GetAriaIdrefListAttributes();
for (const QualifiedName* attr : idref_list_attrs) {
if (AXObject::HasAriaAttribute(element, *attr)) {
return true;
}
}
return false;
}
bool IsAddedOnlyViaSpecialTraversal(const Node* node) {
if (node->IsScrollMarkerPseudoElement()) {
return true;
}
if (node->IsScrollMarkerGroupPseudoElement() ||
node->IsScrollButtonPseudoElement()) {
return true;
}
return false;
}
VectorOf<Node> UnpackScrollerWithSiblingControls(Element* element) {
CHECK(element->HasScrollButtonOrMarkerGroupPseudos());
PseudoId ordered_pseudos[] = {
kPseudoIdScrollMarkerGroupBefore, kPseudoIdScrollButtonBlockStart,
kPseudoIdScrollButtonInlineStart, kPseudoIdScrollButtonInlineEnd,
kPseudoIdScrollButtonBlockEnd, kPseudoIdNone,
kPseudoIdScrollMarkerGroupAfter,
};
VectorOf<Node> result;
for (PseudoId pseudo_id : ordered_pseudos) {
if (pseudo_id == kPseudoIdNone) {
result.push_back(element);
} else if (auto* pseudo = element->GetPseudoElement(pseudo_id)) {
result.push_back(pseudo);
}
}
CHECK(!result.empty());
return result;
}
void CollectLayoutTextContentRecursive(StringBuilder& builder,
const LayoutObject* object) {
if (auto* text_object = DynamicTo<LayoutText>(object)) {
builder.Append(text_object->TransformedText());
}
for (auto* child = object->SlowFirstChild(); child;
child = child->NextSibling()) {
CollectLayoutTextContentRecursive(builder, child);
}
}
}
using html_names::kAltAttr;
using html_names::kTitleAttr;
using html_names::kTypeAttr;
using html_names::kValueAttr;
using mojom::blink::FormControlType;
AXNodeObject::AXNodeObject(Node* node, AXObjectCacheImpl& ax_object_cache)
: AXObject(ax_object_cache, true), node_(node) {}
AXNodeObject::AXNodeObject(LayoutObject* layout_object,
AXObjectCacheImpl& ax_object_cache)
: AXObject(ax_object_cache, true),
node_(layout_object->GetNode()),
layout_object_(layout_object) {
#if DCHECK_IS_ON()
layout_object_->SetHasAXObject(true);
#endif
}
AXNodeObject::~AXNodeObject() {
DCHECK(!node_);
DCHECK(!layout_object_);
}
void AXNodeObject::AlterSliderOrSpinButtonValue(bool increase) {
if (!GetNode())
return;
if (!IsSlider() && !IsSpinButton())
return;
float value;
if (!ValueForRange(&value))
return;
if (!RuntimeEnabledFeatures::
SynthesizedKeyboardEventsForAccessibilityActionsEnabled()) {
float step;
if (!StepValueForRange(&step)) {
if (IsNativeSlider() || IsNativeSpinButton()) {
step = StepRange().Step().ToString().ToFloat();
} else {
return;
}
}
value += increase ? step : -step;
if (native_role_ == ax::mojom::blink::Role::kSlider ||
native_role_ == ax::mojom::blink::Role::kSpinButton) {
OnNativeSetValueAction(String::Number(value));
if (IsDetached())
return;
AXObjectCache().HandleValueChanged(GetNode());
return;
}
}
AXAction action =
increase ? AXAction::kActionIncrement : AXAction::kActionDecrement;
LocalDOMWindow* local_dom_window = GetDocument()->domWindow();
AccessibilityOrientation orientation = Orientation();
ax::mojom::blink::WritingDirection text_direction = GetTextDirection();
KeyboardEvent* keydown =
CreateKeyboardEvent(local_dom_window, WebInputEvent::Type::kRawKeyDown,
action, orientation, text_direction);
GetNode()->DispatchEvent(*keydown);
if (!GetNode())
return;
KeyboardEvent* keyup =
CreateKeyboardEvent(local_dom_window, WebInputEvent::Type::kKeyUp, action,
orientation, text_direction);
GetDocument()
->GetTaskRunner(TaskType::kUserInteraction)
->PostDelayedTask(
FROM_HERE,
BindOnce(
[](Node* node, KeyboardEvent* evt) {
if (node) {
node->DispatchEvent(*evt);
}
},
WrapWeakPersistent(GetNode()), WrapPersistent(keyup)),
base::Milliseconds(100));
}
AXObject* AXNodeObject::ActiveDescendant() const {
Element* element = GetElement();
if (!element)
return nullptr;
if (RoleValue() == ax::mojom::blink::Role::kMenuListPopup) {
if (HTMLSelectElement* select =
DynamicTo<HTMLSelectElement>(parent_->GetNode())) {
return !select->IsMultiple() && (select->PopupIsVisible() ||
select->IsFocusedElementInDocument())
? AXObjectCache().Get(select->OptionToBeShown())
: nullptr;
}
}
if (auto* select = DynamicTo<HTMLSelectElement>(GetNode())) {
if (!select->UsesMenuList()) {
return AXObjectCache().Get(select->ActiveSelectionEnd());
}
}
const Element* descendant = ElementFromAttributeOrInternals(
element, html_names::kAriaActivedescendantAttr);
if (!descendant) {
return nullptr;
}
AXObject* ax_descendant = AXObjectCache().Get(descendant);
return ax_descendant && ax_descendant->IsVisible() ? ax_descendant : nullptr;
}
bool IsExemptFromInlineBlockCheck(ax::mojom::blink::Role role) {
return role == ax::mojom::blink::Role::kSvgRoot ||
role == ax::mojom::blink::Role::kCanvas ||
role == ax::mojom::blink::Role::kEmbeddedObject;
}
bool AXNodeObject::HasCustomElementTreeProcessing() const {
if (!RuntimeEnabledFeatures::AccessibilityCustomElementRoleNoneEnabled()) {
return false;
}
if (!GetElement()) {
return false;
}
if (!GetElement()->IsCustomElement()) {
return false;
}
return true;
}
bool AXNodeObject::ShouldIncludeCustomElement() const {
Element* element = GetElement();
DCHECK(element);
DCHECK(element->IsCustomElement()) << element;
if (RoleValue() == ax::mojom::blink::Role::kNone) {
return false;
}
if (RoleValue() != ax::mojom::blink::Role::kGenericContainer) {
return true;
}
if (!element->GetShadowRoot()) {
return true;
}
if (HasAriaAttribute(html_names::kAriaLiveAttr)) {
return true;
}
if (element->GetElementInternals() &&
element->GetElementInternals()->HasAnyAttribute()) {
return true;
}
if (element->IsKeyboardFocusableSlow(
Element::UpdateBehavior::kNoneForAccessibility)) {
return true;
}
DEFINE_STATIC_LOCAL(const AtomicString, web_view_tag, ("webview"));
if (element->HasLocalName(web_view_tag)) {
return true;
}
return false;
}
AXObjectInclusion AXNodeObject::ShouldIncludeBasedOnSemantics(
IgnoredReasons* ignored_reasons) const {
DCHECK(GetDocument());
if (IsA<Document>(GetNode())) {
return kIncludeObject;
}
if (IsPresentational()) {
if (ignored_reasons)
ignored_reasons->push_back(IgnoredReason(kAXPresentational));
return kIgnoreObject;
}
Node* node = GetNode();
if (!node) {
if (IsImage())
return kIncludeObject;
return kDefaultBehavior;
}
if (node->IsScrollMarkerGroupPseudoElement() ||
node->IsScrollMarkerPseudoElement() ||
node->IsScrollButtonPseudoElement()) {
return kIncludeObject;
}
if (RoleValue() == ax::mojom::blink::Role::kRubyAnnotation ||
(RoleValue() == ax::mojom::blink::Role::kStaticText && ParentObject() &&
ParentObject()->RoleValue() ==
ax::mojom::blink::Role::kRubyAnnotation)) {
return kIgnoreObject;
}
Element* element = GetElement();
if (!element) {
return kDefaultBehavior;
}
if (IsExcludedByFormControlsFilter()) {
if (ignored_reasons) {
ignored_reasons->push_back(IgnoredReason(kAXUninteresting));
}
return kIgnoreObject;
}
if (IsA<SVGElement>(node)) {
if (SVGSymbolElement* symbol =
Traversal<SVGSymbolElement>::FirstAncestorOrSelf(*node)) {
if (!symbol->InUseShadowTree() || !IsAtShadowBoundary(symbol)) {
return kIgnoreObject;
}
}
if (IsA<SVGSVGElement>(node) && GetLayoutObject() &&
GetLayoutObject()->IsSVGRoot() && element->firstElementChild()) {
return kIncludeObject;
}
if (ElementTraversal::FirstChild(
*To<ContainerNode>(node), [](auto& element) {
return element.HasTagName(svg_names::kTitleTag) ||
element.HasTagName(svg_names::kDescTag);
})) {
return kIncludeObject;
}
if (IsA<SVGGElement>(node)) {
Settings* settings = GetDocument()->GetSettings();
if (settings->GetAccessibilityIncludeSvgGElement()) {
return kIncludeObject;
}
}
}
if (IsTableLikeRole() || IsTableRowLikeRole() || IsTableCellLikeRole() ||
element->HasTagName(html_names::kTheadTag) ||
element->HasTagName(html_names::kTfootTag)) {
return kIncludeObject;
}
if (IsA<HTMLHtmlElement>(node)) {
if (ignored_reasons) {
ignored_reasons->push_back(IgnoredReason(kAXUninteresting));
}
return kIgnoreObject;
}
if (!IsA<HTMLBodyElement>(node) && CanSetFocusAttribute())
return kIncludeObject;
if (IsLink())
return kIncludeObject;
if (IsClickable())
return kIncludeObject;
if (IsHeading())
return kIncludeObject;
if (node->HasTagName(html_names::kHeaderTag) ||
node->HasTagName(html_names::kFooterTag))
return kIncludeObject;
if (IsControl())
return kIncludeObject;
if (IsA<HTMLOptGroupElement>(node)) {
return kIncludeObject;
}
if (RawAriaRole() != ax::mojom::blink::Role::kUnknown) {
return kIncludeObject;
}
if (IsEditableRoot())
return kIncludeObject;
if (HasCustomElementTreeProcessing()) {
return ShouldIncludeCustomElement() ? kIncludeObject : kIgnoreObject;
}
if (IsA<HTMLLegendElement>(node)) {
for (auto* ancestor = node->parentNode(); ancestor;
ancestor = ancestor->parentNode()) {
if (IsA<HTMLOptGroupElement>(ancestor)) {
return kIgnoreObject;
}
}
return kIncludeObject;
}
static constexpr auto always_included_computed_roles =
base::MakeFixedFlatSet<ax::mojom::blink::Role>({
ax::mojom::blink::Role::kAbbr,
ax::mojom::blink::Role::kApplication,
ax::mojom::blink::Role::kArticle,
ax::mojom::blink::Role::kAudio,
ax::mojom::blink::Role::kBanner,
ax::mojom::blink::Role::kBlockquote,
ax::mojom::blink::Role::kCode,
ax::mojom::blink::Role::kComplementary,
ax::mojom::blink::Role::kContentDeletion,
ax::mojom::blink::Role::kContentInfo,
ax::mojom::blink::Role::kContentInsertion,
ax::mojom::blink::Role::kDefinition,
ax::mojom::blink::Role::kDescriptionList,
ax::mojom::blink::Role::kDetails,
ax::mojom::blink::Role::kDialog,
ax::mojom::blink::Role::kDocAcknowledgments,
ax::mojom::blink::Role::kDocAfterword,
ax::mojom::blink::Role::kDocAppendix,
ax::mojom::blink::Role::kDocBibliography,
ax::mojom::blink::Role::kDocChapter,
ax::mojom::blink::Role::kDocConclusion,
ax::mojom::blink::Role::kDocCredits,
ax::mojom::blink::Role::kDocEndnotes,
ax::mojom::blink::Role::kDocEpilogue,
ax::mojom::blink::Role::kDocErrata,
ax::mojom::blink::Role::kDocForeword,
ax::mojom::blink::Role::kDocGlossary,
ax::mojom::blink::Role::kDocIntroduction,
ax::mojom::blink::Role::kDocPart,
ax::mojom::blink::Role::kDocPreface,
ax::mojom::blink::Role::kDocPrologue,
ax::mojom::blink::Role::kDocToc,
ax::mojom::blink::Role::kEmphasis,
ax::mojom::blink::Role::kFigcaption,
ax::mojom::blink::Role::kFigure,
ax::mojom::blink::Role::kFooter,
ax::mojom::blink::Role::kForm,
ax::mojom::blink::Role::kHeader,
ax::mojom::blink::Role::kList,
ax::mojom::blink::Role::kListItem,
ax::mojom::blink::Role::kMain,
ax::mojom::blink::Role::kMark,
ax::mojom::blink::Role::kMath,
ax::mojom::blink::Role::kMathMLMath,
ax::mojom::blink::Role::kMathMLFraction,
ax::mojom::blink::Role::kMathMLIdentifier,
ax::mojom::blink::Role::kMathMLMultiscripts,
ax::mojom::blink::Role::kMathMLNoneScript,
ax::mojom::blink::Role::kMathMLNumber,
ax::mojom::blink::Role::kMathMLOperator,
ax::mojom::blink::Role::kMathMLOver,
ax::mojom::blink::Role::kMathMLPrescriptDelimiter,
ax::mojom::blink::Role::kMathMLRoot,
ax::mojom::blink::Role::kMathMLRow,
ax::mojom::blink::Role::kMathMLSquareRoot,
ax::mojom::blink::Role::kMathMLStringLiteral,
ax::mojom::blink::Role::kMathMLSub,
ax::mojom::blink::Role::kMathMLSubSup,
ax::mojom::blink::Role::kMathMLSup,
ax::mojom::blink::Role::kMathMLTable,
ax::mojom::blink::Role::kMathMLTableCell,
ax::mojom::blink::Role::kMathMLTableRow,
ax::mojom::blink::Role::kMathMLText,
ax::mojom::blink::Role::kMathMLUnder,
ax::mojom::blink::Role::kMathMLUnderOver,
ax::mojom::blink::Role::kMeter,
ax::mojom::blink::Role::kMenuBar,
ax::mojom::blink::Role::kMenuListOption,
ax::mojom::blink::Role::kMenuListPopup,
ax::mojom::blink::Role::kNavigation,
ax::mojom::blink::Role::kPluginObject,
ax::mojom::blink::Role::kProgressIndicator,
ax::mojom::blink::Role::kRegion,
ax::mojom::blink::Role::kRuby,
ax::mojom::blink::Role::kSearch,
ax::mojom::blink::Role::kSection,
ax::mojom::blink::Role::kSplitter,
ax::mojom::blink::Role::kSubscript,
ax::mojom::blink::Role::kSuperscript,
#if !BUILDFLAG(ARKWEB_ACCESSIBILITY)
ax::mojom::blink::Role::kStrong,
#endif
ax::mojom::blink::Role::kTerm,
ax::mojom::blink::Role::kTime,
ax::mojom::blink::Role::kVideo,
});
if (base::Contains(always_included_computed_roles, RoleValue())) {
return kIncludeObject;
}
if (GetNode()->HasTagName(html_names::kHgroupTag)) {
return kIncludeObject;
}
if (ElementHasAnyAriaAttribute()) {
return kIncludeObject;
}
if (!GetElement()->FastGetAttribute(kTitleAttr).empty()) {
return kIncludeObject;
}
if (IsImage() && !IsA<SVGElement>(node)) {
String alt = GetElement()->FastGetAttribute(kAltAttr);
if (!alt.empty() || alt.IsNull())
return kIncludeObject;
if (ignored_reasons)
ignored_reasons->push_back(IgnoredReason(kAXEmptyAlt));
return kIgnoreObject;
}
if (IsPotentialInPageLinkTarget(*element))
return kIncludeObject;
if (AXObjectCache().GetAXMode().has_mode(ui::AXMode::kInlineTextBoxes)) {
if (!IsExemptFromInlineBlockCheck(native_role_) && GetLayoutObject() &&
GetLayoutObject()->IsInline() &&
GetLayoutObject()->IsAtomicInlineLevel() &&
node->parentNode()->childElementCount() > 1) {
return kIncludeObject;
}
}
std::optional<String> alt_text = GetCSSAltText(GetElement());
if (alt_text) {
if (alt_text->empty()) {
return kIgnoreObject;
} else {
return kIncludeObject;
}
}
if (IsA<HTMLSpanElement>(node)) {
if (ignored_reasons)
ignored_reasons->push_back(IgnoredReason(kAXUninteresting));
return kIgnoreObject;
}
if (HTMLLabelElement* label = DynamicTo<HTMLLabelElement>(node)) {
if (IsRedundantLabel(label)) {
if (ignored_reasons) {
ignored_reasons->push_back(
IgnoredReason(kAXLabelFor, AXObjectCache().Get(label->Control())));
}
return kIgnoreObject;
}
return kIncludeObject;
}
if (IsA<SVGForeignObjectElement>(node)) {
if (ignored_reasons) {
ignored_reasons->push_back(IgnoredReason(kAXPresentational));
}
return kIgnoreObject;
}
return kDefaultBehavior;
}
bool AXNodeObject::ComputeIsIgnoredAsInsideInactiveScrollMarkerTab() {
Node* node = GetNode();
if (!node || !RuntimeEnabledFeatures::CSSScrollMarkerGroupModesEnabled()) {
return false;
}
if (node->IsCarouselPseudoElement()) {
return false;
}
if (IsOriginatingElementForInactiveScrollMarkerInTabsMode(node)) {
return true;
}
if (!ParentObject()) {
return false;
}
return ParentObject()
->InsideOriginatingElementForInactiveScrollMarkerInTabsMode();
}
bool AXNodeObject::ComputeIsIgnored(IgnoredReasons* ignored_reasons) const {
Node* node = GetNode();
if (InsideOriginatingElementForInactiveScrollMarkerInTabsMode()) {
if (ignored_reasons) {
ignored_reasons->push_back(IgnoredReason(kAXInactiveCarouselTabContent));
}
return true;
}
if (ShouldIgnoreForHiddenOrInert(ignored_reasons)) {
if (IsAriaHidden()) {
return true;
}
if (const AXObject* ax_menu_list = ParentObject()->AncestorMenuList()) {
for (const AXObject* ancestor = this;
ancestor && ancestor != ax_menu_list;
ancestor = ancestor->ParentObject()) {
if (HTMLSelectElement::IsSlottedButton(ancestor->GetNode())) {
return true;
}
}
return ax_menu_list->IsIgnored();
}
if (IsHiddenViaStyle() || !node || !node->ParentOrShadowHostElement() ||
!node->ParentOrShadowHostElement()->IsCanvasOrInCanvasSubtree()) {
return true;
}
}
AXObjectInclusion include = ShouldIncludeBasedOnSemantics(ignored_reasons);
if (include == kIncludeObject) {
return false;
}
if (include == kIgnoreObject) {
return true;
}
if (!GetLayoutObject()) {
if (IsA<Text>(node)) {
return false;
}
if (node->ParentOrShadowHostElement() &&
node->ParentOrShadowHostElement()->IsCanvasOrInCanvasSubtree()) {
if (!GetElement() || !GetElement()->HasDisplayContentsStyle()) {
return false;
}
}
if (ignored_reasons) {
ignored_reasons->push_back(IgnoredReason(kAXUninteresting));
}
return true;
}
if (node) {
const TextControlElement* text_control = EnclosingTextControl(node);
if (text_control) {
return text_control->InnerEditorElement() != node &&
text_control->InnerEditorElement() != NodeTraversal::Parent(*node);
}
}
if (GetLayoutObject()->IsLayoutEmbeddedContent()) {
return false;
}
if (node && node->IsInUserAgentShadowRoot()) {
Element* host = node->OwnerShadowHost();
if (auto* containing_media_element = DynamicTo<HTMLMediaElement>(host)) {
if (!containing_media_element->ShouldShowControls()) {
return true;
}
}
if (IsA<HTMLOptGroupElement>(host)) {
return false;
}
}
if (IsCanvas()) {
if (CanvasHasFallbackContent()) {
return false;
}
const auto* canvas = DynamicTo<LayoutHTMLCanvas>(GetLayoutObject());
if (canvas) {
PhysicalSize size = canvas->StitchedSize();
if (size.height <= 1 || size.width <= 1) {
if (ignored_reasons) {
ignored_reasons->push_back(IgnoredReason(kAXProbablyPresentational));
}
return true;
}
}
}
if (GetLayoutObject()->IsBR()) {
return false;
}
if (GetLayoutObject()->IsText()) {
if (GetLayoutObject()->IsInListMarker()) {
const AXObject* list_marker_object =
ContainerListMarkerIncludingIgnored();
if (list_marker_object &&
(list_marker_object->GetLayoutObject()->IsListMarkerForSummary() ||
!list_marker_object->IsIgnored())) {
if (ignored_reasons) {
ignored_reasons->push_back(IgnoredReason(kAXPresentational));
}
return true;
}
}
if (IsUsedForLabelOrDescription()) {
const AXObject* ancestor = ParentObject();
while (ancestor && ancestor->IsIgnored()) {
if (ancestor->RoleValue() == ax::mojom::blink::Role::kLabelText) {
if (ignored_reasons) {
ignored_reasons->push_back(IgnoredReason(kAXPresentational));
}
return true;
}
ancestor = ancestor->ParentObject();
}
}
return false;
}
if (GetLayoutObject()->IsListMarker()) {
if (GetLayoutObject()->IsListMarkerForSummary()) {
if (ignored_reasons) {
ignored_reasons->push_back(IgnoredReason(kAXPresentational));
}
return true;
}
return false;
}
if (!GetLayoutObject()->IsPseudoElement()) {
if (IsScrollableContainer()) {
return false;
}
if (GetLayoutObject()->IsPositioned()) {
return false;
}
}
if (auto* block_flow = DynamicTo<LayoutBlockFlow>(GetLayoutObject())) {
if (block_flow->ChildrenInline() && block_flow->FirstChild()) {
return false;
}
}
if (ignored_reasons) {
ignored_reasons->push_back(IgnoredReason(kAXUninteresting));
}
return true;
}
std::optional<String> AXNodeObject::GetCSSAltText(const Element* element) {
if (!element) {
return std::nullopt;
}
const ComputedStyle* style = element->GetComputedStyle();
if (!style || style->ContentBehavesAsNormal()) {
return std::nullopt;
}
if (element->IsPseudoElement()) {
for (const ContentData* content_data = style->GetContentData();
content_data; content_data = content_data->Next()) {
if (content_data->IsAlt()) {
return ContentData::ConcatenateAltText(*content_data);
}
}
return std::nullopt;
}
const ContentData* content_data = style->GetContentData();
if (content_data && content_data->IsImage() && content_data->Next() &&
content_data->Next()->IsAlt()) {
return ContentData::ConcatenateAltText(*content_data->Next());
}
return std::nullopt;
}
std::optional<String> AXNodeObject::GetCSSContentText(const Element* element) {
if (!element || !element->IsPseudoElement() || !element->GetLayoutObject()) {
return std::nullopt;
}
StringBuilder builder;
CollectLayoutTextContentRecursive(builder, element->GetLayoutObject());
return builder.ToString();
}
static HashSet<ax::mojom::blink::Role>& GetLandmarkIsNotAllowedAncestorRoles(
ax::mojom::blink::Role landmark) {
DEFINE_STATIC_LOCAL(
HashSet<ax::mojom::blink::Role>, complementary_is_not_allowed_roles,
({
ax::mojom::blink::Role::kArticle,
ax::mojom::blink::Role::kComplementary,
ax::mojom::blink::Role::kNavigation,
ax::mojom::blink::Role::kSection
}));
DEFINE_STATIC_LOCAL(
HashSet<ax::mojom::blink::Role>, landmark_is_not_allowed_roles,
({
ax::mojom::blink::Role::kArticle,
ax::mojom::blink::Role::kComplementary,
ax::mojom::blink::Role::kMain,
ax::mojom::blink::Role::kNavigation,
ax::mojom::blink::Role::kSection
}));
if (landmark == ax::mojom::blink::Role::kComplementary) {
return complementary_is_not_allowed_roles;
}
return landmark_is_not_allowed_roles;
}
bool AXNodeObject::IsDescendantOfLandmarkDisallowedElement() const {
if (!GetNode())
return false;
auto role_names = GetLandmarkIsNotAllowedAncestorRoles(RoleValue());
for (AXObject* parent = ParentObjectUnignored(); parent;
parent = parent->ParentObjectUnignored()) {
if (role_names.Contains(parent->RoleValue())) {
return true;
}
}
return false;
}
static bool IsNonEmptyNonHeaderCell(const Node* cell) {
return cell && cell->hasChildren() && cell->HasTagName(html_names::kTdTag);
}
static bool IsHeaderCell(const Node* cell) {
return cell && cell->HasTagName(html_names::kThTag);
}
static ax::mojom::blink::Role DecideRoleFromSiblings(Element* cell) {
const Node* next_cell = LayoutTreeBuilderTraversal::NextSibling(*cell);
const Node* previous_cell =
LayoutTreeBuilderTraversal::PreviousSibling(*cell);
if (!next_cell && !previous_cell)
return ax::mojom::blink::Role::kColumnHeader;
if (IsHeaderCell(next_cell) && IsHeaderCell(previous_cell))
return ax::mojom::blink::Role::kColumnHeader;
if (IsNonEmptyNonHeaderCell(next_cell) ||
IsNonEmptyNonHeaderCell(previous_cell))
return ax::mojom::blink::Role::kRowHeader;
const auto* row = DynamicTo<HTMLTableRowElement>(cell->parentNode());
if (!row)
return ax::mojom::blink::Role::kColumnHeader;
const Element* first_cell = ElementTraversal::FirstChild(*row);
DCHECK(first_cell);
const Element* last_cell = ElementTraversal::LastChild(*row);
DCHECK(last_cell);
if (IsNonEmptyNonHeaderCell(first_cell) || IsNonEmptyNonHeaderCell(last_cell))
return ax::mojom::blink::Role::kRowHeader;
if (IsNonEmptyNonHeaderCell(ElementTraversal::NextSibling(*first_cell)) ||
IsNonEmptyNonHeaderCell(ElementTraversal::PreviousSibling(*last_cell)))
return ax::mojom::blink::Role::kRowHeader;
return ax::mojom::blink::Role::kColumnHeader;
}
ax::mojom::blink::Role AXNodeObject::DetermineTableSectionRole() const {
if (!GetElement())
return ax::mojom::blink::Role::kGenericContainer;
AXObject* parent = GetDOMTableAXAncestor(GetNode(), AXObjectCache());
if (!parent || !parent->IsTableLikeRole())
return ax::mojom::blink::Role::kGenericContainer;
if (parent->RoleValue() == ax::mojom::blink::Role::kLayoutTable)
return ax::mojom::blink::Role::kGenericContainer;
return ax::mojom::blink::Role::kRowGroup;
}
ax::mojom::blink::Role AXNodeObject::DetermineTableRowRole() const {
AXObject* parent = GetDOMTableAXAncestor(GetNode(), AXObjectCache());
if (!parent || !parent->IsTableLikeRole())
return ax::mojom::blink::Role::kGenericContainer;
if (parent->RoleValue() == ax::mojom::blink::Role::kLayoutTable)
return ax::mojom::blink::Role::kLayoutTableRow;
return ax::mojom::blink::Role::kRow;
}
ax::mojom::blink::Role AXNodeObject::DetermineTableCellRole() const {
AXObject* parent = GetDOMTableAXAncestor(GetNode(), AXObjectCache());
if (!parent || !parent->IsTableRowLikeRole())
return ax::mojom::blink::Role::kGenericContainer;
AXObject* grandparent =
GetDOMTableAXAncestor(parent->GetNode(), AXObjectCache());
if (!grandparent || !grandparent->IsTableLikeRole())
return ax::mojom::blink::Role::kGenericContainer;
if (parent->RoleValue() == ax::mojom::blink::Role::kLayoutTableRow)
return ax::mojom::blink::Role::kLayoutTableCell;
if (!GetElement() || !GetNode()->HasTagName(html_names::kThTag))
return ax::mojom::blink::Role::kCell;
const AtomicString& scope =
GetElement()->FastGetAttribute(html_names::kScopeAttr);
if (EqualIgnoringASCIICase(scope, "row") ||
EqualIgnoringASCIICase(scope, "rowgroup"))
return ax::mojom::blink::Role::kRowHeader;
if (EqualIgnoringASCIICase(scope, "col") ||
EqualIgnoringASCIICase(scope, "colgroup"))
return ax::mojom::blink::Role::kColumnHeader;
return DecideRoleFromSiblings(GetElement());
}
unsigned AXNodeObject::ColumnCount() const {
if (RawAriaRole() != ax::mojom::blink::Role::kUnknown) {
return AXObject::ColumnCount();
}
if (const auto* table = DynamicTo<LayoutTable>(GetLayoutObject())) {
return table->EffectiveColumnCount();
}
return AXObject::ColumnCount();
}
unsigned AXNodeObject::RowCount() const {
if (RawAriaRole() != ax::mojom::blink::Role::kUnknown) {
return AXObject::RowCount();
}
LayoutTable* table;
auto* table_section = FirstTableSection(GetLayoutObject(), &table);
if (!table_section) {
return AXObject::RowCount();
}
unsigned row_count = 0;
while (table_section) {
row_count += table_section->NumRows();
table_section = table->NextSection(table_section);
}
return row_count;
}
unsigned AXNodeObject::ColumnIndex() const {
auto* cell = DynamicTo<LayoutTableCell>(GetLayoutObject());
if (cell && cell->GetNode()) {
return cell->Table()->AbsoluteColumnToEffectiveColumn(
cell->AbsoluteColumnIndex());
}
return AXObject::ColumnIndex();
}
unsigned AXNodeObject::RowIndex() const {
LayoutObject* layout_object = GetLayoutObject();
if (!layout_object || !layout_object->GetNode()) {
return AXObject::RowIndex();
}
unsigned row_index = 0;
const LayoutTableSection* row_section = nullptr;
const LayoutTable* table = nullptr;
if (const auto* row = DynamicTo<LayoutTableRow>(layout_object)) {
row_index = row->RowIndex();
row_section = row->Section();
table = row->Table();
} else if (const auto* cell = DynamicTo<LayoutTableCell>(layout_object)) {
row_index = cell->RowIndex();
row_section = cell->Section();
table = cell->Table();
} else {
return AXObject::RowIndex();
}
if (!table || !row_section) {
return AXObject::RowIndex();
}
const LayoutTableSection* section = table->FirstSection();
while (section && section != row_section) {
row_index += section->NumRows();
section = table->NextSection(section);
}
return row_index;
}
unsigned AXNodeObject::ColumnSpan() const {
auto* cell = DynamicTo<LayoutTableCell>(GetLayoutObject());
if (!cell) {
return AXObject::ColumnSpan();
}
LayoutTable* table = cell->Table();
unsigned absolute_first_col = cell->AbsoluteColumnIndex();
unsigned absolute_last_col = absolute_first_col + cell->ColSpan() - 1;
unsigned effective_first_col =
table->AbsoluteColumnToEffectiveColumn(absolute_first_col);
unsigned effective_last_col =
table->AbsoluteColumnToEffectiveColumn(absolute_last_col);
return effective_last_col - effective_first_col + 1;
}
unsigned AXNodeObject::RowSpan() const {
auto* cell = DynamicTo<LayoutTableCell>(GetLayoutObject());
return cell ? cell->ResolvedRowSpan() : AXObject::RowSpan();
}
ax::mojom::blink::SortDirection AXNodeObject::GetSortDirection() const {
if (RoleValue() != ax::mojom::blink::Role::kRowHeader &&
RoleValue() != ax::mojom::blink::Role::kColumnHeader) {
return ax::mojom::blink::SortDirection::kNone;
}
if (const AtomicString& aria_sort =
AriaTokenAttribute(html_names::kAriaSortAttr)) {
if (EqualIgnoringASCIICase(aria_sort, "none")) {
return ax::mojom::blink::SortDirection::kNone;
}
if (EqualIgnoringASCIICase(aria_sort, "ascending")) {
return ax::mojom::blink::SortDirection::kAscending;
}
if (EqualIgnoringASCIICase(aria_sort, "descending")) {
return ax::mojom::blink::SortDirection::kDescending;
}
return ax::mojom::blink::SortDirection::kOther;
}
return ax::mojom::blink::SortDirection::kNone;
}
AXObject* AXNodeObject::CellForColumnAndRow(unsigned target_column_index,
unsigned target_row_index) const {
LayoutTable* table;
auto* table_section = FirstTableSection(GetLayoutObject(), &table);
if (!table_section) {
return AXObject::CellForColumnAndRow(target_column_index, target_row_index);
}
unsigned row_offset = 0;
while (table_section) {
for (LayoutTableRow* row = table_section->LastRow(); row;
row = row->PreviousRow()) {
unsigned row_index = row->RowIndex() + row_offset;
for (LayoutTableCell* cell = row->LastCell(); cell;
cell = cell->PreviousCell()) {
unsigned absolute_first_col = cell->AbsoluteColumnIndex();
unsigned absolute_last_col = absolute_first_col + cell->ColSpan() - 1;
unsigned effective_first_col =
table->AbsoluteColumnToEffectiveColumn(absolute_first_col);
unsigned effective_last_col =
table->AbsoluteColumnToEffectiveColumn(absolute_last_col);
unsigned row_span = cell->ResolvedRowSpan();
if (target_column_index >= effective_first_col &&
target_column_index <= effective_last_col &&
target_row_index >= row_index &&
target_row_index < row_index + row_span) {
return AXObjectCache().Get(cell);
}
}
}
row_offset += table_section->NumRows();
table_section = table->NextSection(table_section);
}
return nullptr;
}
bool AXNodeObject::FindAllTableCellsWithRole(ax::mojom::blink::Role role,
AXObjectVector& cells) const {
LayoutTable* table;
auto* table_section = FirstTableSection(GetLayoutObject(), &table);
if (!table_section) {
return false;
}
while (table_section) {
for (LayoutTableRow* row = table_section->FirstRow(); row;
row = row->NextRow()) {
for (LayoutTableCell* cell = row->FirstCell(); cell;
cell = cell->NextCell()) {
AXObject* ax_cell = AXObjectCache().Get(cell);
if (ax_cell && ax_cell->RoleValue() == role) {
cells.push_back(ax_cell);
}
}
}
table_section = table->NextSection(table_section);
}
return true;
}
void AXNodeObject::ColumnHeaders(AXObjectVector& headers) const {
if (!FindAllTableCellsWithRole(ax::mojom::blink::Role::kColumnHeader,
headers)) {
AXObject::ColumnHeaders(headers);
}
}
void AXNodeObject::RowHeaders(AXObjectVector& headers) const {
if (!FindAllTableCellsWithRole(ax::mojom::blink::Role::kRowHeader, headers)) {
AXObject::RowHeaders(headers);
}
}
AXObject* AXNodeObject::HeaderObject() const {
auto* row = DynamicTo<LayoutTableRow>(GetLayoutObject());
if (!row) {
return nullptr;
}
for (LayoutTableCell* cell = row->FirstCell(); cell;
cell = cell->NextCell()) {
AXObject* ax_cell = cell ? AXObjectCache().Get(cell) : nullptr;
if (ax_cell && ax_cell->RoleValue() == ax::mojom::blink::Role::kRowHeader) {
return ax_cell;
}
}
return nullptr;
}
bool AXNodeObject::IsDataTable() const {
DCHECK(!IsDetached());
auto* table_element = DynamicTo<HTMLTableElement>(GetNode());
if (!table_element) {
return false;
}
if (!GetLayoutObject()) {
return true;
}
if (HasAriaAttribute(html_names::kRoleAttr)) {
return true;
}
if (GetNode() && blink::IsEditable(*GetNode())) {
return true;
}
if (!table_element->Summary().empty() || table_element->tHead() ||
table_element->tFoot() || table_element->caption()) {
return true;
}
if (!table_element->Rules().empty()) {
return true;
}
if (Traversal<HTMLTableColElement>::FirstChild(*table_element)) {
return true;
}
HTMLTableRowsCollection* rows = table_element->rows();
int num_rows = rows->length();
if (num_rows >= AXObjectCacheImpl::kDataTableHeuristicMinRows) {
return true;
}
if (num_rows <= 0) {
return false;
}
int num_cols_in_first_body = rows->Item(0)->cells()->length();
if (num_rows == 1 && num_cols_in_first_body == 1) {
return false;
}
const ComputedStyle* table_style = GetLayoutObject()->Style();
if (!table_style) {
return false;
}
Color table_bg_color =
table_style->VisitedDependentColor(GetCSSPropertyBackgroundColor());
bool has_cell_spacing = table_style->HorizontalBorderSpacing() &&
table_style->VerticalBorderSpacing();
unsigned valid_cell_count = 0;
unsigned bordered_cell_count = 0;
unsigned background_difference_cell_count = 0;
unsigned cells_with_top_border = 0;
unsigned cells_with_bottom_border = 0;
unsigned cells_with_left_border = 0;
unsigned cells_with_right_border = 0;
std::array<Color, 5> alternating_row_colors;
int alternating_row_color_count = 0;
for (int row = 0; row < num_rows; ++row) {
HTMLTableRowElement* row_element = rows->Item(row);
int n_cols = row_element->cells()->length();
for (int col = 0; col < n_cols; ++col) {
const Element* cell = row_element->cells()->item(col);
if (!cell) {
continue;
}
if (cell->HasTagName(html_names::kThTag)) {
return true;
}
auto* cell_elem = DynamicTo<HTMLTableCellElement>(*cell);
if (cell_elem) {
if (!cell_elem->Headers().empty() || !cell_elem->Abbr().empty() ||
!cell_elem->Axis().empty() ||
!cell_elem->FastGetAttribute(html_names::kScopeAttr).empty()) {
return true;
}
}
LayoutObject* cell_layout_object = cell->GetLayoutObject();
if (!cell_layout_object || !cell_layout_object->IsLayoutBlock()) {
continue;
}
const LayoutBlock* cell_layout_block =
To<LayoutBlock>(cell_layout_object);
PhysicalSize size = cell_layout_block->StitchedSize();
if (size.width < 1 || size.height < 1) {
continue;
}
valid_cell_count++;
const ComputedStyle* computed_style = cell_layout_block->Style();
if (!computed_style) {
continue;
}
if (computed_style->EmptyCells() == EEmptyCells::kHide) {
return true;
}
if ((cell_layout_block->BorderTop() > 0 &&
cell_layout_block->BorderBottom() > 0) ||
(cell_layout_block->BorderLeft() > 0 &&
cell_layout_block->BorderRight() > 0)) {
bordered_cell_count++;
}
if (cell_layout_block->BorderTop() > 0) {
cells_with_top_border++;
}
if (cell_layout_block->BorderBottom() > 0) {
cells_with_bottom_border++;
}
if (cell_layout_block->BorderLeft() > 0) {
cells_with_left_border++;
}
if (cell_layout_block->BorderRight() > 0) {
cells_with_right_border++;
}
Color cell_color = computed_style->VisitedDependentColor(
GetCSSPropertyBackgroundColor());
if (has_cell_spacing && table_bg_color != cell_color &&
!cell_color.IsFullyTransparent()) {
background_difference_cell_count++;
}
if (bordered_cell_count >= 10 || background_difference_cell_count >= 10) {
return true;
}
if (row < 5 && row == alternating_row_color_count) {
LayoutObject* layout_row = cell_layout_block->Parent();
if (!layout_row || !layout_row->IsBoxModelObject() ||
!layout_row->IsTableRow()) {
continue;
}
const ComputedStyle* row_computed_style = layout_row->Style();
if (!row_computed_style) {
continue;
}
Color row_color = row_computed_style->VisitedDependentColor(
GetCSSPropertyBackgroundColor());
alternating_row_colors[alternating_row_color_count] = row_color;
alternating_row_color_count++;
}
}
}
if (valid_cell_count <= 1) {
return false;
}
unsigned needed_cell_count = valid_cell_count / 2;
if (bordered_cell_count >= needed_cell_count ||
cells_with_top_border >= needed_cell_count ||
cells_with_bottom_border >= needed_cell_count ||
cells_with_left_border >= needed_cell_count ||
cells_with_right_border >= needed_cell_count) {
return true;
}
if (background_difference_cell_count >= needed_cell_count) {
return true;
}
if (alternating_row_color_count > 2) {
Color first_color = alternating_row_colors[0];
for (int k = 1; k < alternating_row_color_count; k++) {
if (k % 2 == 1 && alternating_row_colors[k] == first_color) {
return false;
}
if (!(k % 2) && alternating_row_colors[k] != first_color) {
return false;
}
}
return true;
}
return false;
}
ax::mojom::blink::Role AXNodeObject::RoleFromLayoutObjectOrNode() const {
if (!GetLayoutObject()) {
return ax::mojom::blink::Role::kGenericContainer;
}
DCHECK(GetLayoutObject());
if (GetLayoutObject()->IsListMarker()) {
Node* list_item = GetLayoutObject()->GeneratingNode();
if (list_item && ShouldIgnoreListItem(list_item)) {
return ax::mojom::blink::Role::kNone;
}
return ax::mojom::blink::Role::kListMarker;
}
if (GetLayoutObject()->IsListItem()) {
return ax::mojom::blink::Role::kListItem;
}
if (GetLayoutObject()->IsBR()) {
return ax::mojom::blink::Role::kLineBreak;
}
if (GetLayoutObject()->IsText()) {
return ax::mojom::blink::Role::kStaticText;
}
Node* node = GetNode();
if (node && !node->IsPseudoElement()) {
if (GetLayoutObject()->IsTable()) {
return ax::mojom::blink::Role::kLayoutTable;
}
if (GetLayoutObject()->IsTableSection()) {
return DetermineTableSectionRole();
}
if (GetLayoutObject()->IsTableRow()) {
return DetermineTableRowRole();
}
if (GetLayoutObject()->IsTableCell()) {
return DetermineTableCellRole();
}
}
if (IsImageOrAltText(GetLayoutObject(), node)) {
if (IsA<HTMLInputElement>(node)) {
return ButtonRoleType();
}
return ax::mojom::blink::Role::kImage;
}
if (IsA<HTMLCanvasElement>(node)) {
return ax::mojom::blink::Role::kCanvas;
}
if (IsA<LayoutView>(GetLayoutObject())) {
return ParentObject() ? ax::mojom::blink::Role::kGroup
: ax::mojom::blink::Role::kRootWebArea;
}
if (node && node->IsSVGElement()) {
if (GetLayoutObject()->IsSVGImage()) {
return ax::mojom::blink::Role::kImage;
}
if (IsA<SVGSVGElement>(node)) {
return GetLayoutObject()->IsSVGRoot() ? ax::mojom::blink::Role::kSvgRoot
: ax::mojom::blink::Role::kGroup;
}
if (IsA<SVGSymbolElement>(node)) {
if (GetLayoutObject()->IsSVGViewportContainer()) {
return ax::mojom::blink::Role::kGroup;
}
}
if (GetLayoutObject()->IsSVGShape()) {
return ax::mojom::blink::Role::kGraphicsSymbol;
}
if (GetLayoutObject()->IsSVGForeignObject()) {
return ax::mojom::blink::Role::kGroup;
}
if (IsA<SVGUseElement>(node)) {
return ax::mojom::blink::Role::kGraphicsObject;
}
}
if (GetLayoutObject()->IsHR()) {
return ax::mojom::blink::Role::kSplitter;
}
if (RuntimeEnabledFeatures::CSSScrollMarkerGroupModesEnabled() &&
IsOriginatingElementForScrollMarkerInTabsMode(node)) {
return ax::mojom::blink::Role::kTabPanel;
}
if (GetElement() && !HasAriaAttribute(html_names::kRoleAttr)) {
if (IsPopup() != ax::mojom::blink::IsPopup::kNone ||
GetElement()->FastHasAttribute(html_names::kAutofocusAttr) ||
GetElement()->FastHasAttribute(html_names::kDraggableAttr)) {
return ax::mojom::blink::Role::kGroup;
}
if (RuntimeEnabledFeatures::AccessibilityMinRoleTabbableEnabled()) {
if (GetElement()->tabIndex() >= 0) {
return ax::mojom::blink::Role::kGroup;
}
}
}
if (HasCustomElementTreeProcessing()) {
if (ElementHasAnyAriaRelation(*GetElement()) ||
GetElement()->tabIndex() >= 0) {
return ax::mojom::blink::Role::kGroup;
}
}
if (IsA<HTMLPermissionElement>(node)) {
return ax::mojom::blink::Role::kButton;
}
if (IsA<HTMLMenuBarElement>(node)) {
return ax::mojom::blink::Role::kMenuBar;
}
if (IsA<HTMLMenuItemElement>(node)) {
return ax::mojom::blink::Role::kMenuItem;
}
if (IsA<HTMLMenuListElement>(node)) {
return ax::mojom::blink::Role::kMenu;
}
return ax::mojom::blink::Role::kGenericContainer;
}
ax::mojom::blink::Role AXNodeObject::NativeRoleIgnoringAria() const {
if (!GetNode()) {
return RoleFromLayoutObjectOrNode();
}
if (GetNode()->IsPseudoElement()) {
if (GetNode()->IsScrollButtonPseudoElement()) {
return ax::mojom::blink::Role::kButton;
}
if (RuntimeEnabledFeatures::CSSScrollMarkerGroupModesEnabled()) {
if (const auto* scroll_marker_group =
DynamicTo<ScrollMarkerGroupPseudoElement>(GetNode())) {
return scroll_marker_group->ScrollMarkerGroupMode() ==
ScrollMarkerGroup::ScrollMarkerMode::kTabs
? ax::mojom::blink::Role::kTabList
: ax::mojom::blink::Role::kNavigation;
}
if (const auto* scroll_marker =
DynamicTo<ScrollMarkerPseudoElement>(GetNode())) {
CHECK(scroll_marker->ScrollMarkerGroup());
return scroll_marker->ScrollMarkerGroup()->ScrollMarkerGroupMode() ==
ScrollMarkerGroup::ScrollMarkerMode::kTabs
? ax::mojom::blink::Role::kTab
: ax::mojom::blink::Role::kLink;
}
} else {
if (GetNode()->IsScrollMarkerGroupPseudoElement()) {
return ax::mojom::blink::Role::kTabList;
}
if (GetNode()->IsScrollMarkerPseudoElement()) {
return ax::mojom::blink::Role::kTab;
}
}
if (GetCSSAltText(GetElement())) {
const ComputedStyle* style = GetElement()->GetComputedStyle();
ContentData* content_data = style->GetContentData();
if (content_data->IsImage()) {
return ax::mojom::blink::Role::kImage;
}
return ax::mojom::blink::Role::kStaticText;
}
}
if (GetNode()->IsTextNode())
return ax::mojom::blink::Role::kStaticText;
if (IsA<HTMLImageElement>(GetNode()))
return ax::mojom::blink::Role::kImage;
if (IsA<HTMLAnchorElement>(GetNode()) || IsA<SVGAElement>(GetNode())) {
if (GetNode()->IsLink() ||
GetNode()->HasAnyEventListeners(event_util::MouseButtonEventTypes())) {
return ax::mojom::blink::Role::kLink;
}
if (IsA<SVGAElement>(GetNode()) &&
!Traversal<SVGTextElement>::FirstAncestor(*GetNode())) {
return ax::mojom::blink::Role::kGroup;
}
return ax::mojom::blink::Role::kGenericContainer;
}
if (IsA<SVGGElement>(*GetNode())) {
return ax::mojom::blink::Role::kGroup;
}
if (IsA<HTMLButtonElement>(*GetNode()))
return ButtonRoleType();
if (IsA<HTMLDetailsElement>(*GetNode()))
return ax::mojom::blink::Role::kDetails;
if (IsA<HTMLSummaryElement>(*GetNode())) {
ContainerNode* parent = LayoutTreeBuilderTraversal::Parent(*GetNode());
if (ToHTMLSlotElementIfSupportsAssignmentOrNull(parent))
parent = LayoutTreeBuilderTraversal::Parent(*parent);
if (HTMLDetailsElement* parent_details =
DynamicTo<HTMLDetailsElement>(parent)) {
if (parent_details->GetName().empty()) {
return ax::mojom::blink::Role::kDisclosureTriangle;
} else {
return ax::mojom::blink::Role::kDisclosureTriangleGrouped;
}
}
return ax::mojom::blink::Role::kGenericContainer;
}
if (IsA<HTMLTableElement>(*GetNode())) {
if (IsDataTable())
return ax::mojom::blink::Role::kTable;
else
return ax::mojom::blink::Role::kLayoutTable;
}
if (IsA<HTMLTableRowElement>(*GetNode()))
return DetermineTableRowRole();
if (IsA<HTMLTableCellElement>(*GetNode()))
return DetermineTableCellRole();
if (IsA<HTMLTableSectionElement>(*GetNode()))
return DetermineTableSectionRole();
if (const auto* input = DynamicTo<HTMLInputElement>(*GetNode())) {
FormControlType type = input->FormControlType();
if (input->DataList() && type != FormControlType::kInputColor) {
return ax::mojom::blink::Role::kTextFieldWithComboBox;
}
switch (type) {
case FormControlType::kInputButton:
case FormControlType::kInputReset:
case FormControlType::kInputSubmit:
return ButtonRoleType();
case FormControlType::kInputCheckbox:
return ax::mojom::blink::Role::kCheckBox;
case FormControlType::kInputDate:
return ax::mojom::blink::Role::kDate;
case FormControlType::kInputDatetimeLocal:
case FormControlType::kInputMonth:
case FormControlType::kInputWeek:
return ax::mojom::blink::Role::kDateTime;
case FormControlType::kInputFile:
return ax::mojom::blink::Role::kButton;
case FormControlType::kInputRadio:
return ax::mojom::blink::Role::kRadioButton;
case FormControlType::kInputNumber:
return ax::mojom::blink::Role::kSpinButton;
case FormControlType::kInputRange:
return ax::mojom::blink::Role::kSlider;
case FormControlType::kInputSearch:
return ax::mojom::blink::Role::kSearchBox;
case FormControlType::kInputColor:
return ax::mojom::blink::Role::kColorWell;
case FormControlType::kInputTime:
return ax::mojom::blink::Role::kInputTime;
case FormControlType::kInputImage:
return ax::mojom::blink::Role::kButton;
default:
return ax::mojom::blink::Role::kTextField;
}
}
if (auto* select_element = DynamicTo<HTMLSelectElement>(*GetNode())) {
if (select_element->UsesMenuList()) {
return ax::mojom::blink::Role::kComboBoxSelect;
} else {
return ax::mojom::blink::Role::kListBox;
}
}
if (ParentObjectIfPresent() && ParentObjectIfPresent()->RoleValue() ==
ax::mojom::blink::Role::kComboBoxSelect) {
if (HTMLSelectElement::IsPopoverPickerElement(GetNode())) {
return ax::mojom::blink::Role::kMenuListPopup;
}
}
if (auto* option = DynamicTo<HTMLOptionElement>(*GetNode())) {
HTMLSelectElement* select_element = option->OwnerSelectElement();
if (select_element && select_element->UsesMenuList()) {
return ax::mojom::blink::Role::kMenuListOption;
} else {
return ax::mojom::blink::Role::kListBoxOption;
}
}
if (IsA<HTMLOptGroupElement>(GetNode())) {
return ax::mojom::blink::Role::kGroup;
}
if (IsA<HTMLTextAreaElement>(*GetNode()))
return ax::mojom::blink::Role::kTextField;
if (HeadingLevel())
return ax::mojom::blink::Role::kHeading;
if (IsA<HTMLDivElement>(*GetNode()))
return RoleFromLayoutObjectOrNode();
if (IsA<HTMLMenuElement>(*GetNode()) || IsA<HTMLUListElement>(*GetNode()) ||
IsA<HTMLOListElement>(*GetNode())) {
return ax::mojom::blink::Role::kList;
}
if (IsA<HTMLLIElement>(*GetNode())) {
if (ShouldIgnoreListItem(GetNode())) {
return ax::mojom::blink::Role::kNone;
}
return ax::mojom::blink::Role::kListItem;
}
if (IsA<HTMLMeterElement>(*GetNode()))
return ax::mojom::blink::Role::kMeter;
if (IsA<HTMLProgressElement>(*GetNode()))
return ax::mojom::blink::Role::kProgressIndicator;
if (IsA<HTMLOutputElement>(*GetNode()))
return ax::mojom::blink::Role::kStatus;
if (IsA<HTMLParagraphElement>(*GetNode()))
return ax::mojom::blink::Role::kParagraph;
if (IsA<HTMLLabelElement>(*GetNode()))
return ax::mojom::blink::Role::kLabelText;
if (IsA<HTMLLegendElement>(*GetNode()))
return ax::mojom::blink::Role::kLegend;
if (GetNode()->HasTagName(html_names::kRubyTag)) {
return ax::mojom::blink::Role::kRuby;
}
if (IsA<HTMLDListElement>(*GetNode()))
return ax::mojom::blink::Role::kDescriptionList;
if (IsA<HTMLDirectoryElement>(*GetNode())) {
return ax::mojom::blink::Role::kList;
}
if (IsA<HTMLAudioElement>(*GetNode()))
return ax::mojom::blink::Role::kAudio;
if (IsA<HTMLVideoElement>(*GetNode()))
return ax::mojom::blink::Role::kVideo;
if (GetNode()->HasTagName(html_names::kDdTag))
return ax::mojom::blink::Role::kDefinition;
if (GetNode()->HasTagName(html_names::kDfnTag))
return ax::mojom::blink::Role::kTerm;
if (GetNode()->HasTagName(html_names::kDtTag))
return ax::mojom::blink::Role::kTerm;
if (auto* element = DynamicTo<MathMLElement>(GetNode())) {
if (element->HasTagName(mathml_names::kMathTag)) {
return ax::mojom::blink::Role::kMathMLMath;
}
if (element->HasTagName(mathml_names::kMfracTag))
return ax::mojom::blink::Role::kMathMLFraction;
if (element->HasTagName(mathml_names::kMiTag))
return ax::mojom::blink::Role::kMathMLIdentifier;
if (element->HasTagName(mathml_names::kMmultiscriptsTag))
return ax::mojom::blink::Role::kMathMLMultiscripts;
if (element->HasTagName(mathml_names::kMnTag))
return ax::mojom::blink::Role::kMathMLNumber;
if (element->HasTagName(mathml_names::kMoTag))
return ax::mojom::blink::Role::kMathMLOperator;
if (element->HasTagName(mathml_names::kMoverTag))
return ax::mojom::blink::Role::kMathMLOver;
if (element->HasTagName(mathml_names::kMunderTag))
return ax::mojom::blink::Role::kMathMLUnder;
if (element->HasTagName(mathml_names::kMunderoverTag))
return ax::mojom::blink::Role::kMathMLUnderOver;
if (element->HasTagName(mathml_names::kMrootTag))
return ax::mojom::blink::Role::kMathMLRoot;
if (element->HasTagName(mathml_names::kMrowTag) ||
element->HasTagName(mathml_names::kAnnotationXmlTag) ||
element->HasTagName(mathml_names::kMactionTag) ||
element->HasTagName(mathml_names::kMerrorTag) ||
element->HasTagName(mathml_names::kMpaddedTag) ||
element->HasTagName(mathml_names::kMphantomTag) ||
element->HasTagName(mathml_names::kMstyleTag) ||
element->HasTagName(mathml_names::kSemanticsTag)) {
return ax::mojom::blink::Role::kMathMLRow;
}
if (element->HasTagName(mathml_names::kMprescriptsTag))
return ax::mojom::blink::Role::kMathMLPrescriptDelimiter;
if (element->HasTagName(mathml_names::kNoneTag))
return ax::mojom::blink::Role::kMathMLNoneScript;
if (element->HasTagName(mathml_names::kMsqrtTag))
return ax::mojom::blink::Role::kMathMLSquareRoot;
if (element->HasTagName(mathml_names::kMsTag))
return ax::mojom::blink::Role::kMathMLStringLiteral;
if (element->HasTagName(mathml_names::kMsubTag))
return ax::mojom::blink::Role::kMathMLSub;
if (element->HasTagName(mathml_names::kMsubsupTag))
return ax::mojom::blink::Role::kMathMLSubSup;
if (element->HasTagName(mathml_names::kMsupTag))
return ax::mojom::blink::Role::kMathMLSup;
if (element->HasTagName(mathml_names::kMtableTag))
return ax::mojom::blink::Role::kMathMLTable;
if (element->HasTagName(mathml_names::kMtdTag))
return ax::mojom::blink::Role::kMathMLTableCell;
if (element->HasTagName(mathml_names::kMtrTag))
return ax::mojom::blink::Role::kMathMLTableRow;
if (element->HasTagName(mathml_names::kMtextTag) ||
element->HasTagName(mathml_names::kAnnotationTag)) {
return ax::mojom::blink::Role::kMathMLText;
}
}
if (GetNode()->HasTagName(html_names::kRpTag) ||
GetNode()->HasTagName(html_names::kRtTag)) {
return ax::mojom::blink::Role::kRubyAnnotation;
}
if (IsA<HTMLFormElement>(*GetNode())) {
return IsNameFromAuthorAttribute() ? ax::mojom::blink::Role::kForm
: ax::mojom::blink::Role::kSection;
}
if (GetNode()->HasTagName(html_names::kAbbrTag))
return ax::mojom::blink::Role::kAbbr;
if (GetNode()->HasTagName(html_names::kArticleTag))
return ax::mojom::blink::Role::kArticle;
if (GetNode()->HasTagName(html_names::kCodeTag))
return ax::mojom::blink::Role::kCode;
if (GetNode()->HasTagName(html_names::kEmTag))
return ax::mojom::blink::Role::kEmphasis;
if (GetNode()->HasTagName(html_names::kStrongTag))
return ax::mojom::blink::Role::kStrong;
if (GetNode()->HasTagName(html_names::kSearchTag)) {
return ax::mojom::blink::Role::kSearch;
}
if (GetNode()->HasTagName(html_names::kDelTag) ||
GetNode()->HasTagName(html_names::kSTag)) {
return ax::mojom::blink::Role::kContentDeletion;
}
if (GetNode()->HasTagName(html_names::kInsTag))
return ax::mojom::blink::Role::kContentInsertion;
if (GetNode()->HasTagName(html_names::kSubTag))
return ax::mojom::blink::Role::kSubscript;
if (GetNode()->HasTagName(html_names::kSupTag))
return ax::mojom::blink::Role::kSuperscript;
if (GetNode()->HasTagName(html_names::kMainTag))
return ax::mojom::blink::Role::kMain;
if (GetNode()->HasTagName(html_names::kMarkTag))
return ax::mojom::blink::Role::kMark;
if (GetNode()->HasTagName(html_names::kNavTag))
return ax::mojom::blink::Role::kNavigation;
if (GetNode()->HasTagName(html_names::kAsideTag))
return ax::mojom::blink::Role::kComplementary;
if (GetNode()->HasTagName(html_names::kSectionTag)) {
return ax::mojom::blink::Role::kSection;
}
if (GetNode()->HasTagName(html_names::kAddressTag))
return ax::mojom::blink::Role::kGroup;
if (GetNode()->HasTagName(html_names::kHgroupTag)) {
return ax::mojom::blink::Role::kGroup;
}
if (IsA<HTMLDialogElement>(*GetNode()))
return ax::mojom::blink::Role::kDialog;
if (IsA<HTMLHtmlElement>(GetNode()))
return ax::mojom::blink::Role::kGenericContainer;
if (IsFrame(GetNode()))
return ax::mojom::blink::Role::kIframe;
if (GetNode()->HasTagName(html_names::kHeaderTag)) {
return ax::mojom::blink::Role::kHeader;
}
if (GetNode()->HasTagName(html_names::kFooterTag)) {
return ax::mojom::blink::Role::kFooter;
}
if (GetNode()->HasTagName(html_names::kBlockquoteTag))
return ax::mojom::blink::Role::kBlockquote;
if (IsA<HTMLTableCaptionElement>(GetNode()))
return ax::mojom::blink::Role::kCaption;
if (GetNode()->HasTagName(html_names::kFigcaptionTag))
return ax::mojom::blink::Role::kFigcaption;
if (GetNode()->HasTagName(html_names::kFigureTag))
return ax::mojom::blink::Role::kFigure;
if (IsA<HTMLTimeElement>(GetNode()))
return ax::mojom::blink::Role::kTime;
if (IsA<HTMLPlugInElement>(GetNode())) {
if (IsA<HTMLEmbedElement>(GetNode()))
return ax::mojom::blink::Role::kEmbeddedObject;
return ax::mojom::blink::Role::kPluginObject;
}
if (IsA<HTMLHRElement>(*GetNode()))
return ax::mojom::blink::Role::kSplitter;
if (IsFieldset())
return ax::mojom::blink::Role::kGroup;
return RoleFromLayoutObjectOrNode();
}
ax::mojom::blink::Role AXNodeObject::DetermineRoleValue() {
#if DCHECK_IS_ON()
base::AutoReset<bool> reentrancy_protector(&is_computing_role_, true);
#endif
if (IsDetached()) {
NOTREACHED() << "Do not compute role on detached object: " << this;
}
native_role_ = NativeRoleIgnoringAria();
aria_role_ = DetermineAriaRole();
const Element* element = GetElement();
if (element &&
RuntimeEnabledFeatures::FocusgroupEnabled(
element->GetExecutionContext()) &&
aria_role_ == ax::mojom::blink::Role::kUnknown &&
native_role_ == ax::mojom::blink::Role::kGenericContainer && element) {
const FocusgroupData data = element->GetFocusgroupData();
if (focusgroup::IsActualFocusgroup(data)) {
aria_role_ = focusgroup::FocusgroupMinimumAriaRole(data);
}
}
return aria_role_ == ax::mojom::blink::Role::kUnknown ? native_role_
: aria_role_;
}
static Element* SiblingWithAriaRole(String role, Node* node) {
Node* parent = LayoutTreeBuilderTraversal::Parent(*node);
if (!parent)
return nullptr;
for (Node* sibling = LayoutTreeBuilderTraversal::FirstChild(*parent); sibling;
sibling = LayoutTreeBuilderTraversal::NextSibling(*sibling)) {
auto* element = DynamicTo<Element>(sibling);
if (!element)
continue;
const AtomicString& sibling_aria_role =
blink::AXObject::AriaAttribute(*element, html_names::kRoleAttr);
if (EqualIgnoringASCIICase(sibling_aria_role, role))
return element;
}
return nullptr;
}
Element* AXNodeObject::MenuItemElementForMenu() const {
if (RawAriaRole() != ax::mojom::blink::Role::kMenu) {
return nullptr;
}
return SiblingWithAriaRole("menuitem", GetNode());
}
void AXNodeObject::Init(AXObject* parent) {
#if DCHECK_IS_ON()
DCHECK(!initialized_);
initialized_ = true;
#endif
AXObject::Init(parent);
DCHECK(role_ == native_role_ || role_ == aria_role_)
<< "Role must be either the cached native role or cached aria role: "
<< "\n* Final role: " << role_ << "\n* Native role: " << native_role_
<< "\n* Aria role: " << aria_role_ << "\n* Node: " << GetNode();
DCHECK(node_ || (GetLayoutObject() &&
AXObjectCacheImpl::IsRelevantPseudoElementDescendant(
*GetLayoutObject())))
<< "Nodeless AXNodeObject can only exist inside a pseudo-element: "
<< GetLayoutObject();
}
void AXNodeObject::Detach() {
#if AX_FAIL_FAST_BUILD()
SANITIZER_CHECK(!is_adding_children_)
<< "Cannot detach |this| during AddChildren(): " << GetNode();
#endif
AXObject::Detach();
node_ = nullptr;
#if DCHECK_IS_ON()
if (layout_object_) {
layout_object_->SetHasAXObject(false);
}
#endif
layout_object_ = nullptr;
}
bool AXNodeObject::IsAXNodeObject() const {
return true;
}
bool AXNodeObject::IsControl() const {
Node* node = GetNode();
if (!node)
return false;
auto* element = DynamicTo<Element>(node);
return ((element && element->IsFormControlElement()) ||
ui::IsControl(RawAriaRole()));
}
bool AXNodeObject::IsAutofillAvailable() const {
WebAXAutofillSuggestionAvailability suggestion_availability =
AXObjectCache().GetAutofillSuggestionAvailability(AXObjectID());
return suggestion_availability ==
WebAXAutofillSuggestionAvailability::kAutofillAvailable;
}
bool AXNodeObject::IsDefault() const {
if (IsDetached())
return false;
if (Restriction() == kRestrictionDisabled ||
RoleValue() != ax::mojom::blink::Role::kButton) {
return false;
}
return GetElement()->MatchesDefaultPseudoClass();
}
bool AXNodeObject::IsFieldset() const {
return IsA<HTMLFieldSetElement>(GetNode());
}
bool AXNodeObject::IsHovered() const {
if (Node* node = GetNode())
return node->IsHovered();
return false;
}
bool AXNodeObject::IsImageButton() const {
return IsNativeImage() && IsButton();
}
bool AXNodeObject::IsInputImage() const {
auto* html_input_element = DynamicTo<HTMLInputElement>(GetNode());
if (html_input_element && RoleValue() == ax::mojom::blink::Role::kButton) {
return html_input_element->FormControlType() ==
FormControlType::kInputImage;
}
return false;
}
bool AXNodeObject::IsLineBreakingObject() const {
if (IsDetached() || !GetNode())
return false;
if (IsPresentational())
return false;
if (IsEnclosingBlock(GetNode()))
return true;
if (IsA<HTMLBRElement>(GetNode()))
return true;
const LayoutObject* layout_object = GetLayoutObject();
if (!layout_object)
return AXObject::IsLineBreakingObject();
if (layout_object->IsBR())
return true;
if (const LayoutText* layout_text = DynamicTo<LayoutText>(layout_object)) {
const ComputedStyle& style = layout_object->StyleRef();
if (layout_text->HasNonCollapsedText() && style.ShouldPreserveBreaks() &&
layout_text->PlainText().find('\n') != kNotFound) {
return true;
}
}
return AXObject::IsLineBreakingObject();
}
bool AXNodeObject::IsLoaded() const {
if (!GetDocument())
return false;
if (!GetDocument()->IsLoadCompleted())
return false;
if (auto* window = GetDocument()->domWindow()) {
if (window->navigation()->HasNonDroppedOngoingNavigation())
return false;
}
return true;
}
bool AXNodeObject::IsMultiSelectable() const {
if (RoleSupportsAriaAttribute(RoleValue(),
html_names::kAriaMultiselectableAttr)) {
bool multiselectable;
if (AriaBooleanAttribute(html_names::kAriaMultiselectableAttr,
&multiselectable)) {
return multiselectable;
}
}
auto* html_select_element = DynamicTo<HTMLSelectElement>(GetNode());
return html_select_element && html_select_element->IsMultiple();
}
bool AXNodeObject::IsNativeImage() const {
Node* node = GetNode();
if (!node)
return false;
if (IsA<HTMLImageElement>(*node) || IsA<HTMLPlugInElement>(*node))
return true;
if (const auto* input = DynamicTo<HTMLInputElement>(*node))
return input->FormControlType() == FormControlType::kInputImage;
return false;
}
bool AXNodeObject::IsVisible() const {
if (RoleValue() == ax::mojom::blink::Role::kMenuListPopup) {
CHECK(parent_);
return parent_->IsExpanded() == kExpandedExpanded;
}
if (IsRoot()) {
return true;
}
if (const AXObject* ax_select = ParentObject()->AncestorMenuList()) {
if (!ax_select->IsVisible()) {
return false;
}
if (ax_select->IsExpanded() == kExpandedCollapsed) {
if (const AXObject* ax_option = AncestorMenuListOption()) {
return ax_option->IsSelected() == kSelectedStateTrue;
}
return false;
}
}
return AXObject::IsVisible();
}
bool AXNodeObject::IsLinked() const {
if (!IsLinkable(*this)) {
return false;
}
if (auto* anchor = DynamicTo<HTMLAnchorElementBase>(AnchorElement())) {
return !anchor->Href().IsEmpty();
}
return false;
}
bool AXNodeObject::IsVisited() const {
return GetLayoutObject() && GetLayoutObject()->Style()->IsLink() &&
GetLayoutObject()->Style()->InsideLink() ==
EInsideLink::kInsideVisitedLink;
}
bool AXNodeObject::IsProgressIndicator() const {
return RoleValue() == ax::mojom::blink::Role::kProgressIndicator;
}
bool AXNodeObject::IsSlider() const {
return RoleValue() == ax::mojom::blink::Role::kSlider;
}
bool AXNodeObject::IsSpinButton() const {
return RoleValue() == ax::mojom::blink::Role::kSpinButton;
}
bool AXNodeObject::IsNativeSlider() const {
if (const auto* input = DynamicTo<HTMLInputElement>(GetNode()))
return input->FormControlType() == FormControlType::kInputRange;
return false;
}
bool AXNodeObject::IsNativeSpinButton() const {
if (const auto* input = DynamicTo<HTMLInputElement>(GetNode()))
return input->FormControlType() == FormControlType::kInputNumber;
return false;
}
bool AXNodeObject::IsEmbeddingElement() const {
return ui::IsEmbeddingElement(native_role_);
}
bool AXNodeObject::IsClickable() const {
if (GetNode() == GetDocument()->documentElement()) {
return GetNode()->HasAnyEventListeners(
event_util::MouseButtonEventTypes()) ||
GetDocument()->HasAnyEventListeners(
event_util::MouseButtonEventTypes()) ||
GetDocument()->domWindow()->HasAnyEventListeners(
event_util::MouseButtonEventTypes());
}
const Element* element = GetElement();
if (!element)
return false;
if (IsDisabled())
return false;
if (element->HasAnyEventListeners(event_util::MouseButtonEventTypes()))
return true;
if (HasContentEditableAttributeSet())
return true;
if (element->ShadowPseudoId() ==
shadow_element_names::kPseudoCalendarPickerIndicator) {
return true;
}
return ui::IsClickable(native_role_);
}
bool AXNodeObject::IsFocused() const {
if (!GetDocument())
return false;
if (IsWebArea() &&
GetDocument()->GetFrame()->Selection().FrameIsFocusedAndActive()) {
return true;
}
Element* focused_element = GetDocument()->FocusedElement();
return focused_element && focused_element == GetElement();
}
AccessibilitySelectedState AXNodeObject::IsSelected() const {
if (!GetNode() || !IsSubWidget()) {
return kSelectedStateUndefined;
}
bool is_selected;
if (AriaBooleanAttribute(html_names::kAriaSelectedAttr, &is_selected)) {
return is_selected ? kSelectedStateTrue : kSelectedStateFalse;
}
if (!ui::IsSelectRequiredOrImplicit(RoleValue()))
return kSelectedStateUndefined;
if (IsTabItem() && GetNode()->IsScrollMarkerPseudoElement()) {
return To<ScrollMarkerPseudoElement>(GetNode())->IsSelected()
? kSelectedStateTrue
: kSelectedStateFalse;
}
if (auto* option_element = DynamicTo<HTMLOptionElement>(GetNode())) {
if (!CanSetSelectedAttribute()) {
return kSelectedStateUndefined;
}
return (option_element->Selected()) ? kSelectedStateTrue
: kSelectedStateFalse;
}
return IsSelectedFromFocus() ? kSelectedStateTrue : kSelectedStateFalse;
}
bool AXNodeObject::IsSelectedFromFocusSupported() const {
if (!ui::IsSelectRequiredOrImplicit(RoleValue()))
return false;
const AXObject* container = ContainerWidget();
if (!container || container->IsMultiSelectable()) {
return false;
}
if (!AXObjectCache().IsImplicitSelectionAllowed(container)) {
return false;
}
return true;
}
bool AXNodeObject::IsSelectedFromFocus() const {
if (IsTabItem() && IsSelectedFromFocusSupported() && IsTabItemSelected()) {
return true;
}
AXObject* focused_object = AXObjectCache().FocusedObject();
if (focused_object != this &&
(!focused_object || focused_object->ActiveDescendant() != this))
return false;
return IsSelectedFromFocusSupported();
}
bool AXNodeObject::IsNotUserSelectable() const {
if (!GetLayoutObject()) {
return false;
}
if (IsA<PseudoElement>(GetClosestElement())) {
return true;
}
const ComputedStyle* style = GetLayoutObject()->Style();
if (!style) {
return false;
}
return (style->UsedUserSelect() == EUserSelect::kNone);
}
bool AXNodeObject::IsTabItemSelected() const {
if (!IsTabItem() || !GetLayoutObject())
return false;
if (GetNode()->IsScrollMarkerPseudoElement()) {
return To<ScrollMarkerPseudoElement>(GetNode())->IsSelected();
}
Node* node = GetNode();
if (!node || !node->IsElementNode())
return false;
AXObject* focused_element = AXObjectCache().FocusedObject();
if (!focused_element)
return false;
DCHECK(GetElement());
const GCedHeapVector<Member<Element>>* elements =
AXObject::ElementsFromAttributeOrInternals(GetElement(),
html_names::kAriaControlsAttr);
if (!elements) {
return false;
}
for (const auto& element : *elements) {
AXObject* tab_panel = AXObjectCache().Get(element);
if (!tab_panel ||
tab_panel->RoleValue() != ax::mojom::blink::Role::kTabPanel) {
continue;
}
AXObject* check_focus_element = focused_element;
while (check_focus_element) {
if (tab_panel == check_focus_element)
return true;
check_focus_element = check_focus_element->ParentObject();
}
}
return false;
}
AXRestriction AXNodeObject::Restriction() const {
Element* elem = GetElement();
if (!elem)
return kRestrictionNone;
if (IsA<HTMLOptGroupElement>(elem))
return kRestrictionNone;
if (IsDisabled())
return kRestrictionDisabled;
auto* text_area_element = DynamicTo<HTMLTextAreaElement>(*elem);
if (text_area_element && text_area_element->IsReadOnly())
return kRestrictionReadOnly;
if (const auto* input = DynamicTo<HTMLInputElement>(*elem)) {
if (input->IsTextField() && input->IsReadOnly())
return kRestrictionReadOnly;
}
bool is_read_only;
if (SupportsARIAReadOnly() &&
AriaBooleanAttribute(html_names::kAriaReadonlyAttr, &is_read_only)) {
return is_read_only ? kRestrictionReadOnly : kRestrictionNone;
}
if (IsTableCellLikeRole()) {
AXObject* row = ParentObjectUnignored();
if (row && row->IsTableRowLikeRole()) {
AXObject* table = row->ParentObjectUnignored();
if (table && table->IsTableLikeRole() &&
(table->RoleValue() == ax::mojom::blink::Role::kGrid ||
table->RoleValue() == ax::mojom::blink::Role::kTreeGrid)) {
if (table->Restriction() == kRestrictionReadOnly)
return kRestrictionReadOnly;
}
}
}
return kRestrictionNone;
}
AccessibilityExpanded AXNodeObject::IsExpanded() const {
if (!SupportsARIAExpanded())
return kExpandedUndefined;
auto* element = GetElement();
if (!element)
return kExpandedUndefined;
if (RoleValue() == ax::mojom::blink::Role::kComboBoxSelect) {
DCHECK(IsA<HTMLSelectElement>(element));
bool is_expanded = To<HTMLSelectElement>(element)->PopupIsVisible();
return is_expanded ? kExpandedExpanded : kExpandedCollapsed;
}
if (auto* html_element = DynamicTo<HTMLElement>(element)) {
if (HTMLElement* command_for_element =
DynamicTo<HTMLElement>(html_element->commandForElement())) {
CommandEventType command = command_for_element->GetCommandEventType(
html_element->command(), html_element->GetExecutionContext());
bool is_popover_command =
command_for_element->IsValidBuiltinPopoverCommand(*html_element,
command);
if (command_for_element && is_popover_command &&
!element->IsDescendantOrShadowDescendantOf(command_for_element)) {
return command_for_element->popoverOpen() ? kExpandedExpanded
: kExpandedCollapsed;
}
}
}
if (auto* form_control = DynamicTo<HTMLFormControlElement>(element)) {
if (auto popover = form_control->popoverTargetElement().popover) {
if (!form_control->IsDescendantOrShadowDescendantOf(popover)) {
return popover->popoverOpen() ? kExpandedExpanded : kExpandedCollapsed;
}
}
}
if (IsA<HTMLSummaryElement>(*element)) {
if (element->parentNode() &&
IsA<HTMLDetailsElement>(element->parentNode())) {
return To<Element>(element->parentNode())
->FastHasAttribute(html_names::kOpenAttr)
? kExpandedExpanded
: kExpandedCollapsed;
}
}
bool expanded = false;
if (AriaBooleanAttribute(html_names::kAriaExpandedAttr, &expanded)) {
return expanded ? kExpandedExpanded : kExpandedCollapsed;
}
return kExpandedUndefined;
}
bool AXNodeObject::IsRequired() const {
auto* form_control = DynamicTo<HTMLFormControlElement>(GetNode());
if (form_control && form_control->IsRequired())
return true;
if (RoleSupportsAriaAttribute(RoleValue(), html_names::kAriaRequiredAttr)) {
if (IsAriaAttributeTrue(html_names::kAriaRequiredAttr)) {
return true;
}
}
if (RoleValue() == ax::mojom::blink::Role::kRadioButton) {
if (IsAriaAttributeTrue(html_names::kAriaRequiredAttr)) {
return true;
}
}
return false;
}
bool AXNodeObject::CanvasHasFallbackContent() const {
if (IsDetached())
return false;
Node* node = GetNode();
return IsA<HTMLCanvasElement>(node) && node->hasChildren();
}
int AXNodeObject::HeadingLevel() const {
Node* node = GetNode();
if (!node)
return 0;
if (RoleValue() == ax::mojom::blink::Role::kHeading) {
int32_t level;
if (AriaIntAttribute(html_names::kAriaLevelAttr, &level)) {
if (level >= 1 && level <= 9) {
return level;
}
}
}
auto* element = DynamicTo<HTMLElement>(node);
if (!element)
return 0;
if (element->HasTagName(html_names::kH1Tag))
return 1 + element->GetComputedHeadingOffset(8);
if (element->HasTagName(html_names::kH2Tag))
return 2 + element->GetComputedHeadingOffset(7);
if (element->HasTagName(html_names::kH3Tag))
return 3 + element->GetComputedHeadingOffset(6);
if (element->HasTagName(html_names::kH4Tag))
return 4 + element->GetComputedHeadingOffset(5);
if (element->HasTagName(html_names::kH5Tag))
return 5 + element->GetComputedHeadingOffset(4);
if (element->HasTagName(html_names::kH6Tag))
return 6 + element->GetComputedHeadingOffset(3);
if (RoleValue() == ax::mojom::blink::Role::kHeading) {
const String& implicit_value = GetImplicitAriaLevel(RoleValue());
return implicit_value.empty() ? 0 : implicit_value.ToInt();
}
if (::features::IsAccessibilityExposeSummaryAsHeadingEnabled() &&
ui::IsHeading(RoleValue())) {
return 5;
}
return 0;
}
unsigned AXNodeObject::HierarchicalLevel() const {
Element* element = GetElement();
if (!element)
return 0;
int32_t level;
if (AriaIntAttribute(html_names::kAriaLevelAttr, &level)) {
if (level >= 1)
return level;
}
auto accumulateLevel = [&](int initial_level,
ax::mojom::blink::Role target_role) {
int level = initial_level;
for (AXObject* parent = ParentObject(); parent;
parent = parent->ParentObject()) {
if (parent->RoleValue() == target_role)
level++;
}
return level;
};
switch (RoleValue()) {
case ax::mojom::blink::Role::kComment:
return accumulateLevel(1, ax::mojom::blink::Role::kComment);
case ax::mojom::blink::Role::kListItem:
level = accumulateLevel(0, ax::mojom::blink::Role::kList);
return level == 0 ? 1 : level;
case ax::mojom::blink::Role::kTabList:
return accumulateLevel(1, ax::mojom::blink::Role::kTabList);
case ax::mojom::blink::Role::kTreeItem: {
level = 1;
for (AXObject* parent = ParentObject(); parent;
parent = parent->ParentObject()) {
ax::mojom::blink::Role parent_role = parent->RoleValue();
if (parent_role == ax::mojom::blink::Role::kGroup)
level++;
else if (parent_role == ax::mojom::blink::Role::kTree)
break;
}
return level;
}
default:
return 0;
}
}
String AXNodeObject::AutoComplete() const {
if (AXObjectCache().GetAutofillSuggestionAvailability(AXObjectID()) ==
WebAXAutofillSuggestionAvailability::kAutocompleteAvailable) {
return "list";
}
if (IsAtomicTextField() || IsARIATextField()) {
const AtomicString& aria_auto_complete =
AriaTokenAttribute(html_names::kAriaAutocompleteAttr);
if (aria_auto_complete) {
return aria_auto_complete == "none" ? String()
: aria_auto_complete.LowerASCII();
;
}
}
if (auto* input = DynamicTo<HTMLInputElement>(GetNode())) {
if (input->DataList())
return "list";
}
return String();
}
void AXNodeObject::SerializeMarkerAttributes(ui::AXNodeData* node_data) const {
if (!GetNode() || !GetDocument() || !GetDocument()->View())
return;
#if BUILDFLAG(ARKWEB_ACCESSIBILITY)
Element *element_node = DynamicTo<Element>(GetNode());
if (element_node && node_data && element_node->tagName() == "EMBED") {
for (const Attribute &attr : element_node->Attributes()) {
std::string name = attr.GetName().LocalName().LowerASCII().Utf8();
if (base::EqualsCaseInsensitiveASCII(name, "id")) {
std::string value = attr.Value().Utf8();
LOG(DEBUG) << "embed html element id: " << value;
node_data->html_attributes.push_back(std::make_pair(name, value));
break;
}
}
}
#endif
auto* text_node = DynamicTo<Text>(GetNode());
if (!text_node)
return;
std::vector<int32_t> marker_types;
std::vector<int32_t> highlight_types;
std::vector<int32_t> marker_starts;
std::vector<int32_t> marker_ends;
std::optional<DocumentMarker::MarkerType> aria_marker_type =
GetAriaSpellingOrGrammarMarker();
if (aria_marker_type) {
AXRange range = AXRange::RangeOfContents(*this);
marker_types.push_back(ToAXMarkerType(aria_marker_type.value()));
marker_starts.push_back(range.Start().TextOffset());
marker_ends.push_back(range.End().TextOffset());
}
DocumentMarkerController& marker_controller = GetDocument()->Markers();
const DocumentMarker::MarkerTypes markers_used_by_accessibility(
DocumentMarker::kSpelling | DocumentMarker::kGrammar |
DocumentMarker::kTextMatch | DocumentMarker::kActiveSuggestion |
DocumentMarker::kSuggestion | DocumentMarker::kTextFragment |
DocumentMarker::kCustomHighlight);
const DocumentMarkerVector markers =
marker_controller.MarkersFor(*text_node, markers_used_by_accessibility);
for (const DocumentMarker* marker : markers) {
if (aria_marker_type == marker->GetType())
continue;
const Position start_position(*GetNode(), marker->StartOffset());
const Position end_position(*GetNode(), marker->EndOffset());
if (!start_position.IsValidFor(*GetDocument()) ||
!end_position.IsValidFor(*GetDocument())) {
continue;
}
int32_t highlight_type =
static_cast<int32_t>(ax::mojom::blink::HighlightType::kNone);
if (marker->GetType() == DocumentMarker::kCustomHighlight) {
const auto& highlight_marker = To<CustomHighlightMarker>(*marker);
highlight_type =
ToAXHighlightType(highlight_marker.GetHighlight()->type());
}
marker_types.push_back(ToAXMarkerType(marker->GetType()));
highlight_types.push_back(static_cast<int32_t>(highlight_type));
auto start_pos = AXPosition::FromPosition(
start_position, AXObjectCache(), TextAffinity::kDownstream,
AXPositionAdjustmentBehavior::kMoveLeft);
auto end_pos = AXPosition::FromPosition(
end_position, AXObjectCache(), TextAffinity::kDownstream,
AXPositionAdjustmentBehavior::kMoveRight);
marker_starts.push_back(start_pos.TextOffset());
marker_ends.push_back(end_pos.TextOffset());
}
if (marker_types.empty())
return;
node_data->AddIntListAttribute(
ax::mojom::blink::IntListAttribute::kMarkerTypes, marker_types);
node_data->AddIntListAttribute(
ax::mojom::blink::IntListAttribute::kHighlightTypes, highlight_types);
node_data->AddIntListAttribute(
ax::mojom::blink::IntListAttribute::kMarkerStarts, marker_starts);
node_data->AddIntListAttribute(
ax::mojom::blink::IntListAttribute::kMarkerEnds, marker_ends);
}
ax::mojom::blink::ListStyle AXNodeObject::GetListStyle() const {
const LayoutObject* layout_object = GetLayoutObject();
if (!layout_object) {
return AXObject::GetListStyle();
}
const ComputedStyle* computed_style = layout_object->Style();
if (!computed_style) {
return AXObject::GetListStyle();
}
const StyleImage* style_image = computed_style->ListStyleImage();
if (style_image && !style_image->ErrorOccurred()) {
return ax::mojom::blink::ListStyle::kImage;
}
if (RuntimeEnabledFeatures::CSSAtRuleCounterStyleSpeakAsDescriptorEnabled()) {
if (!computed_style->ListStyleType()) {
return ax::mojom::blink::ListStyle::kNone;
}
if (computed_style->ListStyleType()->IsString()) {
return ax::mojom::blink::ListStyle::kOther;
}
DCHECK(computed_style->ListStyleType()->IsCounterStyle());
const CounterStyle& counter_style =
ListMarker::GetCounterStyle(*GetDocument(), *computed_style);
switch (counter_style.EffectiveSpeakAs()) {
case CounterStyleSpeakAs::kBullets: {
UChar symbol = counter_style.GenerateTextAlternative(0)[0];
switch (symbol) {
case 0x2022:
return ax::mojom::blink::ListStyle::kDisc;
case 0x25E6:
return ax::mojom::blink::ListStyle::kCircle;
case 0x25A0:
return ax::mojom::blink::ListStyle::kSquare;
default:
return ax::mojom::blink::ListStyle::kOther;
}
}
case CounterStyleSpeakAs::kNumbers:
return ax::mojom::blink::ListStyle::kNumeric;
case CounterStyleSpeakAs::kWords:
return ax::mojom::blink::ListStyle::kOther;
case CounterStyleSpeakAs::kAuto:
case CounterStyleSpeakAs::kReference:
NOTREACHED();
}
}
switch (ListMarker::GetListStyleCategory(*GetDocument(), *computed_style)) {
case ListMarker::ListStyleCategory::kNone:
return ax::mojom::blink::ListStyle::kNone;
case ListMarker::ListStyleCategory::kSymbol: {
const AtomicString& counter_style_name =
computed_style->ListStyleType()->GetCounterStyleName();
if (counter_style_name == keywords::kDisc) {
return ax::mojom::blink::ListStyle::kDisc;
}
if (counter_style_name == keywords::kCircle) {
return ax::mojom::blink::ListStyle::kCircle;
}
if (counter_style_name == keywords::kSquare) {
return ax::mojom::blink::ListStyle::kSquare;
}
return ax::mojom::blink::ListStyle::kOther;
}
case ListMarker::ListStyleCategory::kLanguage: {
const AtomicString& counter_style_name =
computed_style->ListStyleType()->GetCounterStyleName();
if (counter_style_name == keywords::kDecimal) {
return ax::mojom::blink::ListStyle::kNumeric;
}
if (counter_style_name == "decimal-leading-zero") {
if (ListMarker::GetCounterStyle(*GetDocument(), *computed_style)
.IsPredefined()) {
return ax::mojom::blink::ListStyle::kNumeric;
}
}
return ax::mojom::blink::ListStyle::kOther;
}
case ListMarker::ListStyleCategory::kStaticString:
return ax::mojom::blink::ListStyle::kOther;
}
}
AXObject* AXNodeObject::InPageLinkTarget() const {
if (!IsLink() || !GetDocument())
return AXObject::InPageLinkTarget();
const Element* anchor = AnchorElement();
if (!anchor)
return AXObject::InPageLinkTarget();
KURL link_url = anchor->HrefURL();
if (!link_url.IsValid())
return AXObject::InPageLinkTarget();
KURL document_url = GetDocument()->Url();
if (!document_url.IsValid() ||
!EqualIgnoringFragmentIdentifier(document_url, link_url)) {
return AXObject::InPageLinkTarget();
}
String fragment = link_url.FragmentIdentifier().ToString();
TreeScope& tree_scope = anchor->GetTreeScope();
Node* target = tree_scope.FindAnchor(fragment);
AXObject* ax_target = AXObjectCache().Get(target);
if (!ax_target || !IsPotentialInPageLinkTarget(*ax_target->GetNode()))
return AXObject::InPageLinkTarget();
#if DCHECK_IS_ON()
DCHECK(ax_target->IsWebArea() || ax_target->GetElement())
<< "The link target is expected to be a document or an element: "
<< ax_target << "\n* URL fragment = " << fragment;
#endif
if (ax_target->IsIgnored())
return nullptr;
return ax_target;
}
const AtomicString& AXNodeObject::EffectiveTarget() const {
const auto* anchor = DynamicTo<HTMLAnchorElementBase>(GetNode());
if (anchor) {
const AtomicString self_value("_self");
const AtomicString& effective_target = anchor->GetEffectiveTarget();
if (effective_target != self_value) {
return anchor->GetEffectiveTarget();
}
}
return AXObject::EffectiveTarget();
}
AccessibilityOrientation AXNodeObject::Orientation() const {
if (RoleValue() == ax::mojom::blink::Role::kComboBoxGrouping ||
RoleValue() == ax::mojom::blink::Role::kComboBoxMenuButton ||
RoleValue() == ax::mojom::blink::Role::kComboBoxSelect ||
RoleValue() == ax::mojom::blink::Role::kTextFieldWithComboBox) {
const AtomicString& aria_orientation =
AriaTokenAttribute(html_names::kAriaOrientationAttr);
if (EqualIgnoringASCIICase(aria_orientation, "horizontal")) {
return kAccessibilityOrientationHorizontal;
}
if (EqualIgnoringASCIICase(aria_orientation, "vertical")) {
return kAccessibilityOrientationVertical;
}
}
if (!RoleSupportsAriaAttribute(RoleValue(),
html_names::kAriaOrientationAttr)) {
return AXObject::Orientation();
}
const AtomicString& aria_orientation =
AriaTokenAttribute(html_names::kAriaOrientationAttr);
if (EqualIgnoringASCIICase(aria_orientation, "horizontal")) {
return kAccessibilityOrientationHorizontal;
}
if (EqualIgnoringASCIICase(aria_orientation, "vertical")) {
return kAccessibilityOrientationVertical;
}
const String& implicit_orientation = GetImplicitAriaOrientation(RoleValue());
if (EqualIgnoringASCIICase(implicit_orientation, "horizontal")) {
return kAccessibilityOrientationHorizontal;
}
if (EqualIgnoringASCIICase(implicit_orientation, "vertical")) {
return kAccessibilityOrientationVertical;
}
return AXObject::Orientation();
}
AXObject* AXNodeObject::GetChildFigcaption() const {
AXObject* child = FirstChildIncludingIgnored();
if (!child)
return nullptr;
if (child->RoleValue() == ax::mojom::blink::Role::kFigcaption)
return child;
child = LastChildIncludingIgnored();
if (child->RoleValue() == ax::mojom::blink::Role::kFigcaption)
return child;
return nullptr;
}
AXObject::AXObjectVector AXNodeObject::RadioButtonsInGroup() const {
AXObjectVector radio_buttons;
if (!node_ || RoleValue() != ax::mojom::blink::Role::kRadioButton)
return radio_buttons;
if (auto* node_radio_button = DynamicTo<HTMLInputElement>(node_.Get())) {
HeapVector<Member<HTMLInputElement>> html_radio_buttons =
FindAllRadioButtonsWithSameName(node_radio_button);
for (HTMLInputElement* radio_button : html_radio_buttons) {
AXObject* ax_radio_button = AXObjectCache().Get(radio_button);
if (ax_radio_button)
radio_buttons.push_back(ax_radio_button);
}
return radio_buttons;
}
AXObject* parent = ParentObjectUnignored();
if (parent && parent->RoleValue() == ax::mojom::blink::Role::kRadioGroup) {
for (AXObject* child : parent->UnignoredChildrenSlow()) {
DCHECK(child);
if (child->RoleValue() == ax::mojom::blink::Role::kRadioButton &&
child->IsIncludedInTree()) {
radio_buttons.push_back(child);
}
}
}
return radio_buttons;
}
HeapVector<Member<HTMLInputElement>>
AXNodeObject::FindAllRadioButtonsWithSameName(HTMLInputElement* radio_button) {
HeapVector<Member<HTMLInputElement>> all_radio_buttons;
if (!radio_button ||
radio_button->FormControlType() != FormControlType::kInputRadio) {
return all_radio_buttons;
}
constexpr bool kTraverseForward = true;
constexpr bool kTraverseBackward = false;
HTMLInputElement* first_radio_button = radio_button;
do {
radio_button = RadioInputType::NextRadioButtonInGroup(first_radio_button,
kTraverseBackward);
if (radio_button)
first_radio_button = radio_button;
} while (radio_button);
HTMLInputElement* next_radio_button = first_radio_button;
do {
all_radio_buttons.push_back(next_radio_button);
next_radio_button = RadioInputType::NextRadioButtonInGroup(
next_radio_button, kTraverseForward);
} while (next_radio_button);
return all_radio_buttons;
}
ax::mojom::blink::WritingDirection AXNodeObject::GetTextDirection() const {
if (!GetLayoutObject())
return AXObject::GetTextDirection();
const ComputedStyle* style = GetLayoutObject()->Style();
if (!style)
return AXObject::GetTextDirection();
switch (style->GetWritingDirection().InlineEnd()) {
case PhysicalDirection::kRight:
return ax::mojom::blink::WritingDirection::kLtr;
case PhysicalDirection::kLeft:
return ax::mojom::blink::WritingDirection::kRtl;
case PhysicalDirection::kDown:
return ax::mojom::blink::WritingDirection::kTtb;
case PhysicalDirection::kUp:
return ax::mojom::blink::WritingDirection::kBtt;
}
NOTREACHED();
}
ax::mojom::blink::TextPosition AXNodeObject::GetTextPositionFromRole() const {
AXObject* obj = nullptr;
if (RoleValue() == ax::mojom::blink::Role::kStaticText)
obj = ParentObject();
else
obj = const_cast<AXNodeObject*>(this);
if (obj->RoleValue() == ax::mojom::blink::Role::kSubscript)
return ax::mojom::blink::TextPosition::kSubscript;
if (obj->RoleValue() == ax::mojom::blink::Role::kSuperscript)
return ax::mojom::blink::TextPosition::kSuperscript;
if (!GetLayoutObject() || !GetLayoutObject()->IsInline())
return ax::mojom::blink::TextPosition::kNone;
if (auto* parent = obj->ParentObjectUnignored())
return static_cast<AXNodeObject*>(parent)->GetTextPositionFromRole();
return ax::mojom::blink::TextPosition::kNone;
}
ax::mojom::blink::TextPosition AXNodeObject::GetTextPosition() const {
if (GetNode()) {
const auto& text_position = GetTextPositionFromRole();
if (text_position != ax::mojom::blink::TextPosition::kNone)
return text_position;
}
if (!GetLayoutObject())
return AXObject::GetTextPosition();
const ComputedStyle* style = GetLayoutObject()->Style();
if (!style)
return AXObject::GetTextPosition();
switch (style->VerticalAlign()) {
case EVerticalAlign::kBaseline:
case EVerticalAlign::kMiddle:
case EVerticalAlign::kTextTop:
case EVerticalAlign::kTextBottom:
case EVerticalAlign::kTop:
case EVerticalAlign::kBottom:
case EVerticalAlign::kBaselineMiddle:
case EVerticalAlign::kLength:
return AXObject::GetTextPosition();
case EVerticalAlign::kSub:
return ax::mojom::blink::TextPosition::kSubscript;
case EVerticalAlign::kSuper:
return ax::mojom::blink::TextPosition::kSuperscript;
}
}
void AXNodeObject::GetTextStyleAndTextDecorationStyle(
int32_t* text_style,
ax::mojom::blink::TextDecorationStyle* text_overline_style,
ax::mojom::blink::TextDecorationStyle* text_strikethrough_style,
ax::mojom::blink::TextDecorationStyle* text_underline_style) const {
if (!GetLayoutObject()) {
AXObject::GetTextStyleAndTextDecorationStyle(
text_style, text_overline_style, text_strikethrough_style,
text_underline_style);
return;
}
const ComputedStyle* style = GetLayoutObject()->Style();
if (!style) {
AXObject::GetTextStyleAndTextDecorationStyle(
text_style, text_overline_style, text_strikethrough_style,
text_underline_style);
return;
}
*text_style = 0;
*text_overline_style = ax::mojom::blink::TextDecorationStyle::kNone;
*text_strikethrough_style = ax::mojom::blink::TextDecorationStyle::kNone;
*text_underline_style = ax::mojom::blink::TextDecorationStyle::kNone;
if (style->GetFontWeight() == kBoldWeightValue) {
*text_style |= TextStyleFlag(ax::mojom::blink::TextStyle::kBold);
}
if (style->GetFontDescription().Style() == kItalicSlopeValue) {
*text_style |= TextStyleFlag(ax::mojom::blink::TextStyle::kItalic);
}
for (const auto& decoration : style->AppliedTextDecorations()) {
if (EnumHasFlags(decoration.Lines(), TextDecorationLine::kOverline)) {
*text_style |= TextStyleFlag(ax::mojom::blink::TextStyle::kOverline);
*text_overline_style =
TextDecorationStyleToAXTextDecorationStyle(decoration.Style());
}
if (EnumHasFlags(decoration.Lines(), TextDecorationLine::kLineThrough)) {
*text_style |= TextStyleFlag(ax::mojom::blink::TextStyle::kLineThrough);
*text_strikethrough_style =
TextDecorationStyleToAXTextDecorationStyle(decoration.Style());
}
if (EnumHasFlags(decoration.Lines(), TextDecorationLine::kUnderline)) {
*text_style |= TextStyleFlag(ax::mojom::blink::TextStyle::kUnderline);
*text_underline_style =
TextDecorationStyleToAXTextDecorationStyle(decoration.Style());
}
}
}
ax::mojom::blink::TextAlign AXNodeObject::GetTextAlign() const {
if (IsTextObject() || !GetLayoutObject())
return ax::mojom::blink::TextAlign::kNone;
const ComputedStyle* style = GetLayoutObject()->Style();
if (!style)
return ax::mojom::blink::TextAlign::kNone;
switch (style->GetTextAlign()) {
case ETextAlign::kLeft:
case ETextAlign::kWebkitLeft:
case ETextAlign::kStart:
return ax::mojom::blink::TextAlign::kLeft;
case ETextAlign::kRight:
case ETextAlign::kWebkitRight:
case ETextAlign::kEnd:
return ax::mojom::blink::TextAlign::kRight;
case ETextAlign::kCenter:
case ETextAlign::kWebkitCenter:
return ax::mojom::blink::TextAlign::kCenter;
case ETextAlign::kJustify:
return ax::mojom::blink::TextAlign::kJustify;
case ETextAlign::kMatchParent:
return style->IsLeftToRightDirection()
? ax::mojom::blink::TextAlign::kLeft
: ax::mojom::blink::TextAlign::kRight;
}
}
float AXNodeObject::GetTextIndent() const {
if (IsTextObject() || !GetLayoutObject())
return 0.0f;
const ComputedStyle* style = GetLayoutObject()->Style();
if (!style)
return 0.0f;
const blink::LayoutBlock* layout_block =
GetLayoutObject()->InclusiveContainingBlock();
if (!layout_block)
return 0.0f;
float text_indent = layout_block->TextIndentOffset().ToFloat();
return text_indent / kCssPixelsPerMillimeter;
}
String AXNodeObject::ImageDataUrl(const gfx::Size& max_size) const {
Node* node = GetNode();
if (!node)
return String();
ImageBitmapOptions* options = ImageBitmapOptions::Create();
ImageBitmap* image_bitmap = nullptr;
if (auto* image = DynamicTo<HTMLImageElement>(node)) {
image_bitmap =
MakeGarbageCollected<ImageBitmap>(image, std::nullopt, options);
} else if (auto* canvas = DynamicTo<HTMLCanvasElement>(node)) {
image_bitmap =
MakeGarbageCollected<ImageBitmap>(canvas, std::nullopt, options);
} else if (auto* video = DynamicTo<HTMLVideoElement>(node)) {
image_bitmap =
MakeGarbageCollected<ImageBitmap>(video, std::nullopt, options);
}
if (!image_bitmap)
return String();
scoped_refptr<StaticBitmapImage> bitmap_image = image_bitmap->BitmapImage();
if (!bitmap_image)
return String();
sk_sp<SkImage> image =
bitmap_image->PaintImageForCurrentFrame().GetSwSkImage();
if (!image || image->width() <= 0 || image->height() <= 0)
return String();
float x_scale =
max_size.width() ? max_size.width() * 1.0 / image->width() : 1.0;
float y_scale =
max_size.height() ? max_size.height() * 1.0 / image->height() : 1.0;
float scale = std::min(x_scale, y_scale);
if (scale >= 1.0)
scale = 1.0;
int width = std::round(image->width() * scale);
int height = std::round(image->height() * scale);
SkBitmap bitmap;
SkPixmap unscaled_pixmap;
if (scale == 1.0 && image->peekPixels(&unscaled_pixmap)) {
bitmap.installPixels(unscaled_pixmap);
} else {
bitmap.allocPixels(
SkImageInfo::MakeN32(width, height, kPremul_SkAlphaType));
SkCanvas canvas(bitmap, SkSurfaceProps{});
canvas.clear(SK_ColorTRANSPARENT);
canvas.drawImageRect(image, SkRect::MakeIWH(width, height),
SkSamplingOptions());
}
SkImageInfo info = SkImageInfo::Make(width, height, kRGBA_8888_SkColorType,
kUnpremul_SkAlphaType);
size_t row_bytes = info.minRowBytes();
Vector<char> pixel_storage(
base::checked_cast<wtf_size_t>(info.computeByteSize(row_bytes)));
SkPixmap pixmap(info, pixel_storage.data(), row_bytes);
if (!SkImages::RasterFromBitmap(bitmap)->readPixels(pixmap, 0, 0)) {
return String();
}
std::unique_ptr<ImageDataBuffer> buffer = ImageDataBuffer::Create(pixmap);
if (!buffer)
return String();
return buffer->ToDataURL(kMimeTypePng, 1.0);
}
const AtomicString& AXNodeObject::AccessKey() const {
auto* element = DynamicTo<Element>(GetNode());
if (!element)
return g_null_atom;
return element->FastGetAttribute(html_names::kAccesskeyAttr);
}
RGBA32 AXNodeObject::ColorValue() const {
auto* input = DynamicTo<HTMLInputElement>(GetNode());
if (!input || !IsColorWell())
return AXObject::ColorValue();
const AtomicString& type = input->getAttribute(kTypeAttr);
if (!EqualIgnoringASCIICase(type, "color"))
return AXObject::ColorValue();
Color color;
bool success = color.SetFromString(input->Value());
DCHECK(success);
return color.Rgb();
}
RGBA32 AXNodeObject::BackgroundColor() const {
LayoutObject* layout_object = GetLayoutObject();
if (!layout_object)
return Color::kTransparent.Rgb();
if (IsA<Document>(GetNode())) {
LocalFrameView* view = DocumentFrameView();
if (view)
return view->BaseBackgroundColor().Rgb();
else
return Color::kWhite.Rgb();
}
const ComputedStyle* style = layout_object->Style();
if (!style || !style->HasBackground())
return Color::kTransparent.Rgb();
return style->VisitedDependentColor(GetCSSPropertyBackgroundColor()).Rgb();
}
RGBA32 AXNodeObject::GetColor() const {
if (!GetLayoutObject() || IsColorWell())
return AXObject::GetColor();
const ComputedStyle* style = GetLayoutObject()->Style();
if (!style)
return AXObject::GetColor();
Color color = style->VisitedDependentColor(GetCSSPropertyColor());
return color.Rgb();
}
const AtomicString& AXNodeObject::ComputedFontFamily() const {
if (!GetLayoutObject())
return AXObject::ComputedFontFamily();
const ComputedStyle* style = GetLayoutObject()->Style();
if (!style)
return AXObject::ComputedFontFamily();
const FontDescription& font_description = style->GetFontDescription();
return font_description.FirstFamily().FamilyName();
}
String AXNodeObject::FontFamilyForSerialization() const {
if (!GetLayoutObject())
return AXObject::FontFamilyForSerialization();
const ComputedStyle* style = GetLayoutObject()->Style();
if (!style)
return AXObject::FontFamilyForSerialization();
const SimpleFontData* primary_font = style->GetFont()->PrimaryFont();
if (!primary_font)
return AXObject::FontFamilyForSerialization();
return primary_font->PlatformData().FontFamilyName();
}
float AXNodeObject::FontSize() const {
if (!GetLayoutObject())
return AXObject::FontSize();
const ComputedStyle* style = GetLayoutObject()->Style();
if (!style)
return AXObject::FontSize();
return style->SpecifiedFontSize();
}
float AXNodeObject::FontWeight() const {
if (!GetLayoutObject())
return AXObject::FontWeight();
const ComputedStyle* style = GetLayoutObject()->Style();
if (!style)
return AXObject::FontWeight();
return style->GetFontWeight();
}
ax::mojom::blink::AriaCurrentState AXNodeObject::GetAriaCurrentState() const {
const AtomicString& attribute_value =
AriaTokenAttribute(html_names::kAriaCurrentAttr);
if (attribute_value.IsNull()) {
if (RuntimeEnabledFeatures::CSSScrollTargetGroupAriaCurrentEnabled()) {
if (auto* anchor_element = DynamicTo<HTMLAnchorElement>(GetNode())) {
if (ScrollMarkerGroupData* data =
anchor_element->GetScrollTargetGroupContainerData()) {
if (data->Selected() == anchor_element) {
return ax::mojom::blink::AriaCurrentState::kTrue;
}
}
}
}
return ax::mojom::blink::AriaCurrentState::kNone;
}
if (EqualIgnoringASCIICase(attribute_value, "false")) {
return ax::mojom::blink::AriaCurrentState::kFalse;
}
if (EqualIgnoringASCIICase(attribute_value, "page")) {
return ax::mojom::blink::AriaCurrentState::kPage;
}
if (EqualIgnoringASCIICase(attribute_value, "step")) {
return ax::mojom::blink::AriaCurrentState::kStep;
}
if (EqualIgnoringASCIICase(attribute_value, "location")) {
return ax::mojom::blink::AriaCurrentState::kLocation;
}
if (EqualIgnoringASCIICase(attribute_value, "date")) {
return ax::mojom::blink::AriaCurrentState::kDate;
}
if (EqualIgnoringASCIICase(attribute_value, "time")) {
return ax::mojom::blink::AriaCurrentState::kTime;
}
return ax::mojom::blink::AriaCurrentState::kTrue;
}
ax::mojom::blink::InvalidState AXNodeObject::GetInvalidState() const {
if (const AtomicString& attribute_value =
AriaTokenAttribute(html_names::kAriaInvalidAttr)) {
if (EqualIgnoringASCIICase(attribute_value, "false")) {
return ax::mojom::blink::InvalidState::kFalse;
}
if (EqualIgnoringASCIICase(attribute_value, "spelling") ||
EqualIgnoringASCIICase(attribute_value, "grammar")) {
return RoleValue() == ax::mojom::blink::Role::kTextField
? ax::mojom::blink::InvalidState::kTrue
: ax::mojom::blink::InvalidState::kNone;
}
if (!attribute_value.empty()) {
return ax::mojom::blink::InvalidState::kTrue;
}
}
if (GetElement()) {
ListedElement* form_control = ListedElement::From(*GetElement());
if (form_control) {
return IsValidFormControl(form_control)
? ax::mojom::blink::InvalidState::kFalse
: ax::mojom::blink::InvalidState::kTrue;
}
}
return AXObject::GetInvalidState();
}
bool AXNodeObject::IsValidFormControl(ListedElement* form_control) const {
if (form_control->CustomError())
return false;
if (form_control->IsNotCandidateOrValid())
return true;
if (IsAtomicTextField() && IsRequired() && GetValueForControl().length() == 0)
return true;
return false;
}
int AXNodeObject::PosInSet() const {
if (RoleValue() == ax::mojom::blink::Role::kComboBoxSelect) {
if (auto* select_element = DynamicTo<HTMLSelectElement>(*GetNode())) {
return 1 + select_element->selectedIndex();
}
}
if (SupportsARIASetSizeAndPosInSet()) {
int32_t pos_in_set;
if (AriaIntAttribute(html_names::kAriaPosinsetAttr, &pos_in_set)) {
return pos_in_set;
}
}
return 0;
}
int AXNodeObject::SetSize() const {
if (auto* select_element = DynamicTo<HTMLSelectElement>(GetNode())) {
return static_cast<int>(select_element->length());
}
if (RoleValue() == ax::mojom::blink::Role::kMenuListPopup) {
return ParentObject()->SetSize();
}
if (SupportsARIASetSizeAndPosInSet()) {
int32_t set_size;
if (AriaIntAttribute(html_names::kAriaSetsizeAttr, &set_size)) {
return set_size;
}
}
return 0;
}
bool AXNodeObject::ValueForRange(float* out_value) const {
float value_now;
if (AriaFloatAttribute(html_names::kAriaValuenowAttr, &value_now)) {
float min_value, max_value;
if (MinValueForRange(&min_value)) {
if (value_now < min_value) {
*out_value = min_value;
return true;
}
}
if (MaxValueForRange(&max_value)) {
if (value_now > max_value) {
*out_value = max_value;
return true;
}
}
*out_value = value_now;
return true;
}
if (IsNativeSlider() || IsNativeSpinButton()) {
*out_value = To<HTMLInputElement>(*GetNode()).valueAsNumber();
return std::isfinite(*out_value);
}
if (auto* meter = DynamicTo<HTMLMeterElement>(GetNode())) {
*out_value = meter->value();
return true;
}
switch (RawAriaRole()) {
case ax::mojom::blink::Role::kScrollBar:
case ax::mojom::blink::Role::kSlider: {
float min_value, max_value;
if (MinValueForRange(&min_value) && MaxValueForRange(&max_value)) {
*out_value = (min_value + max_value) / 2.0f;
return true;
}
[[fallthrough]];
}
case ax::mojom::blink::Role::kSplitter: {
*out_value = 50.0f;
return true;
}
case ax::mojom::blink::Role::kMeter: {
float min_value;
if (MinValueForRange(&min_value)) {
*out_value = min_value;
return true;
}
[[fallthrough]];
}
case ax::mojom::blink::Role::kSpinButton: {
*out_value = 0.0f;
return true;
}
default:
break;
}
return false;
}
bool AXNodeObject::MaxValueForRange(float* out_value) const {
if (!IsRangeValueSupported()) {
return false;
}
if (AriaFloatAttribute(html_names::kAriaValuemaxAttr, out_value)) {
return true;
}
if (IsNativeSlider() || IsNativeSpinButton()) {
*out_value = static_cast<float>(To<HTMLInputElement>(*GetNode()).Maximum());
return std::isfinite(*out_value);
}
if (auto* meter = DynamicTo<HTMLMeterElement>(GetNode())) {
*out_value = meter->max();
return true;
}
const String& implicit_value = GetImplicitAriaValuemax(RoleValue());
if (!implicit_value.empty()) {
*out_value = implicit_value.ToFloat();
return true;
}
return false;
}
bool AXNodeObject::MinValueForRange(float* out_value) const {
if (!IsRangeValueSupported()) {
return false;
}
if (AriaFloatAttribute(html_names::kAriaValueminAttr, out_value)) {
return true;
}
if (IsNativeSlider() || IsNativeSpinButton()) {
*out_value = static_cast<float>(To<HTMLInputElement>(*GetNode()).Minimum());
return std::isfinite(*out_value);
}
if (auto* meter = DynamicTo<HTMLMeterElement>(GetNode())) {
*out_value = meter->min();
return true;
}
const String& implicit_value = GetImplicitAriaValuemin(RoleValue());
if (!implicit_value.empty()) {
*out_value = implicit_value.ToFloat();
return true;
}
return false;
}
bool AXNodeObject::StepValueForRange(float* out_value) const {
if (IsNativeSlider() || IsNativeSpinButton()) {
auto step_range =
To<HTMLInputElement>(*GetNode()).CreateStepRange(kRejectAny);
auto step = step_range.Step().ToString().ToFloat();
float max = step_range.Maximum().ToString().ToFloat();
float min = step_range.Minimum().ToString().ToFloat();
int num_stops = base::saturated_cast<int>((max - min) / step);
constexpr int kNumStopsForFivePercentRule = 40;
if (num_stops >= kNumStopsForFivePercentRule) {
*out_value = 0.0f;
return false;
}
*out_value = step;
return std::isfinite(*out_value);
}
switch (RawAriaRole()) {
case ax::mojom::blink::Role::kScrollBar:
case ax::mojom::blink::Role::kSplitter:
case ax::mojom::blink::Role::kSlider: {
*out_value = 0.0f;
return true;
}
default:
break;
}
return false;
}
KURL AXNodeObject::Url() const {
if (IsLink())
return GetElement()->HrefURL();
if (IsWebArea()) {
DCHECK(GetDocument());
return GetDocument()->Url();
}
auto* html_image_element = DynamicTo<HTMLImageElement>(GetNode());
if (IsImage() && html_image_element) {
String source_url = html_image_element->ImageSourceURL();
String stripped_image_source_url =
StripLeadingAndTrailingHTMLSpaces(source_url);
if (!stripped_image_source_url.empty())
return GetDocument()->CompleteURL(stripped_image_source_url);
}
if (IsInputImage())
return To<HTMLInputElement>(GetNode())->Src();
return KURL();
}
AXObject* AXNodeObject::ChooserPopup() const {
switch (native_role_) {
case ax::mojom::blink::Role::kColorWell:
case ax::mojom::blink::Role::kComboBoxSelect:
case ax::mojom::blink::Role::kDate:
case ax::mojom::blink::Role::kDateTime:
case ax::mojom::blink::Role::kInputTime:
case ax::mojom::blink::Role::kTextFieldWithComboBox: {
for (const auto& child : ChildrenIncludingIgnored()) {
if (IsA<Document>(child->GetNode())) {
return child.Get();
}
}
return nullptr;
}
default:
#if DCHECK_IS_ON()
for (const auto& child : ChildrenIncludingIgnored()) {
DCHECK(!IsA<Document>(child->GetNode()) ||
!child->ParentObject()->IsVisible())
<< "Chooser popup exists for " << native_role_
<< "\n* Child: " << child
<< "\n* Child's immediate parent: " << child->ParentObject();
}
#endif
return nullptr;
}
}
String AXNodeObject::GetValueForControl() const {
AXObjectSet visited;
return GetValueForControl(visited);
}
String AXNodeObject::GetValueForControl(AXObjectSet& visited) const {
Node* node = GetNode();
if (!node)
return String();
if (const auto* select_element = DynamicTo<HTMLSelectElement>(*node)) {
if (!select_element->UsesMenuList())
return String();
int selected_index = select_element->SelectedListIndex();
const HeapVector<Member<HTMLElement>>& list_items =
select_element->GetListItems();
if (selected_index >= 0 &&
static_cast<wtf_size_t>(selected_index) < list_items.size()) {
const AtomicString& overridden_description = AriaAttribute(
*list_items[selected_index], html_names::kAriaLabelAttr);
if (!overridden_description.IsNull())
return overridden_description;
}
if (select_element->IsAppearanceBase()) {
if (auto* button = select_element->SlottedButton()) {
if (AXObject* button_object = AXObjectCache().Get(button)) {
return button_object->TextFromDescendants(visited, nullptr, false);
}
}
}
return select_element->InnerElement().GetInnerTextWithoutUpdate();
}
if (IsAtomicTextField()) {
String inner_text = ToTextControl(*node).InnerEditorValue();
unsigned int unmasked_text_length = inner_text.length();
if (!unmasked_text_length) {
return String();
}
if (!IsPasswordFieldAndShouldHideValue())
return inner_text;
if (!GetLayoutObject())
return inner_text;
const ComputedStyle* style = GetLayoutObject()->Style();
if (!style)
return inner_text;
UChar mask_character = 0;
switch (style->TextSecurity()) {
case ETextSecurity::kNone:
break;
case ETextSecurity::kDisc:
mask_character = uchar::kBullet;
break;
case ETextSecurity::kCircle:
mask_character = uchar::kWhiteBullet;
break;
case ETextSecurity::kSquare:
mask_character = uchar::kBlackSquare;
break;
}
if (!mask_character)
return inner_text;
StringBuilder masked_text;
masked_text.ReserveCapacity(unmasked_text_length);
for (unsigned int i = 0; i < unmasked_text_length; ++i)
masked_text.Append(mask_character);
return masked_text.ToString();
}
if (IsRangeValueSupported()) {
return AriaAttribute(html_names::kAriaValuetextAttr).GetString();
}
if (const auto* input = DynamicTo<HTMLInputElement>(node)) {
if (input->FormControlType() == FormControlType::kInputFile) {
return input->FileStatusText();
}
if (input->FormControlType() != FormControlType::kInputButton &&
input->FormControlType() != FormControlType::kInputCheckbox &&
input->FormControlType() != FormControlType::kInputImage &&
input->FormControlType() != FormControlType::kInputRadio &&
input->FormControlType() != FormControlType::kInputReset &&
input->FormControlType() != FormControlType::kInputSubmit) {
return input->Value();
}
}
if (RoleValue() == ax::mojom::blink::Role::kComboBoxMenuButton) {
return TextFromDescendants(visited, nullptr, false);
}
return String();
}
String AXNodeObject::SlowGetValueForControlIncludingContentEditable() const {
AXObjectSet visited;
return SlowGetValueForControlIncludingContentEditable(visited);
}
String AXNodeObject::SlowGetValueForControlIncludingContentEditable(
AXObjectSet& visited) const {
if (IsNonAtomicTextField()) {
Element* element = GetElement();
return element ? element->GetInnerTextWithoutUpdate() : String();
}
return GetValueForControl(visited);
}
ax::mojom::blink::Role AXNodeObject::RawAriaRole() const {
return aria_role_;
}
ax::mojom::blink::HasPopup AXNodeObject::HasPopup() const {
auto* element = GetElement();
auto has_popup_from_attribute =
element ? HasPopupFromAttribute(*element) : std::nullopt;
if (has_popup_from_attribute) {
return *has_popup_from_attribute;
}
if (RoleValue() == ax::mojom::blink::Role::kComboBoxSelect) {
return ax::mojom::blink::HasPopup::kMenu;
}
const String& implicit_value = GetImplicitAriaHaspopup(RoleValue());
if (implicit_value == "listbox") {
return ax::mojom::blink::HasPopup::kListbox;
}
if (AXObjectCache().GetAutofillSuggestionAvailability(AXObjectID()) !=
WebAXAutofillSuggestionAvailability::kNoSuggestions) {
return ax::mojom::blink::HasPopup::kMenu;
}
return AXObject::HasPopup();
}
ax::mojom::blink::IsPopup AXNodeObject::IsPopup() const {
if (IsDetached() || !GetElement()) {
return ax::mojom::blink::IsPopup::kNone;
}
const auto* html_element = DynamicTo<HTMLElement>(GetElement());
if (!html_element) {
return ax::mojom::blink::IsPopup::kNone;
}
if (RoleValue() == ax::mojom::blink::Role::kMenuListPopup) {
return ax::mojom::blink::IsPopup::kAuto;
}
switch (html_element->PopoverType()) {
case PopoverValueType::kNone:
return ax::mojom::blink::IsPopup::kNone;
case PopoverValueType::kAuto:
return ax::mojom::blink::IsPopup::kAuto;
case PopoverValueType::kHint:
return ax::mojom::blink::IsPopup::kHint;
case PopoverValueType::kManual:
return ax::mojom::blink::IsPopup::kManual;
}
}
bool AXNodeObject::IsEditableRoot() const {
const Node* node = GetNode();
if (IsDetached() || !node)
return false;
#if DCHECK_IS_ON()
DCHECK(GetDocument());
DCHECK_GE(GetDocument()->Lifecycle().GetState(),
DocumentLifecycle::kStyleClean)
<< "Unclean document style at lifecycle state "
<< GetDocument()->Lifecycle().ToString();
#endif
if (IsAtomicTextField())
return false;
if (EnclosingTextControl(node))
return false;
if (IsRootEditableElement(*node))
return true;
if (HasContentEditableAttributeSet())
return true;
return false;
}
bool AXNodeObject::HasContentEditableAttributeSet() const {
if (IsDetached() || !GetNode())
return false;
const auto* html_element = DynamicTo<HTMLElement>(GetNode());
if (!html_element)
return false;
ContentEditableType normalized_value =
html_element->contentEditableNormalized();
return normalized_value == ContentEditableType::kContentEditable ||
normalized_value == ContentEditableType::kPlaintextOnly;
}
static LayoutBlockFlow* GetNearestBlockFlow(LayoutObject* object) {
LayoutObject* current = object;
while (current) {
if (auto* block_flow = DynamicTo<LayoutBlockFlow>(current)) {
return block_flow;
}
current = current->Parent();
}
NOTREACHED();
}
static bool IsInSameBlockFlow(LayoutObject* r1, LayoutObject* r2) {
if (!r1 || !r2)
return false;
if (!r1->IsInline() || !r2->IsInline())
return false;
LayoutBlockFlow* b1 = GetNearestBlockFlow(r1);
LayoutBlockFlow* b2 = GetNearestBlockFlow(r2);
return b1 && b2 && b1 == b2;
}
bool AXNodeObject::OnNativeSetSelectedAction(bool selected) {
auto* option = DynamicTo<HTMLOptionElement>(GetNode());
if (!option) {
return false;
}
HTMLSelectElement* select_element = option->OwnerSelectElement();
if (!select_element) {
return false;
}
if (!CanSetSelectedAttribute()) {
return false;
}
AccessibilitySelectedState is_option_selected = IsSelected();
if (is_option_selected == kSelectedStateUndefined) {
return false;
}
bool is_selected = (is_option_selected == kSelectedStateTrue) ? true : false;
if ((is_selected && selected) || (!is_selected && !selected)) {
return false;
}
select_element->SelectOptionByAccessKey(To<HTMLOptionElement>(GetNode()));
return true;
}
bool AXNodeObject::OnNativeSetValueAction(const String& string) {
base::UmaHistogramEnumeration("Accessibility.SetValue.Role", RoleValue());
if (!GetNode() || !GetNode()->IsElementNode()) {
return false;
}
const LayoutObject* layout_object = GetLayoutObject();
if (!layout_object || !layout_object->IsBoxModelObject()) {
return false;
}
auto* html_input_element = DynamicTo<HTMLInputElement>(*GetNode());
if (html_input_element && layout_object->IsTextField()) {
html_input_element->SetValue(
string, TextFieldEventBehavior::kDispatchInputAndChangeEvent);
return true;
}
if (auto* text_area_element = DynamicTo<HTMLTextAreaElement>(*GetNode())) {
DCHECK(layout_object->IsTextArea());
text_area_element->SetValue(
string, TextFieldEventBehavior::kDispatchInputAndChangeEvent);
return true;
}
if (HasContentEditableAttributeSet()) {
To<HTMLElement>(GetNode())->setInnerText(string);
return true;
}
return false;
}
String AXNodeObject::GetName(ax::mojom::blink::NameFrom& name_from,
AXObjectVector* name_objects,
AXNodeObject::NameSources* name_sources) const {
String name = AXObject::GetName(name_from, name_objects, name_sources);
if (RoleValue() == ax::mojom::blink::Role::kSpinButton &&
DatetimeAncestor()) {
if (name_objects) {
name_objects->clear();
}
String input_name =
DatetimeAncestor()->GetName(name_from, name_objects, name_sources);
if (!input_name.empty())
return StrCat({name, " ", input_name});
}
const Element* element = GetElement();
if (element && element->IsScrollButtonPseudoElement()) {
std::optional<String> alt_text = GetCSSAltText(element);
if (alt_text && !alt_text->empty()) {
name_from = ax::mojom::blink::NameFrom::kCssAltText;
return *alt_text;
}
if (!name.empty()) {
return name;
}
const ComputedStyle* style =
GetLayoutObject() ? GetLayoutObject()->Style() : nullptr;
if (style) {
PhysicalDirection physical;
if (element->IsScrollButtonBlockStartPseudoElement()) {
physical = style->GetWritingDirection().BlockStart();
} else if (element->IsScrollButtonBlockEndPseudoElement()) {
physical = style->GetWritingDirection().BlockEnd();
} else if (element->IsScrollButtonInlineStartPseudoElement()) {
physical = style->GetWritingDirection().InlineStart();
} else if (element->IsScrollButtonInlineEndPseudoElement()) {
physical = style->GetWritingDirection().InlineEnd();
} else {
NOTREACHED()
<< "ScrollButtonPseudoElement must be one of known directions";
}
name_from = ax::mojom::blink::NameFrom::kCssAltText;
switch (physical) {
case PhysicalDirection::kRight:
return element->GetLocale().QueryString(IDS_AX_CAROUSEL_SCROLL_RIGHT);
case PhysicalDirection::kLeft:
return element->GetLocale().QueryString(IDS_AX_CAROUSEL_SCROLL_LEFT);
case PhysicalDirection::kDown:
return element->GetLocale().QueryString(IDS_AX_CAROUSEL_SCROLL_DOWN);
case PhysicalDirection::kUp:
return element->GetLocale().QueryString(IDS_AX_CAROUSEL_SCROLL_UP);
}
}
}
if (element && element->IsScrollMarkerPseudoElement()) {
std::optional<String> alt_text = GetCSSAltText(element);
if (alt_text && !alt_text->empty()) {
name_from = ax::mojom::blink::NameFrom::kCssAltText;
return *alt_text;
}
std::optional<String> content = GetCSSContentText(element);
if (content && !content->empty()) {
name_from = ax::mojom::blink::NameFrom::kContents;
return *content;
}
const AXObject* scroll_target =
AXObjectCache().Get(element->parentElement());
ax::mojom::blink::NameFrom name_source;
return scroll_target ? scroll_target->GetName(name_source, nullptr, nullptr)
: "";
}
return name;
}
String AXNodeObject::TextAlternative(
bool recursive,
const AXObject* aria_label_or_description_root,
AXObjectSet& visited,
ax::mojom::blink::NameFrom& name_from,
AXRelatedObjectVector* related_objects,
NameSources* name_sources) const {
DCHECK(!name_sources || related_objects);
bool found_text_alternative = false;
Node* node = GetNode();
name_from = ax::mojom::blink::NameFrom::kNone;
if (!node && !GetLayoutObject()) {
return String();
}
if (IsA<HTMLSlotElement>(node) && node->IsInUserAgentShadowRoot() &&
!recursive) {
return String();
}
if (GetLayoutObject()) {
std::optional<String> text_alternative = GetCSSAltText(GetElement());
if (text_alternative) {
name_from = ax::mojom::blink::NameFrom::kCssAltText;
if (name_sources) {
name_sources->push_back(NameSource(false));
name_sources->back().type = name_from;
name_sources->back().text = text_alternative.value();
}
return text_alternative.value();
}
if (GetLayoutObject()->IsBR()) {
text_alternative = String("\n");
found_text_alternative = true;
} else if (GetLayoutObject()->IsText() &&
(!recursive || !GetLayoutObject()->IsCounter())) {
auto* layout_text = To<LayoutText>(GetLayoutObject());
String visible_text = layout_text->PlainText();
if (visible_text.empty()) {
if (layout_text->IsAllCollapsibleWhitespace()) {
if (IsIgnored()) {
return "";
}
text_alternative = " ";
} else {
text_alternative = layout_text->TransformedText();
}
} else {
text_alternative = visible_text;
}
found_text_alternative = true;
} else if (!recursive) {
if (ListMarker* marker = ListMarker::Get(GetLayoutObject())) {
text_alternative = marker->TextAlternative(*GetLayoutObject());
found_text_alternative = true;
}
}
if (found_text_alternative) {
name_from = ax::mojom::blink::NameFrom::kContents;
if (name_sources) {
name_sources->push_back(NameSource(false));
name_sources->back().type = name_from;
name_sources->back().text = text_alternative.value();
}
visited.insert(this);
return text_alternative.value();
}
}
if (recursive && !visited.Contains(this)) {
String value_for_name = GetValueContributionToName(visited);
if (!value_for_name.IsNull()) {
name_from = ax::mojom::blink::NameFrom::kValue;
if (name_sources) {
name_sources->push_back(NameSource(false));
name_sources->back().type = ax::mojom::blink::NameFrom::kValue;
name_sources->back().text = value_for_name;
}
return value_for_name;
}
}
String text_alternative = AriaTextAlternative(
recursive, aria_label_or_description_root, visited, name_from,
related_objects, name_sources, &found_text_alternative);
if (found_text_alternative && !name_sources)
return MaybeAppendFileDescriptionToName(text_alternative);
text_alternative =
NativeTextAlternative(visited, name_from, related_objects, name_sources,
&found_text_alternative);
const bool has_explicitly_empty_native_text_alternative =
text_alternative.empty() &&
name_from == ax::mojom::blink::NameFrom::kAttributeExplicitlyEmpty;
if (!text_alternative.empty() && !name_sources) {
return MaybeAppendFileDescriptionToName(text_alternative);
}
if (ShouldIncludeContentInTextAlternative(
recursive, aria_label_or_description_root, visited)) {
name_from = ax::mojom::blink::NameFrom::kContents;
if (name_sources) {
name_sources->push_back(NameSource(found_text_alternative));
name_sources->back().type = name_from;
}
if (auto* text_node = DynamicTo<Text>(node)) {
text_alternative = text_node->data();
} else if (IsA<HTMLBRElement>(node)) {
text_alternative = String("\n");
} else {
text_alternative =
TextFromDescendants(visited, aria_label_or_description_root, false);
}
if (!text_alternative.empty()) {
if (name_sources) {
found_text_alternative = true;
name_sources->back().text = text_alternative;
} else {
return MaybeAppendFileDescriptionToName(text_alternative);
}
}
}
if (aria_label_or_description_root || !IsNameProhibited()) {
String resulting_text = TextAlternativeFromTooltip(
name_from, name_sources, &found_text_alternative, &text_alternative,
related_objects);
if (!resulting_text.empty()) {
if (name_sources) {
text_alternative = resulting_text;
} else {
return resulting_text;
}
}
}
String saved_text_alternative = GetSavedTextAlternativeFromNameSource(
found_text_alternative, name_from, related_objects, name_sources);
if (!saved_text_alternative.empty()) {
return saved_text_alternative;
}
if (has_explicitly_empty_native_text_alternative) {
name_from = ax::mojom::blink::NameFrom::kAttributeExplicitlyEmpty;
}
return String();
}
static bool ShouldInsertSpaceBetweenObjectsIfNeeded(
AXObject* previous,
AXObject* next,
ax::mojom::blink::NameFrom last_used_name_from,
ax::mojom::blink::NameFrom name_from) {
LayoutObject* next_layout = next->GetLayoutObject();
LayoutObject* prev_layout = previous->GetLayoutObject();
if (!IsInSameBlockFlow(next_layout, prev_layout)) {
return true;
}
CHECK(next_layout);
CHECK(prev_layout);
if (next_layout->IsAtomicInlineLevel() ||
prev_layout->IsAtomicInlineLevel()) {
return true;
}
switch (last_used_name_from) {
case ax::mojom::blink::NameFrom::kNone:
case ax::mojom::blink::NameFrom::kAttributeExplicitlyEmpty:
case ax::mojom::blink::NameFrom::kContents:
case ax::mojom::blink::NameFrom::kProhibited:
case ax::mojom::blink::NameFrom::kProhibitedAndRedundant:
break;
case ax::mojom::blink::NameFrom::kAttribute:
case ax::mojom::blink::NameFrom::kCaption:
case ax::mojom::blink::NameFrom::kCssAltText:
case ax::mojom::blink::NameFrom::kInterestFor:
case ax::mojom::blink::NameFrom::kPlaceholder:
case ax::mojom::blink::NameFrom::kRelatedElement:
case ax::mojom::blink::NameFrom::kTitle:
case ax::mojom::blink::NameFrom::kValue:
case ax::mojom::blink::NameFrom::kPopoverTarget:
return true;
}
switch (name_from) {
case ax::mojom::blink::NameFrom::kNone:
case ax::mojom::blink::NameFrom::kAttributeExplicitlyEmpty:
case ax::mojom::blink::NameFrom::kContents:
case ax::mojom::blink::NameFrom::kProhibited:
case ax::mojom::blink::NameFrom::kProhibitedAndRedundant:
break;
case ax::mojom::blink::NameFrom::kAttribute:
case ax::mojom::blink::NameFrom::kCaption:
case ax::mojom::blink::NameFrom::kCssAltText:
case ax::mojom::blink::NameFrom::kInterestFor:
case ax::mojom::blink::NameFrom::kPlaceholder:
case ax::mojom::blink::NameFrom::kRelatedElement:
case ax::mojom::blink::NameFrom::kTitle:
case ax::mojom::blink::NameFrom::kValue:
case ax::mojom::blink::NameFrom::kPopoverTarget:
return true;
}
if (previous->IsControl() || next->IsControl())
return true;
for (auto* layout_object = previous->GetLayoutObject();
layout_object && layout_object != next_layout;
layout_object = layout_object->NextInPreOrder()) {
if (layout_object->IsBlockInInline())
return true;
}
return false;
}
String AXNodeObject::TextFromDescendants(
AXObjectSet& visited,
const AXObject* aria_label_or_description_root,
bool recursive) const {
if (!CanHaveChildren()) {
return recursive || !GetElement()
? String()
: GetElement()->GetInnerTextWithoutUpdate();
}
StringBuilder accumulated_text;
AXObject* previous = nullptr;
ax::mojom::blink::NameFrom last_used_name_from =
ax::mojom::blink::NameFrom::kNone;
AXObjectVector action_objects =
RelationVectorFromAria(html_names::kAriaActionsAttr);
CHECK(!NeedsToUpdateCachedValues());
const AXObjectVector& children = ChildrenIncludingIgnored();
#if AX_FAIL_FAST_BUILD()
base::AutoReset<bool> auto_reset(&is_computing_text_from_descendants_, true);
#endif
wtf_size_t num_children = children.size();
for (wtf_size_t index = 0; index < num_children; index++) {
DCHECK_EQ(children.size(), num_children);
if (index >= children.size()) {
break;
}
AXObject* child = children[index];
DCHECK(child);
DCHECK(!child->IsDetached()) << child;
constexpr size_t kMaxDescendantsForTextAlternativeComputation = 100;
if (visited.size() > kMaxDescendantsForTextAlternativeComputation)
break;
if (action_objects.Contains(child)) {
continue;
}
if (child->IsHiddenForTextAlternativeCalculation(
aria_label_or_description_root)) {
continue;
}
ax::mojom::blink::NameFrom child_name_from =
ax::mojom::blink::NameFrom::kNone;
String result;
if (child->IsPresentational()) {
result = child->TextFromDescendants(visited,
aria_label_or_description_root, true);
} else {
result = RecursiveTextAlternative(*child, aria_label_or_description_root,
visited, child_name_from);
}
if (!result.empty() && previous && accumulated_text.length() &&
!IsHTMLSpace(accumulated_text[accumulated_text.length() - 1]) &&
!IsHTMLSpace(result[0])) {
if (ShouldInsertSpaceBetweenObjectsIfNeeded(
previous, child, last_used_name_from, child_name_from)) {
accumulated_text.Append(' ');
}
}
accumulated_text.Append(result);
previous = child;
if (!result.empty())
last_used_name_from = child_name_from;
}
return accumulated_text.ToString();
}
bool AXNodeObject::IsNameFromLabelElement(HTMLElement* control) {
if (!control)
return false;
if (IsNameFromAriaAttribute(control))
return false;
auto* labels = control->labels();
return labels && labels->length();
}
bool AXNodeObject::IsRedundantLabel(HTMLLabelElement* label) {
HTMLInputElement* input = DynamicTo<HTMLInputElement>(label->Control());
if (!input)
return false;
if (!input->GetLayoutObject() ||
input->GetLayoutObject()->Style()->Visibility() !=
EVisibility::kVisible) {
return false;
}
if (!input->IsCheckable()) {
return false;
}
if (!IsNameFromLabelElement(input)) {
return false;
}
DCHECK_NE(input->labels()->length(), 0U);
Element* first_child = ElementTraversal::FirstChild(*label);
if (!first_child)
return true;
if (first_child != input)
return false;
return ElementTraversal::NextSibling(*first_child) == nullptr;
}
void AXNodeObject::GetRelativeBounds(AXObject** out_container,
gfx::RectF& out_bounds_in_container,
gfx::Transform& out_container_transform,
bool* clips_children) const {
if (GetLayoutObject()) {
AXObject::GetRelativeBounds(out_container, out_bounds_in_container,
out_container_transform, clips_children);
return;
}
#if DCHECK_IS_ON()
DCHECK(!getting_bounds_) << "GetRelativeBounds reentrant: " << ToString();
base::AutoReset<bool> reentrancy_protector(&getting_bounds_, true);
#endif
*out_container = nullptr;
out_bounds_in_container = gfx::RectF();
out_container_transform.MakeIdentity();
if (RoleValue() == ax::mojom::blink::Role::kMenuListOption) {
auto* select = To<HTMLOptionElement>(GetNode())->OwnerSelectElement();
if (auto* ax_select = AXObjectCache().Get(select)) {
if (ax_select->IsExpanded() == kExpandedExpanded) {
auto* options_bounds = AXObjectCache().GetOptionsBounds(*ax_select);
if (options_bounds) {
unsigned int index = static_cast<unsigned int>(
To<HTMLOptionElement>(GetNode())->index());
if (index < options_bounds->size()) {
out_bounds_in_container = gfx::RectF(options_bounds->at(index));
}
return;
}
}
}
}
if (auto canvas_bounds =
AXObjectCache().GetCanvasElementBounds(this->AXObjectID())) {
*out_container = AXObjectCache().ObjectFromAXID(canvas_bounds->second);
if (*out_container) {
out_bounds_in_container = gfx::RectF(canvas_bounds->first);
return;
}
}
Element* element = GetElement();
if ((GetNode()->ParentOrShadowHostElement() &&
GetNode()->ParentOrShadowHostElement()->IsCanvasOrInCanvasSubtree()) ||
(element && element->HasDisplayContentsStyle())) {
Vector<gfx::RectF> rects;
for (Node& child : NodeTraversal::ChildrenOf(*GetNode())) {
if (child.IsHTMLElement()) {
if (AXObject* obj = AXObjectCache().Get(&child)) {
AXObject* container;
gfx::RectF bounds;
obj->GetRelativeBounds(&container, bounds, out_container_transform,
clips_children);
if (container) {
*out_container = container;
rects.push_back(bounds);
}
}
}
}
if (*out_container) {
for (auto& rect : rects)
out_bounds_in_container.Union(rect);
return;
}
}
for (AXObject* position_provider = ParentObject(); position_provider;
position_provider = position_provider->ParentObject()) {
if (position_provider->GetLayoutObject()) {
position_provider->GetRelativeBounds(
out_container, out_bounds_in_container, out_container_transform,
clips_children);
if (*out_container) {
out_bounds_in_container.set_size(
gfx::SizeF(out_bounds_in_container.width(),
std::min(10.0f, out_bounds_in_container.height())));
}
break;
}
}
}
bool AXNodeObject::HasValidHTMLTableStructureAndLayout() const {
if (!IsTableLikeRole() || !GetLayoutObject() ||
!GetLayoutObject()->IsTable() || !IsA<HTMLTableElement>(GetNode()))
return false;
auto* table = To<HTMLTableElement>(GetNode());
auto* thead = table->tHead();
auto* tfoot = table->tFoot();
for (Node* node = LayoutTreeBuilderTraversal::FirstChild(*GetElement()); node;
node = LayoutTreeBuilderTraversal::NextSibling(*node)) {
if (Element* child = DynamicTo<Element>(node)) {
if (child == thead || child == tfoot) {
continue;
}
if (IsA<HTMLTableSectionElement>(child) &&
child->HasTagName(html_names::kTbodyTag)) {
continue;
}
if (!child->GetLayoutObject() &&
child->HasTagName(html_names::kColgroupTag)) {
continue;
}
if (IsA<HTMLTableCaptionElement>(child) && child == table->caption()) {
continue;
}
} else if (!node->GetLayoutObject()) {
continue;
}
return false;
}
return true;
}
void AXNodeObject::AddTableChildren() {
DCHECK(HasValidHTMLTableStructureAndLayout());
auto* html_table_element = To<HTMLTableElement>(GetNode());
AddNodeChild(html_table_element->caption());
AddNodeChild(html_table_element->tHead());
for (Node* node : *html_table_element->tBodies())
AddNodeChild(node);
AddNodeChild(html_table_element->tFoot());
}
int AXNodeObject::TextOffsetInFormattingContext(int offset) const {
DCHECK_GE(offset, 0);
if (IsDetached())
return 0;
const LayoutObject* layout_obj =
GetNode() ? AssociatedLayoutObjectOf(*GetNode(), offset)
: GetLayoutObject();
if (!layout_obj)
return AXObject::TextOffsetInFormattingContext(offset);
if (layout_obj->IsLayoutInline()) {
const AXObject* first_child = FirstChildIncludingIgnored();
return first_child ? first_child->TextOffsetInFormattingContext(offset)
: offset;
}
const bool is_atomic_inline_level =
layout_obj->IsInline() && layout_obj->IsAtomicInlineLevel();
if (!is_atomic_inline_level && !layout_obj->IsText()) {
return AXObject::TextOffsetInFormattingContext(offset);
}
if (const LayoutText* layout_text = DynamicTo<LayoutText>(layout_obj)) {
if (layout_text->HasEmptyText()) {
return AXObject::TextOffsetInFormattingContext(offset);
}
}
LayoutBlockFlow* formatting_context =
OffsetMapping::GetInlineFormattingContextOf(*layout_obj);
if (!formatting_context || formatting_context == layout_obj)
return AXObject::TextOffsetInFormattingContext(offset);
const OffsetMapping* inline_offset_mapping =
InlineNode::GetOffsetMapping(formatting_context);
if (!inline_offset_mapping)
return AXObject::TextOffsetInFormattingContext(offset);
const base::span<const OffsetMappingUnit> mapping_units =
inline_offset_mapping->GetMappingUnitsForLayoutObject(*layout_obj);
if (mapping_units.empty())
return AXObject::TextOffsetInFormattingContext(offset);
return static_cast<int>(mapping_units.front().TextContentStart()) + offset;
}
void AXNodeObject::LoadInlineTextBoxes() {
#if DCHECK_IS_ON()
DCHECK(GetDocument()->Lifecycle().GetState() >=
DocumentLifecycle::kLayoutClean)
<< "Unclean document at lifecycle "
<< GetDocument()->Lifecycle().ToString();
#endif
std::queue<AXID> work_queue;
work_queue.push(AXObjectID());
while (!work_queue.empty()) {
AXObject* work_obj = AXObjectCache().ObjectFromAXID(work_queue.front());
work_queue.pop();
if (!work_obj || !work_obj->IsIncludedInTree()) {
continue;
}
if (CanHaveInlineTextBoxChildren(work_obj) && HasLayoutText(work_obj)) {
if (work_obj->CachedChildrenIncludingIgnored().empty()) {
work_obj->LoadInlineTextBoxesHelper();
}
} else {
for (const auto& child : work_obj->ChildrenIncludingIgnored())
work_queue.push(child->AXObjectID());
}
}
if (!AXObjectCache().lifecycle().StateAllowsImmediateTreeUpdates()) {
AXObjectCache().UpdateAXForAllDocuments();
}
}
void AXNodeObject::LoadInlineTextBoxesHelper() {
DCHECK(CachedChildrenIncludingIgnored().empty());
#if defined(REDUCE_AX_INLINE_TEXTBOXES)
SetAlwaysLoadInlineTextBoxes(true);
#endif
if (AXObjectCache().lifecycle().StateAllowsImmediateTreeUpdates()) {
if (::features::IsAccessibilityBlockFlowIteratorEnabled()) {
AddInlineTextBoxChildrenWithBlockFlowIterator();
} else {
AddInlineTextBoxChildren();
}
SetNeedsToUpdateChildren(false);
if (!CachedChildrenIncludingIgnored().empty()) {
AXObjectCache().AddDirtyObjectToSerializationQueue(
this, ax::mojom::blink::EventFrom::kNone,
ax::mojom::blink::Action::kNone, {});
}
} else {
AXObjectCache().ChildrenChanged(this);
}
}
void AXNodeObject::AddInlineTextBoxChildrenWithBlockFlowIterator() {
CHECK(GetDocument());
CHECK(ShouldLoadInlineTextBoxes());
CHECK(GetLayoutObject());
GetLayoutObject()->CheckIsNotDestroyed();
CHECK(GetLayoutObject()->IsText()) << GetLayoutObject() << " " << this;
CHECK(!GetLayoutObject()->NeedsLayout());
CHECK(AXObjectCache().lifecycle().StateAllowsImmediateTreeUpdates())
<< AXObjectCache();
AXBlockFlowIterator it(this);
while (it.Next()) {
AXObject* box =
AXObjectCache().GetOrCreate(it.CurrentFragmentIndex(), this);
if (!box) {
return;
}
children_.push_back(box);
}
}
void AXNodeObject::AddInlineTextBoxChildren() {
CHECK(GetDocument());
CHECK(ShouldLoadInlineTextBoxes());
CHECK(GetLayoutObject());
GetLayoutObject()->CheckIsNotDestroyed();
CHECK(GetLayoutObject()->IsText()) << GetLayoutObject() << " " << this;
CHECK(!GetLayoutObject()->NeedsLayout());
CHECK(AXObjectCache().GetAXMode().has_mode(ui::AXMode::kInlineTextBoxes));
CHECK(!AXObjectCache().GetAXMode().HasFilterFlags(
ui::AXMode::kFormsAndLabelsOnly))
<< "Form controls mode should not have inline text boxes turned on.";
CHECK(AXObjectCache().lifecycle().StateAllowsImmediateTreeUpdates())
<< AXObjectCache();
auto* layout_text = To<LayoutText>(GetLayoutObject());
for (auto* box = layout_text->FirstAbstractInlineTextBox(); box;
box = box->NextInlineTextBox()) {
AXObject* ax_box = AXObjectCache().GetOrCreate(box, this);
if (!ax_box) {
continue;
}
children_.push_back(ax_box);
}
}
void AXNodeObject::AddValidationMessageChild() {
DCHECK(IsWebArea()) << "Validation message must be child of root";
DCHECK_EQ(children_.size(), 0U)
<< "Validation message must be the first child";
AddChildAndCheckIncluded(AXObjectCache().ValidationMessageObjectIfInvalid());
}
void AXNodeObject::AddImageMapChildren() {
HTMLMapElement* map = GetMapForImage(GetNode());
if (!map)
return;
HTMLImageElement* curr_image_element = DynamicTo<HTMLImageElement>(GetNode());
DCHECK(curr_image_element);
DCHECK(curr_image_element->IsLink());
DCHECK(
!curr_image_element->FastGetAttribute(html_names::kUsemapAttr).empty());
HTMLImageElement* primary_image_element = map->ImageElement();
if (primary_image_element != curr_image_element) {
return;
}
Node* child = LayoutTreeBuilderTraversal::FirstChild(*map);
while (child) {
AddChildAndCheckIncluded(AXObjectCache().GetOrCreate(child, this));
child = LayoutTreeBuilderTraversal::NextSibling(*child);
}
}
void AXNodeObject::AddPopupChildren() {
if (AXObjectCache().IsForSnapshot()) {
return;
}
auto* html_select_element = DynamicTo<HTMLSelectElement>(GetNode());
if (html_select_element) {
if (html_select_element->UsesMenuList()) {
AddChildAndCheckIncluded(html_select_element->PopupRootAXObject());
}
return;
}
auto* html_input_element = DynamicTo<HTMLInputElement>(GetNode());
if (html_input_element) {
AddChildAndCheckIncluded(html_input_element->PopupRootAXObject());
}
}
void AXNodeObject::AddPseudoElementChildrenFromLayoutTree() {
if (!IsVisible() || !GetLayoutObject()) {
DCHECK(GetNode());
DCHECK(GetNode()->IsPseudoElement());
return;
}
LayoutObject* child = GetLayoutObject()->SlowFirstChild();
while (child) {
if (AXObject* ax_child = AXObjectCache().GetOrCreate(child, this)) {
DCHECK(AXObjectCacheImpl::IsRelevantPseudoElementDescendant(*child));
AddChildAndCheckIncluded(ax_child);
}
child = child->NextSibling();
}
}
void AXNodeObject::AddNodeChildren() {
if (!node_)
return;
if (IsA<HTMLFrameElementBase>(GetNode()))
return;
HeapVector<Member<Node>> reading_flow_children;
if (Element* element = GetElement()) {
reading_flow_children = element->ReadingFlowChildren();
}
if (!reading_flow_children.empty()) {
HeapHashSet<Member<Node>> ax_children_added;
for (Node* reading_flow_item : reading_flow_children) {
if (IsAddedOnlyViaSpecialTraversal(reading_flow_item)) {
continue;
}
if (ax_children_added.insert(reading_flow_item).is_new_entry) {
AddNodeChild(reading_flow_item);
}
}
#if DCHECK_IS_ON()
size_t num_layout_tree_children = 0;
for (Node* child = LayoutTreeBuilderTraversal::FirstChild(*node_); child;
child = LayoutTreeBuilderTraversal::NextSibling(*child)) {
if (IsAddedOnlyViaSpecialTraversal(child)) {
continue;
}
DCHECK(ax_children_added.Contains(child));
++num_layout_tree_children;
}
DCHECK_EQ(ax_children_added.size(), num_layout_tree_children);
#endif
} else {
for (Node* child = LayoutTreeBuilderTraversal::FirstChild(*node_); child;
child = LayoutTreeBuilderTraversal::NextSibling(*child)) {
if (IsAddedOnlyViaSpecialTraversal(child)) {
continue;
}
AddNodeChild(child);
}
}
}
void AXNodeObject::AddOwnedChildren() {
AXObjectVector owned_children;
AXObjectCache().ValidatedAriaOwnedChildren(this, owned_children);
DCHECK(owned_children.size() == 0 || AXRelationCache::IsValidOwner(this))
<< "This object is not allowed to use aria-owns, but it is.\n"
<< this;
for (const auto& owned_child : owned_children) {
DCHECK(owned_child->GetNode());
DCHECK(AXRelationCache::IsValidOwnedChild(*owned_child->GetNode()))
<< "This object is not allowed to be owned, but it is.\n"
<< owned_child;
AddChildAndCheckIncluded(owned_child, true);
}
}
void AXNodeObject::AddChildrenImpl() {
#define CHECK_ATTACHED() \
if (IsDetached()) { \
NOTREACHED() << "Detached adding children: " << this; \
}
CHECK(NeedsToUpdateChildren());
CHECK(CanHaveChildren());
if (ShouldLoadInlineTextBoxes() && HasLayoutText(this)) {
if (::features::IsAccessibilityBlockFlowIteratorEnabled()) {
AddInlineTextBoxChildrenWithBlockFlowIterator();
} else {
AddInlineTextBoxChildren();
}
CHECK_ATTACHED();
return;
}
if (IsA<HTMLImageElement>(GetNode())) {
AddImageMapChildren();
CHECK_ATTACHED();
return;
}
if (IsWebArea())
AddValidationMessageChild();
CHECK_ATTACHED();
if (HasValidHTMLTableStructureAndLayout()) {
AddTableChildren();
} else if (GetNode() && GetNode()->IsScrollMarkerGroupPseudoElement()) {
AddScrollMarkerGroupChildren();
} else if (ShouldUseLayoutObjectTraversalForChildren()) {
AddPseudoElementChildrenFromLayoutTree();
} else {
AddNodeChildren();
}
CHECK_ATTACHED();
AddPopupChildren();
CHECK_ATTACHED();
AddOwnedChildren();
CHECK_ATTACHED();
}
void AXNodeObject::AddScrollMarkerGroupChildren() {
DCHECK(GetNode() && GetNode()->IsScrollMarkerGroupPseudoElement());
if (!IsVisible() || !GetLayoutObject()) {
DCHECK(GetNode());
DCHECK(GetNode()->IsPseudoElement());
return;
}
LayoutObject* child = GetLayoutObject()->SlowFirstChild();
while (child) {
DCHECK(
child->IsAnonymous() ||
(child->GetNode() && child->GetNode()->IsScrollMarkerPseudoElement()));
if (child->GetNode() && child->GetNode()->IsScrollMarkerPseudoElement()) {
AddNodeChild(child->GetNode());
}
child = child->NextInPreOrder(GetLayoutObject());
}
}
void AXNodeObject::AddChildren() {
#if DCHECK_IS_ON()
DCHECK(!IsDetached());
DCHECK(children_.empty()) << "\nParent still has " << children_.size()
<< " children before adding:" << "\nParent is "
<< this << "\nFirst child is " << children_[0];
#endif
#if AX_FAIL_FAST_BUILD()
SANITIZER_CHECK(!is_computing_text_from_descendants_)
<< "Should not attempt to simultaneously compute text from descendants "
"and add children on: "
<< this;
SANITIZER_CHECK(!is_adding_children_) << " Reentering method on " << this;
base::AutoReset<bool> reentrancy_protector(&is_adding_children_, true);
#endif
AddChildrenImpl();
SetNeedsToUpdateChildren(false);
#if DCHECK_IS_ON()
for (const auto& child : children_) {
DCHECK(!child->IsDetached()) << "A brand new child was detached.\n"
<< child << "\n ... of parent " << this;
}
#endif
}
void AXNodeObject::AddNodeChild(Node* node) {
if (!node)
return;
if (Element* element = DynamicTo<Element>(node);
element && element->HasScrollButtonOrMarkerGroupPseudos()) {
VectorOf<Node> children = UnpackScrollerWithSiblingControls(element);
for (auto child : children) {
AddNodeChildImpl(child.Get());
}
} else {
AddNodeChildImpl(node);
}
}
void AXNodeObject::AddNodeChildImpl(Node* node) {
CHECK(node);
AXObject* ax_child = AXObjectCache().Get(node);
CHECK(!ax_child || !ax_child->IsDetached());
if (AXObjectCache().IsAriaOwned(ax_child))
return;
AXObject* ax_cached_parent =
ax_child ? ax_child->ParentObjectIfPresent() : nullptr;
if (!ax_child) {
ax_child =
AXObjectCache().CreateAndInit(node, node->GetLayoutObject(), this);
if (!ax_child) {
return;
}
CHECK(!ax_child->IsDetached());
}
AddChild(ax_child);
bool did_add_child_as_included =
children_.size() && children_[children_.size() - 1] == ax_child;
if (did_add_child_as_included && ax_cached_parent) {
CHECK(ax_child->IsIncludedInTree());
DUMP_WILL_BE_CHECK(ax_cached_parent->AXObjectID() == AXObjectID())
<< "Newly added child shouldn't have a different preexisting parent:"
<< "\nChild = " << ax_child << "\nNew parent = " << this
<< "\nPreexisting parent = " << ax_cached_parent;
}
}
#if DCHECK_IS_ON()
void AXNodeObject::CheckValidChild(AXObject* child) {
DCHECK(!child->IsDetached()) << "Cannot add a detached child.\n" << child;
Node* child_node = child->GetNode();
if (IsA<HTMLAreaElement>(child_node)) {
AXObject* ancestor = this;
while (ancestor && !IsA<HTMLImageElement>(ancestor->GetNode()))
ancestor = ancestor->ParentObject();
DCHECK(ancestor && IsA<HTMLImageElement>(ancestor->GetNode()))
<< "Area elements can only be added by image parents: " << child
<< " had a parent of " << this;
}
DCHECK(!IsA<HTMLFrameElementBase>(GetNode()) ||
IsA<Document>(child->GetNode()))
<< "Cannot have a non-document child of a frame or iframe."
<< "\nChild: " << child << "\nParent: " << child->ParentObject();
}
#endif
void AXNodeObject::AddChild(AXObject* child, bool is_from_aria_owns) {
if (!child)
return;
#if DCHECK_IS_ON()
CheckValidChild(child);
#endif
unsigned int index = children_.size();
InsertChild(child, index, is_from_aria_owns);
}
void AXNodeObject::AddChildAndCheckIncluded(AXObject* child,
bool is_from_aria_owns) {
if (!child)
return;
DCHECK(child->CachedIsIncludedInTree());
AddChild(child, is_from_aria_owns);
}
void AXNodeObject::InsertChild(AXObject* child,
unsigned index,
bool is_from_aria_owns) {
if (!child)
return;
DCHECK(CanHaveChildren());
DCHECK(!child->IsDetached()) << "Cannot add a detached child: " << child;
DCHECK_EQ(AXObjectCache().IsAriaOwned(child), is_from_aria_owns);
child->SetParent(this);
if (ChildrenNeedToUpdateCachedValues()) {
child->InvalidateCachedValues(TreeUpdateReason::kChildInserted);
}
child->UpdateCachedAttributeValuesIfNeeded(
false);
if (!child->IsIncludedInTree()) {
DCHECK(!is_from_aria_owns) << "Owned elements must be in tree: " << child
<< "\nRecompute included in tree: "
<< child->ComputeIsIgnoredButIncludedInTree();
const auto& children = child->ChildrenIncludingIgnored();
wtf_size_t length = children.size();
int new_index = index;
for (wtf_size_t i = 0; i < length; ++i) {
if (children[i]->IsDetached()) {
DUMP_WILL_BE_NOTREACHED()
<< "Cannot add a detached child: " << "\n* Child: " << children[i]
<< "\n* Parent: " << child << "\n* Grandparent: " << this;
continue;
}
if (!AXObjectCache().IsAriaOwned(children[i]))
children_.insert(new_index++, children[i]);
}
} else {
children_.insert(index, child);
}
}
bool AXNodeObject::CanHaveChildren() const {
if (IsDetached()) {
DUMP_WILL_BE_NOTREACHED()
<< "Calling CanHaveChildren on a detached node is not allowed." << this;
return false;
}
if (AXObjectCache().GetAXObjectChildAXTreeID(AXObjectID())) {
return false;
}
bool result = !GetElement() || AXObject::CanHaveChildren(*GetElement());
switch (native_role_) {
case ax::mojom::blink::Role::kListBoxOption:
break;
case ax::mojom::blink::Role::kCheckBox:
case ax::mojom::blink::Role::kMenuItemCheckBox:
case ax::mojom::blink::Role::kMenuItemRadio:
case ax::mojom::blink::Role::kProgressIndicator:
case ax::mojom::blink::Role::kRadioButton:
case ax::mojom::blink::Role::kScrollBar:
case ax::mojom::blink::Role::kSlider:
case ax::mojom::blink::Role::kSplitter:
case ax::mojom::blink::Role::kSwitch:
case ax::mojom::blink::Role::kTab:
DCHECK(!result) << "Expected to disallow children for:" << "\n* Node: "
<< GetNode() << "\n* Layout Object: " << GetLayoutObject()
<< "\n* Native role: " << native_role_
<< "\n* Aria role: " << RawAriaRole();
break;
case ax::mojom::blink::Role::kComboBoxSelect:
case ax::mojom::blink::Role::kMenuItem:
case ax::mojom::blink::Role::kPopUpButton:
case ax::mojom::blink::Role::kStaticText:
DCHECK(result) << "Expected to allow children for " << GetElement()
<< " on role " << native_role_;
break;
default:
break;
}
return result;
}
double AXNodeObject::EstimatedLoadingProgress() const {
if (!GetDocument())
return 0;
if (IsLoaded())
return 1.0;
if (LocalFrame* frame = GetDocument()->GetFrame())
return frame->Loader().Progress().EstimatedProgress();
return 0;
}
Element* AXNodeObject::ActionElement() const {
const AXObject* current = this;
if (blink::IsA<blink::Document>(current->GetNode()))
return nullptr;
DCHECK(current->GetElement() || current->IsTextObject() ||
current->ShouldUseLayoutObjectTraversalForChildren());
while (current) {
if (current->IsClickable()) {
Element* click_element = current->GetElement();
DCHECK(click_element) << "Only elements are clickable";
if (!GetNode() || click_element->contains(GetNode()))
return click_element;
return nullptr;
}
current = current->ParentObject();
}
return nullptr;
}
Element* AXNodeObject::AnchorElement() const {
const AXObject* current = this;
while (current) {
if (current->IsLink()) {
if (!current->GetElement()) {
DUMP_WILL_BE_NOTREACHED()
<< "An AXObject* that is a link should always have an element.\n"
<< this << "\n"
<< current;
}
return current->GetElement();
}
current = current->ParentObject();
}
return nullptr;
}
Document* AXNodeObject::GetDocument() const {
if (GetNode()) {
return &GetNode()->GetDocument();
}
if (GetLayoutObject()) {
return &GetLayoutObject()->GetDocument();
}
return nullptr;
}
LayoutObject* AXNodeObject::GetLayoutObject() const {
return layout_object_;
}
bool AXNodeObject::OnNativeBlurAction() {
Document* document = GetDocument();
Node* node = GetNode();
if (!document || !node) {
return false;
}
Element* element = GetElement();
if (element) {
element->blur();
return true;
}
if (IsA<Document>(GetNode())) {
document->ClearFocusedElement();
return true;
}
return false;
}
bool AXNodeObject::OnNativeFocusAction() {
Document* document = GetDocument();
Node* node = GetNode();
if (!document || !node)
return false;
if (!CanSetFocusAttribute())
return false;
if (IsWebArea()) {
if (Page* page = document->GetPage()) {
page->GetFocusController().SetFocusedElement(document->documentElement(),
document->GetFrame());
} else {
document->ClearFocusedElement();
}
return true;
}
Element* element = GetElement();
if (!element) {
document->ClearFocusedElement();
return true;
}
#if BUILDFLAG(IS_ANDROID) || BUILDFLAG(ARKWEB_ACCESSIBILITY)
if (document->FocusedElement() == element) {
document->ClearFocusedElement();
if (IsDetached()) {
return false;
}
}
#endif
element->Focus(FocusParams(FocusTrigger::kUserGesture));
LocalFrame::NotifyUserActivation(
document->GetFrame(),
mojom::blink::UserActivationNotificationType::kInteraction);
return true;
}
bool AXNodeObject::OnNativeIncrementAction() {
LocalFrame* frame = GetDocument() ? GetDocument()->GetFrame() : nullptr;
LocalFrame::NotifyUserActivation(
frame, mojom::blink::UserActivationNotificationType::kInteraction);
AlterSliderOrSpinButtonValue(true);
return true;
}
bool AXNodeObject::OnNativeDecrementAction() {
LocalFrame* frame = GetDocument() ? GetDocument()->GetFrame() : nullptr;
LocalFrame::NotifyUserActivation(
frame, mojom::blink::UserActivationNotificationType::kInteraction);
AlterSliderOrSpinButtonValue(false);
return true;
}
bool AXNodeObject::OnNativeSetSequentialFocusNavigationStartingPointAction() {
if (!GetNode())
return false;
Document* document = GetDocument();
document->ClearFocusedElement();
document->SetSequentialFocusNavigationStartingPoint(GetNode());
return true;
}
void AXNodeObject::SelectedOptions(AXObjectVector& options) const {
if (auto* select = DynamicTo<HTMLSelectElement>(GetNode())) {
for (auto* const option : *select->selectedOptions()) {
AXObject* ax_option = AXObjectCache().Get(option);
if (ax_option)
options.push_back(ax_option);
}
return;
}
const AXObjectVector& children = ChildrenIncludingIgnored();
if (RoleValue() == ax::mojom::blink::Role::kComboBoxGrouping ||
RoleValue() == ax::mojom::blink::Role::kComboBoxMenuButton) {
for (const auto& obj : children) {
if (obj->RoleValue() == ax::mojom::blink::Role::kListBox) {
obj->SelectedOptions(options);
return;
}
}
}
for (const auto& obj : children) {
if (obj->IsSelected() == kSelectedStateTrue)
options.push_back(obj);
}
}
void AXNodeObject::HandleAriaExpandedChanged() {
AXObject* container_parent = ParentObject();
while (container_parent) {
bool found_parent = false;
switch (container_parent->RoleValue()) {
case ax::mojom::blink::Role::kLayoutTable:
case ax::mojom::blink::Role::kTree:
case ax::mojom::blink::Role::kTreeGrid:
case ax::mojom::blink::Role::kGrid:
case ax::mojom::blink::Role::kTable:
found_parent = true;
break;
default:
break;
}
if (found_parent)
break;
container_parent = container_parent->ParentObject();
}
if (container_parent) {
AXObjectCache().PostNotification(container_parent,
ax::mojom::blink::Event::kRowCountChanged);
}
AccessibilityExpanded expanded = IsExpanded();
if (!expanded)
return;
if (RoleValue() == ax::mojom::blink::Role::kRow ||
RoleValue() == ax::mojom::blink::Role::kTreeItem) {
ax::mojom::blink::Event notification =
ax::mojom::blink::Event::kRowExpanded;
if (expanded == kExpandedCollapsed)
notification = ax::mojom::blink::Event::kRowCollapsed;
AXObjectCache().PostNotification(this, notification);
} else {
AXObjectCache().PostNotification(this,
ax::mojom::blink::Event::kExpandedChanged);
}
}
void AXNodeObject::HandleActiveDescendantChanged() {
if (!GetLayoutObject() || !GetNode() || !GetDocument())
return;
Node* focused_node = GetDocument()->FocusedElement();
if (focused_node == GetNode()) {
AXObject* active_descendant = ActiveDescendant();
if (active_descendant) {
if (active_descendant->IsSelectedFromFocus()) {
AXObjectCache().HandleAriaSelectedChangedWithCleanLayout(
active_descendant->GetNode());
} else if (active_descendant->RoleValue() ==
ax::mojom::blink::Role::kRow) {
AXObjectCache().MarkAXObjectDirtyWithCleanLayout(active_descendant);
}
}
AXObjectCache().MarkAXObjectDirtyWithCleanLayout(this);
}
}
AXObject::AXObjectVector AXNodeObject::ErrorMessage() const {
if (GetInvalidState() == ax::mojom::blink::InvalidState::kFalse)
return AXObjectVector();
AXObjectVector aria_error_messages =
RelationVectorFromAria(html_names::kAriaErrormessageAttr);
if (aria_error_messages.size() > 0) {
return aria_error_messages;
}
AXObjectVector html_error_messages = ErrorMessageFromHTML();
if (html_error_messages.size() > 0) {
return html_error_messages;
}
return AXObjectVector();
}
AXObject::AXObjectVector AXNodeObject::RelationVectorFromAria(
const QualifiedName& attr_name) const {
Element* el = GetElement();
if (!el) {
return AXObjectVector();
}
const GCedHeapVector<Member<Element>>* elements_from_attribute =
ElementsFromAttributeOrInternals(el, attr_name);
if (!elements_from_attribute) {
return AXObjectVector();
}
AXObjectVector objects;
for (Element* element : *elements_from_attribute) {
AXObject* obj = AXObjectCache().Get(element);
if (obj && !obj->IsIgnored()) {
objects.push_back(obj);
}
}
return objects;
}
AXObject::AXObjectVector AXNodeObject::ErrorMessageFromHTML() const {
if (this != AXObjectCache().FocusedObject()) {
return AXObjectVector();
}
AXObject* native_error_message =
AXObjectCache().ValidationMessageObjectIfInvalid();
if (native_error_message && !native_error_message->IsDetached()) {
CHECK_GE(native_error_message->IndexInParent(), 0);
return AXObjectVector({native_error_message});
}
return AXObjectVector();
}
String AXNodeObject::TextAlternativeFromTooltip(
ax::mojom::blink::NameFrom& name_from,
NameSources* name_sources,
bool* found_text_alternative,
String* text_alternative,
AXRelatedObjectVector* related_objects) const {
if (!GetElement()) {
return String();
}
name_from = ax::mojom::blink::NameFrom::kTitle;
const AtomicString& title = GetElement()->FastGetAttribute(kTitleAttr);
String title_text = TextAlternativeFromTitleAttribute(
title, name_from, name_sources, found_text_alternative);
if (!title_text.empty()) {
*text_alternative = title_text;
return title_text;
}
AXObject* popover_ax_object = nullptr;
if (RuntimeEnabledFeatures::HTMLInterestForAttributeEnabled()) {
popover_ax_object = AXObjectCache().Get(GetElement()->InterestForElement());
}
if (popover_ax_object) {
DCHECK(RuntimeEnabledFeatures::HTMLInterestForAttributeEnabled());
name_from = ax::mojom::blink::NameFrom::kInterestFor;
} else {
auto* form_control = DynamicTo<HTMLFormControlElement>(GetElement());
if (!form_control) {
return String();
}
auto popover_target = form_control->popoverTargetElement();
if (!popover_target.popover ||
popover_target.popover->PopoverType() != PopoverValueType::kHint) {
return String();
}
popover_ax_object = AXObjectCache().Get(popover_target.popover);
name_from = ax::mojom::blink::NameFrom::kPopoverTarget;
}
if (name_sources) {
name_sources->push_back(
NameSource(*found_text_alternative, html_names::kPopovertargetAttr));
name_sources->back().type = name_from;
}
if (!popover_ax_object || !popover_ax_object->IsPlainContent()) {
return String();
}
AXObjectSet visited;
String popover_text =
RecursiveTextAlternative(*popover_ax_object, popover_ax_object, visited);
if (popover_text.StripWhiteSpace() ==
GetElement()->GetInnerTextWithoutUpdate().StripWhiteSpace()) {
return String();
}
*text_alternative = popover_text;
if (related_objects) {
related_objects->push_back(MakeGarbageCollected<NameSourceRelatedObject>(
popover_ax_object, popover_text));
}
if (name_sources) {
NameSource& source = name_sources->back();
source.related_objects = *related_objects;
source.text = *text_alternative;
*found_text_alternative = true;
}
return popover_text;
}
String AXNodeObject::TextAlternativeFromTitleAttribute(
const AtomicString& title,
ax::mojom::blink::NameFrom& name_from,
NameSources* name_sources,
bool* found_text_alternative) const {
DCHECK(GetElement());
String text_alternative;
if (name_sources) {
name_sources->push_back(NameSource(*found_text_alternative, kTitleAttr));
name_sources->back().type = name_from;
}
name_from = ax::mojom::blink::NameFrom::kTitle;
if (!title.IsNull() &&
String(title).StripWhiteSpace() !=
GetElement()->GetInnerTextWithoutUpdate().StripWhiteSpace()) {
text_alternative = title;
if (name_sources) {
NameSource& source = name_sources->back();
source.attribute_value = title;
source.attribute_value = title;
source.text = text_alternative;
*found_text_alternative = true;
}
}
return text_alternative;
}
String AXNodeObject::NativeTextAlternative(
AXObjectSet& visited,
ax::mojom::blink::NameFrom& name_from,
AXRelatedObjectVector* related_objects,
NameSources* name_sources,
bool* found_text_alternative) const {
if (!GetNode())
return String();
if (name_sources)
DCHECK(related_objects);
String text_alternative;
AXRelatedObjectVector local_related_objects;
if (auto* option_element = DynamicTo<HTMLOptionElement>(GetNode())) {
if (option_element->HasOneTextChild()) {
name_from = ax::mojom::blink::NameFrom::kContents;
text_alternative = option_element->DisplayLabel();
if (!text_alternative.empty()) {
if (name_sources) {
name_sources->push_back(NameSource(*found_text_alternative));
name_sources->back().type = name_from;
name_sources->back().text = text_alternative;
*found_text_alternative = true;
}
return text_alternative;
}
}
}
if (auto* opt_group_element = DynamicTo<HTMLOptGroupElement>(GetNode())) {
name_from = ax::mojom::blink::NameFrom::kAttribute;
text_alternative = opt_group_element->GroupLabelText();
if (!text_alternative.empty()) {
if (name_sources) {
name_sources->push_back(NameSource(*found_text_alternative));
name_sources->back().type = name_from;
name_sources->back().text = text_alternative;
*found_text_alternative = true;
}
return text_alternative;
}
}
auto* html_element = DynamicTo<HTMLElement>(GetNode());
if (html_element && html_element->IsLabelable()) {
name_from = ax::mojom::blink::NameFrom::kRelatedElement;
if (name_sources) {
name_sources->push_back(NameSource(*found_text_alternative));
name_sources->back().type = name_from;
name_sources->back().native_source = kAXTextFromNativeHTMLLabel;
}
LabelsNodeList* labels = nullptr;
if (AXObjectCache().MayHaveHTMLLabel(*html_element))
labels = html_element->labels();
if (labels && labels->length() > 0) {
HeapVector<Member<Element>> label_elements;
for (unsigned label_index = 0; label_index < labels->length();
++label_index) {
Element* label = labels->item(label_index);
if (name_sources) {
if (!label->FastGetAttribute(html_names::kForAttr).empty() &&
label->FastGetAttribute(html_names::kForAttr) ==
html_element->GetIdAttribute()) {
name_sources->back().native_source = kAXTextFromNativeHTMLLabelFor;
} else {
name_sources->back().native_source =
kAXTextFromNativeHTMLLabelWrapped;
}
}
label_elements.push_back(label);
}
text_alternative =
TextFromElements(false, visited, label_elements, related_objects);
if (!text_alternative.IsNull()) {
*found_text_alternative = true;
if (name_sources) {
NameSource& source = name_sources->back();
source.related_objects = *related_objects;
source.text = text_alternative;
} else {
return text_alternative.StripWhiteSpace();
}
} else if (name_sources) {
name_sources->back().invalid = true;
}
}
}
const auto* input_element = DynamicTo<HTMLInputElement>(GetNode());
if (input_element && input_element->IsTextButton()) {
name_from = ax::mojom::blink::NameFrom::kValue;
if (name_sources) {
name_sources->push_back(NameSource(*found_text_alternative, kValueAttr));
name_sources->back().type = name_from;
}
String value = input_element->Value();
if (!value.IsNull()) {
text_alternative = value;
if (name_sources) {
NameSource& source = name_sources->back();
source.text = text_alternative;
*found_text_alternative = true;
} else {
return text_alternative;
}
}
if (!GetLayoutObject()) {
String default_label = input_element->ValueOrDefaultLabel();
if (value.IsNull() && !default_label.IsNull()) {
name_from = ax::mojom::blink::NameFrom::kContents;
if (name_sources) {
name_sources->push_back(NameSource(*found_text_alternative));
name_sources->back().type = name_from;
}
text_alternative = default_label;
if (name_sources) {
NameSource& source = name_sources->back();
source.text = text_alternative;
*found_text_alternative = true;
} else {
return text_alternative;
}
}
}
return text_alternative;
}
if (input_element &&
input_element->getAttribute(kTypeAttr) == input_type_names::kImage) {
const AtomicString& alt = input_element->getAttribute(kAltAttr);
const bool is_empty = alt.empty() && !alt.IsNull();
name_from = is_empty ? ax::mojom::blink::NameFrom::kAttributeExplicitlyEmpty
: ax::mojom::blink::NameFrom::kAttribute;
if (name_sources) {
name_sources->push_back(NameSource(*found_text_alternative, kAltAttr));
name_sources->back().type = name_from;
}
if (!alt.empty()) {
text_alternative = alt;
if (name_sources) {
NameSource& source = name_sources->back();
source.attribute_value = alt;
source.text = text_alternative;
*found_text_alternative = true;
} else {
return text_alternative;
}
}
if (name_sources) {
name_sources->push_back(NameSource(*found_text_alternative, kValueAttr));
name_sources->back().type = name_from;
}
name_from = ax::mojom::blink::NameFrom::kAttribute;
String value = input_element->Value();
if (!value.IsNull()) {
text_alternative = value;
if (name_sources) {
NameSource& source = name_sources->back();
source.text = text_alternative;
*found_text_alternative = true;
} else {
return text_alternative;
}
}
String resulting_text = TextAlternativeFromTooltip(
name_from, name_sources, found_text_alternative, &text_alternative,
related_objects);
if (!resulting_text.empty()) {
if (name_sources) {
text_alternative = resulting_text;
} else {
return resulting_text;
}
}
name_from = ax::mojom::blink::NameFrom::kValue;
text_alternative =
input_element->GetLocale().QueryString(IDS_FORM_SUBMIT_LABEL);
if (name_sources) {
name_sources->push_back(NameSource(*found_text_alternative, kTypeAttr));
NameSource& source = name_sources->back();
source.attribute_value = input_element->getAttribute(kTypeAttr);
source.type = name_from;
source.text = text_alternative;
*found_text_alternative = true;
} else {
return text_alternative;
}
return text_alternative;
}
if (input_element &&
input_element->FormControlType() == FormControlType::kInputFile) {
name_from = ax::mojom::blink::NameFrom::kContents;
if (name_sources) {
name_sources->push_back(NameSource(*found_text_alternative, kValueAttr));
name_sources->back().type = name_from;
}
if (ShadowRoot* shadow_root = input_element->UserAgentShadowRoot()) {
text_alternative =
To<HTMLInputElement>(shadow_root->firstElementChild())->Value();
if (name_sources) {
NameSource& source = name_sources->back();
source.text = text_alternative;
*found_text_alternative = true;
} else {
return text_alternative;
}
}
}
if (html_element && html_element->IsTextControl()) {
String resulting_text = TextAlternativeFromTooltip(
name_from, name_sources, found_text_alternative, &text_alternative,
related_objects);
if (!resulting_text.empty()) {
if (name_sources) {
text_alternative = resulting_text;
} else {
return resulting_text;
}
}
name_from = ax::mojom::blink::NameFrom::kPlaceholder;
if (name_sources) {
name_sources->push_back(
NameSource(*found_text_alternative, html_names::kPlaceholderAttr));
NameSource& source = name_sources->back();
source.type = name_from;
}
const String placeholder = PlaceholderFromNativeAttribute();
if (!placeholder.empty()) {
text_alternative = placeholder;
if (name_sources) {
NameSource& source = name_sources->back();
source.text = text_alternative;
source.attribute_value =
html_element->FastGetAttribute(html_names::kPlaceholderAttr);
*found_text_alternative = true;
} else {
return text_alternative;
}
}
}
if (IsTextField()) {
name_from = ax::mojom::blink::NameFrom::kPlaceholder;
if (name_sources) {
name_sources->push_back(NameSource(*found_text_alternative,
html_names::kAriaPlaceholderAttr));
NameSource& source = name_sources->back();
source.type = name_from;
}
const AtomicString& aria_placeholder =
AriaAttribute(html_names::kAriaPlaceholderAttr);
if (!aria_placeholder.empty()) {
text_alternative = aria_placeholder;
if (name_sources) {
NameSource& source = name_sources->back();
source.text = text_alternative;
source.attribute_value = aria_placeholder;
*found_text_alternative = true;
} else {
return text_alternative;
}
}
return text_alternative;
}
if (IsA<HTMLImageElement>(GetNode()) || IsA<HTMLAreaElement>(GetNode())) {
const AtomicString& alt = GetElement()->FastGetAttribute(kAltAttr);
const bool is_empty = alt.empty() && !alt.IsNull();
name_from = is_empty ? ax::mojom::blink::NameFrom::kAttributeExplicitlyEmpty
: ax::mojom::blink::NameFrom::kAttribute;
if (name_sources) {
name_sources->push_back(NameSource(*found_text_alternative, kAltAttr));
name_sources->back().type = name_from;
}
if (!alt.empty()) {
text_alternative = alt;
if (name_sources) {
NameSource& source = name_sources->back();
source.attribute_value = alt;
source.text = text_alternative;
*found_text_alternative = true;
} else {
return text_alternative;
}
}
return text_alternative;
}
if (auto* table_element = DynamicTo<HTMLTableElement>(GetNode())) {
name_from = ax::mojom::blink::NameFrom::kCaption;
if (name_sources) {
name_sources->push_back(NameSource(*found_text_alternative));
name_sources->back().type = name_from;
name_sources->back().native_source = kAXTextFromNativeHTMLTableCaption;
}
HTMLTableCaptionElement* caption = table_element->caption();
if (caption) {
AXObject* caption_ax_object = AXObjectCache().Get(caption);
if (caption_ax_object) {
text_alternative =
RecursiveTextAlternative(*caption_ax_object, nullptr, visited);
if (related_objects) {
local_related_objects.push_back(
MakeGarbageCollected<NameSourceRelatedObject>(caption_ax_object,
text_alternative));
*related_objects = local_related_objects;
local_related_objects.clear();
}
if (name_sources) {
NameSource& source = name_sources->back();
source.related_objects = *related_objects;
source.text = text_alternative;
*found_text_alternative = true;
} else {
return text_alternative;
}
}
}
name_from = ax::mojom::blink::NameFrom::kAttribute;
if (name_sources) {
name_sources->push_back(
NameSource(*found_text_alternative, html_names::kSummaryAttr));
name_sources->back().type = name_from;
}
const AtomicString& summary =
GetElement()->FastGetAttribute(html_names::kSummaryAttr);
if (!summary.IsNull()) {
text_alternative = summary;
if (name_sources) {
NameSource& source = name_sources->back();
source.attribute_value = summary;
source.text = text_alternative;
*found_text_alternative = true;
} else {
return text_alternative;
}
}
return text_alternative;
}
if (GetNode()->IsSVGElement()) {
name_from = ax::mojom::blink::NameFrom::kRelatedElement;
if (name_sources) {
name_sources->push_back(NameSource(*found_text_alternative));
name_sources->back().type = name_from;
name_sources->back().native_source = kAXTextFromNativeTitleElement;
}
auto* container_node = To<ContainerNode>(GetNode());
Element* title = ElementTraversal::FirstChild(
*container_node, HasTagName(svg_names::kTitleTag));
if (title) {
text_alternative = title->GetInnerTextWithoutUpdate();
if (!text_alternative.empty()) {
if (name_sources) {
NameSource& source = name_sources->back();
source.text = text_alternative;
source.related_objects = *related_objects;
*found_text_alternative = true;
} else {
return text_alternative;
}
}
}
if (IsA<SVGAElement>(GetNode())) {
name_from = ax::mojom::blink::NameFrom::kAttribute;
if (name_sources) {
name_sources->push_back(
NameSource(*found_text_alternative, xlink_names::kTitleAttr));
name_sources->back().type = name_from;
}
const AtomicString& title_attr =
DynamicTo<Element>(GetNode())->FastGetAttribute(
xlink_names::kTitleAttr);
if (!title_attr.empty()) {
text_alternative = title_attr;
if (name_sources) {
NameSource& source = name_sources->back();
source.text = text_alternative;
source.attribute_value = title_attr;
*found_text_alternative = true;
} else {
return text_alternative;
}
}
}
}
if (auto* html_field_set_element =
DynamicTo<HTMLFieldSetElement>(GetNode())) {
name_from = ax::mojom::blink::NameFrom::kRelatedElement;
if (name_sources) {
name_sources->push_back(NameSource(*found_text_alternative));
name_sources->back().type = name_from;
name_sources->back().native_source = kAXTextFromNativeHTMLLegend;
}
HTMLElement* legend = html_field_set_element->Legend();
if (legend) {
AXObject* legend_ax_object = AXObjectCache().Get(legend);
if (legend_ax_object && !visited.Contains(legend_ax_object)) {
text_alternative =
RecursiveTextAlternative(*legend_ax_object, nullptr, visited);
if (related_objects) {
local_related_objects.push_back(
MakeGarbageCollected<NameSourceRelatedObject>(legend_ax_object,
text_alternative));
*related_objects = local_related_objects;
local_related_objects.clear();
}
if (name_sources) {
NameSource& source = name_sources->back();
source.related_objects = *related_objects;
source.text = text_alternative;
*found_text_alternative = true;
} else {
return text_alternative;
}
}
}
}
if (Document* document = DynamicTo<Document>(GetNode())) {
if (document) {
name_from = ax::mojom::blink::NameFrom::kAttribute;
if (name_sources) {
name_sources->push_back(
NameSource(found_text_alternative, html_names::kAriaLabelAttr));
name_sources->back().type = name_from;
}
if (Element* document_element = document->documentElement()) {
const AtomicString& aria_label =
AriaAttribute(*document_element, html_names::kAriaLabelAttr);
if (!aria_label.empty()) {
text_alternative = aria_label;
if (name_sources) {
NameSource& source = name_sources->back();
source.text = text_alternative;
source.attribute_value = aria_label;
*found_text_alternative = true;
} else {
return text_alternative;
}
}
}
text_alternative = document->title();
bool is_empty_title_element =
text_alternative.empty() && document->TitleElement();
if (is_empty_title_element)
name_from = ax::mojom::blink::NameFrom::kAttributeExplicitlyEmpty;
else
name_from = ax::mojom::blink::NameFrom::kRelatedElement;
if (name_sources) {
name_sources->push_back(NameSource(*found_text_alternative));
NameSource& source = name_sources->back();
source.type = name_from;
source.native_source = kAXTextFromNativeTitleElement;
source.text = text_alternative;
*found_text_alternative = true;
} else {
return text_alternative;
}
}
}
return text_alternative;
}
String AXNodeObject::GetSavedTextAlternativeFromNameSource(
bool found_text_alternative,
ax::mojom::NameFrom& name_from,
AXRelatedObjectVector* related_objects,
NameSources* name_sources) {
name_from = ax::mojom::blink::NameFrom::kNone;
if (!name_sources || !found_text_alternative) {
return String();
}
for (NameSource& name_source : *name_sources) {
if (name_source.text.empty() || name_source.superseded) {
continue;
}
name_from = name_source.type;
if (!name_source.related_objects.empty()) {
*related_objects = name_source.related_objects;
}
return name_source.text;
}
return String();
}
String AXNodeObject::MaybeAppendFileDescriptionToName(
const String& name) const {
const auto* input_element = DynamicTo<HTMLInputElement>(GetNode());
if (!input_element ||
input_element->FormControlType() != FormControlType::kInputFile) {
return name;
}
String displayed_file_path = GetValueForControl();
if (!displayed_file_path.empty()) {
bool is_rtl =
GetTextDirection() == ax::mojom::blink::WritingDirection::kRtl;
return StrCat({name, is_rtl ? " :" : ": ", displayed_file_path});
}
return name;
}
bool AXNodeObject::ShouldIncludeContentInTextAlternative(
bool recursive,
const AXObject* aria_label_or_description_root,
AXObjectSet& visited) const {
if (!aria_label_or_description_root &&
!SupportsNameFromContents(recursive, true)) {
return false;
}
if (IsA<HTMLSelectElement>(GetNode())) {
return false;
}
if (IsTextField() && aria_label_or_description_root != this) {
return false;
}
return true;
}
String AXNodeObject::Description(
ax::mojom::blink::NameFrom name_from,
ax::mojom::blink::DescriptionFrom& description_from,
AXObjectVector* description_objects) const {
AXRelatedObjectVector related_objects;
String result =
Description(name_from, description_from, nullptr, &related_objects);
if (description_objects) {
description_objects->clear();
for (NameSourceRelatedObject* related_object : related_objects)
description_objects->push_back(related_object->object);
}
result = result.SimplifyWhiteSpace(IsHTMLSpace<UChar>);
if (RoleValue() == ax::mojom::blink::Role::kSpinButton &&
DatetimeAncestor()) {
const AXObject* datetime_ancestor = DatetimeAncestor();
ax::mojom::blink::NameFrom datetime_ancestor_name_from;
datetime_ancestor->GetName(datetime_ancestor_name_from, nullptr, nullptr);
if (description_objects)
description_objects->clear();
String ancestor_description = DatetimeAncestor()->Description(
datetime_ancestor_name_from, description_from, description_objects);
if (!result.empty() && !ancestor_description.empty())
return StrCat({result, " ", ancestor_description});
if (!ancestor_description.empty())
return ancestor_description;
}
return result;
}
String AXNodeObject::Description(
ax::mojom::blink::NameFrom name_from,
ax::mojom::blink::DescriptionFrom& description_from,
DescriptionSources* description_sources,
AXRelatedObjectVector* related_objects) const {
if (description_sources)
DCHECK(related_objects);
if (!GetNode())
return String();
String description;
bool found_description = false;
description_from = ax::mojom::blink::DescriptionFrom::kRelatedElement;
if (description_sources) {
description_sources->push_back(
DescriptionSource(found_description, html_names::kAriaDescribedbyAttr));
description_sources->back().type = description_from;
}
Element* element = GetElement();
if (!element)
return String();
const GCedHeapVector<Member<Element>>* elements_from_attribute =
ElementsFromAttributeOrInternals(element,
html_names::kAriaDescribedbyAttr);
if (elements_from_attribute) {
if (description_sources) {
description_sources->back().attribute_value =
AriaAttribute(html_names::kAriaDescribedbyAttr);
}
AXObjectSet visited;
description = TextFromElements(true, visited, *elements_from_attribute,
related_objects);
if (!description.IsNull()) {
if (description_sources) {
DescriptionSource& source = description_sources->back();
source.type = description_from;
source.related_objects = *related_objects;
source.text = description;
found_description = true;
} else {
return description;
}
} else if (description_sources) {
description_sources->back().invalid = true;
}
}
const AtomicString& aria_desc =
AriaAttribute(html_names::kAriaDescriptionAttr);
if (aria_desc) {
description_from = ax::mojom::blink::DescriptionFrom::kAriaDescription;
description = aria_desc;
if (description_sources) {
found_description = true;
description_sources->back().text = description;
} else {
return description;
}
}
if (IsA<SVGElement>(GetNode())) {
String svg_description = SVGDescription(
name_from, description_from, description_sources, related_objects);
if (!svg_description.empty()) {
return svg_description;
}
}
const auto* input_element = DynamicTo<HTMLInputElement>(GetNode());
if (name_from != ax::mojom::blink::NameFrom::kValue && input_element &&
input_element->IsTextButton()) {
description_from = ax::mojom::blink::DescriptionFrom::kButtonLabel;
if (description_sources) {
description_sources->push_back(
DescriptionSource(found_description, kValueAttr));
description_sources->back().type = description_from;
}
String value = input_element->Value();
if (!value.IsNull()) {
description = value;
if (description_sources) {
DescriptionSource& source = description_sources->back();
source.text = description;
found_description = true;
} else {
return description;
}
}
}
if (RoleValue() == ax::mojom::blink::Role::kRuby) {
description_from = ax::mojom::blink::DescriptionFrom::kRubyAnnotation;
if (description_sources) {
description_sources->push_back(DescriptionSource(found_description));
description_sources->back().type = description_from;
description_sources->back().native_source =
kAXTextFromNativeHTMLRubyAnnotation;
}
AXObject* ruby_annotation_ax_object = nullptr;
for (const auto& child : children_) {
if (child->RoleValue() == ax::mojom::blink::Role::kRubyAnnotation &&
child->GetNode() &&
child->GetNode()->HasTagName(html_names::kRtTag)) {
ruby_annotation_ax_object = child;
break;
}
}
if (ruby_annotation_ax_object) {
AXObjectSet visited;
description =
RecursiveTextAlternative(*ruby_annotation_ax_object, this, visited);
if (related_objects) {
related_objects->push_back(
MakeGarbageCollected<NameSourceRelatedObject>(
ruby_annotation_ax_object, description));
}
if (description_sources) {
DescriptionSource& source = description_sources->back();
source.related_objects = *related_objects;
source.text = description;
found_description = true;
} else {
return description;
}
}
}
auto* table_element = DynamicTo<HTMLTableElement>(element);
if (name_from != ax::mojom::blink::NameFrom::kCaption && table_element) {
description_from = ax::mojom::blink::DescriptionFrom::kTableCaption;
if (description_sources) {
description_sources->push_back(DescriptionSource(found_description));
description_sources->back().type = description_from;
description_sources->back().native_source =
kAXTextFromNativeHTMLTableCaption;
}
HTMLTableCaptionElement* caption = table_element->caption();
if (caption) {
AXObject* caption_ax_object = AXObjectCache().Get(caption);
if (caption_ax_object) {
AXObjectSet visited;
description =
RecursiveTextAlternative(*caption_ax_object, nullptr, visited);
if (related_objects) {
related_objects->push_back(
MakeGarbageCollected<NameSourceRelatedObject>(caption_ax_object,
description));
}
if (description_sources) {
DescriptionSource& source = description_sources->back();
source.related_objects = *related_objects;
source.text = description;
found_description = true;
} else {
return description;
}
}
}
}
if (name_from != ax::mojom::blink::NameFrom::kContents &&
IsA<HTMLSummaryElement>(GetNode())) {
description_from = ax::mojom::blink::DescriptionFrom::kSummary;
if (description_sources) {
description_sources->push_back(DescriptionSource(found_description));
description_sources->back().type = description_from;
}
AXObjectSet visited;
description = TextFromDescendants(visited, nullptr, false);
if (!description.empty()) {
if (description_sources) {
found_description = true;
description_sources->back().text = description;
} else {
return description;
}
}
}
if (name_from != ax::mojom::blink::NameFrom::kTitle) {
description_from = ax::mojom::blink::DescriptionFrom::kTitle;
if (description_sources) {
description_sources->push_back(
DescriptionSource(found_description, kTitleAttr));
description_sources->back().type = description_from;
}
const AtomicString& title = element->FastGetAttribute(kTitleAttr);
if (!title.empty() &&
String(title).StripWhiteSpace() !=
element->GetInnerTextWithoutUpdate().StripWhiteSpace()) {
description = title;
if (description_sources) {
found_description = true;
description_sources->back().text = description;
} else {
return description;
}
}
}
if (RuntimeEnabledFeatures::HTMLInterestForAttributeEnabled() &&
name_from != ax::mojom::blink::NameFrom::kInterestFor) {
if (Element* target = element->InterestForElement()) {
DCHECK(RuntimeEnabledFeatures::HTMLInterestForAttributeEnabled());
description_from = ax::mojom::blink::DescriptionFrom::kInterestFor;
if (description_sources) {
description_sources->push_back(
DescriptionSource(found_description, html_names::kInterestforAttr));
description_sources->back().type = description_from;
}
AXObject* interest_ax_object = AXObjectCache().Get(target);
if (interest_ax_object && interest_ax_object->IsPlainContent()) {
AXObjectSet visited;
description = RecursiveTextAlternative(*interest_ax_object,
interest_ax_object, visited);
if (related_objects) {
related_objects->push_back(
MakeGarbageCollected<NameSourceRelatedObject>(interest_ax_object,
description));
}
if (description_sources) {
DescriptionSource& source = description_sources->back();
source.related_objects = *related_objects;
source.text = description;
found_description = true;
} else {
return description;
}
}
}
}
if (name_from != ax::mojom::blink::NameFrom::kPopoverTarget) {
if (auto* form_control = DynamicTo<HTMLFormControlElement>(element)) {
auto popover_target = form_control->popoverTargetElement();
if (popover_target.popover &&
popover_target.popover->PopoverType() == PopoverValueType::kHint) {
description_from = ax::mojom::blink::DescriptionFrom::kPopoverTarget;
if (description_sources) {
description_sources->push_back(DescriptionSource(
found_description, html_names::kPopovertargetAttr));
description_sources->back().type = description_from;
}
AXObject* popover_ax_object =
AXObjectCache().Get(popover_target.popover);
if (popover_ax_object && popover_ax_object->IsPlainContent()) {
AXObjectSet visited;
description = RecursiveTextAlternative(*popover_ax_object,
popover_ax_object, visited);
if (related_objects) {
related_objects->push_back(
MakeGarbageCollected<NameSourceRelatedObject>(popover_ax_object,
description));
}
if (description_sources) {
DescriptionSource& source = description_sources->back();
source.related_objects = *related_objects;
source.text = description;
found_description = true;
} else {
return description;
}
}
}
}
}
if (name_from == ax::mojom::blink::NameFrom::kProhibited) {
description_from = ax::mojom::blink::DescriptionFrom::kProhibitedNameRepair;
ax::mojom::blink::NameFrom orig_name_from_without_prohibited;
HeapHashSet<Member<const AXObject>> visited;
description = TextAlternative(false, nullptr, visited,
orig_name_from_without_prohibited,
related_objects, nullptr);
DCHECK(!description.empty());
if (description_sources) {
description_sources->push_back(DescriptionSource(found_description));
DescriptionSource& source = description_sources->back();
source.type = description_from;
source.related_objects = *related_objects;
source.text = description;
found_description = true;
} else {
return description;
}
}
description_from = ax::mojom::blink::DescriptionFrom::kNone;
if (found_description) {
DCHECK(description_sources)
<< "Should only reach here if description_sources are tracked";
for (DescriptionSource& description_source : *description_sources) {
if (!description_source.text.IsNull() && !description_source.superseded) {
description_from = description_source.type;
if (!description_source.related_objects.empty())
*related_objects = description_source.related_objects;
return description_source.text;
}
}
}
return String();
}
String AXNodeObject::SVGDescription(
ax::mojom::blink::NameFrom name_from,
ax::mojom::blink::DescriptionFrom& description_from,
DescriptionSources* description_sources,
AXRelatedObjectVector* related_objects) const {
DCHECK(IsA<SVGElement>(GetNode()));
String description;
bool found_description = false;
Element* element = GetElement();
description_from = ax::mojom::blink::DescriptionFrom::kSvgDescElement;
if (description_sources) {
description_sources->push_back(DescriptionSource(found_description));
description_sources->back().type = description_from;
description_sources->back().native_source = kAXTextFromNativeSVGDescElement;
}
if (Element* desc = ElementTraversal::FirstChild(
*element, HasTagName(svg_names::kDescTag))) {
description = desc->GetInnerTextWithoutUpdate();
if (!description.empty()) {
if (description_sources) {
DescriptionSource& source = description_sources->back();
source.related_objects = *related_objects;
source.text = description;
found_description = true;
} else {
return description;
}
}
}
if (IsNameFromAriaAttribute(element)) {
description_from = ax::mojom::blink::DescriptionFrom::kTitle;
if (description_sources) {
description_sources->push_back(DescriptionSource(found_description));
description_sources->back().type = description_from;
description_sources->back().native_source = kAXTextFromNativeTitleElement;
}
if (Element* title = ElementTraversal::FirstChild(
*element, HasTagName(svg_names::kTitleTag))) {
description = title->GetInnerTextWithoutUpdate();
if (!description.empty()) {
if (description_sources) {
DescriptionSource& source = description_sources->back();
source.related_objects = *related_objects;
source.text = description;
found_description = true;
} else {
return description;
}
}
}
}
if (IsA<SVGAElement>(GetNode()) &&
name_from != ax::mojom::blink::NameFrom::kAttribute) {
description_from = ax::mojom::blink::DescriptionFrom::kTitle;
const AtomicString& title_attr =
DynamicTo<Element>(GetNode())->FastGetAttribute(
xlink_names::kTitleAttr);
if (!title_attr.empty()) {
description = title_attr;
if (description_sources) {
found_description = true;
description_sources->back().text = description;
} else {
return description;
}
}
}
return String();
}
String AXNodeObject::Placeholder(ax::mojom::blink::NameFrom name_from) const {
if (name_from == ax::mojom::blink::NameFrom::kPlaceholder)
return String();
Node* node = GetNode();
if (!node || !node->IsHTMLElement())
return String();
String native_placeholder = PlaceholderFromNativeAttribute();
if (!native_placeholder.empty())
return native_placeholder;
const AtomicString& aria_placeholder =
AriaAttribute(html_names::kAriaPlaceholderAttr);
if (!aria_placeholder.empty())
return aria_placeholder;
return String();
}
String AXNodeObject::Title(ax::mojom::blink::NameFrom name_from) const {
if (name_from == ax::mojom::blink::NameFrom::kTitle)
return String();
return GetTitle(GetElement());
}
String AXNodeObject::PlaceholderFromNativeAttribute() const {
Node* node = GetNode();
if (!node || !blink::IsTextControl(*node))
return String();
return ToTextControl(node)->StrippedPlaceholder();
}
String AXNodeObject::GetValueContributionToName(AXObjectSet& visited) const {
if (IsTextField())
return SlowGetValueForControlIncludingContentEditable();
if (IsRangeValueSupported()) {
const AtomicString& aria_valuetext =
AriaAttribute(html_names::kAriaValuetextAttr);
if (aria_valuetext) {
return aria_valuetext.GetString();
}
float value;
if (ValueForRange(&value))
return String::Number(value);
}
if (UseNameFromSelectedOption()) {
AXObjectVector selected_options;
SelectedOptions(selected_options);
if (selected_options.size() == 0) {
return SlowGetValueForControlIncludingContentEditable(visited);
} else {
StringBuilder accumulated_text;
for (const auto& child : selected_options) {
if (visited.insert(child).is_new_entry) {
if (accumulated_text.length()) {
accumulated_text.Append(" ");
}
accumulated_text.Append(child->ComputedName());
}
}
return accumulated_text.ToString();
}
}
return String();
}
bool AXNodeObject::UseNameFromSelectedOption() const {
switch (RoleValue()) {
case ax::mojom::blink::Role::kComboBoxGrouping:
case ax::mojom::blink::Role::kComboBoxMenuButton:
case ax::mojom::blink::Role::kComboBoxSelect:
case ax::mojom::blink::Role::kListBox:
return true;
default:
return false;
}
}
ScrollableArea* AXNodeObject::GetScrollableAreaIfScrollable() const {
if (IsA<Document>(GetNode())) {
return DocumentFrameView()->LayoutViewport();
}
if (auto* box = DynamicTo<LayoutBox>(GetLayoutObject())) {
PaintLayerScrollableArea* scrollable_area = box->GetScrollableArea();
if (scrollable_area && scrollable_area->HasOverflow()) {
return scrollable_area;
}
}
return nullptr;
}
AXObject* AXNodeObject::AccessibilityHitTest(const gfx::Point& point) const {
if (!IsA<Document>(GetNode())) {
return nullptr;
}
CHECK(GetLayoutObject());
DCHECK_GE(GetDocument()->Lifecycle().GetState(),
DocumentLifecycle::kPrePaintClean);
DCHECK(GetLayoutObject()->IsLayoutView());
PaintLayer* layer = To<LayoutBox>(GetLayoutObject())->Layer();
DCHECK(layer);
HitTestRequest request(HitTestRequest::kReadOnly | HitTestRequest::kActive);
HitTestLocation location(point);
HitTestResult hit_test_result = HitTestResult(request, location);
layer->HitTest(location, hit_test_result, PhysicalRect(InfiniteIntRect()));
Node* node = hit_test_result.InnerNode();
if (!node) {
return nullptr;
}
if (auto* area = DynamicTo<HTMLAreaElement>(node)) {
return AccessibilityImageMapHitTest(area, point);
}
if (auto* option = DynamicTo<HTMLOptionElement>(node)) {
node = option->OwnerSelectElement();
if (!node) {
return nullptr;
}
}
const TreeScope& tree_scope = node->GetTreeScope();
if (auto* shadow_root = DynamicTo<ShadowRoot>(tree_scope.RootNode())) {
if (shadow_root->IsUserAgent()) {
node = &shadow_root->host();
}
}
LayoutObject* obj = node->GetLayoutObject();
AXObject* result = AXObjectCache().Get(obj);
if (!result) {
return nullptr;
}
result = result->ElementAccessibilityHitTest(point);
while (result && result->IsIgnored()) {
CHECK(!result->IsDetached());
if (auto* label = DynamicTo<HTMLLabelElement>(result->GetNode())) {
if (HTMLElement* control = label->Control()) {
if (AXObject* ax_control = AXObjectCache().Get(control)) {
result = ax_control;
break;
}
}
}
result = result->ParentObject();
}
return result->IsIncludedInTree() ? result
: result->ParentObjectIncludedInTree();
}
AXObject* AXNodeObject::AccessibilityImageMapHitTest(
HTMLAreaElement* area,
const gfx::Point& point) const {
if (!area) {
return nullptr;
}
AXObject* parent = AXObjectCache().Get(area->ImageElement());
if (!parent) {
return nullptr;
}
PhysicalOffset physical_point(point);
for (const auto& child : parent->ChildrenIncludingIgnored()) {
if (child->GetBoundsInFrameCoordinates().Contains(physical_point)) {
return child.Get();
}
}
return nullptr;
}
AXObject* AXNodeObject::GetFirstInlineBlockOrDeepestInlineAXChildInLayoutTree(
AXObject* start_object,
bool first) const {
if (!start_object) {
return nullptr;
}
AXObject* result = start_object;
Node* current_node = start_object->GetNode();
while (current_node) {
AXObject* ax_object = start_object->AXObjectCache().Get(current_node);
if (ax_object && ax_object->IsIncludedInTree() &&
!current_node->IsMarkerPseudoElement()) {
if (ax_object->GetLayoutObject() &&
ax_object->GetLayoutObject()->IsInline() &&
ax_object->GetLayoutObject()->IsAtomicInlineLevel()) {
return ax_object;
}
}
current_node = first ? LayoutTreeBuilderTraversal::FirstChild(*current_node)
: LayoutTreeBuilderTraversal::LastChild(*current_node);
if (!current_node) {
break;
}
AXObject* tentative_child = start_object->AXObjectCache().Get(current_node);
if (tentative_child && tentative_child->IsIncludedInTree()) {
result = tentative_child;
}
}
if (!result->IsIncludedInTree()) {
return nullptr;
}
if (!result->ChildCountIncludingIgnored()) {
return result;
}
return first ? result->DeepestFirstChildIncludingIgnored()
: result->DeepestLastChildIncludingIgnored();
}
void AXNodeObject::MaybeResetCache() const {
uint64_t generation = AXObjectCache().GenerationalCacheId();
if (!generational_cache_) {
generational_cache_ = MakeGarbageCollected<GenerationalCache>();
}
DCHECK(AXObjectCache().IsFrozen());
if (generation != generational_cache_->generation) {
generational_cache_->generation = generation;
generational_cache_->next_on_line = nullptr;
generational_cache_->previous_on_line = nullptr;
} else {
#if DCHECK_IS_ON()
if (AXObject* next = generational_cache_->next_on_line) {
CHECK(!next->IsDetached());
}
if (AXObject* previous = generational_cache_->previous_on_line) {
CHECK(!previous->IsDetached());
}
#endif
}
}
void AXNodeObject::GenerationalCache::Trace(Visitor* visitor) const {
visitor->Trace(next_on_line);
visitor->Trace(previous_on_line);
}
AXObject* AXNodeObject::SetNextOnLine(AXObject* next_on_line) const {
CHECK(generational_cache_) << "Must call MaybeResetCache ahead of this call";
generational_cache_->next_on_line = next_on_line;
return next_on_line;
}
AXObject* AXNodeObject::SetPreviousOnLine(AXObject* previous_on_line) const {
CHECK(generational_cache_) << "Must call MaybeResetCache ahead of this call";
generational_cache_->previous_on_line = previous_on_line;
return previous_on_line;
}
AXObject* AXNodeObject::NextOnLine() const {
DCHECK(!IsDetached());
MaybeResetCache();
if (generational_cache_->next_on_line) {
return generational_cache_->next_on_line;
}
const LayoutObject* layout_object = GetLayoutObject();
if (!layout_object) {
return SetNextOnLine(nullptr);
}
if (!AXObjectCache().IsFrozen() ||
!AXObjectCache().HasCachedDataForNodesOnLine()) {
AXObjectCache().ComputeNodesOnLine(layout_object);
}
if (DisplayLockUtilities::LockedAncestorPreventingPaint(*layout_object)) {
return SetNextOnLine(nullptr);
}
if (const auto* list_marker =
GetListMarker(*layout_object, ParentObjectIfPresent())) {
auto* ax_list_marker = AXObjectCache().Get(list_marker);
if (ax_list_marker) {
AXObject* next_sibling = ax_list_marker->UnignoredNextSiblingSlow();
if (next_sibling) {
return SetNextOnLine(
GetFirstInlineBlockOrDeepestInlineAXChildInLayoutTree(next_sibling,
true));
}
}
return SetNextOnLine(nullptr);
}
if (!ShouldUseLayoutNG(*layout_object)) {
return SetNextOnLine(nullptr);
}
if (!layout_object->IsInLayoutNGInlineFormattingContext()) {
return SetNextOnLine(nullptr);
}
const LayoutObject* next_layout_object = nullptr;
while (layout_object) {
next_layout_object = AXObjectCache().CachedNextOnLine(layout_object);
if (next_layout_object) {
break;
}
const auto* child = layout_object->SlowLastChild();
if (!child) {
break;
}
layout_object = child;
}
while (next_layout_object) {
AXObject* result = AXObjectCache().Get(next_layout_object);
bool should_keep_looking =
result ? result->IsInert() || result->IsAriaHidden() : false;
result =
GetFirstInlineBlockOrDeepestInlineAXChildInLayoutTree(result, true);
if (result && !should_keep_looking) {
return SetNextOnLine(result);
}
if (!should_keep_looking) {
break;
}
next_layout_object = AXObjectCache().CachedNextOnLine(next_layout_object);
}
return SetNextOnLine(nullptr);
}
AXObject* AXNodeObject::PreviousOnLine() const {
DCHECK(!IsDetached());
MaybeResetCache();
if (generational_cache_->previous_on_line) {
return generational_cache_->previous_on_line;
}
const LayoutObject* layout_object = GetLayoutObject();
if (!layout_object) {
return SetPreviousOnLine(nullptr);
}
if (!AXObjectCache().IsFrozen() ||
!AXObjectCache().HasCachedDataForNodesOnLine()) {
AXObjectCache().ComputeNodesOnLine(layout_object);
}
if (!ShouldUseLayoutNG(*layout_object)) {
return SetPreviousOnLine(nullptr);
}
if (DisplayLockUtilities::LockedAncestorPreventingPaint(*layout_object)) {
return SetPreviousOnLine(nullptr);
}
AXObject* previous_sibling =
IsIncludedInTree() ? UnignoredPreviousSiblingSlow() : nullptr;
if (previous_sibling && previous_sibling->GetLayoutObject()) {
const auto* list_marker =
GetListMarker(*previous_sibling->GetLayoutObject(),
previous_sibling->ParentObjectIfPresent());
auto* ax_list_marker =
list_marker ? AXObjectCache().Get(list_marker) : nullptr;
if (ax_list_marker && ax_list_marker->GetLayoutObject() &&
ax_list_marker->GetLayoutObject()->IsLayoutOutsideListMarker()) {
return SetPreviousOnLine(
GetFirstInlineBlockOrDeepestInlineAXChildInLayoutTree(ax_list_marker,
false));
}
}
if (layout_object->IsLayoutOutsideListMarker() ||
!layout_object->IsInLayoutNGInlineFormattingContext()) {
return SetPreviousOnLine(nullptr);
}
const LayoutObject* previous_layout_object = nullptr;
while (layout_object) {
previous_layout_object =
AXObjectCache().CachedPreviousOnLine(layout_object);
if (previous_layout_object) {
break;
}
const auto* child = layout_object->SlowFirstChild();
if (!child) {
break;
}
layout_object = child;
}
while (previous_layout_object) {
AXObject* result = AXObjectCache().Get(previous_layout_object);
bool should_keep_looking =
result ? result->IsInert() || result->IsAriaHidden() : false;
result =
GetFirstInlineBlockOrDeepestInlineAXChildInLayoutTree(result, false);
if (result && !should_keep_looking) {
return SetPreviousOnLine(result);
}
if (!should_keep_looking) {
break;
}
previous_layout_object =
AXObjectCache().CachedPreviousOnLine(previous_layout_object);
}
return SetPreviousOnLine(nullptr);
}
void AXNodeObject::HandleAutofillSuggestionAvailabilityChanged(
WebAXAutofillSuggestionAvailability suggestion_availability) {
if (GetLayoutObject()) {
AXObjectCache().SetAutofillSuggestionAvailability(AXObjectID(),
suggestion_availability);
}
}
void AXNodeObject::GetWordBoundaries(Vector<int>& word_starts,
Vector<int>& word_ends) const {
if (!GetLayoutObject() || !GetLayoutObject()->IsListMarker()) {
return;
}
String text_alternative;
if (ListMarker* marker = ListMarker::Get(GetLayoutObject())) {
text_alternative = marker->TextAlternative(*GetLayoutObject());
}
if (text_alternative.ContainsOnlyWhitespaceOrEmpty()) {
return;
}
Vector<AbstractInlineTextBox::WordBoundaries> boundaries;
AbstractInlineTextBox::GetWordBoundariesForText(boundaries, text_alternative);
word_starts.reserve(boundaries.size());
word_ends.reserve(boundaries.size());
for (const auto& boundary : boundaries) {
word_starts.push_back(boundary.start_index);
word_ends.push_back(boundary.end_index);
}
}
void AXNodeObject::Trace(Visitor* visitor) const {
visitor->Trace(node_);
visitor->Trace(layout_object_);
visitor->Trace(generational_cache_);
AXObject::Trace(visitor);
}
}