blob: 8ecfa3e9f06dd8bbcf30045f7ce90cc6a0866710 [file] [log] [blame]
/*
* Copyright (C) 2014, 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_object_cache_impl.h"
#include <iterator>
#include <numeric>
#include "base/auto_reset.h"
#include "base/containers/contains.h"
#include "base/memory/scoped_refptr.h"
#include "base/metrics/histogram_macros.h"
#include "base/ranges/algorithm.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "third_party/blink/public/mojom/permissions/permission.mojom-blink.h"
#include "third_party/blink/public/mojom/permissions/permission_status.mojom-blink.h"
#include "third_party/blink/public/mojom/render_accessibility.mojom-blink.h"
#include "third_party/blink/public/platform/task_type.h"
#include "third_party/blink/public/web/web_local_frame_client.h"
#include "third_party/blink/public/web/web_plugin_container.h"
#include "third_party/blink/renderer/core/accessibility/scoped_blink_ax_event_intent.h"
#include "third_party/blink/renderer/core/aom/accessible_node.h"
#include "third_party/blink/renderer/core/aom/computed_accessible_node.h"
#include "third_party/blink/renderer/core/display_lock/display_lock_utilities.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/document_lifecycle.h"
#include "third_party/blink/renderer/core/dom/dom_node_ids.h"
#include "third_party/blink/renderer/core/dom/node_computed_style.h"
#include "third_party/blink/renderer/core/dom/slot_assignment_engine.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/events/event_util.h"
#include "third_party/blink/renderer/core/execution_context/agent.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/local_frame_client.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/frame/web_local_frame_impl.h"
#include "third_party/blink/renderer/core/html/canvas/html_canvas_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_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/listed_element.h"
#include "third_party/blink/renderer/core/html/html_area_element.h"
#include "third_party/blink/renderer/core/html/html_frame_owner_element.h"
#include "third_party/blink/renderer/core/html/html_head_element.h"
#include "third_party/blink/renderer/core/html/html_image_element.h"
#include "third_party/blink/renderer/core/html/html_object_element.h"
#include "third_party/blink/renderer/core/html/html_plugin_element.h"
#include "third_party/blink/renderer/core/html/html_progress_element.h"
#include "third_party/blink/renderer/core/html/html_script_element.h"
#include "third_party/blink/renderer/core/html/html_slot_element.h"
#include "third_party/blink/renderer/core/html/html_style_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_names.h"
#include "third_party/blink/renderer/core/input_type_names.h"
#include "third_party/blink/renderer/core/layout/inline/abstract_inline_text_box.h"
#include "third_party/blink/renderer/core/layout/layout_image.h"
#include "third_party/blink/renderer/core/layout/layout_inline.h"
#include "third_party/blink/renderer/core/layout/layout_text.h"
#include "third_party/blink/renderer/core/layout/layout_view.h"
#include "third_party/blink/renderer/core/layout/table/layout_table.h"
#include "third_party/blink/renderer/core/layout/table/layout_table_cell.h"
#include "third_party/blink/renderer/core/layout/table/layout_table_row.h"
#include "third_party/blink/renderer/core/page/chrome_client.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/page/page_animator.h"
#include "third_party/blink/renderer/core/style/content_data.h"
#include "third_party/blink/renderer/core/svg/svg_graphics_element.h"
#include "third_party/blink/renderer/core/svg/svg_style_element.h"
#include "third_party/blink/renderer/modules/accessibility/aria_notification.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_list_box.h"
#include "third_party/blink/renderer/modules/accessibility/ax_list_box_option.h"
#include "third_party/blink/renderer/modules/accessibility/ax_media_control.h"
#include "third_party/blink/renderer/modules/accessibility/ax_media_element.h"
#include "third_party/blink/renderer/modules/accessibility/ax_menu_list.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_progress_indicator.h"
#include "third_party/blink/renderer/modules/accessibility/ax_relation_cache.h"
#include "third_party/blink/renderer/modules/accessibility/ax_slider.h"
#include "third_party/blink/renderer/modules/accessibility/ax_validation_message.h"
#include "third_party/blink/renderer/modules/accessibility/ax_virtual_object.h"
#include "third_party/blink/renderer/modules/permissions/permission_utils.h"
#include "third_party/blink/renderer/platform/instrumentation/tracing/trace_event.h"
#include "third_party/blink/renderer/platform/wtf/functional.h"
#include "ui/accessibility/ax_common.h"
#include "ui/accessibility/ax_enums.mojom-blink.h"
#include "ui/accessibility/ax_event.h"
#include "ui/accessibility/ax_node.h"
#include "ui/accessibility/ax_role_properties.h"
#include "ui/accessibility/mojom/ax_relative_bounds.mojom-blink.h"
#if DCHECK_IS_ON()
#include "third_party/blink/renderer/modules/accessibility/ax_debug_utils.h"
#endif
// Prevent code that runs during the lifetime of the stack from altering the
// document lifecycle, for the main document, and the popup document if present.
#if DCHECK_IS_ON()
#define SCOPED_DISALLOW_LIFECYCLE_TRANSITION() \
DocumentLifecycle::DisallowTransitionScope scoped(document_->Lifecycle()); \
DocumentLifecycle::DisallowTransitionScope scoped2( \
popup_document_ ? popup_document_->Lifecycle() \
: document_->Lifecycle());
#else
#define SCOPED_DISALLOW_LIFECYCLE_TRANSITION()
#endif // DCHECK_IS_ON()
namespace blink {
using mojom::blink::FormControlType;
namespace {
bool IsInitialEmptyDocument(const Document& document) {
// Do not fire for initial empty top document. This helps avoid thrashing the
// a11y tree, causing an extra serialization.
// TODO(accessibility) This is an ugly special case -- find a better way.
// Note: Document::IsInitialEmptyDocument() did not work -- should it?
if (document.body() && document.body()->hasChildren())
return false;
if (document.head() && document.head()->hasChildren())
return false;
if (document.ParentDocument())
return false;
// No contents and not a child document, return true if about::blank.
return document.Url().IsAboutBlankURL();
}
// Return true if display locked or inside slot recalc, false otherwise.
// Also returns false if not a safe time to perform the check.
bool IsDisplayLocked(const Node* node, bool inclusive = false) {
if (!node)
return false;
// The IsDisplayLockedPreventingPaint() function may attempt to do
// a flat tree traversal of ancestors. If we're in a flat tree traversal
// forbidden scope, return false. Additionally, flat tree traversal
// might call AssignedSlot, so if we're in a slot assignment recalc
// forbidden scope, return false.
if (node->GetDocument().IsFlatTreeTraversalForbidden() ||
node->GetDocument()
.GetSlotAssignmentEngine()
.HasPendingSlotAssignmentRecalc()) {
return false; // Cannot safely perform this check now.
}
return DisplayLockUtilities::IsDisplayLockedPreventingPaint(node, inclusive);
}
bool IsDisplayLocked(const LayoutObject* object) {
bool inclusive = false;
while (object) {
if (const auto* node = object->GetNode())
return IsDisplayLocked(node, inclusive);
inclusive = true;
object = object->Parent();
}
return false;
}
bool IsActive(Document& document) {
return document.IsActive() && !document.IsDetached();
}
bool HasAriaCellRole(Element* elem) {
DCHECK(elem);
const AtomicString& role_str = elem->FastGetAttribute(html_names::kRoleAttr);
if (role_str.empty())
return false;
return ui::IsCellOrTableHeader(AXObject::AriaRoleStringToRoleEnum(role_str));
}
// Can role="presentation" aka "none" propagate to descendants of this node?
// Example: it propagates from table->tbody->tr->td, making them all ignored.
bool RolePresentationPropagates(Node* node) {
// Check for list markup.
if (IsA<HTMLMenuElement>(node) || IsA<HTMLUListElement>(node) ||
IsA<HTMLOListElement>(node)) {
return true;
}
// Check for <table>.
if (IsA<HTMLTableElement>(node))
return true; // table section, table row, table cells,
// Check for display: table CSS.
if (node->GetLayoutObject() && node->GetLayoutObject()->IsTable())
return true;
return false;
}
// Return true if whitespace is not necessary to keep adjacent_node separate
// in screen reader output from surrounding nodes.
bool CanIgnoreSpaceNextTo(LayoutObject* layout_object,
bool is_after,
int counter = 0) {
if (!layout_object)
return true;
if (counter > 3)
return false; // Don't recurse more than 3 times.
auto* elem = DynamicTo<Element>(layout_object->GetNode());
// Can usually ignore space next to a <br>.
// Exception: if the space was next to a <br> with an ARIA role.
if (layout_object->IsBR()) {
// As an example of a <br> with a role, Google Docs uses:
// <span contenteditable=false> <br role="presentation></span>.
// This construct hides the <br> from the AX tree and uses the space
// instead, presenting a hard line break as a soft line break.
DCHECK(elem);
return !is_after || !elem->FastHasAttribute(html_names::kRoleAttr);
}
// If adjacent to a whitespace character, the current space can be ignored.
if (layout_object->IsText()) {
auto* layout_text = To<LayoutText>(layout_object);
if (layout_text->HasEmptyText())
return false;
if (layout_text->TransformedText()
.Impl()
->ContainsOnlyWhitespaceOrEmpty()) {
return true;
}
auto adjacent_char =
is_after ? layout_text->FirstCharacterAfterWhitespaceCollapsing()
: layout_text->LastCharacterAfterWhitespaceCollapsing();
return adjacent_char == ' ' || adjacent_char == '\n' ||
adjacent_char == '\t';
}
// Keep spaces between images and other visible content, in case the image is
// used inline as a symbol mimicking text. This is not necessary for other
// types of images, such as a canvas.
// Note that relying the layout object via IsLayoutImage() was a cause of
// flakiness, as the layout object could change to a LayoutBlockFlow if the
// image failed to load. However, we still check IsLayoutImage() in order
// to detect CSS images, which don't have the same issue of changing layout.
if (layout_object->IsLayoutImage() || IsA<HTMLImageElement>(elem) ||
(IsA<HTMLInputElement>(elem) &&
To<HTMLInputElement>(elem)->FormControlType() ==
FormControlType::kInputImage)) {
return false;
}
// Do not keep spaces between blocks.
if (!layout_object->IsLayoutInline())
return true;
// If next to an element that a screen reader will always read separately,
// the the space can be ignored.
// Elements that are naturally focusable even without a tabindex tend
// to be rendered separately even if there is no space between them.
// Some ARIA roles act like table cells and don't need adjacent whitespace to
// indicate separation.
// False negatives are acceptable in that they merely lead to extra whitespace
// static text nodes.
if (elem && HasAriaCellRole(elem))
return true;
// Test against the appropriate child text node.
auto* layout_inline = To<LayoutInline>(layout_object);
LayoutObject* child =
is_after ? layout_inline->FirstChild() : layout_inline->LastChild();
if (!child && elem) {
// No children of inline element. Check adjacent sibling in same direction.
Node* adjacent_node =
is_after ? NodeTraversal::NextIncludingPseudoSkippingChildren(*elem)
: NodeTraversal::PreviousAbsoluteSiblingIncludingPseudo(*elem);
return adjacent_node &&
CanIgnoreSpaceNextTo(adjacent_node->GetLayoutObject(), is_after,
++counter);
}
return CanIgnoreSpaceNextTo(child, is_after, ++counter);
}
// TODO(accessibility) Rearrange methods so that a forward decl is unnecessary.
bool CanIgnoreSpace(const LayoutText& layout_text);
bool IsLayoutTextRelevantForAccessibility(const LayoutText& layout_text) {
if (!layout_text.Parent())
return false;
Node* node = layout_text.GetNode();
DCHECK(node); // Anonymous text is processed earlier, doesn't reach here.
#if DCHECK_IS_ON()
DCHECK(node->GetDocument().Lifecycle().GetState() >=
DocumentLifecycle::kAfterPerformLayout)
<< "Unclean document at lifecycle "
<< node->GetDocument().Lifecycle().ToString();
#endif
// Ignore empty text.
if (layout_text.HasEmptyText())
return false;
// Always keep if anything other than collapsible whitespace.
if (!layout_text.IsAllCollapsibleWhitespace() || layout_text.IsBR())
return true;
// Use previous decision for this whitespace. This is helpful for performance,
// consistency (flake reduction) and code simplicity, as we do not need to
// recompute block subtrees when inline nodes change. It also helps ensure
// that whitespace nodes do not change between AXNodeObject/AXLayoutObject
// at inopportune times.
// TODO(accessibility) Convert this method and callers of it to member
// methods so we can access whitespace_ignored_map_ directly.
AXObjectCacheImpl* cache = static_cast<AXObjectCacheImpl*>(
node->GetDocument().ExistingAXObjectCache());
auto& whitespace_ignored_map = cache->whitespace_ignored_map();
DOMNodeId whitespace_node_id = node->GetDomNodeId();
auto it = whitespace_ignored_map.find(whitespace_node_id);
if (it != whitespace_ignored_map.end()) {
return it->value;
}
// Compute ignored value for whitespace and record decision.
bool ignore_whitespace = CanIgnoreSpace(layout_text);
// Memoize the result.
whitespace_ignored_map.insert(whitespace_node_id, ignore_whitespace);
return ignore_whitespace;
}
bool CanIgnoreSpace(const LayoutText& layout_text) {
Node* node = layout_text.GetNode();
// Will now look at sibling nodes. We need the closest element to the
// whitespace markup-wise, e.g. tag1 in these examples:
// [whitespace] <tag1><tag2>x</tag2></tag1>
// <span>[whitespace]</span> <tag1><tag2>x</tag2></tag1>.
// Do not use LayoutTreeBuilderTraversal or FlatTreeTraversal as this may need
// to be called during slot assignment, when flat tree traversal is forbidden.
Node* prev_node =
NodeTraversal::PreviousAbsoluteSiblingIncludingPseudo(*node);
if (!prev_node)
return false;
Node* next_node = NodeTraversal::NextIncludingPseudoSkippingChildren(*node);
if (!next_node)
return false;
// Ignore extra whitespace-only text if a sibling will be presented
// separately by screen readers whether whitespace is there or not.
if (CanIgnoreSpaceNextTo(prev_node->GetLayoutObject(), false) ||
CanIgnoreSpaceNextTo(next_node->GetLayoutObject(), true)) {
return false;
}
// If the prev/next node is also a text node and the adjacent character is
// not whitespace, CanIgnoreSpaceNextTo will return false. In some cases that
// is what we want; in other cases it is not. Examples:
//
// 1a: <p><span>Hello</span><span>[whitespace]</span><span>World</span></p>
// 1b: <p><span>Hello</span>[whitespace]<span>World</span></p>
// 2: <div><ul><li style="display:inline;">x</li>[whitespace]</ul>y</div>
//
// In the first case, we want to preserve the whitespace (crbug.com/435765).
// In the second case, the whitespace in the markup is not relevant because
// the "x" is separated from the "y" by virtue of being inside a different
// block. In order to distinguish these two scenarios, we can use the
// LayoutBox associated with each node. For the first scenario, each node's
// LayoutBox is the LayoutBlockFlow associated with the <p>. For the second
// scenario, the LayoutBox of "x" and the whitespace is the LayoutBlockFlow
// associated with the <ul>; the LayoutBox of "y" is the one associated with
// the <div>.
LayoutBox* box = layout_text.EnclosingBox();
if (!box)
return false;
if (prev_node->GetLayoutObject() && prev_node->GetLayoutObject()->IsText()) {
LayoutBox* prev_box = prev_node->GetLayoutObject()->EnclosingBox();
if (prev_box != box)
return false;
}
if (next_node->GetLayoutObject() && next_node->GetLayoutObject()->IsText()) {
LayoutBox* next_box = next_node->GetLayoutObject()->EnclosingBox();
if (next_box != box)
return false;
}
return true;
}
bool IsHiddenTextNodeRelevantForAccessibility(const Text& text_node,
bool is_display_locked) {
// Children of an <iframe> tag will always be replaced by a new Document,
// either loaded from the iframe src or empty. In fact, we don't even parse
// them and they are treated like one text node. Consider irrelevant.
if (AXObject::IsFrame(text_node.parentElement()))
return false;
// Layout has more info available to determine if whitespace is relevant.
// If display-locked, layout object may be missing or stale:
// Assume that all display-locked text nodes are relevant, but only create
// an AXNodeObject in order to avoid using a stale layout object.
if (is_display_locked)
return true;
// If unrendered + no parent, it is in a shadow tree. Consider irrelevant.
if (!text_node.parentElement()) {
DCHECK(text_node.IsInShadowTree());
return false;
}
// If unrendered and in <canvas>, consider even whitespace relevant.
if (text_node.parentElement()->IsInCanvasSubtree())
return true;
// Must be unrendered because of CSS. Consider relevant if non-whitespace.
// Allowing rendered non-whitespace to be considered relevant will allow
// use for accessible relations such as labelledby and describedby.
return !text_node.ContainsOnlyWhitespaceOrEmpty();
}
bool IsShadowContentRelevantForAccessibility(const Node* node) {
DCHECK(node->ContainingShadowRoot());
// Return false if inside a shadow tree of something that can't have children,
// for example, an <img> has a user agent shadow root containing a <span> for
// the alt text. Do not create an accessible for that as it would be unable
// to have a parent that has it as a child.
if (!AXObject::CanHaveChildren(To<Element>(*node->OwnerShadowHost()))) {
return false;
}
// Native <img> create extra child nodes to hold alt text, which are not
// allowed as children. Note: images can have image map children, but these
// are moved from the <map> descendants and are not descendants of the image.
// See AXNodeObject::AddImageMapChildren().
if (node->IsInUserAgentShadowRoot() &&
IsA<HTMLImageElement>(node->OwnerShadowHost())) {
return false;
}
// Don't use non-<option> descendants of an AXMenuList.
// If the UseAXMenuList flag is on, we use a specialized class AXMenuList
// for handling the user-agent shadow DOM exposed by a <select> element.
// That class adds a mock AXMenuListPopup, which adds AXMenuListOption
// children for <option> descendants only.
if (AXObjectCacheImpl::UseAXMenuList() && node->IsInUserAgentShadowRoot() &&
!IsA<HTMLOptionElement>(node)) {
// Find any ancestor <select> if it is present.
Node* host = node->OwnerShadowHost();
auto* select_element = DynamicTo<HTMLSelectElement>(host);
if (!select_element) {
// An <optgroup> can be a shadow host too -- look for it's owner <select>.
if (auto* opt_group_element = DynamicTo<HTMLOptGroupElement>(host))
select_element = opt_group_element->OwnerSelectElement();
}
if (select_element) {
if (!select_element->GetLayoutObject())
return select_element->IsInCanvasSubtree();
// Non-option: only create AXObject if not inside an AXMenuList.
return !AXObjectCacheImpl::ShouldCreateAXMenuListFor(
select_element->GetLayoutObject());
}
}
// Outside of AXMenuList descendants, all other non-slot user agent shadow
// nodes are relevant.
const HTMLSlotElement* slot_element =
ToHTMLSlotElementIfSupportsAssignmentOrNull(node);
if (!slot_element)
return true;
// Slots are relevant if they have content.
// However, this can only be checked during safe times.
// During other times we must assume that the <slot> is relevant.
// TODO(accessibility) Consider removing this rule, but it will require
// a different way of dealing with these PDF test failures:
// https://chromium-review.googlesource.com/c/chromium/src/+/2965317
// For some reason the iframe tests hang, waiting for content to change. In
// other words, returning true here causes some tree updates not to occur.
if (node->GetDocument().IsFlatTreeTraversalForbidden() ||
node->GetDocument()
.GetSlotAssignmentEngine()
.HasPendingSlotAssignmentRecalc()) {
return true;
}
// If the slot element's host is an <object>/<embed>with any descendant nodes
// (including whitespace), LayoutTreeBuilderTraversal::FirstChild will
// return a node. We should only treat that node as slot content if it is
// being used as fallback content.
if (const HTMLPlugInElement* plugin_element =
DynamicTo<HTMLPlugInElement>(node->OwnerShadowHost())) {
return plugin_element->UseFallbackContent();
}
return LayoutTreeBuilderTraversal::FirstChild(*slot_element);
}
bool IsLayoutObjectRelevantForAccessibility(const LayoutObject& layout_object) {
if (layout_object.IsAnonymous()) {
// Anonymous means there is no DOM node, and it's been inserted by the
// layout engine within the tree. An example is an anonymous block that is
// inserted as a parent of an inline where there are block siblings.
return AXObjectCacheImpl::IsRelevantPseudoElementDescendant(layout_object);
}
if (layout_object.IsText())
return IsLayoutTextRelevantForAccessibility(To<LayoutText>(layout_object));
// An AXMenuListOption will be created, which is a subclass of AXNodeObject,
// not of AXLayoutObject.
if (AXObjectCacheImpl::ShouldCreateAXMenuListOptionFor(
layout_object.GetNode())) {
return false;
}
// An AXImageMapLink will be created, which is a subclass of AXNodeObject, not
// of AXLayoutObject.
if (IsA<HTMLAreaElement>(layout_object.GetNode()))
return false;
return true;
}
bool IsSubtreePrunedForAccessibility(const Element* node) {
if (IsA<HTMLAreaElement>(node) && !IsA<HTMLMapElement>(node->parentNode()))
return true; // <area> without parent <map> is not relevant.
if (IsA<HTMLMapElement>(node))
return true; // Contains children for an img, but is not its own object.
if (node->HasTagName(html_names::kColgroupTag) ||
node->HasTagName(html_names::kColTag)) {
return true; // Affects table layout, but doesn't get it's own AXObject.
}
if (node->IsPseudoElement()) {
if (!AXObjectCacheImpl::IsRelevantPseudoElement(*node))
return true;
}
if (const HTMLSlotElement* slot =
ToHTMLSlotElementIfSupportsAssignmentOrNull(node)) {
if (!AXObjectCacheImpl::IsRelevantSlotElement(*slot))
return true;
}
// <optgroup> is irrelevant inside of a <select> menulist.
if (auto* opt_group = DynamicTo<HTMLOptGroupElement>(node)) {
if (auto* select = opt_group->OwnerSelectElement()) {
if (select->UsesMenuList())
return true;
}
}
// An HTML <title> does not require an AXObject: the document's name is
// retrieved directly via the inner text.
if (IsA<HTMLTitleElement>(node))
return true;
return false;
}
// Return true if node is head/style/script or any descendant of those.
// Also returns true for descendants of any type of frame, because the frame
// itself is in the tree, but not DOM descendants (their contents are in a
// different document).
bool IsInPrunableHiddenContainerInclusive(const Node& node,
bool parent_ax_known,
bool is_display_locked) {
int max_depth_to_check = INT_MAX;
if (parent_ax_known) {
// Optimization: only need to check the current object if the parent the
// parent_ax is already known, because it we are attempting to add this
// object from something already relevant in the AX tree, and therefore
// can't be inside a <head>, <style>, <script> or SVG <style> element.
// However, there is an edge case that if it is display locked content
// we must also check the parent, which can be visible and included
// in the tree. This edge case is handled to satisfy tests and is not
// likely to be a real-world condition.
max_depth_to_check = is_display_locked ? 2 : 1;
}
for (const Node* ancestor = &node; ancestor;
ancestor = ancestor->parentElement()) {
// Objects inside <head> are pruned.
if (IsA<HTMLHeadElement>(ancestor))
return true;
// Objects inside a <style> are pruned.
if (IsA<HTMLStyleElement>(ancestor))
return true;
// Objects inside a <script> are true.
if (IsA<HTMLScriptElement>(ancestor))
return true;
// Elements inside of a frame/iframe are true unless inside a document
// that is a child of the frame. In the case where descendants are allowed,
// they will be in a different document, and therefore this loop will not
// reach the frame/iframe.
if (AXObject::IsFrame(ancestor))
return true;
// Style elements in SVG are not display: none, unlike HTML style
// elements, but they are still hidden along with their contents and thus
// treated as true for accessibility.
if (IsA<SVGStyleElement>(ancestor))
return true;
if (--max_depth_to_check <= 0)
break;
}
// All other nodes are relevant, even if hidden.
return false;
}
// -----------------------------------------------------------------------------
// DetermineAXObjectType() determines what type of AXObject should be created
// for the given node and layout_object.
// * Pass in the Node, the LayoutObject or both.
// * Passing in |parent_ax_known| when there is known parent is an optimization
// and does not affect the return value.
// Some general rules:
// * If neither the node nor layout object are relevant for accessibility, will
// return kPruneSubtree, which will cause no AXObject to be created, and
// result in the entire subtree being pruned at that point.
// * If the node is part of a forbidden subtree, then kPruneSubtree is used.
// * If both the node and layout are relevant, kAXLayoutObject is preferred,
// otherwise: kAXNodeObject for relevant nodes, kLayoutObject for layout.
// -----------------------------------------------------------------------------
AXObjectType DetermineAXObjectType(const Node* node,
const LayoutObject* layout_object,
bool parent_ax_known = false) {
DCHECK(layout_object || node);
bool is_display_locked =
node ? IsDisplayLocked(node) : IsDisplayLocked(layout_object);
if (is_display_locked)
layout_object = nullptr;
DCHECK(!node || !layout_object || layout_object->GetNode() == node);
bool is_node_relevant = false;
if (node) {
if (!node->isConnected()) {
return kPruneSubtree;
}
if (node->ContainingShadowRoot() &&
!IsShadowContentRelevantForAccessibility(node)) {
return kPruneSubtree;
}
if (!IsA<Element>(node) && !IsA<Text>(node)) {
// All remaining types, such as the document node, doctype node.
return layout_object ? kAXLayoutObject : kPruneSubtree;
}
if (const Element* element = DynamicTo<Element>(node)) {
if (IsSubtreePrunedForAccessibility(element))
return kPruneSubtree;
else
is_node_relevant = true;
} else { // Text is the only remaining type.
if (layout_object) {
// If there's layout for this text, it will either be pruned or an
// AXLayoutObject will be created for it. The logic of whether to return
// kAXLayoutObject or kPruneSubtree will come purely from
// is_layout_relevant further down.
return IsLayoutObjectRelevantForAccessibility(*layout_object)
? kAXLayoutObject
: kPruneSubtree;
} else {
// Otherwise, base the decision on the best info we have on the node.
is_node_relevant = IsHiddenTextNodeRelevantForAccessibility(
To<Text>(*node), is_display_locked);
}
}
}
bool is_layout_relevant =
layout_object && IsLayoutObjectRelevantForAccessibility(*layout_object);
// Prune if neither the LayoutObject nor Node are relevant.
if (!is_layout_relevant && !is_node_relevant)
return kPruneSubtree;
// If a node is not rendered, prune if it is in head/style/script or a DOM
// descendant of an iframe.
if (!is_layout_relevant && IsInPrunableHiddenContainerInclusive(
*node, parent_ax_known, is_display_locked)) {
return kPruneSubtree;
}
return is_layout_relevant ? kAXLayoutObject : kAXNodeObject;
}
const int kSizeMb = 1000000;
const int kSize10Mb = 10 * kSizeMb;
const int kSizeGb = 1000 * kSizeMb;
const int kBucketCount = 100;
void LogNodeDataSizeDistribution(
const ui::AXNodeData::AXNodeDataSize& node_data_size) {
UMA_HISTOGRAM_CUSTOM_COUNTS(
"Accessibility.Performance.AXObjectCacheImpl.Incremental.Int",
base::saturated_cast<int>(node_data_size.int_attribute_size), 1,
kSize10Mb, kBucketCount);
UMA_HISTOGRAM_CUSTOM_COUNTS(
"Accessibility.Performance.AXObjectCacheImpl.Incremental.Float",
base::saturated_cast<int>(node_data_size.float_attribute_size), 1,
kSize10Mb, kBucketCount);
UMA_HISTOGRAM_CUSTOM_COUNTS(
"Accessibility.Performance.AXObjectCacheImpl.Incremental.Bool",
base::saturated_cast<int>(node_data_size.bool_attribute_size), 1, kSizeMb,
kBucketCount);
UMA_HISTOGRAM_CUSTOM_COUNTS(
"Accessibility.Performance.AXObjectCacheImpl.Incremental.String",
base::saturated_cast<int>(node_data_size.string_attribute_size), 1,
kSizeGb, kBucketCount);
UMA_HISTOGRAM_CUSTOM_COUNTS(
"Accessibility.Performance.AXObjectCacheImpl.Incremental.IntList",
base::saturated_cast<int>(node_data_size.int_list_attribhute_size), 1,
kSize10Mb, kBucketCount);
UMA_HISTOGRAM_CUSTOM_COUNTS(
"Accessibility.Performance.AXObjectCacheImpl.Incremental.StringList",
base::saturated_cast<int>(node_data_size.string_list_attribute_size), 1,
kSizeGb, kBucketCount);
UMA_HISTOGRAM_CUSTOM_COUNTS(
"Accessibility.Performance.AXObjectCacheImpl.Incremental.HTML",
base::saturated_cast<int>(node_data_size.html_attribute_size), 1, kSizeGb,
kBucketCount);
UMA_HISTOGRAM_CUSTOM_COUNTS(
"Accessibility.Performance.AXObjectCacheImpl.Incremental.ChildIds",
base::saturated_cast<int>(node_data_size.child_ids_size), 1, kSize10Mb,
kBucketCount);
}
} // namespace
// static
bool AXObjectCacheImpl::use_ax_menu_list_ = false;
// static
AXObjectCache* AXObjectCacheImpl::Create(Document& document,
const ui::AXMode& ax_mode) {
return MakeGarbageCollected<AXObjectCacheImpl>(document, ax_mode);
}
AXObjectCacheImpl::AXObjectCacheImpl(Document& document,
const ui::AXMode& ax_mode)
: document_(document),
ax_mode_(ax_mode),
validation_message_axid_(0),
active_aria_modal_dialog_(nullptr),
accessibility_event_permission_(mojom::blink::PermissionStatus::ASK),
permission_service_(document.GetExecutionContext()),
permission_observer_receiver_(this, document.GetExecutionContext()),
render_accessibility_host_(document.GetExecutionContext()),
ax_tree_source_(BlinkAXTreeSource::Create(*this)) {
use_ax_menu_list_ = GetSettings()->GetUseAXMenuList();
}
AXObjectCacheImpl::~AXObjectCacheImpl() {
#if DCHECK_IS_ON()
DCHECK(has_been_disposed_);
#endif
}
// This is called shortly before the AXObjectCache is deleted.
// The destruction of the AXObjectCache will do most of the cleanup.
void AXObjectCacheImpl::Dispose() {
DCHECK(!has_been_disposed_) << "Something is wrong, trying to dispose twice.";
// Don't perform expensive computations while tearing down.
has_been_disposed_ = true;
// Detach all objects now. This prevents more work from occurring if we wait
// for the rendering engine to detach each node individually, because that
// will cause the renderer to attempt to potentially repair parents, and
// detach each child individually as Detach() calls ClearChildren().
// TODO(accessibility) We could just remove this method if code that checks
// HasBeenDisposed()/has_been_disposed_ had another way to check for shutdown.
for (auto& entry : objects_) {
AXObject* obj = entry.value;
obj->Detach();
}
// Destroy any pending task to serialize the tree.
weak_factory_for_serialization_pipeline_.Invalidate();
}
void AXObjectCacheImpl::AddInspectorAgent(InspectorAccessibilityAgent* agent) {
agents_.insert(agent);
}
void AXObjectCacheImpl::RemoveInspectorAgent(
InspectorAccessibilityAgent* agent) {
agents_.erase(agent);
}
void AXObjectCacheImpl::EnsureRelationCache() {
if (!relation_cache_) {
relation_cache_ = std::make_unique<AXRelationCache>(this);
relation_cache_->Init();
}
}
void AXObjectCacheImpl::EnsureSerializer() {
if (!ax_tree_serializer_) {
ax_tree_serializer_ = std::make_unique<
ui::AXTreeSerializer<AXObject*, HeapVector<Member<AXObject>>>>(
ax_tree_source_,
/*crash_on_error*/ true);
}
}
AXObject* AXObjectCacheImpl::Root() {
if (AXObject* root = Get(document_)) {
return root;
}
ProcessDeferredAccessibilityEvents(GetDocument(), /*force*/ true);
return Get(document_.Get());
}
AXObject* AXObjectCacheImpl::ObjectFromAXID(AXID id) const {
auto it = objects_.find(id);
return it != objects_.end() ? it->value : nullptr;
}
Node* AXObjectCacheImpl::FocusedNode() {
Node* focused_node = document_->FocusedElement();
if (!focused_node)
focused_node = document_;
// A popup is showing: return the focus within instead of the focus in the
// main document. Do not do this for HTML <select>, which has special
// focus manager using the kActiveDescendantId.
if (GetPopupDocumentIfShowing() && !IsA<HTMLSelectElement>(focused_node)) {
if (Node* focus_in_popup = GetPopupDocumentIfShowing()->FocusedElement())
return focus_in_popup;
}
return focused_node;
}
void AXObjectCacheImpl::UpdateLifecycleIfNeeded(Document& document) {
DCHECK(document.defaultView());
DCHECK(document.GetFrame());
DCHECK(document.View());
document.View()->UpdateAllLifecyclePhasesExceptPaint(
DocumentUpdateReason::kAccessibility);
}
void AXObjectCacheImpl::UpdateAXForAllDocuments() {
#if DCHECK_IS_ON()
DCHECK(!IsFrozen())
<< "Don't call UpdateAXForAllDocuments() here; layout and a11y are "
"already clean at the start of serialization.";
DCHECK(!updating_layout_and_ax_) << "Undesirable recursion.";
base::AutoReset<bool> updating(&updating_layout_and_ax_, true);
#endif
// First update the layout for the main and popup document.
UpdateLifecycleIfNeeded(GetDocument());
if (Document* popup_document = GetPopupDocumentIfShowing())
UpdateLifecycleIfNeeded(*popup_document);
// Next flush all accessibility events and dirty objects, for both the main
// and popup document, and update tree if needed.
if (IsDirty() || HasDirtyObjects()) {
ProcessDeferredAccessibilityEvents(GetDocument(), /*force*/ true);
}
}
AXObject* AXObjectCacheImpl::FocusedObject() {
#if DCHECK_IS_ON()
DCHECK(GetDocument().Lifecycle().GetState() >=
DocumentLifecycle::kAfterPerformLayout);
if (GetPopupDocumentIfShowing()) {
DCHECK(GetPopupDocumentIfShowing()->Lifecycle().GetState() >=
DocumentLifecycle::kAfterPerformLayout);
}
#endif
Node* focused_node = FocusedNode();
CHECK(focused_node);
AXObject* obj = Get(focused_node);
if (!obj) {
// In rare cases it's possible for the focus to not exist in the tree.
// An example would be a focused element inside of an image map that
// gets trimmed.
// In these cases, treat the focus as on the root object itself, so that
// AT users have some starting point.
DLOG(ERROR) << "The focus was not part of the a11y tree: " << focused_node;
return Root();
}
// the HTML element, for example, is focusable but has an AX object that is
// ignored
if (!obj->AccessibilityIsIncludedInTree())
obj = obj->ParentObjectIncludedInTree();
return obj;
}
const ui::AXMode& AXObjectCacheImpl::GetAXMode() {
return ax_mode_;
}
void AXObjectCacheImpl::SetAXMode(const ui::AXMode& ax_mode) {
ax_mode_ = ax_mode;
}
AXObject* AXObjectCacheImpl::Get(const LayoutObject* layout_object,
AXObject* parent_for_repair) {
if (!layout_object)
return nullptr;
if (Node* node = layout_object->GetNode()) {
// If there is a node, it is preferred for backing the AXObject.
DCHECK(!layout_object_mapping_.Contains(layout_object));
return Get(node);
}
auto it_id = layout_object_mapping_.find(layout_object);
if (it_id == layout_object_mapping_.end()) {
return nullptr;
}
AXID ax_id = it_id->value;
DCHECK(!WTF::IsHashTraitsDeletedValue<HashTraits<AXID>>(ax_id));
auto it_result = objects_.find(ax_id);
AXObject* result = it_result != objects_.end() ? it_result->value : nullptr;
DCHECK(result) << "Had AXID for Node but no entry in objects_";
DCHECK(result->IsAXNodeObject());
// Do not allow detached objects except when disposing entire tree.
DCHECK(!result->IsDetached() || has_been_disposed_)
<< "Detached AXNodeObject in map: "
<< "AXID#" << ax_id << " LayoutObject=" << layout_object;
if (result->CachedParentObject()) {
DCHECK(!parent_for_repair ||
parent_for_repair == result->CachedParentObject())
<< "If there is both a previous parent, and a parent supplied for "
"repair, they must match.";
} else if (parent_for_repair) {
result->SetParent(parent_for_repair);
}
// If there is no node for the AXObject, then it is an anonymous layout
// object (e.g. a pseudo-element or object introduced to match the structure
// of content). Such objects can only be created or destroyed via creation of
// their parents and recursion via AddPseudoElementChildrenFromLayoutTree.
// RepairMissingParent will not be able to restore a missing parent; instead
// we should never need to do that.
DCHECK(!result->IsMissingParent() || !result->GetNode())
<< "Had AXObject but is missing parent: " << layout_object << " "
<< result->ToString(true, true);
return result;
}
AXObject* AXObjectCacheImpl::Get(const Node* node) {
if (!node)
return nullptr;
#if DCHECK_IS_ON()
if (const Element* element = DynamicTo<Element>(node)) {
if (AccessibleNode* accessible_node = element->ExistingAccessibleNode()) {
DCHECK(!accessible_node_mapping_.Contains(accessible_node))
<< "The accessible node directly attached to an element should not "
"have its own AXObject: "
<< element;
}
}
#endif
AXID node_id = static_cast<AXID>(DOMNodeIds::ExistingIdForNode(node));
if (!node_id) {
// An ID hasn't yet been generated for this DOM node, but ::CreateAndInit()
// will ensure a DOMNodeID is generated by using node->GetDomNodeId().
// Therefore if an id doesn't exist for a DOM node, it means that it can't
// have an associated AXObject.
return nullptr;
}
auto it_result = objects_.find(node_id);
if (it_result == objects_.end()) {
return nullptr;
}
AXObject* result = it_result->value;
DCHECK(result) << "AXID#" << node_id
<< " in map, but matches an AXObject of null, for " << node;
// When shutting down, allow detached nodes to be in the map, and do not
// attempt invalidations.
if (has_been_disposed_) {
return result->IsDetached() ? nullptr : result;
}
DCHECK(!result->IsDetached()) << "Detached object was in map.";
return result;
}
AXObject* AXObjectCacheImpl::Get(AbstractInlineTextBox* inline_text_box) {
if (!inline_text_box)
return nullptr;
auto it_ax = inline_text_box_object_mapping_.find(inline_text_box);
AXID ax_id =
it_ax != inline_text_box_object_mapping_.end() ? it_ax->value : 0;
if (!ax_id)
return nullptr;
DCHECK(!WTF::IsHashTraitsEmptyOrDeletedValue<HashTraits<AXID>>(ax_id));
auto it_result = objects_.find(ax_id);
AXObject* result = it_result != objects_.end() ? it_result->value : nullptr;
#if DCHECK_IS_ON()
DCHECK(result) << "Had AXID for inline text box but no entry in objects_";
DCHECK(result->IsAXInlineTextBox());
// Do not allow detached objects except when disposing entire tree.
DCHECK(!result->IsDetached() || has_been_disposed_)
<< "Detached AXInlineTextBox in map: "
<< "AXID#" << ax_id << " Node=" << inline_text_box->GetText();
#endif
return result;
}
AXObject* AXObjectCacheImpl::Get(AccessibleNode* accessible_node) {
if (!accessible_node)
return nullptr;
if (accessible_node->element()) {
DCHECK(!accessible_node_mapping_.Contains(accessible_node))
<< "The accessible node directly attached to an element should not "
"have its own AXObject: "
<< accessible_node->element();
// When the AccessibleNode is attached to an element, return the element's
// accessible object instead.
return Get(accessible_node->element());
}
auto it_ax = accessible_node_mapping_.find(accessible_node);
AXID ax_id = it_ax != accessible_node_mapping_.end() ? it_ax->value : 0;
if (!ax_id)
return nullptr;
DCHECK(!WTF::IsHashTraitsEmptyOrDeletedValue<HashTraits<AXID>>(ax_id));
auto it_result = objects_.find(ax_id);
AXObject* result = it_result != objects_.end() ? it_result->value : nullptr;
#if DCHECK_IS_ON()
DCHECK(result) << "Had AXID for accessible_node but no entry in objects_";
DCHECK(IsA<AXVirtualObject>(result));
// Do not allow detached objects except when disposing entire tree.
DCHECK(!result->IsDetached() || has_been_disposed_)
<< "Detached AXVirtualObject in map: "
<< "AXID#" << ax_id << " Node=" << accessible_node->element();
#endif
return result;
}
AXObject* AXObjectCacheImpl::GetAXImageForMap(HTMLMapElement& map) {
// Find first child node of <map> that has an AXObject and return it's
// parent, which should be a native image.
Node* child = LayoutTreeBuilderTraversal::FirstChild(map);
while (child) {
if (AXObject* ax_child = Get(child)) {
if (AXObject* ax_image = ax_child->CachedParentObject()) {
if (ax_image->IsDetached()) {
return nullptr;
}
DCHECK(IsA<HTMLImageElement>(ax_image->GetNode()))
<< "Expected image AX parent of <map>'s DOM child, got: "
<< ax_image->GetNode() << "\n* Map's DOM child was: " << child
<< "\n* ax_image: " << ax_image->ToString(true, true);
return ax_image;
}
}
child = LayoutTreeBuilderTraversal::NextSibling(*child);
}
return nullptr;
}
AXObject* AXObjectCacheImpl::CreateFromRenderer(LayoutObject* layout_object) {
Node* node = layout_object->GetNode();
// media element
if (node && node->IsMediaElement())
return AccessibilityMediaElement::Create(layout_object, *this);
if (node && node->IsMediaControlElement())
return AccessibilityMediaControl::Create(layout_object, *this);
if (IsA<HTMLOptionElement>(node))
return MakeGarbageCollected<AXListBoxOption>(layout_object, *this);
if (auto* html_input_element = DynamicTo<HTMLInputElement>(node)) {
FormControlType type = html_input_element->FormControlType();
if (type == FormControlType::kInputRange) {
return MakeGarbageCollected<AXSlider>(layout_object, *this);
}
}
if (auto* select_element = DynamicTo<HTMLSelectElement>(node)) {
if (select_element->UsesMenuList()) {
if (use_ax_menu_list_) {
DCHECK(ShouldCreateAXMenuListFor(layout_object));
return MakeGarbageCollected<AXMenuList>(layout_object, *this);
}
} else {
return MakeGarbageCollected<AXListBox>(layout_object, *this);
}
}
if (IsA<HTMLProgressElement>(node)) {
return MakeGarbageCollected<AXProgressIndicator>(layout_object, *this);
}
return MakeGarbageCollected<AXLayoutObject>(layout_object, *this);
}
// Returns true if |node| is an <option> element and its parent <select>
// is a menu list (not a list box).
// static
bool AXObjectCacheImpl::ShouldCreateAXMenuListOptionFor(const Node* node) {
auto* option_element = DynamicTo<HTMLOptionElement>(node);
if (!option_element)
return false;
if (auto* select = option_element->OwnerSelectElement())
return ShouldCreateAXMenuListFor(select->GetLayoutObject());
return false;
}
// static
bool AXObjectCacheImpl::ShouldCreateAXMenuListFor(LayoutObject* layout_object) {
if (!layout_object)
return false;
if (!AXObjectCacheImpl::UseAXMenuList())
return false;
if (auto* select = DynamicTo<HTMLSelectElement>(layout_object->GetNode()))
return select->UsesMenuList();
return false;
}
// static
bool AXObjectCacheImpl::IsRelevantSlotElement(const HTMLSlotElement& slot) {
DCHECK(AXObject::CanSafelyUseFlatTreeTraversalNow(slot.GetDocument()));
DCHECK(slot.SupportsAssignment());
// Don't use a <slot> inside of an AXMenuList.
// TODO(accessibility) Remove AXMenuList and follow the shadow DOM.
if (slot.IsInUserAgentShadowRoot()) {
if (HTMLSelectElement* select =
DynamicTo<HTMLSelectElement>(slot.OwnerShadowHost())) {
if (ShouldCreateAXMenuListFor(select->GetLayoutObject())) {
return false;
}
}
}
// HasAssignedNodesNoRecalc() will return false when the slot is not in the
// flat tree. We must also return true when the slot has ordinary children
// (fallback content).
return slot.HasAssignedNodesNoRecalc() || slot.hasChildren();
}
// static
bool AXObjectCacheImpl::IsRelevantPseudoElement(const Node& node) {
DCHECK(node.IsPseudoElement());
if (!node.GetLayoutObject())
return false;
// ::before, ::after and ::marker are relevant.
// Allowing these pseudo elements ensures that all visible descendant
// pseudo content will be reached, despite only being able to walk layout
// inside of pseudo content.
// However, AXObjects aren't created for ::first-letter subtrees. The text
// of ::first-letter is already available in the child text node of the
// element that the CSS ::first letter applied to.
if (node.IsMarkerPseudoElement() || node.IsBeforePseudoElement() ||
node.IsAfterPseudoElement()) {
// Ignore non-inline whitespace content, which is used by many pages as
// a "Micro Clearfix Hack" to clear floats without extra HTML tags. See
// http://nicolasgallagher.com/micro-clearfix-hack/
if (node.GetLayoutObject()->IsInline())
return true; // Inline: not a clearfix hack.
if (!node.parentNode()->GetLayoutObject() ||
node.parentNode()->GetLayoutObject()->IsInline()) {
return true; // Parent inline: not a clearfix hack.
}
const ComputedStyle* style = node.GetLayoutObject()->Style();
DCHECK(style);
ContentData* content_data = style->GetContentData();
if (!content_data)
return true;
if (!content_data->IsText())
return true; // Not text: not a clearfix hack.
if (!To<TextContentData>(content_data)
->GetText()
.ContainsOnlyWhitespaceOrEmpty()) {
return true; // Not whitespace: not a clearfix hack.
}
return false; // Is the clearfix hack: ignore pseudo element.
}
// ::first-letter is relevant if and only if its parent layout object is a
// relevant pseudo element. If it's not a pseudo element, then this the
// ::first-letter text would end up being repeated in the AX Tree.
if (node.IsFirstLetterPseudoElement()) {
LayoutObject* layout_parent = node.GetLayoutObject()->Parent();
DCHECK(layout_parent);
Node* layout_parent_node = layout_parent->GetNode();
return layout_parent_node && layout_parent_node->IsPseudoElement() &&
IsRelevantPseudoElement(*layout_parent_node);
}
// The remaining possible pseudo element types are not relevant.
if (node.IsBackdropPseudoElement() || node.IsViewTransitionPseudoElement()) {
return false;
}
// If this is reached, then a new pseudo element type was added and is not
// yet handled by accessibility. See PseudoElementTagName() in
// pseudo_element.cc for all possible types.
SANITIZER_NOTREACHED() << "Unhandled type of pseudo element on: " << node;
return false;
}
// static
bool AXObjectCacheImpl::IsRelevantPseudoElementDescendant(
const LayoutObject& layout_object) {
if (layout_object.IsText() && To<LayoutText>(layout_object).HasEmptyText())
return false;
const LayoutObject* ancestor = &layout_object;
while (true) {
ancestor = ancestor->Parent();
if (!ancestor)
return false;
if (ancestor->IsPseudoElement()) {
// When an ancestor is exposed using CSS alt text, descendants are pruned.
if (AXNodeObject::GetCSSAltText(To<Element>(ancestor->GetNode()))) {
return false;
}
return IsRelevantPseudoElement(*ancestor->GetNode());
}
if (!ancestor->IsAnonymous())
return false;
}
}
AXObject* AXObjectCacheImpl::CreateFromNode(Node* node) {
if (ShouldCreateAXMenuListOptionFor(node)) {
return MakeGarbageCollected<AXMenuListOption>(To<HTMLOptionElement>(node),
*this);
}
if (auto* area = DynamicTo<HTMLAreaElement>(node))
return MakeGarbageCollected<AXImageMapLink>(area, *this);
return MakeGarbageCollected<AXNodeObject>(node, *this);
}
AXObject* AXObjectCacheImpl::CreateFromInlineTextBox(
AbstractInlineTextBox* inline_text_box) {
return MakeGarbageCollected<AXInlineTextBox>(inline_text_box, *this);
}
AXObject* AXObjectCacheImpl::GetOrCreate(AccessibleNode* accessible_node,
AXObject* parent) {
if (AXObject* obj = Get(accessible_node))
return obj;
// New AXObjects cannot be created when the tree is frozen.
if (IsFrozen()) {
return nullptr;
}
DCHECK_EQ(accessible_node->GetDocument(), &GetDocument());
DCHECK(parent)
<< "A virtual object must have a parent, and cannot exist without one. "
"The parent is set when the object is constructed.";
DCHECK(!accessible_node->element())
<< "The accessible node directly attached to an element should not "
"have its own AXObject, since the AXObject will be keyed off of the "
"element instead: "
<< accessible_node->element();
if (!parent->CanHaveChildren())
return nullptr;
AXObject* new_obj =
MakeGarbageCollected<AXVirtualObject>(*this, accessible_node);
const AXID ax_id = AssociateAXID(new_obj);
accessible_node_mapping_.Set(accessible_node, ax_id);
new_obj->Init(parent);
return new_obj;
}
AXObject* AXObjectCacheImpl::GetOrCreate(const Node* node) {
return GetOrCreate(node, nullptr);
}
AXObject* AXObjectCacheImpl::GetOrCreate(Node* node) {
return GetOrCreate(node, nullptr);
}
AXObject* AXObjectCacheImpl::GetOrCreate(const Node* node,
AXObject* parent_if_known) {
return GetOrCreate(const_cast<Node*>(node), parent_if_known);
}
AXObject* AXObjectCacheImpl::GetOrCreate(Node* node,
AXObject* parent_if_known) {
CHECK(IsProcessingDeferredEvents())
<< "Only create AXObjects while processing AX events and tree: " << node;
if (!node)
return nullptr;
if (AXObject* obj = Get(node)) {
// The object already exists.
CHECK(!obj->IsDetached());
if (!obj->IsMissingParent()) {
return obj;
}
if (parent_if_known) {
// The parent is provided when the object is being added to the parent.
// This is expected when re-adding a child to a parent via
// AXNodeObject::AddChildren(), as the parent on previous children
// will have been cleared immediately before re-adding any of them.
obj->SetParent(parent_if_known);
return obj;
}
// TODO(accessibility) Try to get rid of repair situations by addressing
// partial subtrees and mid-tree object removal directly when they occur.
return RepairChildrenOfIncludedParent(node);
}
return CreateAndInit(node, node->GetLayoutObject(), parent_if_known);
}
// TODO(accessibility) Try to get rid of repair situations by addressing
// partial subtrees and mid-tree object removal directly when they occur.
AXObject* AXObjectCacheImpl::RepairChildrenOfIncludedParent(Node* child) {
CHECK(Root());
// Create a list of ancestor nodes up to and including the first one included
// in the tree.
// These will be used to repair the local included subtree top down.
HeapDeque<Member<Node>> ancestors_to_repair;
Node* ancestor = child;
AXObject* ax_ancestor = Get(ancestor);
AXObject* ax_included_ancestor = nullptr;
while (true) {
// Check for an aria-owns relation. If one is found, both the owner and
// child's AXObject will exist, with correctly set parent-child relations.
if (AXObject* ax_owner =
relation_cache_->GetOrCreateAriaOwnerFor(ancestor, ax_ancestor)) {
// There cannot be any other ancestors to repair, brecause both aria-owns
// parents and children are both always included in the tree.
// Create the child with the aria-owns parent as its parent.
GetOrCreate(ancestor, ax_owner);
if (AXObject* ax_owned_child = Get(ancestor)) {
if (IsAriaOwned(ax_owned_child) &&
ax_owned_child->CachedParentObject() == ax_owner) {
// aria-owns relation was valid and created the child.
CHECK(ax_owned_child->AccessibilityIsIncludedInTree());
CHECK(ax_owner->AccessibilityIsIncludedInTree());
CHECK_GE(ax_owned_child->IndexInParent(), 0);
ax_included_ancestor = ax_owned_child;
break;
}
// Detach from parent, so that when we fall through, it can be
// reattached when the correct parent adds it as a child.
ax_owned_child->DetachFromParent();
// Ensure that the cached values are recomputed with the new parent as
// a basis, since some cached values are inherited from the parent.
ax_owned_child->InvalidateCachedValues();
}
// aria-owns relation was not valid, fall through to using DOM ancestry.
}
// Loop stops on included ancestor, and the root is always included.
CHECK(!ax_ancestor || !ax_ancestor->IsRoot()) << "Should not reach root.";
// Get or compute the next ancestor's Node and AXObject.
AXObject* next_ax_ancestor = nullptr;
if (ax_ancestor && ax_ancestor->CachedParentObject()) {
// Use computed parent if available, it could be from aria-owns.
next_ax_ancestor = ax_ancestor->CachedParentObject();
} else if (ancestor) {
next_ax_ancestor = AXObject::ComputeNonARIAParent(*this, ancestor);
}
if (!next_ax_ancestor) {
// A parent was not reached. An example is a node that is no longer in the
// flat tree, such as a slot that is unassigned.
RemoveSubtreeWhenSafe(ancestor ? ancestor : child);
return nullptr;
}
ax_ancestor = next_ax_ancestor;
ancestor = next_ax_ancestor->GetNode();
// Push this ancestor to the front, as the subtree will be built top-down
// in the next loop.
if (ancestor) {
ancestors_to_repair.push_front(ancestor);
}
if (ax_ancestor) {
if (ax_ancestor->LastKnownIsIncludedInTreeValue() &&
!ancestors_to_repair.empty()) {
ax_included_ancestor = ax_ancestor;
break;
}
ax_ancestor->SetNeedsToUpdateChildren();
}
}
// The first included ancestor needs to update its children.
ax_included_ancestor->ChildrenChangedWithCleanLayout();
// Starting from the highest ancestor, add children.
for (Node* ancestor_to_repair : ancestors_to_repair) {
ax_ancestor = Get(ancestor_to_repair);
if (!ax_ancestor || ax_ancestor->IsDetached()) {
RemoveSubtreeWhenSafe(ancestor_to_repair);
return nullptr;
}
ax_ancestor->UpdateChildrenIfNecessary();
}
CHECK(!ax_included_ancestor->NeedsToUpdateChildren());
AXObject* result = Get(child);
if (!result || result->IsDetached()) {
RemoveSubtreeWhenSafe(child);
return nullptr;
}
return result;
}
// Caller must provide a node, a layout object, or both (where they match).
AXObject* AXObjectCacheImpl::CreateAndInit(Node* node,
LayoutObject* layout_object,
AXObject* parent_if_known) {
// New AXObjects cannot be created when the tree is frozen.
// In this state, the tree should already be complete because
// of UpdateTreeIfNeeded().
CHECK(IsProcessingDeferredEvents())
<< "Only create AXObjects while processing AX events and tree: " << node
<< " " << layout_object;
#if DCHECK_IS_ON()
DCHECK(node || layout_object);
DCHECK(!node || !layout_object || layout_object->GetNode() == node);
DCHECK(!parent_if_known || parent_if_known->CanHaveChildren());
DCHECK(GetDocument().Lifecycle().GetState() >=
DocumentLifecycle::kAfterPerformLayout)
<< "Unclean document at lifecycle "
<< GetDocument().Lifecycle().ToString();
#endif // DCHECK_IS_ON()
CHECK(!has_been_disposed_)
<< "Don't attempt to create AXObject during teardown: " << node << " "
<< layout_object;
// The object must be in one of two documents:
// 1. The main document retrieved from GetDocument().
// 2. The popup document retrieved from GetPopupDocumentIfShowing(). Note
// that this must be null iff the current popup is <select size=1>, as
// we do not want to create objects inside of that kind of popup document
// since they would be duplicating objects in the subtree built by
// AXMenuList* classes.
// TODO(accessibility) turn into a CHECK once we stop supporting AXMenuList.
Document& document_for_ax_object =
node ? node->GetDocument() : layout_object->GetDocument();
if (document_for_ax_object != GetDocument() &&
&document_for_ax_object != GetPopupDocumentIfShowing()) {
return nullptr;
}
// Determine the type of accessibility object to be created.
AXObjectType ax_type =
DetermineAXObjectType(node, layout_object, parent_if_known);
if (ax_type == kPruneSubtree) {
return nullptr;
}
#if DCHECK_IS_ON()
if (node) {
DCHECK(layout_object || ax_type != kAXLayoutObject);
DCHECK(node->isConnected());
DCHECK(node->GetDocument().GetFrame())
<< "Creating AXObject in a dead document: " << node;
DCHECK(node->IsElementNode() || node->IsTextNode() ||
node->IsDocumentNode())
<< "Should only attempt to create AXObjects for the following types of "
"node types: document, element and text."
<< "\n* Node is: " << node;
} else {
// No node, therefore the only possibility is to create an AXLayoutObject.
DCHECK(layout_object->GetDocument().GetFrame())
<< "Creating AXObject in a dead document: " << layout_object;
DCHECK_EQ(ax_type, kAXLayoutObject);
DCHECK(!IsA<LayoutView>(layout_object))
<< "AXObject for document is always created with a node.";
}
#endif
// Determine the parent.
AXObject* parent = nullptr;
if (parent_if_known) {
// Parent is known because the tree is being explored downward, and as the
// parent adds its children it passes itself in.
parent = parent_if_known;
} else if (node == &GetDocument()) {
// The root object does not have a parent.
parent = nullptr;
} else {
// The AXObject is being created in the middle of the tree, without a known
// parent. First compute the parent, and then add all of its children.
// If the AXObject for |node| was created during that process, we will
// return it. If not, that means the entire subtree is not viable, and
// therefore we ensure it is removed, and null is returned.
CHECK(node)
<< "AXLayoutObjects without a node are pseudo element descendants, and "
"they must be created with a passed-in |parent_if_known|: "
<< layout_object;
// If the node is for a document, it must be a popup document to reach here.
if (auto* document = DynamicTo<Document>(node)) {
CHECK(document->GetFrame() && document->GetFrame()->PagePopupOwner());
}
// TODO(accessibility) Try to get rid of repair situations by addressing
// partial subtrees and mid-tree object removal directly when they occur.
if (AXObject* result = RepairChildrenOfIncludedParent(node)) {
// If this is the target of an aria-activedescendant relation, fire the
// relevant events.
CHECK(relation_cache_);
relation_cache_->UpdateRelatedActiveDescendant(node);
return result;
}
return nullptr;
}
// If there is a DOM node, use its dom_node_id, otherwise, generate an AXID.
// The dom_node_id can be used even if there is also a layout object.
AXID axid;
if (node) {
axid = static_cast<AXID>(node->GetDomNodeId());
if (ax_tree_serializer_) {
// In the case where axid is being reused, because a previous AXObject
// existed for the same node, ensure that the serializer sees it as new.
ax_tree_serializer_->MarkNodeDirty(axid);
}
} else {
axid = GenerateAXID();
}
DCHECK(!base::Contains(objects_, axid));
// Create the new AXObject.
AXObject* new_obj = nullptr;
if (ax_type == kAXLayoutObject) {
// Prefer to create from renderer if there is a layout object because
// AXLayoutObjects can provide information about bounding boxes.
if (!node) {
DCHECK(!layout_object_mapping_.Contains(layout_object))
<< "Already have an AXObject for " << layout_object;
layout_object_mapping_.Set(layout_object, axid);
}
new_obj = CreateFromRenderer(layout_object);
} else {
new_obj = CreateFromNode(node);
}
DCHECK(new_obj) << "Could not create AXObject.";
// Give the AXObject its ID and initialize.
AssociateAXID(new_obj, axid);
new_obj->Init(parent);
// Process new relations.
// Only elements (non-pseudo ones) can have relations.
if (IsA<Element>(node) && !node->IsPseudoElement()) {
CHECK(relation_cache_);
// Register incomplete relations with the relation cache, so that when the
// target id shows up at a later time, the source node can be reserialized
// with the completed relation.
relation_cache_->RegisterIncompleteRelations(new_obj);
#if DCHECK_IS_ON()
// Ensure that the relation cache is properly initialized with information
// from this element.
relation_cache_->CheckRelationsCached(*To<Element>(node));
#endif
}
return new_obj;
}
AXObject* AXObjectCacheImpl::GetOrCreate(LayoutObject* layout_object) {
return GetOrCreate(layout_object, nullptr);
}
AXObject* AXObjectCacheImpl::GetOrCreate(LayoutObject* layout_object,
AXObject* parent_if_known) {
CHECK(IsProcessingDeferredEvents())
<< "Only create AXObjects while processing AX events and tree: "
<< layout_object;
if (!layout_object)
return nullptr;
if (AXObject* obj = Get(layout_object, parent_if_known)) {
return obj;
}
return CreateAndInit(layout_object->GetNode(), layout_object,
parent_if_known);
}
AXObject* AXObjectCacheImpl::GetOrCreate(AbstractInlineTextBox* inline_text_box,
AXObject* parent) {
CHECK(IsProcessingDeferredEvents())
<< "Only create AXObjects while processing AX events and tree";
if (!inline_text_box)
return nullptr;
if (!parent) {
LayoutText* layout_text_parent = inline_text_box->GetLayoutText();
DCHECK(layout_text_parent);
parent = GetOrCreate(layout_text_parent);
if (!parent) {
DCHECK(inline_text_box->GetText().ContainsOnlyWhitespaceOrEmpty() ||
IsFrozen() ||
!IsRelevantPseudoElementDescendant(*layout_text_parent))
<< "No parent for non-whitespace inline textbox: "
<< layout_text_parent
<< "\nParent of parent: " << layout_text_parent->Parent();
return nullptr;
}
}
// 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.
if (parent->LastKnownIsIgnoredValue()) {
return nullptr;
}
if (AXObject* obj = Get(inline_text_box)) {
#if DCHECK_IS_ON()
DCHECK(!obj->IsDetached())
<< "AXObject for inline text box should not be detached: "
<< obj->ToString(true, true);
// AXInlineTextbox objects can't get a new parent, unlike other types of
// accessible objects that can get a new parent because they moved or
// because of aria-owns.
// AXInlineTextbox objects are only added via AddChildren() on static text
// or line break parents. The children are cleared, and detached from their
// parent before AddChildren() executes. There should be no previous parent.
DCHECK(parent->RoleValue() == ax::mojom::blink::Role::kStaticText ||
parent->RoleValue() == ax::mojom::blink::Role::kLineBreak);
DCHECK(!obj->CachedParentObject() || obj->CachedParentObject() == parent)
<< "Mismatched old and new parent:"
<< "\n* Old parent: " << obj->CachedParentObject()->ToString(true, true)
<< "\n* New parent: " << parent->ToString(true, true);
DCHECK(ui::CanHaveInlineTextBoxChildren(parent->RoleValue()))
<< "Unexpected parent of inline text box: " << parent->RoleValue();
#endif
DCHECK(obj->ParentObject() == parent);
return obj;
}
// New AXObjects cannot be created when the tree is frozen.
if (IsFrozen()) {
return nullptr;
}
AXObject* new_obj = CreateFromInlineTextBox(inline_text_box);
AXID axid = AssociateAXID(new_obj);
inline_text_box_object_mapping_.Set(inline_text_box, axid);
new_obj->Init(parent);
return new_obj;
}
AXObject* AXObjectCacheImpl::CreateAndInit(ax::mojom::blink::Role role,
AXObject* parent) {
DCHECK(parent);
DCHECK(parent->CanHaveChildren());
AXObject* obj = nullptr;
switch (role) {
case ax::mojom::blink::Role::kMenuListPopup:
DCHECK(use_ax_menu_list_);
obj = MakeGarbageCollected<AXMenuListPopup>(*this);
break;
default:
obj = nullptr;
}
if (!obj)
return nullptr;
AssociateAXID(obj);
obj->Init(parent);
return obj;
}
void AXObjectCacheImpl::Remove(AXObject* object, bool notify_parent) {
DCHECK(object);
if (object->IsAXInlineTextBox()) {
Remove(object->GetInlineTextBox(), notify_parent);
} else if (object->GetNode()) {
Remove(object->GetNode(), notify_parent);
} else if (object->GetLayoutObject()) {
Remove(object->GetLayoutObject(), notify_parent);
} else if (object->GetAccessibleNode()) {
Remove(object->GetAccessibleNode(), notify_parent);
} else {
Remove(object->AXObjectID(), notify_parent);
}
}
// This is safe to call even if there isn't a current mapping.
// This is called by other Remove() methods, called by Blink for DOM and layout
// changes, iterating over all removed content in the subtree:
// - When a DOM subtree is removed, it is called with the root node first, and
// then descending down into the subtree.
// - When layout for a subtree is detached, it is called on layout objects,
// starting with leaves and moving upward, ending with the subtree root.
void AXObjectCacheImpl::Remove(AXID ax_id, bool notify_parent) {
DCHECK(!IsFrozen());
if (!ax_id)
return;
// First, fetch object to operate some cleanup functions on it.
auto it = objects_.find(ax_id);
AXObject* obj = it != objects_.end() ? it->value : nullptr;
if (!obj)
return;
#if DCHECK_IS_ON()
if (obj->LastKnownIsIncludedInTreeValue()) {
--included_node_count_;
}
#endif
if (!has_been_disposed_) {
if (notify_parent) {
ChildrenChangedOnAncestorOf(obj);
}
// TODO(aleventhal) This is for web tests only, in order to record MarkDirty
// events. Is there a way to avoid these calls for normal browsing?
// Maybe we should use dependency injection from AccessibilityController.
if (auto* client = GetWebLocalFrameClient()) {
client->HandleAXObjectDetachedForTest(ax_id);
}
}
// Remove references to AXID before detaching, so that nothing will retrieve a
// detached object, which is illegal.
RemoveReferencesToAXID(ax_id);
obj->Detach();
// Remove the object.
// TODO(accessibility) We don't use the return value, can we use .erase()
// and it will still make sure that the object is cleaned up?
objects_.Take(ax_id);
// Removing an aria-modal dialog can affect the entire tree.
if (active_aria_modal_dialog_ &&
active_aria_modal_dialog_ == obj->GetElement()) {
Settings* settings = GetSettings();
if (settings && settings->GetAriaModalPrunesAXTree()) {
MarkDocumentDirty();
}
active_aria_modal_dialog_ = nullptr;
}
}
// This is safe to call even if there isn't a current mapping.
void AXObjectCacheImpl::Remove(AccessibleNode* accessible_node) {
Remove(accessible_node, /* notify_parent */ true);
}
void AXObjectCacheImpl::Remove(AccessibleNode* accessible_node,
bool notify_parent) {
DCHECK(accessible_node);
auto iter = accessible_node_mapping_.find(accessible_node);
if (iter == accessible_node_mapping_.end())
return;
AXID ax_id = iter->value;
accessible_node_mapping_.erase(iter);
Remove(ax_id, notify_parent);
}
void AXObjectCacheImpl::Remove(LayoutObject* layout_object,
bool notify_parent) {
CHECK(layout_object);
if (IsA<LayoutView>(layout_object)) {
// A document is being destroyed.
// This code is only reached when it is a popup being destroyed.
// TODO(accessibility) Can we remove this case since Blink calls
// RemovePopup(document) for us?
DCHECK(!popup_document_ ||
popup_document_ == &layout_object->GetDocument());
// Popup has been destroyed.
if (popup_document_) {
RemovePopup(popup_document_);
}
}
// If a DOM node is present, it will have been used to back the AXObject, in
// which case we need to call Remove(node) instead.
if (Node* node = layout_object->GetNode()) {
// Pseudo elements are a special case. The entire subtree needs to be marked
// dirty so that it is recomputed (it is disappearing or changing).
if (node->IsPseudoElement()) {
MarkSubtreeDirty(node);
}
if (IsA<HTMLImageElement>(node)) {
// If an image is removed, ensure its entire subtree is deleted as there
// may have been children supplied via a map.
if (auto* layout_image =
DynamicTo<LayoutImage>(node->GetLayoutObject())) {
if (auto* map = layout_image->ImageMap()) {
if (map->ImageElement() == node) {
RemoveSubtreeWithFlatTraversal(map, /*remove_root*/ false);
}
}
}
}
Remove(node, notify_parent);
return;
}
auto iter = layout_object_mapping_.find(layout_object);
if (iter == layout_object_mapping_.end())
return;
AXID ax_id = iter->value;
DCHECK(ax_id);
layout_object_mapping_.erase(iter);
Remove(ax_id, false);
}
// This is safe to call even if there isn't a current mapping.
void AXObjectCacheImpl::Remove(Node* node) {
Remove(node, /* notify_parent */ true);
}
void AXObjectCacheImpl::Remove(Node* node, bool notify_parent) {
DCHECK(node);
LayoutObject* layout_object = node->GetLayoutObject();
DCHECK(!layout_object || layout_object_mapping_.find(layout_object) ==
layout_object_mapping_.end())
<< "AXObject cannot be backed by both a layout object and node.";
AXID axid = node->GetDomNodeId();
whitespace_ignored_map_.erase(axid);
if (node == active_aria_modal_dialog_) {
UpdateActiveAriaModalDialog(FocusedNode());
}
DCHECK_GE(axid, 1);
Remove(axid, notify_parent);
}
void AXObjectCacheImpl::RemovePopup(Document* popup_document) {
// The only 2 documents that partake in the cache are the main document and
// the popup document. This method is only be called for the popup document,
// because if the main document is shutting down, the cache is disposed.
DCHECK(popup_document);
// This can be called even when GetPopupDocumentIfShowing() when the popup
// is from a <select size=1>, which is a special case since AXMenuList*
// classes build their subtrees manually, and in order to avoid duplicate
// objects, which treat that situations as if there is no popup showing.
// TODO(accessibility) Remove this early return once AXMenuList* is removed,
// because at that point, the popup document will not be null in the
// select size=1 case anymore.
if (!GetPopupDocumentIfShowing()) {
return;
}
DCHECK(IsPopup(*popup_document)) << "Use Dispose() to remove main document.";
RemoveSubtreeWhenSafe(popup_document);
popup_document_ = nullptr;
notifications_to_post_popup_.clear();
tree_update_callback_queue_popup_.clear();
}
// This is safe to call even if there isn't a current mapping.
void AXObjectCacheImpl::Remove(AbstractInlineTextBox* inline_text_box) {
Remove(inline_text_box, /* notify_parent */ true);
}
void AXObjectCacheImpl::Remove(AbstractInlineTextBox* inline_text_box,
bool notify_parent) {
if (!inline_text_box)
return;
auto iter = inline_text_box_object_mapping_.find(inline_text_box);
if (iter == inline_text_box_object_mapping_.end())
return;
AXID ax_id = iter->value;
inline_text_box_object_mapping_.erase(iter);
Remove(ax_id, notify_parent);
}
void AXObjectCacheImpl::RemoveIncludedSubtree(AXObject* object,
bool remove_root) {
DCHECK(object);
if (object->IsDetached()) {
return;
}
for (const auto& ax_child : object->CachedChildrenIncludingIgnored()) {
RemoveIncludedSubtree(ax_child, /* remove_root */ true);
}
if (remove_root) {
Remove(object, /* notify_parent */ false);
}
}
void AXObjectCacheImpl::RemoveAXObjectsInLayoutSubtree(
LayoutObject* subtree_root) {
Remove(subtree_root, /*notify_parent*/ true);
LayoutObject* iter = subtree_root;
while ((iter = iter->NextInPreOrder(subtree_root)) != nullptr) {
Remove(iter, /*notify_parent*/ false);
}
}
void AXObjectCacheImpl::RemoveAXObjectsInLayoutSubtree(Node* subtree_root) {
// Remove the root immediately in order to avoid DCHECK(!has_ax_object_)
// in LayoutObject::Destroy() when removing a subtrree from
// DetachLayoutSubtree().
Remove(subtree_root);
// Remove the rest when safe (when flat tree traversal is allowed, and
// slot assignments are complete).
RemoveSubtreeWhenSafe(subtree_root, /*remove_root*/ false);
}
void AXObjectCacheImpl::ProcessSubtreeRemovals() {
for (auto& node : nodes_for_subtree_removal_) {
ProcessSubtreeRemoval(node.first, node.second);
}
nodes_for_subtree_removal_.clear();
}
void AXObjectCacheImpl::ProcessSubtreeRemoval(Node* node, bool remove_root) {
if (remove_root) {
RemoveSubtreeWithFlatTraversal(node, /* remove root */ true,
/* notify_parent */ true);
} else {
if (IsA<ShadowRoot>(node)) {
node = &To<ShadowRoot>(node)->host();
}
for (Node* child_node = LayoutTreeBuilderTraversal::FirstChild(*node);
child_node;
child_node = LayoutTreeBuilderTraversal::NextSibling(*child_node)) {
RemoveSubtreeWithFlatTraversal(child_node, /* remove root */ true,
/* notify_parent */ true);
}
}
}
void AXObjectCacheImpl::RemoveSubtreeWhenSafe(Node* node, bool remove_root) {
if (!node || !node->isConnected()) {
return;
}
if (AXObject::CanSafelyUseFlatTreeTraversalNow(node->GetDocument())) {
ProcessSubtreeRemoval(node, remove_root);
return;
}
nodes_for_subtree_removal_.push_back(std::make_pair(node, remove_root));
}
void AXObjectCacheImpl::RemoveSubtreeWithFlatTraversal(const Node* node,
bool remove_root,
bool notify_parent) {
DCHECK(node);
// Previously used DCHECK(AXObject::CanSafelyUseFlatTreeTraversalNow()) but
// failed because document had pending slot assignment in
// external/wpt/dom/nodes/node-appendchild-crash.html.
DCHECK(!node->GetDocument().IsFlatTreeTraversalForbidden());
AXObject* object = Get(node);
if (!object && !remove_root) {
// Nothing remaining to do for this subtree. Already removed.
return;
}
if (!IsA<ShadowRoot>(node)) {
// Remove children found through flat traversal.
for (Node* child_node = LayoutTreeBuilderTraversal::FirstChild(*node);
child_node;
child_node = LayoutTreeBuilderTraversal::NextSibling(*child_node)) {
RemoveSubtreeWithFlatTraversal(child_node, /* remove_root */ true,
/* notify_parent */ false);
}
}
if (!object) {
return;
}
// When removing children, use the cached children to avoid creating a child
// just to destroy it.
for (AXObject* ax_included_child : object->CachedChildrenIncludingIgnored()) {
if (ax_included_child->CachedParentObject() != object) {
continue;
}
if (ui::CanHaveInlineTextBoxChildren(object->RoleValue())) {
// Just remove child inline textboxes, don't use their node which is the
// same as that static text's parent and would cause an infinite loop.
Remove(ax_included_child, /* notify_parent */ false);
} else if (ax_included_child->GetNode()) {
DCHECK(ax_included_child->GetNode() != node);
RemoveSubtreeWithFlatTraversal(ax_included_child->GetNode(),
/* remove_root */ true,
/* notify_parent */ false);
} else {
RemoveIncludedSubtree(ax_included_child, /* remove_root */ true);
}
}
// The code below uses ChildrenChangedWithCleanLayout() instead of
// notify_parent param in Remove(), which would be queued, and it needs to
// happen immediately.
AXObject* parent_to_notify =
notify_parent ? object->CachedParentObject() : nullptr;
if (remove_root) {
Remove(object, /* notify_parent */ false);
}
if (parent_to_notify) {
if (processing_deferred_events_) {
ChildrenChangedWithCleanLayout(parent_to_notify);
} else {
ChildrenChanged(parent_to_notify);
}
}
}
// All generated AXIDs are negative, ranging from kFirstGeneratedRendererNodeID
// to kLastGeneratedRendererNodeID, in order to avoid conflict with the ids
// reused from dom_node_ids, which are positive, and generated IDs on the
// browser side, which are negative, starting at -1.
AXID AXObjectCacheImpl::GenerateAXID() const {
// The first id is close to INT_MIN/2, leaving plenty of room for negative
// generated IDs both here and on the browser side, but starting at an even
// number makes it easier to read when debugging.
static AXID last_used_id = ui::kFirstGeneratedRendererNodeID;
// Generate a new ID.
AXID obj_id = last_used_id;
do {
if (--obj_id == ui::kLastGeneratedRendererNodeID) {
// This is very unlikely to happen, but if we find that it happens, we
// could gracefully turn off a11y instead of crashing the renderer.
CHECK(!has_axid_generator_looped_)
<< "Not enough room more generated accessibility objects.";
has_axid_generator_looped_ = true;
obj_id = ui::kFirstGeneratedRendererNodeID;
}
} while (has_axid_generator_looped_ && objects_.Contains(obj_id));
DCHECK(!WTF::IsHashTraitsEmptyOrDeletedValue<HashTraits<AXID>>(obj_id));
last_used_id = obj_id;
return obj_id;
}
void AXObjectCacheImpl::AddToFixedOrStickyNodeList(const AXObject* object) {
DCHECK(object);
DCHECK(!object->IsDetached());
fixed_or_sticky_node_ids_.insert(object->AXObjectID());
}
AXID AXObjectCacheImpl::AssociateAXID(AXObject* obj, AXID use_axid) {
// Check for already-assigned ID.
DCHECK(!obj->AXObjectID()) << "Object should not already have an AXID";
AXID new_axid = use_axid ? use_axid : GenerateAXID();
bool should_have_node_id = obj->IsAXNodeObject() && obj->GetNode();
DCHECK_EQ(should_have_node_id, IsDOMNodeID(new_axid))
<< "An AXID is also a DOMNodeID (positive integer) if any only if the "
"AXObject is an AXNodeObject with a DOM node.";
obj->SetAXObjectID(new_axid);
objects_.Set(new_axid, obj);
return new_axid;
}
void AXObjectCacheImpl::RemoveReferencesToAXID(AXID obj_id) {
DCHECK(!WTF::IsHashTraitsDeletedValue<HashTraits<AXID>>(obj_id));
// Clear AXIDs from maps. Note: do not need to erase id from
// changed_bounds_ids_, a set which is cleared each time
// SerializeLocationChanges() is finished. Also, do not need to erase id from
// invalidated_ids_main_ or invalidated_ids_popup_, which are cleared each
// time ProcessInvalidatedObjects() finishes, and having extra ids in those
// sets is not harmful.
cached_bounding_boxes_.erase(obj_id);
if (IsDOMNodeID(obj_id)) {
// Optimization: these maps only contain ids for AXObjects with a DOM node.
fixed_or_sticky_node_ids_.erase(obj_id);
// Only objects with a DOM node can be in the relation cache.
if (relation_cache_) {
relation_cache_->RemoveAXID(obj_id);
}
// Allow the new AXObject for the same node to be serialized correctly.
nodes_with_pending_children_changed_.erase(obj_id);
computed_node_mapping_.erase(obj_id);
} else {
// Non-DOM ids should never find their way into these maps.
DCHECK(!fixed_or_sticky_node_ids_.Contains(obj_id));
DCHECK(!computed_node_mapping_.Contains(obj_id));
DCHECK(!nodes_with_pending_children_changed_.Contains(obj_id));
}
}
AXObject* AXObjectCacheImpl::NearestExistingAncestor(Node* node) {
// Find the nearest ancestor that already has an accessibility object, since
// we might be in the middle of a layout.
while (node) {
if (AXObject* obj = Get(node))
return obj;
node = node->parentNode();
}
return nullptr;
}
void AXObjectCacheImpl::UpdateNumTreeUpdatesQueuedBeforeLayoutHistogram() {
UMA_HISTOGRAM_COUNTS_100000(
"Blink.Accessibility.NumTreeUpdatesQueuedBeforeLayout",
tree_update_callback_queue_main_.size() +
tree_update_callback_queue_popup_.size());
}
void AXObjectCacheImpl::InvalidateBoundingBoxForFixedOrStickyPosition() {
for (AXID id : fixed_or_sticky_node_ids_)
changed_bounds_ids_.insert(id);
}
bool AXObjectCacheImpl::CanDeferTreeUpdate(Document* tree_update_document) {
DCHECK(!has_been_disposed_);
DCHECK(!IsFrozen());
if (!IsActive(GetDocument()) || tree_updates_paused_)
return false;
// Ensure the tree update document is in a good state.
if (!tree_update_document || !IsActive(*tree_update_document)) {
return false;
}
if (tree_update_document != document_) {
// If the popup_document_ is null, throw this tree update away, because:
// - Updates that occur BEFORE the popup is tracked in a11y don't matter,
// as we will build the entire popup's AXObject subtree once we are
// notified about the popup.
// - Updates that occur AFTER the popup is no longer tracked could occur
// while the popup is currently closing, in which case the updates are no
// longer useful.
if (!popup_document_) {
return false;
}
// If we are queuing an update to a document other than the main document,
// then it must be in an active popup document. The cache would never
// receive notifications from other documents.
DUMP_WILL_BE_CHECK_EQ(tree_update_document, popup_document_)
<< "Update in non-main, non-popup document: "
<< tree_update_document->Url().GetString();
}
return true;
}
bool AXObjectCacheImpl::PauseTreeUpdatesIfQueueFull() {
// Check the main document's queue. If there are too many entries, pause all
// updates and resume later after rebuilding the tree from scratch.
// Popup is excluded because it's controlled by us and will not have too many
// updates. In the case of a web page having too many updates, we need to
// clear all queues, including the popup's.
if (tree_update_callback_queue_main_.size() >= max_pending_updates_) {
UpdateNumTreeUpdatesQueuedBeforeLayoutHistogram();
tree_updates_paused_ = true;
LOG(INFO) << "Accessibility tree update queue is too big, updates have "
"been paused";
// Clear updates from both documents.
tree_update_callback_queue_main_.clear();
tree_update_callback_queue_popup_.clear();
notifications_to_post_main_.clear();
notifications_to_post_popup_.clear();
return true;
}
return false;
}
void AXObjectCacheImpl::DeferTreeUpdate(
AXObjectCacheImpl::TreeUpdateReason update_reason,
Node* node,
ax::mojom::blink::Event event) {
CHECK(node);
CHECK(!has_been_disposed_);
CHECK(!IsFrozen());
CHECK(!processing_deferred_events_)
<< "Call clean layout method directly while processing deferred events.";
CHECK(!updating_tree_);
Document& tree_update_document = node->GetDocument();
if (!CanDeferTreeUpdate(&tree_update_document)) {
return;
}
if (PauseTreeUpdatesIfQueueFull()) {
return;
}
TreeUpdateCallbackQueue& queue =
GetTreeUpdateCallbackQueue(tree_update_document);
TreeUpdateParams* tree_update = MakeGarbageCollected<TreeUpdateParams>(
node, 0u, ComputeEventFrom(), active_event_from_action_,
ActiveEventIntents(), update_reason, event);
queue.push_back(tree_update);
if (AXObject* obj = Get(node)) {
obj->InvalidateCachedValues();
}
// These events are fired during RunPostLifecycleTasks(),
// ensure there is a document lifecycle update scheduled.
if (IsImmediateProcessingRequired(tree_update)) {
// Ensure that processing of tree updates occurs immediately in cases
// where a user action such as focus or selection occurs, so that the user
// gets immediate feedback.
ScheduleImmediateSerialization();
} else {
// Otherwise, batch updates to improve performance.
ScheduleAXUpdate();
}
}
void AXObjectCacheImpl::DeferTreeUpdate(
AXObjectCacheImpl::TreeUpdateReason update_reason,
AXObject* obj,
ax::mojom::blink::Event event) {
// Called for updates that do not have a DOM node, e.g. a children or text
// changed event that occurs on an anonymous layout block flow.
CHECK(obj);
CHECK(!has_been_disposed_);
CHECK(!IsFrozen());
CHECK(!processing_deferred_events_)
<< "Call clean layout method directly while processing deferred events.";
CHECK(!updating_tree_);
if (obj->IsDetached()) {
return;
}
CHECK(obj->AXObjectID());
Document* tree_update_document = obj->GetDocument();
if (!CanDeferTreeUpdate(tree_update_document)) {
return;
}
if (PauseTreeUpdatesIfQueueFull()) {
return;
}
TreeUpdateCallbackQueue& queue =
GetTreeUpdateCallbackQueue(*tree_update_document);
queue.push_back(MakeGarbageCollected<TreeUpdateParams>(
nullptr, obj->AXObjectID(), ComputeEventFrom(), active_event_from_action_,
ActiveEventIntents(), update_reason, event));
obj->InvalidateCachedValues();
// These events are fired during RunPostLifecycleTasks(),
// ensure there is a document lifecycle update scheduled.
ScheduleAXUpdate();
}
void AXObjectCacheImpl::SelectionChanged(Node* node) {
if (!node)
return;
PostNotification(&GetDocument(),
ax::mojom::blink::Event::kDocumentSelectionChanged);
// If there is a text control, mark it dirty to serialize
// IntAttribute::kTextSelStart/kTextSelEnd changes.
// TODO(accessibility) Remove once we remove kTextSelStart/kTextSelEnd.
if (TextControlElement* text_control = EnclosingTextControl(node))
MarkElementDirty(text_control);
}
void AXObjectCacheImpl::StyleChanged(const LayoutObject* layout_object,
bool visibility_or_inertness_changed) {
DCHECK(layout_object);
SCOPED_DISALLOW_LIFECYCLE_TRANSITION();
AXObject* ax_object = Get(layout_object->GetNode());
if (!ax_object) {
// No object exists to mark dirty yet -- there can sometimes be a layout in
// the initial empty document, or style has changed before the object cache
// becomes aware that the node exists. It's too early for the style change
// to be useful.
return;
}
if (visibility_or_inertness_changed) {
ChildrenChanged(ax_object);
ChildrenChanged(ax_object->CachedParentObject());
}
MarkAXObjectDirty(ax_object);
}
void AXObjectCacheImpl::TextChanged(Node* node) {
if (!node)
return;
// A text changed event is redundant with children changed on the same node.
if (AXID node_id = static_cast<AXID>(node->GetDomNodeId())) {
if (nodes_with_pending_children_changed_.find(node_id) !=
nodes_with_pending_children_changed_.end()) {
return;
}
}
DeferTreeUpdate(TreeUpdateReason::kTextChangedOnNode, node);
}
// Return a node for the current layout object or ancestor layout object.
Node* AXObjectCacheImpl::GetClosestNodeForLayoutObject(
const LayoutObject* layout_object) {
if (!layout_object) {
return nullptr;
}
Node* node = layout_object->GetNode();
return node ? node : GetClosestNodeForLayoutObject(layout_object->Parent());
}
void AXObjectCacheImpl::TextChanged(const LayoutObject* layout_object) {
if (!layout_object)
return;
// The node may be null when the text changes on an anonymous layout object,
// such as a layout block flow that is inserted to parent an inline object
// when it has a block sibling.
Node* node = GetClosestNodeForLayoutObject(layout_object);
if (node) {
// If the text changed in a pseudo element, rebuild the entire subtree.
if (node->IsPseudoElement()) {
RemoveAXObjectsInLayoutSubtree(node->GetLayoutObject());
} else if (AXID node_id = static_cast<AXID>(node->GetDomNodeId())) {
// Text changed is redundant with children changed on the same node.
if (base::Contains(nodes_with_pending_children_changed_, node_id)) {
return;
}
}
DeferTreeUpdate(TreeUpdateReason::kTextChangedOnClosestNodeForLayoutObject,
node);
return;
}
if (Get(layout_object)) {
DeferTreeUpdate(TreeUpdateReason::kTextChangedOnLayoutObject,
Get(layout_object));
}
}
void AXObjectCacheImpl::TextChangedWithCleanLayout(
Node* optional_node_for_relation_update,
AXObject* obj) {
if (obj ? obj->IsDetached() : !optional_node_for_relation_update)
return;
#if DCHECK_IS_ON()
Document* document = obj ? obj->GetDocument()
: &optional_node_for_relation_update->GetDocument();
DCHECK(document->Lifecycle().GetState() >= DocumentLifecycle::kLayoutClean)
<< "Unclean document at lifecycle " << document->Lifecycle().ToString();
#endif // DCHECK_IS_ON()
if (obj) {
if (obj->RoleValue() == ax::mojom::blink::Role::kStaticText &&
obj->AccessibilityIsIncludedInTree()) {
if (obj->ShouldLoadInlineTextBoxes()) {
// Update inline text box children.
ChildrenChangedWithCleanLayout(optional_node_for_relation_update, obj);
return;
}
}
MarkAXObjectDirtyWithCleanLayout(obj);
}
if (optional_node_for_relation_update) {
CHECK(relation_cache_);
relation_cache_->UpdateRelatedTree(optional_node_for_relation_update, obj);
}
}
void AXObjectCacheImpl::TextChangedWithCleanLayout(Node* node) {
if (!node)
return;
DCHECK(!node->GetDocument().NeedsLayoutTreeUpdateForNode(*node));
TextChangedWithCleanLayout(node, Get(node));
}
void AXObjectCacheImpl::FocusableChangedWithCleanLayout(Node* node) {
Element* element = To<Element>(node);
DCHECK(!element->GetDocument().NeedsLayoutTreeUpdateForNode(*element));
AXObject* obj = Get(element);
if (!obj)
return;
if (obj->IsAriaHidden()) {
// Elements that are hidden but focusable are not ignored. Therefore, if a
// hidden element's focusable state changes, it's ignored state must be
// recomputed. It may be newly included in the tree, which means the
// parents must be updated. We invalidate the entire subtree so that
// the inclusion state is recomputed for all nodes potentially in a name
// from contents computation.
RemoveSubtreeWhenSafe(node);
return;
}
// Refresh the focusable state and State::kIgnored on the exposed object.
MarkAXObjectDirtyWithCleanLayout(obj);
}
void AXObjectCacheImpl::DocumentTitleChanged() {
DocumentLifecycle::DisallowTransitionScope disallow(document_->Lifecycle());
AXObject* root = Get(document_);
if (root)
PostNotification(root, ax::mojom::blink::Event::kDocumentTitleChanged);
}
bool AXObjectCacheImpl::IsReadyToProcessTreeUpdatesForNode(const Node* node) {
DCHECK(node);
// The maximum number of nodes after whitespace is parsed before a tree update
// should occur. The value was chosen based on what was needed to eliminate
// flakiness in existing tests and may need adjustment. Example: the
// `AccessibilityCSSPseudoElementsSeparatedByWhitespace` Yielding Parser test
// regularly fails if this value is set to 2, but passes if set to at least 3.
constexpr int kMaxAllowedTreeUpdatePauses = 3;
// If we have a node that must be fully parsed before updates can continue,
// we're ready to process tree updates only if that node has finished parsing
// its children. In this scenario, the maximum number of tree update pauses is
// irrelevant.
if (node_to_parse_before_more_tree_updates_) {
return node_to_parse_before_more_tree_updates_->IsFinishedParsingChildren();
}
// There should be no reason to pause for a script element. Plus if we pause
// for the script element, the slow-document-load.html web test fails.
if (IsA<HTMLScriptElement>(node)) {
return true;
}
if (auto* text = DynamicTo<Text>(node)) {
if (!text->ContainsOnlyWhitespaceOrEmpty()) {
return true;
}
// Whitespace at the end of parsed content is a problem because we won't
// know if that whitespace node is relevant until we have some text or a
// block node. And we won't know the layout of a node at connection time.
// Therefore, if this is a whitespace node, reset the maximum number of
// allowed pauses and wait.
allowed_tree_update_pauses_remaining_ = kMaxAllowedTreeUpdatePauses;
return false;
}
// If the node following a whitespace node is a pseudo element, we won't have
// its contents at the time the node is connected. Those contents can impact
// the relevance of the whitespace node. So remain paused if node is a pseudo
// element, without resetting the maximum number of allowed pauses.
if (node->IsPseudoElement()) {
return false;
}
// No new reason to pause, and there are no prior requested pauses remaining.
if (!allowed_tree_update_pauses_remaining_) {
return true;
}
// No new reason to pause, but we're not ready to unpause yet. So decrement
// the number of pauses requested and wait for the next connected node.
CHECK_GT(allowed_tree_update_pauses_remaining_, 0u);
allowed_tree_update_pauses_remaining_--;
return false;
}
void AXObjectCacheImpl::NodeIsConnected(Node* node) {
if (IsParsingMainDocument()) {
if (IsReadyToProcessTreeUpdatesForNode(node)) {
node_to_parse_before_more_tree_updates_ = nullptr;
allowed_tree_update_pauses_remaining_ = 0;
}
} else {
// Handle case where neither NodeIsAttached() nor SubtreeIsAttached() will
// be called for this node. This occurs for nodes that are added to
// display:none subtrees. Ensure that these nodes partake in the AX tree.
ChildrenChanged(node->parentNode());
}
// Process relations.
if (Element* element = DynamicTo<Element>(node)) {
if (relation_cache_) {
// Register relation ids so that reverse relations can be computed.
relation_cache_->CacheRelationIds(*element);
ScheduleAXUpdate();
}
if (AXObject::HasARIAOwns(element)) {
DeferTreeUpdate(TreeUpdateReason::kUpdateAriaOwns, element);
}
}
}
void AXObjectCacheImpl::UpdateAriaOwnsWithCleanLayout(Node* node) {
// Process any relation attributes that can affect ax objects already created.
// Force computation of aria-owns, so that original parents that already
// computed their children get the aria-owned children removed.
if (AXObject::HasARIAOwns(To<Element>(node))) {
if (AXObject* obj = Get(node)) {
CHECK(relation_cache_);
relation_cache_->UpdateAriaOwnsWithCleanLayout(obj);
}
}
}
void AXObjectCacheImpl::SubtreeIsAttached(Node* node) {
// If the node is the root of a display locked subtree, or was previously
// display:none, the entire AXObject subtree needs to be destroyed and rebuilt
// using AXLayoutObjects.
AXObject* obj = Get(node);
if (!obj) {
if (!node->GetLayoutObject() && !node->IsFinishedParsingChildren() &&
!node_to_parse_before_more_tree_updates_) {
// Unrendered subtrees that are not fully parsed are unsafe to
// process until they are complete, because there are no NodeIsAttached()
// signals for incrementally loaded content.
node_to_parse_before_more_tree_updates_ = node;
}
// No AX subtree to invalidate: just add an AXObject for this node.
// It will automatically add its subtree.
ChildrenChanged(LayoutTreeBuilderTraversal::Parent(*node));
return;
}
// TODO(accessibility) Remove AXMenuList* cases once these classes go away.
if (IsA<AXMenuListOption>(obj) || IsA<AXMenuList>(obj)) {
Remove(obj, /* notify_parent */ true);
return;
}
// Note that technically we do not need to remove the root node for a
// display-locking (content-visibility) change, since it is only the
// descendants that gain or lose their objects, but its easier to be
// consistent here.
RemoveSubtreeWithFlatTraversal(node);
}
void AXObjectCacheImpl::NodeIsAttached(Node* node) {
CHECK(node);
CHECK(node->isConnected());
SCOPED_DISALLOW_LIFECYCLE_TRANSITION();
// It normally is not necessary to process text nodes here, because we'll
// also get a call for the attachment of the parent element. However in the
// YieldingParser scenario, the `previousOnLineId` can be unexpectedly null
// for whitespace-only nodes whose inclusion had not yet been determined.
// Sample flake: AccessibilityContenteditableDocsLi. Therefore, find the
// highest `LayoutInline` ancestor and mark it dirty.
if (Text* text = DynamicTo<Text>(node)) {
if (text->ContainsOnlyWhitespaceOrEmpty()) {
if (auto* layout_object = node->GetLayoutObject()) {
auto* layout_parent = layout_object->Parent();
while (layout_parent && layout_parent->Parent() &&
layout_parent->Parent()->IsLayoutInline()) {
layout_parent = layout_parent->Parent();
}
MarkSubtreeDirty(layout_parent->GetNode());
}
}
return;
}
Document* document = DynamicTo<Document>(node);
if (document) {
Element* focused_element = GetDocument().FocusedElement();
if (IsA<HTMLSelectElement>(focused_element) &&
ShouldCreateAXMenuListFor(focused_element->GetLayoutObject())) {
// HTML <select> has its own specialized handling.
// TODO(accessibility) Remove this rule once we stop using AXMenuList*.
return;
}
// A popup is being shown.
DCHECK(*document != GetDocument());
DCHECK(!popup_document_) << "Last popup was not cleared.";
DCHECK(!popup_document_ || popup_document_ == document)
<< "Last popup was not cleared: " << (void*)popup_document_;
popup_document_ = document;
DCHECK(IsPopup(*document));
// Fire children changed on the focused element that owns this popup.
ChildrenChanged(focused_element);
return;
}
// Handle subtree that was previously display:none gaining layout.
if (node->GetLayoutObject()) {
if (AXObject* obj = Get(node); obj && !IsA<AXLayoutObject>(obj)) {
// Had a previous AXObject, but wasn't an AXLayoutObject, even though
// there is a layout object available.
RemoveSubtreeWithFlatTraversal(node);
return;
}
if (IsA<HTMLTableElement>(node) && !node->IsFinishedParsingChildren() &&
!node_to_parse_before_more_tree_updates_) {
// Tables must be fully parsed before building, because many of the
// computed properties require the entire table.
node_to_parse_before_more_tree_updates_ = node;
}
// Rare edge case: if an image is added, it could have changed the order of
// images with the same usemap in the document. Only the first image for a
// given <map> should have the <area> children. Therefore, get the current
// primary image before it's updated, and ensure its children are
// recalculated.
if (IsA<HTMLImageElement>(node)) {
if (HTMLMapElement* map = AXObject::GetMapForImage(node)) {
HTMLImageElement* primary_image_element = map->ImageElement();
if (node != primary_image_element) {
// This is a secondary image for its map, and therefore the map does
// not apply to it. Make sure the primary image recomputes its
// children.
ChildrenChanged(primary_image_element);
} else if (AXObject* ax_previous_parent = GetAXImageForMap(*map)) {
// This is the primary image for its map and the map's children
// were previously parented by an AXObject for an <img>
if (ax_previous_parent->GetNode() != node) {
// The previous AXObject parent for the maps children does not
// match!
ChildrenChanged(ax_previous_parent);
ax_previous_parent->ClearChildren();
}
}
}
}
}
DeferTreeUpdate(TreeUpdateReason::kNodeIsAttached, node);
}
void AXObjectCacheImpl::NodeIsAttachedWithCleanLayout(Node* node) {
if (!node || !node->isConnected()) {
return;
}
Element* element = DynamicTo<Element>(node);
#if DCHECK_IS_ON()
DCHECK(node->GetDocument().Lifecycle().GetState() >=
DocumentLifecycle::kLayoutClean)
<< "Unclean document at lifecycle "
<< node->GetDocument().Lifecycle().ToString();
#endif // DCHECK_IS_ON()
if (AccessibleNode::GetPropertyOrARIAAttributeValue(
element, AOMRelationProperty::kActiveDescendant)) {
HandleActiveDescendantChangedWithCleanLayout(element);
}
AXObject* obj = Get(node);
CHECK(obj);
CHECK(obj->CachedParentObject());
MaybeNewRelationTarget(*node, obj);
// Even if the node or parent are ignored, an ancestor may need to include
// descendants of the attached node, thus ChildrenChangedWithCleanLayout()
// must be called. It handles ignored logic, ensuring that the first ancestor
// that should have this as a child will be updated.
ChildrenChangedWithCleanLayout(obj->CachedParentObject());
if (IsA<HTMLAreaElement>(node)) {
ChildrenChangedWithCleanLayout(obj);
}
// Check if a row or cell's table changed to or from a data table.
if (IsA<HTMLTableRowElement>(node) || IsA<HTMLTableCellElement>(node)) {
Element* parent = node->parentElement();
while (parent) {
if (DynamicTo<HTMLTableElement>(parent)) {
break;
}
parent = parent->parentElement();
}
if (parent) {
UpdateTableRoleWithCleanLayout(parent);
}
TableCellRoleMaybeChanged(node);
}
}
// Note: do not call this when a child is becoming newly included, because
// it will return early if |obj| was last known to be unincluded.
void AXObjectCacheImpl::ChildrenChangedOnAncestorOf(AXObject* obj) {
DCHECK(obj);
DCHECK(!obj->IsDetached());
DCHECK(!updating_tree_);
// Clear children of ancestors in order to ensure this detached object is not
// cached in an ancestor's list of children:
// Any ancestor up to the first included ancestor can contain the now-detached
// child in it's cached children, and therefore must update children.
if (processing_deferred_events_) {
ChildrenChangedWithCleanLayout(obj->CachedParentObject());
return;
}
AXObject* ax_ancestor = ChildrenChanged(obj->CachedParentObject());
if (!ax_ancestor) {
return;
}
CHECK(!IsFrozen())
<< "Attempting to change children on an ancestor is dangerous during "
"serialization, because the ancestor may have already been "
"visited. Reaching this line indicates that AXObjectCacheImpl did "
"not handle a signal and call ChildrenChanged() earlier."
<< "\nChild: " << obj->ToString(true) << "\nParent: "
<< (obj->CachedParentObject() ? obj->CachedParentObject()->ToString(true)
: "")
<< "\nAncestor: " << ax_ancestor->ToString(true);
}
void AXObjectCacheImpl::ChildrenChangedWithCleanLayout(AXObject* obj) {
if (AXObject* ax_ancestor_for_notification = InvalidateChildren(obj)) {
if (ax_ancestor_for_notification->GetNode() &&
nodes_with_pending_children_changed_.Contains(
ax_ancestor_for_notification->AXObjectID())) {
return;
}
ChildrenChangedWithCleanLayout(ax_ancestor_for_notification->GetNode(),
ax_ancestor_for_notification);
}
}
AXObject* AXObjectCacheImpl::ChildrenChanged(AXObject* obj) {
DCHECK(!processing_deferred_events_)
<< "Call ChildrenChangedWithCleanLayout() directly while processing "
"deferred events.";
if (AXObject* ax_ancestor_for_notification = InvalidateChildren(obj)) {
// Don't enqueue a deferred event on the same node more than once.
CHECK(!updating_tree_);
CHECK(!IsFrozen());
if (ax_ancestor_for_notification->GetNode() &&
!nodes_with_pending_children_changed_
.insert(ax_ancestor_for_notification->AXObjectID())
.is_new_entry) {
return nullptr;
}
DeferTreeUpdate(TreeUpdateReason::kChildrenChanged,
ax_ancestor_for_notification);
return ax_ancestor_for_notification;
}
return nullptr;
}
AXObject* AXObjectCacheImpl::InvalidateChildren(AXObject* obj) {
if (!obj)
return nullptr;
// Clear children of ancestors in order to ensure this detached object is not
// cached an ancestor's list of children:
AXObject* ancestor = obj;
while (ancestor) {
if (ancestor->NeedsToUpdateChildren() || ancestor->IsDetached())
return nullptr; // Processing has already occurred for this ancestor.
ancestor->SetNeedsToUpdateChildren();
// Any ancestor up to the first included ancestor can contain the
// now-detached child in it's cached children, and therefore must update
// children.
if (ancestor->LastKnownIsIncludedInTreeValue()) {
break;
}
ancestor = ancestor->CachedParentObject();
}
// Only process ChildrenChanged() events on the included ancestor. This allows
// deduping of ChildrenChanged() occurrences within the same subtree.
// For example, if a subtree has unincluded children, but included
// grandchildren have changed, only the root children changed needs to be
// processed.
if (!ancestor)
return nullptr;
// Return ancestor to fire children changed notification on.
DCHECK(ancestor->LastKnownIsIncludedInTreeValue())
<< "ChildrenChanged() must only be called on included nodes: "
<< ancestor->ToString(true, true);
return ancestor;
}
void AXObjectCacheImpl::SlotAssignmentWillChange(Node* node) {
ChildrenChanged(node);
}
void AXObjectCacheImpl::ChildrenChanged(Node* node) {
ChildrenChanged(Get(node));
}
// ChildrenChanged gets called a lot. For the accessibility tests that
// measure performance when many nodes change, ChildrenChanged can be
// called tens of thousands of times. We need to balance catching changes
// for this metric with not slowing the perf bots down significantly.
// Tracing every 25 calls is an attempt at achieving that balance and
// may need to be adjusted further.
constexpr int kChildrenChangedTraceFrequency = 25;
void AXObjectCacheImpl::ChildrenChanged(const LayoutObject* layout_object) {
static int children_changed_counter = 0;
if (++children_changed_counter % kChildrenChangedTraceFrequency == 0) {
TRACE_EVENT0("accessibility",
"AXObjectCacheImpl::ChildrenChanged(LayoutObject)");
}
if (!layout_object)
return;
// Ensure that this object is touched, so that Get() can Invalidate() it if
// necessary, e.g. to change whether it's an AXNodeObject <--> AXLayoutObject.
Get(layout_object);
// Update using nearest node (walking ancestors if necessary).
Node* node = GetClosestNodeForLayoutObject(layout_object);
if (!node)
return;
ChildrenChanged(node);
}
void AXObjectCacheImpl::ChildrenChanged(AccessibleNode* accessible_node) {
DCHECK(accessible_node);
ChildrenChanged(Get(accessible_node));
}
void AXObjectCacheImpl::ChildrenChangedWithCleanLayout(Node* node) {
if (AXObject* obj = Get(node)) {
ChildrenChangedWithCleanLayout(node, obj);
}
}
// TODO can node be non-optional?
void AXObjectCacheImpl::ChildrenChangedWithCleanLayout(Node* optional_node,
AXObject* obj) {
CHECK(obj);
CHECK(!obj->IsDetached());
#if DCHECK_IS_ON()
if (optional_node) {
DCHECK_EQ(obj->GetNode(), optional_node);
DCHECK_EQ(obj, Get(optional_node));
}
Document* document = obj->GetDocument();
DCHECK(document);
DCHECK(document->Lifecycle().GetState() >= DocumentLifecycle::kLayoutClean)
<< "Unclean document at lifecycle " << document->Lifecycle().ToString();
#endif // DCHECK_IS_ON()
obj->ChildrenChangedWithCleanLayout();
// TODO(accessibility) Only needed for <select> size changes.
// This can turn into a DCHECK if the shadow DOM is used for <select>
// elements instead of AXMenuList* and AXListBox* classes.
if (obj->IsDetached()) {
RemoveSubtreeWhenSafe(optional_node);
return;
}
if (optional_node) {
CHECK(relation_cache_);
relation_cache_->UpdateRelatedTree(optional_node, obj);
}
TableCellRoleMaybeChanged(optional_node);
}
void AXObjectCacheImpl::UpdateTreeIfNeeded() {
DCHECK(!updating_tree_);
if (Root()->HasDirtyDescendants()) {
base::AutoReset<bool> updating(&updating_tree_, true);
HeapDeque<Member<AXObject>> objects_to_process;
objects_to_process.push_back(Root());
while (!objects_to_process.empty()) {
AXObject* obj = objects_to_process.front();
objects_to_process.pop_front();
if (obj->IsDetached()) {
continue;
}
obj->UpdateChildrenIfNecessary();
if (obj->HasDirtyDescendants()) {
obj->SetHasDirtyDescendants(false);
for (auto& child : obj->ChildrenIncludingIgnored()) {
objects_to_process.push_back(child);
}
}
}
}
CheckTreeIsUpdated();
}
void AXObjectCacheImpl::CheckStyleIsComplete(Document& document) const {
#if EXPENSIVE_DCHECKS_ARE_ON()
Element* root_element = document.documentElement();
if (!root_element) {
return;
}
{
// Check that all style is up-to-date when layout is clean, when a11y is on.
// This allows content-visibility: auto subtrees to have proper a11y
// semantics, e.g. for the hidden and focusable states.
Node* node = root_element;
do {
CHECK(!node->NeedsStyleRecalc()) << "Need style on: " << node;
auto* element = DynamicTo<Element>(node);
const ComputedStyle* style =
element ? element->GetComputedStyle() : nullptr;
if (!style || style->ContentVisibility() == EContentVisibility::kHidden ||
style->IsEnsuredInDisplayNone()) {
// content-visibility:hidden nodes are an exception and do not
// compute style.
node =
LayoutTreeBuilderTraversal::NextSkippingChildren(*node, &document);
} else {
node = LayoutTreeBuilderTraversal::Next(*node, &document);
}
} while (node