blob: a161a7cfb69bf95295152317859d9bb3da1e1c1e [file] [log] [blame]
/*
* 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 <memory>
#include <optional>
#include <queue>
#include "base/auto_reset.h"
#include "base/containers/contains.h"
#include "base/containers/fixed_flat_set.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/aom/accessible_node.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/layout_tree_builder_traversal.h"
#include "third_party/blink/renderer/core/dom/node_computed_style.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/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_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_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_select_element.h"
#include "third_party/blink/renderer/core/html/forms/html_select_list_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_details_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_frame_element_base.h"
#include "third_party/blink/renderer/core/html/html_image_element.h"
#include "third_party/blink/renderer/core/html/html_map_element.h"
#include "third_party/blink/renderer/core/html/html_meter_element.h"
#include "third_party/blink/renderer/core/html/html_plugin_element.h"
#include "third_party/blink/renderer/core/html/html_slot_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/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/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/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_constants.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_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/xlink_names.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_layout_object.h"
#include "third_party/blink/renderer/modules/accessibility/ax_menu_list_option.h"
#include "third_party/blink/renderer/modules/accessibility/ax_menu_list_popup.h"
#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/text/string_builder.h"
#include "third_party/skia/include/core/SkImage.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/transform.h"
namespace {
// It is not easily possible to find out if an element is the target of an
// in-page link.
// As a workaround, we consider the following to be potential targets:
// - <a name>
// - <foo id> -- an element with an id that is not SVG, a <label> or <optgroup>.
// <label> does not make much sense as an in-page link target.
// Exposing <optgroup> is redundant, as the group is already exposed via a
// child in its shadow DOM, which contains the accessible name.
// #document -- this is always a potential link target via <a name="#">.
// This is a compromise that does not include too many elements, and
// has minimal impact on tests.
bool IsPotentialInPageLinkTarget(blink::Node& node) {
auto* element = blink::DynamicTo<blink::Element>(&node);
if (!element) {
// The document itself is a potential link target, e.g. via <a name="#">.
return blink::IsA<blink::Document>(node);
}
// We exclude elements that are in the shadow DOM. They cannot be linked by a
// document fragment from the main page:as they have their own id namespace.
if (element->ContainingShadowRoot())
return false;
// SVG elements are unlikely link targets, and we want to avoid creating
// a lot of noise in the AX tree or breaking tests unnecessarily.
if (element->IsSVGElement())
return false;
// <a name>
if (auto* anchor = blink::DynamicTo<blink::HTMLAnchorElement>(element)) {
if (anchor->HasName())
return true;
}
// <foo id> not in an <optgroup> or <label>.
if (element->HasID() && !blink::IsA<blink::HTMLLabelElement>(element) &&
!blink::IsA<blink::HTMLOptGroupElement>(element)) {
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;
}
// Within a table, provide the accessible, semantic parent of |node|,
// by traversing the DOM tree, ignoring elements that are neutral in a table.
// Return the AXObject for the ancestor.
blink::AXObject* GetDOMTableAXAncestor(blink::Node* node,
blink::AXObjectCacheImpl& cache) {
// Used by code to determine roles of elements inside of an HTML table,
// Use DOM to get parent since parent_ is not initialized yet when role is
// being computed, and because HTML table structure should not take into
// account aria-owns.
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;
}
}
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 { // horizontal and left to right
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 { // horizontal and left to right
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();
return ax::mojom::blink::TextDecorationStyle::kNone;
}
String GetTitle(blink::Element* element) {
if (!element)
return String();
if (blink::SVGElement* svg_element =
blink::DynamicTo<blink::SVGElement>(element)) {
// Don't use title() in SVG, as it calls innerText() which updates layout.
// Unfortunately, this must duplicate some logic from SVGElement::title().
if (svg_element->InUseShadowTree()) {
String title = GetTitle(svg_element->OwnerShadowHost());
if (!title.empty())
return title;
}
// If we aren't an instance in a <use> or the <use> title was not found,
// then find the first <title> child of this element. If a title child was
// found, return the text contents.
if (auto* title_element =
blink::Traversal<blink::SVGTitleElement>::FirstChild(*element)) {
return title_element->GetInnerTextWithoutUpdate();
}
return String();
}
return element->title();
}
bool CanHaveInlineTextBoxChildren(const blink::AXObject* obj) {
if (!ui::CanHaveInlineTextBoxChildren(obj->RoleValue())) {
return false;
}
// Requires a layout object for there to be any inline text boxes.
if (!obj->GetLayoutObject()) {
return false;
}
// Inline text boxes are included if and only if the parent is unignored.
// If the parent is ignored but included in tree, the inline textbox is
// still withheld.
return !obj->LastKnownIsIgnoredValue();
}
bool HasLayoutText(const blink::AXObject* obj) {
// This method should only be used when layout is clean.
#if DCHECK_IS_ON()
DCHECK(obj->GetDocument()->Lifecycle().GetState() >=
blink::DocumentLifecycle::kLayoutClean)
<< "Unclean document at lifecycle "
<< obj->GetDocument()->Lifecycle().ToString();
#endif
// If no layout object, could be display:none or display locked.
if (!obj->GetLayoutObject()) {
return false;
}
if (blink::DisplayLockUtilities::LockedAncestorPreventingPaint(
*obj->GetLayoutObject())) {
return false;
}
// Only text has inline textbox children.
if (!obj->GetLayoutObject()->IsText()) {
return false;
}
// TODO(accessibility): Unclear why text would need layout if it's not display
// locked and the document is currently in a clean layout state.
// It seems to be fairly rare, but is creating some crashes, and there is
// no repro case yet.
if (obj->GetLayoutObject()->NeedsLayout()) {
DCHECK(false) << "LayoutText needed layout but was not display locked: "
<< obj->ToString(true, true);
return false;
}
return true;
}
} // namespace
namespace blink {
using html_names::kAltAttr;
using html_names::kTitleAttr;
using html_names::kTypeAttr;
using html_names::kValueAttr;
using mojom::blink::FormControlType;
// In ARIA 1.1, default value of aria-level was changed to 2.
const int kDefaultHeadingLevel = 2;
AXNodeObject::AXNodeObject(Node* node, AXObjectCacheImpl& ax_object_cache)
: AXObject(ax_object_cache),
native_role_(ax::mojom::blink::Role::kUnknown),
aria_role_(ax::mojom::blink::Role::kUnknown),
node_(node) {}
AXNodeObject::~AXNodeObject() {
DCHECK(!node_);
}
void AXNodeObject::AlterSliderOrSpinButtonValue(bool increase) {
if (!GetNode())
return;
if (!IsSlider() && !IsSpinButton())
return;
float value;
if (!ValueForRange(&value))
return;
if (!RuntimeEnabledFeatures::
SynthesizedKeyboardEventsForAccessibilityActionsEnabled()) {
// If synthesized keyboard events are disabled, we need to set the value
// directly here.
// If no step was provided on the element, use a default value.
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));
// Dispatching an event could result in changes to the document, like
// this AXObject becoming detached.
if (IsDetached())
return;
AXObjectCache().HandleValueChanged(GetNode());
return;
}
}
// If we have synthesized keyboard events enabled, we generate a keydown
// event:
// * For a native slider, the dispatch of the event will reach
// RangeInputType::HandleKeydownEvent(), where the value will be set and the
// AXObjectCache notified. The corresponding keydown/up JS events will be
// fired so the website doesn't know it was produced by an AT action.
// * For an ARIA slider, the corresponding keydown/up JS events will be
// fired. It is expected that the handlers for those events manage the
// update of the slider value.
AXAction action =
increase ? AXAction::kActionIncrement : AXAction::kActionDecrement;
LocalDOMWindow* local_dom_window = GetDocument()->domWindow();
AccessibilityOrientation orientation = Orientation();
ax::mojom::blink::WritingDirection text_direction = GetTextDirection();
// A kKeyDown event is kRawKeyDown + kChar events. We cannot synthesize it
// because the KeyboardEvent constructor will prevent it, to force us to
// decide if we must produce both events. In our case, we don't have to
// produce a kChar event because we are synthesizing arrow key presses, and
// only keys that output characters are expected to produce kChar events.
KeyboardEvent* keydown =
CreateKeyboardEvent(local_dom_window, WebInputEvent::Type::kRawKeyDown,
action, orientation, text_direction);
GetNode()->DispatchEvent(*keydown);
// The keydown handler may have caused the node to be removed.
if (!GetNode())
return;
KeyboardEvent* keyup =
CreateKeyboardEvent(local_dom_window, WebInputEvent::Type::kKeyUp, action,
orientation, text_direction);
// Add a 100ms delay between keydown and keyup to make events look less
// evidently synthesized.
GetDocument()
->GetTaskRunner(TaskType::kUserInteraction)
->PostDelayedTask(
FROM_HERE,
WTF::BindOnce(
[](Node* node, KeyboardEvent* evt) {
if (node) {
node->DispatchEvent(*evt);
}
},
WrapWeakPersistent(GetNode()), WrapPersistent(keyup)),
base::Milliseconds(100));
}
AXObject* AXNodeObject::ActiveDescendant() {
Element* element = GetElement();
if (!element)
return nullptr;
Element* descendant =
GetAOMPropertyOrARIAAttribute(AOMRelationProperty::kActiveDescendant);
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;
}
AXObjectInclusion AXNodeObject::ShouldIncludeBasedOnSemantics(
IgnoredReasons* ignored_reasons) const {
DCHECK(GetDocument());
// All nodes must have an unignored parent within their tree under
// the root node of the web area, so force that node to always be unignored.
if (IsA<Document>(GetNode())) {
return kIncludeObject;
}
if (IsPresentational()) {
if (ignored_reasons)
ignored_reasons->push_back(IgnoredReason(kAXPresentational));
return kIgnoreObject;
}
Node* node = GetNode();
if (!node) {
// Nodeless pseudo element images are included, even if they don't have CSS
// alt text. This can allow auto alt to be applied to them.
if (IsImage())
return kIncludeObject;
return kDefaultBehavior;
}
// Avoid double speech. The ruby text describes pronunciation of the ruby
// base, and generally produces redundant screen reader output. Expose it only
// as a description on the <ruby> element so that screen reader users can
// toggle it on/off as with other descriptions/annotations.
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)) {
// The symbol element is used to define graphical templates which can be
// instantiated by a use element but which are not rendered directly. We
// don't want to include these template objects, or their subtrees, where
// they appear in the DOM. Any associated semantic information (e.g. the
// title child of a symbol) may participate in the text alternative
// computation where it is instantiated by the use element.
// https://svgwg.org/svg2-draft/struct.html#SymbolElement
if (Traversal<SVGSymbolElement>::FirstAncestorOrSelf(*node))
return kIgnoreObject;
// The SVG-AAM states that user agents MUST provide an accessible object
// for rendered SVG elements that have at least one direct child title or
// desc element that is not empty after trimming whitespace. But it also
// says, "User agents MAY include elements with these child elements without
// checking for valid text content." So just check for their existence in
// order to be performant. https://w3c.github.io/svg-aam/#include_elements
if (ElementTraversal::FirstChild(
*To<ContainerNode>(node), [](auto& element) {
return element.HasTagName(svg_names::kTitleTag) ||
element.HasTagName(svg_names::kDescTag);
})) {
return kIncludeObject;
}
// If setting enabled, do not ignore SVG grouping (<g>) elements.
if (IsA<SVGGElement>(node)) {
Settings* settings = GetDocument()->GetSettings();
if (settings->GetAccessibilityIncludeSvgGElement()) {
return kIncludeObject;
}
}
// If we return kDefaultBehavior here, the logic related to inclusion of
// clickable objects, links, controls, etc. will not be reached. We handle
// SVG elements early to ensure properties in a <symbol> subtree do not
// result in inclusion.
}
if (IsTableLikeRole() || IsTableRowLikeRole() || IsTableCellLikeRole())
return kIncludeObject;
if (IsA<HTMLHtmlElement>(node)) {
if (ignored_reasons) {
ignored_reasons->push_back(IgnoredReason(kAXUninteresting));
}
return kIgnoreObject;
}
// All focusable elements except the <body> and <html> are included.
if (!IsA<HTMLBodyElement>(node) && CanSetFocusAttribute())
return kIncludeObject;
if (IsLink())
return kIncludeObject;
// A click handler might be placed on an otherwise ignored non-empty block
// element, e.g. a div. We shouldn't ignore such elements because if an AT
// sees the |ax::mojom::blink::DefaultActionVerb::kClickAncestor|, it will
// look for the clickable ancestor and it expects to find one.
if (IsClickable())
return kIncludeObject;
if (IsHeading())
return kIncludeObject;
// Header and footer tags may also be exposed as landmark roles but not
// always.
if (node->HasTagName(html_names::kHeaderTag) ||
node->HasTagName(html_names::kFooterTag))
return kIncludeObject;
// All controls are accessible.
if (IsControl())
return kIncludeObject;
// Anything with an explicit ARIA role should be included.
if (AriaRoleAttribute() != ax::mojom::blink::Role::kUnknown)
return kIncludeObject;
// Anything with CSS alt should be included.
// Descendants are pruned: IsRelevantPseudoElementDescendant() returns false.
// Note: this is duplicated from AXLayoutObject because CSS alt text may apply
// to both Elements and pseudo-elements.
std::optional<String> alt_text = GetCSSAltText(GetElement());
if (alt_text && !alt_text->empty())
return kIncludeObject;
// Don't ignored legends, because JAWS uses them to determine redundant text.
if (IsA<HTMLLegendElement>(node))
return kIncludeObject;
// Anything that is an editable root should not be ignored. However, one
// cannot just call `AXObject::IsEditable()` since that will include the
// contents of an editable region too. Only the editable root should always be
// exposed.
if (IsEditableRoot())
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,
// Don't ignore MathML nodes by default, since MathML
// relies on child positions to determine semantics
// (e.g. numerator is the first child of a fraction).
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::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,
ax::mojom::blink::Role::kStrong,
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;
}
// An <hgroup> element has the "group" aria role.
if (GetNode()->HasTagName(html_names::kHgroupTag)) {
return kIncludeObject;
}
// Using the title or accessibility description (so we
// check if there's some kind of accessible name for the element)
// to decide an element's visibility is not as definitive as
// previous checks, so this should remain as one of the last.
if (HasAriaAttribute() || !GetAttribute(kTitleAttr).empty())
return kIncludeObject;
if (IsImage() && !IsA<SVGElement>(node)) {
String alt = GetAttribute(kAltAttr);
// A null alt attribute means the attribute is not present. We assume this
// is a mistake, and expose the image so that it can be repaired.
// In contrast, alt="" is treated as intentional markup to ignore the image.
if (!alt.empty() || alt.IsNull())
return kIncludeObject;
if (ignored_reasons)
ignored_reasons->push_back(IgnoredReason(kAXEmptyAlt));
return kIgnoreObject;
}
// Process potential in-page link targets
if (IsPotentialInPageLinkTarget(*element))
return kIncludeObject;
if (AXObjectCache().GetAXMode().has_mode(ui::AXMode::kInlineTextBoxes)) {
// We are including inline block elements since we might rely on these for
// NextOnLine/PreviousOnLine computations.
//
// If we have an element with inline
// block specified, we should include. There are some roles where we
// shouldn't include even if inline block, or we'll get test failures.
//
// We also only want to include in the tree if the inline block element has
// siblings.
// Otherwise we will include nodes that we don't need for anything.
// Consider a structure where we have a subtree of 12 layers, where each
// layer has an inline-block node with a single child that points to the
// next layer. All nodes have a single child, meaning that this child has no
// siblings.
if (!IsExemptFromInlineBlockCheck(native_role_) && GetLayoutObject() &&
GetLayoutObject()->IsInline() &&
GetLayoutObject()->IsAtomicInlineLevel() &&
node->parentNode()->childElementCount() > 1) {
return kIncludeObject;
}
}
// <span> tags are inline tags and not meant to convey information if they
// have no other ARIA information on them. If we don't ignore them, they may
// emit signals expected to come from their parent.
if (IsA<HTMLSpanElement>(node)) {
if (ignored_reasons)
ignored_reasons->push_back(IgnoredReason(kAXUninteresting));
return kIgnoreObject;
}
// Ignore labels that are already used to name a control.
// See IsRedundantLabel() for more commentary.
if (HTMLLabelElement* label = DynamicTo<HTMLLabelElement>(node)) {
if (IsRedundantLabel(label)) {
if (ignored_reasons) {
ignored_reasons->push_back(
IgnoredReason(kAXLabelFor, AXObjectCache().Get(label->control())));
}
return kIgnoreObject;
}
return kIncludeObject;
}
// The SVG-AAM says the foreignObject element is normally presentational.
if (IsA<SVGForeignObjectElement>(node)) {
if (ignored_reasons) {
ignored_reasons->push_back(IgnoredReason(kAXPresentational));
}
return kIgnoreObject;
}
return kDefaultBehavior;
}
bool AXNodeObject::ComputeAccessibilityIsIgnored(
IgnoredReasons* ignored_reasons) const {
Node* node = GetNode();
if (ShouldIgnoreForHiddenOrInert(ignored_reasons)) {
// Fallback elements inside of a <canvas> are invisible, but are not ignored
// if they are semantic and not aria-hidden or hidden via style.
if (IsAriaHidden() || IsHiddenViaStyle() || IsHiddenByChildTree() ||
!node || !node->parentElement() ||
!node->parentElement()->IsInCanvasSubtree()) {
return true;
}
}
// Handle content that is either visible or in a canvas subtree.
AXObjectInclusion include = ShouldIncludeBasedOnSemantics(ignored_reasons);
if (include == kIncludeObject) {
return false;
}
if (include == kIgnoreObject) {
return true;
}
if (!GetLayoutObject()) {
// Text without a layout object that has reached this point is not
// explicitly hidden, e.g. is in a <canvas> fallback or is display locked.
if (IsA<Text>(node)) {
return false;
}
if (ignored_reasons) {
ignored_reasons->push_back(IgnoredReason(kAXUninteresting));
}
return true;
}
// Inner editor element of editable area with empty text provides bounds
// used to compute the character extent for index 0. This is the same as
// what the caret's bounds would be if the editable area is focused.
if (node) {
const TextControlElement* text_control = EnclosingTextControl(node);
if (text_control) {
// Keep only the inner editor element and it's children.
// If inline textboxes are being loaded, then the inline textbox for the
// text wil be included by AXNodeObject::AddInlineTextboxChildren().
// By only keeping the inner editor and its text, it makes finding the
// inner editor simpler on the browser side.
// See BrowserAccessibility::GetTextFieldInnerEditorElement().
// TODO(accessibility) In the future, we may want to keep all descendants
// of the inner text element -- right now we only include one internally
// used container, it's text, and possibly the text's inlinext text box.
return text_control->InnerEditorElement() != node &&
text_control->InnerEditorElement() != NodeTraversal::Parent(*node);
}
}
// A LayoutEmbeddedContent is an iframe element or embedded object element or
// something like that. We don't want to ignore those.
if (GetLayoutObject()->IsLayoutEmbeddedContent()) {
return false;
}
if (node && node->IsInUserAgentShadowRoot()) {
if (auto* containing_media_element =
DynamicTo<HTMLMediaElement>(node->OwnerShadowHost())) {
if (!containing_media_element->ShouldShowControls()) {
return true;
}
}
}
// Layers are used on objects that have styles where Blink is likely to
// attempt to optimize them in for the GPU, such as animations, z-indexing and
// hidden overflow. Ensure SVG layered objects are unignored.
// We used to do this for any element, which caused extra divs to be added
// to the tree fo no clear reason. For now, we are limiting to only SVG.
// TODO(accessibility) Consider removal of this special case.
if (IsA<SVGElement>(node) && GetLayoutObject()->HasLayer() &&
node->hasChildren()) {
return false;
}
if (IsCanvas()) {
if (CanvasHasFallbackContent()) {
return false;
}
// A 1x1 canvas is too small for the user to see and thus ignored.
const auto* canvas = DynamicTo<LayoutHTMLCanvas>(GetLayoutObject());
if (canvas && (canvas->Size().height <= 1 || canvas->Size().width <= 1)) {
if (ignored_reasons) {
ignored_reasons->push_back(IgnoredReason(kAXProbablyPresentational));
}
return true;
}
// Otherwise fall through; use presence of help text, title, or description
// to decide.
}
if (GetLayoutObject()->IsBR()) {
return false;
}
if (GetLayoutObject()->IsText()) {
if (GetLayoutObject()->IsInListMarker()) {
// Ignore TextAlternative of the list marker for SUMMARY because:
// - TextAlternatives for disclosure-* are triangle symbol characters
// used to visually indicate the expansion state.
// - It's redundant. The host DETAILS exposes the expansion state.
// Also ignore text descendants of any non-ignored list marker because the
// text descendants do not provide any extra information than the
// TextAlternative on the list marker. Besides, with 'speak-as', they will
// be inconsistent with the list marker.
const AXObject* list_marker_object =
ContainerListMarkerIncludingIgnored();
if (list_marker_object &&
(list_marker_object->GetLayoutObject()->IsListMarkerForSummary() ||
!list_marker_object->AccessibilityIsIgnored())) {
if (ignored_reasons) {
ignored_reasons->push_back(IgnoredReason(kAXPresentational));
}
return true;
}
}
// Ignore text inside of an ignored <label>.
// To save processing, only walk up the ignored objects.
// This means that other interesting objects inside the <label> will
// cause the text to be unignored.
if (IsUsedForLabelOrDescription()) {
AXObject* ancestor = ParentObject();
while (ancestor && ancestor->AccessibilityIsIgnored()) {
if (ancestor->RoleValue() == ax::mojom::blink::Role::kLabelText) {
if (ignored_reasons) {
ignored_reasons->push_back(IgnoredReason(kAXPresentational));
}
return true;
}
ancestor = ancestor->ParentObject();
}
}
return false;
}
std::optional<String> alt_text = GetCSSAltText(GetElement());
if (alt_text) {
return alt_text->empty();
}
if (GetLayoutObject()->IsListMarker()) {
// Ignore TextAlternative of the list marker for SUMMARY because:
// - TextAlternatives for disclosure-* are triangle symbol characters used
// to visually indicate the expansion state.
// - It's redundant. The host DETAILS exposes the expansion state.
if (GetLayoutObject()->IsListMarkerForSummary()) {
if (ignored_reasons) {
ignored_reasons->push_back(IgnoredReason(kAXPresentational));
}
return true;
}
return false;
}
// Positioned elements and scrollable containers are important for determining
// bounding boxes, so don't ignore them unless they are pseudo-content.
if (!GetLayoutObject()->IsPseudoElement()) {
if (IsScrollableContainer()) {
return false;
}
if (GetLayoutObject()->IsPositioned()) {
return false;
}
}
// Ignore a block flow (display:block, display:inline-block), unless it
// directly parents inline children.
// This effectively trims a lot of uninteresting divs out of the tree.
if (auto* block_flow = DynamicTo<LayoutBlockFlow>(GetLayoutObject())) {
if (block_flow->ChildrenInline() && block_flow->FirstChild()) {
return false;
}
}
// By default, objects should be ignored so that the AX hierarchy is not
// filled with unnecessary items.
if (ignored_reasons) {
ignored_reasons->push_back(IgnoredReason(kAXUninteresting));
}
return true;
}
// static
std::optional<String> AXNodeObject::GetCSSAltText(const Element* element) {
// CSS alt text rules allow text to be assigned to ::before/::after content.
// For example, the following CSS assigns "bullet" text to bullet.png:
// .something::before {
// content: url(bullet.png) / "bullet";
// }
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->IsAltText())
return To<AltTextContentData>(content_data)->GetText();
}
return std::nullopt;
}
// If the content property is used on a non-pseudo element, match the
// behaviour of LayoutObject::CreateObject and only honour the style if
// there is exactly one piece of content, which is an image.
const ContentData* content_data = style->GetContentData();
if (content_data && content_data->IsImage() && content_data->Next() &&
content_data->Next()->IsAltText()) {
return To<AltTextContentData>(content_data->Next())->GetText();
}
return std::nullopt;
}
// The following lists are for deciding whether the tags aside,
// header and footer can be interpreted as roles complementary, banner and
// contentInfo or if they should be interpreted as generic.
// This function only handles the complementary, banner, and contentInfo roles,
// which belong to the landmark roles set.
static HashSet<ax::mojom::blink::Role>& GetLandmarkIsNotAllowedAncestorRoles(
ax::mojom::blink::Role landmark) {
// clang-format off
DEFINE_STATIC_LOCAL(
// https://html.spec.whatwg.org/multipage/dom.html#sectioning-content-2
// The aside element should not assume the complementary role when nested
// within the following sectioning content elements.
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
}));
// https://w3c.github.io/html-aam/#el-header-ancestorbody
// The header and footer elements should not assume the banner and
// contentInfo roles, respectively, when nested within any of the
// sectioning content elements or the main element.
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
}));
// clang-format on
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;
if (AriaRoleAttribute() == ax::mojom::blink::Role::kComplementary) {
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) {
// If this header is only cell in its row, it is a column header.
// It is also a column header if it has a header on either side of it.
// If instead it has a non-empty td element next to it, it is a row header.
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;
// If this row's first or last cell is a non-empty td, this is a row header.
// Do the same check for the second and second-to-last cells because tables
// often have an empty cell at the intersection of the row and column headers.
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;
// We have no evidence that this is not a column header.
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;
// Ensure table container.
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 = GetAttribute(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());
}
// The following is a heuristic used to determine if a
// <table> should be with ax::mojom::blink::Role::kTable or
// ax::mojom::blink::Role::kLayoutTable.
// Only "data" tables should be exposed as tables.
// Unfortunately, there is no determinsistic or precise way to differentiate a
// layout table vs a data table. Fortunately, CSS authoring techniques have
// improved a lot and mostly supplanted the practice of using tables for layout.
bool AXNodeObject::IsDataTable() const {
DCHECK(!IsDetached());
auto* table_element = DynamicTo<HTMLTableElement>(GetNode());
if (!table_element) {
return false;
}
if (!GetLayoutObject()) {
// The table is not rendered, so the author has no reason to use the table
// for layout. Treat as a data table by default as there is not enough
// information to decide otherwise.
// One useful result of this is that a table inside a canvas fallback is
// treated as a data table.
return true;
}
// If it has an ARIA role, it's definitely a data table.
AtomicString role;
if (HasAOMPropertyOrARIAAttribute(AOMStringProperty::kRole, role)) {
return true;
}
// When a section of the document is contentEditable, all tables should be
// treated as data tables, otherwise users may not be able to work with rich
// text editors that allow creating and editing tables.
if (GetNode() && blink::IsEditable(*GetNode())) {
return true;
}
// If there is a caption element, summary, THEAD, or TFOOT section, it's most
// certainly a data table
if (!table_element->Summary().empty() || table_element->tHead() ||
table_element->tFoot() || table_element->caption()) {
return true;
}
// if someone used "rules" attribute than the table should appear
if (!table_element->Rules().empty()) {
return true;
}
// if there's a colgroup or col element, it's probably a data table.
if (Traversal<HTMLTableColElement>::FirstChild(*table_element)) {
return true;
}
// If there are at least 20 rows, we'll call it a data table.
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 there's only one cell, it's not a good AXTable candidate.
if (num_rows == 1 && num_cols_in_first_body == 1) {
return false;
}
// Store the background color of the table to check against cell's background
// colors.
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();
// check enough of the cells to find if the table matches our criteria
// Criteria:
// 1) must have at least one valid cell (and)
// 2) at least half of cells have borders (or)
// 3) at least half of cells have different bg colors than the table, and
// there is cell spacing
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;
Color alternating_row_colors[5];
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;
}
// Any <th> tag -> treat as data table.
if (cell->HasTagName(html_names::kThTag)) {
return true;
}
// Check for an explicitly assigned a "data" table attribute.
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);
if (cell_layout_block->Size().width < 1 ||
cell_layout_block->Size().height < 1) {
continue;
}
valid_cell_count++;
const ComputedStyle* computed_style = cell_layout_block->Style();
if (!computed_style) {
continue;
}
// If the empty-cells style is set, we'll call it a data table.
if (computed_style->EmptyCells() == EEmptyCells::kHide) {
return true;
}
// If a cell has matching bordered sides, call it a (fully) bordered cell.
if ((cell_layout_block->BorderTop() > 0 &&
cell_layout_block->BorderBottom() > 0) ||
(cell_layout_block->BorderLeft() > 0 &&
cell_layout_block->BorderRight() > 0)) {
bordered_cell_count++;
}
// Also keep track of each individual border, so we can catch tables where
// most cells have a bottom border, for example.
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++;
}
// If the cell has a different color from the table and there is cell
// spacing, then it is probably a data table cell (spacing and colors take
// the place of borders).
Color cell_color = computed_style->VisitedDependentColor(
GetCSSPropertyBackgroundColor());
if (has_cell_spacing && table_bg_color != cell_color &&
!cell_color.IsFullyTransparent()) {
background_difference_cell_count++;
}
// If we've found 10 "good" cells, we don't need to keep searching.
if (bordered_cell_count >= 10 || background_difference_cell_count >= 10) {
return true;
}
// For the first 5 rows, cache the background color so we can check if
// this table has zebra-striped rows.
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 there is less than two valid cells, it's not a data table
if (valid_cell_count <= 1) {
return false;
}
// half of the cells had borders, it's a data table
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;
}
// half had different background colors, it's a data table
if (background_difference_cell_count >= needed_cell_count) {
return true;
}
// Check if there is an alternating row background color indicating a zebra
// striped style pattern.
if (alternating_row_color_count > 2) {
Color first_color = alternating_row_colors[0];
for (int k = 1; k < alternating_row_color_count; k++) {
// If an odd row was the same color as the first row, its not alternating.
if (k % 2 == 1 && alternating_row_colors[k] == first_color) {
return false;
}
// If an even row is not the same as the first row, its not alternating.
if (!(k % 2) && alternating_row_colors[k] != first_color) {
return false;
}
}
return true;
}
return false;
}
ax::mojom::blink::Role AXNodeObject::RoleFromLayoutObjectOrNode() const {
return ax::mojom::blink::Role::kGenericContainer;
}
// Does not check ARIA role, but does check some ARIA properties, specifically
// @aria-haspopup/aria-pressed via ButtonType().
// TODO(accessibility) Ensure that if the native role needs to change, that the
// object is destroyed and a new one is created. Examples are changes to
// IsClickable(), DataList(), aria-pressed, the parent's tag, @role.
ax::mojom::blink::Role AXNodeObject::NativeRoleIgnoringAria() const {
if (!GetNode()) {
// Can be null in the case of pseudo content.
return RoleFromLayoutObjectOrNode();
}
if (GetNode()->IsPseudoElement() && GetCSSAltText(GetElement())) {
const ComputedStyle* style = GetElement()->GetComputedStyle();
ContentData* content_data = style->GetContentData();
// We just check the first item of the content list to determine the
// appropriate role, should only ever be image or text.
// TODO(accessibility) Is it possible to use CSS alt text on an HTML tag
// with strong semantics? If so, why are we overriding the role here?
// We only need to ensure the accessible name gets the CSS alt text.
// Note: by doing this, we are often hiding child pseudo element content
// because IsRelevantPseudoElementDescendant() returns false when an
// ancestor has CSS alt text.
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 (auto* button = DynamicTo<HTMLButtonElement>(GetNode())) {
if (button->OwnerSelectList()) {
return ax::mojom::blink::Role::kComboBoxMenuButton;
}
}
if (IsA<HTMLListboxElement>(GetNode())) {
return ax::mojom::blink::Role::kListBox;
}
if (IsA<HTMLImageElement>(GetNode()))
return ax::mojom::blink::Role::kImage;
// <a> or <svg:a>.
if (IsA<HTMLAnchorElement>(GetNode()) || IsA<SVGAElement>(GetNode())) {
// Assume that an anchor element is a Role::kLink if it has an href or a
// click event listener.
if (GetNode()->IsLink() || IsClickable())
return ax::mojom::blink::Role::kLink;
// According to the SVG-AAM, a non-link 'a' element should be exposed like
// a 'g' if it does not descend from a 'text' element and like a 'tspan'
// if it does. This is consistent with the SVG spec which states that an
// 'a' within 'text' acts as an inline element, whereas it otherwise acts
// as a container element.
if (IsA<SVGAElement>(GetNode()) &&
!Traversal<SVGTextElement>::FirstAncestor(*GetNode())) {
return ax::mojom::blink::Role::kGroup;
}
return ax::mojom::blink::Role::kGenericContainer;
}
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;
}
// Chrome exposes both table markup and table CSS as a table, letting
// the screen reader determine what to do for CSS tables.
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->IsMultiple())
return ax::mojom::blink::Role::kListBox;
else
return ax::mojom::blink::Role::kComboBoxSelect;
}
if (auto* option = DynamicTo<HTMLOptionElement>(*GetNode())) {
HTMLSelectElement* select_element = option->OwnerSelectElement();
if (!select_element || select_element->IsMultiple() ||
option->OwnerSelectList()) {
return ax::mojom::blink::Role::kListBoxOption;
} else {
return ax::mojom::blink::Role::kMenuListOption;
}
}
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())) {
// <menu> is a deprecated feature of HTML 5, but is included for semantic
// compatibility with HTML3, and may contain list items. Exposing it as an
// unordered list works better than the current HTML-AAM recommendaton of
// exposing as a role=menu, because if it's just used semantically, it won't
// be interactive. If used as a widget, the author must provide role=menu.
return ax::mojom::blink::Role::kList;
}
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<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;
// Mapping of MathML elements. See https://w3c.github.io/mathml-aam/
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())) {
// Only treat <form> as role="form" when it has an accessible name, which
// can only occur when the name is assigned by the author via aria-label,
// aria-labelledby, or title. Otherwise, treat as a <section>.
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)) {
// Treat a named <section> as role="region".
return IsNameFromAuthorAttribute() ? ax::mojom::blink::Role::kRegion
: ax::mojom::blink::Role::kSection;
}
if (GetNode()->HasTagName(html_names::kAddressTag))
return 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;
// The HTML element.
if (IsA<HTMLHtmlElement>(GetNode()))
return RoleFromLayoutObjectOrNode();
// Treat <iframe>, <frame> and <fencedframe> the same.
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::DetermineAccessibilityRole() {
#if DCHECK_IS_ON()
base::AutoReset<bool> reentrancy_protector(&is_computing_role_, true);
#endif
if (IsDetached()) {
NOTREACHED() << "Do not compute role on detached object: "
<< ToString(true, true);
return ax::mojom::blink::Role::kUnknown;
}
native_role_ = NativeRoleIgnoringAria();
aria_role_ = DetermineAriaRoleAttribute();
return aria_role_ == ax::mojom::blink::Role::kUnknown ? native_role_
: aria_role_;
}
void AXNodeObject::AccessibilityChildrenFromAOMProperty(
AOMRelationListProperty property,
AXObject::AXObjectVector& children) const {
HeapVector<Member<Element>> elements;
if (!HasAOMPropertyOrARIAAttribute(property, elements))
return;
AXObjectCacheImpl& cache = AXObjectCache();
for (const auto& element : elements) {
if (AXObject* child = cache.Get(element)) {
// Only aria-labelledby and aria-describedby can target hidden elements.
if (!child)
continue;
if (child->AccessibilityIsIgnored() &&
property != AOMRelationListProperty::kLabeledBy &&
property != AOMRelationListProperty::kDescribedBy) {
continue;
}
children.push_back(child);
}
}
}
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 =
AccessibleNode::GetPropertyOrARIAAttribute(element,
AOMStringProperty::kRole);
if (EqualIgnoringASCIICase(sibling_aria_role, role))
return element;
}
return nullptr;
}
Element* AXNodeObject::MenuItemElementForMenu() const {
if (AriaRoleAttribute() != 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 defined(AX_FAIL_FAST_BUILD)
SANITIZER_CHECK(!is_adding_children_)
<< "Cannot detach |this| during AddChildren(): " << GetNode();
#endif
AXObject::Detach();
node_ = 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()) ||
AXObject::IsARIAControl(AriaRoleAttribute()));
}
bool AXNodeObject::IsAutofillAvailable() const {
// Autofill suggestion availability is stored in AXObjectCache.
WebAXAutofillSuggestionAvailability suggestion_availability =
AXObjectCache().GetAutofillSuggestionAvailability(AXObjectID());
return suggestion_availability ==
WebAXAutofillSuggestionAvailability::kAutofillAvailable;
}
bool AXNodeObject::IsDefault() const {
if (IsDetached())
return false;
// Checks for any kind of disabled, including aria-disabled.
if (Restriction() == kRestrictionDisabled ||
RoleValue() != ax::mojom::blink::Role::kButton) {
return false;
}
// Will only match :default pseudo class if it's the first default button in
// a form.
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 {
// According to Blink Editing, objects without an associated DOM node such as
// pseudo-elements and list bullets, are never considered as paragraph
// boundaries.
if (IsDetached() || !GetNode())
return false;
// Presentational objects should not contribute any of their semantic meaning
// to the accessibility tree, including to its text representation.
if (IsPresentational())
return false;
// `IsEnclosingBlock` includes all elements with display block, inline block,
// table related, flex, grid, list item, flow-root, webkit-box, and display
// contents. This is the same function used by Blink > Editing for determining
// paragraph boundaries, i.e. line breaking objects.
if (IsEnclosingBlock(GetNode()))
return true;
// Not all <br> elements have an associated layout object. They might be
// "visibility: hidden" or within a display locked region. We need to check
// their DOM node first.
if (IsA<HTMLBRElement>(GetNode()))
return true;
const LayoutObject* layout_object = GetLayoutObject();
if (!layout_object)
return AXObject::IsLineBreakingObject();
if (layout_object->IsBR())
return true;
// LayoutText objects could include a paragraph break in their text. This can
// only occur if line breaks are preserved and a newline character is present
// in their collapsed text. Text collapsing removes all whitespace found in
// the HTML file, but a special style rule could be used to preserve line
// breaks.
//
// The best example is the <pre> element:
// <pre>Line 1
// Line 2</pre>
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') != WTF::kNotFound) {
return true;
}
}
// Rely on the ARIA role to figure out if this object is line breaking.
return AXObject::IsLineBreakingObject();
}
bool AXNodeObject::IsLoaded() const {
if (!GetDocument())
return false;
if (!GetDocument()->IsLoadCompleted())
return false;
// Check for a navigation API single-page app navigation in progress.
if (auto* window = GetDocument()->domWindow()) {
if (window->navigation()->HasNonDroppedOngoingNavigation())
return false;
}
return true;
}
bool AXNodeObject::IsMultiSelectable() const {
switch (RoleValue()) {
case ax::mojom::blink::Role::kGrid:
case ax::mojom::blink::Role::kTreeGrid:
case ax::mojom::blink::Role::kTree:
case ax::mojom::blink::Role::kListBox:
case ax::mojom::blink::Role::kTabList: {
bool multiselectable = false;
if (HasAOMPropertyOrARIAAttribute(AOMBooleanProperty::kMultiselectable,
multiselectable)) {
return multiselectable;
}
break;
}
default:
break;
}
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::IsOffScreen() const {
if (IsDetached())
return false;
DCHECK(GetNode());
// Differs fromAXLayoutObject::IsOffScreen() in that there is no bounding box.
// However, we know that if it is display-locked that is an indicator that it
// is currently offscreen, and will likely be onscreen once scrolled to.
return DisplayLockUtilities::IsDisplayLockedPreventingPaint(GetNode());
}
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 {
// Determine whether the element is clickable either because there is a
// mouse button handler or because it has a native element where click
// performs an action. Disabled nodes are never considered clickable.
// Note: we can't call |node->WillRespondToMouseClickEvents()| because that
// triggers a style recalc and can delete this.
// Treat mouse button listeners on the |window|, |document| as if they're on
// the |documentElement|.
if (GetNode() == GetDocument()->documentElement()) {
return GetNode()->HasAnyEventListeners(
event_util::MouseButtonEventTypes()) ||
GetDocument()->HasAnyEventListeners(
event_util::MouseButtonEventTypes()) ||
GetDocument()->domWindow()->HasAnyEventListeners(
event_util::MouseButtonEventTypes());
}
// Look for mouse listeners only on element nodes, e.g. skip text nodes.
const Element* element = GetElement();
if (!element)
return false;
if (IsDisabled())
return false;
if (element->HasAnyEventListeners(event_util::MouseButtonEventTypes()))
return true;
if (HasContentEditableAttributeSet())
return true;
// Certain user-agent shadow DOM elements are expected to be clickable but
// they do not have event listeners attached or a clickable native role. We
// whitelist them here.
if (element->ShadowPseudoId() ==
shadow_element_names::kPseudoCalendarPickerIndicator) {
return true;
}
// Only use native roles. For ARIA elements, require a click listener.
return ui::IsClickable(native_role_);
}
bool AXNodeObject::IsFocused() const {
if (!GetDocument())
return false;
// A web area is represented by the Document node in the DOM tree, which isn't
// focusable. Check instead if the frame's selection controller is focused.
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() || !GetLayoutObject() || !IsSubWidget())
return kSelectedStateUndefined;
// The aria-selected attribute overrides automatic behaviors.
bool is_selected;
if (HasAOMPropertyOrARIAAttribute(AOMBooleanProperty::kSelected, is_selected))
return is_selected ? kSelectedStateTrue : kSelectedStateFalse;
// The selection should only follow the focus when the aria-selected attribute
// is marked as required or implied for this element in the ARIA specs.
// If this object can't follow the focus, then we can't say that it's selected
// nor that it's not.
if (!ui::IsSelectRequiredOrImplicit(RoleValue()))
return kSelectedStateUndefined;
// Selection follows focus, but ONLY in single selection containers, and only
// if aria-selected was not present to override.
return IsSelectedFromFocus() ? kSelectedStateTrue : kSelectedStateFalse;
}
bool AXNodeObject::IsSelectedFromFocusSupported() const {
// The selection should only follow the focus when the aria-selected attribute
// is marked as required or implied for this element in the ARIA specs.
// If this object can't follow the focus, then we can't say that it's selected
// nor that it's not.
// TODO(crbug.com/1143483): Consider allowing more roles.
if (!ui::IsSelectRequiredOrImplicit(RoleValue()))
return false;
// https://www.w3.org/TR/wai-aria-1.1/#aria-selected
// Any explicit assignment of aria-selected takes precedence over the implicit
// selection based on focus.
bool is_selected;
if (HasAOMPropertyOrARIAAttribute(AOMBooleanProperty::kSelected, is_selected))
return false;
// Selection follows focus only when in a single selection container.
const AXObject* container = ContainerWidget();
if (!container || container->IsMultiSelectable())
return false;
// TODO(crbug.com/1143451): https://www.w3.org/TR/wai-aria-1.1/#aria-selected
// If any DOM element in the widget is explicitly marked as selected, the user
// agent MUST NOT convey implicit selection for the widget.
return true;
}
// In single selection containers, selection follows focus unless aria_selected
// is set to false. This is only valid for a subset of elements.
bool AXNodeObject::IsSelectedFromFocus() const {
if (!IsSelectedFromFocusSupported())
return false;
// A tab item can also be selected if it is associated to a focused tabpanel
// via the aria-labelledby attribute.
if (IsTabItem() && IsTabItemSelected())
return true;
// If this object is not accessibility focused, then it is not selected from
// focus.
AXObject* focused_object = AXObjectCache().FocusedObject();
if (focused_object != this &&
(!focused_object || focused_object->ActiveDescendant() != this))
return false;
return true;
}
bool AXNodeObject::IsTabItemSelected() const {
if (!IsTabItem() || !GetLayoutObject())
return false;
Node* node = GetNode();
if (!node || !node->IsElementNode())
return false;
// The ARIA spec says a tab item can also be selected if it is aria-labeled by
// a tabpanel that has keyboard focus inside of it, or if a tabpanel in its
// aria-controls list has KB focus inside of it.
AXObject* focused_element = AXObjectCache().FocusedObject();
if (!focused_element)
return false;
HeapVector<Member<Element>> elements;
if (!HasAOMPropertyOrARIAAttribute(AOMRelationListProperty::kControls,
elements))
return false;
for (const auto& element : elements) {
AXObject* tab_panel = AXObjectCache().Get(element);
// A tab item should only control tab panels.
if (!tab_panel ||
tab_panel->RoleValue() != ax::mojom::blink::Role::kTabPanel) {
continue;
}
AXObject* check_focus_element = focused_element;
// Check if the focused element is a descendant of the element controlled by
// the tab item.
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;
// An <optgroup> is not exposed directly in the AX tree.
if (IsA<HTMLOptGroupElement>(elem))
return kRestrictionNone;
// According to ARIA, all elements of the base markup can be disabled.
// According to CORE-AAM, any focusable descendant of aria-disabled
// ancestor is also disabled.
if (IsDisabled())
return kRestrictionDisabled;
// Only editable fields can be marked @readonly (unlike @aria-readonly).
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;
}
// Check aria-readonly if supported by current role.
bool is_read_only;
if (SupportsARIAReadOnly() &&
HasAOMPropertyOrARIAAttribute(AOMBooleanProperty::kReadOnly,
is_read_only)) {
// ARIA overrides other readonly state markup.
return is_read_only ? kRestrictionReadOnly : kRestrictionNone;
}
// If a grid cell does not have it's own ARIA input restriction,
// fall back on parent grid's readonly state.
// See ARIA specification regarding grid/treegrid and readonly.
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;
}
}
}
// This is a node that is not readonly and not disabled.
return kRestrictionNone;
}
AccessibilityExpanded AXNodeObject::IsExpanded() const {
if (!SupportsARIAExpanded())
return kExpandedUndefined;
auto* element = GetElement();
if (!element)
return kExpandedUndefined;
if (auto* button = DynamicTo<HTMLButtonElement>(element)) {
if (auto* select_list = button->OwnerSelectList()) {
return select_list->open() ? kExpandedExpanded : kExpandedCollapsed;
}
}
if (RoleValue() == ax::mojom::blink::Role::kComboBoxSelect &&
IsA<HTMLSelectElement>(*element)) {
return To<HTMLSelectElement>(element)->PopupIsVisible()
? kExpandedExpanded
: kExpandedCollapsed;
}
// For form controls that act as triggering elements for popovers of type
// kAuto, then set aria-expanded=false when the popover is hidden, and
// aria-expanded=true when it is showing.
if (auto* form_control = DynamicTo<HTMLFormControlElement>(element)) {
if (auto popover = form_control->popoverTargetElement().popover;
popover && popover->PopoverType() == PopoverValueType::kAuto) {
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 (HasAOMPropertyOrARIAAttribute(AOMBooleanProperty::kExpanded, 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 (AOMPropertyOrARIAAttributeIsTrue(AOMBooleanProperty::kRequired))
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 {
// headings can be in block flow and non-block flow
Node* node = GetNode();
if (!node)
return 0;
if (RoleValue() == ax::mojom::blink::Role::kHeading) {
uint32_t level;
if (HasAOMPropertyOrARIAAttribute(AOMUIntProperty::kLevel, 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;
if (element->HasTagName(html_names::kH2Tag))
return 2;
if (element->HasTagName(html_names::kH3Tag))
return 3;
if (element->HasTagName(html_names::kH4Tag))
return 4;
if (element->HasTagName(html_names::kH5Tag))
return 5;
if (element->HasTagName(html_names::kH6Tag))
return 6;
if (RoleValue() == ax::mojom::blink::Role::kHeading)
return kDefaultHeadingLevel;
return 0;
}
unsigned AXNodeObject::HierarchicalLevel() const {
Element* element = GetElement();
if (!element)
return 0;
uint32_t level;
if (HasAOMPropertyOrARIAAttribute(AOMUIntProperty::kLevel, level)) {
if (level >= 1)
return level;
}
// Helper lambda for calculating hierarchical levels by counting ancestor
// nodes that match a target role.
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:
// Comment: level is based on counting comment ancestors until the root.
return accumulateLevel(1, ax::mojom::blink::Role::kComment);
case ax::mojom::blink::Role::kListItem:
level = accumulateLevel(0, ax::mojom::blink::Role::kList);
// When level count is 0 due to this list item not having an ancestor of
// Role::kList, not nested in list groups, this list item has a level
// of 1.
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: {
// Hierarchy leveling starts at 1, to match the aria-level spec.
// We measure tree hierarchy by the number of groups that the item is
// within.
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 {
// Check cache for auto complete state.
if (AXObjectCache().GetAutofillSuggestionAvailability(AXObjectID()) ==
WebAXAutofillSuggestionAvailability::kAutocompleteAvailable) {
return "list";
}
if (IsAtomicTextField() || IsARIATextField()) {
const AtomicString& aria_auto_complete =
GetAOMPropertyOrARIAAttribute(AOMStringProperty::kAutocomplete)
.LowerASCII();
// Illegal values must be passed through, according to CORE-AAM.
if (!aria_auto_complete.IsNull())
return aria_auto_complete == "none" ? String() : aria_auto_complete;
}
if (auto* input = DynamicTo<HTMLInputElement>(GetNode())) {
if (input->DataList())
return "list";
}
return String();
}
// TODO(nektar): Consider removing this method in favor of
// AXInlineTextBox::GetDocumentMarkers, or add document markers to the tree data
// instead of nodes objects.
void AXNodeObject::SerializeMarkerAttributes(ui::AXNodeData* node_data) const {
if (!GetNode() || !GetDocument() || !GetDocument()->View())
return;
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;
// First use ARIA markers for spelling/grammar if available.
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, TextAffinity::kDownstream,
AXPositionAdjustmentBehavior::kMoveLeft);
auto end_pos =
AXPosition::FromPosition(end_position, 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);
}
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();
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()
// Link targets always have an element, unless it is the document itself,
// e.g. via <a href="#">.
DCHECK(ax_target->IsWebArea() || ax_target->GetElement())
<< "The link target is expected to be a document or an element: "
<< ax_target->ToString(true, true) << "\n* URL fragment = " << fragment;
#endif
// Usually won't be ignored, but could be e.g. if aria-hidden.
if (ax_target->AccessibilityIsIgnored())
return nullptr;
return ax_target;
}
const AtomicString& AXNodeObject::EffectiveTarget() const {
// The "target" attribute defines the target browser context and is supported
// on <a>, <area>, <base>, and <form>. Valid values are: "frame_name", "self",
// "blank", "top", and "parent", where "frame_name" is the value of the "name"
// attribute on any enclosing iframe.
//
// <area> is a subclass of <a>, while <base> provides the document's base
// target that any <a>'s or any <area>'s target can override.
// `HtmlAnchorElement::GetEffectiveTarget()` will take <base> into account.
//
// <form> is out of scope, because it affects the target to which the form is
// submitted, and could also be overridden by a "formTarget" attribute on e.g.
// a form's submit button. However, screen reader users have no need to know
// to which target (browser context) a form would be submitted.
const auto* anchor = DynamicTo<HTMLAnchorElement>(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 {
const AtomicString& aria_orientation =
GetAOMPropertyOrARIAAttribute(AOMStringProperty::kOrientation);
AccessibilityOrientation orientation = kAccessibilityOrientationUndefined;
if (EqualIgnoringASCIICase(aria_orientation, "horizontal"))
orientation = kAccessibilityOrientationHorizontal;
else if (EqualIgnoringASCIICase(aria_orientation, "vertical"))
orientation = kAccessibilityOrientationVertical;
switch (RoleValue()) {
case ax::mojom::blink::Role::kListBox:
case ax::mojom::blink::Role::kMenu:
case ax::mojom::blink::Role::kScrollBar:
case ax::mojom::blink::Role::kTree:
if (orientation == kAccessibilityOrientationUndefined)
orientation = kAccessibilityOrientationVertical;
return orientation;
case ax::mojom::blink::Role::kMenuBar:
case ax::mojom::blink::Role::kSlider:
case ax::mojom::blink::Role::kSplitter:
case ax::mojom::blink::Role::kTabList:
case ax::mojom::blink::Role::kToolbar:
if (orientation == kAccessibilityOrientationUndefined)
orientation = kAccessibilityOrientationHorizontal;
return orientation;
case ax::mojom::blink::Role::kComboBoxGrouping:
case ax::mojom::blink::Role::kComboBoxMenuButton:
case ax::mojom::blink::Role::kRadioGroup:
case ax::mojom::blink::Role::kTreeGrid:
return orientation;
default:
return AXObject::Orientation();
}
}
// According to the standard, the figcaption should only be the first or
// last child: https://html.spec.whatwg.org/#the-figcaption-element
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;
}
// If the immediate parent is a radio group, return all its children that are
// radio buttons.
AXObject* parent = ParentObjectUnignored();
if (parent && parent->RoleValue() == ax::mojom::blink::Role::kRadioGroup) {
for (AXObject* child : parent->UnignoredChildren()) {
DCHECK(child);
if (child->RoleValue() == ax::mojom::blink::Role::kRadioButton &&
child->AccessibilityIsIncludedInTree()) {
radio_buttons.push_back(child);
}
}
}
return radio_buttons;
}
// static
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;