| /* |
| * 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 |