blob: 47a6903b26781f92335c8f7ffed0e4e7d7caa43c [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 <memory>
#include <algorithm>
#include "base/optional.h"
#include "third_party/blink/public/common/input/web_keyboard_event.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/display_lock/display_lock_utilities.h"
#include "third_party/blink/renderer/core/dom/element.h"
#include "third_party/blink/renderer/core/dom/flat_tree_traversal.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/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_frame_view.h"
#include "third_party/blink/renderer/core/frame/settings.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/forms/html_field_set_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_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_div_element.h"
#include "third_party/blink/renderer/core/html/html_dlist_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_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_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/portal/html_portal_element.h"
#include "third_party/blink/renderer/core/html/shadow/shadow_element_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/layout_block_flow.h"
#include "third_party/blink/renderer/core/layout/layout_box_model_object.h"
#include "third_party/blink/renderer/core/layout/layout_image.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_table.h"
#include "third_party/blink/renderer/core/layout/layout_view.h"
#include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_node.h"
#include "third_party/blink/renderer/core/layout/ng/inline/ng_offset_mapping.h"
#include "third_party/blink/renderer/core/loader/progress_tracker.h"
#include "third_party/blink/renderer/core/mathml_names.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_element.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/modules/media_controls/elements/media_control_elements_helper.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/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 "ui/accessibility/ax_role_properties.h"
#include "ui/events/keycodes/dom/dom_code.h"
#include "ui/events/keycodes/dom/keycode_converter.h"
namespace {
blink::HTMLMapElement* GetMapForImage(blink::LayoutObject* layout_object) {
blink::LayoutImage* layout_image =
blink::DynamicTo<blink::LayoutImage>(layout_object);
if (!layout_image)
return nullptr;
return layout_image->ImageMap();
}
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::NodeTraversal::Parent(*node);
if (!node)
return nullptr;
blink::AXObject* ax_object = cache.GetOrCreate(node);
if (ax_object && !IsNeutralWithinTable(ax_object))
return ax_object;
}
return nullptr;
}
enum class AXAction {
kActionIncrement = 0,
kActionDecrement,
};
blink::KeyboardEvent* CreateKeyboardEvent(
blink::LocalDOMWindow* local_dom_window,
blink::WebInputEvent::Type type,
AXAction action) {
blink::WebKeyboardEvent key(type,
blink::WebInputEvent::Modifiers::kNoModifiers,
base::TimeTicks::Now());
// TODO(crbug.com/1099069): Fire different arrow events depending on
// orientation and dir (RTL/LTR)
switch (action) {
case AXAction::kActionIncrement:
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;
break;
case AXAction::kActionDecrement:
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;
break;
}
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;
}
} // namespace
namespace blink {
using html_names::kAltAttr;
using html_names::kTitleAttr;
using html_names::kTypeAttr;
using html_names::kValueAttr;
// 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),
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 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 this is a native element, set the value directly.
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;
}
// TODO(crbug.com/1099069): add a separate flag for keyboard event synthesis
if (!RuntimeEnabledFeatures::AccessibilityObjectModelEnabled())
return;
// Otherwise, fire a keyboard event instead.
AXAction action =
increase ? AXAction::kActionIncrement : AXAction::kActionDecrement;
LocalDOMWindow* local_dom_window = GetDocument()->domWindow();
KeyboardEvent* keydown = CreateKeyboardEvent(
local_dom_window, WebInputEvent::Type::kRawKeyDown, action);
GetNode()->DispatchEvent(*keydown);
// TODO(crbug.com/1099069): add a brief pause between keydown and keyup?
// TODO(crbug.com/1099069): fire a "char" event depending on platform?
// The keydown handler may have caused the node to be removed.
if (!GetNode())
return;
KeyboardEvent* keyup = CreateKeyboardEvent(
local_dom_window, WebInputEvent::Type::kKeyUp, action);
GetNode()->DispatchEvent(*keyup);
}
AXObject* AXNodeObject::ActiveDescendant() {
Element* element = GetElement();
if (!element)
return nullptr;
Element* descendant =
GetAOMPropertyOrARIAAttribute(AOMRelationProperty::kActiveDescendant);
if (!descendant)
return nullptr;
AXObject* ax_descendant = AXObjectCache().GetOrCreate(descendant);
return ax_descendant && ax_descendant->IsVisible() ? ax_descendant : nullptr;
}
AXObjectInclusion AXNodeObject::ShouldIncludeBasedOnSemantics(
IgnoredReasons* ignored_reasons) const {
// If this element is within a parent that cannot have children, it should not
// be exposed.
if (IsDescendantOfLeafNode()) {
if (ignored_reasons)
ignored_reasons->push_back(
IgnoredReason(kAXAncestorIsLeafNode, LeafNodeAncestor()));
return kIgnoreObject;
}
if (HasInheritedPresentationalRole()) {
if (ignored_reasons) {
const AXObject* inherits_from = InheritsPresentationalRoleFrom();
if (inherits_from == this) {
ignored_reasons->push_back(IgnoredReason(kAXPresentational));
} else {
ignored_reasons->push_back(
IgnoredReason(kAXInheritsPresentation, inherits_from));
}
}
return kIgnoreObject;
}
// Objects inside a portal should be ignored. Portals don't directly expose
// their contents as the contents are not focusable (portals do not currently
// support input events). Portals do use their contents to compute a default
// accessible name.
if (GetDocument() && GetDocument()->GetPage() &&
GetDocument()->GetPage()->InsidePortal()) {
return kIgnoreObject;
}
if (IsTableLikeRole() || IsTableRowLikeRole() || IsTableCellLikeRole())
return kIncludeObject;
// Ignore labels that are already referenced by a control but are not set to
// be focusable.
AXObject* control_ax_object = CorrespondingControlAXObjectForLabelElement();
if (control_ax_object && control_ax_object->IsCheckboxOrRadio() &&
control_ax_object->NameFromLabelElement() &&
AccessibleNode::GetPropertyOrARIAAttribute(
LabelElementContainer(), AOMStringProperty::kRole) == g_null_atom) {
AXObject* label_ax_object = CorrespondingLabelAXObject();
// If the label is set to be focusable, we should expose it.
if (label_ax_object && label_ax_object->CanSetFocusAttribute())
return kIncludeObject;
if (ignored_reasons) {
if (label_ax_object && label_ax_object != this)
ignored_reasons->push_back(
IgnoredReason(kAXLabelContainer, label_ax_object));
ignored_reasons->push_back(IgnoredReason(kAXLabelFor, control_ax_object));
}
return kIgnoreObject;
}
if (GetNode() && !IsA<HTMLBodyElement>(GetNode()) && CanSetFocusAttribute())
return kIncludeObject;
if (IsLink() || IsInPageLinkTarget())
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() || IsLandmarkRelated())
return kIncludeObject;
// Header and footer tags may also be exposed as landmark roles but not
// always.
if (GetNode() && (GetNode()->HasTagName(html_names::kHeaderTag) ||
GetNode()->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.
// Note: this is duplicated from AXLayoutObject because CSS alt text may apply
// to both Elements and pseudo-elements.
base::Optional<String> alt_text = GetCSSAltText(GetNode());
if (alt_text && !alt_text->IsEmpty())
return kIncludeObject;
// Don't ignore labels, because they serve as TitleUIElements.
Node* node = GetNode();
if (IsA<HTMLLabelElement>(node))
return kIncludeObject;
// Don't ignored legends, because JAWS uses them to determine redundant text.
if (IsA<HTMLLegendElement>(node))
return kIncludeObject;
// Anything that is content editable should not be ignored.
// However, one cannot just call node->hasEditableStyle() since that will ask
// if its parents are also editable. Only the top level content editable
// region should be exposed.
if (HasContentEditableAttributeSet())
return kIncludeObject;
static const HashSet<ax::mojom::blink::Role> always_included_computed_roles =
{
ax::mojom::blink::Role::kAbbr,
ax::mojom::blink::Role::kBlockquote,
ax::mojom::blink::Role::kContentDeletion,
ax::mojom::blink::Role::kContentInsertion,
ax::mojom::blink::Role::kDetails,
ax::mojom::blink::Role::kDescriptionList,
ax::mojom::blink::Role::kDescriptionListDetail,
ax::mojom::blink::Role::kDescriptionListTerm,
ax::mojom::blink::Role::kDialog,
ax::mojom::blink::Role::kFigcaption,
ax::mojom::blink::Role::kFigure,
ax::mojom::blink::Role::kList,
ax::mojom::blink::Role::kListItem,
ax::mojom::blink::Role::kMark,
ax::mojom::blink::Role::kMath,
ax::mojom::blink::Role::kMeter,
ax::mojom::blink::Role::kPluginObject,
ax::mojom::blink::Role::kProgressIndicator,
ax::mojom::blink::Role::kRuby,
ax::mojom::blink::Role::kSplitter,
ax::mojom::blink::Role::kTime,
};
if (always_included_computed_roles.find(RoleValue()) !=
always_included_computed_roles.end())
return kIncludeObject;
// 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;
}
// If this element has aria attributes on it, it should not be ignored.
if (HasGlobalARIAAttribute())
return kIncludeObject;
bool has_non_empty_alt_attribute = !GetAttribute(kAltAttr).IsEmpty();
if (IsImage()) {
if (has_non_empty_alt_attribute || GetAttribute(kAltAttr).IsNull())
return kIncludeObject;
else if (ignored_reasons)
ignored_reasons->push_back(IgnoredReason(kAXEmptyAlt));
return kIgnoreObject;
}
// 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.
//
// These checks are simplified in the interest of execution speed;
// for example, any element having an alt attribute will make it
// not ignored, rather than just images.
if (HasAriaAttribute() || !GetAttribute(kTitleAttr).IsEmpty() ||
has_non_empty_alt_attribute)
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 (node && IsA<HTMLSpanElement>(node)) {
if (ignored_reasons)
ignored_reasons->push_back(IgnoredReason(kAXUninteresting));
return kIgnoreObject;
}
return kDefaultBehavior;
}
base::Optional<String> AXNodeObject::GetCSSAltText(const Node* node) {
if (!node || !node->GetComputedStyle() ||
node->GetComputedStyle()->ContentBehavesAsNormal()) {
return base::nullopt;
}
const ComputedStyle* style = node->GetComputedStyle();
if (node->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 base::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 base::nullopt;
}
bool AXNodeObject::ComputeAccessibilityIsIgnored(
IgnoredReasons* ignored_reasons) const {
#if DCHECK_IS_ON()
// Double-check that an AXObject is never accessed before
// it's been initialized.
DCHECK(initialized_);
#endif
// If we don't have a node, then ignore the node object.
// TODO(vmpstr/aleventhal): Investigate how this can happen.
if (!GetNode()) {
NOTREACHED();
return true;
}
// 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 (IsWebArea())
return false;
DCHECK_NE(role_, ax::mojom::blink::Role::kUnknown);
// Use AXLayoutObject::ComputeAccessibilityIsIgnored().
DCHECK(!GetLayoutObject());
if (DisplayLockUtilities::NearestLockedExclusiveAncestor(*GetNode())) {
if (DisplayLockUtilities::ShouldIgnoreNodeDueToDisplayLock(
*GetNode(), DisplayLockActivationReason::kAccessibility)) {
if (ignored_reasons)
ignored_reasons->push_back(IgnoredReason(kAXNotRendered));
return true;
}
return ShouldIncludeBasedOnSemantics(ignored_reasons) == kIgnoreObject;
}
auto* element = DynamicTo<Element>(GetNode());
if (!element)
element = GetNode()->parentElement();
if (!element)
return true;
if (element->IsInCanvasSubtree())
return ShouldIncludeBasedOnSemantics(ignored_reasons) == kIgnoreObject;
if (AOMPropertyOrARIAAttributeIsFalse(AOMBooleanProperty::kHidden))
return false;
if (element->HasDisplayContentsStyle()) {
if (ShouldIncludeBasedOnSemantics(ignored_reasons) == kIncludeObject)
return false;
}
if (ignored_reasons)
ignored_reasons->push_back(IgnoredReason(kAXNotRendered));
return true;
}
static bool IsListElement(Node* node) {
return IsA<HTMLUListElement>(*node) || IsA<HTMLOListElement>(*node) ||
IsA<HTMLDListElement>(*node);
}
static bool IsRequiredOwnedElement(AXObject* parent,
ax::mojom::blink::Role current_role,
HTMLElement* current_element) {
Node* parent_node = parent->GetNode();
auto* parent_html_element = DynamicTo<HTMLElement>(parent_node);
if (!parent_html_element)
return false;
if (current_role == ax::mojom::blink::Role::kListItem)
return IsListElement(parent_node);
if (current_role == ax::mojom::blink::Role::kListMarker)
return IsA<HTMLLIElement>(*parent_node);
if (!current_element)
return false;
if (IsA<HTMLTableCellElement>(*current_element))
return IsA<HTMLTableRowElement>(*parent_node);
if (IsA<HTMLTableRowElement>(*current_element))
return IsA<HTMLTableSectionElement>(parent_html_element);
// In case of ListboxRole and its child, ListBoxOptionRole, inheritance of
// presentation role is handled in AXListBoxOption because ListBoxOption Role
// doesn't have any child.
// If it's just ignored because of presentation, we can't see any AX tree
// related to ListBoxOption.
return false;
}
const AXObject* AXNodeObject::InheritsPresentationalRoleFrom() const {
// ARIA states if an item can get focus, it should not be presentational.
if (CanSetFocusAttribute())
return nullptr;
if (IsPresentational())
return this;
// http://www.w3.org/TR/wai-aria/complete#presentation
// ARIA spec says that the user agent MUST apply an inherited role of
// presentation to any owned elements that do not have an explicit role.
if (AriaRoleAttribute() != ax::mojom::blink::Role::kUnknown)
return nullptr;
AXObject* parent = ParentObject();
if (!parent)
return nullptr;
auto* element = DynamicTo<HTMLElement>(GetNode());
if (!parent->HasInheritedPresentationalRole())
return nullptr;
// ARIA spec says that when a parent object is presentational and this object
// is a required owned element of that parent, then this object is also
// presentational.
if (IsRequiredOwnedElement(parent, RoleValue(), element))
return parent;
return nullptr;
}
// There should only be one banner/contentInfo per page. If header/footer are
// being used within an article, aside, nave, section, blockquote, details,
// fieldset, figure, td, or main, then it should not be exposed as whole
// page's banner/contentInfo.
static HashSet<QualifiedName>& GetLandmarkRolesNotAllowed() {
DEFINE_STATIC_LOCAL(HashSet<QualifiedName>, landmark_roles_not_allowed, ());
if (landmark_roles_not_allowed.IsEmpty()) {
landmark_roles_not_allowed.insert(html_names::kArticleTag);
landmark_roles_not_allowed.insert(html_names::kAsideTag);
landmark_roles_not_allowed.insert(html_names::kNavTag);
landmark_roles_not_allowed.insert(html_names::kSectionTag);
landmark_roles_not_allowed.insert(html_names::kBlockquoteTag);
landmark_roles_not_allowed.insert(html_names::kDetailsTag);
landmark_roles_not_allowed.insert(html_names::kFieldsetTag);
landmark_roles_not_allowed.insert(html_names::kFigureTag);
landmark_roles_not_allowed.insert(html_names::kTdTag);
landmark_roles_not_allowed.insert(html_names::kMainTag);
}
return landmark_roles_not_allowed;
}
bool AXNodeObject::IsDescendantOfElementType(
HashSet<QualifiedName>& tag_names) const {
if (!GetNode())
return false;
for (Element* parent = GetNode()->parentElement(); parent;
parent = parent->parentElement()) {
if (tag_names.Contains(parent->TagQName()))
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 = To<Element>(cell->parentNode());
if (!row || !row->HasTagName(html_names::kTrTag))
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::kUnknown;
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());
}
// TODO(accessibility) Needs a new name as it does check ARIA, including
// checking the @role for an iframe, and @aria-haspopup/aria-pressed via
// ButtonType().
// TODO(accessibility) This value is cached in native_role_ so it needs to
// be recached if anything it depends on change, such as IsClickable(),
// DataList(), aria-pressed, the parent's tag, role on an iframe, etc.
ax::mojom::blink::Role AXNodeObject::NativeRoleIgnoringAria() const {
if (!GetNode())
return RoleFromLayoutObject(ax::mojom::blink::Role::kUnknown);
if (GetNode()->IsLink()) {
if (IsA<HTMLImageElement>(GetNode())) {
// If the image will have area children, it is a map, otherwise an image.
// Unlike most roles, this may be recomputed again in the lifetime of
// |this| object, as children are gained or removed.
HTMLMapElement* map_element = GetMapForImage(GetLayoutObject());
// Make sure this is the primary image for this <map>. For more details on
// multiple images referring to the same map, see AddImageMapChildren().
if (map_element && map_element->ImageElement() == GetElement())
return ax::mojom::blink::Role::kImageMap;
return ax::mojom::blink::Role::kImage;
}
if (IsA<HTMLImageElement>(GetNode())) {
return children_.size() ? ax::mojom::blink::Role::kImageMap
: ax::mojom::blink::Role::kImage;
} else { // <a href> or <svg:a xlink:href>
// |HTMLAnchorElement| sets isLink only when it has kHrefAttr.
return ax::mojom::blink::Role::kLink;
}
}
if (IsA<HTMLPortalElement>(*GetNode())) {
return ax::mojom::blink::Role::kPortal;
}
if (IsA<HTMLAnchorElement>(*GetNode())) {
// We assume that an anchor element is LinkRole if it has event listeners
// even though it doesn't have kHrefAttr.
if (IsClickable())
return ax::mojom::blink::Role::kLink;
return ax::mojom::blink::Role::kAnchor;
}
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 (IsA<HTMLSlotElement>(parent))
parent = LayoutTreeBuilderTraversal::Parent(*parent);
if (parent && IsA<HTMLDetailsElement>(parent))
return ax::mojom::blink::Role::kDisclosureTriangle;
return RoleFromLayoutObject(ax::mojom::blink::Role::kUnknown);
}
// Chrome exposes both table markup and table CSS as a tables, 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())) {
const AtomicString& type = input->type();
if (input->DataList() && type != input_type_names::kColor)
return ax::mojom::blink::Role::kTextFieldWithComboBox;
if (type == input_type_names::kButton)
return ButtonRoleType();
if (type == input_type_names::kCheckbox)
return ax::mojom::blink::Role::kCheckBox;
if (type == input_type_names::kDate)
return ax::mojom::blink::Role::kDate;
if (type == input_type_names::kDatetime ||
type == input_type_names::kDatetimeLocal ||
type == input_type_names::kMonth || type == input_type_names::kWeek)
return ax::mojom::blink::Role::kDateTime;
if (type == input_type_names::kFile)
return ax::mojom::blink::Role::kButton;
if (type == input_type_names::kRadio)
return ax::mojom::blink::Role::kRadioButton;
if (type == input_type_names::kNumber)
return ax::mojom::blink::Role::kSpinButton;
if (input->IsTextButton())
return ButtonRoleType();
if (type == input_type_names::kRange)
return ax::mojom::blink::Role::kSlider;
if (type == input_type_names::kSearch)
return ax::mojom::blink::Role::kSearchBox;
if (type == input_type_names::kColor)
return ax::mojom::blink::Role::kColorWell;
if (type == input_type_names::kTime)
return ax::mojom::blink::Role::kInputTime;
if (type == input_type_names::kButton || type == input_type_names::kImage ||
type == input_type_names::kReset || type == input_type_names::kSubmit) {
return ax::mojom::blink::Role::kButton;
}
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::kPopUpButton;
}
if (auto* option = DynamicTo<HTMLOptionElement>(*GetNode())) {
HTMLSelectElement* select_element = option->OwnerSelectElement();
if (!select_element || select_element->IsMultiple())
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 RoleFromLayoutObject(ax::mojom::blink::Role::kGenericContainer);
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 (IsA<HTMLRubyElement>(*GetNode()))
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::kDescriptionListDetail;
if (GetNode()->HasTagName(html_names::kDtTag))
return ax::mojom::blink::Role::kDescriptionListTerm;
if (GetNode()->nodeName() == mathml_names::kMathTag.LocalName())
return ax::mojom::blink::Role::kMath;
if (GetNode()->HasTagName(html_names::kRpTag) ||
GetNode()->HasTagName(html_names::kRtTag))
return ax::mojom::blink::Role::kRubyAnnotation;
if (IsA<HTMLFormElement>(*GetNode()))
return ax::mojom::blink::Role::kForm;
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::kDelTag))
return ax::mojom::blink::Role::kContentDeletion;
if (GetNode()->HasTagName(html_names::kInsTag))
return ax::mojom::blink::Role::kContentInsertion;
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::kPreTag))
return ax::mojom::blink::Role::kPre;
if (GetNode()->HasTagName(html_names::kSectionTag))
return ax::mojom::blink::Role::kSection;
if (GetNode()->HasTagName(html_names::kAddressTag))
return RoleFromLayoutObject(ax::mojom::blink::Role::kGenericContainer);
if (IsA<HTMLDialogElement>(*GetNode()))
return ax::mojom::blink::Role::kDialog;
// The HTML element.
if (IsA<HTMLHtmlElement>(GetNode()))
return RoleFromLayoutObject(ax::mojom::blink::Role::kGenericContainer);
// Treat <iframe> and <frame> the same.
if (IsA<HTMLIFrameElement>(*GetNode()) || IsA<HTMLFrameElement>(*GetNode())) {
const AtomicString& aria_role =
GetAOMPropertyOrARIAAttribute(AOMStringProperty::kRole);
if (aria_role == "none" || aria_role == "presentation")
return ax::mojom::blink::Role::kIframePresentational;
return ax::mojom::blink::Role::kIframe;
}
// There should only be one banner/contentInfo per page. If header/footer are
// being used within an article or section then it should not be exposed as
// whole page's banner/contentInfo but as a generic container role.
if (GetNode()->HasTagName(html_names::kHeaderTag)) {
if (IsDescendantOfElementType(GetLandmarkRolesNotAllowed()))
return ax::mojom::blink::Role::kHeaderAsNonLandmark;
return ax::mojom::blink::Role::kHeader;
}
if (GetNode()->HasTagName(html_names::kFooterTag)) {
if (IsDescendantOfElementType(GetLandmarkRolesNotAllowed()))
return ax::mojom::blink::Role::kFooterAsNonLandmark;
return ax::mojom::blink::Role::kFooter;
}
if (GetNode()->HasTagName(html_names::kBlockquoteTag))
return ax::mojom::blink::Role::kBlockquote;
if (GetNode()->HasTagName(html_names::kCaptionTag))
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 (GetNode()->HasTagName(html_names::kTimeTag))
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 RoleFromLayoutObject(ax::mojom::blink::Role::kUnknown);
}
ax::mojom::blink::Role AXNodeObject::DetermineAccessibilityRole() {
if (!GetNode()) {
NOTREACHED();
return ax::mojom::blink::Role::kUnknown;
}
native_role_ = NativeRoleIgnoringAria();
if ((aria_role_ = DetermineAriaRoleAttribute()) !=
ax::mojom::blink::Role::kUnknown)
return aria_role_;
if (GetNode()->IsTextNode())
return ax::mojom::blink::Role::kStaticText;
return native_role_ == ax::mojom::blink::Role::kUnknown
? ax::mojom::blink::Role::kGenericContainer
: native_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.GetOrCreate(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);
}
}
}
bool AXNodeObject::IsMultiline() const {
Node* node = this->GetNode();
if (!node)
return false;
const ax::mojom::blink::Role role = RoleValue();
const bool is_edit_box = role == ax::mojom::blink::Role::kSearchBox ||
role == ax::mojom::blink::Role::kTextField;
if (!IsEditable() && !is_edit_box)
return false; // Doesn't support multiline.
// Supports aria-multiline, so check for attribute.
bool is_multiline = false;
if (HasAOMPropertyOrARIAAttribute(AOMBooleanProperty::kMultiline,
is_multiline)) {
return is_multiline;
}
// Default for <textarea> is true.
if (IsA<HTMLTextAreaElement>(*node))
return true;
// Default for other edit boxes is false, including for ARIA, says CORE-AAM.
if (is_edit_box)
return false;
// If root of contenteditable area and no ARIA role of textbox/searchbox used,
// default to multiline=true which is what the default behavior is.
return HasContentEditableAttributeSet();
}
// This only returns true if this is the element that actually has the
// contentEditable attribute set, unlike node->hasEditableStyle() which will
// also return true if an ancestor is editable.
bool AXNodeObject::HasContentEditableAttributeSet() const {
const AtomicString& content_editable_value =
GetAttribute(html_names::kContenteditableAttr);
if (content_editable_value.IsNull())
return false;
// Both "true" (case-insensitive) and the empty string count as true.
return content_editable_value.IsEmpty() ||
EqualIgnoringASCIICase(content_editable_value, "true");
}
bool AXNodeObject::IsTextControl() const {
if (!GetNode())
return false;
if (IsNativeTextControl() || HasContentEditableAttributeSet() ||
IsARIATextControl()) {
return true;
}
return false;
}
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());
}
Element* AXNodeObject::MouseButtonListener() const {
Node* node = this->GetNode();
if (!node)
return nullptr;
auto* element = DynamicTo<Element>(node);
if (!element)
node = node->parentElement();
if (!node)
return nullptr;
for (element = To<Element>(node); element;
element = element->parentElement()) {
if (element->HasAnyEventListeners(event_util::MouseButtonEventTypes()))
return element;
}
return nullptr;
}
void AXNodeObject::Init(AXObject* parent_if_known) {
#if DCHECK_IS_ON()
DCHECK(!initialized_);
initialized_ = true;
#endif
AXObject::Init(parent_if_known);
DCHECK(node_ ||
(GetLayoutObject() &&
AXObjectCacheImpl::IsPseudoElementDescendant(*GetLayoutObject())))
<< "Nodeless AXNodeObject can only exist inside a pseudo element: "
<< GetLayoutObject();
}
void AXNodeObject::Detach() {
#if DCHECK_IS_ON()
DCHECK(!is_adding_children_) << "Cannot Detach |this| during AddChildren()";
#endif
AXObject::Detach();
node_ = nullptr;
}
bool AXNodeObject::IsAXNodeObject() const {
return true;
}
bool AXNodeObject::IsControl() const {
Node* node = this->GetNode();
if (!node)
return false;
auto* element = DynamicTo<Element>(node);
return ((element && element->IsFormControlElement()) ||
AXObject::IsARIAControl(AriaRoleAttribute()));
}
bool AXNodeObject::IsControllingVideoElement() const {
Node* node = this->GetNode();
if (!node)
return true;
return IsA<HTMLVideoElement>(
MediaControlElementsHelper::ToParentMediaElement(node));
}
bool AXNodeObject::IsAutofillAvailable() const {
// Autofill state is stored in AXObjectCache.
WebAXAutofillState state = AXObjectCache().GetAutofillState(AXObjectID());
return state == WebAXAutofillState::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::ComputeIsEditableRoot() const {
Node* node = GetNode();
if (!node)
return false;
if (IsNativeTextControl())
return true;
if (IsRootEditableElement(*node)) {
// Editable roots created by the user agent are handled by
// |IsNativeTextControl| above.
ShadowRoot* root = node->ContainingShadowRoot();
return !root || !root->IsUserAgent();
}
return false;
}
bool AXNodeObject::IsFieldset() const {
return IsA<HTMLFieldSetElement>(GetNode());
}
bool AXNodeObject::IsHovered() const {
if (Node* node = this->GetNode())
return node->IsHovered();
return false;
}
bool AXNodeObject::IsImageButton() const {
return IsNativeImage() && IsButton();
}
bool AXNodeObject::IsInputImage() const {
auto* html_input_element = DynamicTo<HTMLInputElement>(this->GetNode());
if (html_input_element && RoleValue() == ax::mojom::blink::Role::kButton)
return html_input_element->type() == input_type_names::kImage;
return false;
}
// It is not easily possible to find out if an element is the target of an
// in-page link.
// As a workaround, we check if the element is a sectioning element with an ID,
// or an anchor with a name.
bool AXNodeObject::IsInPageLinkTarget() const {
auto* element = DynamicTo<Element>(node_.Get());
if (!element)
return false;
// We exclude elements that are in the shadow DOM.
if (element->ContainingShadowRoot())
return false;
if (auto* anchor = DynamicTo<HTMLAnchorElement>(element)) {
return anchor->HasName() || anchor->HasID();
}
if (element->HasID() &&
(IsLandmarkRelated() || IsA<HTMLSpanElement>(element) ||
IsA<HTMLDivElement>(element))) {
return true;
}
return false;
}
bool AXNodeObject::IsLoaded() const {
if (!GetDocument())
return false;
return !GetDocument()->Parser();
}
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 = this->GetNode();
if (!node)
return false;
if (IsA<HTMLImageElement>(*node) || IsA<HTMLPlugInElement>(*node))
return true;
if (const auto* input = DynamicTo<HTMLInputElement>(*node))
return input->type() == input_type_names::kImage;
return false;
}
bool AXNodeObject::IsNativeTextControl() const {
Node* node = this->GetNode();
if (!node)
return false;
if (IsA<HTMLTextAreaElement>(*node))
return true;
if (const auto* input = DynamicTo<HTMLInputElement>(*node))
return input->IsTextField();
return false;
}
bool AXNodeObject::IsNonNativeTextControl() const {
if (IsNativeTextControl())
return false;
if (HasContentEditableAttributeSet())
return true;
if (IsARIATextControl())
return true;
return false;
}
bool AXNodeObject::IsOffScreen() const {
if (IsDetached())
return false;
DCHECK(GetNode());
return DisplayLockUtilities::NearestLockedExclusiveAncestor(*GetNode());
}
bool AXNodeObject::IsPasswordField() const {
auto* html_input_element = DynamicTo<HTMLInputElement>(this->GetNode());
if (!html_input_element)
return false;
ax::mojom::blink::Role aria_role = AriaRoleAttribute();
if (aria_role != ax::mojom::blink::Role::kTextField &&
aria_role != ax::mojom::blink::Role::kUnknown)
return false;
return html_input_element->type() == input_type_names::kPassword;
}
bool AXNodeObject::IsProgressIndicator() const {
return RoleValue() == ax::mojom::blink::Role::kProgressIndicator;
}
bool AXNodeObject::IsRichlyEditable() const {
return HasContentEditableAttributeSet();
}
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->type() == input_type_names::kRange;
return false;
}
bool AXNodeObject::IsNativeSpinButton() const {
if (const auto* input = DynamicTo<HTMLInputElement>(GetNode()))
return input->type() == input_type_names::kNumber;
return false;
}
bool AXNodeObject::IsClickable() const {
Node* node = GetNode();
if (!node)
return false;
auto* element = DynamicTo<Element>(node);
if (element && element->IsDisabledFormControl()) {
return false;
}
// Note: we can't call |node->WillRespondToMouseClickEvents()| because that
// triggers a style recalc and can delete this.
if (node->HasAnyEventListeners(event_util::MouseButtonEventTypes()))
return true;
return IsTextControl() || AXObject::IsClickable();
}
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();
}
// aria-grabbed is deprecated in WAI-ARIA 1.1.
AccessibilityGrabbedState AXNodeObject::IsGrabbed() const {
if (!SupportsARIADragging())
return kGrabbedStateUndefined;
const AtomicString& grabbed = GetAttribute(html_names::kAriaGrabbedAttr);
return EqualIgnoringASCIICase(grabbed, "true") ? kGrabbedStateTrue
: kGrabbedStateFalse;
}
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 (!SelectionShouldFollowFocus())
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;
}
// 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 (!SelectionShouldFollowFocus())
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 kSelectedStateTrue;
// If not a single selection container, selection does not follow focus.
AXObject* container = ContainerWidget();
if (!container || container->IsMultiSelectable())
return false;
// 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;
// In single selection container and accessibility focused => true if
// aria-selected wasn't used as an override.
bool is_selected;
return !HasAOMPropertyOrARIAAttribute(AOMBooleanProperty::kSelected,
is_selected);
}
// Returns true if the node's aria-selected attribute should be set to true
// when the node is focused. This is true for only a subset of roles.
bool AXNodeObject::SelectionShouldFollowFocus() const {
switch (RoleValue()) {
case ax::mojom::blink::Role::kListBoxOption:
case ax::mojom::blink::Role::kMenuListOption:
case ax::mojom::blink::Role::kTab:
return true;
default:
break;
}
return false;
}
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().GetOrCreate(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.
bool is_disabled;
if (HasAOMPropertyOrARIAAttribute(AOMBooleanProperty::kDisabled,
is_disabled)) {
// Has aria-disabled, overrides native markup determining disabled.
if (is_disabled)
return kRestrictionDisabled;
} else if (elem->IsDisabledFormControl() ||
(CanSetFocusAttribute() && IsDescendantOfDisabledNode())) {
// No aria-disabled, but other markup says it's disabled.
return kRestrictionDisabled;
}
// 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;
}
// 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;
}
// 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;
if (RoleValue() == ax::mojom::blink::Role::kPopUpButton && GetNode() &&
IsA<HTMLSelectElement>(*GetNode())) {
return To<HTMLSelectElement>(GetNode())->PopupIsVisible()
? kExpandedExpanded
: kExpandedCollapsed;
}
if (GetNode() && IsA<HTMLSummaryElement>(*GetNode())) {
if (GetNode()->parentNode() &&
IsA<HTMLDetailsElement>(GetNode()->parentNode())) {
return To<Element>(GetNode()->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 = this->GetNode();
return IsA<HTMLCanvasElement>(node) && node->hasChildren();
}
int AXNodeObject::HeadingLevel() const {
// headings can be in block flow and non-block flow
Node* node = this->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 && level <= 9)
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;
}
return 0;
}
String AXNodeObject::AutoComplete() const {
// Check cache for auto complete state.
if (AXObjectCache().GetAutofillState(AXObjectID()) ==
WebAXAutofillState::kAutocompleteAvailable)
return "list";
if (IsNativeTextControl() || IsARIATextControl()) {
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> marker_starts;
std::vector<int32_t> marker_ends;
// First use ARIA markers for spelling/grammar if available.
base::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);
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;
}
marker_types.push_back(ToAXMarkerType(marker->GetType()));
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::kMarkerStarts, marker_starts);
node_data->AddIntListAttribute(
ax::mojom::blink::IntListAttribute::kMarkerEnds, marker_ends);
}
AXObject* AXNodeObject::InPageLinkTarget() const {
if (!IsAnchor() || !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);
if (!target)
return AXObject::InPageLinkTarget();
// If the target is not in the accessibility tree, get the first unignored
// sibling.
return AXObjectCache().FirstAccessibleObjectFromNode(target);
}
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();
}
}
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().GetOrCreate(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 = ParentObject();
if (parent && parent->RoleValue() == ax::mojom::blink::Role::kRadioGroup) {
for (AXObject* child : parent->ChildrenIncludingIgnored()) {
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->type() != input_type_names::kRadio)
return all_radio_buttons;
constexpr bool kTraverseForward = true;
constexpr bool kTraverseBackward = false;
HTMLInputElement* first_radio_button = radio_button;
do {
radio_button = RadioInputType::NextRadioButtonInGroup(first_radio_button,
kTraverseBackward);
if (radio_button)
first_radio_button = radio_button;
} while (radio_button);
HTMLInputElement* next_radio_button = first_radio_button;
do {
all_radio_buttons.push_back(next_radio_button);
next_radio_button = RadioInputType::NextRadioButtonInGroup(
next_radio_button, kTraverseForward);
} while (next_radio_button);
return all_radio_buttons;
}
String AXNodeObject::GetText() const {
if (!IsTextControl())
return String();
Node* node = this->GetNode();
if (!node)
return String();
if (IsNativeTextControl() &&
(IsA<HTMLTextAreaElement>(*node) || IsA<HTMLInputElement>(*node))) {
// We should not simply return the "value" attribute because it might be
// sanitized in some input control types, e.g. email fields. If we do that,
// then "selectionStart" and "selectionEnd" indices will not match with the
// text in the sanitized value.
return ToTextControl(*node).InnerEditorValue();
}
auto* element = DynamicTo<Element>(node);
return element ? element->GetInnerTextWithoutUpdate() : String();
}
ax::mojom::blink::WritingDirection AXNodeObject::GetTextDirection() const {
if (!GetLayoutObject())
return AXObject::GetTextDirection();
const ComputedStyle* style = GetLayoutObject()->Style();
if (!style)
return AXObject::GetTextDirection();
if (style->IsHorizontalWritingMode()) {
switch (style->Direction()) {
case TextDirection::kLtr:
return ax::mojom::blink::WritingDirection::kLtr;
case TextDirection::kRtl:
return ax::mojom::blink::WritingDirection::kRtl;
}
} else {
switch (style->Direction()) {
case TextDirection::kLtr:
return ax::mojom::blink::WritingDirection::kTtb;
case TextDirection::kRtl:
return ax::mojom::blink::WritingDirection::kBtt;
}
}
return AXNodeObject::GetTextDirection();
}
ax::mojom::blink::TextPosition AXNodeObject::GetTextPosition() const {
if (!GetLayoutObject())
return AXObject::GetTextPosition();
const ComputedStyle* style = GetLayoutObject()->Style();
if (!style)
return AXObject::GetTextPosition();
switch (style->VerticalAlign()) {
case EVerticalAlign::kBaseline:
case EVerticalAlign::kMiddle:
case EVerticalAlign::kTextTop:
case EVerticalAlign::kTextBottom:
case EVerticalAlign::kTop:
case EVerticalAlign::kBottom:
case EVerticalAlign::kBaselineMiddle:
case EVerticalAlign::kLength:
return AXObject::GetTextPosition();
case EVerticalAlign::kSub:
return ax::mojom::blink::TextPosition::kSubscript;
case EVerticalAlign::kSuper:
return ax::mojom::blink::TextPosition::kSuperscript;
}
}
void AXNodeObject::GetTextStyleAndTextDecorationStyle(
int32_t* text_style,
ax::mojom::blink::TextDecorationStyle* text_overline_style,
ax::mojom::blink::TextDecorationStyle* text_strikethrough_style,
ax::mojom::blink::TextDecorationStyle* text_underline_style) const {
if (!GetLayoutObject()) {
AXObject::GetTextStyleAndTextDecorationStyle(
text_style, text_overline_style, text_strikethrough_style,
text_underline_style);
return;
}
const ComputedStyle* style = GetLayoutObject()->Style();
if (!style) {
AXObject::GetTextStyleAndTextDecorationStyle(
text_style, text_overline_style, text_strikethrough_style,
text_underline_style);
return;
}
*text_style = 0;
*text_overline_style = ax::mojom::blink::TextDecorationStyle::kNone;
*text_strikethrough_style = ax::mojom::blink::TextDecorationStyle::kNone;
*text_underline_style = ax::mojom::blink::TextDecorationStyle::kNone;
if (style->GetFontWeight() == BoldWeightValue())
*text_style |= TextStyleFlag(ax::mojom::blink::TextStyle::kBold);
if (style->GetFontDescription().Style() == ItalicSlopeValue())
*text_style |= TextStyleFlag(ax::mojom::blink::TextStyle::kItalic);
for (const auto& decoration : style->AppliedTextDecorations()) {
if (EnumHasFlags(decoration.Lines(), TextDecoration::kOverline)) {
*text_style |= TextStyleFlag(ax::mojom::blink::TextStyle::kOverline);
*text_overline_style =
TextDecorationStyleToAXTextDecorationStyle(decoration.Style());
}
if (EnumHasFlags(decoration.Lines(), TextDecoration::kLineThrough)) {
*text_style |= TextStyleFlag(ax::mojom::blink::TextStyle::kLineThrough);
*text_strikethrough_style =
TextDecorationStyleToAXTextDecorationStyle(decoration.Style());
}
if (EnumHasFlags(decoration.Lines(), TextDecoration::kUnderline)) {
*text_style |= TextStyleFlag(ax::mojom::blink::TextStyle::kUnderline);
*text_underline_style =
TextDecorationStyleToAXTextDecorationStyle(decoration.Style());
}
}
}
ax::mojom::blink::TextAlign AXNodeObject::GetTextAlign() const {
// Object attributes are not applied to text objects.
if (IsTextObject() || !GetLayoutObject())
return ax::mojom::blink::TextAlign::kNone;
const ComputedStyle* style = GetLayoutObject()->Style();
if (!style)
return ax::mojom::blink::TextAlign::kNone;
switch (style->GetTextAlign()) {
case ETextAlign::kLeft:
case ETextAlign::kWebkitLeft:
case ETextAlign::kStart:
return ax::mojom::blink::TextAlign::kLeft;
case ETextAlign::kRight:
case ETextAlign::kWebkitRight:
case ETextAlign::kEnd:
return ax::mojom::blink::TextAlign::kRight;
case ETextAlign::kCenter:
case ETextAlign::kWebkitCenter:
return ax::mojom::blink::TextAlign::kCenter;
case ETextAlign::kJustify:
return ax::mojom::blink::TextAlign::kJustify;
}
}
float AXNodeObject::GetTextIndent() const {
// Text-indent applies to lines or blocks, but not text.
if (IsTextObject() || !GetLayoutObject())
return 0.0f;
const ComputedStyle* style = GetLayoutObject()->Style();
if (!style)
return 0.0f;
const blink::LayoutBlock* layout_block =
GetLayoutObject()->InclusiveContainingBlock();
if (!layout_block)
return 0.0f;
float text_indent = layout_block->TextIndentOffset().ToFloat();
return text_indent / kCssPixelsPerMillimeter;
}
String AXNodeObject::ImageDataUrl(const IntSize& max_size) const {
Node* node = GetNode();
if (!node)
return String();
ImageBitmapOptions* options = ImageBitmapOptions::Create();
ImageBitmap* image_bitmap = nullptr;
if (auto* image = DynamicTo<HTMLImageElement>(node)) {
image_bitmap = MakeGarbageCollected<ImageBitmap>(
image, base::Optional<IntRect>(), options);
} else if (auto* canvas = DynamicTo<HTMLCanvasElement>(node)) {
image_bitmap = MakeGarbageCollected<ImageBitmap>(
canvas, base::Optional<IntRect>(), options);
} else if (auto* video = DynamicTo<HTMLVideoElement>(node)) {
image_bitmap = MakeGarbageCollected<ImageBitmap>(
video, base::Optional<IntRect>(), options);
}
if (!image_bitmap)
return String();
scoped_refptr<StaticBitmapImage> bitmap_image = image_bitmap->BitmapImage();
if (!bitmap_image)
return String();
sk_sp<SkImage> image =
bitmap_image->PaintImageForCurrentFrame().GetSwSkImage();
if (!image || image->width() <= 0 || image->height() <= 0)
return String();
// Determine the width and height of the output image, using a proportional
// scale factor such that it's no larger than |maxSize|, if |maxSize| is not
// empty. It only resizes the image to be smaller (if necessary), not
// larger.
float x_scale =
max_size.Width() ? max_size.Width() * 1.0 / image->width() : 1.0;
float y_scale =
max_size.Height() ? max_size.Height() * 1.0 / image->height() : 1.0;
float scale = std::min(x_scale, y_scale);
if (scale >= 1.0)
scale = 1.0;
int width = std::round(image->width() * scale);
int height = std::round(image->height() * scale);
// Draw the image into a bitmap in native format.
SkBitmap bitmap;
SkPixmap unscaled_pixmap;
if (scale == 1.0 && image->peekPixels(&unscaled_pixmap)) {
bitmap.installPixels(unscaled_pixmap);
} else {
bitmap.allocPixels(
SkImageInfo::MakeN32(width, height, kPremul_SkAlphaType));
SkCanvas canvas(bitmap, SkSurfaceProps{});
canvas.clear(SK_ColorTRANSPARENT);
canvas.drawImageRect(image, SkRect::MakeIWH(width, height),
SkSamplingOptions());
}
// Copy the bits into a buffer in RGBA_8888 unpremultiplied format
// for encoding.
SkImageInfo info = SkImageInfo::Make(width, height, kRGBA_8888_SkColorType,
kUnpremul_SkAlphaType);
size_t row_bytes = info.minRowBytes();
Vector<char> pixel_storage(
SafeCast<wtf_size_t>(info.computeByteSize(row_bytes)));
SkPixmap pixmap(info, pixel_storage.data(), row_bytes);
if (!SkImage::MakeFromBitmap(bitmap)->readPixels(pixmap, 0, 0))
return String();
// Encode as a PNG and return as a data url.
std::unique_ptr<ImageDataBuffer> buffer = ImageDataBuffer::Create(pixmap);
if (!buffer)
return String();
return buffer->ToDataURL(kMimeTypePng, 1.0);
}
const AtomicString& AXNodeObject::AccessKey() const {
auto* element = DynamicTo<Element>(GetNode());
if (!element)
return g_null_atom;
return element->FastGetAttribute(html_names::kAccesskeyAttr);
}
int AXNodeObject::TextLength() const {
if (!IsTextControl())
return -1;
return GetText().length();
}
RGBA32 AXNodeObject::ColorValue() const {
auto* input = DynamicTo<HTMLInputElement>(GetNode());
if (!input || !IsColorWell())
return AXObject::ColorValue();
const AtomicString& type = input->getAttribute(kTypeAttr);
if (!EqualIgnoringASCIICase(type, "color"))
return AXObject::ColorValue();
// HTMLInputElement::value always returns a string parseable by Color.
Color color;
bool success = color.SetFromString(input->value());
DCHECK(success);
return color.Rgb();
}
RGBA32 AXNodeObject::BackgroundColor() const {
LayoutObject* layout_object = GetLayoutObject();
if (!layout_object)
return Color::kTransparent;
if (IsWebArea()) {
LocalFrameView* view = DocumentFrameView();
if (view)
return view->BaseBackgroundColor().Rgb();
else
return Color::kWhite;
}
const ComputedStyle* style = layout_object->Style();
if (!style || !style->HasBackground())
return Color::kTransparent;
return style->VisitedDependentColor(GetCSSPropertyBackgroundColor()).Rgb();
}
RGBA32 AXNodeObject::GetColor() const {
if (!GetLayoutObject() || IsColorWell())
return AXObject::GetColor();
const ComputedStyle* style = GetLayoutObject()->Style();
if (!style)
return AXObject::GetColor();
Color color = style->VisitedDependentColor(GetCSSPropertyColor());
return color.Rgb();
}
String AXNodeObject::FontFamily() const {
if (!GetLayoutObject())
return AXObject::FontFamily();
const ComputedStyle* style = GetLayoutObject()->Style();
if (!style)
return AXObject::FontFamily();
const SimpleFontData* primary_font = style->GetFont().PrimaryFont();
if (!primary_font)
return AXObject::FontFamily();
return primary_font->PlatformData().FontFamilyName();
}
// Font size is in pixels.
float AXNodeObject::FontSize() const {
if (!GetLayoutObject())
return AXObject::FontSize();
const ComputedStyle* style = GetLayoutObject()->Style();
if (!style)
return AXObject::FontSize();
return style->ComputedFontSize();
}
float AXNodeObject::FontWeight() const {
if (!GetLayoutObject())
return AXObject::FontWeight();
const ComputedStyle* style = GetLayoutObject()->Style();
if (!style)
return AXObject::FontWeight();
return style->GetFontWeight();
}
ax::mojom::blink::AriaCurrentState AXNodeObject::GetAriaCurrentState() const {
const AtomicString& attribute_value =
GetAOMPropertyOrARIAAttribute(AOMStringProperty::kCurrent);
if (attribute_value.IsNull())
return ax::mojom::blink::AriaCurrentState::kNone;
if (attribute_value.IsEmpty() ||
EqualIgnoringASCIICase(attribute_value, "false"))
return ax::mojom::blink::AriaCurrentState::kFalse;
if (EqualIgnoringASCIICase(attribute_value, "true"))
return ax::mojom::blink::AriaCurrentState::kTrue;
if (EqualIgnoringASCIICase(attribute_value, "page"))
return ax::mojom::blink::AriaCurrentState::kPage;
if (EqualIgnoringASCIICase(attribute_value, "step"))
return ax::mojom::blink::AriaCurrentState::kStep;
if (EqualIgnoringASCIICase(attribute_value, "location"))
return ax::mojom::blink::AriaCurrentState::kLocation;
if (EqualIgnoringASCIICase(attribute_value, "date"))
return ax::mojom::blink::AriaCurrentState::kDate;
if (EqualIgnoringASCIICase(attribute_value, "time"))
return ax::mojom::blink::AriaCurrentState::kTime;
// An unknown value should return true.
if (!attribute_value.IsEmpty())
return ax::mojom::blink::AriaCurrentState::kTrue;
return AXObject::GetAriaCurrentState();
}
ax::mojom::blink::InvalidState AXNodeObject::GetInvalidState() const {
const AtomicString& attribute_value =
GetAOMPropertyOrARIAAttribute(AOMStringProperty::kInvalid);
if (EqualIgnoringASCIICase(attribute_value, "false"))
return ax::mojom::blink::InvalidState::kFalse;
if (EqualIgnoringASCIICase(attribute_value, "true"))
return ax::mojom::blink::InvalidState::kTrue;
// "spelling" and "grammar" are also invalid values: they are exposed via
// Markers() as if they are native errors, but also use the invalid entry
// state on the node itself, therefore they are treated like "true".
// in terms of the node's invalid state
// A yet unknown value.
if (!attribute_value.IsEmpty())
return ax::mojom::blink::InvalidState::kOther;
if (GetElement()) {
ListedElement* form_control = ListedElement::From(*GetElement());
if (form_control) {
if (form_control->IsNotCandidateOrValid())
return ax::mojom::blink::InvalidState::kFalse;
else
return ax::mojom::blink::InvalidState::kTrue;
}
}
return AXObject::GetInvalidState();
}
int AXNodeObject::PosInSet() const {
if (RoleValue() == ax::mojom::blink::Role::kPopUpButton && GetNode() &&
!AXObjectCache().UseAXMenuList()) {
if (auto* select_element = DynamicTo<HTMLSelectElement>(*GetNode()))
return 1 + select_element->selectedIndex();
}
if (SupportsARIASetSizeAndPosInSet()) {
uint32_t pos_in_set;
if (HasAOMPropertyOrARIAAttribute(AOMUIntProperty::kPosInSet, pos_in_set))
return pos_in_set;
}
return 0;
}
int AXNodeObject::SetSize() const {
if (RoleValue() == ax::mojom::blink::Role::kPopUpButton && GetNode() &&
!AXObjectCache().UseAXMenuList()) {
if (auto* select_element = DynamicTo<HTMLSelectElement>(*GetNode()))
return static_cast<int>(select_element->length());
}
if (SupportsARIASetSizeAndPosInSet()) {
int32_t set_size;
if (HasAOMPropertyOrARIAAttribute(AOMIntProperty::kSetSize, set_size))
return set_size;
}
return 0;
}
String AXNodeObject::AriaInvalidValue() const {
if (GetInvalidState() == ax::mojom::blink::InvalidState::kOther)
return GetAOMPropertyOrARIAAttribute(AOMStringProperty::kInvalid);
return String();
}
String AXNodeObject::ValueDescription() const {
if (!IsRangeValueSupported())
return String();
return GetAOMPropertyOrARIAAttribute(AOMStringProperty::kValueText)
.GetString();
}
bool AXNodeObject::ValueForRange(float* out_value) const {
float value_now;
if (HasAOMPropertyOrARIAAttribute(AOMFloatProperty::kValueNow, value_now)) {
*out_value = value_now;
return true;
}
if (IsNativeSlider() || IsNativeSpinButton()) {
*out_value = To<HTMLInputElement>(*GetNode()).valueAsNumber();
return std::isfinite(*out_value);
}
if (auto* meter = DynamicTo<HTMLMeterElement>(GetNode())) {
*out_value = meter->value();
return true;
}
// In ARIA 1.1, default values for aria-valuenow were changed as below.
// - scrollbar, slider : half way between aria-valuemin and aria-valuemax
// - separator : 50
// - spinbutton : 0
switch (AriaRoleAttribute()) {
case ax::mojom::blink::Role::kScrollBar:
case ax::mojom::blink::Role::kSlider: {
float min_value, max_value;
if (MinValueForRange(&min_value) && MaxValueForRange(&max_value)) {
*out_value = (min_value + max_value) / 2.0f;
return true;
}
FALLTHROUGH;
}
case ax::mojom::blink::Role::kSplitter: {
*out_value = 50.0f;
return true;
}
case ax::mojom::blink::Role::kSpinButton: {
*out_value = 0.0f;
return true;
}
default:
break;
}
return false;
}
bool AXNodeObject::MaxValueForRange(float* out_value) const {
float value_max;
if (HasAOMPropertyOrARIAAttribute(AOMFloatProperty::kValueMax, value_max)) {
*out_value = value_max;
return true;
}
if (IsNativeSlider() || IsNativeSpinButton()) {
*out_value = static_cast<float>(To<HTMLInputElement>(*GetNode()).Maximum());
return std::isfinite(*out_value);
}
if (auto* meter = DynamicTo<HTMLMeterElement>(GetNode())) {
*out_value = meter->max();
return true;
}
// In ARIA 1.1, default value of scrollbar, separator and slider
// for aria-valuemax were changed to 100.
switch (AriaRoleAttribute()) {
case ax::mojom::blink::Role::kScrollBar:
case ax::mojom::blink::Role::kSplitter:
case ax::mojom::blink::Role::kSlider: {
*out_value = 100.0f;
return true;
}
default:
break;
}
return false;
}
bool AXNodeObject::MinValueForRange(float* out_value) const {
float value_min;
if (HasAOMPropertyOrARIAAttribute(AOMFloatProperty::kValueMin, value_min)) {
*out_value = value_min;
return true;
}
if (IsNativeSlider() || IsNativeSpinButton()) {
*out_value = static_cast<float>(To<HTMLInputElement>(*GetNode()).Minimum());
return std::isfinite(*out_value);
}
if (auto* meter = DynamicTo<HTMLMeterElement>(GetNode())) {
*out_value = meter->min();
return true;
}
// In ARIA 1.1, default value of scrollbar, separator and slider
// for aria-valuemin were changed to 0.
switch (AriaRoleAttribute()) {
case ax::mojom::blink::Role::kScrollBar:
case ax::mojom::blink::Role::kSplitter:
case ax::mojom::blink::Role::kSlider: {
*out_value = 0.0f;
return true;
}
default:
break;
}
return false;
}
bool AXNodeObject::StepValueForRange(float* out_value) const {
if (IsNativeSlider() || IsNativeSpinButton()) {
// AT may want to know whether a step value was explicitly provided or not,
// so return false if there was not one set.
if (!To<HTMLInputElement>(*GetNode())
.FastGetAttribute(html_names::kStepAttr)) {
*out_value = 0.0f;
return false;
}
auto step =
To<HTMLInputElement>(*GetNode()).CreateStepRange(kRejectAny).Step();
*out_value = step.ToString().ToFloat();
return std::isfinite(*out_value);
}
switch (AriaRoleAttribute()) {
case ax::mojom::blink::Role::kScrollBar:
case ax::mojom::blink::Role::kSplitter:
case ax::mojom::blink::Role::kSlider: {
*out_value = 0.0f;
return true;
}
default:
break;
}
return false;
}
KURL AXNodeObject::Url() const {
if (IsAnchor()) {
const Element* anchor = AnchorElement();
if (const auto* html_anchor = DynamicTo<HTMLAnchorElement>(anchor)) {
return html_anchor->Href();
}
// Some non-HTML elements, most notably SVG <a> elements, can act as
// links/anchors.
if (anchor)
return anchor->HrefURL();
}
if (IsWebArea() && GetDocument())
return GetDocument()->Url();
auto* html_image_element = DynamicTo<HTMLImageElement>(GetNode());
if (IsImage() && html_image_element) {
// Using ImageSourceURL handles both src and srcset.
String source_url = html_image_element->ImageSourceURL();
String stripped_image_source_url =
StripLeadingAndTrailingHTMLSpaces(source_url);
if (!stripped_image_source_url.IsEmpty())
return GetDocument()->CompleteURL(stripped_image_source_url);
}
if (IsInputImage())
return To<HTMLInputElement>(GetNode())->Src();
return KURL();
}
AXObject* AXNodeObject::ChooserPopup() const {
// When color & date chooser popups are visible, they can be found in the tree
// as a WebArea child of the <input> control itself.
switch (native_role_) {
case ax::mojom::blink::Role::kColorWell:
case ax::mojom::blink::Role::kDate:
case ax::mojom::blink::Role::kDateTime: {
for (const auto& child : ChildrenIncludingIgnored()) {
if (child->IsWebArea())
return child;
}
return nullptr;
}
default:
return nullptr;
}
}
String AXNodeObject::StringValue() const {
Node* node = this->GetNode();
if (!node)
return String();
if (auto* select_element = DynamicTo<HTMLSelectElement>(*node)) {
int selected_index = select_element->SelectedListIndex();
const HeapVector<Member<HTMLElement>>& list_items =
select_element->GetListItems();
if (selected_index >= 0 &&
static_cast<wtf_size_t>(selected_index) < list_items.size()) {
const AtomicString& overridden_description =
list_items[selected_index]->FastGetAttribute(
html_names::kAriaLabelAttr);
if (!overridden_description.IsNull())
return overridden_description;
}
if (!select_element->IsMultiple())
return select_element->value();
return String();
}
if (IsNativeTextControl())
return GetText();
// Handle other HTML input elements that aren't text controls, like date and
// time controls, by returning their value converted to text, with the
// exception of checkboxes and radio buttons (which would return "on"), and
// buttons which will return their name.
// https://html.spec.whatwg.org/C/#dom-input-value
if (const auto* input = DynamicTo<HTMLInputElement>(node)) {
if (input->type() != input_type_names::kButton &&
input->type() != input_type_names::kCheckbox &&
input->type() != input_type_names::kImage &&
input->type() != input_type_names::kRadio &&
input->type() != input_type_names::kReset &&
input->type() != input_type_names::kSubmit) {
return input->value();
}
}
// ARIA combobox can get value from inner contents.
if (AriaRoleAttribute() == ax::mojom::blink::Role::kComboBoxMenuButton) {
AXObjectSet visited;
return TextFromDescendants(visited, false);
}
return String();
}
ax::mojom::blink::Role AXNodeObject::AriaRoleAttribute() const {
return aria_role_;
}
bool AXNodeObject::HasAriaAttribute() const {
Element* element = GetElement();
if (!element)
return false;
// Explicit ARIA role should be considered an aria attribute.
if (AriaRoleAttribute() != ax::mojom::blink::Role::kUnknown)
return true;
AttributeCollection attributes = element->AttributesWithoutUpdate();
for (const Attribute& attr : attributes) {
// Attributes cache their uppercase names.
if (attr.GetName().LocalNameUpper().StartsWith("ARIA-"))
return true;
}
return false;
}
void AXNodeObject::AriaDescribedbyElements(AXObjectVector& describedby) const {
AccessibilityChildrenFromAOMProperty(AOMRelationListProperty::kDescribedBy,
describedby);
}
void AXNodeObject::AriaOwnsElements(AXObjectVector& owns) const {
AccessibilityChildrenFromAOMProperty(AOMRelationListProperty::kOwns, owns);
}
// TODO(accessibility): Aria-dropeffect and aria-grabbed are deprecated in
// aria 1.1 Also those properties are expected to be replaced by a new feature
// in a future version of WAI-ARIA. After that we will re-implement them
// following new spec.
bool AXNodeObject::SupportsARIADragging() const {
const AtomicString& grabbed = GetAttribute(html_names::kAriaGrabbedAttr);
return EqualIgnoringASCIICase(grabbed, "true") ||
EqualIgnoringASCIICase(grabbed, "false");
}
ax::mojom::blink::Dropeffect AXNodeObject::ParseDropeffect(
String& dropeffect) const {
if (EqualIgnoringASCIICase(dropeffect, "copy"))
return ax::mojom::blink::Dropeffect::kCopy;
if (EqualIgnoringASCIICase(dropeffect, "execute"))
return ax::mojom::blink::Dropeffect::kExecute;
if (EqualIgnoringASCIICase(dropeffect, "link"))
return ax::mojom::blink::Dropeffect::kLink;
if (EqualIgnoringASCIICase(dropeffect, "move"))
return ax::mojom::blink::Dropeffect::kMove;
if (EqualIgnoringASCIICase(dropeffect, "popup"))
return ax::mojom::blink::Dropeffect::kPopup;
return ax::mojom::blink::Dropeffect::kNone;
}
void AXNodeObject::Dropeffects(
Vector<ax::mojom::blink::Dropeffect>& dropeffects) const {
if (!HasAttribute(html_names::kAriaDropeffectAttr))
return;
Vector<String> str_dropeffects;
TokenVectorFromAttribute(str_dropeffects, html_names::kAriaDropeffectAttr);
if (str_dropeffects.IsEmpty()) {
dropeffects.push_back(ax::mojom::blink::Dropeffect::kNone);
return;
}
for (auto&& str : str_dropeffects) {
dropeffects.push_back(ParseDropeffect(str));
}
}
ax::mojom::blink::HasPopup AXNodeObject::HasPopup() const {
const AtomicString& has_popup =
GetAOMPropertyOrARIAAttribute(AOMStringProperty::kHasPopUp);
if (!has_popup.IsNull()) {
if (EqualIgnoringASCIICase(has_popup, "false"))
return ax::mojom::blink::HasPopup::kFalse;
if (EqualIgnoringASCIICase(has_popup, "listbox"))
return ax::mojom::blink::HasPopup::kListbox;
if (EqualIgnoringASCIICase(has_popup, "tree"))
return ax::mojom::blink::HasPopup::kTree;
if (EqualIgnoringASCIICase(has_popup, "grid"))
return ax::mojom::blink::HasPopup::kGrid;
if (EqualIgnoringASCIICase(has_popup, "dialog"))
return ax::mojom::blink::HasPopup::kDialog;
// To provide backward compatibility with ARIA 1.0 content,
// user agents MUST treat an aria-haspopup value of true
// as equivalent to a value of menu.
if (EqualIgnoringASCIICase(has_popup, "true") ||
EqualIgnoringASCIICase(has_popup, "menu"))
return ax::mojom::blink::HasPopup::kMenu;
}
// ARIA 1.1 default value of haspopup for combobox is "listbox".
if (RoleValue() == ax::mojom::blink::Role::kComboBoxMenuButton ||
RoleValue() == ax::mojom::blink::Role::kTextFieldWithComboBox)
return ax::mojom::blink::HasPopup::kListbox;
if (AXObjectCache().GetAutofillState(AXObjectID()) !=
WebAXAutofillState::kNoSuggestions) {
return ax::mojom::blink::HasPopup::kMenu;
}
return AXObject::HasPopup();
}
// Returns the nearest block-level LayoutBlockFlow ancestor
static LayoutBlockFlow* NonInlineBlockFlow(LayoutObject* object) {
LayoutObject* current = object;
while (current) {
auto* block_flow = DynamicTo<LayoutBlockFlow>(current);
if (block_flow && !block_flow->IsAtomicInlineLevel())
return block_flow;
current = current->Parent();
}
NOTREACHED();
return nullptr;
}
// Returns true if |r1| and |r2| are both non-null, both inline, and are
// contained within the same non-inline LayoutBlockFlow.
static bool IsInSameNonInlineBlockFlow(LayoutObject* r1, LayoutObject* r2) {
if (!r1 || !r2)
return false;
if (!r1->IsInline() || !r2->IsInline())
return false;
LayoutBlockFlow* b1 = NonInlineBlockFlow(r1);
LayoutBlockFlow* b2 = NonInlineBlockFlow(r2);
return b1 && b2 && b1 == b2;
}
//
// Modify or take an action on an object.
//
bool AXNodeObject::OnNativeSetValueAction(const String& string) {
if (!GetNode() || !GetNode()->IsElementNode())
return false;
const LayoutObject* layout_object = GetLayoutObject();
if (!layout_object || !layout_object->IsBoxModelObject())
return false;
auto* html_input_element = DynamicTo<HTMLInputElement>(*GetNode());
if (html_input_element && layout_object->IsTextFieldIncludingNG()) {
html_input_element->setValue(
string, TextFieldEventBehavior::kDispatchInputAndChangeEvent);
return true;
}
if (auto* text_area_element = DynamicTo<HTMLTextAreaElement>(*GetNode())) {
DCHECK(layout_object->IsTextAreaIncludingNG());
text_area_element->setValue(
string, TextFieldEventBehavior::kDispatchInputAndChangeEvent);
return true;
}
if (HasContentEditableAttributeSet()) {
ExceptionState exception_state(v8::Isolate::GetCurrent(),
ExceptionState::kUnknownContext, nullptr,
nullptr);
To<HTMLElement>(GetNode())->setInnerText(string, exception_state);
if (exception_state.HadException()) {
exception_state.ClearException();
return false;
}
return true;
}
return false;
}
//
// New AX name calculation.
//
String AXNodeObject::GetName(ax::mojom::blink::NameFrom& name_from,
AXObjectVector* name_objects) const {
String name = AXObject::GetName(name_from, name_objects);
if (RoleValue() == ax::mojom::blink::Role::kSpinButton &&
DatetimeAncestor()) {
// Fields inside a datetime control need to merge the field name with
// the name of the <input> element.
name_objects->clear();
String input_name = DatetimeAncestor()->GetName(name_from, name_objects);
if (!input_name.IsEmpty())
return name + " " + input_name;
}
return name;
}
String AXNodeObject::TextAlternative(bool recursive,
bool in_aria_labelled_by_traversal,
AXObjectSet& visited,
ax::mojom::blink::NameFrom& name_from,
AXRelatedObjectVector* related_objects,
NameSources* name_sources) const {
// If nameSources is non-null, relatedObjects is used in filling it in, so it
// must be non-null as well.
if (name_sources)
DCHECK(related_objects);
bool found_text_alternative = false;
if (!GetNode() && !GetLayoutObject())
return String();
// Exclude offscreen objects inside a portal.
// NOTE: If an object is found to be offscreen, this also omits its children,
// which may not be offscreen in some cases.
Page* page = GetNode() ? GetNode()->GetDocument().GetPage() : nullptr;
if (page && page->InsidePortal()) {
LayoutRect bounds = GetBoundsInFrameCoordinates();
IntSize document_size =
GetNode()->GetDocument().GetLayoutView()->GetLayoutSize();
bool is_visible = bounds.Intersects(LayoutRect(IntPoint(), document_size));
if (!is_visible)
return String();
}
// Step 2E from: http://www.w3.org/TR/accname-aam-1.1 -- value from control.
// This must occur before 2C, because 2C is not applied if 2E will be:
// 2C: "If traversal of the current node is due to recursion and the current
// node is an embedded control as defined in step 2E, ignore aria-label and
// skip to rule 2E".
// Note that 2E only applies the label is for "another widget", therefore, the
// value cannot be used to label the original control, even if aria-labelledby
// points to itself. The easiest way to check this is by testing whether this
// node has already been visited.
if (recursive && !visited.Contains(this)) {
String value_for_name = GetValueContributionToName();
if (!value_for_name.IsNull())
return value_for_name;
}
// Step 2C from: http://www.w3.org/TR/accname-aam-1.1 -- aria-label.
String text_alternative = AriaTextAlternative(
recursive, in_aria_labelled_by_traversal, visited, name_from,
related_objects, name_sources, &found_text_alternative);
if (found_text_alternative && !name_sources)
return text_alternative;
// Step 2D from: http://www.w3.org/TR/accname-aam-1.1 -- native markup.
text_alternative =
NativeTextAlternative(visited, name_from, related_objects, name_sources,
&found_text_alternative);
const bool has_text_alternative =
!text_alternative.IsEmpty() ||
name_from == ax::mojom::blink::NameFrom::kAttributeExplicitlyEmpty;
if (has_text_alternative && !name_sources)
return text_alternative;
// Step 2F / 2G from: http://www.w3.org/TR/accname-aam-1.1 -- from content.
if (in_aria_labelled_by_traversal || SupportsNameFromContents(recursive)) {
Node* node = GetNode();
if (!IsA<HTMLSelectElement>(node)) { // Avoid option descendant text
name_from = ax::mojom::blink::NameFrom::kContents;
if (name_sources) {
name_sources->push_back(NameSource(found_text_alternative));
name_sources->back().type = name_from;
}
if (auto* text_node = DynamicTo<Text>(node))
text_alternative = text_node->data();
else if (IsA<HTMLBRElement>(node))
text_alternative = String("\n");
else
text_alternative = TextFromDescendants(visited, false);
if (!text_alternative.IsEmpty()) {
if (name_sources) {
found_text_alternative = true;
name_sources->back().text = text_alternative;
} else {
return text_alternative;
}
}
}
}
// Step 2H from: http://www.w3.org/TR/accname-aam-1.1
name_from = ax::mojom::blink::NameFrom::kTitle;
if (name_sources) {
name_sources->push_back(NameSource(found_text_alternative, kTitleAttr));
name_sources->back().type = name_from;
}
const AtomicString& title = GetAttribute(kTitleAttr);
if (!title.IsEmpty()) {
text_alternative = title;
name_from = ax::mojom::blink::NameFrom::kTitle;
if (name_sources) {
found_text_alternative = true;
name_sources->back().text = text_alternative;
} else {
return text_alternative;
}
}
name_from = ax::mojom::blink::NameFrom::kUninitialized;
if (name_sources && found_text_alternative) {
for (NameSource& name_source : *name_sources) {
if (!name_source.text.IsNull() && !name_source.superseded) {
name_from = name_source.type;
if (!name_source.related_objects.IsEmpty())
*related_objects = name_source.related_objects;
return name_source.text;
}
}
}
return String();
}
static bool ShouldInsertSpaceBetweenObjectsIfNeeded(
AXObject* previous,
AXObject* next,
ax::mojom::blink::NameFrom last_used_name_from,
ax::mojom::blink::NameFrom name_from) {
// If we're going between two layoutObjects that are in separate
// LayoutBoxes, add whitespace if it wasn't there already. Intuitively if
// you have <span>Hello</span><span>World</span>, those are part of the same
// LayoutBox so we should return "HelloWorld", but given
// <div>Hello</div><div>World</div> the strings are in separate boxes so we
// should return "Hello World".
if (!IsInSameNonInlineBlockFlow(next->GetLayoutObject(),
previous->GetLayoutObject()))
return true;
// Even if it is in the same inline block flow, if we are using a text
// alternative such as an ARIA label or HTML title, we should separate
// the strings. Doing so is consistent with what is stated in the AccName
// spec and with what is done in other user agents.
switch (last_used_name_from) {
case ax::mojom::blink::NameFrom::kNone:
case ax::mojom::blink::NameFrom::kUninitialized:
case ax::mojom::blink::NameFrom::kAttributeExplicitlyEmpty:
case ax::mojom::blink::NameFrom::kContents:
break;
case ax::mojom::blink::NameFrom::kAttribute:
case ax::mojom::blink::NameFrom::kCaption:
case ax::mojom::blink::NameFrom::kPlaceholder:
case ax::mojom::blink::NameFrom::kRelatedElement:
case ax::mojom::blink::NameFrom::kTitle:
case ax::mojom::blink::NameFrom::kValue:
return true;
}
switch (name_from) {
case ax::mojom::blink::NameFrom::kNone:
case ax::mojom::blink::NameFrom::kUninitialized:
case ax::mojom::blink::NameFrom::kAttributeExplicitlyEmpty:
case ax::mojom::blink::NameFrom::kContents:
break;
case ax::mojom::blink::NameFrom::kAttribute:
case ax::mojom::blink::NameFrom::kCaption:
case ax::mojom::blink::NameFrom::kPlaceholder:
case ax::mojom::blink::NameFrom::kRelatedElement:
case ax::mojom::blink::NameFrom::kTitle:
case ax::mojom::blink::NameFrom::kValue:
return true;
}
// According to the AccName spec, we need to separate controls from text nodes
// using a space.
return previous->IsControl() || next->IsControl();
}
String AXNodeObject::TextFromDescendants(AXObjectSet& visited,
bool recursive) const {
if (!CanHaveChildren() && recursive)
return String();
StringBuilder accumulated_text;
AXObject* previous = nullptr;
ax::mojom::blink::NameFrom last_used_name_from =
ax::mojom::blink::NameFrom::kUninitialized;
AXObjectVector children;
HeapVector<Member<AXObject>> owned_children;
AXObjectCache().GetAriaOwnedChildren(this, owned_children);
// TODO(aleventhal) Why isn't this just using cached children?
AXNodeObject* parent = const_cast<AXNodeObject*>(this);
for (Node* child = LayoutTreeBuilderTraversal::FirstChild(*node_); child;
child = LayoutTreeBuilderTraversal::NextSibling(*child)) {
auto* child_text_node = DynamicTo<Text>(child);
if (child_text_node &&
child_text_node->wholeText().ContainsOnlyWhitespaceOrEmpty()) {
// skip over empty text nodes
continue;
}
AXObject* child_obj = AXObjectCache().GetOrCreate(child, parent);
if (child_obj && !AXObjectCache().IsAriaOwned(child_obj))
children.push_back(child_obj);
}
for (const auto& owned_child : owned_children)
children.push_back(owned_child);
for (AXObject* child : children) {
constexpr size_t kMaxDescendantsForTextAlternativeComputation = 100;
if (visited.size() > kMaxDescendantsForTextAlternativeComputation + 1)
break; // Need to add 1 because the root naming node is in the list.
// If a child is a continuation, we should ignore attributes like
// hidden and presentational. See LAYOUT TREE WALKING ALGORITHM in
// ax_layout_object.cc for more information on continuations.
bool is_continuation = child->GetLayoutObject() &&
child->GetLayoutObject()->IsElementContinuation();
// Don't recurse into children that are explicitly hidden.
// Note that we don't call IsInertOrAriaHidden because that would return
// true if any ancestor is hidden, but we need to be able to compute the
// accessible name of object inside hidden subtrees (for example, if
// aria-labelledby points to an object that's hidden).
if (!is_continuation &&
(child->AOMPropertyOrARIAAttributeIsTrue(AOMBooleanProperty::kHidden) ||
child->IsHiddenForTextAlternativeCalculation()))
continue;
ax::mojom::blink::NameFrom child_name_from =
ax::mojom::blink::NameFrom::kUninitialized;
String result;
if (!is_continuation && child->IsPresentational()) {
result = child->TextFromDescendants(visited, true);
} else {
result =
RecursiveTextAlternative(*child, false, visited, child_name_from);
}
if (!result.IsEmpty() && previous && accumulated_text.length() &&
!IsHTMLSpace(accumulated_text[accumulated_text.length() - 1]) &&
!IsHTMLSpace(result[0])) {
if (ShouldInsertSpaceBetweenObjectsIfNeeded(
previous, child, last_used_name_from, child_name_from)) {
accumulated_text.Append(' ');
}
}
accumulated_text.Append(result);
// We keep track of all non-hidden children, even those whose content is
// not included, because all rendered children impact whether or not a
// space should be inserted between objects. Example: A label which has
// a single, nameless input surrounded by CSS-generated content should
// have a space separating the before and after content.
previous = child;
// We only keep track of the source of children whose content is included.
// Example: Three spans, the first with an aria-label, the second with no
// content, and the third whose name comes from content. There should be a
// space between the first and third because of the aria-label in the first.
if (!result.IsEmpty())
last_used_name_from = child_name_from;
}
return accumulated_text.ToString();
}
bool AXNodeObject::NameFromLabelElement() const {
// This unfortunately duplicates a bit of logic from textAlternative and
// nativeTextAlternative, but it's necessary because nameFromLabelElement
// needs to be called from computeAccessibilityIsIgnored, which isn't allowed
// to call axObjectCache->getOrCreate.
if (!GetNode() && !GetLayoutObject())
return false;
// Step 2A from: http://www.w3.org/TR/accname-aam-1.1
if (IsHiddenForTextAlternativeCalculation())
return false;
// Step 2B from: http://www.w3.org/TR/accname-aam-1.1
// Try both spellings, but prefer aria-labelledby, which is the official spec.
const QualifiedName& attr =
HasAttribute(html_names::kAriaLabeledbyAttr) &&
!HasAttribute(html_names::kAriaLabelledbyAttr)
? html_names::kAriaLabeledbyAttr
: html_names::kAriaLabelledbyAttr;
HeapVector<Member<Element>> elements_from_attribute;
Vector<String> ids;
ElementsFromAttribute(elements_from_attribute, attr, ids);
if (elements_from_attribute.size() > 0)
return false;
// Step 2C from: http://www.w3.org/TR/accname-aam-1.1
const AtomicString& aria_label =
GetAOMPropertyOrARIAAttribute(AOMStringProperty::kLabel);
if (!aria_label.IsEmpty())
return false;
// Based on
// http://rawgit.com/w3c/aria/master/html-aam/html-aam.html#accessible-name-and-description-calculation
// 5.1/5.5 Text inputs, Other labelable Elements
auto* html_element = DynamicTo<HTMLElement>(GetNode());
if (html_element && html_element->IsLabelable()) {
if (html_element->labels() && html_element->labels()->length() > 0)
return true;
}
return false;
}
void AXNodeObject::GetRelativeBounds(AXObject** out_container,
FloatRect& out_bounds_in_container,
SkMatrix44& out_container_transform,
bool* clips_children) const {
if (GetLayoutObject()) {
AXObject::GetRelativeBounds(out_container, out_bounds_in_container,
out_container_transform, clips_children);
return;
}
#if DCHECK_IS_ON()
DCHECK(!getting_bounds_) << "GetRelativeBounds reentrant: " << ToString(true);
base::AutoReset<bool> reentrancy_protector(&getting_bounds_, true);
#endif
*out_container = nullptr;
out_bounds_in_container = FloatRect();
out_container_transform.setIdentity();
// First check if it has explicit bounds, for example if this element is tied
// to a canvas path. When explicit coordinates are provided, the ID of the
// explicit container element that the coordinates are relative to must be
// provided too.
if (!explicit_element_rect_.IsEmpty()) {
*out_container = AXObjectCache().ObjectFromAXID(explicit_container_id_);
if (*out_container) {
out_bounds_in_container = FloatRect(explicit_element_rect_);
return;
}
}
Element* element = GetElement();
// If it's in a canvas but doesn't have an explicit rect, or has display:
// contents set, get the bounding rect of its children.
if ((GetNode()->parentElement() &&
GetNode()->parentElement()->IsInCanvasSubtree()) ||
(element && element->HasDisplayContentsStyle())) {
Vector<FloatRect> rects;
for (Node& child : NodeTraversal::ChildrenOf(*GetNode())) {
if (child.IsHTMLElement()) {
if (AXObject* obj = AXObjectCache().Get(&child)) {
AXObject* container;
FloatRect bounds;
obj->GetRelativeBounds(&container, bounds, out_container_transform,
clips_children);
if (container) {
*out_container = container;
rects.push_back(bounds);
}
}
}
}
if (*out_container) {
out_bounds_in_container = UnionRect(rects);
return;
}
}
// If this object doesn't have an explicit element rect or computable from its
// children, for now, let's return the position of the ancestor that does have
// a position, and make it the width of that parent, and about the height of a
// line of text, so that it's clear the object is a child of the parent.
for (AXObject* position_provider = ParentObject(); position_provider;
position_provider = position_provider->ParentObject()) {
if (IsA<AXLayoutObject>(position_provider)) {
position_provider->GetRelativeBounds(
out_container, out_bounds_in_container, out_container_transform,
clips_children);
if (*out_container)
out_bounds_in_container.SetSize(
FloatSize(out_bounds_in_container.Width(),
std::min(10.0f, out_bounds_in_container.Height())));
break;
}
}
}
bool AXNodeObject::HasValidHTMLTableStructureAndLayout() const {
// Is it a visible <table> with a table-like role and layout?
if (!IsTableLikeRole() || !GetLayoutObject() ||
!GetLayoutObject()->IsTable() || !IsA<HTMLTableElement>(GetNode()))
return false;
// Check for any invalid children, as far as W3C table validity is concerned.
// * If no invalid children exist, this will be considered a valid table,
// and AddTableChildren() can be used to add the children in rendered order.
// * If any invalid children exist, this table will be considered invalid.
// In that case the children will still be added via AddNodeChildren(),
// so that no content is lost.
// See comments in AddTableChildren() for more information about valid tables.
for (Element* child = ElementTraversal::FirstChild(*GetElement()); child;
child = ElementTraversal::NextSibling(*child)) {
if (!IsA<HTMLTableSectionElement>(child) &&
!IsA<HTMLTableCaptionElement>(child) &&
!child->HasTagName(html_names::kColgroupTag)) {
return false;
}
}
return true;
}
void AXNodeObject::AddTableChildren() {
// Add the caption (if any) and table sections in the visible order.
//
// Implementation notes:
//
// * In a valid table, there is always at least one section child DOM node.
// For example, if the HTML of the web page includes <tr>s as direct
// children of a <table>, Blink will insert a <tbody> as a child of the
// table, and parent of the <tr> elements.
//
// * Rendered order can differ from DOM order:
// The valid DOM order of <table> children is specified here:
// https://html.spec.whatwg.org/multipage/tables.html#the-table-element,
// "... optionally a caption element, followed by zero or more
// colgroup elements, followed optionally by a thead element, followed by
// either zero or more tbody elements or one or more tr elements, followed
// optionally by a tfoot element"
// However, even if the DOM children occur in an incorrect order, Blink
// automatically renders them as if they were in the correct order.
// The following code ensures that the children are added to the AX tree in
// the same order as Blink renders them.
DCHECK(HasValidHTMLTableStructureAndLayout());
auto* html_table_element = To<HTMLTableElement>(GetNode());
AddNodeChild(html_table_element->caption());
AddNodeChild(html_table_element->tHead());
for (Node* node : *html_table_element->tBodies())
AddNodeChild(node);
AddNodeChild(html_table_element->tFoot());
}
int AXNodeObject::TextOffsetInFormattingContext(int offset) const {
DCHECK_GE(offset, 0);
if (IsDetached())
return 0;
// When a node has the first-letter CSS style applied to it, it is split into
// two parts (two branches) in the layout tree. The "first-letter part"
// contains its first letter and any surrounding Punctuation. The "remaining
// part" contains the rest of the text.
//
// We need to ensure that we retrieve the correct layout object: either the
// one for the "first-letter part" or the one for the "remaining part",
// depending of the value of |offset|.
const LayoutObject* layout_obj =
GetNode() ? AssociatedLayoutObjectOf(*GetNode(), offset)
: GetLayoutObject();
if (!layout_obj)
return AXObject::TextOffsetInFormattingContext(offset);
// We support calculating the text offset from the start of the formatting
// contexts of the following layout objects, provided that they are at
// inline-level, (display=inline) or "display=inline-block":
//
// (Note that in the following examples, the paragraph is the formatting
// context.
//
// Layout replaced, e.g. <p><img></p>.
// Layout inline with a layout text child, e.g. <p><a href="#">link</a></p>.
// Layout block flow, e.g. <p><b style="display: inline-block;"></b></p>.
// Layout text, e.g. <p>Hello</p>.
// Layout br (subclass of layout text), e.g. <p><br></p>.
if (layout_obj->IsLayoutInline()) {
// The NGOffsetMapping class doesn't map layout inline objects to their text
// mappings because such an operation could be ambiguous. An inline object
// may have another inline object inside it. For example,
// <span><span>Inner</span outer</span>. We need to recursively retrieve the
// first layout text or layout replaced child so that any potential
// ambiguity would be removed.
const AXObject* first_child = FirstChildIncludingIgnored();
return first_child ? first_child->TextOffsetInFormattingContext(offset)
: offset;
}
// TODO(crbug.com/567964): LayoutObject::IsAtomicInlineLevel() also includes
// block-level replaced elements. We need to explicitly exclude them via
// LayoutObject::IsInline().
const bool is_atomic_inline_level =
layout_obj->IsInline() && layout_obj->IsAtomicInlineLevel();
if (!is_atomic_inline_level && !layout_obj->IsText()) {
// Not in a formatting context in which text offsets are meaningful.
return AXObject::TextOffsetInFormattingContext(offset);
}
// TODO(crbug.com/1149171): NGInlineOffsetMappingBuilder does not properly
// compute offset mappings for empty LayoutText objects. Other text objects
// (such as some list markers) are not affected.
if (const LayoutText* layout_text = DynamicTo<LayoutText>(layout_obj)) {
if (layout_text->GetText().IsEmpty())
return AXObject::TextOffsetInFormattingContext(offset);
}
LayoutBlockFlow* formatting_context =
NGOffsetMapping::GetInlineFormattingContextOf(*layout_obj);
if (!formatting_context || formatting_context == layout_obj)
return AXObject::TextOffsetInFormattingContext(offset);
// If "formatting_context" is not a Layout NG object, the offset mappings will
// be computed on demand and cached.
const NGOffsetMapping* inline_offset_mapping =
NGInlineNode::GetOffsetMapping(formatting_context);
if (!inline_offset_mapping)
return AXObject::TextOffsetInFormattingContext(offset);
const base::span<const NGOffsetMappingUnit> mapping_units =
inline_offset_mapping->GetMappingUnitsForLayoutObject(*layout_obj);
if (mapping_units.empty())
return AXObject::TextOffsetInFormattingContext(offset);
return int{mapping_units.front().TextContentStart()} + offset;
}
//
// Inline text boxes.
//
void AXNodeObject::LoadInlineTextBoxes() {
if (!GetLayoutObject())
return;
if (GetLayoutObject()->IsText()) {
ClearChildren();
AddInlineTextBoxChildren(true);
children_dirty_ = false; // Avoid adding these children twice.
return;
}
for (const auto& child : ChildrenIncludingIgnored()) {
child->LoadInlineTextBoxes();
}
}
void AXNodeObject::AddInlineTextBoxChildren(bool force) {
Document* document = GetDocument();
if (!document) {
NOTREACHED();
return;
}
Settings* settings = document->GetSettings();
if (!force &&
(!settings || !settings->GetInlineTextBoxAccessibilityEnabled())) {
return;
}
if (!GetLayoutObject() || !GetLayoutObject()->IsText())
return;
if (GetLayoutObject()->NeedsLayout()) {
// If a LayoutText or a LayoutBR needs layout, its inline text boxes are
// either nonexistent or invalid, so defer until the layout happens and the
// layoutObject calls AXObjectCacheImpl::inlineTextBoxesUpdated.
return;
}
if (LastKnownIsIgnoredValue()) {
// Inline textboxes 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;
}
auto* layout_text = To<LayoutText>(GetLayoutObject());
for (scoped_refptr<AbstractInlineTextBox> box =
layout_text->FirstAbstractInlineTextBox();
box.get(); box = box->NextInlineTextBox()) {
AXObject* ax_box = AXObjectCache().GetOrCreate(box.get(), this);
if (!ax_box)
continue;
children_.push_back(ax_box);
}
}
void AXNodeObject::AddValidationMessageChild() {
DCHECK(IsWebArea()) << "Validation message must be child of root";
// First child requirement enables easy checking to see if a children changed
// event is needed in AXObjectCacheImpl::ValidationMessageObjectIfInvalid().
DCHECK_EQ(children_.size(), 0U)
<< "Validation message must be the first child";
AddChildAndCheckIncluded(AXObjectCache().ValidationMessageObjectIfInvalid(
/* suppress children changed, already processing that */ false));
}
void AXNodeObject::AddImageMapChildren() {
HTMLMapElement* map = GetMapForImage(GetLayoutObject());
if (!map)
return;
HTMLImageElement* curr_image_element = DynamicTo<HTMLImageElement>(GetNode());
DCHECK(curr_image_element);
DCHECK(curr_image_element->IsLink());
String usemap = curr_image_element->FastGetAttribute(html_names::kUsemapAttr);
DCHECK(!usemap.IsEmpty());
// Even though several images can point to the same map via usemap, only
// use one reported via HTMLImageMapElement::ImageElement(), which is always
// the first image in the DOM that matches the #usemap, even if there are
// changes to the DOM. Only allow map children for the primary image.
// This avoids two problems:
// 1. Focusing the same area but in a different image scrolls the page to
// the first image that uses that map. Safari does the same thing, and
// Firefox does something similar (but seems to prefer the last image).
// 2. When an object has multiple parents, serialization errors occur.
// While allowed in the spec, using multiple images with the same map is not
// handled well in browsers (problem #1), and serializer support for multiple
// parents of the same area children is messy.
// Get the primary image, which is the first image using this map.
HTMLImageElement* primary_image_element = map->ImageElement();
DCHECK(primary_image_element);
#if DCHECK_IS_ON()
// Prove that this is the same as getting the first image using this map.
String usemap_selector = "img[usemap=\"";
usemap_selector = usemap_selector + usemap + "\"]";
Element* first_image_with_this_usemap =
GetDocument()->QuerySelector(AtomicString(usemap_selector));
DCHECK(primary_image_element) << "No match for " << usemap_selector;
DCHECK_EQ(primary_image_element, first_image_with_this_usemap);
#endif
// Is this the primary image for this map?
if (primary_image_element != curr_image_element) {
// No, the current image (for |this|) is not the primary image.
// Therefore, do not add area children to it.
AXObject* ax_primary_image =
AXObjectCache().GetOrCreate(primary_image_element);
if (ax_primary_image &&
ax_primary_image->ChildCountIncludingIgnored() == 0 &&
Traversal<HTMLAreaElement>::FirstWithin(*map)) {
// The primary image still needs to add the area children, and there's at
// least one to add. Its role also needs to change to kImageMap.
// The children change will force the role change as well.
AXObjectCache().ChildrenChanged(primary_image_element);
// The current image may change role from an image map to an image.
// Unlike many role changes, this can be done on the same object.
// There's no need to fire a role changed event or MarkDirty because the
// only time the role changes is when we're updating children anyway.
if (role_ == ax::mojom::blink::Role::kImageMap) {
ax_primary_image->ClearChildren(); // Clear the children now.
ax_primary_image->UpdateRoleForImage();
}
}
return;
}
// Yes, this is the primary image.
HTMLAreaElement* first_area = Traversal<HTMLAreaElement>::FirstWithin(*map);
if (first_area) {
// If the <area> children were part of a different parent, notify that
// parent that its children have changed.
if (AXObject* ax_preexisting = AXObjectCache().Get(first_area)) {
if (AXObject* ax_previous_parent = ax_preexisting->CachedParentObject()) {
DCHECK_NE(ax_previous_parent, this);
DCHECK(ax_previous_parent->GetNode());
AXObjectCache().ChildrenChangedWithCleanLayout(
ax_previous_parent->GetNode(), ax_previous_parent);
if (ax_previous_parent->RoleValue() ==
ax::mojom::blink::Role::kImageMap) {
ax_previous_parent->ClearChildren();
ax_previous_parent->UpdateRoleForImage();
}
}
}
// Add the area children to |this|.
for (HTMLAreaElement& area :
Traversal<HTMLAreaElement>::DescendantsOf(*map)) {
// Add an <area> element for this child if it has a link and is visible.
AddChildAndCheckIncluded(AXObjectCache().GetOrCreate(&area, this));
}
// The current image may change role from an image to an image map.
// Unlike many role changes, this can be done on the same object.
if (role_ == ax::mojom::blink::Role::kImage) {
// There's no need to fire a role changed event or MarkDirty because the
// only time the role changes is when we're updating children anyway.
UpdateRoleForImage();
}
}
}
void AXNodeObject::AddPopupChildren() {
if (!AXObjectCache().UseAXMenuList()) {
auto* html_select_element = DynamicTo<HTMLSelectElement>(GetNode());
if (html_select_element && html_select_element->UsesMenuList())
AddChildAndCheckIncluded(html_select_element->PopupRootAXObject());
return;
}
auto* html_input_element = DynamicTo<HTMLInputElement>(GetNode());
if (!html_input_element)
return;
AddChildAndCheckIncluded(html_input_element->PopupRootAXObject());
}
bool AXNodeObject::CanAddLayoutChild(LayoutObject& child) {
if (child.IsAnonymous())
return true;
// An non-anonymous layout object (has a DOM node) is only reached when a
// pseudo element is inside another pseudo element.
// This is because layout object traversal only occurs for pseudo element
// subtrees -- see AXObject::ShouldUseLayoutObjectTraversalForChildren().
// The only way a node will occur inside of that subtree is if it's another
// pseudo element.
DCHECK(child.GetNode()->IsPseudoElement());
// Only add this inner pseudo element if it hasn't been added elsewhere.
// An example is ::before with ::first-letter.
AXObject* ax_preexisting = AXObjectCache().Get(&child);
return !ax_preexisting || !ax_preexisting->CachedParentObject() ||
ax_preexisting->CachedParentObject() == this;
}
#if DCHECK_IS_ON()
#define CHECK_NO_OTHER_PARENT_FOR(child) \
AXObject* ax_preexisting = AXObjectCache().Get(child); \
DCHECK(!ax_preexisting || !ax_preexisting->CachedParentObject() || \
ax_preexisting->CachedParentObject() == this) \
<< "Newly added child can't have a different preexisting parent:" \
<< "\nChild = " << ax_preexisting->ToString(true, true) \
<< "\nNew parent = " << ToString(true, true) \
<< "\nPreexisting parent = " \
<< ax_preexisting->CachedParentObject()->ToString(true, true);
#else
#define CHECK_NO_OTHER_PARENT_FOR(child) (void(0))
#endif
void AXNodeObject::AddLayoutChildren() {
// Children are added this way only for pseudo-element subtrees.
// See AXObject::ShouldUseLayoutObjectTraversalForChildren().
DCHECK(GetLayoutObject());
LayoutObject* child = GetLayoutObject()->SlowFirstChild();
while (child) {
DCHECK(AXObjectCacheImpl::IsPseudoElementDescendant(*child));
if (CanAddLayoutChild(*child)) {
CHECK_NO_OTHER_PARENT_FOR(child);
// All added pseudo element desecendants are included in the tree.
AddChildAndCheckIncluded(AXObjectCache().GetOrCreate(child, this));
}
child = child->NextSibling();
}
}
void AXNodeObject::AddNodeChildren() {
if (!node_)
return;
for (Node* child = LayoutTreeBuilderTraversal::FirstChild(*node_); child;
child = LayoutTreeBuilderTraversal::NextSibling(*child)) {
AddNodeChild(child);
}
}
void AXNodeObject::AddAccessibleNodeChildren() {
Element* element = GetElement();
if (!element)
return;
AccessibleNode* accessible_node = element->ExistingAccessibleNode();
if (!accessible_node)
return;
for (const auto& child : accessible_node->GetChildren()) {
CHECK_NO_OTHER_PARENT_FOR(child);
AddChildAndCheckIncluded(AXObjectCache().GetOrCreate(child, this));
}
}
void AXNodeObject::AddOwnedChildren() {
AXObjectVector owned_children;
AXObjectCache().GetAriaOwnedChildren(this, owned_children);
DCHECK(owned_children.size() == 0 || AXRelationCache::IsValidOwner(this))
<< "This object is not allowed to use aria-owns, but is: "
<< ToString(true, true);
// Always include owned children.
for (const auto& owned_child : owned_children) {
DCHECK(AXRelationCache::IsValidOwnedChild(owned_child))
<< "This object is not allowed to be owned, but is: "
<< owned_child->ToString(true, true);
AddChildAndCheckIncluded(owned_child, true);
}
}
void AXNodeObject::AddChildrenImpl() {
#define CHECK_ATTACHED() \
if (IsDetached()) { \
NOTREACHED() << "Detached adding children: " << ToString(true, true); \
return; \
}
DCHECK(children_dirty_);
if (!CanHaveChildren()) {
NOTREACHED()
<< "Should not reach AddChildren() if CanHaveChildren() is false "
<< ToString(true, true);
return;
}
if (ui::CanHaveInlineTextBoxChildren(RoleValue())) {
AddInlineTextBoxChildren();
CHECK_ATTACHED();
return;
}
if (IsA<HTMLImageElement>(GetNode())) {
AddImageMapChildren();
CHECK_ATTACHED();
return;
}
// If validation message exists, always make it the first child of the root,
// to enable easy checking of whether it's a known child of the root.
if (IsWebArea())
AddValidationMessageChild();
CHECK_ATTACHED();
if (HasValidHTMLTableStructureAndLayout())
AddTableChildren();
else if (ShouldUseLayoutObjectTraversalForChildren())
AddLayoutChildren();
else
AddNodeChildren();
CHECK_ATTACHED();
AddPopupChildren();
CHECK_ATTACHED();
AddAccessibleNodeChildren();
CHECK_ATTACHED();
AddOwnedChildren();
CHECK_ATTACHED();
}
void AXNodeObject::AddChildren() {
#if DCHECK_IS_ON()
DCHECK(!IsDetached());
DCHECK(!is_adding_children_) << " Reentering method on " << GetNode();
base::AutoReset<bool> reentrancy_protector(&is_adding_children_, true);
// If the need to add more children in addition to existing children arises,
// childrenChanged should have been called, which leads to children_dirty_
// being true, then UpdateChildrenIfNecessary() clears the children before
// calling AddChildren().
DCHECK_EQ(children_.size(), 0U)
<< "\nParent still has " << children_.size() << " children before adding:"
<< "\nParent is " << ToString(true, true) << "\nFirst child is "
<< children_[0]->ToString(true, true);
#endif
AddChildrenImpl();
children_dirty_ = false;
#if DCHECK_IS_ON()
// All added children must be attached.
for (const auto& child : children_) {
DCHECK(!child->IsDetached())
<< "A brand new child was detached: " << child->ToString(true, true)
<< "\n ... of parent " << ToString(true, true);
}
#endif
}
// Add non-owned children that are backed with a DOM node.
void AXNodeObject::AddNodeChild(Node* node) {
if (!node)
return;
AXObject* ax_child = AXObjectCache().Get(node);
// Should not have another parent unless owned.
if (AXObjectCache().IsAriaOwned(ax_child))
return; // Do not add owned children to their natural parent.
#if DCHECK_IS_ON()
AXObject* ax_cached_parent =
ax_child ? ax_child->CachedParentObject() : nullptr;
size_t num_children_before_add = children_.size();
#endif
if (!ax_child) {
ax_child = AXObjectCache().GetOrCreate(node, this);
if (!ax_child)
return;
}
AddChild(ax_child);
#if DCHECK_IS_ON()
bool did_add_child = children_.size() == num_children_before_add + 1 &&
children_[0] == ax_child;
if (did_add_child) {
DCHECK(!ax_cached_parent || ax_cached_parent->AXObjectID() == AXObjectID())
<< "Newly added child shouldn't have a different preexisting parent:"
<< "\nChild = " << ax_child->ToString(true, true)
<< "\nNew parent = " << ToString(true, true)
<< "\nPreexisting parent = " << ax_cached_parent->ToString(true, true);
}
#endif
}
#if DCHECK_IS_ON()
void AXNodeObject::CheckValidChild(AXObject* child) {
DCHECK(!child->IsDetached())
<< "Cannot add a detached child: " << child->ToString(true, true);
Node* child_node = child->GetNode();
// An HTML image can only have area children.
DCHECK(!IsA<HTMLImageElement>(GetNode()) || IsA<HTMLAreaElement>(child_node))
<< "Image elements can only have area children, had "
<< child->ToString(true, true);
// <area> children should only be added via AddImageMapChildren(), as the
// children of an <image usemap>, and never alone or as children of a <map>.
DCHECK(IsA<HTMLImageElement>(GetNode()) || !IsA<HTMLAreaElement>(child_node))
<< "Area elements can only be added by image parents: "
<< child->ToString(true, true) << " had a parent of "
<< ToString(true, true);
// An option or popup for a <select size=1> must only be added via an
// overridden AddChildren() on AXMenuList/AXMenuListPopup.
// These AXObjects must be added in an overridden AddChildren() method, and
// that will only occur if AXObjectCacheImpl::UseAXMenuList() returns true.
DCHECK(!IsA<AXMenuListOption>(child))
<< "Adding menulist option child in wrong place: "
<< "\nChild: " << child->ToString(true, true)
<< "\nParent: " << child->ParentObject()->ToString(true, true)
<< "\nUseAXMenuList()=" << AXObjectCacheImpl::UseAXMenuList();
// An popup for a <select size=1> must only be added via an overridden
// AddChildren() on AXMenuList.
DCHECK(!IsA<AXMenuListPopup>(child))
<< "Adding menulist popup in wrong place: "
<< "\nChild: " << child->ToString(true, true)
<< "\nParent: " << child->ParentObject()->ToString(true, true)
<< "\nUseAXMenuList()=" << AXObjectCacheImpl::UseAXMenuList()
<< "\nShouldCreateAXMenuListOptionFor()="
<< AXObjectCacheImpl::ShouldCreateAXMenuListOptionFor(child_node);
}
#endif
void AXNodeObject::AddChild(AXObject* child, bool is_from_aria_owns) {
if (!child)
return;
#if DCHECK_IS_ON()
CheckValidChild(child);
#endif
unsigned int index = children_.size();
InsertChild(child, index, is_from_aria_owns);
}
void AXNodeObject::AddChildAndCheckIncluded(AXObject* child,
bool is_from_aria_owns) {
if (!child)
return;
DCHECK(child->AccessibilityIsIncludedInTree())
<< "A parent " << ToString(true, true) << "\n ... tried to add child "
<< child->ToString(true, true);
AddChild(child, is_from_aria_owns);
}
void AXNodeObject::InsertChild(AXObject* child,
unsigned index,
bool is_from_aria_owns) {
if (!child)
return;
DCHECK(CanHaveChildren());
DCHECK(!child->IsDetached())
<< "Cannot add a detached child: " << child->ToString(true, true);
// Enforce expected aria-owns status:
// - Don't add a non-aria-owned child when called from AddOwnedChildren().
// - Don't add an aria-owned child to its natural parent, because it will
// already be the child of the element with aria-owns.
DCHECK_EQ(AXObjectCache().IsAriaOwned(child), is_from_aria_owns);
// Set the parent:
// - For a new object it will have already been set.
// - For a reused, older object, it may need to be changed to a new parent.
child->SetParent(this);
#if DCHECK_IS_ON()
child->EnsureCorrectParentComputation();
#endif
// Update cached values preemptively, but don't allow children changed to be
// called if ignored change, we are already recomputing children and don't
// want to recurse.
child->UpdateCachedAttributeValuesIfNeeded(false);
if (!child->LastKnownIsIncludedInTreeValue()) {
DCHECK(!is_from_aria_owns)
<< "Owned elements must be in tree: " << child->ToString(true, true)
<< "\nRecompute included in tree: "
<< child->ComputeAccessibilityIsIgnoredButIncludedInTree();
// Child is ignored and not in the tree.
// Recompute the child's children now as we skip over the ignored object.
child->SetNeedsToUpdateChildren();
// Get the ignored child's children and add to children of ancestor
// included in tree. This will recurse if necessary, skipping levels of
// unignored descendants as it goes.
const auto& children = child->ChildrenIncludingIgnored();
wtf_size_t length = children.size();
int new_index = index;
for (wtf_size_t i = 0; i < length; ++i) {
// If the child was owned, it will be added elsewhere as a direct
// child of the object owning it.
if (!AXObjectCache().IsAriaOwned(children[i])) {
DCHECK(!children[i]->IsDetached()) << "Cannot add a detached child: "
<< children[i]->ToString(true, true);
children_.insert(new_index++, children[i]);
}
}
} else {
children_.insert(index, child);
}
}
bool AXNodeObject::CanHaveChildren() const {
// If this is an AXLayoutObject, then it's okay if this object
// doesn't have a node - there are some layoutObjects that don't have
// associated nodes, like scroll areas and css-generated text.
if (!GetNode() && !IsAXLayoutObject())
return false;
DCHECK(!IsA<HTMLMapElement>(GetNode()));
// Placeholder gets exposed as an attribute on the input accessibility node,
// so there's no need to add its text children. Placeholder text is a separate
// node that gets removed when it disappears, so this will only be present if
// the placeholder is visible.
if (GetElement() && GetElement()->ShadowPseudoId() ==
shadow_element_names::kPseudoInputPlaceholder) {
return false;
}
switch (native_role_) {
case ax::mojom::blink::Role::kCheckBox:
case ax::mojom::blink::Role::kListBoxOption:
case ax::mojom::blink::Role::kMenuListOption:
case ax::mojom::blink::Role::kMenuItem:
case ax::mojom::blink::Role::kMenuItemCheckBox:
case ax::mojom::blink::Role::kMenuItemRadio:
case ax::mojom::blink::Role::kProgressIndicator:
case ax::mojom::blink::Role::kRadioButton:
case ax::mojom::blink::Role::kScrollBar:
// case ax::mojom::blink::Role::kSearchBox:
case ax::mojom::blink::Role::kSlider:
case ax::mojom::blink::Role::kSplitter:
case ax::mojom::blink::Role::kSwitch:
case ax::mojom::blink::Role::kTab:
// case ax::mojom::blink::Role::kTextField:
case ax::mojom::blink::Role::kToggleButton:
return false;
case ax::mojom::blink::Role::kPopUpButton:
return true;
case ax::mojom::blink::Role::kLineBreak:
case ax::mojom::blink::Role::kStaticText:
return AXObjectCache().InlineTextBoxAccessibilityEnabled();
case ax::mojom::blink::Role::kImage:
// Can turn into an image map if gains children later.
return GetNode() && GetNode()->IsLink();
default:
break;
}
switch (AriaRoleAttribute()) {
case ax::mojom::blink::Role::kImage:
return false;
case ax::mojom::blink::Role::kCheckBox:
case ax::mojom::blink::Role::kListBoxOption:
case ax::mojom::blink::Role::kMath: // role="math" is flat, unlike <math>
case ax::mojom::blink::Role::kMenuListOption:
case ax::mojom::blink::Role::kMenuItem:
case ax::mojom::blink::Role::kMenuItemCheckBox:
case ax::mojom::blink::Role::kMenuItemRadio:
case ax::mojom::blink::Role::kPopUpButton:
case ax::mojom::blink::Role::kProgressIndicator:
case ax::mojom::blink::Role::kRadioButton:
case ax::mojom::blink::Role::kScrollBar:
case ax::mojom::blink::Role::kSlider:
case ax::mojom::blink::Role::kSplitter:
case ax::mojom::blink::Role::kSwitch:
case ax::mojom::blink::Role::kTab:
case ax::mojom::blink::Role::kToggleButton: {
// These roles have ChildrenPresentational: true in the ARIA spec.
// We used to remove/prune all descendants of them, but that removed
// useful content if the author didn't follow the spec perfectly, for
// example if they wanted a complex radio button with a textfield child.
// We are now only pruning these if there is a single text child,
// otherwise the subtree is exposed. The ChildrenPresentational rule
// is thus useful for authoring/verification tools but does not break
// complex widget implementations.
Element* element = GetElement();
return element && !element->HasOneTextChild();
}
default:
break;
}
return true;
}
//
// Properties of the object's owning document or page.
//
double AXNodeObject::EstimatedLoadingProgress() const {
if (!GetDocument())
return 0;
if (IsLoaded())
return 1.0;
if (LocalFrame* frame = GetDocument()->GetFrame())
return frame->Loader().Progress().EstimatedProgress();
return 0;
}
//
// DOM and Render tree access.
//
Element* AXNodeObject::ActionElement() const {
Node* node = this->GetNode();
if (!node)
return nullptr;
auto* element = DynamicTo<Element>(node);
if (element && IsClickable())
return element;
Element* anchor = AnchorElement();
Element* click_element = MouseButtonListener();
if (!anchor || (click_element && click_element->IsDescendantOf(anchor)))
return click_element;
return anchor;
}
Element* AXNodeObject::AnchorElement() const {
Node* node = this->GetNode();
if (!node)
return nullptr;
AXObjectCacheImpl& cache = AXObjectCache();
// search up the DOM tree for an anchor element
// NOTE: this assumes that any non-image with an anchor is an
// HTMLAnchorElement
for (; node; node = node->parentNode()) {
if (IsA<HTMLAnchorElement>(*node))
return To<Element>(node);
if (LayoutObject* layout_object = node->GetLayoutObject()) {
AXObject* ax_object = cache.GetOrCreate(layout_object);
if (ax_object && ax_object->IsAnchor())
return To<Element>(node);
}
}
return nullptr;
}
Document* AXNodeObject::GetDocument() const {
if (!GetNode())
return nullptr;
return &GetNode()->GetDocument();
}
// TODO(chrishall): consider merging this with AXObject::Language in followup.
AtomicString AXNodeObject::Language() const {
if (!GetNode())
return AXObject::Language();
// If it's the root, get the computed language for the document element,
// because the root LayoutObject doesn't have the right value.
if (RoleValue() == ax::mojom::blink::Role::kRootWebArea) {
Element* document_element = GetDocument()->documentElement();
if (!document_element)
return g_empty_atom;
// Ensure we return only the first language tag. ComputeInheritedLanguage
// consults ContentLanguage which can be set from 2 different sources.
// DocumentLoader::DidInstallNewDocument from HTTP headers which truncates
// until the first comma.
// HttpEquiv::Process from <meta> tag which does not truncate.
// TODO(chrishall): Consider moving this comma handling to setter side.
AtomicString lang = document_element->ComputeInheritedLanguage();
Vector<String> languages;
String(lang).Split(',', languages);
if (!languages.IsEmpty())
return AtomicString(languages[0].StripWhiteSpace());
}
return AXObject::Language();
}
bool AXNodeObject::HasAttribute(const QualifiedName& attribute) const {
Element* element = GetElement();
if (!element)
return false;
if (element->FastHasAttribute(attribute))
return true;
return HasInternalsAttribute(*element, attribute);
}
const AtomicString& AXNodeObject::GetAttribute(
const QualifiedName& attribute) const {
Element* element = GetElement();
if (!element)
return g_null_atom;
const AtomicString& value = element->FastGetAttribute(attribute);
if (!value.IsNull())
return value;
return GetInternalsAttribute(*element, attribute);
}
bool AXNodeObject::HasInternalsAttribute(Element& element,
const QualifiedName& attribute) const {
if (!element.DidAttachInternals())
return false;
return element.EnsureElementInternals().HasAttribute(attribute);
}
const AtomicString& AXNodeObject::GetInternalsAttribute(
Element& element,
const QualifiedName& attribute) const {
if (!element.DidAttachInternals())
return g_null_atom;
return element.EnsureElementInternals().FastGetAttribute(attribute);
}
AXObject* AXNodeObject::CorrespondingControlAXObjectForLabelElement() const {
HTMLLabelElement* label_element = LabelElementContainer();
if (!label_element)
return nullptr;
HTMLElement* corresponding_control = label_element->control();
if (!corresponding_control)
return nullptr;
// Make sure the corresponding control isn't a descendant of this label
// that's in the middle of being destroyed.
if (corresponding_control->GetLayoutObject() &&
!corresponding_control->GetLayoutObject()->Parent())
return nullptr;
return AXObjectCache().GetOrCreate(corresponding_control);
}
AXObject* AXNodeObject::CorrespondingLabelAXObject() const {
HTMLLabelElement* label_element = LabelElementContainer();
if (!label_element)
return nullptr;
return AXObjectCache().GetOrCreate(label_element);
}
HTMLLabelElement* AXNodeObject::LabelElementContainer() const {
if (!GetNode())
return nullptr;
// the control element should not be considered part of the label
if (IsControl())
return nullptr;
// the link element should not be considered part of the label
if (IsLink())
return nullptr;
// find if this has a ancestor that is a label
return Traversal<HTMLLabelElement>::FirstAncestorOrSelf(*GetNode());
}
bool AXNodeObject::OnNativeFocusAction() {
// Checking if node is focusable in a native focus action requires that we
// have updated style and layout tree, since the focus check relies on the
// existence of layout objects to determine the result. However, these layout
// objects may have been deferred by display-locking.
Document* document = GetDocument();
Node* node = GetNode();
if (document && node)
document->UpdateStyleAndLayoutTreeForNode(node);
if (!CanSetFocusAttribute())
return false;
if (IsWebArea()) {
// If another Frame has focused content (e.g. nested iframe), then we
// need to clear focus for the other Document Frame.
// Here we set the focused element via the FocusController so that the
// other Frame loses focus, and the target Document Element gains focus.
// This fixes a scenario with Narrator Item Navigation when the user
// navigates from the outer UI to the document when the last focused
// element was within a nested iframe before leaving the document frame.
Page* page = document->GetPage();
// Elements inside a portal should not be focusable.
if (page && !page->InsidePortal()) {
page->GetFocusController().SetFocusedElement(document->documentElement(),
document->GetFrame());
} else {
document->ClearFocusedElement();
}
return true;
}
Element* element = GetElement();
if (!element) {
document->ClearFocusedElement();
return true;
}
// If this node is already the currently focused node, then calling
// focus() won't do anything. That is a problem when focus is removed
// from the webpage to chrome, and then returns. In these cases, we need
// to do what keyboard and mouse focus do, which is reset focus first.
if (document->FocusedElement() == element) {
document->ClearFocusedElement();
// Calling ClearFocusedElement could result in changes to the document,
// like this AXObject becoming detached.
if (IsDetached())
return false;
}
// If the object is not natively focusable but can be focused using an ARIA
// active descendant, perform a native click instead. This will enable Web
// apps that set accessibility focus using an active descendant to capture and
// act on the click event. Otherwise, there is no other way to inform the app
// that an AT has requested the focus to be changed, except if the app is
// using AOM. To be extra safe, exclude objects that are clickable themselves.
// This won't prevent anyone from having a click handler on the object's
// container.
if (!IsClickable() && CanBeActiveDescendant()) {
return OnNativeClickAction();
}
element->focus();
return true;
}
bool AXNodeObject::OnNativeIncrementAction() {
LocalFrame* frame = GetDocument() ? GetDocument()->GetFrame() : nullptr;
LocalFrame::NotifyUserActivation(
frame, mojom::blink::UserActivationNotificationType::kInteraction);
AlterSliderOrSpinButtonValue(true);
return true;
}
bool AXNodeObject::OnNativeDecrementAction() {
LocalFrame* frame = GetDocument() ? GetDocument()->GetFrame() : nullptr;
LocalFrame::NotifyUserActivation(
frame, mojom::blink::UserActivationNotificationType::kInteraction);
AlterSliderOrSpinButtonValue(false);
return true;
}
bool AXNodeObject::OnNativeSetSequentialFocusNavigationStartingPointAction() {
if (!GetNode())
return false;
Document* document = GetDocument();
document->ClearFocusedElement();
document->SetSequentialFocusNavigationStartingPoint(GetNode());
return true;
}
void AXNodeObject::ChildrenChanged() {
if (!GetNode() && !GetLayoutObject())
return;
DCHECK(!IsDetached()) << "Avoid ChildrenChanged() on detached node: "
<< ToString(true, true);
// When children changed on a <map> that means we need to forward the
// children changed to the <img> that parents the <area> elements.
// TODO(accessibility) Consider treating <img usemap> as aria-owns so that
// we get implementation "for free" vai relation cache, etc.
if (HTMLMapElement* map_element = DynamicTo<HTMLMapElement>(GetNode())) {
HTMLImageElement* image_element = map_element->ImageElement();
if (image_element) {
AXObject* ax_image = AXObjectCache().Get(image_element);
if (ax_image) {
ax_image->ChildrenChanged();
return;
}
}
}
// Always update current object, in case it wasn't included in the tree but
// now is. In that case, the LastKnownIsIncludedInTreeValue() won't have been
// updated yet, so we can't use that. Unfortunately, this is not a safe time
// to get the current included in tree value, therefore, we'll play it safe
// and update the children in two places sometimes.
SetNeedsToUpdateChildren();
// If this node is not in the tree, update the children of the first ancesor
// that is included in the tree.
if (!LastKnownIsIncludedInTreeValue()) {
// The first object (this or ancestor) that is included in the tree is the
// one whose children may have changed.
// Can be null, e.g. if <title> contents change
if (AXObject* node_to_update = ParentObjectIncludedInTree())
node_to_update->SetNeedsToUpdateChildren();
}
// If this node's children are not part of the accessibility tree then
// skip notification and walking up the ancestors.
// Cases where this happens:
// - an ancestor has only presentational children, or
// - this or an ancestor is a leaf node
// Uses |cached_is_descendant_of_leaf_node_| to avoid updating cached
// attributes for eachc change via | UpdateCachedAttributeValuesIfNeeded()|.
if (!CanHaveChildren() || LastKnownIsDescendantOfLeafNode())
return;
// TODO(aleventhal) Consider removing.
if (IsDetached()) {
NOTREACHED() << "None of the above calls should be able to detach |this|: "
<< ToString(true, true);
return;
}
AXObjectCache().PostNotification(this,
ax::mojom::blink::Event::kChildrenChanged);
}
void AXNodeObject::SelectedOptions(AXObjectVector& options) const {
if (auto* select = DynamicTo<HTMLSelectElement>(GetNode())) {
for (auto* const option : *select->selectedOptions()) {
AXObject* ax_option = AXObjectCache().GetOrCreate(option);
if (ax_option)
options.push_back(ax_option);
}
return;
}
const AXObjectVector& children = ChildrenIncludingIgnored();
if (RoleValue() == ax::mojom::blink::Role::kComboBoxGrouping ||
RoleValue() == ax::mojom::blink::Role::kComboBoxMenuButton) {
for (const auto& obj : children) {
if (obj->RoleValue() == ax::mojom::blink::Role::kListBox) {
obj->SelectedOptions(options);
return;
}
}
}
for (const auto& obj : children) {
if (obj->IsSelected() == kSelectedStateTrue)
options.push_back(obj);
}
}
void AXNodeObject::SelectionChanged() {
// Post the selected text changed event on the first ancestor that's
// focused (to handle form controls, ARIA text boxes and contentEditable),
// or the web area if the selection is just in the document somewhere.
if (IsFocused() || IsWebArea()) {
AXObjectCache().PostNotification(
this, ax::mojom::blink::Event::kTextSelectionChanged);
if (GetDocument()) {
AXObject* document_object = AXObjectCache().GetOrCreate(GetDocument());
AXObjectCache().PostNotification(
document_object, ax::mojom::blink::Event::kDocumentSelectionChanged);
}
} else {
AXObject::SelectionChanged(); // Calls selectionChanged on parent.
}
}
AXObject* AXNodeObject::ErrorMessage() const {
// Check for aria-errormessage.
Element* existing_error_message =
GetAOMPropertyOrARIAAttribute(AOMRelationProperty::kErrorMessage);
if (existing_error_message)
return AXObjectCache().GetOrCreate(existing_error_message);
// Check for visible validationMessage. This can only be visible for a focused
// control. Corollary: if there is a visible validationMessage alert box, then
// it is related to the current focus.
if (this != AXObjectCache().FocusedObject())
return nullptr;
return AXObjectCache().ValidationMessageObjectIfInvalid(true);
}
// According to the standard, the figcaption should only be the first or
// last child: https://html.spec.whatwg.org/#the-figcaption-element
static Element* GetChildFigcaption(const Node& node) {
Element* element = ElementTraversal::FirstChild(node);
if (!element)
return nullptr;
if (element->HasTagName(html_names::kFigcaptionTag))
return element;
element = ElementTraversal::LastChild(node);
if (element->HasTagName(html_names::kFigcaptionTag))
return element;
return nullptr;
}
// Based on
// http://rawgit.com/w3c/aria/master/html-aam/html-aam.html#accessible-name-and-description-calculation
String AXNodeObject::NativeTextAlternative(
AXObjectSet& visited,
ax::mojom::blink::NameFrom& name_from,
AXRelatedObjectVector* related_objects,
NameSources* name_sources,
bool* found_text_alternative) const {
if (!GetNode())
return String();
// If nameSources is non-null, relatedObjects is used in filling it in, so it
// must be non-null as well.
if (name_sources)
DCHECK(related_objects);
String text_alternative;
AXRelatedObjectVector local_related_objects;
// 5.1/5.5 Text inputs, Other labelable Elements
// If you change this logic, update AXNodeObject::nameFromLabelElement, too.
auto* html_element = DynamicTo<HTMLElement>(GetNode());
if (html_element && html_element->IsLabelable()) {
name_from = ax::mojom::blink::NameFrom::kRelatedElement;
if (name_sources) {
name_sources->push_back(NameSource(*found_text_alternative));
name_sources->back().type = name_from;
name_sources->back().native_source = kAXTextFromNativeHTMLLabel;
}
LabelsNodeList* labels = nullptr;
if (AXObjectCache().MayHaveHTMLLabel(*html_element))
labels = html_element->labels();
if (labels && labels->length() > 0) {
HeapVector<Member<Element>> label_elements;
for (unsigned label_index = 0; label_index < labels->length();
++label_index) {
Element* label = labels->item(label_index);
if (name_sources) {
if (!label->FastGetAttribute(html_names::kForAttr).IsEmpty() &&
label->FastGetAttribute(html_names::kForAttr) ==
html_element->GetIdAttribute()) {
name_sources->back().native_source = kAXTextFromNativeHTMLLabelFor;
} else {
name_sources->back().native_source =
kAXTextFromNativeHTMLLabelWrapped;
}
}
label_elements.push_back(label);
}
text_alternative =
TextFromElements(false, visited, label_elements, related_objects);
if (!text_alternative.IsNull()) {
*found_text_alternative = true;
if (name_sources) {
NameSource& source = name_sources->back();
source.related_objects = *related_objects;
source.text = text_alternative;
} else {
return text_alternative.StripWhiteSpace();
}
} else if (name_sources) {
name_sources->back().invalid = true;
}
}
}
// 5.2 input type="button", input type="submit" and input type="reset"
const auto* input_element = DynamicTo<HTMLInputElement>(GetNode());
if (input_element && input_element->IsTextButton()) {
// value attribute.
name_from = ax::mojom::blink::NameFrom::kValue;
if (name_sources) {
name_sources->push_back(NameSource(*found_text_alternative, kValueAttr));
name_sources->back().type = name_from;
}
String value = input_element->value();
if (!value.IsNull()) {
text_alternative = value;
if (name_sources) {
NameSource& source = name_sources->back();
source.text = text_alternative;
*found_text_alternative = true;
} else {
return text_alternative;
}
}
// Get default value if object is not laid out.
// If object is laid out, it will have a layout object for the label.
if (!GetLayoutObject()) {
String default_label = input_element->ValueOrDefaultLabel();
if (value.IsNull() && !default_label.IsNull()) {
// default label
name_from = ax::mojom::blink::NameFrom::kContents;
if (name_sources) {
name_sources->push_back(NameSource(*found_text_alternative));
name_sources->back().type = name_from;
}
text_alternative = default_label;
if (name_sources) {
NameSource& source = name_sources->back();
source.text = text_alternative;
*found_text_alternative = true;
} else {
return text_alternative;
}
}
}
return text_alternative;
}
// 5.3 input type="image"
if (input_element &&
input_element->getAttribute(kTypeAttr) == input_type_names::kImage) {
// alt attr
const AtomicString& alt = input_element->getAttribute(kAltAttr);
const bool is_empty = alt.IsEmpty() && !alt.IsNull();
name_from = is_empty ? ax::mojom::blink::NameFrom::kAttributeExplicitlyEmpty
: ax::mojom::blink::NameFrom::kAttribute;
if (name_sources) {
name_sources->push_back(NameSource(*found_text_alternative, kAltAttr));
name_sources->back().type = name_from;
}
if (!alt.IsNull()) {
text_alternative = alt;
if (name_sources) {
NameSource& source = name_sources->back();
source.attribute_value = alt;
source.text = text_alternative;
*found_text_alternative = true;
} else {
return text_alternative;
}
}
// value attribute.
if (name_sources) {
name_sources->push_back(NameSource(*found_text_alternative, kValueAttr));
name_sources->back().type = name_from;
}
name_from = ax::mojom::blink::NameFrom::kAttribute;
String value = input_element->value();
if (!value.IsNull()) {
text_alternative = value;
if (name_sources) {
NameSource& source = name_sources->back();
source.text = text_alternative;
*found_text_alternative = true;
} else {
return text_alternative;
}
}
// title attr
if (name_sources) {
name_sources->push_back(NameSource(*found_text_alternative, kTitleAttr));
name_sources->back().type = name_from;
}
name_from = ax::mojom::blink::NameFrom::kTitle;
const AtomicString& title = input_element->getAttribute(kTitleAttr);
if (!title.IsNull()) {
text_alternative = title;
if (name_sources) {
NameSource& source = name_sources->back();
source.attribute_value = title;
source.text = text_alternative;
*found_text_alternative = true;
} else {
return text_alternative;
}
}
// localised default value ("Submit")
name_from = ax::mojom::blink::NameFrom::kValue;
text_alternative =
input_element->GetLocale().QueryString(IDS_FORM_SUBMIT_LABEL);
if (name_sources) {
name_sources->push_back(NameSource(*found_text_alternative, kTypeAttr));
NameSource& source = name_sources->back();
source.attribute_value = input_element->getAttribute(kTypeAttr);
source.type = name_from;
source.text = text_alternative;
*found_text_alternative = true;
} else {
return text_alternative;
}
return text_alternative;
}
// 5.1 Text inputs - step 3 (placeholder attribute)
if (html_element && html_element->IsTextControl()) {
name_from = ax::mojom::blink::NameFrom::kPlaceholder;
if (name_sources) {
name_sources->push_back(
NameSource(*found_text_alternative, html_names::kPlaceholderAttr));
NameSource& source = name_sources->back();
source.type = name_from;
}
const String placeholder = PlaceholderFromNativeAttribute();
if (!placeholder.IsEmpty()) {
text_alternative = placeholder;
if (name_sources) {
NameSource& source = name_sources->back();
source.text = text_alternative;
source.attribute_value =
html_element->FastGetAttribute(html_names::kPlaceholderAttr);
*found_text_alternative = true;
} else {
return text_alternative;
}
}
}
// Input type=file. Not part of the spec, but Blink implements it
// as a single control that has both a button ("Choose file...") and
// some text showing the filename, and we need to concatenate both into
// the name of the button.
if (input_element && input_element->type() == input_type_names::kFile) {
name_from = ax::mojom::blink::NameFrom::kValue;
String displayed_file_path = StringValue();
String upload_button_text = input_element->UploadButton()->value();
if (!displayed_file_path.IsEmpty()) {
text_alternative = displayed_file_path + ", " + upload_button_text;
} else {
text_alternative = upload_button_text;
}
*found_text_alternative = true;
if (name_sources) {
name_sources->push_back(NameSource(true, kValueAttr));
name_sources->back().type = name_from;
name_sources->back().text = text_alternative;
} else {
return text_alternative;
}
}
// Also check for aria-placeholder.
if (IsTextControl()) {
name_from = ax::mojom::blink::NameFrom::kPlaceholder;
if (name_sources) {
name_sources->push_back(NameSource(*found_text_alternative,
html_names::kAriaPlaceholderAttr));
NameSource& source = name_sources->back();
source.type = name_from;
}
const AtomicString& aria_placeholder =
GetAOMPropertyOrARIAAttribute(AOMStringProperty::kPlaceholder);
if (!aria_placeholder.IsEmpty()) {
text_alternative = aria_placeholder;
if (name_sources) {
NameSource& source = name_sources->back();
source.text = text_alternative;
source.attribute_value = aria_placeholder;
*found_text_alternative = true;
} else {
return text_alternative;
}
}
return text_alternative;
}
// 5.7 figure and figcaption Elements
if (GetNode()->HasTagName(html_names::kFigureTag)) {
// figcaption
name_from = ax::mojom::blink::NameFrom::kRelatedElement;
if (name_sources) {
name_sources->push_back(NameSource(*found_text_alternative));
name_sources->back().type = name_from;
name_sources->back().native_source = kAXTextFromNativeHTMLFigcaption;
}
Element* figcaption = GetChildFigcaption(*(GetNode()));
if (figcaption) {
AXObject* figcaption_ax_object = AXObjectCache().GetOrCreate(figcaption);
if (figcaption_ax_object) {
text_alternative =
RecursiveTextAlternative(*figcaption_ax_object, false, visited);
if (related_objects) {
local_related_objects.push_back(
MakeGarbageCollected<NameSourceRelatedObject>(
figcaption_ax_object, text_alternative));
*related_objects = local_related_objects;
local_related_objects.clear();
}
if (name_sources) {
NameSource& source = name_sources->back();
source.related_objects = *related_objects;
source.text = text_alternative;
*found_text_alternative = true;
} else {
return text_alternative;
}
}
}
return text_alternative;
}
// 5.8 img or area Element
if (IsA<HTMLImageElement>(GetNode()) || IsA<HTMLAreaElement>(GetNode()) ||
(GetLayoutObject() && GetLayoutObject()->IsSVGImage())) {
// alt
const AtomicString& alt = GetAttribute(kAltAttr);
const bool is_empty = alt.IsEmpty() && !alt.IsNull();
name_from = is_empty ? ax::mojom::blink::NameFrom::kAttributeExplicitlyEmpty
: ax::mojom::blink::NameFrom::kAttribute;
if (name_sources) {
name_sources->push_back(NameSource(*found_text_alternative, kAltAttr));
name_sources->back().type = name_from;
}
if (!alt.IsNull()) {
text_alternative = alt;
if (name_sources) {
NameSource& source = name_sources->back();
source.attribute_value = alt;
source.text = text_alternative;
*found_text_alternative = true;
} else {
return text_alternative;
}
}
return text_alternative;
}
// 5.9 table Element
if (auto* table_element = DynamicTo<HTMLTableElement>(GetNode())) {
// caption
name_from = ax::mojom::blink::NameFrom::kCaption;
if (name_sources) {
name_sources->push_back(NameSource(*found_text_alternative));
name_sources->back().type = name_from;
name_sources->back().native_source = kAXTextFromNativeHTMLTableCaption;
}
HTMLTableCaptionElement* caption = table_element->caption();
if (caption) {
AXObject* caption_ax_object = AXObjectCache().GetOrCreate(caption);
if (caption_ax_object) {
text_alternative =
RecursiveTextAlternative(*caption_ax_object, false, visited);
if (related_objects) {
local_related_objects.push_back(
MakeGarbageCollected<NameSourceRelatedObject>(caption_ax_object,
text_alternative));
*related_objects = local_related_objects;
local_related_objects.clear();
}
if (name_sources) {
NameSource& source = name_sources->back();
source.related_objects = *related_objects;
source.text = text_alternative;
*found_text_alternative = true;
} else {
return text_alternative;
}
}
}
// summary
name_from = ax::mojom::blink::NameFrom::kAttribute;
if (name_sources) {
name_sources->push_back(
NameSource(*found_text_alternative, html_names::kSummaryAttr));
name_sources->back().type = name_from;
}
const AtomicString& summary = GetAttribute(html_names::kSummaryAttr);
if (!summary.IsNull()) {
text_alternative = summary;
if (name_sources) {
NameSource& source = name_sources->back();
source.attribute_value = summary;
source.text = text_alternative;
*found_text_alternative = true;
} else {
return text_alternative;
}
}
return text_alternative;
}
// Per SVG AAM 1.0's modifications to 2D of this algorithm.
if (GetNode()->IsSVGElement()) {
name_from = ax::mojom::blink::NameFrom::kRelatedElement;
if (name_sources) {
name_sources->push_back(NameSource(*found_text_alternative));
name_sources->back().type = name_from;
name_sources->back().native_source = kAXTextFromNativeHTMLTitleElement;
}
auto* container_node = To<ContainerNode>(GetNode());
Element* title = ElementTraversal::FirstChild(
*container_node, HasTagName(svg_names::kTitleTag));
if (title) {
AXObject* title_ax_object = AXObjectCache().GetOrCreate(title);
if (title_ax_object && !visited.Contains(title_ax_object)) {
text_alternative =
RecursiveTextAlternative(*title_ax_object, false, visited);
if (related_objects) {
local_related_objects.push_back(
MakeGarbageCollected<NameSourceRelatedObject>(title_ax_object,
text_alternative));
*related_objects = local_related_objects;
local_related_objects.clear();
}
}
if (name_sources) {
NameSource& source = name_sources->back();
source.text = text_alternative;
source.related_objects = *related_objects;
*found_text_alternative = true;
} else {
return text_alternative;
}
}
}
// Fieldset / legend.
if (auto* html_field_set_element =
DynamicTo<HTMLFieldSetElement>(GetNode())) {
name_from = ax::mojom::blink::NameFrom::kRelatedElement;
if (name_sources) {
name_sources->push_back(NameSource(*found_text_alternative));
name_sources->back().type = name_from;
name_sources->back().native_source = kAXTextFromNativeHTMLLegend;
}
HTMLElement* legend = html_field_set_element->Legend();
if (legend) {
AXObject* legend_ax_object = AXObjectCache().GetOrCreate(legend);
// Avoid an infinite loop
if (legend_ax_object && !visited.Contains(legend_ax_object)) {
text_alternative =
RecursiveTextAlternative(*legend_ax_object, false, visited);
if (related_objects) {
local_related_objects.push_back(
MakeGarbageCollected<NameSourceRelatedObject>(legend_ax_object,
text_alternative));
*related_objects = local_related_objects;
local_related_objects.clear();
}
if (name_sources) {
NameSource& source = name_sources->back();
source.related_objects = *related_objects;
source.text = text_alternative;
*found_text_alternative = true;
} else {
return text_alternative;
}
}
}
}
// Document.
if (IsWebArea()) {
Document* document = this->GetDocument();
if (document) {
name_from = ax::mojom::blink::NameFrom::kAttribute;
if (name_sources) {
name_sources->push_back(
NameSource(found_text_alternative, html_names::kAriaLabelAttr));
name_sources->back().type = name_from;
}
if (Element* document_element = document->documentElement()) {
const AtomicString& aria_label =
AccessibleNode::GetPropertyOrARIAAttribute(
document_element, AOMStringProperty::kLabel);
if (!aria_label.IsEmpty()) {
text_alternative = aria_label;
if (name_sources) {
NameSource& source = name_sources->back();
source.text = text_alternative;
source.attribute_value = aria_label;
*found_text_alternative = true;
} else {
return text_alternative;
}
}
}
text_alternative = document->title();
bool is_empty_title_element =
text_alternative.IsEmpty() && document->TitleElement();
if (is_empty_title_element)
name_from = ax::mojom::blink::NameFrom::kAttributeExplicitlyEmpty;
else
name_from = ax::mojom::blink::NameFrom::kRelatedElement;
if (name_sources) {
name_sources->push_back(NameSource(*found_text_alternative));
name_sources->back().type = name_from;
name_sources->back().native_source = kAXTextFromNativeHTMLTitleElement;
}
Element* title_element = document->TitleElement();
AXObject* title_ax_object = AXObjectCache().GetOrCreate(
title_element, AXObjectCache().Get(document));
if (title_ax_object) {
if (related_objects) {
local_related_objects.push_back(
MakeGarbageCollected<NameSourceRelatedObject>(title_ax_object,
text_alternative));
*related_objects = local_related_objects;
local_related_objects.clear();
}
if (name_sources) {
NameSource& source = name_sources->back();
source.related_objects = *related_objects;
source.text = text_alternative;
*found_text_alternative = true;
} else {
return text_alternative;
}
}
}
}
return text_alternative;
}
String AXNodeObject::Description(
ax::mojom::blink::NameFrom name_from,
ax::mojom::blink::DescriptionFrom& description_from,
AXObjectVector* description_objects) const {
AXRelatedObjectVector related_objects;
String result =
Description(name_from, description_from, nullptr, &related_objects);
if (description_objects) {
description_objects->clear();
for (NameSourceRelatedObject* related_object : related_objects)
description_objects->push_back(related_object->object);
}
result = CollapseWhitespace(result);
if (RoleValue() == ax::mojom::blink::Role::kSpinButton &&
DatetimeAncestor()) {
// Fields inside a datetime control need to merge the field description
// with the description of the <input> element.
const AXObject* datetime_ancestor = DatetimeAncestor();
ax::mojom::blink::NameFrom datetime_ancestor_name_from;
datetime_ancestor->GetName(datetime_ancestor_name_from, nullptr);
description_objects->clear();
String ancestor_description = DatetimeAncestor()->Description(
datetime_ancestor_name_from, description_from, description_objects);
if (!result.IsEmpty() && !ancestor_description.IsEmpty())
return result + " " + ancestor_description;
if (!ancestor_description.IsEmpty())
return ancestor_description;
}
return result;
}
// Based on
// http://rawgit.com/w3c/aria/master/html-aam/html-aam.html#accessible-name-and-description-calculation
String AXNodeObject::Description(
ax::mojom::blink::NameFrom name_from,
ax::mojom::blink::DescriptionFrom& description_from,
DescriptionSources* description_sources,
AXRelatedObjectVector* related_objects) const {
// If descriptionSources is non-null, relatedObjects is used in filling it in,
// so it must be non-null as well.
// Important: create a DescriptionSource for every *potential* description
// source, even if it ends up not being present.
// When adding a new description_from type:
// * Also add it to AXValueNativeSourceType here:
// blink/public/devtools_protocol/browser_protocol.pdl
// * Update InspectorTypeBuilderHelper to map the new enum to
// the browser_protocol enum in NativeSourceType():
// blink/renderer/modules/accessibility/inspector_type_builder_helper.cc
// * Update devtools_frontend to add a new string for the new type of
// description. See AXNativeSourceTypes at:
// devtools-frontend/src/front_end/accessibility/AccessibilityStrings.js
if (description_sources)
DCHECK(related_objects);
if (!GetNode())
return String();
String description;
bool found_description = false;
description_from = ax::mojom::blink::DescriptionFrom::kRelatedElement;
if (description_sources) {
description_sources->push_back(
DescriptionSource(found_description, html_names::kAriaDescribedbyAttr));
description_sources->back().type = description_from;
}
// aria-describedby overrides any other accessible description, from:
// http://rawgit.com/w3c/aria/master/html-aam/html-aam.html
Element* element = GetElement();
if (!element)
return String();
Vector<String> ids;
HeapVector<Member<Element>> elements_from_attribute;
ElementsFromAttribute(elements_from_attribute,
html_names::kAriaDescribedbyAttr, ids);
if (!elements_from_attribute.IsEmpty()) {
// TODO(meredithl): Determine description sources when |aria_describedby| is
// the empty string, in order to make devtools work with attr-associated
// elements.
if (description_sources) {
description_sources->back().attribute_value =
GetAttribute(html_names::kAriaDescribedbyAttr);
}
AXObjectSet visited;
description = TextFromElements(true, visited, elements_from_attribute,
related_objects);
for (auto& element : elements_from_attribute)
ids.push_back(element->GetIdAttribute());
TokenVectorFromAttribute(ids, html_names::kAriaDescribedbyAttr);
AXObjectCache().UpdateReverseRelations(this, ids);
if (!description.IsNull()) {
if (description_sources) {
DescriptionSource& source = description_sources->back();
source.type = description_from;
source.related_objects = *related_objects;
source.text = description;
found_description = true;
} else {
return description;
}
} else if (description_sources) {
description_sources->back().invalid = true;
}
}
// aria-description overrides any HTML-based accessible description,
// but not aria-describedby.
const AtomicString& aria_desc =
GetAOMPropertyOrARIAAttribute(AOMStringProperty::kDescription);
if (!aria_desc.IsNull()) {
description_from = ax::mojom::blink::DescriptionFrom::kAttribute;
description = aria_desc;
if (description_sources) {
found_description = true;
description_sources->back().text = description;
} else {
return description;
}
}
const auto* input_element = DynamicTo<HTMLInputElement>(GetNode());
// value, 5.2.2 from: http://rawgit.com/w3c/aria/master/html-aam/html-aam.html
if (name_from != ax::mojom::blink::NameFrom::kValue && input_element &&
input_element->IsTextButton()) {
description_from = ax::mojom::blink::DescriptionFrom::kAttribute;
if (description_sources) {
description_sources->push_back(
DescriptionSource(found_description, kValueAttr));
description_sources->back().type = description_from;
}
String value = input_element->value();
if (!value.IsNull()) {
description = value;
if (description_sources) {
DescriptionSource& source = description_sources->back();
source.text = description;
found_description = true;
} else {
return description;
}
}
}
if (RoleValue() == ax::mojom::blink::Role::kRuby) {
description_from = ax::mojom::blink::DescriptionFrom::kRelatedElement;
if (description_sources) {
description_sources->push_back(DescriptionSource(found_description));
description_sources->back().type = description_from;
description_sources->back().native_source =
kAXTextFromNativeHTMLRubyAnnotation;
}
AXObject* ruby_annotation_ax_object = nullptr;
for (const auto& child : children_) {
if (child->RoleValue() == ax::mojom::blink::Role::kRubyAnnotation &&
child->GetNode() &&
child->GetNode()->HasTagName(html_names::kRtTag)) {
ruby_annotation_ax_object = child;
break;
}
}
if (ruby_annotation_ax_object) {
AXObjectSet visited;
description =
RecursiveTextAlternative(*ruby_annotation_ax_object, true, visited);
if (related_objects) {
related_objects->push_back(
MakeGarbageCollected<NameSourceRelatedObject>(
ruby_annotation_ax_object, description));
}
if (description_sources) {
DescriptionSource& source = description_sources->back();
source.related_objects = *related_objects;
source.text = description;
found_description = true;
} else {
return description;
}
}
}
// table caption, 5.9.2 from:
// http://rawgit.com/w3c/aria/master/html-aam/html-aam.html
auto* table_element = DynamicTo<HTMLTableElement>(element);
if (name_from != ax::mojom::blink::NameFrom::kCaption && table_element) {
description_from = ax::mojom::blink::DescriptionFrom::kRelatedElement;
if (description_sources) {
description_sources->push_back(DescriptionSource(found_description));
description_sources->back().type = description_from;
description_sources->back().native_source =
kAXTextFromNativeHTMLTableCaption;
}
HTMLTableCaptionElement* caption = table_element->caption();
if (caption) {
AXObject* caption_ax_object = AXObjectCache().GetOrCreate(caption);
if (caption_ax_object) {
AXObjectSet visited;
description =
RecursiveTextAlternative(*caption_ax_object, false, visited);
if (related_objects) {
related_objects->push_back(
MakeGarbageCollected<NameSourceRelatedObject>(caption_ax_object,
description));
}
if (description_sources) {
DescriptionSource& source = description_sources->back();
source.related_objects = *related_objects;
source.text = description;
found_description = true;
} else {
return description;
}
}
}
}
// summary, 5.6.2 from:
// http://rawgit.com/w3c/aria/master/html-aam/html-aam.html
if (name_from != ax::mojom::blink::NameFrom::kContents &&
IsA<HTMLSummaryElement>(GetNode())) {
description_from = ax::mojom::blink::DescriptionFrom::kContents;
if (description_sources) {
description_sources->push_back(DescriptionSource(found_description));
description_sources->back().type = description_from;
}
AXObjectSet visited;
description = TextFromDescendants(visited, false);
if (!description.IsEmpty()) {
if (description_sources) {
found_description = true;
description_sources->back().text = description;
} else {
return description;
}
}
}
// title attribute, from:
// http://rawgit.com/w3c/aria/master/html-aam/html-aam.html
if (name_from != ax::mojom::blink::NameFrom::kTitle) {
description_from = ax::mojom::blink::DescriptionFrom::kTitle;
if (description_sources) {
description_sources->push_back(
DescriptionSource(found_description, kTitleAttr));
description_sources->back().type = description_from;
}
const AtomicString& title = GetAttribute(kTitleAttr);
if (!title.IsEmpty()) {
description = title;
if (description_sources) {
found_description = true;
description_sources->back().text = description;
} else {
return description;
}
}
}
description_from = ax::mojom::blink::DescriptionFrom::kUninitialized;
if (found_description) {
for (DescriptionSource& description_source : *description_sources) {
if (!description_source.text.IsNull() && !description_source.superseded) {
description_from = description_source.type;
if (!description_source.related_objects.IsEmpty())
*related_objects = description_source.related_objects;
return description_source.text;
}
}
}
return String();
}
String AXNodeObject::Placeholder(ax::mojom::blink::NameFrom name_from) const {
if (name_from == ax::mojom::blink::NameFrom::kPlaceholder)
return String();
Node* node = GetNode();
if (!node || !node->IsHTMLElement())
return String();
String native_placeholder = PlaceholderFromNativeAttribute();
if (!native_placeholder.IsEmpty())
return native_placeholder;
const AtomicString& aria_placeholder =
GetAOMPropertyOrARIAAttribute(AOMStringProperty::kPlaceholder);
if (!aria_placeholder.IsEmpty())
return aria_placeholder;
return String();
}
String AXNodeObject::Title(ax::mojom::blink::NameFrom name_from) const {
if (name_from == ax::mojom::blink::NameFrom::kTitle)
return String();
if (const auto* element = GetElement()) {
String title = element->title();
if (!title.IsEmpty())
return title;
}
return String();
}
String AXNodeObject::PlaceholderFromNativeAttribute() const {
Node* node = GetNode();
if (!node || !blink::IsTextControl(*node))
return String();
return ToTextControl(node)->StrippedPlaceholder();
}
String AXNodeObject::GetValueContributionToName() const {
if (CanSetValueAttribute()) {
if (IsTextControl())
return GetText();
if (IsRangeValueSupported()) {
const AtomicString& aria_valuetext =
GetAOMPropertyOrARIAAttribute(AOMStringProperty::kValueText);
if (!aria_valuetext.IsNull())
return aria_valuetext.GetString();
float value;
if (ValueForRange(&value))
return String::Number(value);
}
}
// "If the embedded control has role combobox or listbox, return the text
// alternative of the chosen option."
if (UseNameFromSelectedOption()) {
StringBuilder accumulated_text;
AXObjectVector selected_options;
SelectedOptions(selected_options);
for (const auto& child : selected_options) {
if (accumulated_text.length())
accumulated_text.Append(" ");
accumulated_text.Append(child->ComputedName());
}
return accumulated_text.ToString();
}
return String();
}
bool AXNodeObject::UseNameFromSelectedOption() const {
// Assumes that the node was reached via recursion in the name calculation.
switch (RoleValue()) {
// Step 2E from: http://www.w3.org/TR/accname-aam-1.1
case ax::mojom::blink::Role::kComboBoxGrouping:
case ax::mojom::blink::Role::kComboBoxMenuButton:
case ax::mojom::blink::Role::kListBox:
return true;
// This can be either a button widget with a non-false value of
// aria-haspopup or a select element with size of 1.
case ax::mojom::blink::Role::kPopUpButton:
return DynamicTo<HTMLSelectElement>(*GetNode());
default:
return false;
}
}
void AXNodeObject::Trace(Visitor* visitor) const {
visitor->Trace(node_);
AXObject::Trace(visitor);
}
} // namespace blink