| /* |
| * 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 <algorithm> |
| |
| #include "base/auto_reset.h" |
| #include "base/memory/scoped_refptr.h" |
| #include "base/metrics/histogram_macros.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/platform/task_type.h" |
| #include "third_party/blink/public/web/web_local_frame_client.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/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/slot_assignment_engine.h" |
| #include "third_party/blink/renderer/core/editing/editing_utilities.h" |
| #include "third_party/blink/renderer/core/events/event_util.h" |
| #include "third_party/blink/renderer/core/frame/local_frame.h" |
| #include "third_party/blink/renderer/core/frame/local_frame_view.h" |
| #include "third_party/blink/renderer/core/frame/settings.h" |
| #include "third_party/blink/renderer/core/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_plugin_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_names.h" |
| #include "third_party/blink/renderer/core/input_type_names.h" |
| #include "third_party/blink/renderer/core/layout/layout_inline.h" |
| #include "third_party/blink/renderer/core/layout/layout_progress.h" |
| #include "third_party/blink/renderer/core/layout/layout_table.h" |
| #include "third_party/blink/renderer/core/layout/layout_table_cell.h" |
| #include "third_party/blink/renderer/core/layout/layout_table_row.h" |
| #include "third_party/blink/renderer/core/layout/layout_view.h" |
| #include "third_party/blink/renderer/core/layout/line/abstract_inline_text_box.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/svg/svg_style_element.h" |
| #include "third_party/blink/renderer/modules/accessibility/ax_image_map_link.h" |
| #include "third_party/blink/renderer/modules/accessibility/ax_inline_text_box.h" |
| #include "third_party/blink/renderer/modules/accessibility/ax_layout_object.h" |
| #include "third_party/blink/renderer/modules/accessibility/ax_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 "ui/accessibility/ax_common.h" |
| #include "ui/accessibility/ax_enums.mojom-blink.h" |
| #include "ui/accessibility/ax_event.h" |
| #include "ui/accessibility/ax_role_properties.h" |
| |
| // Prevent code that runs during the lifetime of the stack from altering the |
| // document lifecycle. Usually doc is the same as document_, but it can be |
| // different when it is a popup document. Because it's harmless to test both |
| // documents, even if they are the same, the scoped check is initialized for |
| // both documents. |
| // clang-format off |
| #if DCHECK_IS_ON() |
| #define SCOPED_DISALLOW_LIFECYCLE_TRANSITION(document) \ |
| DocumentLifecycle::DisallowTransitionScope scoped1((document).Lifecycle()); \ |
| DocumentLifecycle::DisallowTransitionScope scoped2(document_->Lifecycle()) |
| #else |
| #define SCOPED_DISALLOW_LIFECYCLE_TRANSITION(document) |
| #endif // DCHECK_IS_ON() |
| // clang-format on |
| |
| namespace blink { |
| |
| namespace { |
| |
| // Return a node for the current layout object or ancestor layout object. |
| Node* GetClosestNodeForLayoutObject(const LayoutObject* layout_object) { |
| if (!layout_object) |
| return nullptr; |
| Node* node = layout_object->GetNode(); |
| return node ? node : GetClosestNodeForLayoutObject(layout_object->Parent()); |
| } |
| |
| // 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) { |
| if (!node) |
| return false; |
| // The NearestLockedExclusiveAncestor() function will 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::NearestLockedExclusiveAncestor(*node); |
| } |
| |
| 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.IsEmpty()) |
| return false; |
| |
| return ui::IsCellOrTableHeader(AXObject::AriaRoleStringToRoleEnum(role_str)); |
| } |
| |
| // How deep can role="presentation" propagate from this node (inclusive)? |
| // For example, propagates from table->tbody->tr->td (4). |
| // Limiting the depth is an optimization that keeps recursion under control. |
| int RolePresentationPropagationDepth(Node* node) { |
| // Check for list markup. |
| if (IsA<HTMLMenuElement>(node) || IsA<HTMLUListElement>(node) || |
| IsA<HTMLOListElement>(node)) { |
| return 2; |
| } |
| |
| // Check for <table> |
| if (IsA<HTMLTableElement>(node)) |
| return 4; // table section, table row, table cells, |
| |
| // Check for display: table CSS. |
| if (node->GetLayoutObject() && node->GetLayoutObject()->IsTable()) |
| return 4; |
| |
| return 0; |
| } |
| |
| // 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->GetText().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. |
| if (layout_object->IsLayoutImage()) |
| 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); |
| } |
| |
| bool IsTextRelevantForAccessibility(const LayoutText& layout_text) { |
| if (!layout_text.Parent()) |
| return false; |
| |
| const Node* node = layout_text.GetNode(); |
| DCHECK(node); // Anonymous text is processed earlier, doesn't reach here. |
| |
| // 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; |
| |
| // 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; |
| } |
| |
| // Text elements with empty whitespace are returned, because of cases |
| // such as <span>Hello</span><span> </span><span>World</span>. Keeping |
| // the whitespace-only node means we now correctly expose "Hello World". |
| // See crbug.com/435765. |
| return true; |
| } |
| |
| bool IsShadowContentRelevantForAccessibility(const Node* node) { |
| DCHECK(node->ContainingShadowRoot()); |
| |
| // Native <img> create extra child nodes to hold alt text. |
| 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 = DynamicTo<HTMLSlotElement>(node); |
| if (!slot_element || !slot_element->SupportsAssignment()) |
| 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. |
| return node->GetDocument().IsFlatTreeTraversalForbidden() || |
| node->GetDocument() |
| .GetSlotAssignmentEngine() |
| .HasPendingSlotAssignmentRecalc() || |
| 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); |
| } |
| |
| Node* node = layout_object.GetNode(); |
| DCHECK(node) << "Non-anonymous layout objects always have a node"; |
| |
| if (node->ContainingShadowRoot() && |
| !IsShadowContentRelevantForAccessibility(node)) { |
| return false; |
| } |
| |
| if (layout_object.IsText()) |
| return IsTextRelevantForAccessibility(To<LayoutText>(layout_object)); |
| |
| // Menu list option and HTML area elements are indexed by DOM node, never by |
| // layout object. |
| if (AXObjectCacheImpl::ShouldCreateAXMenuListOptionFor(node)) |
| return false; |
| |
| // TODO(accessibility) Refactor so that the following rules are not repeated |
| // in IsNodeRelevantForAccessibility(). |
| |
| if (IsA<HTMLAreaElement>(node)) |
| return false; |
| |
| if (node->IsPseudoElement()) |
| return AXObjectCacheImpl::IsRelevantPseudoElement(*node); |
| |
| // <optgroup> is irrelevant inside of a <select> menulist. |
| if (auto* opt_group = DynamicTo<HTMLOptGroupElement>(node)) { |
| if (auto* select = opt_group->OwnerSelectElement()) |
| return !select->UsesMenuList(); |
| } |
| |
| // An HTML <title> does not require an AXObject: the document's name is |
| // retrieved directly via the inner text. |
| if (IsA<HTMLTitleElement>(node)) |
| return false; |
| |
| return true; |
| } |
| |
| // ----------------------------------------------------------------------------- |
| // IsNodeRelevantForAccessibility() and IsLayoutObjectRelevantForAccessibility() |
| // * if the LayoutObject is relevant and not display-locked, |
| // GetOrCreate() will return an object that will be an AXLayoutObject or |
| // derivative. Note that the node may or may not be relevant. |
| // * Else if the Node is relevant, GetOrCreate() will return an object that will |
| // be an AXNodeObject or derivative. |
| // * Else neither are relevant, and the tree will be truncated (no descendants) |
| // at this point. |
| // ----------------------------------------------------------------------------- |
| // TODO(accessibility) Merge IsNodeRelevantForAccessibility() and |
| // IsLayoutObjectRelevantForAccessibility() producing a function like |
| // GetAXType(node, layout_object) returning kTruncateSubtree, |
| // kAXNodeObject, or kAXLayoutObject. This will allow some of the checks that |
| // currently happen twice, to only happen once. |
| bool IsNodeRelevantForAccessibility(const Node* node, |
| bool parent_ax_known, |
| bool is_layout_object_relevant) { |
| if (!node || !node->isConnected()) |
| return false; |
| |
| if (node->IsDocumentNode()) |
| return true; |
| |
| if (node->ContainingShadowRoot() && |
| !IsShadowContentRelevantForAccessibility(node)) { |
| return false; |
| } |
| |
| if (node->IsTextNode()) { |
| // 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. |
| if (DisplayLockUtilities::NearestLockedInclusiveAncestor(*node)) |
| return true; |
| |
| // If rendered, decision is from IsLayoutObjectRelevantForAccessibility(). |
| if (node->GetLayoutObject()) |
| return is_layout_object_relevant; |
| |
| // If unrendered + no parent, it is in a shadow tree. Consider irrelevant. |
| if (!node->parentElement()) { |
| DCHECK(node->IsInShadowTree()); |
| return false; |
| } |
| |
| // If unrendered and in <canvas>, consider even whitespace relevant. |
| // TODO(aleventhal) Consider including all text, even unrendered whitespace, |
| // whether or not in <canvas>. For now this matches previous behavior. |
| // Including all text would allow simply returning true at this point. |
| if (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 !To<Text>(node)->ContainsOnlyWhitespaceOrEmpty(); |
| } |
| |
| if (!node->IsElementNode()) |
| return false; // Only documents, elements and text nodes get ax objects. |
| |
| if (IsA<HTMLAreaElement>(node) && !IsA<HTMLMapElement>(node->parentNode())) { |
| return false; // <area> without parent <map> is not relevant. |
| } |
| |
| if (IsA<HTMLMapElement>(node)) |
| return false; // Contains children for an img, but is not its own object. |
| |
| if (node->IsPseudoElement()) |
| return AXObjectCacheImpl::IsRelevantPseudoElement(*node); |
| |
| // <optgroup> is irrelevant inside of a <select> menulist. |
| if (auto* opt_group = DynamicTo<HTMLOptGroupElement>(node)) { |
| if (auto* select = opt_group->OwnerSelectElement()) |
| return !select->UsesMenuList(); |
| } |
| |
| // When there is a layout object, the element is known to be visible, so |
| // consider it relevant and return early. Checking the layout object is only |
| // useful when display locking (content-visibility) is not used. |
| if (node->GetLayoutObject() && |
| !DisplayLockUtilities::NearestLockedInclusiveAncestor(*node)) { |
| 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 false; |
| |
| // The node is either hidden or display locked: |
| // Do not consider <head>/<style>/<script> relevant in these cases. |
| if (IsA<HTMLHeadElement>(node)) |
| return false; |
| if (IsA<HTMLStyleElement>(node)) |
| return false; |
| if (IsA<HTMLScriptElement>(node)) |
| return false; |
| |
| // Style elements in SVG are not display: none, unlike HTML style elements, |
| // but they are still hidden and thus treated as irrelevant for accessibility. |
| if (IsA<SVGStyleElement>(node)) |
| return false; |
| |
| // Not a <head>/<style>/<script>, or SVG<style>: |
| // Use a slower check to see if this node is anywhere inside of a <head>, |
| // <style> or <script>. |
| // This check is not necessary if the parent_ax is already known, which means |
| // 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. |
| if (parent_ax_known) |
| return true; // No need to check inside if the parent exists. |
| |
| // Objects inside <head> are irrelevant. |
| if (Traversal<HTMLHeadElement>::FirstAncestor(*node)) |
| return false; |
| // Objects inside a <style> are irrelevant. |
| if (Traversal<HTMLStyleElement>::FirstAncestor(*node)) |
| return false; |
| // Objects inside a <script> are irrelevant. |
| if (Traversal<HTMLScriptElement>::FirstAncestor(*node)) |
| return false; |
| // Objects inside an SVG <style> are irrelevant. |
| if (Traversal<SVGStyleElement>::FirstAncestor(*node)) |
| return false; |
| |
| // All other objects are relevant, even if hidden. |
| return true; |
| } |
| |
| } // 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), |
| modification_count_(0), |
| validation_message_axid_(0), |
| active_aria_modal_dialog_(nullptr), |
| relation_cache_(std::make_unique<AXRelationCache>(this)), |
| accessibility_event_permission_(mojom::blink::PermissionStatus::ASK), |
| permission_service_(document.GetExecutionContext()), |
| permission_observer_receiver_(this, document.GetExecutionContext()) { |
| if (document_->LoadEventFinished()) |
| AddPermissionStatusListener(); |
| use_ax_menu_list_ = GetSettings()->GetUseAXMenuList(); |
| } |
| |
| AXObjectCacheImpl::~AXObjectCacheImpl() { |
| #if DCHECK_IS_ON() |
| DCHECK(has_been_disposed_); |
| #endif |
| } |
| |
| void AXObjectCacheImpl::Dispose() { |
| #if DCHECK_IS_ON() |
| DCHECK(!has_been_disposed_) << "Something is wrong, trying to dispose twice."; |
| has_been_disposed_ = true; |
| #endif |
| |
| for (auto& entry : objects_) { |
| AXObject* obj = entry.value; |
| obj->Detach(); |
| RemoveAXID(obj); |
| } |
| |
| permission_observer_receiver_.reset(); |
| } |
| |
| AXObject* AXObjectCacheImpl::Root() { |
| return GetOrCreate(document_); |
| } |
| |
| Node* AXObjectCacheImpl::FocusedElement() { |
| Node* focused_node = document_->FocusedElement(); |
| if (!focused_node) |
| focused_node = document_; |
| |
| // See if there's a page popup, for example a calendar picker. |
| Element* adjusted_focused_element = document_->AdjustedFocusedElement(); |
| if (auto* input = DynamicTo<HTMLInputElement>(adjusted_focused_element)) { |
| if (AXObject* ax_popup = input->PopupRootAXObject()) { |
| if (Element* focused_element_in_popup = |
| ax_popup->GetDocument()->FocusedElement()) |
| focused_node = focused_element_in_popup; |
| } |
| } |
| |
| return focused_node; |
| } |
| |
| AXObject* AXObjectCacheImpl::GetOrCreateFocusedObjectFromNode(Node* node) { |
| if (node->GetDocument() != GetDocument() && |
| node->GetDocument().Lifecycle().GetState() < |
| DocumentLifecycle::kLayoutClean) { |
| // Node is in a different, unclean document. This can occur in an open |
| // popup. Ensure the popup document has a clean layout before trying to |
| // create an AXObject from a node in it. |
| if (node->GetDocument().View()) { |
| node->GetDocument().View()->UpdateAllLifecyclePhasesExceptPaint( |
| DocumentUpdateReason::kAccessibility); |
| } |
| } |
| |
| AXObject* obj = GetOrCreate(node); |
| if (!obj) |
| return nullptr; |
| |
| // the HTML element, for example, is focusable but has an AX object that is |
| // ignored |
| if (!obj->AccessibilityIsIncludedInTree()) |
| obj = obj->ParentObjectIncludedInTree(); |
| |
| return obj; |
| } |
| |
| AXObject* AXObjectCacheImpl::FocusedObject() { |
| return GetOrCreateFocusedObjectFromNode(FocusedElement()); |
| } |
| |
| 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) { |
| if (!layout_object) |
| return nullptr; |
| |
| AXID ax_id = layout_object_mapping_.at(layout_object); |
| DCHECK(!HashTraits<AXID>::IsDeletedValue(ax_id)); |
| |
| Node* node = layout_object->GetNode(); |
| |
| if (!ax_id) |
| return node ? Get(node) : nullptr; |
| |
| if (IsDisplayLocked(node) || |
| !IsLayoutObjectRelevantForAccessibility(*layout_object)) { |
| // Change from AXLayoutObject -> AXNodeObject. |
| // We previously saved the node in the cache with its layout object, |
| // but now it's in a locked subtree so we should remove the entry with its |
| // layout object and replace it with an AXNodeObject created from the node |
| // instead. Do this later at a safe time. |
| if (node) { |
| Invalidate(node->GetDocument(), ax_id); |
| } else { |
| // Happens if pseudo content no longer relevant. |
| Remove(const_cast<LayoutObject*>(layout_object)); |
| return nullptr; |
| } |
| } |
| |
| AXObject* result = objects_.at(ax_id); |
| #if DCHECK_IS_ON() |
| 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 << " Node=" << node; |
| #endif |
| return result; |
| } |
| |
| AXObject* AXObjectCacheImpl::GetWithoutInvalidation(const Node* node) { |
| if (!node) |
| return nullptr; |
| |
| LayoutObject* layout_object = node->GetLayoutObject(); |
| |
| AXID layout_id = layout_object ? layout_object_mapping_.at(layout_object) : 0; |
| DCHECK(!HashTraits<AXID>::IsDeletedValue(layout_id)); |
| if (layout_id) |
| return objects_.at(layout_id); |
| |
| AXID node_id = node_object_mapping_.at(node); |
| DCHECK(!HashTraits<AXID>::IsDeletedValue(node_id)); |
| return node_id ? objects_.at(node_id) : nullptr; |
| } |
| |
| AXObject* AXObjectCacheImpl::Get(const Node* node) { |
| if (!node) |
| return nullptr; |
| |
| LayoutObject* layout_object = node->GetLayoutObject(); |
| |
| AXID layout_id = layout_object ? layout_object_mapping_.at(layout_object) : 0; |
| DCHECK(!HashTraits<AXID>::IsDeletedValue(layout_id)); |
| |
| AXID node_id = node_object_mapping_.at(node); |
| DCHECK(!HashTraits<AXID>::IsDeletedValue(node_id)); |
| |
| if (!layout_id && !node_id) |
| return nullptr; |
| |
| // Some elements such as <area> are indexed by DOM node, not by layout object. |
| if (!layout_object || |
| !IsLayoutObjectRelevantForAccessibility(*layout_object)) { |
| // Only text nodes still are able to become suddenly irrelevant. |
| if (layout_id && node->IsTextNode() && |
| !IsNodeRelevantForAccessibility(node, /*parent known*/ false, |
| /*layout relevant*/ false)) { |
| // Layout object and node are now both irrelevant for accessibility. |
| // For example, text becomes irrelevant when it changes to whitespace, or |
| // if it already is whitespace and the text around it changes to makes it |
| // redundant whitespace. In this case, Invalidate(), which will remove |
| // objects that are no longer relevant. |
| Invalidate(node->GetDocument(), layout_id); |
| } else { |
| // Layout object is irrelevant, but node object can still be relevant. |
| if (!node_id) { |
| DCHECK(layout_id); // One of of node_id, layout_id is non-zero. |
| Invalidate(layout_object->GetDocument(), layout_id); |
| } else { |
| layout_object = nullptr; |
| layout_id = 0; |
| } |
| } |
| } |
| |
| if (layout_id && IsDisplayLocked(node)) { |
| // Change from AXLayoutObject -> AXNodeObject. |
| // The node is in a display locked subtree, but we've previously put it in |
| // the cache with its layout object. |
| Invalidate(layout_object->GetDocument(), layout_id); |
| } else if (layout_object && node_id && !layout_id && !IsDisplayLocked(node)) { |
| // Change from AXNodeObject -> AXLayoutObject. |
| // Has a layout object but no layout_id, meaning that when the AXObject was |
| // originally created only for Node*, the LayoutObject* didn't exist yet. |
| // This can happen if an AXNodeObject is created for a node that's not laid |
| // out, but later something changes and it gets a layoutObject (like if it's |
| // reparented). It's also possible the layout object changed. |
| Invalidate(layout_object->GetDocument(), node_id); |
| } |
| |
| if (layout_id) { |
| AXObject* result = objects_.at(layout_id); |
| #if DCHECK_IS_ON() |
| DCHECK(result) << "Had AXID for LayoutObject but no entry in objects_"; |
| DCHECK(result->IsAXLayoutObject()); |
| // Do not allow detached objects except when disposing entire tree. |
| DCHECK(!result->IsDetached() || has_been_disposed_) |
| << "Detached AXLayoutObject in map: " |
| << "AXID#" << layout_id << " LayoutObject=" << layout_object; |
| #endif |
| return result; |
| } |
| |
| DCHECK(node_id); |
| |
| AXObject* result = objects_.at(node_id); |
| #if DCHECK_IS_ON() |
| 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#" << node_id << " Node=" << node; |
| #endif |
| return result; |
| } |
| |
| AXObject* AXObjectCacheImpl::Get(AbstractInlineTextBox* inline_text_box) { |
| if (!inline_text_box) |
| return nullptr; |
| |
| AXID ax_id = inline_text_box_object_mapping_.at(inline_text_box); |
| DCHECK(!HashTraits<AXID>::IsDeletedValue(ax_id)); |
| if (!ax_id) |
| return nullptr; |
| |
| AXObject* result = objects_.at(ax_id); |
| #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; |
| } |
| |
| void AXObjectCacheImpl::Invalidate(Document& document, AXID ax_id) { |
| if (GetInvalidatedIds(document).insert(ax_id).is_new_entry) |
| ScheduleVisualUpdate(); |
| } |
| |
| AXID AXObjectCacheImpl::GetAXID(Node* node) { |
| AXObject* ax_object = GetOrCreate(node); |
| if (!ax_object) |
| return 0; |
| return ax_object->AXObjectID(); |
| } |
| |
| Element* AXObjectCacheImpl::GetElementFromAXID(AXID axid) { |
| AXObject* ax_object = ObjectFromAXID(axid); |
| if (!ax_object || !ax_object->GetElement()) |
| return nullptr; |
| return ax_object->GetElement(); |
| } |
| |
| AXObject* AXObjectCacheImpl::Get(AccessibleNode* accessible_node) { |
| if (!accessible_node) |
| return nullptr; |
| |
| AXID ax_id = accessible_node_mapping_.at(accessible_node); |
| DCHECK(!HashTraits<AXID>::IsDeletedValue(ax_id)); |
| if (!ax_id) |
| return nullptr; |
| |
| AXObject* result = objects_.at(ax_id); |
| #if DCHECK_IS_ON() |
| DCHECK(result) << "Had AXID for accessible_node but no entry in objects_"; |
| DCHECK(result->IsVirtualObject()); |
| // 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()) { |
| DCHECK(!ax_image->IsDetached()); |
| 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) { |
| // FIXME: How could layoutObject->node() ever not be an Element? |
| 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)) { |
| const AtomicString& type = html_input_element->type(); |
| if (type == input_type_names::kRange) |
| 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); |
| } |
| } |
| |
| // progress bar |
| if (layout_object->IsProgress()) { |
| return MakeGarbageCollected<AXProgressIndicator>( |
| To<LayoutProgress>(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::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()) { |
| return true; |
| } |
| |
| DCHECK(node.IsFirstLetterPseudoElement()) |
| << "The only remaining type that should reach here."; |
| |
| if (LayoutObject* layout_parent = node.GetLayoutObject()->Parent()) { |
| if (Node* layout_parent_node = layout_parent->GetNode()) { |
| if (layout_parent_node->IsPseudoElement()) |
| return IsRelevantPseudoElement(*layout_parent_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(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; |
| |
| DCHECK(parent) |
| << "A virtual object must have a parent, and cannot exist without one. " |
| "The parent is set when the object is constructed."; |
| |
| 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) { |
| if (!node) |
| return nullptr; |
| |
| if (AXObject* obj = Get(node)) |
| return obj; |
| |
| return CreateAndInit(node, parent_if_known); |
| } |
| |
| AXObject* AXObjectCacheImpl::CreateAndInit(Node* node, |
| AXObject* parent_if_known, |
| AXID use_axid) { |
| DCHECK(node); |
| DCHECK(!parent_if_known || parent_if_known->CanHaveChildren()); |
| |
| // If the node has a layout object, prefer using that as the primary key for |
| // the AXObject, with the exception of the HTMLAreaElement and nodes within |
| // a locked subtree, which are created based on its node. |
| LayoutObject* layout_object = node->GetLayoutObject(); |
| if (layout_object && IsLayoutObjectRelevantForAccessibility(*layout_object) && |
| !IsDisplayLocked(node)) { |
| return CreateAndInit(layout_object, parent_if_known, use_axid); |
| } |
| |
| if (!IsNodeRelevantForAccessibility(node, parent_if_known, false)) |
| return nullptr; |
| |
| #if DCHECK_IS_ON() |
| DCHECK(node->isConnected()); |
| DCHECK(node->IsElementNode() || node->IsTextNode() || node->IsDocumentNode()); |
| Document* document = &node->GetDocument(); |
| DCHECK(document); |
| DCHECK(document->Lifecycle().GetState() >= |
| DocumentLifecycle::kAfterPerformLayout) |
| << "Unclean document at lifecycle " << document->Lifecycle().ToString(); |
| DCHECK_NE(node, document_) |
| << "The document's AXObject is backed by its layout object."; |
| #endif // DCHECK_IS_ON() |
| |
| // Return null 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 (node->IsInShadowTree()) { |
| AXObject* shadow_host = Get(node->OwnerShadowHost()); |
| if (shadow_host && !shadow_host->CanHaveChildren()) |
| return nullptr; |
| } |
| |
| #if DCHECK_IS_ON() |
| if (!IsA<HTMLOptionElement>(node) && node->IsInUserAgentShadowRoot()) { |
| if (Node* owner_shadow_host = node->OwnerShadowHost()) { |
| DCHECK(!AXObjectCacheImpl::ShouldCreateAXMenuListFor( |
| owner_shadow_host->GetLayoutObject())) |
| << "DOM descendants of an AXMenuList should not be added to the AX " |
| "hierarchy, except for the AXMenuListOption children added in " |
| "AXMenuListPopup. An attempt was made to create an AXObject for: " |
| << node; |
| } |
| } |
| #endif |
| |
| AXObject* parent = parent_if_known |
| ? parent_if_known |
| : AXObject::ComputeNonARIAParent(*this, node); |
| // An AXObject backed only by a DOM node must have a parent, because it's |
| // never the root, which will always have a layout object. |
| if (!parent) |
| return nullptr; |
| |
| DCHECK(parent->CanHaveChildren()); |
| |
| // One of the above calls could have already created the planned object via a |
| // recursive call to GetOrCreate(). If so, just return that object. |
| if (node_object_mapping_.at(node)) |
| return Get(node); |
| |
| AXObject* new_obj = CreateFromNode(node); |
| |
| // Will crash later if we have two objects for the same node. |
| DCHECK(!node_object_mapping_.at(node)) |
| << "Already have an AXObject for " << node; |
| |
| const AXID ax_id = AssociateAXID(new_obj, use_axid); |
| DCHECK(!HashTraits<AXID>::IsDeletedValue(ax_id)); |
| node_object_mapping_.Set(node, ax_id); |
| new_obj->Init(parent); |
| |
| return new_obj; |
| } |
| |
| AXObject* AXObjectCacheImpl::GetOrCreate(LayoutObject* layout_object) { |
| return GetOrCreate(layout_object, nullptr); |
| } |
| |
| AXObject* AXObjectCacheImpl::GetOrCreate(LayoutObject* layout_object, |
| AXObject* parent_if_known) { |
| if (!layout_object) |
| return nullptr; |
| |
| if (AXObject* obj = Get(layout_object)) |
| return obj; |
| |
| return CreateAndInit(layout_object, parent_if_known); |
| } |
| |
| AXObject* AXObjectCacheImpl::CreateAndInit(LayoutObject* layout_object, |
| AXObject* parent_if_known, |
| AXID use_axid) { |
| #if DCHECK_IS_ON() |
| DCHECK(layout_object); |
| Document* document = &layout_object->GetDocument(); |
| DCHECK(document); |
| DCHECK(document->Lifecycle().GetState() >= |
| DocumentLifecycle::kAfterPerformLayout) |
| << "Unclean document at lifecycle " << document->Lifecycle().ToString(); |
| DCHECK(!parent_if_known || parent_if_known->CanHaveChildren()); |
| #endif // DCHECK_IS_ON() |
| if (!IsLayoutObjectRelevantForAccessibility(*layout_object)) |
| return nullptr; |
| |
| Node* node = layout_object->GetNode(); |
| |
| if (node && !IsNodeRelevantForAccessibility(node, parent_if_known, true)) |
| return nullptr; |
| |
| // Return null 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 (node && node->IsInShadowTree()) { |
| AXObject* shadow_host = Get(node->OwnerShadowHost()); |
| if (shadow_host && !shadow_host->CanHaveChildren()) |
| return nullptr; |
| } |
| |
| #if DCHECK_IS_ON() |
| if (node && !IsA<HTMLOptionElement>(node) && |
| node->IsInUserAgentShadowRoot()) { |
| if (Node* owner_shadow_host = node->OwnerShadowHost()) { |
| DCHECK(!AXObjectCacheImpl::ShouldCreateAXMenuListFor( |
| owner_shadow_host->GetLayoutObject())) |
| << "DOM descendants of an AXMenuList should not be added to the AX " |
| "hierarchy, except for the AXMenuListOption children added in " |
| "AXMenuListPopup. An attempt was made to create an AXObject for: " |
| << node; |
| } |
| } |
| #endif |
| |
| // Prefer creating AXNodeObjects over AXLayoutObjects in locked subtrees |
| // (e.g. content-visibility: auto), even if a LayoutObject is available, |
| // because the LayoutObject is not guaranteed to be up-to-date (it might come |
| // from a previous layout update), or even it is up-to-date, it may not remain |
| // up-to-date. Blink doesn't update style/layout for nodes in locked |
| // subtrees, so creating a matching AXLayoutObjects could lead to the use of |
| // old information. Note that Blink will recreate the AX objects as |
| // AXLayoutObjects when a locked element is activated, aka it becomes visible. |
| // Visit https://wicg.github.io/display-locking/#accessibility for more info. |
| if (DisplayLockUtilities::NearestLockedExclusiveAncestor(*layout_object)) { |
| if (!node) { |
| // Nodeless objects such as anonymous blocks do not get accessible objects |
| // in a locked subtree. Anonymous blocks are added to help layout when |
| // a block and inline are siblings. |
| // This prevents an odd mixture of ax objects in a locked subtree, e.g. |
| // AXNodeObjects when there is a node, and AXLayoutObjects |
| // when there isn't. The locked subtree should not have AXLayoutObjects. |
| return nullptr; |
| } |
| return CreateAndInit(node, parent_if_known, use_axid); |
| } |
| |
| AXObject* parent = parent_if_known ? parent_if_known |
| : AXObject::ComputeNonARIAParent( |
| *this, node, layout_object); |
| if (node == document_) |
| DCHECK(!parent); |
| else if (!parent) |
| return nullptr; |
| else |
| DCHECK(parent->CanHaveChildren()); |
| |
| // One of the above calls could have already created the planned object via a |
| // recursive call to GetOrCreate(). If so, just return that object. |
| // Example: parent calls Init() => ComputeAccessibilityIsIgnored() => |
| // CanSetFocusAttribute() => CanBeActiveDescendant() => |
| // IsARIAControlledByTextboxWithActiveDescendant() => GetOrCreate(). |
| if (layout_object_mapping_.at(layout_object)) { |
| AXObject* result = Get(layout_object); |
| DCHECK(result) << "Missing cached AXObject for " << layout_object; |
| return result; |
| } |
| |
| AXObject* new_obj = CreateFromRenderer(layout_object); |
| |
| DCHECK(new_obj) << "Could not create AXObject for " << layout_object; |
| |
| // Will crash later if we have two objects for the same layoutObject. |
| DCHECK(!layout_object_mapping_.at(layout_object)) |
| << "Already have an AXObject for " << layout_object; |
| |
| const AXID axid = AssociateAXID(new_obj, use_axid); |
| layout_object_mapping_.Set(layout_object, axid); |
| new_obj->Init(parent); |
| |
| return new_obj; |
| } |
| |
| AXObject* AXObjectCacheImpl::GetOrCreate(AbstractInlineTextBox* inline_text_box, |
| AXObject* parent) { |
| if (!inline_text_box) |
| return nullptr; |
| |
| if (!parent) { |
| LayoutObject* layout_text_parent = inline_text_box->GetLayoutObject(); |
| DCHECK(layout_text_parent); |
| DCHECK(layout_text_parent->IsText()); |
| parent = GetOrCreate(layout_text_parent); |
| if (!parent) { |
| // Allowed to not have a parent if the text was irrelevant whitespace. |
| DCHECK(inline_text_box->GetText().ContainsOnlyWhitespaceOrEmpty()) |
| << "No parent for non-whitespace inline textbox: " |
| << layout_text_parent; |
| 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); |
| #endif |
| obj->SetParent(parent); |
| return obj; |
| } |
| |
| AXObject* new_obj = CreateFromInlineTextBox(inline_text_box); |
| |
| const 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::RemoveAXObjectsInLayoutSubtree(AXObject* subtree, |
| int depth) { |
| if (!subtree || depth <= 0) |
| return; |
| |
| depth--; |
| |
| LayoutObject* layout_object = subtree->GetLayoutObject(); |
| if (layout_object) { |
| LayoutObject* layout_child = layout_object->SlowFirstChild(); |
| while (layout_child) { |
| RemoveAXObjectsInLayoutSubtree(Get(layout_child), depth); |
| layout_child = layout_child->NextSibling(); |
| } |
| } |
| |
| Remove(subtree); |
| } |
| |
| void AXObjectCacheImpl::Remove(AXObject* object) { |
| DCHECK(object); |
| if (object->GetNode()) |
| Remove(object->GetNode()); |
| else if (object->GetLayoutObject()) |
| Remove(object->GetLayoutObject()); |
| else if (object->GetAccessibleNode()) |
| Remove(object->GetAccessibleNode()); |
| else |
| Remove(object->AXObjectID()); |
| } |
| |
| // 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) { |
| if (!ax_id) |
| return; |
| |
| // First, fetch object to operate some cleanup functions on it. |
| AXObject* obj = objects_.at(ax_id); |
| if (!obj) |
| return; |
| |
| ChildrenChangedOnAncestorOf(obj); |
| obj->Detach(); |
| |
| RemoveAXID(obj); |
| |
| // Finally, 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? |
| if (!objects_.Take(ax_id)) |
| return; |
| |
| DCHECK_GE(objects_.size(), ids_in_use_.size()); |
| } |
| |
| // This is safe to call even if there isn't a current mapping. |
| void AXObjectCacheImpl::Remove(AccessibleNode* accessible_node) { |
| if (!accessible_node) |
| return; |
| |
| 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); |
| } |
| |
| // This is safe to call even if there isn't a current mapping. |
| bool AXObjectCacheImpl::Remove(LayoutObject* layout_object) { |
| if (!layout_object) |
| return false; |
| |
| auto iter = layout_object_mapping_.find(layout_object); |
| if (iter == layout_object_mapping_.end()) |
| return false; |
| |
| AXID ax_id = iter->value; |
| DCHECK(ax_id); |
| |
| layout_object_mapping_.erase(iter); |
| Remove(ax_id); |
| |
| return true; |
| } |
| |
| // This is safe to call even if there isn't a current mapping. |
| void AXObjectCacheImpl::Remove(Node* node) { |
| if (!node) |
| return; |
| |
| LayoutObject* layout_object = node->GetLayoutObject(); |
| |
| // A layout object will be used whenever it is available and relevant. It's |
| // the preferred backing object, rather than the DOM node. |
| if (Remove(node->GetLayoutObject())) { |
| DCHECK_EQ(node_object_mapping_.find(node), node_object_mapping_.end()) |
| << "AXObject cannot be backed by both a layout object and node."; |
| return; |
| } |
| |
| auto iter = node_object_mapping_.find(node); |
| if (iter != node_object_mapping_.end()) { |
| 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 ax_id = iter->value; |
| DCHECK(ax_id); |
| node_object_mapping_.erase(iter); |
| Remove(ax_id); |
| } |
| } |
| |
| // This is safe to call even if there isn't a current mapping. |
| void AXObjectCacheImpl::Remove(AbstractInlineTextBox* inline_text_box) { |
| 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); |
| } |
| |
| AXID AXObjectCacheImpl::GenerateAXID() const { |
| static AXID last_used_id = 0; |
| |
| // Generate a new ID. |
| AXID obj_id = last_used_id; |
| do { |
| ++obj_id; |
| } while (!obj_id || HashTraits<AXID>::IsDeletedValue(obj_id) || |
| ids_in_use_.Contains(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"; |
| |
| const AXID new_axid = use_axid ? use_axid : GenerateAXID(); |
| |
| ids_in_use_.insert(new_axid); |
| obj->SetAXObjectID(new_axid); |
| objects_.Set(new_axid, obj); |
| |
| return new_axid; |
| } |
| |
| void AXObjectCacheImpl::RemoveAXID(AXObject* object) { |
| if (!object) |
| return; |
| |
| fixed_or_sticky_node_ids_.clear(); |
| |
| if (active_aria_modal_dialog_ == object) |
| active_aria_modal_dialog_ = nullptr; |
| |
| AXID obj_id = object->AXObjectID(); |
| if (!obj_id) |
| return; |
| DCHECK(!HashTraits<AXID>::IsDeletedValue(obj_id)); |
| DCHECK(ids_in_use_.Contains(obj_id)); |
| object->SetAXObjectID(0); |
| ids_in_use_.erase(obj_id); |
| autofill_state_map_.erase(obj_id); |
| |
| relation_cache_->RemoveAXID(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); |
| } |
| |
| void AXObjectCacheImpl::DeferTreeUpdateInternal(base::OnceClosure callback, |
| AXObject* obj) { |
| // 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. |
| DCHECK(obj); |
| |
| if (!IsActive(GetDocument()) || tree_updates_paused_) |
| return; |
| |
| if (obj->IsDetached()) |
| return; |
| |
| Document* tree_update_document = obj->GetDocument(); |
| |
| // Ensure the tree update document is in a good state. |
| if (!tree_update_document || !IsActive(*tree_update_document)) |
| return; |
| |
| TreeUpdateCallbackQueue& queue = |
| GetTreeUpdateCallbackQueue(*tree_update_document); |
| |
| if (queue.size() >= max_pending_updates_) { |
| UpdateNumTreeUpdatesQueuedBeforeLayoutHistogram(); |
| |
| tree_updates_paused_ = true; |
| queue.clear(); |
| return; |
| } |
| |
| #if DCHECK_IS_ON() |
| // TODO(accessibility) Restore this check. Currently must be removed because a |
| // loop in ProcessDeferredAccessibilityEvents() is allowed to queue deferred |
| // ChildrenChanged() events and process them. |
| // DCHECK(!tree_update_document->GetPage()->Animator().IsServicingAnimations() |
| // || |
| // (tree_update_document->Lifecycle().GetState() < |
| // DocumentLifecycle::kInAccessibility || |
| // tree_update_document->Lifecycle().StateAllowsDetach())) |
| // << "DeferTreeUpdateInternal should only be outside of the lifecycle or |
| // " |
| // "before the accessibility state:" |
| // << "\n* IsServicingAnimations: " |
| // << tree_update_document->GetPage()->Animator().IsServicingAnimations() |
| // << "\n* Lifecycle: " << tree_update_document->Lifecycle().ToString(); |
| #endif |
| |
| queue.push_back(MakeGarbageCollected<TreeUpdateParams>( |
| obj->GetNode(), obj->AXObjectID(), ComputeEventFrom(), |
| active_event_from_action_, ActiveEventIntents(), std::move(callback))); |
| |
| // These events are fired during DocumentLifecycle::kInAccessibility, |
| // ensure there is a document lifecycle update scheduled. |
| ScheduleVisualUpdate(); |
| } |
| |
| void AXObjectCacheImpl::DeferTreeUpdateInternal(base::OnceClosure callback, |
| const Node* node) { |
| DCHECK(node); |
| |
| if (!IsActive(GetDocument()) || tree_updates_paused_) |
| return; |
| |
| Document& tree_update_document = node->GetDocument(); |
| |
| // Ensure the tree update document is in a good state. |
| if (!IsActive(tree_update_document)) |
| return; |
| |
| TreeUpdateCallbackQueue& queue = |
| GetTreeUpdateCallbackQueue(tree_update_document); |
| |
| if (queue.size() >= max_pending_updates_) { |
| UpdateNumTreeUpdatesQueuedBeforeLayoutHistogram(); |
| |
| tree_updates_paused_ = true; |
| queue.clear(); |
| return; |
| } |
| |
| #if DCHECK_IS_ON() |
| DCHECK(!tree_update_document.GetPage()->Animator().IsServicingAnimations() || |
| (tree_update_document.Lifecycle().GetState() < |
| DocumentLifecycle::kInAccessibility || |
| tree_update_document.Lifecycle().StateAllowsDetach())) |
| << "DeferTreeUpdateInternal should only be outside of the lifecycle or " |
| "before the accessibility state:" |
| << "\n* IsServicingAnimations: " |
| << tree_update_document.GetPage()->Animator().IsServicingAnimations() |
| << "\n* Lifecycle: " << tree_update_document.Lifecycle().ToString(); |
| #endif |
| |
| queue.push_back(MakeGarbageCollected<TreeUpdateParams>( |
| node, 0, ComputeEventFrom(), active_event_from_action_, |
| ActiveEventIntents(), std::move(callback))); |
| |
| // These events are fired during DocumentLifecycle::kInAccessibility, |
| // ensure there is a document lifecycle update scheduled. |
| ScheduleVisualUpdate(); |
| } |
| |
| void AXObjectCacheImpl::DeferTreeUpdate( |
| void (AXObjectCacheImpl::*method)(const Node*), |
| const Node* node) { |
| base::OnceClosure callback = |
| WTF::Bind(method, WrapWeakPersistent(this), WrapWeakPersistent(node)); |
| DeferTreeUpdateInternal(std::move(callback), node); |
| } |
| |
| void AXObjectCacheImpl::DeferTreeUpdate( |
| void (AXObjectCacheImpl::*method)(Node*), |
| Node* node) { |
| base::OnceClosure callback = |
| WTF::Bind(method, WrapWeakPersistent(this), WrapWeakPersistent(node)); |
| DeferTreeUpdateInternal(std::move(callback), node); |
| } |
| |
| void AXObjectCacheImpl::DeferTreeUpdate( |
| void (AXObjectCacheImpl::*method)(Node* node, |
| ax::mojom::blink::Event event), |
| Node* node, |
| ax::mojom::blink::Event event) { |
| base::OnceClosure callback = WTF::Bind(method, WrapWeakPersistent(this), |
| WrapWeakPersistent(node), event); |
| DeferTreeUpdateInternal(std::move(callback), node); |
| } |
| |
| void AXObjectCacheImpl::DeferTreeUpdate( |
| void (AXObjectCacheImpl::*method)(const QualifiedName&, Element* element), |
| const QualifiedName& attr_name, |
| Element* element) { |
| base::OnceClosure callback = WTF::Bind( |
| method, WrapWeakPersistent(this), attr_name, WrapWeakPersistent(element)); |
| DeferTreeUpdateInternal(std::move(callback), element); |
| } |
| |
| void AXObjectCacheImpl::DeferTreeUpdate( |
| void (AXObjectCacheImpl::*method)(Node*, AXObject*), |
| AXObject* obj) { |
| Node* node = obj ? obj->GetNode() : nullptr; |
| base::OnceClosure callback = |
| WTF::Bind(method, WrapWeakPersistent(this), WrapWeakPersistent(node), |
| WrapWeakPersistent(obj)); |
| if (obj) { |
| DeferTreeUpdateInternal(std::move(callback), obj); |
| } else { |
| DeferTreeUpdateInternal(std::move(callback), node); |
| } |
| } |
| |
| void AXObjectCacheImpl::SelectionChanged(Node* node) { |
| if (!node) |
| return; |
| |
| Settings* settings = GetSettings(); |
| if (settings && settings->GetAriaModalPrunesAXTree()) |
| UpdateActiveAriaModalDialog(node); |
| |
| DeferTreeUpdate(&AXObjectCacheImpl::SelectionChangedWithCleanLayout, node); |
| } |
| |
| void AXObjectCacheImpl::SelectionChangedWithCleanLayout(Node* node) { |
| if (!node) |
| return; |
| |
| AXObject* ax_object = GetOrCreate(node); |
| if (ax_object) |
| ax_object->SelectionChanged(); |
| } |
| |
| void AXObjectCacheImpl::UpdateReverseRelations( |
| const AXObject* relation_source, |
| const Vector<String>& target_ids) { |
| relation_cache_->UpdateReverseRelations(relation_source, target_ids); |
| } |
| |
| void AXObjectCacheImpl::StyleChanged(const LayoutObject* layout_object) { |
| DCHECK(layout_object); |
| SCOPED_DISALLOW_LIFECYCLE_TRANSITION(layout_object->GetDocument()); |
| Node* node = GetClosestNodeForLayoutObject(layout_object); |
| if (node) |
| DeferTreeUpdate(&AXObjectCacheImpl::StyleChangedWithCleanLayout, node); |
| } |
| |
| void AXObjectCacheImpl::StyleChangedWithCleanLayout(Node* node) { |
| DCHECK(node); |
| DCHECK(!node->GetDocument().NeedsLayoutTreeUpdateForNode(*node)); |
| |
| // There is a ton of style change notifications coming from newly-opened |
| // calendar popups for pickers. Solving that problem is what inspired the |
| // approach below, which is likely true for all elements. |
| // |
| // If we don't know about an object, then its style did not change as far as |
| // we (and ATs) are concerned. For this reason, don't call GetOrCreate. |
| AXObject* obj = Get(node); |
| if (!obj) |
| return; |
| |
| DCHECK(!obj->IsDetached()); |
| |
| // If the foreground or background color on an item inside a container which |
| // supports selection changes, it can be the result of the selection changing |
| // as well as the container losing focus. We handle these notifications via |
| // their state changes, so no need to mark them dirty here. |
| AXObject* parent = obj->CachedParentObject(); |
| if (parent && ui::IsContainerWithSelectableChildren(parent->RoleValue())) |
| return; |
| |
| MarkAXObjectDirtyWithCleanLayout(obj); |
| } |
| |
| void AXObjectCacheImpl::TextChanged(Node* node) { |
| if (!node) |
| return; |
| |
| // A text changed event is redundant with children changed on the same node. |
| if (nodes_with_pending_children_changed_.find(node) != |
| nodes_with_pending_children_changed_.end()) { |
| return; |
| } |
| |
| DeferTreeUpdate(&AXObjectCacheImpl::TextChangedWithCleanLayout, node); |
| } |
| |
| 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) { |
| // A text changed event is redundant with children changed on the same node. |
| if (nodes_with_pending_children_changed_.find(node) != |
| nodes_with_pending_children_changed_.end()) { |
| return; |
| } |
| |
| DeferTreeUpdate(&AXObjectCacheImpl::TextChangedWithCleanLayout, node); |
| return; |
| } |
| |
| if (Get(layout_object)) { |
| DeferTreeUpdate(&AXObjectCacheImpl::TextChangedWithCleanLayout, |
| 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->LastKnownIsIncludedInTreeValue()) { |
| Settings* settings = GetSettings(); |
| if (settings && settings->GetInlineTextBoxAccessibilityEnabled()) { |
| // Update inline text box children. |
| ChildrenChangedWithCleanLayout(optional_node_for_relation_update, obj); |
| return; |
| } |
| } |
| |
| MarkAXObjectDirtyWithCleanLayout(obj); |
| } |
| |
| if (optional_node_for_relation_update) |
| relation_cache_->UpdateRelatedTree(optional_node_for_relation_update, obj); |
| } |
| |
| void AXObjectCacheImpl::TextChangedWithCleanLayout(Node* node) { |
| if (!node) |
| return; |
| |
| DCHECK(!node->GetDocument().NeedsLayoutTreeUpdateForNode(*node)); |
| TextChangedWithCleanLayout(node, GetOrCreate(node)); |
| } |
| |
| void AXObjectCacheImpl::FocusableChangedWithCleanLayout(Element* element) { |
| DCHECK(element); |
| DCHECK(!element->GetDocument().NeedsLayoutTreeUpdateForNode(*element)); |
| AXObject* obj = GetOrCreate(element); |
| if (!obj) |
| return; |
| |
| if (obj->AriaHiddenRoot()) { |
| // 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. |
| // TODO(accessibility) Is this necessary? We have other places in the code |
| // that automatically do a children changed on parents of nodes whose |
| // ignored or included states change. |
| ChildrenChangedWithCleanLayout(obj->CachedParentObject()); |
| } |
| |
| // 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); |
| } |
| |
| void AXObjectCacheImpl::UpdateCacheAfterNodeIsAttached(Node* node) { |
| DCHECK(node); |
| SCOPED_DISALLOW_LIFECYCLE_TRANSITION(node->GetDocument()); |
| DeferTreeUpdate( |
| &AXObjectCacheImpl::UpdateCacheAfterNodeIsAttachedWithCleanLayout, node); |
| } |
| |
| void AXObjectCacheImpl::UpdateCacheAfterNodeIsAttachedWithCleanLayout( |
| Node* node) { |
| if (!node || !node->isConnected()) |
| return; |
| |
| // Ignore attached nodes that are not elements, including text nodes and |
| // #shadow-root nodes. This matches previous implementations that worked, |
| // but it is not clear if that could potentially lead to missing content. |
| Element* element = DynamicTo<Element>(node); |
| if (!element) |
| return; |
| |
| Document* document = &node->GetDocument(); |
| if (!document) |
| return; |
| |
| #if DCHECK_IS_ON() |
| DCHECK(document->Lifecycle().GetState() >= DocumentLifecycle::kLayoutClean) |
| << "Unclean document at lifecycle " << document->Lifecycle().ToString(); |
| #endif // DCHECK_IS_ON() |
| |
| // 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(element)) |
| HandleAttributeChangedWithCleanLayout(html_names::kAriaOwnsAttr, element); |
| |
| MaybeNewRelationTarget(*node, Get(node)); |
| |
| // 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( |
| Get(LayoutTreeBuilderTraversal::Parent(*node))); |
| } |
| |
| void AXObjectCacheImpl::DidInsertChildrenOfNode(Node* node) { |
| // If a node is inserted that is a descendant of a leaf node in the |
| // accessibility tree, notify the root of that subtree that its children have |
| // changed. |
| DCHECK(node); |
| while (node) { |
| if (AXObject* obj = Get(node)) { |
| TextChanged(node); |
| return; |
| } |
| node = NodeTraversal::Parent(*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()); |
| |
| // If |obj| is not included, and it has no included descendants, then there is |
| // nothing in any ancestor's cached children that needs clearing. This rule |
| // improves performance when removing an entire subtree of unincluded nodes. |
| // For example, if a <div id="root" style="display:none"> will be |
| // included because it is a potential relation target. If unincluded |
| // descendants change, no ChildrenChanged() processing is necessary, because |
| // #root has no children. |
| if (!obj->LastKnownIsIncludedInTreeValue() && |
| obj->CachedChildrenIncludingIgnored().IsEmpty()) { |
| return; |
| } |
| |
| // Clear children of ancestors in order to ensure this detached object is not |
| // cached 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. |
| ChildrenChanged(obj->CachedParentObject()); |
| } |
| |
| void AXObjectCacheImpl::ChildrenChangedWithCleanLayout(AXObject* obj) { |
| if (AXObject* ax_ancestor_for_notification = InvalidateChildren(obj)) { |
| ChildrenChangedWithCleanLayout(ax_ancestor_for_notification->GetNode(), |
| ax_ancestor_for_notification); |
| } |
| } |
| |
| void AXObjectCacheImpl::ChildrenChanged(AXObject* obj) { |
| if (AXObject* ax_ancestor_for_notification = InvalidateChildren(obj)) { |
| DeferTreeUpdate(&AXObjectCacheImpl::ChildrenChangedWithCleanLayout, |
| ax_ancestor_for_notification); |
| } |
| } |
| |
| 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: |
| // Any ancestor up to the first included ancestor can contain the now-detached |
| // child in it's cached children, and therefore must update children. |
| AXObject* ancestor = obj; |
| while (ancestor && !ancestor->LastKnownIsIncludedInTreeValue()) { |
| if (ancestor->NeedsToUpdateChildren() || ancestor->IsDetached()) |
| return nullptr; // Processing has already occurred for this ancestor. |
| ancestor->SetNeedsToUpdateChildren(); |
| 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; |
| // Don't enqueue a deferred event on the same node more than once. |
| if (ancestor->GetNode() && |
| !nodes_with_pending_children_changed_.insert(ancestor->GetNode()) |
| .is_new_entry) { |
| 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) { |
| // Use GetWithoutInvalidation(), because right before slot assignment is a |
| // dangerous time to test whether the slot must be invalidated, because |
| // this currently requires looking at the <slot> children in |
| // IsShadowContentRelevantForAccessibility(), resulting in an infinite loop |
| // as looking at the children causes slot assignment to be recalculated. |
| // TODO(accessibility) In the future this may be simplified. |
| // See crbug.com/1219311. |
| ChildrenChanged(GetWithoutInvalidation(node)); |
| } |
| |
| void AXObjectCacheImpl::ChildrenChanged(Node* node) { |
| ChildrenChanged(Get(node)); |
| } |
| |
| void AXObjectCacheImpl::ChildrenChanged(const LayoutObject* layout_object) { |
| 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(Get(node)); |
| |
| if (!layout_object->IsAnonymous()) |
| return; |
| |
| DCHECK_NE(node->GetLayoutObject(), layout_object); |
| |
| // The passed-in layout object was anonymous, e.g. anonymous block flow |
| // inserted by blink as an inline's parent when it had a block sibling. |
| // If children change on an anonymous layout object, this can |
| // mean that child AXObjects actually had their children change. |
| // Therefore, invalidate any of those children as well, using the nearest |
| // parent that participates in the tree. |
| // In this example, if ChildrenChanged() is called on the anonymous block, |
| // then we also process ChildrenChanged() on the <div> and <a>: |
| // <div> |
| // | \ |
| // <p> Anonymous block (Note: Anonymous blocks do not get AXObjects) |
| // \ |
| // <a> |
| // \ |
| // text |
| |
| // TODO(aleventhal) Why is this needed for shadow-distribution.js test? |
| if (GetDocument().IsFlatTreeTraversalForbidden()) |
| return; |
| |
| for (Node* child = LayoutTreeBuilderTraversal::FirstChild(*node); child; |
| child = LayoutTreeBuilderTraversal::NextSibling(*child)) { |
| ChildrenChanged(Get(child)); |
| } |
| } |
| |
| void AXObjectCacheImpl::ChildrenChanged(AccessibleNode* accessible_node) { |
| ChildrenChanged(Get(accessible_node)); |
| } |
| |
| void AXObjectCacheImpl::ChildrenChangedWithCleanLayout(Node* node) { |
| if (!node) |
| return; |
| |
| LayoutObject* layout_object = node->GetLayoutObject(); |
| AXID layout_id = layout_object ? layout_object_mapping_.at(layout_object) : 0; |
| DCHECK(!HashTraits<AXID>::IsDeletedValue(layout_id)); |
| |
| AXID node_id = node_object_mapping_.at(node); |
| DCHECK(!HashTraits<AXID>::IsDeletedValue(node_id)); |
| DCHECK(!node->GetDocument().NeedsLayoutTreeUpdateForNode(*node)); |
| |
| ChildrenChangedWithCleanLayout(node, Get(node)); |
| } |
| |
| void AXObjectCacheImpl::ChildrenChangedWithCleanLayout(Node* optional_node, |
| AXObject* obj) { |
| if (HTMLMapElement* map_element = DynamicTo<HTMLMapElement>(optional_node)) { |
| obj = GetAXImageForMap(*map_element); |
| if (!obj) |
| return; |
| optional_node = obj->GetNode(); |
| } |
| if (obj ? obj->IsDetached() : !optional_node) |
| return; |
| |
| #if DCHECK_IS_ON() |
| if (obj && optional_node) { |
| DCHECK_EQ(obj->GetNode(), optional_node); |
| DCHECK_EQ(obj, GetWithoutInvalidation(optional_node)); |
| } |
| Document* document = obj ? obj->GetDocument() : &optional_node->GetDocument(); |
| DCHECK(document); |
| DCHECK(document->Lifecycle().GetState() >= DocumentLifecycle::kLayoutClean) |
| << "Unclean document at lifecycle " << document->Lifecycle().ToString(); |
| #endif // DCHECK_IS_ON() |
| |
| if (obj) |
| obj->ChildrenChangedWithCleanLayout(); |
| |
| if (optional_node) |
| relation_cache_->UpdateRelatedTree(optional_node, obj); |
| } |
| |
| void AXObjectCacheImpl::ProcessDeferredAccessibilityEvents(Document& document) { |
| TRACE_EVENT0("accessibility", "ProcessDeferredAccessibilityEvents"); |
| |
| if (document.Lifecycle().GetState() != DocumentLifecycle::kInAccessibility) { |
| NOTREACHED() << "Deferred events should only be processed during the " |
| "accessibility document lifecycle."; |
| return; |
| } |
| |
| if (!IsDirty()) |
| return; |
| |
| DCHECK(GetDocument().IsAccessibilityEnabled()) |
| << "ProcessDeferredAccessibilityEvents should not perform work when " |
| "accessibility is not enabled." |
| << "\n* IsPopup? " << IsPopup(document); |
| |
| SCOPED_UMA_HISTOGRAM_TIMER( |
| "Accessibility.Performance.ProcessDeferredAccessibilityEvents"); |
| |
| #if DCHECK_IS_ON() |
| int loop_counter = 0; |
| #endif |
| |
| do { |
| // Destroy and recreate any objects which are no longer valid, for example |
| // they used AXNodeObject and now must be an AXLayoutObject, or vice-versa. |
| // Also fires children changed on the parent of these nodes. |
| ProcessInvalidatedObjects(document); |
| |
| // Call the queued callback methods that do processing which must occur when |
| // layout is clean. These callbacks are stored in |
| // tree_update_callback_queue_, and have names like |
| // FooBarredWithCleanLayout(). |
| ProcessCleanLayoutCallbacks(document); |
| |
| // Changes to ids or aria-owns may have resulted in queued up relation |
| // cache work; do that now. |
| relation_cache_->ProcessUpdatesWithCleanLayout(); |
| |
| // Keep going if there are more ids to invalidate or children changes to |
| // process from previous steps. For examople, a display locked |
| // (content-visibility:auto) element could be invalidated as it is scrolled |
| // in or out of view, causing Invalidate() to add it to invalidated_ids_. |
| // As ProcessInvalidatedObjects() refreshes the objectt and calls |
| // ChildrenChanged() on the parent, more objects may be invalidated, or |
| // more objects may have children changed called on them. |
| #if DCHECK_IS_ON() |
| DCHECK_LE(++loop_counter, 100) << "Probable infinite loop detected."; |
| #endif |
| } while (!nodes_with_pending_children_changed_.IsEmpty() || |
| !GetInvalidatedIds(document).IsEmpty()); |
| |
| // Send events to RenderAccessibilityImpl, which serializes them and then |
| // sends the serialized events and dirty objects to the browser process. |
| PostNotifications(document); |
| } |
| |
| bool AXObjectCacheImpl::IsDirty() const { |
| if (tree_updates_paused_) |
| return false; |
| return !tree_update_callback_queue_main_.IsEmpty() || |
| !tree_update_callback_queue_popup_.IsEmpty() || |
| !notifications_to_post_main_.IsEmpty() || |
| !notifications_to_post_popup_.IsEmpty() || |
| !invalidated_ids_main_.IsEmpty() || |
| !invalidated_ids_popup_.IsEmpty() || relation_cache_->IsDirty(); |
| } |
| |
| void AXObjectCacheImpl::EmbeddingTokenChanged(HTMLFrameOwnerElement* element) { |
| if (!element) |
| return; |
| |
| MarkElementDirty(element); |
| } |
| |
| bool AXObjectCacheImpl::IsPopup(Document& document) const { |
| // There are 1-2 documents per AXObjectCache: the main document and |
| // sometimes a popup document. |
| int is_popup = document != GetDocument(); |
| #if DCHECK_IS_ON() |
| if (is_popup) { |
| // Verify that the popup document's owner is the main document. |
| LocalFrame* frame = document.GetFrame(); |
| DCHECK(frame); |
| Element* popup_owner = frame->PagePopupOwner(); |
| DCHECK(popup_owner); |
| DCHECK_EQ(popup_owner->GetDocument(), GetDocument()) |
| << "The popup document's owner should be in the main document."; |
| Page* main_page = GetDocument().GetPage(); |
| DCHECK(main_page); |
| // TODO(accessibility) Verify that the main document's popup is |document|. |
| // PagePopupController* popup_controller = |
| // PagePopupController::From(*main_page); |
| // DCHECK(popup_controller); |
| // AXObject* popup_root_ax_object = popup_controller->RootAXObject(); |
| // DCHECK(popup_root_ax_object); |
| // DCHECK_EQ(popup_root_ax_object->GetDocument(), &document) |
| // << "There can be only one active popup document."; |
| } |
| #endif |
| return is_popup; |
| } |
| |
| HashSet<AXID>& AXObjectCacheImpl::GetInvalidatedIds(Document& document) { |
| return IsPopup(document) ? invalidated_ids_popup_ : invalidated_ids_main_; |
| } |
| |
| AXObjectCacheImpl::TreeUpdateCallbackQueue& |
| AXObjectCacheImpl::GetTreeUpdateCallbackQueue(Document& document) { |
| return IsPopup(document) ? tree_update_callback_queue_popup_ |
| : tree_update_callback_queue_main_; |
| } |
| |
| HeapVector<Member<AXObjectCacheImpl::AXEventParams>>& |
| AXObjectCacheImpl::GetNotificationsToPost(Document& document) { |
| return IsPopup(document) ? notifications_to_post_popup_ |
| : notifications_to_post_main_; |
| } |
| |
| void AXObjectCacheImpl::ProcessInvalidatedObjects(Document& document) { |
| // Create a new object with the same AXID as the old one. |
| // Currently only supported for objects with a backing node. |
| // Returns the new object. |
| auto refresh = [this](AXObject* current) { |
| DCHECK(current); |
| Node* node = current->GetNode(); |
| DCHECK(node) << "Refresh() is currently only supported for objects " |
| "with a backing node."; |
| AXID retained_axid = current->AXObjectID(); |
| // Remove from relevant maps, but not from relation cache, as the relations |
| // between AXIDs will still be the same. |
| node_object_mapping_.erase(node); |
| if (current->GetLayoutObject()) { |
| layout_object_mapping_.erase(current->GetLayoutObject()); |
| } else if (node->GetLayoutObject()) { |
| DCHECK(!layout_object_mapping_.at(node->GetLayoutObject())) |
| << node << " " << node->GetLayoutObject(); |
| } |
| |
| ChildrenChangedOnAncestorOf(current); |
| current->Detach(); |
| |
| // 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(retained_axid); |
| // Do not pass in the previous parent. It may not end up being the same, |
| // e.g. in the case of a <select> option where the select changed size. |
| // TODO(accessibility) That may be the only example of this, in which case |
| // it could be handled in RoleChangedWithCleanLayout(), and the cached |
| // parent could be used. |
| AXObject* new_object = CreateAndInit(node, nullptr, retained_axid); |
| if (new_object) { |
| // Any owned objects need to reset their parent_ to point to the |
| // new object. |
| if (AXObject::HasARIAOwns(DynamicTo<Element>(node)) && |
| AXRelationCache::IsValidOwner(new_object)) { |
| relation_cache_->UpdateAriaOwnsWithCleanLayout(new_object, true); |
| } |
| } else { |
| // Failed to create, so remove object completely. |
| RemoveAXID(current); |
| } |
| |
| return new_object; |
| }; |
| |
| // ChildrenChanged() calls from below work may invalidate more objects. |
| // Thereore, work from a separate list of ids, allowing new invalidated_ids. |
| HashSet<AXID> old_invalidated_ids; |
| old_invalidated_ids.swap(GetInvalidatedIds(document)); |
| for (AXID ax_id : old_invalidated_ids) { |
| AXObject* object = ObjectFromAXID(ax_id); |
| if (!object || object->IsDetached()) |
| continue; |
| DCHECK_EQ(object->GetDocument(), &document); |
| |
| #if defined(AX_FAIL_FAST_BUILD) |
| bool did_use_layout_object_traversal = |
| object->ShouldUseLayoutObjectTraversalForChildren(); |
| #endif |
| |
| // Invalidate children on the first available non-detached parent that is |
| // included in the tree. Sometimes a cached parent is detached because |
| // an object was detached in the middle of the tree, and cached parents |
| // are not corrected until the call to UpdateChildrenIfNecessary() below. |
| AXObject* parent = object; |
| while (true) { |
| AXObject* candidate_parent = parent->CachedParentObject(); |
| if (!candidate_parent || candidate_parent->IsDetached()) { |
| // The cached parent pointed to a detached AXObject. Compute a new |
| // candidate parent and repair the cached parent now, so that |
| // refreshing and initializing the new object can occur (a parent is |
| // required). |
| candidate_parent = parent->ComputeParent(); |
| if (candidate_parent) |
| parent->SetParent(candidate_parent); |
| } |
| |
| parent = candidate_parent; |
| if (!parent) |
| break; // No higher candidate parent found, will invalidate |parent|. |
| |
| // Queue up a ChildrenChanged() call for this parent. |
| if (parent->LastKnownIsIncludedInTreeValue()) |
| break; // Stop here (otherwise continue to higher ancestor). |
| } |
| |
| if (!parent) { |
| // If no parent is possible, prune from the tree. |
| Remove(object); |
| continue; |
| } |
| |
| AXObject* new_object = refresh(object); |
| MarkAXObjectDirtyWithCleanLayout(new_object); |
| |
| #if defined(AX_FAIL_FAST_BUILD) |
| SANITIZER_CHECK(!new_object || |
| new_object->ShouldUseLayoutObjectTraversalForChildren() == |
| did_use_layout_object_traversal) |
| << "This should no longer be possible, an object only uses layout " |
| "object traversal if it is part of a pseudo element subtree, " |
| "and that never changes: " |
| << new_object->ToString(true, true); |
| #endif |
| } |
| } |
| |
| void AXObjectCacheImpl::ProcessCleanLayoutCallbacks(Document& document) { |
| SCOPED_DISALLOW_LIFECYCLE_TRANSITION(document); |
| |
| if (tree_updates_paused_) { |
| ChildrenChangedWithCleanLayout(nullptr, GetOrCreate(&document)); |
| tree_updates_paused_ = false; |
| return; |
| } |
| |
| UpdateNumTreeUpdatesQueuedBeforeLayoutHistogram(); |
| |
| TreeUpdateCallbackQueue old_tree_update_callback_queue; |
| GetTreeUpdateCallbackQueue(document).swap(old_tree_update_callback_queue); |
| nodes_with_pending_children_changed_.clear(); |
| |
| for (auto& tree_update : old_tree_update_callback_queue) { |
| const Node* node = tree_update->node; |
| AXID axid = tree_update->axid; |
| |
| // Need either an DOM node or an AXObject to be a valid update. |
| // These may have been destroyed since the original update occurred. |
| if (!node) { |
| if (!axid || !ObjectFromAXID(axid)) |
| continue; |
| } |
| |
| #if DCHECK_IS_ON() |
| if (axid) { |
| AXObject* obj = ObjectFromAXID(axid); |
| if (obj) { |
| DCHECK(!obj->IsDetached()); |
| if (node) { |
| DCHECK_EQ(node, obj->GetNode()); |
| DCHECK_EQ(GetWithoutInvalidation(node), obj); |
| } |
| } |
| } |
| #endif |
| base::OnceClosure& callback = tree_update->callback; |
| // Insure the update is for the correct document. |
| // If no node, this update must be from an AXObject with no DOM node, |
| // such as an AccessibleNode. In that case, ensure the update is in the |
| // main document. |
| Document& tree_update_document = node ? node->GetDocument() : GetDocument(); |
| if (document != tree_update_document) { |
| // Document does not match one of the supported documents for this |
| // AXObjectCacheImpl. This can happen if a node is adopted by another |
| // document. In this case, we throw the callback on the floor. |
| continue; |
| } |
| |
| FireTreeUpdatedEventImmediately( |
| document, tree_update->event_from, tree_update->event_from_action, |
| tree_update->event_intents, std::move(callback)); |
| } |
| } |
| |
| void AXObjectCacheImpl::PostNotifications(Document& document) { |
| HeapVector<Member<AXEventParams>> old_notifications_to_post; |
| GetNotificationsToPost(document).swap(old_notifications_to_post); |
| for (auto& params : old_notifications_to_post) { |
| AXObject* obj = params->target; |
| |
| if (!obj || !obj->AXObjectID()) |
| continue; |
| |
| if (obj->IsDetached()) |
| continue; |
| |
| DCHECK_EQ(obj->GetDocument(), &document) |
| << "Wrong document in PostNotifications"; |
| |
| ax::mojom::blink::Event event_type = params->event_type; |
| ax::mojom::blink::EventFrom event_from = params->event_from; |
| ax::mojom::blink::Action event_from_action = params->event_from_action; |
| const BlinkAXEventIntentsSet& event_intents = params->event_intents; |
| FireAXEventImmediately(obj, event_type, event_from, event_from_action, |
| event_intents); |
| } |
| } |
| |
| void AXObjectCacheImpl::PostNotification(const LayoutObject* layout_object, |
| ax::mojom::blink::Event notification) { |
| if (!layout_object) |
| return; |
| PostNotification(Get(layout_object), notification); |
| } |
| |
| void AXObjectCacheImpl::PostNotification(Node* node, |
| ax::mojom::blink::Event notification) { |
| if (!node) |
| return; |
| PostNotification(Get(node), notification); |
| } |
| |
| void AXObjectCacheImpl::EnsurePostNotification( |
| Node* node, |
| ax::mojom::blink::Event notification) { |
| if (!node) |
| return; |
| PostNotification(GetOrCreate(node), notification); |
| } |
| |
| void AXObjectCacheImpl::PostNotification(AXObject* object, |
| ax::mojom::blink::Event event_type) { |
| if (!object || !object->AXObjectID() || object->IsDetached()) |
| return; |
| |
| modification_count_++; |
| |
| Document& document = *object->GetDocument(); |
| |
| // It's possible for FireAXEventImmediately to post another notification. |
| // If we're still in the accessibility document lifecycle, fire these events |
| // immediately rather than deferring them. |
| if (document.Lifecycle().GetState() == DocumentLifecycle::kInAccessibility) { |
| FireAXEventImmediately(object, event_type, ComputeEventFrom(), |
| active_event_from_action_, ActiveEventIntents()); |
| return; |
| } |
| |
| GetNotificationsToPost(document).push_back( |
| MakeGarbageCollected<AXEventParams>( |
| object, event_type, ComputeEventFrom(), active_event_from_action_, |
| ActiveEventIntents())); |
| |
| // These events are fired during DocumentLifecycle::kInAccessibility, |
| // ensure there is a visual update scheduled. |
| ScheduleVisualUpdate(); |
| } |
| |
| void AXObjectCacheImpl::ScheduleVisualUpdate() { |
| // Scheduling visual updates before the document is finished loading can |
| // interfere with event ordering. |
| if (!GetDocument().IsLoadCompleted()) |
| return; |
| |
| // If there was a document change that doesn't trigger a lifecycle update on |
| // its own, (e.g. because it doesn't make layout dirty), make sure we run |
| // lifecycle phases to update the computed accessibility tree. |
| LocalFrameView* frame_view = GetDocument().View(); |
| Page* page = GetDocument().GetPage(); |
| if (!frame_view || !page) |
| return; |
| |
| if (!frame_view->CanThrottleRendering() && |
| (!GetDocument().GetPage()->Animator().IsServicingAnimations() || |
| GetDocument().Lifecycle().GetState() >= |
| DocumentLifecycle::kInAccessibility)) { |
| page->Animator().ScheduleVisualUpdate(GetDocument().GetFrame()); |
| } |
| } |
| |
| void AXObjectCacheImpl::FireTreeUpdatedEventImmediately( |
| Document& document, |
| ax::mojom::blink::EventFrom event_from, |
| ax::mojom::blink::Action event_from_action, |
| const BlinkAXEventIntentsSet& event_intents, |
| base::OnceClosure callback) { |
| DCHECK_EQ(document.Lifecycle().GetState(), |
| DocumentLifecycle::kInAccessibility); |
| |
| base::AutoReset<ax::mojom::blink::EventFrom> event_from_resetter( |
| &active_event_from_, event_from); |
| base::AutoReset<ax::mojom::blink::Action> event_from_action_resetter( |
| &active_event_from_action_, event_from_action); |
| ScopedBlinkAXEventIntent defered_event_intents(event_intents.AsVector(), |
| &document); |
| std::move(callback).Run(); |
| } |
| |
| void AXObjectCacheImpl::FireAXEventImmediately( |
| AXObject* obj, |
| ax::mojom::blink::Event event_type, |
| ax::mojom::blink::EventFrom event_from, |
| ax::mojom::blink::Action event_from_action, |
| const BlinkAXEventIntentsSet& event_intents) { |
| DCHECK_EQ(obj->GetDocument()->Lifecycle().GetState(), |
| DocumentLifecycle::kInAccessibility); |
| |
| #if DCHECK_IS_ON() |
| // Make sure none of the layout views are in the process of being laid out. |
| // Notifications should only be sent after the layoutObject has finished |
| auto* ax_layout_object = DynamicTo<AXLayoutObject>(obj); |
| if (ax_layout_object) { |
| LayoutObject* layout_object = ax_layout_object->GetLayoutObject(); |
| if (layout_object && layout_object->View()) |
| DCHECK(!layout_object->View()->GetLayoutState()); |
| } |
| |
| SCOPED_DISALLOW_LIFECYCLE_TRANSITION(*obj->GetDocument()); |
| #endif // DCHECK_IS_ON() |
| |
| PostPlatformNotification(obj, event_type, event_from, event_from_action, |
| event_intents); |
| |
| if (event_type == ax::mojom::blink::Event::kChildrenChanged && |
| obj->CachedParentObject()) { |
| const bool was_ignored = obj->LastKnownIsIgnoredValue(); |
| const bool was_in_tree = obj->LastKnownIsIncludedInTreeValue(); |
| obj->UpdateCachedAttributeValuesIfNeeded(false); |
| const bool is_ignored = obj->LastKnownIsIgnoredValue(); |
| const bool is_in_tree = obj->LastKnownIsIncludedInTreeValue(); |
| |
| if (is_ignored != was_ignored || was_in_tree != is_in_tree) |
| ChildrenChangedWithCleanLayout(obj->CachedParentObject()); |
| } |
| } |
| |
| bool AXObjectCacheImpl::IsAriaOwned(const AXObject* object) const { |
| return relation_cache_->IsAriaOwned(object); |
| } |
| |
| AXObject* AXObjectCacheImpl::GetAriaOwnedParent(const AXObject* object) const { |
| return relation_cache_->GetAriaOwnedParent(object); |
| } |
| |
| void AXObjectCacheImpl::GetAriaOwnedChildren( |
| const AXObject* owner, |
| HeapVector<Member<AXObject>>& owned_children) { |
| DCHECK(GetDocument().Lifecycle().GetState() >= |
| DocumentLifecycle::kLayoutClean); |
| relation_cache_->GetAriaOwnedChildren(owner, owned_children); |
| } |
| |
| bool AXObjectCacheImpl::MayHaveHTMLLabel(const HTMLElement& elem) { |
| // Return false if this type of element will not accept a <label for> label. |
| if (!elem.IsLabelable()) |
| return false; |
| |
| // Return true if a <label for> pointed to this element at some point. |
| if (relation_cache_->MayHaveHTMLLabelViaForAttribute(elem)) |
| return true; |
| |
| // Return true if any amcestor is a label, as in <label><input></label>. |
| return Traversal<HTMLLabelElement>::FirstAncestor(elem); |
| } |
| |
| void AXObjectCacheImpl::CheckedStateChanged(Node* node) { |
| DeferTreeUpdate(&AXObjectCacheImpl::PostNotification, node, |
| ax::mojom::blink::Event::kCheckedStateChanged); |
| } |
| |
| void AXObjectCacheImpl::ListboxOptionStateChanged(HTMLOptionElement* option) { |
| PostNotification(option, ax::mojom::Event::kCheckedStateChanged); |
| } |
| |
| void AXObjectCacheImpl::ListboxSelectedChildrenChanged( |
| HTMLSelectElement* select) { |
| PostNotification(select, ax::mojom::Event::kSelectedChildrenChanged); |
| } |
| |
| void AXObjectCacheImpl::ListboxActiveIndexChanged(HTMLSelectElement* select) { |
| SCOPED_DISALLOW_LIFECYCLE_TRANSITION(select->GetDocument()); |
| |
| auto* ax_object = DynamicTo<AXListBox>(Get(select)); |
| if (!ax_object) |
| return; |
| |
| ax_object->ActiveIndexChanged(); |
| } |
| |
| void AXObjectCacheImpl::LocationChanged(const LayoutObject* layout_object) { |
| // No need to send this notification if the object is aria-hidden. |
| // Note that if the node is ignored for other reasons, it still might |
| // be important to send this notification if any of its children are |
| // visible - but in the case of aria-hidden we can safely ignore it. |
| AXObject* obj = Get(layout_object); |
| if (obj && obj->AriaHiddenRoot()) |
| return; |
| |
| PostNotification(layout_object, ax::mojom::Event::kLocationChanged); |
| } |
| |
| void AXObjectCacheImpl::ImageLoaded(const LayoutObject* layout_object) { |
| AXObject* obj = Get(layout_object); |
| MarkAXObjectDirty(obj); |
| } |
| |
| void AXObjectCacheImpl::HandleClicked(Node* node) { |
| if (AXObject* obj = Get(node)) |
| PostNotification(obj, ax::mojom::Event::kClicked); |
| } |
| |
| void AXObjectCacheImpl::HandleAttributeChanged( |
| const QualifiedName& attr_name, |
| AccessibleNode* accessible_node) { |
| if (!accessible_node) |
| return; |
| modification_count_++; |
| if (AXObject* obj = Get(accessible_node)) |
| PostNotification(obj, ax::mojom::Event::kAriaAttributeChanged); |
| } |
| |
| void AXObjectCacheImpl::HandleAriaExpandedChangeWithCleanLayout(Node* node) { |
| if (!node) |
| return; |
| |
| SCOPED_DISALLOW_LIFECYCLE_TRANSITION(node->GetDocument()); |
| |
| DCHECK(!node->GetDocument().NeedsLayoutTreeUpdateForNode(*node)); |
| if (AXObject* obj = GetOrCreate(node)) |
| obj->HandleAriaExpandedChanged(); |
| } |
| |
| void AXObjectCacheImpl::HandleAriaSelectedChangedWithCleanLayout(Node* node) { |
| DCHECK(node); |
| SCOPED_DISALLOW_LIFECYCLE_TRANSITION(node->GetDocument()); |
| |
| DCHECK(!node->GetDocument().NeedsLayoutTreeUpdateForNode(*node)); |
| AXObject* obj = Get(node); |
| if (!obj) |
| return; |
| |
| PostNotification(obj, ax::mojom::Event::kCheckedStateChanged); |
| |
| AXObject* listbox = obj->ParentObjectUnignored(); |
| if (listbox && listbox->RoleValue() == ax::mojom::Role::kListBox) { |
| // Ensure listbox options are in sync as selection status may have changed |
| MarkAXSubtreeDirty(listbox); |
| PostNotification(listbox, ax::mojom::Event::kSelectedChildrenChanged); |
| } |
| } |
| |
| void AXObjectCacheImpl::HandleNodeLostFocusWithCleanLayout(Node* node) { |
| DCHECK(node); |
| DCHECK(!node->GetDocument().NeedsLayoutTreeUpdateForNode(*node)); |
| AXObject* obj = Get(node); |
| if (!obj) |
| return; |
| |
| TRACE_EVENT1("accessibility", |
| "AXObjectCacheImpl::HandleNodeLostFocusWithCleanLayout", "id", |
| obj->AXObjectID()); |
| PostNotification(obj, ax::mojom::Event::kBlur); |
| |
| if (AXObject* active_descendant = obj->ActiveDescendant()) { |
| if (active_descendant->IsSelectedFromFocusSupported()) |
| HandleAriaSelectedChangedWithCleanLayout(active_descendant->GetNode()); |
| } |
| } |
| |
| void AXObjectCacheImpl::HandleNodeGainedFocusWithCleanLayout(Node* node) { |
| node = FocusedElement(); // Needs to get this with clean layout. |
| if (!node || !node->GetDocument().View()) |
| return; |
| |
| if (node->GetDocument().NeedsLayoutTreeUpdateForNode(*node)) { |
| // This should only occur when focus goes into a popup document. The main |
| // document has an updated layout, but the popup does not. |
| DCHECK_NE(document_, node->GetDocument()); |
| node->GetDocument().View()->UpdateAllLifecyclePhasesExceptPaint( |
| DocumentUpdateReason::kAccessibility); |
| } |
| |
| AXObject* obj = GetOrCreateFocusedObjectFromNode(node); |
| if (!obj) |
| return; |
| |
| TRACE_EVENT1("accessibility", |
| "AXObjectCacheImpl::HandleNodeGainedFocusWithCleanLayout", "id", |
| obj->AXObjectID()); |
| PostNotification(obj, ax::mojom::Event::kFocus); |
| |
| if (AXObject* active_descendant = obj->ActiveDescendant()) { |
| if (active_descendant->IsSelectedFromFocusSupported()) |
| HandleAriaSelectedChangedWithCleanLayout(active_descendant->GetNode()); |
| } |
| } |
| |
| // This might be the new target of a relation. Handle all possible cases. |
| void AXObjectCacheImpl::MaybeNewRelationTarget(Node& node, AXObject* obj) { |
| // Track reverse relations |
| relation_cache_->UpdateRelatedTree(&node, obj); |
| |
| if (!obj) |
| return; |
| |
| DCHECK_EQ(obj->GetNode(), &node); |
| |
| // Check whether aria-activedescendant on the focused object points to |
| // |obj|. If so, fire activedescendantchanged event now. This is only for |
| // ARIA active descendants, not in a native control like a listbox, which |
| // has its own initial active descendant handling. |
| Node* focused_node = document_->FocusedElement(); |
| if (focused_node) { |
| AXObject* focus = Get(focused_node); |
| if (focus && |
| focus->GetAOMPropertyOrARIAAttribute( |
| AOMRelationProperty::kActiveDescendant) == &node && |
| obj->CanBeActiveDescendant()) { |
| focus->HandleActiveDescendantChanged(); |
| } |
| } |
| } |
| |
| void AXObjectCacheImpl::HandleActiveDescendantChangedWithCleanLayout( |
| Node* node) { |
| DCHECK(node); |
| DCHECK(!node->GetDocument().NeedsLayoutTreeUpdateForNode(*node)); |
| // Changing the active descendant should trigger recomputing all |
| // cached values even if it doesn't result in a notification, because |
| // it can affect what's focusable or not. |
| modification_count_++; |
| |
| if (AXObject* obj = GetOrCreate(node)) |
| obj->HandleActiveDescendantChanged(); |
| } |
| |
| // A <section> or role=region uses the region role if and only if it has a name. |
| void AXObjectCacheImpl::SectionOrRegionRoleMaybeChanged(Element* element) { |
| AXObject* ax_object = Get(element); |
| if (!ax_object) |
| return; |
| |
| // Require <section> or role="region" markup. |
| if (!element->HasTagName(html_names::kSectionTag) && |
| ax_object->RawAriaRole() != ax::mojom::blink::Role::kRegion) { |
| return; |
| } |
| |
| // If role would stay the same, do nothing. |
| if (ax_object->RoleValue() == ax_object->DetermineAccessibilityRole()) |
| return; |
| |
| Invalidate(element->GetDocument(), ax_object->AXObjectID()); |
| } |
| |
| // Be as safe as possible about changes that could alter the accessibility role, |
| // as this may require a different subclass of AXObject. |
| // Role changes are disallowed by the spec but we must handle it gracefully, see |
| // https://www.w3.org/TR/wai-aria-1.1/#h-roles for more information. |
| void AXObjectCacheImpl::HandleRoleChangeWithCleanLayout(Node* node) { |
| if (!node) |
| return; // Virtual AOM node. |
| |
| DCHECK(!node->GetDocument().NeedsLayoutTreeUpdateForNode(*node)); |
| |
| // Remove the current object and make the parent reconsider its children. |
| if (AXObject* obj = GetOrCreate(node)) { |
| // When the role of `obj` is changed, its AXObject needs to be destroyed and |
| // a new one needs to be created in its place. We destroy the current |
| // AXObject in this method and call ChildrenChangeWithCleanLayout() on the |
| // parent so that future updates to its children will create the alert. |
| ChildrenChangedWithCleanLayout(obj->CachedParentObject()); |
| if (int depth = RolePresentationPropagationDepth(node)) { |
| // If role changes on a table, menu, or list invalidate the subtree of |
| // objects that may require a specific parent role in order to keep their |
| // role. For example, rows and cells require a table ancestor, and list |
| // items require a parent list (must be direct DOM parent). |
| RemoveAXObjectsInLayoutSubtree(obj, depth); |
| } else { |
| // The children of this thing need to detach from parent. |
| Remove(obj); |
| } |
| // The aria-owns relation may have changed if the role changed, |
| // because some roles allow aria-owns and others don't. |
| // In addition, any owned objects need to reset their parent_ to point |
| // to the new object. |
| if (AXObject* new_object = GetOrCreate(node)) |
| relation_cache_->UpdateAriaOwnsWithCleanLayout(new_object, true); |
| } |
| } |
| |
| void AXObjectCacheImpl::HandleAriaHiddenChangedWithCleanLayout(Node* node) { |
| if (!node) |
| return; |
| |
| SCOPED_DISALLOW_LIFECYCLE_TRANSITION(node->GetDocument()); |
| DCHECK(!node->GetDocument().NeedsLayoutTreeUpdateForNode(*node)); |
| |
| AXObject* obj = GetOrCreate(node); |
| if (!obj) |
| return; |
| |
| // https://www.w3.org/TR/wai-aria-1.1/#aria-hidden |
| // An element is considered hidden if it, or any of its ancestors are not |
| // rendered or have their aria-hidden attribute value set to true. |
| AXObject* parent = obj->ParentObject(); |
| if (parent) { |
| // If the parent is inert or aria-hidden, then the subtree will be |
| // ignored and changing aria-hidden will have no effect. |
| // |IsInertOrAriaHidden| returns true if the element or one of its |
| // ancestors is either inert or within an aria-hidden subtree. |
| if (parent->IsInertOrAriaHidden()) |
| return; |
| // If the parent is 'display: none', then the subtree will be ignored and |
| // changing aria-hidden will have no effect. |
| if (parent->GetLayoutObject()) { |
| // For elements with layout objects we can get their style directly. |
| if (parent->GetLayoutObject()->Style()->Display() == EDisplay::kNone) |
| return; |
| } else if (Element* parent_element = parent->GetElement()) { |
| // No layout object: must ensure computed style. |
| const ComputedStyle* parent_style = parent_element->EnsureComputedStyle(); |
| if (!parent_style || parent_style->IsEnsuredInDisplayNone()) |
| return; |
| } |
| // Unlike AXObject's |IsVisible| or |IsHiddenViaStyle| this method does not |
| // consider 'visibility: [hidden|collapse]', because while the visibility |
| // property is inherited it can be overridden by any descendant by providing |
| // 'visibility: visible' so it would be safest to invalidate the subtree in |
| // such a case. |
| } |
| |
| // Changing the aria hidden state should trigger recomputing all |
| // cached values even if it doesn't result in a notification, because |
| // it affects accessibility ignored state. |
| modification_count_++; |
| |
| // Invalidate the subtree because aria-hidden affects the |
| // accessibility ignored state for the entire subtree. |
| MarkAXSubtreeDirtyWithCleanLayout(obj); |
| ChildrenChangedWithCleanLayout(obj->CachedParentObject()); |
| } |
| |
| void AXObjectCacheImpl::HandleAttributeChanged(const QualifiedName& attr_name, |
| Element* element) { |
| DCHECK(element); |
| DeferTreeUpdate(&AXObjectCacheImpl::HandleAttributeChangedWithCleanLayout, |
| attr_name, element); |
| } |
| |
| void AXObjectCacheImpl::HandleAttributeChangedWithCleanLayout( |
| const QualifiedName& attr_name, |
| Element* element) { |
| DCHECK(element); |
| DCHECK(!element->GetDocument().NeedsLayoutTreeUpdateForNode(*element)); |
| if (attr_name == html_names::kRoleAttr || |
| attr_name == html_names::kTypeAttr) { |
| HandleRoleChangeWithCleanLayout(element); |
| } else if (attr_name == html_names::kSizeAttr || |
| attr_name == html_names::kAriaHaspopupAttr) { |
| // Role won't change on edits, so avoid invalidation so that object is not |
| // destroyed during editing. |
| if (AXObject* obj = Get(element)) { |
| if (!obj->IsTextField()) |
| HandleRoleChangeWithCleanLayout(element); |
| } |
| } else if (attr_name == html_names::kAltAttr) { |
| TextChangedWithCleanLayout(element); |
| } else if (attr_name == html_names::kTitleAttr) { |
| TextChangedWithCleanLayout(element); |
| SectionOrRegionRoleMaybeChanged(element); |
| } else if (attr_name == html_names::kForAttr && |
| IsA<HTMLLabelElement>(*element)) { |
| LabelChangedWithCleanLayout(element); |
| } else if (attr_name == html_names::kIdAttr) { |
| MaybeNewRelationTarget(*element, Get(element)); |
| } else if (attr_name == html_names::kTabindexAttr) { |
| FocusableChangedWithCleanLayout(element); |
| } else if (attr_name == html_names::kDisabledAttr || |
| attr_name == html_names::kReadonlyAttr) { |
| MarkElementDirtyWithCleanLayout(element); |
| } else if (attr_name == html_names::kValueAttr) { |
| HandleValueChanged(element); |
| } else if (attr_name == html_names::kMinAttr || |
| attr_name == html_names::kMaxAttr) { |
| MarkElementDirtyWithCleanLayout(element); |
| } else if (attr_name == html_names::kStepAttr) { |
| MarkElementDirtyWithCleanLayout(element); |
| } else if (attr_name == html_names::kUsemapAttr) { |
| HandleUseMapAttributeChangedWithCleanLayout(element); |
| } else if (attr_name == html_names::kNameAttr) { |
| HandleNameAttributeChangedWithCleanLayout(element); |
| } else if (attr_name == html_names::kControlsAttr) { |
| ChildrenChangedWithCleanLayout(element); |
| } |
| |
| if (!attr_name.LocalName().StartsWith("aria-")) |
| return; |
| |
| // Perform updates specific to each attribute. |
| if (attr_name == html_names::kAriaActivedescendantAttr) { |
| HandleActiveDescendantChangedWithCleanLayout(element); |
| } else if (attr_name == html_names::kAriaValuenowAttr || |
| attr_name == html_names::kAriaValuetextAttr) { |
| HandleValueChanged(element); |
| } else if (attr_name == html_names::kAriaLabelAttr || |
| attr_name == html_names::kAriaLabeledbyAttr || |
| attr_name == html_names::kAriaLabelledbyAttr) { |
| TextChangedWithCleanLayout(element); |
| SectionOrRegionRoleMaybeChanged(element); |
| } else if (attr_name == html_names::kAriaDescriptionAttr || |
| attr_name == html_names::kAriaDescribedbyAttr) { |
| TextChangedWithCleanLayout(element); |
| } else if (attr_name == html_names::kAriaCheckedAttr || |
| attr_name == html_names::kAriaPressedAttr) { |
| PostNotification(element, ax::mojom::blink::Event::kCheckedStateChanged); |
| } else if (attr_name == html_names::kAriaSelectedAttr) { |
| HandleAriaSelectedChangedWithCleanLayout(element); |
| } else if (attr_name == html_names::kAriaExpandedAttr) { |
| HandleAriaExpandedChangeWithCleanLayout(element); |
| } else if (attr_name == html_names::kAriaHiddenAttr) { |
| HandleAriaHiddenChangedWithCleanLayout(element); |
| } else if (attr_name == html_names::kAriaInvalidAttr) { |
| MarkElementDirtyWithCleanLayout(element); |
| } else if (attr_name == html_names::kAriaErrormessageAttr) { |
| MarkElementDirtyWithCleanLayout(element); |
| } else if (attr_name == html_names::kAriaOwnsAttr) { |
| if (AXObject* obj = GetOrCreate(element)) |
| relation_cache_->UpdateAriaOwnsWithCleanLayout(obj); |
| } else { |
| PostNotification(element, ax::mojom::Event::kAriaAttributeChanged); |
| } |
| } |
| |
| void AXObjectCacheImpl::HandleUseMapAttributeChangedWithCleanLayout( |
| Element* element) { |
| if (!IsA<HTMLImageElement>(element)) |
| return; |
| // Get an area (aka image link) from the previous usemap. |
| AXObject* ax_image = Get(element); |
| AXObject* ax_image_link = |
| ax_image ? ax_image->FirstChildIncludingIgnored() : nullptr; |
| HTMLMapElement* previous_map = |
| ax_image_link && ax_image_link->GetNode() |
| ? Traversal<HTMLMapElement>::FirstAncestor(*ax_image_link->GetNode()) |
| : nullptr; |
| // Both the old and new image may change image <--> image map. |
| HandleRoleChangeWithCleanLayout(element); |
| if (previous_map) |
| HandleRoleChangeWithCleanLayout(previous_map->ImageElement()); |
| } |
| |
| void AXObjectCacheImpl::HandleNameAttributeChangedWithCleanLayout( |
| Element* element) { |
| // Changing a map name can alter an image's role and children. |
| // The name has already changed, so we can no longer find the primary image |
| // via the DOM. Use an area child's parent to find the old image. |
| // If the old image was treated as a map, and now isn't, it will take care |
| // of updating any other image that is newly associated with the map, |
| // via AXNodeObject::AddImageMapChildren(). |
| if (HTMLMapElement* map = DynamicTo<HTMLMapElement>(element)) { |
| if (AXObject* ax_previous_image = GetAXImageForMap(*map)) |
| HandleRoleChangeWithCleanLayout(ax_previous_image->GetNode()); |
| } |
| } |
| |
| AXObject* AXObjectCacheImpl::GetOrCreateValidationMessageObject() { |
| AXObject* message_ax_object = nullptr; |
| // Create only if it does not already exist. |
| if (validation_message_axid_) { |
| message_ax_object = ObjectFromAXID(validation_message_axid_); |
| } |
| if (message_ax_object) { |
| DCHECK(!message_ax_object->IsDetached()); |
| message_ax_object->SetParent(Root()); // Reattach to parent (root). |
| } else { |
| message_ax_object = MakeGarbageCollected<AXValidationMessage>(*this); |
| DCHECK(message_ax_object); |
| // Cache the validation message container for reuse. |
| validation_message_axid_ = AssociateAXID(message_ax_object); |
| // Validation message alert object is a child of the document, as not all |
| // form controls can have a child. Also, there are form controls such as |
| // listbox that technically can have children, but they are probably not |
| // expected to have alerts within AT client code. |
| message_ax_object->Init(Root()); |
| } |
| return message_ax_object; |
| } |
| |
| AXObject* AXObjectCacheImpl::ValidationMessageObjectIfInvalid( |
| bool notify_children_changed) { |
| Element* focused_element = document_->FocusedElement(); |
| if (focused_element) { |
| ListedElement* form_control = ListedElement::From(*focused_element); |
| if (form_control && !form_control->IsNotCandidateOrValid()) { |
| // These must both be true: |
| // * Focused control is currently invalid. |
| // * Validation message was previously created but hidden |
| // from timeout or currently visible. |
| bool was_validation_message_already_created = validation_message_axid_; |
| if (was_validation_message_already_created || |
| form_control->IsValidationMessageVisible()) { |
| AXObject* focused_object = FocusedObject(); |
| if (focused_object) { |
| // Return as long as the focused form control isn't overriding with a |
| // different message via aria-errormessage. |
| bool override_native_validation_message = |
| focused_object->GetAOMPropertyOrARIAAttribute( |
| AOMRelationProperty::kErrorMessage); |
| if (!override_native_validation_message) { |
| AXObject* message = GetOrCreateValidationMessageObject(); |
| DCHECK(message); |
| DCHECK(!message->IsDetached()); |
| if (notify_children_changed && |
| Root()->FirstChildIncludingIgnored() != message) { |
| // Only notify children changed if not already processing new root |
| // children, and the root doesn't already have this child. |
| ChildrenChanged(document_); |
| } |
| DCHECK_EQ(message->CachedParentObject(), Root()); |
| return message; |
| } |
| } |
| } |
| } |
| } |
| |
| // No focused, invalid form control. |
| if (validation_message_axid_) { |
| DeferTreeUpdate( |
| &AXObjectCacheImpl::RemoveValidationMessageObjectWithCleanLayout, |
| document_); |
| } |
| return nullptr; |
| } |
| |
| void AXObjectCacheImpl::RemoveValidationMessageObjectWithCleanLayout( |
| Node* document) { |
| DCHECK_EQ(document, document_); |
| if (validation_message_axid_) { |
| // Remove when it becomes hidden, so that a new object is created the next |
| // time the message becomes visible. It's not possible to reuse the same |
| // alert, because the event generator will not generate an alert event if |
| // the same object is hidden and made visible quickly, which occurs if the |
| // user submits the form when an alert is already visible. |
| Remove(validation_message_axid_); |
| validation_message_axid_ = 0; |
| ChildrenChanged(document_); |
| } |
| } |
| |
| // Native validation error popup for focused form control in current document. |
| void AXObjectCacheImpl::HandleValidationMessageVisibilityChanged( |
| const Node* form_control) { |
| DCHECK(form_control); |
| SCOPED_DISALLOW_LIFECYCLE_TRANSITION(form_control->GetDocument()); |
| |
| DeferTreeUpdate(&AXObjectCacheImpl:: |
| HandleValidationMessageVisibilityChangedWithCleanLayout, |
| form_control); |
| } |
| |
| void AXObjectCacheImpl::HandleValidationMessageVisibilityChangedWithCleanLayout( |
| const Node* form_control) { |
| #if DCHECK_IS_ON() |
| DCHECK(form_control); |
| Document* document = &form_control->GetDocument(); |
| DCHECK(document); |
| DCHECK(document->Lifecycle().GetState() >= DocumentLifecycle::kLayoutClean) |
| << "Unclean document at lifecycle " << document->Lifecycle().ToString(); |
| #endif // DCHECK_IS_ON() |
| |
| AXObject* message_ax_object = ValidationMessageObjectIfInvalid( |
| /* Fire children changed on root if it gains message child */ true); |
| if (message_ax_object) // May be invisible now. |
| MarkAXObjectDirtyWithCleanLayout(message_ax_object); |
| |
| // If the form control is invalid, it will now have an error message relation |
| // to the message container. |
| MarkElementDirtyWithCleanLayout(form_control); |
| } |
| |
| void AXObjectCacheImpl::HandleEventListenerAdded( |
| const Node& node, |
| const AtomicString& event_type) { |
| // If this is the first |event_type| listener for |node|, handle the |
| // subscription change. |
| if (node.NumberOfEventListeners(event_type) == 1) |
| HandleEventSubscriptionChanged(node, event_type); |
| } |
| |
| void AXObjectCacheImpl::HandleEventListenerRemoved( |
| const Node& node, |
| const AtomicString& event_type) { |
| // If there are no more |event_type| listeners for |node|, handle the |
| // subscription change. |
| if (node.NumberOfEventListeners(event_type) == 0) |
| HandleEventSubscriptionChanged(node, event_type); |
| } |
| |
| bool AXObjectCacheImpl::DoesEventListenerImpactIgnoredState( |
| const AtomicString& event_type) const { |
| return event_util::IsMouseButtonEventType(event_type); |
| } |
| |
| void AXObjectCacheImpl::HandleEventSubscriptionChanged( |
| const Node& node, |
| const AtomicString& event_type) { |
| // Adding or Removing an event listener for certain events may affect whether |
| // a node or its descendants should be accessibility ignored. |
| if (!DoesEventListenerImpactIgnoredState(event_type)) |
| return; |
| |
| // If the |event_type| may affect the ignored state of |node|, which means |
| // that the parent's children may have changed. |
| modification_count_++; |
| MarkElementDirty(&node); |
| } |
| |
| void AXObjectCacheImpl::LabelChangedWithCleanLayout(Element* element) { |
| // Will call back to TextChanged() when done updating relation cache. |
| relation_cache_->LabelChanged(element); |
| } |
| |
| void AXObjectCacheImpl::InlineTextBoxesUpdated(LayoutObject* layout_object) { |
| if (!InlineTextBoxAccessibilityEnabled()) |
| return; |
| |
| AXID ax_id = layout_object_mapping_.at(layout_object); |
| DCHECK(!HashTraits<AXID>::IsDeletedValue(ax_id)); |
| |
| // Only update if the accessibility object already exists and it's |
| // not already marked as dirty. |
| // Do not use Get(): it does extra work to determine whether the object should |
| // be invalidated, including calling IsLayoutObjectRelevantForAccessibility(), |
| // which uses the NGInlineCursor. However, the NGInlineCursor cannot be used |
| // while inline boxes are being updated. |
| if (ax_id) { |
| AXObject* obj = objects_.at(ax_id); |
| DCHECK(obj); |
| DCHECK(obj->IsAXLayoutObject()); |
| DCHECK(!obj->IsDetached()); |
| if (!obj->NeedsToUpdateChildren()) { |
| obj->SetNeedsToUpdateChildren(); |
| PostNotification(obj, ax::mojom::blink::Event::kChildrenChanged); |
| } |
| } |
| } |
| |
| Settings* AXObjectCacheImpl::GetSettings() { |
| return document_->GetSettings(); |
| } |
| |
| bool AXObjectCacheImpl::InlineTextBoxAccessibilityEnabled() { |
| Settings* settings = GetSettings(); |
| if (!settings) |
| return false; |
| return settings->GetInlineTextBoxAccessibilityEnabled(); |
| } |
| |
| const Element* AXObjectCacheImpl::RootAXEditableElement(const Node* node) { |
| const Element* result = RootEditableElement(*node); |
| const auto* element = DynamicTo<Element>(node); |
| if (!element) |
| element = node->parentElement(); |
| |
| for (; element; element = element->parentElement()) { |
| if (NodeIsTextControl(element)) |
| result = element; |
| } |
| |
| return result; |
| } |
| |
| bool AXObjectCacheImpl::NodeIsTextControl(const Node* node) { |
| if (!node) |
| return false; |
| |
| const AXObject* ax_object = GetOrCreate(const_cast<Node*>(node)); |
| return ax_object && ax_object->IsTextField(); |
| } |
| |
| bool IsNodeAriaVisible(Node* node) { |
| auto* element = DynamicTo<Element>(node); |
| if (!element) |
| return false; |
| |
| bool is_null = true; |
| bool hidden = AccessibleNode::GetPropertyOrARIAAttribute( |
| element, AOMBooleanProperty::kHidden, is_null); |
| return !is_null && !hidden; |
| } |
| |
| void AXObjectCacheImpl::PostPlatformNotification( |
| AXObject* obj, |
| ax::mojom::blink::Event event_type, |
| ax::mojom::blink::EventFrom event_from, |
| ax::mojom::blink::Action event_from_action, |
| const BlinkAXEventIntentsSet& event_intents) { |
| if (!document_ || !document_->View() || |
| !document_->View()->GetFrame().GetPage()) { |
| return; |
| } |
| |
| WebLocalFrameImpl* web_frame = |
| WebLocalFrameImpl::FromFrame(document_->AXObjectCacheOwner().GetFrame()); |
| if (web_frame && web_frame->Client()) { |
| ui::AXEvent event; |
| event.id = obj->AXObjectID(); |
| event.event_type = event_type; |
| event.event_from = event_from; |
| event.event_from_action = event_from_action; |
| event.event_intents.resize(event_intents.size()); |
| // We need to filter out the counts from every intent. |
| std::transform(event_intents.begin(), event_intents.end(), |
| event.event_intents.begin(), |
| [](const auto& intent) { return intent.key.intent(); }); |
| |
| web_frame->Client()->PostAccessibilityEvent(event); |
| } |
| } |
| |
| void AXObjectCacheImpl::MarkAXObjectDirtyWithCleanLayoutHelper(AXObject* obj, |
| bool subtree) { |
| if (!obj || obj->IsDetached() || !obj->GetDocument() || |
| !obj->GetDocument()->View() || |
| !obj->GetDocument()->View()->GetFrame().GetPage()) { |
| return; |
| } |
| |
| WebLocalFrameImpl* webframe = WebLocalFrameImpl::FromFrame( |
| obj->GetDocument()->AXObjectCacheOwner().GetFrame()); |
| if (webframe && webframe->Client()) |
| webframe->Client()->MarkWebAXObjectDirty(WebAXObject(obj), subtree); |
| obj->UpdateCachedAttributeValuesIfNeeded(true); |
| } |
| |
| void AXObjectCacheImpl::MarkAXObjectDirtyWithCleanLayout(AXObject* obj) { |
| MarkAXObjectDirtyWithCleanLayoutHelper(obj, false); |
| } |
| |
| void AXObjectCacheImpl::MarkAXSubtreeDirtyWithCleanLayout(AXObject* obj) { |
| MarkAXObjectDirtyWithCleanLayoutHelper(obj, true); |
| } |
| |
| void AXObjectCacheImpl::MarkAXObjectDirty(AXObject* obj) { |
| if (!obj) |
| return; |
| base::OnceClosure callback = |
| WTF::Bind(&AXObjectCacheImpl::MarkAXObjectDirtyWithCleanLayout, |
| WrapWeakPersistent(this), WrapWeakPersistent(obj)); |
| DeferTreeUpdateInternal(std::move(callback), obj); |
| } |
| |
| void AXObjectCacheImpl::MarkAXSubtreeDirty(AXObject* obj) { |
| if (!obj) |
| return; |
| base::OnceClosure callback = |
| WTF::Bind(&AXObjectCacheImpl::MarkAXSubtreeDirtyWithCleanLayout, |
| WrapWeakPersistent(this), WrapWeakPersistent(obj)); |
| DeferTreeUpdateInternal(std::move(callback), obj); |
| } |
| |
| void AXObjectCacheImpl::MarkElementDirty(const Node* element) { |
| // Warning, if no AXObject exists for element, nothing is marked dirty. |
| MarkAXObjectDirty(Get(element)); |
| } |
| |
| void AXObjectCacheImpl::MarkElementDirtyWithCleanLayout(const Node* element) { |
| // Warning, if no AXObject exists for element, nothing is marked dirty. |
| MarkAXObjectDirtyWithCleanLayout(Get(element)); |
| } |
| |
| void AXObjectCacheImpl::HandleFocusedUIElementChanged( |
| Element* old_focused_element, |
| Element* new_focused_element) { |
| TRACE_EVENT0("accessibility", |
| "AXObjectCacheImpl::HandleFocusedUIElementChanged"); |
| Document& focused_doc = |
| new_focused_element ? new_focused_element->GetDocument() : *document_; |
| |
| #if DCHECK_IS_ON() |
| // The focus can be in a different document when a popup is open. |
| SCOPED_DISALLOW_LIFECYCLE_TRANSITION(focused_doc); |
| #endif // DCHECK_IS_ON() |
| |
| if (focused_doc.GetPage() && focused_doc.GetPage()->InsidePortal()) |
| return; // Elements inside a portal are not considered focusable. |
| |
| if (validation_message_axid_) { |
| DeferTreeUpdate( |
| &AXObjectCacheImpl::RemoveValidationMessageObjectWithCleanLayout, |
| document_); |
| } |
| |
| if (!new_focused_element) { |
| // When focus is cleared, implicitly focus the document by sending a blur. |
| if (GetDocument().documentElement()) { |
| DeferTreeUpdate(&AXObjectCacheImpl::HandleNodeLostFocusWithCleanLayout, |
| GetDocument().documentElement()); |
| } |
| return; |
| } |
| |
| Page* page = new_focused_element->GetDocument().GetPage(); |
| if (!page) |
| return; |
| |
| if (old_focused_element) { |
| DeferTreeUpdate(&AXObjectCacheImpl::HandleNodeLostFocusWithCleanLayout, |
| old_focused_element); |
| } |
| |
| Settings* settings = GetSettings(); |
| if (settings && settings->GetAriaModalPrunesAXTree()) |
| UpdateActiveAriaModalDialog(new_focused_element); |
| |
| DeferTreeUpdate(&AXObjectCacheImpl::HandleNodeGainedFocusWithCleanLayout, |
| FocusedElement()); |
| } |
| |
| // Check if the focused node is inside an active aria-modal dialog. If so, we |
| // should mark the cache as dirty to recompute the ignored status of each node. |
| void AXObjectCacheImpl::UpdateActiveAriaModalDialog(Node* node) { |
| AXObject* new_active_aria_modal = AncestorAriaModalDialog(node); |
| if (active_aria_modal_dialog_ == new_active_aria_modal) |
| return; |
| |
| active_aria_modal_dialog_ = new_active_aria_modal; |
| modification_count_++; |
| MarkAXSubtreeDirty(Root()); |
| } |
| |
| AXObject* AXObjectCacheImpl::AncestorAriaModalDialog(Node* node) { |
| for (Element* ancestor = Traversal<Element>::FirstAncestorOrSelf(*node); |
| ancestor; ancestor = Traversal<Element>::FirstAncestor(*ancestor)) { |
| if (!ancestor->FastHasAttribute(html_names::kAriaModalAttr)) |
| continue; |
| |
| AtomicString aria_modal = |
| ancestor->FastGetAttribute(html_names::kAriaModalAttr); |
| if (!EqualIgnoringASCIICase(aria_modal, "true")) { |
| continue; |
| } |
| |
| AXObject* ancestor_ax_object = GetOrCreate(ancestor); |
| if (!ancestor_ax_object) |
| return nullptr; |
| ax::mojom::blink::Role ancestor_role = ancestor_ax_object->RoleValue(); |
| |
| if (!ui::IsDialog(ancestor_role)) |
| continue; |
| |
| return ancestor_ax_object; |
| } |
| return nullptr; |
| } |
| |
| AXObject* AXObjectCacheImpl::GetActiveAriaModalDialog() const { |
| return active_aria_modal_dialog_; |
| } |
| |
| HeapVector<Member<AXObject>> |
| AXObjectCacheImpl::GetAllObjectsWithChangedBounds() { |
| VectorOf<AXObject> changed_bounds_objects; |
| changed_bounds_objects.ReserveCapacity(changed_bounds_ids_.size()); |
| for (AXID changed_bounds_id : changed_bounds_ids_) { |
| if (AXObject* obj = ObjectFromAXID(changed_bounds_id)) |
| changed_bounds_objects.push_back(obj); |
| } |
| changed_bounds_ids_.clear(); |
| return changed_bounds_objects; |
| } |
| |
| void AXObjectCacheImpl::HandleInitialFocus() { |
| PostNotification(document_, ax::mojom::Event::kFocus); |
| } |
| |
| void AXObjectCacheImpl::HandleEditableTextContentChanged(Node* node) { |
| if (!node) |
| return; |
| |
| SCOPED_DISALLOW_LIFECYCLE_TRANSITION(node->GetDocument()); |
| |
| DeferTreeUpdate( |
| &AXObjectCacheImpl::HandleEditableTextContentChangedWithCleanLayout, |
| node); |
| } |
| |
| void AXObjectCacheImpl::HandleEditableTextContentChangedWithCleanLayout( |
| Node* node) { |
| AXObject* obj = nullptr; |
| do { |
| obj = GetOrCreate(node); |
| } while (!obj && (node = node->parentNode())); |
| if (!obj) |
| return; |
| |
| while (obj && !obj->IsTextField()) |
| obj = obj->ParentObject(); |
| |
| PostNotification(obj, ax::mojom::Event::kValueChanged); |
| } |
| |
| void AXObjectCacheImpl::HandleScaleAndLocationChanged(Document* document) { |
| if (!document) |
| return; |
| PostNotification(document, ax::mojom::Event::kLocationChanged); |
| } |
| |
| void AXObjectCacheImpl::HandleTextFormControlChanged(Node* node) { |
| HandleEditableTextContentChanged(node); |
| } |
| |
| void AXObjectCacheImpl::HandleTextMarkerDataAdded(Node* start, Node* end) { |
| if (!start || !end) |
| return; |
| |
| // Notify the client of new text marker data. |
| ChildrenChanged(start); |
| if (start != end) { |
| ChildrenChanged(end); |
| } |
| } |
| |
| void AXObjectCacheImpl::HandleValueChanged(Node* node) { |
| PostNotification(node, ax::mojom::Event::kValueChanged); |
| |
| // If it's a slider, invalidate the thumb's bounding box. |
| AXObject* ax_object = Get(node); |
| if (ax_object && ax_object->RoleValue() == ax::mojom::blink::Role::kSlider && |
| !ax_object->NeedsToUpdateChildren() && |
| ax_object->ChildCountIncludingIgnored() == 1) { |
| changed_bounds_ids_.insert( |
| ax_object->ChildAtIncludingIgnored(0)->AXObjectID()); |
| } |
| } |
| |
| void AXObjectCacheImpl::HandleUpdateActiveMenuOption(Node* menu_list) { |
| if (!use_ax_menu_list_) { |
| MarkElementDirty(menu_list); |
| return; |
| } |
| |
| DeferTreeUpdate( |
| &AXObjectCacheImpl::HandleUpdateActiveMenuOptionWithCleanLayout, |
| menu_list); |
| } |
| |
| void AXObjectCacheImpl::HandleUpdateActiveMenuOptionWithCleanLayout( |
| Node* menu_list) { |
| if (AXMenuList* ax_menu_list = DynamicTo<AXMenuList>(GetOrCreate(menu_list))) |
| ax_menu_list->DidUpdateActiveOption(); |
| } |
| |
| void AXObjectCacheImpl::DidShowMenuListPopup(LayoutObject* menu_list) { |
| SCOPED_DISALLOW_LIFECYCLE_TRANSITION(menu_list->GetDocument()); |
| |
| DCHECK(menu_list->GetNode()); |
| DeferTreeUpdate(&AXObjectCacheImpl::DidShowMenuListPopupWithCleanLayout, |
| menu_list->GetNode()); |
| } |
| |
| void AXObjectCacheImpl::DidShowMenuListPopupWithCleanLayout(Node* menu_list) { |
| if (!use_ax_menu_list_) { |
| MarkAXObjectDirtyWithCleanLayout(Get(menu_list)); |
| return; |
| } |
| |
| auto* ax_object = DynamicTo<AXMenuList>(Get(menu_list)); |
| if (ax_object) |
| ax_object->DidShowPopup(); |
| } |
| |
| void AXObjectCacheImpl::DidHideMenuListPopup(LayoutObject* menu_list) { |
| SCOPED_DISALLOW_LIFECYCLE_TRANSITION(menu_list->GetDocument()); |
| |
| DCHECK(menu_list->GetNode()); |
| DeferTreeUpdate(&AXObjectCacheImpl::DidHideMenuListPopupWithCleanLayout, |
| menu_list->GetNode()); |
| } |
| |
| void AXObjectCacheImpl::DidHideMenuListPopupWithCleanLayout(Node* menu_list) { |
| if (!use_ax_menu_list_) { |
| MarkAXObjectDirtyWithCleanLayout(Get(menu_list)); |
| return; |
| } |
| |
| auto* ax_object = DynamicTo<AXMenuList>(Get(menu_list)); |
| if (ax_object) |
| ax_object->DidHidePopup(); |
| } |
| |
| void AXObjectCacheImpl::HandleLoadComplete(Document* document) { |
| SCOPED_DISALLOW_LIFECYCLE_TRANSITION(*document); |
| |
| AddPermissionStatusListener(); |
| DeferTreeUpdate(&AXObjectCacheImpl::HandleLoadCompleteWithCleanLayout, |
| document); |
| } |
| |
| void AXObjectCacheImpl::HandleLoadCompleteWithCleanLayout(Node* document_node) { |
| DCHECK(document_node); |
| DCHECK(IsA<Document>(document_node)); |
| #if DCHECK_IS_ON() |
| Document* document = To<Document>(document_node); |
| DCHECK(document->Lifecycle().GetState() >= DocumentLifecycle::kLayoutClean) |
| << "Unclean document at lifecycle " << document->Lifecycle().ToString(); |
| #endif // DCHECK_IS_ON() |
| |
| AddPermissionStatusListener(); |
| PostNotification(GetOrCreate(document_node), |
| ax::mojom::blink::Event::kLoadComplete); |
| } |
| |
| void AXObjectCacheImpl::HandleLayoutComplete(Document* document) { |
| SCOPED_DISALLOW_LIFECYCLE_TRANSITION(*document); |
| if (document->Lifecycle().GetState() >= |
| DocumentLifecycle::kAfterPerformLayout) { |
| PostNotification(GetOrCreate(document), |
| ax::mojom::blink::Event::kLayoutComplete); |
| } else { |
| DeferTreeUpdate(&AXObjectCacheImpl::EnsurePostNotification, document, |
| ax::mojom::blink::Event::kLayoutComplete); |
| } |
| } |
| |
| void AXObjectCacheImpl::HandleScrolledToAnchor(const Node* anchor_node) { |
| if (!anchor_node) |
| return; |
| |
| SCOPED_DISALLOW_LIFECYCLE_TRANSITION(anchor_node->GetDocument()); |
| |
| AXObject* obj = GetOrCreate(anchor_node->GetLayoutObject()); |
| if (!obj) |
| return; |
| if (!obj->AccessibilityIsIncludedInTree()) |
| obj = obj->ParentObjectUnignored(); |
| PostNotification(obj, ax::mojom::Event::kScrolledToAnchor); |
| } |
| |
| void AXObjectCacheImpl::HandleFrameRectsChanged(Document& document) { |
| MarkElementDirty(&document); |
| } |
| |
| void AXObjectCacheImpl::InvalidateBoundingBox( |
| const LayoutObject* layout_object) { |
| if (AXObject* obj = Get(const_cast<LayoutObject*>(layout_object))) |
| changed_bounds_ids_.insert(obj->AXObjectID()); |
| } |
| |
| void AXObjectCacheImpl::HandleScrollPositionChanged( |
| LocalFrameView* frame_view) { |
| SCOPED_DISALLOW_LIFECYCLE_TRANSITION(*frame_view->GetFrame().GetDocument()); |
| |
| InvalidateBoundingBoxForFixedOrStickyPosition(); |
| MarkElementDirty(document_); |
| DeferTreeUpdate(&AXObjectCacheImpl::EnsurePostNotification, document_, |
| ax::mojom::blink::Event::kLayoutComplete); |
| } |
| |
| void AXObjectCacheImpl::HandleScrollPositionChanged( |
| LayoutObject* layout_object) { |
| SCOPED_DISALLOW_LIFECYCLE_TRANSITION(layout_object->GetDocument()); |
| InvalidateBoundingBoxForFixedOrStickyPosition(); |
| Node* node = GetClosestNodeForLayoutObject(layout_object); |
| if (node) { |
| MarkElementDirty(node); |
| DeferTreeUpdate(&AXObjectCacheImpl::EnsurePostNotification, node, |
| ax::mojom::blink::Event::kLayoutComplete); |
| } |
| } |
| |
| const AtomicString& AXObjectCacheImpl::ComputedRoleForNode(Node* node) { |
| SCOPED_DISALLOW_LIFECYCLE_TRANSITION(node->GetDocument()); |
| |
| AXObject* obj = GetOrCreate(node); |
| if (!obj) |
| return AXObject::ARIARoleName(ax::mojom::blink::Role::kUnknown); |
| return AXObject::ARIARoleName(obj->RoleValue()); |
| } |
| |
| String AXObjectCacheImpl::ComputedNameForNode(Node* node) { |
| SCOPED_DISALLOW_LIFECYCLE_TRANSITION(node->GetDocument()); |
| AXObject* obj = GetOrCreate(node); |
| if (!obj) |
| return ""; |
| |
| return obj->ComputedName(); |
| } |
| |
| void AXObjectCacheImpl::OnTouchAccessibilityHover(const IntPoint& location) { |
| DocumentLifecycle::DisallowTransitionScope disallow(document_->Lifecycle()); |
| AXObject* hit = Root()->AccessibilityHitTest(location); |
| if (hit) { |
| // Ignore events on a frame or plug-in, because the touch events |
| // will be re-targeted there and we don't want to fire duplicate |
| // accessibility events. |
| if (hit->GetLayoutObject() && |
| hit->GetLayoutObject()->IsLayoutEmbeddedContent()) |
| return; |
| |
| PostNotification(hit, ax::mojom::Event::kHover); |
| } |
| } |
| |
| void AXObjectCacheImpl::SetCanvasObjectBounds(HTMLCanvasElement* canvas, |
| Element* element, |
| const LayoutRect& rect) { |
| SCOPED_DISALLOW_LIFECYCLE_TRANSITION(element->GetDocument()); |
| |
| AXObject* obj = GetOrCreate(element); |
| if (!obj) |
| return; |
| |
| AXObject* ax_canvas = GetOrCreate(canvas); |
| if (!ax_canvas) |
| return; |
| |
| obj->SetElementRect(rect, ax_canvas); |
| } |
| |
| void AXObjectCacheImpl::AddPermissionStatusListener() { |
| if (!document_->GetExecutionContext()) |
| return; |
| |
| // Passing an Origin to Mojo crashes if the host is empty because |
| // blink::SecurityOrigin sets unique to false, but url::Origin sets |
| // unique to true. This only happens for some obscure corner cases |
| // like on Android where the system registers unusual protocol handlers, |
| // and we don't need any special permissions in those cases. |
| // |
| // http://crbug.com/759528 and http://crbug.com/762716 |
| if (document_->Url().Protocol() != "file" && |
| document_->Url().Host().IsEmpty()) { |
| return; |
| } |
| |
| if (permission_service_.is_bound()) |
| permission_service_.reset(); |
| |
| ConnectToPermissionService( |
| document_->GetExecutionContext(), |
| permission_service_.BindNewPipeAndPassReceiver( |
| document_->GetTaskRunner(TaskType::kUserInteraction))); |
| |
| if (permission_observer_receiver_.is_bound()) |
| permission_observer_receiver_.reset(); |
| |
| mojo::PendingRemote<mojom::blink::PermissionObserver> observer; |
| permission_observer_receiver_.Bind( |
| observer.InitWithNewPipeAndPassReceiver(), |
| document_->GetTaskRunner(TaskType::kUserInteraction)); |
| permission_service_->AddPermissionObserver( |
| CreatePermissionDescriptor( |
| mojom::blink::PermissionName::ACCESSIBILITY_EVENTS), |
| accessibility_event_permission_, std::move(observer)); |
| } |
| |
| void AXObjectCacheImpl::OnPermissionStatusChange( |
| mojom::PermissionStatus status) { |
| accessibility_event_permission_ = status; |
| } |
| |
| bool AXObjectCacheImpl::CanCallAOMEventListeners() const { |
| return accessibility_event_permission_ == mojom::PermissionStatus::GRANTED; |
| } |
| |
| void AXObjectCacheImpl::RequestAOMEventListenerPermission() { |
| if (accessibility_event_permission_ != mojom::PermissionStatus::ASK) |
| return; |
| |
| if (!permission_service_.is_bound()) |
| return; |
| |
| permission_service_->RequestPermission( |
| CreatePermissionDescriptor( |
| mojom::blink::PermissionName::ACCESSIBILITY_EVENTS), |
| LocalFrame::HasTransientUserActivation(document_->GetFrame()), |
| WTF::Bind(&AXObjectCacheImpl::OnPermissionStatusChange, |
| WrapPersistent(this))); |
| } |
| |
| void AXObjectCacheImpl::Trace(Visitor* visitor) const { |
| visitor->Trace(document_); |
| visitor->Trace(accessible_node_mapping_); |
| visitor->Trace(node_object_mapping_); |
| visitor->Trace(active_aria_modal_dialog_); |
| |
| visitor->Trace(objects_); |
| visitor->Trace(notifications_to_post_main_); |
| visitor->Trace(notifications_to_post_popup_); |
| visitor->Trace(permission_service_); |
| visitor->Trace(permission_observer_receiver_); |
| visitor->Trace(tree_update_callback_queue_main_); |
| visitor->Trace(tree_update_callback_queue_popup_); |
| visitor->Trace(nodes_with_pending_children_changed_); |
| AXObjectCache::Trace(visitor); |
| } |
| |
| ax::mojom::blink::EventFrom AXObjectCacheImpl::ComputeEventFrom() { |
| if (active_event_from_ != ax::mojom::blink::EventFrom::kNone) |
| return active_event_from_; |
| |
| if (document_ && document_->View() && |
| LocalFrame::HasTransientUserActivation( |
| &(document_->View()->GetFrame()))) { |
| return ax::mojom::blink::EventFrom::kUser; |
| } |
| |
| return ax::mojom::blink::EventFrom::kPage; |
| } |
| |
| WebAXAutofillState AXObjectCacheImpl::GetAutofillState(AXID id) const { |
| auto iter = autofill_state_map_.find(id); |
| if (iter == autofill_state_map_.end()) |
| return WebAXAutofillState::kNoSuggestions; |
| return iter->value; |
| } |
| |
| void AXObjectCacheImpl::SetAutofillState(AXID id, WebAXAutofillState state) { |
| WebAXAutofillState previous_state = GetAutofillState(id); |
| if (state != previous_state) { |
| autofill_state_map_.Set(id, state); |
| MarkAXObjectDirty(ObjectFromAXID(id)); |
| } |
| } |
| |
| } // namespace blink |