| /* |
| * Copyright (C) 2012, Google Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of |
| * its contributors may be used to endorse or promote products derived |
| * from this software without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY |
| * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
| * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY |
| * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES |
| * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
| * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND |
| * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF |
| * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "third_party/blink/renderer/modules/accessibility/ax_node_object.h" |
| |
| #include <math.h> |
| |
| #include <algorithm> |
| #include <memory> |
| #include <optional> |
| #include <queue> |
| |
| #include "base/auto_reset.h" |
| #include "base/containers/contains.h" |
| #include "base/containers/fixed_flat_set.h" |
| #include "base/numerics/safe_conversions.h" |
| #include "third_party/blink/public/common/features.h" |
| #include "third_party/blink/public/common/input/web_keyboard_event.h" |
| #include "third_party/blink/public/mojom/frame/user_activation_notification_type.mojom-blink.h" |
| #include "third_party/blink/public/strings/grit/blink_strings.h" |
| #include "third_party/blink/renderer/bindings/core/v8/v8_image_bitmap_options.h" |
| #include "third_party/blink/renderer/core/aom/accessible_node.h" |
| #include "third_party/blink/renderer/core/css/css_resolution_units.h" |
| #include "third_party/blink/renderer/core/css/properties/longhands.h" |
| #include "third_party/blink/renderer/core/display_lock/display_lock_utilities.h" |
| #include "third_party/blink/renderer/core/dom/flat_tree_traversal.h" |
| #include "third_party/blink/renderer/core/dom/focus_params.h" |
| #include "third_party/blink/renderer/core/dom/layout_tree_builder_traversal.h" |
| #include "third_party/blink/renderer/core/dom/node_computed_style.h" |
| #include "third_party/blink/renderer/core/dom/node_traversal.h" |
| #include "third_party/blink/renderer/core/dom/qualified_name.h" |
| #include "third_party/blink/renderer/core/dom/shadow_root.h" |
| #include "third_party/blink/renderer/core/dom/text.h" |
| #include "third_party/blink/renderer/core/editing/editing_utilities.h" |
| #include "third_party/blink/renderer/core/editing/markers/custom_highlight_marker.h" |
| #include "third_party/blink/renderer/core/editing/markers/document_marker_controller.h" |
| #include "third_party/blink/renderer/core/editing/position.h" |
| #include "third_party/blink/renderer/core/events/event_util.h" |
| #include "third_party/blink/renderer/core/events/keyboard_event.h" |
| #include "third_party/blink/renderer/core/frame/local_dom_window.h" |
| #include "third_party/blink/renderer/core/frame/local_frame.h" |
| #include "third_party/blink/renderer/core/frame/local_frame_view.h" |
| #include "third_party/blink/renderer/core/frame/settings.h" |
| #include "third_party/blink/renderer/core/highlight/highlight.h" |
| #include "third_party/blink/renderer/core/html/canvas/html_canvas_element.h" |
| #include "third_party/blink/renderer/core/html/canvas/image_data.h" |
| #include "third_party/blink/renderer/core/html/custom/element_internals.h" |
| #include "third_party/blink/renderer/core/html/fenced_frame/html_fenced_frame_element.h" |
| #include "third_party/blink/renderer/core/html/forms/html_button_element.h" |
| #include "third_party/blink/renderer/core/html/forms/html_field_set_element.h" |
| #include "third_party/blink/renderer/core/html/forms/html_form_control_element.h" |
| #include "third_party/blink/renderer/core/html/forms/html_input_element.h" |
| #include "third_party/blink/renderer/core/html/forms/html_label_element.h" |
| #include "third_party/blink/renderer/core/html/forms/html_legend_element.h" |
| #include "third_party/blink/renderer/core/html/forms/html_opt_group_element.h" |
| #include "third_party/blink/renderer/core/html/forms/html_option_element.h" |
| #include "third_party/blink/renderer/core/html/forms/html_select_element.h" |
| #include "third_party/blink/renderer/core/html/forms/html_select_list_element.h" |
| #include "third_party/blink/renderer/core/html/forms/html_text_area_element.h" |
| #include "third_party/blink/renderer/core/html/forms/labels_node_list.h" |
| #include "third_party/blink/renderer/core/html/forms/radio_input_type.h" |
| #include "third_party/blink/renderer/core/html/forms/text_control_element.h" |
| #include "third_party/blink/renderer/core/html/html_anchor_element.h" |
| #include "third_party/blink/renderer/core/html/html_details_element.h" |
| #include "third_party/blink/renderer/core/html/html_div_element.h" |
| #include "third_party/blink/renderer/core/html/html_dlist_element.h" |
| #include "third_party/blink/renderer/core/html/html_element.h" |
| #include "third_party/blink/renderer/core/html/html_frame_element_base.h" |
| #include "third_party/blink/renderer/core/html/html_image_element.h" |
| #include "third_party/blink/renderer/core/html/html_map_element.h" |
| #include "third_party/blink/renderer/core/html/html_meter_element.h" |
| #include "third_party/blink/renderer/core/html/html_plugin_element.h" |
| #include "third_party/blink/renderer/core/html/html_slot_element.h" |
| #include "third_party/blink/renderer/core/html/html_table_caption_element.h" |
| #include "third_party/blink/renderer/core/html/html_table_cell_element.h" |
| #include "third_party/blink/renderer/core/html/html_table_col_element.h" |
| #include "third_party/blink/renderer/core/html/html_table_element.h" |
| #include "third_party/blink/renderer/core/html/html_table_row_element.h" |
| #include "third_party/blink/renderer/core/html/html_table_section_element.h" |
| #include "third_party/blink/renderer/core/html/media/html_media_element.h" |
| #include "third_party/blink/renderer/core/html/media/html_video_element.h" |
| #include "third_party/blink/renderer/core/html/parser/html_parser_idioms.h" |
| #include "third_party/blink/renderer/core/html/shadow/shadow_element_names.h" |
| #include "third_party/blink/renderer/core/html_names.h" |
| #include "third_party/blink/renderer/core/imagebitmap/image_bitmap.h" |
| #include "third_party/blink/renderer/core/input_type_names.h" |
| #include "third_party/blink/renderer/core/layout/inline/abstract_inline_text_box.h" |
| #include "third_party/blink/renderer/core/layout/inline/inline_cursor.h" |
| #include "third_party/blink/renderer/core/layout/inline/inline_node.h" |
| #include "third_party/blink/renderer/core/layout/inline/offset_mapping.h" |
| #include "third_party/blink/renderer/core/layout/layout_block_flow.h" |
| #include "third_party/blink/renderer/core/layout/layout_box_model_object.h" |
| #include "third_party/blink/renderer/core/layout/layout_html_canvas.h" |
| #include "third_party/blink/renderer/core/layout/layout_inline.h" |
| #include "third_party/blink/renderer/core/layout/layout_object.h" |
| #include "third_party/blink/renderer/core/layout/layout_view.h" |
| #include "third_party/blink/renderer/core/loader/progress_tracker.h" |
| #include "third_party/blink/renderer/core/mathml/mathml_element.h" |
| #include "third_party/blink/renderer/core/mathml_names.h" |
| #include "third_party/blink/renderer/core/navigation_api/navigation_api.h" |
| #include "third_party/blink/renderer/core/page/focus_controller.h" |
| #include "third_party/blink/renderer/core/page/page.h" |
| #include "third_party/blink/renderer/core/style/computed_style_constants.h" |
| #include "third_party/blink/renderer/core/svg/svg_desc_element.h" |
| #include "third_party/blink/renderer/core/svg/svg_element.h" |
| #include "third_party/blink/renderer/core/svg/svg_symbol_element.h" |
| #include "third_party/blink/renderer/core/svg/svg_text_element.h" |
| #include "third_party/blink/renderer/core/svg/svg_title_element.h" |
| #include "third_party/blink/renderer/core/xlink_names.h" |
| #include "third_party/blink/renderer/modules/accessibility/ax_image_map_link.h" |
| #include "third_party/blink/renderer/modules/accessibility/ax_inline_text_box.h" |
| #include "third_party/blink/renderer/modules/accessibility/ax_layout_object.h" |
| #include "third_party/blink/renderer/modules/accessibility/ax_menu_list_option.h" |
| #include "third_party/blink/renderer/modules/accessibility/ax_menu_list_popup.h" |
| #include "third_party/blink/renderer/modules/accessibility/ax_object_cache_impl.h" |
| #include "third_party/blink/renderer/modules/accessibility/ax_position.h" |
| #include "third_party/blink/renderer/modules/accessibility/ax_range.h" |
| #include "third_party/blink/renderer/modules/accessibility/ax_relation_cache.h" |
| #include "third_party/blink/renderer/platform/graphics/image_data_buffer.h" |
| #include "third_party/blink/renderer/platform/keyboard_codes.h" |
| #include "third_party/blink/renderer/platform/runtime_enabled_features.h" |
| #include "third_party/blink/renderer/platform/text/platform_locale.h" |
| #include "third_party/blink/renderer/platform/text/text_direction.h" |
| #include "third_party/blink/renderer/platform/weborigin/kurl.h" |
| #include "third_party/blink/renderer/platform/wtf/text/string_builder.h" |
| #include "third_party/skia/include/core/SkImage.h" |
| #include "ui/accessibility/ax_common.h" |
| #include "ui/accessibility/ax_role_properties.h" |
| #include "ui/events/keycodes/dom/dom_code.h" |
| #include "ui/events/keycodes/dom/keycode_converter.h" |
| #include "ui/gfx/geometry/transform.h" |
| |
| namespace { |
| |
| // It is not easily possible to find out if an element is the target of an |
| // in-page link. |
| // As a workaround, we consider the following to be potential targets: |
| // - <a name> |
| // - <foo id> -- an element with an id that is not SVG, a <label> or <optgroup>. |
| // <label> does not make much sense as an in-page link target. |
| // Exposing <optgroup> is redundant, as the group is already exposed via a |
| // child in its shadow DOM, which contains the accessible name. |
| // #document -- this is always a potential link target via <a name="#">. |
| // This is a compromise that does not include too many elements, and |
| // has minimal impact on tests. |
| bool IsPotentialInPageLinkTarget(blink::Node& node) { |
| auto* element = blink::DynamicTo<blink::Element>(&node); |
| if (!element) { |
| // The document itself is a potential link target, e.g. via <a name="#">. |
| return blink::IsA<blink::Document>(node); |
| } |
| |
| // We exclude elements that are in the shadow DOM. They cannot be linked by a |
| // document fragment from the main page:as they have their own id namespace. |
| if (element->ContainingShadowRoot()) |
| return false; |
| |
| // SVG elements are unlikely link targets, and we want to avoid creating |
| // a lot of noise in the AX tree or breaking tests unnecessarily. |
| if (element->IsSVGElement()) |
| return false; |
| |
| // <a name> |
| if (auto* anchor = blink::DynamicTo<blink::HTMLAnchorElement>(element)) { |
| if (anchor->HasName()) |
| return true; |
| } |
| |
| // <foo id> not in an <optgroup> or <label>. |
| if (element->HasID() && !blink::IsA<blink::HTMLLabelElement>(element) && |
| !blink::IsA<blink::HTMLOptGroupElement>(element)) { |
| return true; |
| } |
| |
| return false; |
| } |
| |
| bool IsNeutralWithinTable(blink::AXObject* obj) { |
| if (!obj) |
| return false; |
| ax::mojom::blink::Role role = obj->RoleValue(); |
| return role == ax::mojom::blink::Role::kGroup || |
| role == ax::mojom::blink::Role::kGenericContainer || |
| role == ax::mojom::blink::Role::kRowGroup; |
| } |
| |
| // Within a table, provide the accessible, semantic parent of |node|, |
| // by traversing the DOM tree, ignoring elements that are neutral in a table. |
| // Return the AXObject for the ancestor. |
| blink::AXObject* GetDOMTableAXAncestor(blink::Node* node, |
| blink::AXObjectCacheImpl& cache) { |
| // Used by code to determine roles of elements inside of an HTML table, |
| // Use DOM to get parent since parent_ is not initialized yet when role is |
| // being computed, and because HTML table structure should not take into |
| // account aria-owns. |
| if (!node) |
| return nullptr; |
| |
| while (true) { |
| node = blink::LayoutTreeBuilderTraversal::Parent(*node); |
| if (!node) |
| return nullptr; |
| |
| blink::AXObject* ax_object = cache.Get(node); |
| if (ax_object && !IsNeutralWithinTable(ax_object)) |
| return ax_object; |
| } |
| } |
| |
| enum class AXAction { |
| kActionIncrement = 0, |
| kActionDecrement, |
| }; |
| |
| blink::KeyboardEvent* CreateKeyboardEvent( |
| blink::LocalDOMWindow* local_dom_window, |
| blink::WebInputEvent::Type type, |
| AXAction action, |
| blink::AccessibilityOrientation orientation, |
| ax::mojom::blink::WritingDirection text_direction) { |
| blink::WebKeyboardEvent key(type, |
| blink::WebInputEvent::Modifiers::kNoModifiers, |
| base::TimeTicks::Now()); |
| |
| if (action == AXAction::kActionIncrement) { |
| if (orientation == blink::kAccessibilityOrientationVertical) { |
| key.dom_key = ui::DomKey::ARROW_UP; |
| key.dom_code = static_cast<int>(ui::DomCode::ARROW_UP); |
| key.native_key_code = key.windows_key_code = blink::VKEY_UP; |
| } else if (text_direction == ax::mojom::blink::WritingDirection::kRtl) { |
| key.dom_key = ui::DomKey::ARROW_LEFT; |
| key.dom_code = static_cast<int>(ui::DomCode::ARROW_LEFT); |
| key.native_key_code = key.windows_key_code = blink::VKEY_LEFT; |
| } else { // horizontal and left to right |
| key.dom_key = ui::DomKey::ARROW_RIGHT; |
| key.dom_code = static_cast<int>(ui::DomCode::ARROW_RIGHT); |
| key.native_key_code = key.windows_key_code = blink::VKEY_RIGHT; |
| } |
| } else if (action == AXAction::kActionDecrement) { |
| if (orientation == blink::kAccessibilityOrientationVertical) { |
| key.dom_key = ui::DomKey::ARROW_DOWN; |
| key.dom_code = static_cast<int>(ui::DomCode::ARROW_DOWN); |
| key.native_key_code = key.windows_key_code = blink::VKEY_DOWN; |
| } else if (text_direction == ax::mojom::blink::WritingDirection::kRtl) { |
| key.dom_key = ui::DomKey::ARROW_RIGHT; |
| key.dom_code = static_cast<int>(ui::DomCode::ARROW_RIGHT); |
| key.native_key_code = key.windows_key_code = blink::VKEY_RIGHT; |
| } else { // horizontal and left to right |
| key.dom_key = ui::DomKey::ARROW_LEFT; |
| key.dom_code = static_cast<int>(ui::DomCode::ARROW_LEFT); |
| key.native_key_code = key.windows_key_code = blink::VKEY_LEFT; |
| } |
| } |
| |
| return blink::KeyboardEvent::Create(key, local_dom_window, true); |
| } |
| |
| unsigned TextStyleFlag(ax::mojom::blink::TextStyle text_style_enum) { |
| return static_cast<unsigned>(1 << static_cast<int>(text_style_enum)); |
| } |
| |
| ax::mojom::blink::TextDecorationStyle |
| TextDecorationStyleToAXTextDecorationStyle( |
| const blink::ETextDecorationStyle text_decoration_style) { |
| switch (text_decoration_style) { |
| case blink::ETextDecorationStyle::kDashed: |
| return ax::mojom::blink::TextDecorationStyle::kDashed; |
| case blink::ETextDecorationStyle::kSolid: |
| return ax::mojom::blink::TextDecorationStyle::kSolid; |
| case blink::ETextDecorationStyle::kDotted: |
| return ax::mojom::blink::TextDecorationStyle::kDotted; |
| case blink::ETextDecorationStyle::kDouble: |
| return ax::mojom::blink::TextDecorationStyle::kDouble; |
| case blink::ETextDecorationStyle::kWavy: |
| return ax::mojom::blink::TextDecorationStyle::kWavy; |
| } |
| |
| NOTREACHED(); |
| return ax::mojom::blink::TextDecorationStyle::kNone; |
| } |
| |
| String GetTitle(blink::Element* element) { |
| if (!element) |
| return String(); |
| |
| if (blink::SVGElement* svg_element = |
| blink::DynamicTo<blink::SVGElement>(element)) { |
| // Don't use title() in SVG, as it calls innerText() which updates layout. |
| // Unfortunately, this must duplicate some logic from SVGElement::title(). |
| if (svg_element->InUseShadowTree()) { |
| String title = GetTitle(svg_element->OwnerShadowHost()); |
| if (!title.empty()) |
| return title; |
| } |
| // If we aren't an instance in a <use> or the <use> title was not found, |
| // then find the first <title> child of this element. If a title child was |
| // found, return the text contents. |
| if (auto* title_element = |
| blink::Traversal<blink::SVGTitleElement>::FirstChild(*element)) { |
| return title_element->GetInnerTextWithoutUpdate(); |
| } |
| return String(); |
| } |
| |
| return element->title(); |
| } |
| |
| bool CanHaveInlineTextBoxChildren(const blink::AXObject* obj) { |
| if (!ui::CanHaveInlineTextBoxChildren(obj->RoleValue())) { |
| return false; |
| } |
| |
| // Requires a layout object for there to be any inline text boxes. |
| if (!obj->GetLayoutObject()) { |
| return false; |
| } |
| |
| // Inline text boxes are included if and only if the parent is unignored. |
| // If the parent is ignored but included in tree, the inline textbox is |
| // still withheld. |
| return !obj->LastKnownIsIgnoredValue(); |
| } |
| |
| bool HasLayoutText(const blink::AXObject* obj) { |
| // This method should only be used when layout is clean. |
| #if DCHECK_IS_ON() |
| DCHECK(obj->GetDocument()->Lifecycle().GetState() >= |
| blink::DocumentLifecycle::kLayoutClean) |
| << "Unclean document at lifecycle " |
| << obj->GetDocument()->Lifecycle().ToString(); |
| #endif |
| |
| // If no layout object, could be display:none or display locked. |
| if (!obj->GetLayoutObject()) { |
| return false; |
| } |
| |
| if (blink::DisplayLockUtilities::LockedAncestorPreventingPaint( |
| *obj->GetLayoutObject())) { |
| return false; |
| } |
| |
| // Only text has inline textbox children. |
| if (!obj->GetLayoutObject()->IsText()) { |
| return false; |
| } |
| |
| // TODO(accessibility): Unclear why text would need layout if it's not display |
| // locked and the document is currently in a clean layout state. |
| // It seems to be fairly rare, but is creating some crashes, and there is |
| // no repro case yet. |
| if (obj->GetLayoutObject()->NeedsLayout()) { |
| DCHECK(false) << "LayoutText needed layout but was not display locked: " |
| << obj->ToString(true, true); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| } // namespace |
| |
| namespace blink { |
| |
| using html_names::kAltAttr; |
| using html_names::kTitleAttr; |
| using html_names::kTypeAttr; |
| using html_names::kValueAttr; |
| using mojom::blink::FormControlType; |
| |
| // In ARIA 1.1, default value of aria-level was changed to 2. |
| const int kDefaultHeadingLevel = 2; |
| |
| AXNodeObject::AXNodeObject(Node* node, AXObjectCacheImpl& ax_object_cache) |
| : AXObject(ax_object_cache), |
| native_role_(ax::mojom::blink::Role::kUnknown), |
| aria_role_(ax::mojom::blink::Role::kUnknown), |
| node_(node) {} |
| |
| AXNodeObject::~AXNodeObject() { |
| DCHECK(!node_); |
| } |
| |
| void AXNodeObject::AlterSliderOrSpinButtonValue(bool increase) { |
| if (!GetNode()) |
| return; |
| if (!IsSlider() && !IsSpinButton()) |
| return; |
| |
| float value; |
| if (!ValueForRange(&value)) |
| return; |
| |
| if (!RuntimeEnabledFeatures:: |
| SynthesizedKeyboardEventsForAccessibilityActionsEnabled()) { |
| // If synthesized keyboard events are disabled, we need to set the value |
| // directly here. |
| |
| // If no step was provided on the element, use a default value. |
| float step; |
| if (!StepValueForRange(&step)) { |
| if (IsNativeSlider() || IsNativeSpinButton()) { |
| step = StepRange().Step().ToString().ToFloat(); |
| } else { |
| return; |
| } |
| } |
| |
| value += increase ? step : -step; |
| |
| if (native_role_ == ax::mojom::blink::Role::kSlider || |
| native_role_ == ax::mojom::blink::Role::kSpinButton) { |
| OnNativeSetValueAction(String::Number(value)); |
| // Dispatching an event could result in changes to the document, like |
| // this AXObject becoming detached. |
| if (IsDetached()) |
| return; |
| |
| AXObjectCache().HandleValueChanged(GetNode()); |
| return; |
| } |
| } |
| |
| // If we have synthesized keyboard events enabled, we generate a keydown |
| // event: |
| // * For a native slider, the dispatch of the event will reach |
| // RangeInputType::HandleKeydownEvent(), where the value will be set and the |
| // AXObjectCache notified. The corresponding keydown/up JS events will be |
| // fired so the website doesn't know it was produced by an AT action. |
| // * For an ARIA slider, the corresponding keydown/up JS events will be |
| // fired. It is expected that the handlers for those events manage the |
| // update of the slider value. |
| |
| AXAction action = |
| increase ? AXAction::kActionIncrement : AXAction::kActionDecrement; |
| LocalDOMWindow* local_dom_window = GetDocument()->domWindow(); |
| AccessibilityOrientation orientation = Orientation(); |
| ax::mojom::blink::WritingDirection text_direction = GetTextDirection(); |
| |
| // A kKeyDown event is kRawKeyDown + kChar events. We cannot synthesize it |
| // because the KeyboardEvent constructor will prevent it, to force us to |
| // decide if we must produce both events. In our case, we don't have to |
| // produce a kChar event because we are synthesizing arrow key presses, and |
| // only keys that output characters are expected to produce kChar events. |
| KeyboardEvent* keydown = |
| CreateKeyboardEvent(local_dom_window, WebInputEvent::Type::kRawKeyDown, |
| action, orientation, text_direction); |
| GetNode()->DispatchEvent(*keydown); |
| |
| // The keydown handler may have caused the node to be removed. |
| if (!GetNode()) |
| return; |
| |
| KeyboardEvent* keyup = |
| CreateKeyboardEvent(local_dom_window, WebInputEvent::Type::kKeyUp, action, |
| orientation, text_direction); |
| |
| // Add a 100ms delay between keydown and keyup to make events look less |
| // evidently synthesized. |
| GetDocument() |
| ->GetTaskRunner(TaskType::kUserInteraction) |
| ->PostDelayedTask( |
| FROM_HERE, |
| WTF::BindOnce( |
| [](Node* node, KeyboardEvent* evt) { |
| if (node) { |
| node->DispatchEvent(*evt); |
| } |
| }, |
| WrapWeakPersistent(GetNode()), WrapPersistent(keyup)), |
| base::Milliseconds(100)); |
| } |
| |
| AXObject* AXNodeObject::ActiveDescendant() { |
| Element* element = GetElement(); |
| if (!element) |
| return nullptr; |
| |
| Element* descendant = |
| GetAOMPropertyOrARIAAttribute(AOMRelationProperty::kActiveDescendant); |
| if (!descendant) |
| return nullptr; |
| |
| AXObject* ax_descendant = AXObjectCache().Get(descendant); |
| return ax_descendant && ax_descendant->IsVisible() ? ax_descendant : nullptr; |
| } |
| |
| bool IsExemptFromInlineBlockCheck(ax::mojom::blink::Role role) { |
| return role == ax::mojom::blink::Role::kSvgRoot || |
| role == ax::mojom::blink::Role::kCanvas || |
| role == ax::mojom::blink::Role::kEmbeddedObject; |
| } |
| |
| AXObjectInclusion AXNodeObject::ShouldIncludeBasedOnSemantics( |
| IgnoredReasons* ignored_reasons) const { |
| DCHECK(GetDocument()); |
| |
| // All nodes must have an unignored parent within their tree under |
| // the root node of the web area, so force that node to always be unignored. |
| if (IsA<Document>(GetNode())) { |
| return kIncludeObject; |
| } |
| |
| if (IsPresentational()) { |
| if (ignored_reasons) |
| ignored_reasons->push_back(IgnoredReason(kAXPresentational)); |
| return kIgnoreObject; |
| } |
| |
| Node* node = GetNode(); |
| if (!node) { |
| // Nodeless pseudo element images are included, even if they don't have CSS |
| // alt text. This can allow auto alt to be applied to them. |
| if (IsImage()) |
| return kIncludeObject; |
| |
| return kDefaultBehavior; |
| } |
| |
| // Avoid double speech. The ruby text describes pronunciation of the ruby |
| // base, and generally produces redundant screen reader output. Expose it only |
| // as a description on the <ruby> element so that screen reader users can |
| // toggle it on/off as with other descriptions/annotations. |
| if (RoleValue() == ax::mojom::blink::Role::kRubyAnnotation || |
| (RoleValue() == ax::mojom::blink::Role::kStaticText && ParentObject() && |
| ParentObject()->RoleValue() == |
| ax::mojom::blink::Role::kRubyAnnotation)) { |
| return kIgnoreObject; |
| } |
| |
| Element* element = GetElement(); |
| if (!element) { |
| return kDefaultBehavior; |
| } |
| |
| if (IsExcludedByFormControlsFilter()) { |
| if (ignored_reasons) { |
| ignored_reasons->push_back(IgnoredReason(kAXUninteresting)); |
| } |
| return kIgnoreObject; |
| } |
| |
| if (IsA<SVGElement>(node)) { |
| // The symbol element is used to define graphical templates which can be |
| // instantiated by a use element but which are not rendered directly. We |
| // don't want to include these template objects, or their subtrees, where |
| // they appear in the DOM. Any associated semantic information (e.g. the |
| // title child of a symbol) may participate in the text alternative |
| // computation where it is instantiated by the use element. |
| // https://svgwg.org/svg2-draft/struct.html#SymbolElement |
| if (Traversal<SVGSymbolElement>::FirstAncestorOrSelf(*node)) |
| return kIgnoreObject; |
| |
| // The SVG-AAM states that user agents MUST provide an accessible object |
| // for rendered SVG elements that have at least one direct child title or |
| // desc element that is not empty after trimming whitespace. But it also |
| // says, "User agents MAY include elements with these child elements without |
| // checking for valid text content." So just check for their existence in |
| // order to be performant. https://w3c.github.io/svg-aam/#include_elements |
| if (ElementTraversal::FirstChild( |
| *To<ContainerNode>(node), [](auto& element) { |
| return element.HasTagName(svg_names::kTitleTag) || |
| element.HasTagName(svg_names::kDescTag); |
| })) { |
| return kIncludeObject; |
| } |
| |
| // If setting enabled, do not ignore SVG grouping (<g>) elements. |
| if (IsA<SVGGElement>(node)) { |
| Settings* settings = GetDocument()->GetSettings(); |
| if (settings->GetAccessibilityIncludeSvgGElement()) { |
| return kIncludeObject; |
| } |
| } |
| |
| // If we return kDefaultBehavior here, the logic related to inclusion of |
| // clickable objects, links, controls, etc. will not be reached. We handle |
| // SVG elements early to ensure properties in a <symbol> subtree do not |
| // result in inclusion. |
| } |
| |
| if (IsTableLikeRole() || IsTableRowLikeRole() || IsTableCellLikeRole()) |
| return kIncludeObject; |
| |
| if (IsA<HTMLHtmlElement>(node)) { |
| if (ignored_reasons) { |
| ignored_reasons->push_back(IgnoredReason(kAXUninteresting)); |
| } |
| return kIgnoreObject; |
| } |
| |
| // All focusable elements except the <body> and <html> are included. |
| if (!IsA<HTMLBodyElement>(node) && CanSetFocusAttribute()) |
| return kIncludeObject; |
| |
| if (IsLink()) |
| return kIncludeObject; |
| |
| // A click handler might be placed on an otherwise ignored non-empty block |
| // element, e.g. a div. We shouldn't ignore such elements because if an AT |
| // sees the |ax::mojom::blink::DefaultActionVerb::kClickAncestor|, it will |
| // look for the clickable ancestor and it expects to find one. |
| if (IsClickable()) |
| return kIncludeObject; |
| |
| if (IsHeading()) |
| return kIncludeObject; |
| |
| // Header and footer tags may also be exposed as landmark roles but not |
| // always. |
| if (node->HasTagName(html_names::kHeaderTag) || |
| node->HasTagName(html_names::kFooterTag)) |
| return kIncludeObject; |
| |
| // All controls are accessible. |
| if (IsControl()) |
| return kIncludeObject; |
| |
| // Anything with an explicit ARIA role should be included. |
| if (AriaRoleAttribute() != ax::mojom::blink::Role::kUnknown) |
| return kIncludeObject; |
| |
| // Anything with CSS alt should be included. |
| // Descendants are pruned: IsRelevantPseudoElementDescendant() returns false. |
| // Note: this is duplicated from AXLayoutObject because CSS alt text may apply |
| // to both Elements and pseudo-elements. |
| std::optional<String> alt_text = GetCSSAltText(GetElement()); |
| if (alt_text && !alt_text->empty()) |
| return kIncludeObject; |
| |
| // Don't ignored legends, because JAWS uses them to determine redundant text. |
| if (IsA<HTMLLegendElement>(node)) |
| return kIncludeObject; |
| |
| // Anything that is an editable root should not be ignored. However, one |
| // cannot just call `AXObject::IsEditable()` since that will include the |
| // contents of an editable region too. Only the editable root should always be |
| // exposed. |
| if (IsEditableRoot()) |
| return kIncludeObject; |
| |
| static constexpr auto always_included_computed_roles = |
| base::MakeFixedFlatSet<ax::mojom::blink::Role>({ |
| ax::mojom::blink::Role::kAbbr, |
| ax::mojom::blink::Role::kApplication, |
| ax::mojom::blink::Role::kArticle, |
| ax::mojom::blink::Role::kAudio, |
| ax::mojom::blink::Role::kBanner, |
| ax::mojom::blink::Role::kBlockquote, |
| ax::mojom::blink::Role::kCode, |
| ax::mojom::blink::Role::kComplementary, |
| ax::mojom::blink::Role::kContentDeletion, |
| ax::mojom::blink::Role::kContentInfo, |
| ax::mojom::blink::Role::kContentInsertion, |
| ax::mojom::blink::Role::kDefinition, |
| ax::mojom::blink::Role::kDescriptionList, |
| ax::mojom::blink::Role::kDetails, |
| ax::mojom::blink::Role::kDialog, |
| ax::mojom::blink::Role::kDocAcknowledgments, |
| ax::mojom::blink::Role::kDocAfterword, |
| ax::mojom::blink::Role::kDocAppendix, |
| ax::mojom::blink::Role::kDocBibliography, |
| ax::mojom::blink::Role::kDocChapter, |
| ax::mojom::blink::Role::kDocConclusion, |
| ax::mojom::blink::Role::kDocCredits, |
| ax::mojom::blink::Role::kDocEndnotes, |
| ax::mojom::blink::Role::kDocEpilogue, |
| ax::mojom::blink::Role::kDocErrata, |
| ax::mojom::blink::Role::kDocForeword, |
| ax::mojom::blink::Role::kDocGlossary, |
| ax::mojom::blink::Role::kDocIntroduction, |
| ax::mojom::blink::Role::kDocPart, |
| ax::mojom::blink::Role::kDocPreface, |
| ax::mojom::blink::Role::kDocPrologue, |
| ax::mojom::blink::Role::kDocToc, |
| ax::mojom::blink::Role::kEmphasis, |
| ax::mojom::blink::Role::kFigcaption, |
| ax::mojom::blink::Role::kFigure, |
| ax::mojom::blink::Role::kFooter, |
| ax::mojom::blink::Role::kForm, |
| ax::mojom::blink::Role::kHeader, |
| ax::mojom::blink::Role::kList, |
| ax::mojom::blink::Role::kListItem, |
| ax::mojom::blink::Role::kMain, |
| ax::mojom::blink::Role::kMark, |
| ax::mojom::blink::Role::kMath, |
| ax::mojom::blink::Role::kMathMLMath, |
| // Don't ignore MathML nodes by default, since MathML |
| // relies on child positions to determine semantics |
| // (e.g. numerator is the first child of a fraction). |
| ax::mojom::blink::Role::kMathMLFraction, |
| ax::mojom::blink::Role::kMathMLIdentifier, |
| ax::mojom::blink::Role::kMathMLMultiscripts, |
| ax::mojom::blink::Role::kMathMLNoneScript, |
| ax::mojom::blink::Role::kMathMLNumber, |
| ax::mojom::blink::Role::kMathMLOperator, |
| ax::mojom::blink::Role::kMathMLOver, |
| ax::mojom::blink::Role::kMathMLPrescriptDelimiter, |
| ax::mojom::blink::Role::kMathMLRoot, |
| ax::mojom::blink::Role::kMathMLRow, |
| ax::mojom::blink::Role::kMathMLSquareRoot, |
| ax::mojom::blink::Role::kMathMLStringLiteral, |
| ax::mojom::blink::Role::kMathMLSub, |
| ax::mojom::blink::Role::kMathMLSubSup, |
| ax::mojom::blink::Role::kMathMLSup, |
| ax::mojom::blink::Role::kMathMLTable, |
| ax::mojom::blink::Role::kMathMLTableCell, |
| ax::mojom::blink::Role::kMathMLTableRow, |
| ax::mojom::blink::Role::kMathMLText, |
| ax::mojom::blink::Role::kMathMLUnder, |
| ax::mojom::blink::Role::kMathMLUnderOver, |
| ax::mojom::blink::Role::kMeter, |
| ax::mojom::blink::Role::kNavigation, |
| ax::mojom::blink::Role::kPluginObject, |
| ax::mojom::blink::Role::kProgressIndicator, |
| ax::mojom::blink::Role::kRegion, |
| ax::mojom::blink::Role::kRuby, |
| ax::mojom::blink::Role::kSearch, |
| ax::mojom::blink::Role::kSection, |
| ax::mojom::blink::Role::kSplitter, |
| ax::mojom::blink::Role::kSubscript, |
| ax::mojom::blink::Role::kSuperscript, |
| ax::mojom::blink::Role::kStrong, |
| ax::mojom::blink::Role::kTerm, |
| ax::mojom::blink::Role::kTime, |
| ax::mojom::blink::Role::kVideo, |
| }); |
| |
| if (base::Contains(always_included_computed_roles, RoleValue())) { |
| return kIncludeObject; |
| } |
| |
| // An <hgroup> element has the "group" aria role. |
| if (GetNode()->HasTagName(html_names::kHgroupTag)) { |
| return kIncludeObject; |
| } |
| |
| // Using the title or accessibility description (so we |
| // check if there's some kind of accessible name for the element) |
| // to decide an element's visibility is not as definitive as |
| // previous checks, so this should remain as one of the last. |
| if (HasAriaAttribute() || !GetAttribute(kTitleAttr).empty()) |
| return kIncludeObject; |
| |
| if (IsImage() && !IsA<SVGElement>(node)) { |
| String alt = GetAttribute(kAltAttr); |
| // A null alt attribute means the attribute is not present. We assume this |
| // is a mistake, and expose the image so that it can be repaired. |
| // In contrast, alt="" is treated as intentional markup to ignore the image. |
| if (!alt.empty() || alt.IsNull()) |
| return kIncludeObject; |
| if (ignored_reasons) |
| ignored_reasons->push_back(IgnoredReason(kAXEmptyAlt)); |
| return kIgnoreObject; |
| } |
| |
| // Process potential in-page link targets |
| if (IsPotentialInPageLinkTarget(*element)) |
| return kIncludeObject; |
| |
| if (AXObjectCache().GetAXMode().has_mode(ui::AXMode::kInlineTextBoxes)) { |
| // We are including inline block elements since we might rely on these for |
| // NextOnLine/PreviousOnLine computations. |
| // |
| // If we have an element with inline |
| // block specified, we should include. There are some roles where we |
| // shouldn't include even if inline block, or we'll get test failures. |
| // |
| // We also only want to include in the tree if the inline block element has |
| // siblings. |
| // Otherwise we will include nodes that we don't need for anything. |
| // Consider a structure where we have a subtree of 12 layers, where each |
| // layer has an inline-block node with a single child that points to the |
| // next layer. All nodes have a single child, meaning that this child has no |
| // siblings. |
| if (!IsExemptFromInlineBlockCheck(native_role_) && GetLayoutObject() && |
| GetLayoutObject()->IsInline() && |
| GetLayoutObject()->IsAtomicInlineLevel() && |
| node->parentNode()->childElementCount() > 1) { |
| return kIncludeObject; |
| } |
| } |
| |
| // <span> tags are inline tags and not meant to convey information if they |
| // have no other ARIA information on them. If we don't ignore them, they may |
| // emit signals expected to come from their parent. |
| if (IsA<HTMLSpanElement>(node)) { |
| if (ignored_reasons) |
| ignored_reasons->push_back(IgnoredReason(kAXUninteresting)); |
| return kIgnoreObject; |
| } |
| |
| // Ignore labels that are already used to name a control. |
| // See IsRedundantLabel() for more commentary. |
| if (HTMLLabelElement* label = DynamicTo<HTMLLabelElement>(node)) { |
| if (IsRedundantLabel(label)) { |
| if (ignored_reasons) { |
| ignored_reasons->push_back( |
| IgnoredReason(kAXLabelFor, AXObjectCache().Get(label->control()))); |
| } |
| return kIgnoreObject; |
| } |
| return kIncludeObject; |
| } |
| |
| // The SVG-AAM says the foreignObject element is normally presentational. |
| if (IsA<SVGForeignObjectElement>(node)) { |
| if (ignored_reasons) { |
| ignored_reasons->push_back(IgnoredReason(kAXPresentational)); |
| } |
| return kIgnoreObject; |
| } |
| |
| return kDefaultBehavior; |
| } |
| |
| bool AXNodeObject::ComputeAccessibilityIsIgnored( |
| IgnoredReasons* ignored_reasons) const { |
| Node* node = GetNode(); |
| |
| if (ShouldIgnoreForHiddenOrInert(ignored_reasons)) { |
| // Fallback elements inside of a <canvas> are invisible, but are not ignored |
| // if they are semantic and not aria-hidden or hidden via style. |
| if (IsAriaHidden() || IsHiddenViaStyle() || IsHiddenByChildTree() || |
| !node || !node->parentElement() || |
| !node->parentElement()->IsInCanvasSubtree()) { |
| return true; |
| } |
| } |
| |
| // Handle content that is either visible or in a canvas subtree. |
| |
| AXObjectInclusion include = ShouldIncludeBasedOnSemantics(ignored_reasons); |
| if (include == kIncludeObject) { |
| return false; |
| } |
| if (include == kIgnoreObject) { |
| return true; |
| } |
| |
| if (!GetLayoutObject()) { |
| // Text without a layout object that has reached this point is not |
| // explicitly hidden, e.g. is in a <canvas> fallback or is display locked. |
| if (IsA<Text>(node)) { |
| return false; |
| } |
| if (ignored_reasons) { |
| ignored_reasons->push_back(IgnoredReason(kAXUninteresting)); |
| } |
| return true; |
| } |
| |
| // Inner editor element of editable area with empty text provides bounds |
| // used to compute the character extent for index 0. This is the same as |
| // what the caret's bounds would be if the editable area is focused. |
| if (node) { |
| const TextControlElement* text_control = EnclosingTextControl(node); |
| if (text_control) { |
| // Keep only the inner editor element and it's children. |
| // If inline textboxes are being loaded, then the inline textbox for the |
| // text wil be included by AXNodeObject::AddInlineTextboxChildren(). |
| // By only keeping the inner editor and its text, it makes finding the |
| // inner editor simpler on the browser side. |
| // See BrowserAccessibility::GetTextFieldInnerEditorElement(). |
| // TODO(accessibility) In the future, we may want to keep all descendants |
| // of the inner text element -- right now we only include one internally |
| // used container, it's text, and possibly the text's inlinext text box. |
| return text_control->InnerEditorElement() != node && |
| text_control->InnerEditorElement() != NodeTraversal::Parent(*node); |
| } |
| } |
| |
| // A LayoutEmbeddedContent is an iframe element or embedded object element or |
| // something like that. We don't want to ignore those. |
| if (GetLayoutObject()->IsLayoutEmbeddedContent()) { |
| return false; |
| } |
| |
| if (node && node->IsInUserAgentShadowRoot()) { |
| if (auto* containing_media_element = |
| DynamicTo<HTMLMediaElement>(node->OwnerShadowHost())) { |
| if (!containing_media_element->ShouldShowControls()) { |
| return true; |
| } |
| } |
| } |
| |
| // Layers are used on objects that have styles where Blink is likely to |
| // attempt to optimize them in for the GPU, such as animations, z-indexing and |
| // hidden overflow. Ensure SVG layered objects are unignored. |
| // We used to do this for any element, which caused extra divs to be added |
| // to the tree fo no clear reason. For now, we are limiting to only SVG. |
| // TODO(accessibility) Consider removal of this special case. |
| if (IsA<SVGElement>(node) && GetLayoutObject()->HasLayer() && |
| node->hasChildren()) { |
| return false; |
| } |
| |
| if (IsCanvas()) { |
| if (CanvasHasFallbackContent()) { |
| return false; |
| } |
| |
| // A 1x1 canvas is too small for the user to see and thus ignored. |
| const auto* canvas = DynamicTo<LayoutHTMLCanvas>(GetLayoutObject()); |
| if (canvas && (canvas->Size().height <= 1 || canvas->Size().width <= 1)) { |
| if (ignored_reasons) { |
| ignored_reasons->push_back(IgnoredReason(kAXProbablyPresentational)); |
| } |
| return true; |
| } |
| |
| // Otherwise fall through; use presence of help text, title, or description |
| // to decide. |
| } |
| |
| if (GetLayoutObject()->IsBR()) { |
| return false; |
| } |
| |
| if (GetLayoutObject()->IsText()) { |
| if (GetLayoutObject()->IsInListMarker()) { |
| // Ignore TextAlternative of the list marker for SUMMARY because: |
| // - TextAlternatives for disclosure-* are triangle symbol characters |
| // used to visually indicate the expansion state. |
| // - It's redundant. The host DETAILS exposes the expansion state. |
| // Also ignore text descendants of any non-ignored list marker because the |
| // text descendants do not provide any extra information than the |
| // TextAlternative on the list marker. Besides, with 'speak-as', they will |
| // be inconsistent with the list marker. |
| const AXObject* list_marker_object = |
| ContainerListMarkerIncludingIgnored(); |
| if (list_marker_object && |
| (list_marker_object->GetLayoutObject()->IsListMarkerForSummary() || |
| !list_marker_object->AccessibilityIsIgnored())) { |
| if (ignored_reasons) { |
| ignored_reasons->push_back(IgnoredReason(kAXPresentational)); |
| } |
| return true; |
| } |
| } |
| |
| // Ignore text inside of an ignored <label>. |
| // To save processing, only walk up the ignored objects. |
| // This means that other interesting objects inside the <label> will |
| // cause the text to be unignored. |
| if (IsUsedForLabelOrDescription()) { |
| AXObject* ancestor = ParentObject(); |
| while (ancestor && ancestor->AccessibilityIsIgnored()) { |
| if (ancestor->RoleValue() == ax::mojom::blink::Role::kLabelText) { |
| if (ignored_reasons) { |
| ignored_reasons->push_back(IgnoredReason(kAXPresentational)); |
| } |
| return true; |
| } |
| ancestor = ancestor->ParentObject(); |
| } |
| } |
| return false; |
| } |
| |
| std::optional<String> alt_text = GetCSSAltText(GetElement()); |
| if (alt_text) { |
| return alt_text->empty(); |
| } |
| |
| if (GetLayoutObject()->IsListMarker()) { |
| // Ignore TextAlternative of the list marker for SUMMARY because: |
| // - TextAlternatives for disclosure-* are triangle symbol characters used |
| // to visually indicate the expansion state. |
| // - It's redundant. The host DETAILS exposes the expansion state. |
| if (GetLayoutObject()->IsListMarkerForSummary()) { |
| if (ignored_reasons) { |
| ignored_reasons->push_back(IgnoredReason(kAXPresentational)); |
| } |
| return true; |
| } |
| return false; |
| } |
| |
| // Positioned elements and scrollable containers are important for determining |
| // bounding boxes, so don't ignore them unless they are pseudo-content. |
| if (!GetLayoutObject()->IsPseudoElement()) { |
| if (IsScrollableContainer()) { |
| return false; |
| } |
| if (GetLayoutObject()->IsPositioned()) { |
| return false; |
| } |
| } |
| |
| // Ignore a block flow (display:block, display:inline-block), unless it |
| // directly parents inline children. |
| // This effectively trims a lot of uninteresting divs out of the tree. |
| if (auto* block_flow = DynamicTo<LayoutBlockFlow>(GetLayoutObject())) { |
| if (block_flow->ChildrenInline() && block_flow->FirstChild()) { |
| return false; |
| } |
| } |
| |
| // By default, objects should be ignored so that the AX hierarchy is not |
| // filled with unnecessary items. |
| if (ignored_reasons) { |
| ignored_reasons->push_back(IgnoredReason(kAXUninteresting)); |
| } |
| return true; |
| } |
| |
| // static |
| std::optional<String> AXNodeObject::GetCSSAltText(const Element* element) { |
| // CSS alt text rules allow text to be assigned to ::before/::after content. |
| // For example, the following CSS assigns "bullet" text to bullet.png: |
| // .something::before { |
| // content: url(bullet.png) / "bullet"; |
| // } |
| |
| if (!element) { |
| return std::nullopt; |
| } |
| const ComputedStyle* style = element->GetComputedStyle(); |
| if (!style || style->ContentBehavesAsNormal()) { |
| return std::nullopt; |
| } |
| |
| if (element->IsPseudoElement()) { |
| for (const ContentData* content_data = style->GetContentData(); |
| content_data; content_data = content_data->Next()) { |
| if (content_data->IsAltText()) |
| return To<AltTextContentData>(content_data)->GetText(); |
| } |
| return std::nullopt; |
| } |
| |
| // If the content property is used on a non-pseudo element, match the |
| // behaviour of LayoutObject::CreateObject and only honour the style if |
| // there is exactly one piece of content, which is an image. |
| const ContentData* content_data = style->GetContentData(); |
| if (content_data && content_data->IsImage() && content_data->Next() && |
| content_data->Next()->IsAltText()) { |
| return To<AltTextContentData>(content_data->Next())->GetText(); |
| } |
| |
| return std::nullopt; |
| } |
| |
| // The following lists are for deciding whether the tags aside, |
| // header and footer can be interpreted as roles complementary, banner and |
| // contentInfo or if they should be interpreted as generic. |
| // This function only handles the complementary, banner, and contentInfo roles, |
| // which belong to the landmark roles set. |
| static HashSet<ax::mojom::blink::Role>& GetLandmarkIsNotAllowedAncestorRoles( |
| ax::mojom::blink::Role landmark) { |
| // clang-format off |
| DEFINE_STATIC_LOCAL( |
| // https://html.spec.whatwg.org/multipage/dom.html#sectioning-content-2 |
| // The aside element should not assume the complementary role when nested |
| // within the following sectioning content elements. |
| HashSet<ax::mojom::blink::Role>, complementary_is_not_allowed_roles, |
| ({ |
| ax::mojom::blink::Role::kArticle, |
| ax::mojom::blink::Role::kComplementary, |
| ax::mojom::blink::Role::kNavigation, |
| ax::mojom::blink::Role::kSection |
| })); |
| // https://w3c.github.io/html-aam/#el-header-ancestorbody |
| // The header and footer elements should not assume the banner and |
| // contentInfo roles, respectively, when nested within any of the |
| // sectioning content elements or the main element. |
| DEFINE_STATIC_LOCAL( |
| HashSet<ax::mojom::blink::Role>, landmark_is_not_allowed_roles, |
| ({ |
| ax::mojom::blink::Role::kArticle, |
| ax::mojom::blink::Role::kComplementary, |
| ax::mojom::blink::Role::kMain, |
| ax::mojom::blink::Role::kNavigation, |
| ax::mojom::blink::Role::kSection |
| })); |
| // clang-format on |
| |
| if (landmark == ax::mojom::blink::Role::kComplementary) { |
| return complementary_is_not_allowed_roles; |
| } |
| return landmark_is_not_allowed_roles; |
| } |
| |
| bool AXNodeObject::IsDescendantOfLandmarkDisallowedElement() const { |
| if (!GetNode()) |
| return false; |
| |
| if (AriaRoleAttribute() == ax::mojom::blink::Role::kComplementary) { |
| return false; |
| } |
| |
| auto role_names = GetLandmarkIsNotAllowedAncestorRoles(RoleValue()); |
| |
| for (AXObject* parent = ParentObjectUnignored(); parent; |
| parent = parent->ParentObjectUnignored()) { |
| if (role_names.Contains(parent->RoleValue())) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| static bool IsNonEmptyNonHeaderCell(const Node* cell) { |
| return cell && cell->hasChildren() && cell->HasTagName(html_names::kTdTag); |
| } |
| |
| static bool IsHeaderCell(const Node* cell) { |
| return cell && cell->HasTagName(html_names::kThTag); |
| } |
| |
| static ax::mojom::blink::Role DecideRoleFromSiblings(Element* cell) { |
| // If this header is only cell in its row, it is a column header. |
| // It is also a column header if it has a header on either side of it. |
| // If instead it has a non-empty td element next to it, it is a row header. |
| |
| const Node* next_cell = LayoutTreeBuilderTraversal::NextSibling(*cell); |
| const Node* previous_cell = |
| LayoutTreeBuilderTraversal::PreviousSibling(*cell); |
| if (!next_cell && !previous_cell) |
| return ax::mojom::blink::Role::kColumnHeader; |
| if (IsHeaderCell(next_cell) && IsHeaderCell(previous_cell)) |
| return ax::mojom::blink::Role::kColumnHeader; |
| if (IsNonEmptyNonHeaderCell(next_cell) || |
| IsNonEmptyNonHeaderCell(previous_cell)) |
| return ax::mojom::blink::Role::kRowHeader; |
| |
| const auto* row = DynamicTo<HTMLTableRowElement>(cell->parentNode()); |
| if (!row) |
| return ax::mojom::blink::Role::kColumnHeader; |
| |
| // If this row's first or last cell is a non-empty td, this is a row header. |
| // Do the same check for the second and second-to-last cells because tables |
| // often have an empty cell at the intersection of the row and column headers. |
| const Element* first_cell = ElementTraversal::FirstChild(*row); |
| DCHECK(first_cell); |
| |
| const Element* last_cell = ElementTraversal::LastChild(*row); |
| DCHECK(last_cell); |
| |
| if (IsNonEmptyNonHeaderCell(first_cell) || IsNonEmptyNonHeaderCell(last_cell)) |
| return ax::mojom::blink::Role::kRowHeader; |
| |
| if (IsNonEmptyNonHeaderCell(ElementTraversal::NextSibling(*first_cell)) || |
| IsNonEmptyNonHeaderCell(ElementTraversal::PreviousSibling(*last_cell))) |
| return ax::mojom::blink::Role::kRowHeader; |
| |
| // We have no evidence that this is not a column header. |
| return ax::mojom::blink::Role::kColumnHeader; |
| } |
| |
| ax::mojom::blink::Role AXNodeObject::DetermineTableSectionRole() const { |
| if (!GetElement()) |
| return ax::mojom::blink::Role::kGenericContainer; |
| |
| AXObject* parent = GetDOMTableAXAncestor(GetNode(), AXObjectCache()); |
| if (!parent || !parent->IsTableLikeRole()) |
| return ax::mojom::blink::Role::kGenericContainer; |
| |
| if (parent->RoleValue() == ax::mojom::blink::Role::kLayoutTable) |
| return ax::mojom::blink::Role::kGenericContainer; |
| |
| return ax::mojom::blink::Role::kRowGroup; |
| } |
| |
| ax::mojom::blink::Role AXNodeObject::DetermineTableRowRole() const { |
| AXObject* parent = GetDOMTableAXAncestor(GetNode(), AXObjectCache()); |
| |
| if (!parent || !parent->IsTableLikeRole()) |
| return ax::mojom::blink::Role::kGenericContainer; |
| |
| if (parent->RoleValue() == ax::mojom::blink::Role::kLayoutTable) |
| return ax::mojom::blink::Role::kLayoutTableRow; |
| |
| return ax::mojom::blink::Role::kRow; |
| } |
| |
| ax::mojom::blink::Role AXNodeObject::DetermineTableCellRole() const { |
| AXObject* parent = GetDOMTableAXAncestor(GetNode(), AXObjectCache()); |
| if (!parent || !parent->IsTableRowLikeRole()) |
| return ax::mojom::blink::Role::kGenericContainer; |
| |
| // Ensure table container. |
| AXObject* grandparent = |
| GetDOMTableAXAncestor(parent->GetNode(), AXObjectCache()); |
| if (!grandparent || !grandparent->IsTableLikeRole()) |
| return ax::mojom::blink::Role::kGenericContainer; |
| |
| if (parent->RoleValue() == ax::mojom::blink::Role::kLayoutTableRow) |
| return ax::mojom::blink::Role::kLayoutTableCell; |
| |
| if (!GetElement() || !GetNode()->HasTagName(html_names::kThTag)) |
| return ax::mojom::blink::Role::kCell; |
| |
| const AtomicString& scope = GetAttribute(html_names::kScopeAttr); |
| if (EqualIgnoringASCIICase(scope, "row") || |
| EqualIgnoringASCIICase(scope, "rowgroup")) |
| return ax::mojom::blink::Role::kRowHeader; |
| if (EqualIgnoringASCIICase(scope, "col") || |
| EqualIgnoringASCIICase(scope, "colgroup")) |
| return ax::mojom::blink::Role::kColumnHeader; |
| |
| return DecideRoleFromSiblings(GetElement()); |
| } |
| |
| // The following is a heuristic used to determine if a |
| // <table> should be with ax::mojom::blink::Role::kTable or |
| // ax::mojom::blink::Role::kLayoutTable. |
| // Only "data" tables should be exposed as tables. |
| // Unfortunately, there is no determinsistic or precise way to differentiate a |
| // layout table vs a data table. Fortunately, CSS authoring techniques have |
| // improved a lot and mostly supplanted the practice of using tables for layout. |
| bool AXNodeObject::IsDataTable() const { |
| DCHECK(!IsDetached()); |
| |
| auto* table_element = DynamicTo<HTMLTableElement>(GetNode()); |
| if (!table_element) { |
| return false; |
| } |
| |
| if (!GetLayoutObject()) { |
| // The table is not rendered, so the author has no reason to use the table |
| // for layout. Treat as a data table by default as there is not enough |
| // information to decide otherwise. |
| // One useful result of this is that a table inside a canvas fallback is |
| // treated as a data table. |
| return true; |
| } |
| |
| // If it has an ARIA role, it's definitely a data table. |
| AtomicString role; |
| if (HasAOMPropertyOrARIAAttribute(AOMStringProperty::kRole, role)) { |
| return true; |
| } |
| |
| // When a section of the document is contentEditable, all tables should be |
| // treated as data tables, otherwise users may not be able to work with rich |
| // text editors that allow creating and editing tables. |
| if (GetNode() && blink::IsEditable(*GetNode())) { |
| return true; |
| } |
| |
| // If there is a caption element, summary, THEAD, or TFOOT section, it's most |
| // certainly a data table |
| if (!table_element->Summary().empty() || table_element->tHead() || |
| table_element->tFoot() || table_element->caption()) { |
| return true; |
| } |
| |
| // if someone used "rules" attribute than the table should appear |
| if (!table_element->Rules().empty()) { |
| return true; |
| } |
| |
| // if there's a colgroup or col element, it's probably a data table. |
| if (Traversal<HTMLTableColElement>::FirstChild(*table_element)) { |
| return true; |
| } |
| |
| // If there are at least 20 rows, we'll call it a data table. |
| HTMLTableRowsCollection* rows = table_element->rows(); |
| int num_rows = rows->length(); |
| if (num_rows >= AXObjectCacheImpl::kDataTableHeuristicMinRows) { |
| return true; |
| } |
| if (num_rows <= 0) { |
| return false; |
| } |
| |
| int num_cols_in_first_body = rows->Item(0)->cells()->length(); |
| // If there's only one cell, it's not a good AXTable candidate. |
| if (num_rows == 1 && num_cols_in_first_body == 1) { |
| return false; |
| } |
| |
| // Store the background color of the table to check against cell's background |
| // colors. |
| const ComputedStyle* table_style = GetLayoutObject()->Style(); |
| if (!table_style) { |
| return false; |
| } |
| |
| Color table_bg_color = |
| table_style->VisitedDependentColor(GetCSSPropertyBackgroundColor()); |
| bool has_cell_spacing = table_style->HorizontalBorderSpacing() && |
| table_style->VerticalBorderSpacing(); |
| |
| // check enough of the cells to find if the table matches our criteria |
| // Criteria: |
| // 1) must have at least one valid cell (and) |
| // 2) at least half of cells have borders (or) |
| // 3) at least half of cells have different bg colors than the table, and |
| // there is cell spacing |
| unsigned valid_cell_count = 0; |
| unsigned bordered_cell_count = 0; |
| unsigned background_difference_cell_count = 0; |
| unsigned cells_with_top_border = 0; |
| unsigned cells_with_bottom_border = 0; |
| unsigned cells_with_left_border = 0; |
| unsigned cells_with_right_border = 0; |
| |
| Color alternating_row_colors[5]; |
| int alternating_row_color_count = 0; |
| for (int row = 0; row < num_rows; ++row) { |
| HTMLTableRowElement* row_element = rows->Item(row); |
| int n_cols = row_element->cells()->length(); |
| for (int col = 0; col < n_cols; ++col) { |
| const Element* cell = row_element->cells()->item(col); |
| if (!cell) { |
| continue; |
| } |
| // Any <th> tag -> treat as data table. |
| if (cell->HasTagName(html_names::kThTag)) { |
| return true; |
| } |
| |
| // Check for an explicitly assigned a "data" table attribute. |
| auto* cell_elem = DynamicTo<HTMLTableCellElement>(*cell); |
| if (cell_elem) { |
| if (!cell_elem->Headers().empty() || !cell_elem->Abbr().empty() || |
| !cell_elem->Axis().empty() || |
| !cell_elem->FastGetAttribute(html_names::kScopeAttr).empty()) { |
| return true; |
| } |
| } |
| |
| LayoutObject* cell_layout_object = cell->GetLayoutObject(); |
| if (!cell_layout_object || !cell_layout_object->IsLayoutBlock()) { |
| continue; |
| } |
| |
| const LayoutBlock* cell_layout_block = |
| To<LayoutBlock>(cell_layout_object); |
| if (cell_layout_block->Size().width < 1 || |
| cell_layout_block->Size().height < 1) { |
| continue; |
| } |
| |
| valid_cell_count++; |
| |
| const ComputedStyle* computed_style = cell_layout_block->Style(); |
| if (!computed_style) { |
| continue; |
| } |
| |
| // If the empty-cells style is set, we'll call it a data table. |
| if (computed_style->EmptyCells() == EEmptyCells::kHide) { |
| return true; |
| } |
| |
| // If a cell has matching bordered sides, call it a (fully) bordered cell. |
| if ((cell_layout_block->BorderTop() > 0 && |
| cell_layout_block->BorderBottom() > 0) || |
| (cell_layout_block->BorderLeft() > 0 && |
| cell_layout_block->BorderRight() > 0)) { |
| bordered_cell_count++; |
| } |
| |
| // Also keep track of each individual border, so we can catch tables where |
| // most cells have a bottom border, for example. |
| if (cell_layout_block->BorderTop() > 0) { |
| cells_with_top_border++; |
| } |
| if (cell_layout_block->BorderBottom() > 0) { |
| cells_with_bottom_border++; |
| } |
| if (cell_layout_block->BorderLeft() > 0) { |
| cells_with_left_border++; |
| } |
| if (cell_layout_block->BorderRight() > 0) { |
| cells_with_right_border++; |
| } |
| |
| // If the cell has a different color from the table and there is cell |
| // spacing, then it is probably a data table cell (spacing and colors take |
| // the place of borders). |
| Color cell_color = computed_style->VisitedDependentColor( |
| GetCSSPropertyBackgroundColor()); |
| if (has_cell_spacing && table_bg_color != cell_color && |
| !cell_color.IsFullyTransparent()) { |
| background_difference_cell_count++; |
| } |
| |
| // If we've found 10 "good" cells, we don't need to keep searching. |
| if (bordered_cell_count >= 10 || background_difference_cell_count >= 10) { |
| return true; |
| } |
| |
| // For the first 5 rows, cache the background color so we can check if |
| // this table has zebra-striped rows. |
| if (row < 5 && row == alternating_row_color_count) { |
| LayoutObject* layout_row = cell_layout_block->Parent(); |
| if (!layout_row || !layout_row->IsBoxModelObject() || |
| !layout_row->IsTableRow()) { |
| continue; |
| } |
| const ComputedStyle* row_computed_style = layout_row->Style(); |
| if (!row_computed_style) { |
| continue; |
| } |
| Color row_color = row_computed_style->VisitedDependentColor( |
| GetCSSPropertyBackgroundColor()); |
| alternating_row_colors[alternating_row_color_count] = row_color; |
| alternating_row_color_count++; |
| } |
| } |
| } |
| |
| // if there is less than two valid cells, it's not a data table |
| if (valid_cell_count <= 1) { |
| return false; |
| } |
| |
| // half of the cells had borders, it's a data table |
| unsigned needed_cell_count = valid_cell_count / 2; |
| if (bordered_cell_count >= needed_cell_count || |
| cells_with_top_border >= needed_cell_count || |
| cells_with_bottom_border >= needed_cell_count || |
| cells_with_left_border >= needed_cell_count || |
| cells_with_right_border >= needed_cell_count) { |
| return true; |
| } |
| |
| // half had different background colors, it's a data table |
| if (background_difference_cell_count >= needed_cell_count) { |
| return true; |
| } |
| |
| // Check if there is an alternating row background color indicating a zebra |
| // striped style pattern. |
| if (alternating_row_color_count > 2) { |
| Color first_color = alternating_row_colors[0]; |
| for (int k = 1; k < alternating_row_color_count; k++) { |
| // If an odd row was the same color as the first row, its not alternating. |
| if (k % 2 == 1 && alternating_row_colors[k] == first_color) { |
| return false; |
| } |
| // If an even row is not the same as the first row, its not alternating. |
| if (!(k % 2) && alternating_row_colors[k] != first_color) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| return false; |
| } |
| |
| ax::mojom::blink::Role AXNodeObject::RoleFromLayoutObjectOrNode() const { |
| return ax::mojom::blink::Role::kGenericContainer; |
| } |
| |
| // Does not check ARIA role, but does check some ARIA properties, specifically |
| // @aria-haspopup/aria-pressed via ButtonType(). |
| // TODO(accessibility) Ensure that if the native role needs to change, that the |
| // object is destroyed and a new one is created. Examples are changes to |
| // IsClickable(), DataList(), aria-pressed, the parent's tag, @role. |
| ax::mojom::blink::Role AXNodeObject::NativeRoleIgnoringAria() const { |
| if (!GetNode()) { |
| // Can be null in the case of pseudo content. |
| return RoleFromLayoutObjectOrNode(); |
| } |
| |
| if (GetNode()->IsPseudoElement() && GetCSSAltText(GetElement())) { |
| const ComputedStyle* style = GetElement()->GetComputedStyle(); |
| ContentData* content_data = style->GetContentData(); |
| // We just check the first item of the content list to determine the |
| // appropriate role, should only ever be image or text. |
| // TODO(accessibility) Is it possible to use CSS alt text on an HTML tag |
| // with strong semantics? If so, why are we overriding the role here? |
| // We only need to ensure the accessible name gets the CSS alt text. |
| // Note: by doing this, we are often hiding child pseudo element content |
| // because IsRelevantPseudoElementDescendant() returns false when an |
| // ancestor has CSS alt text. |
| if (content_data->IsImage()) |
| return ax::mojom::blink::Role::kImage; |
| |
| return ax::mojom::blink::Role::kStaticText; |
| } |
| |
| if (GetNode()->IsTextNode()) |
| return ax::mojom::blink::Role::kStaticText; |
| |
| if (auto* button = DynamicTo<HTMLButtonElement>(GetNode())) { |
| if (button->OwnerSelectList()) { |
| return ax::mojom::blink::Role::kComboBoxMenuButton; |
| } |
| } |
| |
| if (IsA<HTMLListboxElement>(GetNode())) { |
| return ax::mojom::blink::Role::kListBox; |
| } |
| |
| if (IsA<HTMLImageElement>(GetNode())) |
| return ax::mojom::blink::Role::kImage; |
| |
| // <a> or <svg:a>. |
| if (IsA<HTMLAnchorElement>(GetNode()) || IsA<SVGAElement>(GetNode())) { |
| // Assume that an anchor element is a Role::kLink if it has an href or a |
| // click event listener. |
| if (GetNode()->IsLink() || IsClickable()) |
| return ax::mojom::blink::Role::kLink; |
| |
| // According to the SVG-AAM, a non-link 'a' element should be exposed like |
| // a 'g' if it does not descend from a 'text' element and like a 'tspan' |
| // if it does. This is consistent with the SVG spec which states that an |
| // 'a' within 'text' acts as an inline element, whereas it otherwise acts |
| // as a container element. |
| if (IsA<SVGAElement>(GetNode()) && |
| !Traversal<SVGTextElement>::FirstAncestor(*GetNode())) { |
| return ax::mojom::blink::Role::kGroup; |
| } |
| |
| return ax::mojom::blink::Role::kGenericContainer; |
| } |
| |
| if (IsA<HTMLButtonElement>(*GetNode())) |
| return ButtonRoleType(); |
| |
| if (IsA<HTMLDetailsElement>(*GetNode())) |
| return ax::mojom::blink::Role::kDetails; |
| |
| if (IsA<HTMLSummaryElement>(*GetNode())) { |
| ContainerNode* parent = LayoutTreeBuilderTraversal::Parent(*GetNode()); |
| if (ToHTMLSlotElementIfSupportsAssignmentOrNull(parent)) |
| parent = LayoutTreeBuilderTraversal::Parent(*parent); |
| if (HTMLDetailsElement* parent_details = |
| DynamicTo<HTMLDetailsElement>(parent)) { |
| if (parent_details->GetName().empty()) { |
| return ax::mojom::blink::Role::kDisclosureTriangle; |
| } else { |
| return ax::mojom::blink::Role::kDisclosureTriangleGrouped; |
| } |
| } |
| return ax::mojom::blink::Role::kGenericContainer; |
| } |
| |
| // Chrome exposes both table markup and table CSS as a table, letting |
| // the screen reader determine what to do for CSS tables. |
| if (IsA<HTMLTableElement>(*GetNode())) { |
| if (IsDataTable()) |
| return ax::mojom::blink::Role::kTable; |
| else |
| return ax::mojom::blink::Role::kLayoutTable; |
| } |
| if (IsA<HTMLTableRowElement>(*GetNode())) |
| return DetermineTableRowRole(); |
| if (IsA<HTMLTableCellElement>(*GetNode())) |
| return DetermineTableCellRole(); |
| if (IsA<HTMLTableSectionElement>(*GetNode())) |
| return DetermineTableSectionRole(); |
| |
| if (const auto* input = DynamicTo<HTMLInputElement>(*GetNode())) { |
| FormControlType type = input->FormControlType(); |
| if (input->DataList() && type != FormControlType::kInputColor) { |
| return ax::mojom::blink::Role::kTextFieldWithComboBox; |
| } |
| switch (type) { |
| case FormControlType::kInputButton: |
| case FormControlType::kInputReset: |
| case FormControlType::kInputSubmit: |
| return ButtonRoleType(); |
| case FormControlType::kInputCheckbox: |
| return ax::mojom::blink::Role::kCheckBox; |
| case FormControlType::kInputDate: |
| return ax::mojom::blink::Role::kDate; |
| case FormControlType::kInputDatetimeLocal: |
| case FormControlType::kInputMonth: |
| case FormControlType::kInputWeek: |
| return ax::mojom::blink::Role::kDateTime; |
| case FormControlType::kInputFile: |
| return ax::mojom::blink::Role::kButton; |
| case FormControlType::kInputRadio: |
| return ax::mojom::blink::Role::kRadioButton; |
| case FormControlType::kInputNumber: |
| return ax::mojom::blink::Role::kSpinButton; |
| case FormControlType::kInputRange: |
| return ax::mojom::blink::Role::kSlider; |
| case FormControlType::kInputSearch: |
| return ax::mojom::blink::Role::kSearchBox; |
| case FormControlType::kInputColor: |
| return ax::mojom::blink::Role::kColorWell; |
| case FormControlType::kInputTime: |
| return ax::mojom::blink::Role::kInputTime; |
| case FormControlType::kInputImage: |
| return ax::mojom::blink::Role::kButton; |
| default: |
| return ax::mojom::blink::Role::kTextField; |
| } |
| } |
| |
| if (auto* select_element = DynamicTo<HTMLSelectElement>(*GetNode())) { |
| if (select_element->IsMultiple()) |
| return ax::mojom::blink::Role::kListBox; |
| else |
| return ax::mojom::blink::Role::kComboBoxSelect; |
| } |
| |
| if (auto* option = DynamicTo<HTMLOptionElement>(*GetNode())) { |
| HTMLSelectElement* select_element = option->OwnerSelectElement(); |
| if (!select_element || select_element->IsMultiple() || |
| option->OwnerSelectList()) { |
| return ax::mojom::blink::Role::kListBoxOption; |
| } else { |
| return ax::mojom::blink::Role::kMenuListOption; |
| } |
| } |
| |
| if (IsA<HTMLTextAreaElement>(*GetNode())) |
| return ax::mojom::blink::Role::kTextField; |
| |
| if (HeadingLevel()) |
| return ax::mojom::blink::Role::kHeading; |
| |
| if (IsA<HTMLDivElement>(*GetNode())) |
| return RoleFromLayoutObjectOrNode(); |
| |
| if (IsA<HTMLMenuElement>(*GetNode()) || IsA<HTMLUListElement>(*GetNode()) || |
| IsA<HTMLOListElement>(*GetNode())) { |
| // <menu> is a deprecated feature of HTML 5, but is included for semantic |
| // compatibility with HTML3, and may contain list items. Exposing it as an |
| // unordered list works better than the current HTML-AAM recommendaton of |
| // exposing as a role=menu, because if it's just used semantically, it won't |
| // be interactive. If used as a widget, the author must provide role=menu. |
| return ax::mojom::blink::Role::kList; |
| } |
| |
| if (IsA<HTMLMeterElement>(*GetNode())) |
| return ax::mojom::blink::Role::kMeter; |
| |
| if (IsA<HTMLProgressElement>(*GetNode())) |
| return ax::mojom::blink::Role::kProgressIndicator; |
| |
| if (IsA<HTMLOutputElement>(*GetNode())) |
| return ax::mojom::blink::Role::kStatus; |
| |
| if (IsA<HTMLParagraphElement>(*GetNode())) |
| return ax::mojom::blink::Role::kParagraph; |
| |
| if (IsA<HTMLLabelElement>(*GetNode())) |
| return ax::mojom::blink::Role::kLabelText; |
| |
| if (IsA<HTMLLegendElement>(*GetNode())) |
| return ax::mojom::blink::Role::kLegend; |
| |
| if (GetNode()->HasTagName(html_names::kRubyTag)) { |
| return ax::mojom::blink::Role::kRuby; |
| } |
| |
| if (IsA<HTMLDListElement>(*GetNode())) |
| return ax::mojom::blink::Role::kDescriptionList; |
| |
| if (IsA<HTMLAudioElement>(*GetNode())) |
| return ax::mojom::blink::Role::kAudio; |
| if (IsA<HTMLVideoElement>(*GetNode())) |
| return ax::mojom::blink::Role::kVideo; |
| |
| if (GetNode()->HasTagName(html_names::kDdTag)) |
| return ax::mojom::blink::Role::kDefinition; |
| |
| if (GetNode()->HasTagName(html_names::kDfnTag)) |
| return ax::mojom::blink::Role::kTerm; |
| |
| if (GetNode()->HasTagName(html_names::kDtTag)) |
| return ax::mojom::blink::Role::kTerm; |
| |
| // Mapping of MathML elements. See https://w3c.github.io/mathml-aam/ |
| if (auto* element = DynamicTo<MathMLElement>(GetNode())) { |
| if (element->HasTagName(mathml_names::kMathTag)) { |
| return ax::mojom::blink::Role::kMathMLMath; |
| } |
| if (element->HasTagName(mathml_names::kMfracTag)) |
| return ax::mojom::blink::Role::kMathMLFraction; |
| if (element->HasTagName(mathml_names::kMiTag)) |
| return ax::mojom::blink::Role::kMathMLIdentifier; |
| if (element->HasTagName(mathml_names::kMmultiscriptsTag)) |
| return ax::mojom::blink::Role::kMathMLMultiscripts; |
| if (element->HasTagName(mathml_names::kMnTag)) |
| return ax::mojom::blink::Role::kMathMLNumber; |
| if (element->HasTagName(mathml_names::kMoTag)) |
| return ax::mojom::blink::Role::kMathMLOperator; |
| if (element->HasTagName(mathml_names::kMoverTag)) |
| return ax::mojom::blink::Role::kMathMLOver; |
| if (element->HasTagName(mathml_names::kMunderTag)) |
| return ax::mojom::blink::Role::kMathMLUnder; |
| if (element->HasTagName(mathml_names::kMunderoverTag)) |
| return ax::mojom::blink::Role::kMathMLUnderOver; |
| if (element->HasTagName(mathml_names::kMrootTag)) |
| return ax::mojom::blink::Role::kMathMLRoot; |
| if (element->HasTagName(mathml_names::kMrowTag) || |
| element->HasTagName(mathml_names::kAnnotationXmlTag) || |
| element->HasTagName(mathml_names::kMactionTag) || |
| element->HasTagName(mathml_names::kMerrorTag) || |
| element->HasTagName(mathml_names::kMpaddedTag) || |
| element->HasTagName(mathml_names::kMphantomTag) || |
| element->HasTagName(mathml_names::kMstyleTag) || |
| element->HasTagName(mathml_names::kSemanticsTag)) { |
| return ax::mojom::blink::Role::kMathMLRow; |
| } |
| if (element->HasTagName(mathml_names::kMprescriptsTag)) |
| return ax::mojom::blink::Role::kMathMLPrescriptDelimiter; |
| if (element->HasTagName(mathml_names::kNoneTag)) |
| return ax::mojom::blink::Role::kMathMLNoneScript; |
| if (element->HasTagName(mathml_names::kMsqrtTag)) |
| return ax::mojom::blink::Role::kMathMLSquareRoot; |
| if (element->HasTagName(mathml_names::kMsTag)) |
| return ax::mojom::blink::Role::kMathMLStringLiteral; |
| if (element->HasTagName(mathml_names::kMsubTag)) |
| return ax::mojom::blink::Role::kMathMLSub; |
| if (element->HasTagName(mathml_names::kMsubsupTag)) |
| return ax::mojom::blink::Role::kMathMLSubSup; |
| if (element->HasTagName(mathml_names::kMsupTag)) |
| return ax::mojom::blink::Role::kMathMLSup; |
| if (element->HasTagName(mathml_names::kMtableTag)) |
| return ax::mojom::blink::Role::kMathMLTable; |
| if (element->HasTagName(mathml_names::kMtdTag)) |
| return ax::mojom::blink::Role::kMathMLTableCell; |
| if (element->HasTagName(mathml_names::kMtrTag)) |
| return ax::mojom::blink::Role::kMathMLTableRow; |
| if (element->HasTagName(mathml_names::kMtextTag) || |
| element->HasTagName(mathml_names::kAnnotationTag)) { |
| return ax::mojom::blink::Role::kMathMLText; |
| } |
| } |
| |
| if (GetNode()->HasTagName(html_names::kRpTag) || |
| GetNode()->HasTagName(html_names::kRtTag)) { |
| return ax::mojom::blink::Role::kRubyAnnotation; |
| } |
| |
| if (IsA<HTMLFormElement>(*GetNode())) { |
| // Only treat <form> as role="form" when it has an accessible name, which |
| // can only occur when the name is assigned by the author via aria-label, |
| // aria-labelledby, or title. Otherwise, treat as a <section>. |
| return IsNameFromAuthorAttribute() ? ax::mojom::blink::Role::kForm |
| : ax::mojom::blink::Role::kSection; |
| } |
| |
| if (GetNode()->HasTagName(html_names::kAbbrTag)) |
| return ax::mojom::blink::Role::kAbbr; |
| |
| if (GetNode()->HasTagName(html_names::kArticleTag)) |
| return ax::mojom::blink::Role::kArticle; |
| |
| if (GetNode()->HasTagName(html_names::kCodeTag)) |
| return ax::mojom::blink::Role::kCode; |
| |
| if (GetNode()->HasTagName(html_names::kEmTag)) |
| return ax::mojom::blink::Role::kEmphasis; |
| |
| if (GetNode()->HasTagName(html_names::kStrongTag)) |
| return ax::mojom::blink::Role::kStrong; |
| |
| if (GetNode()->HasTagName(html_names::kSearchTag)) { |
| return ax::mojom::blink::Role::kSearch; |
| } |
| |
| if (GetNode()->HasTagName(html_names::kDelTag) || |
| GetNode()->HasTagName(html_names::kSTag)) { |
| return ax::mojom::blink::Role::kContentDeletion; |
| } |
| |
| if (GetNode()->HasTagName(html_names::kInsTag)) |
| return ax::mojom::blink::Role::kContentInsertion; |
| |
| if (GetNode()->HasTagName(html_names::kSubTag)) |
| return ax::mojom::blink::Role::kSubscript; |
| |
| if (GetNode()->HasTagName(html_names::kSupTag)) |
| return ax::mojom::blink::Role::kSuperscript; |
| |
| if (GetNode()->HasTagName(html_names::kMainTag)) |
| return ax::mojom::blink::Role::kMain; |
| |
| if (GetNode()->HasTagName(html_names::kMarkTag)) |
| return ax::mojom::blink::Role::kMark; |
| |
| if (GetNode()->HasTagName(html_names::kNavTag)) |
| return ax::mojom::blink::Role::kNavigation; |
| |
| if (GetNode()->HasTagName(html_names::kAsideTag)) |
| return ax::mojom::blink::Role::kComplementary; |
| |
| if (GetNode()->HasTagName(html_names::kSectionTag)) { |
| // Treat a named <section> as role="region". |
| return IsNameFromAuthorAttribute() ? ax::mojom::blink::Role::kRegion |
| : ax::mojom::blink::Role::kSection; |
| } |
| |
| if (GetNode()->HasTagName(html_names::kAddressTag)) |
| return ax::mojom::blink::Role::kGroup; |
| |
| if (GetNode()->HasTagName(html_names::kHgroupTag)) { |
| return ax::mojom::blink::Role::kGroup; |
| } |
| |
| if (IsA<HTMLDialogElement>(*GetNode())) |
| return ax::mojom::blink::Role::kDialog; |
| |
| // The HTML element. |
| if (IsA<HTMLHtmlElement>(GetNode())) |
| return RoleFromLayoutObjectOrNode(); |
| |
| // Treat <iframe>, <frame> and <fencedframe> the same. |
| if (IsFrame(GetNode())) |
| return ax::mojom::blink::Role::kIframe; |
| |
| if (GetNode()->HasTagName(html_names::kHeaderTag)) { |
| return ax::mojom::blink::Role::kHeader; |
| } |
| |
| if (GetNode()->HasTagName(html_names::kFooterTag)) { |
| return ax::mojom::blink::Role::kFooter; |
| } |
| |
| if (GetNode()->HasTagName(html_names::kBlockquoteTag)) |
| return ax::mojom::blink::Role::kBlockquote; |
| |
| if (IsA<HTMLTableCaptionElement>(GetNode())) |
| return ax::mojom::blink::Role::kCaption; |
| |
| if (GetNode()->HasTagName(html_names::kFigcaptionTag)) |
| return ax::mojom::blink::Role::kFigcaption; |
| |
| if (GetNode()->HasTagName(html_names::kFigureTag)) |
| return ax::mojom::blink::Role::kFigure; |
| |
| if (IsA<HTMLTimeElement>(GetNode())) |
| return ax::mojom::blink::Role::kTime; |
| |
| if (IsA<HTMLPlugInElement>(GetNode())) { |
| if (IsA<HTMLEmbedElement>(GetNode())) |
| return ax::mojom::blink::Role::kEmbeddedObject; |
| return ax::mojom::blink::Role::kPluginObject; |
| } |
| |
| if (IsA<HTMLHRElement>(*GetNode())) |
| return ax::mojom::blink::Role::kSplitter; |
| |
| if (IsFieldset()) |
| return ax::mojom::blink::Role::kGroup; |
| |
| return RoleFromLayoutObjectOrNode(); |
| } |
| |
| ax::mojom::blink::Role AXNodeObject::DetermineAccessibilityRole() { |
| #if DCHECK_IS_ON() |
| base::AutoReset<bool> reentrancy_protector(&is_computing_role_, true); |
| #endif |
| |
| if (IsDetached()) { |
| NOTREACHED() << "Do not compute role on detached object: " |
| << ToString(true, true); |
| return ax::mojom::blink::Role::kUnknown; |
| } |
| |
| native_role_ = NativeRoleIgnoringAria(); |
| |
| aria_role_ = DetermineAriaRoleAttribute(); |
| |
| return aria_role_ == ax::mojom::blink::Role::kUnknown ? native_role_ |
| : aria_role_; |
| } |
| |
| void AXNodeObject::AccessibilityChildrenFromAOMProperty( |
| AOMRelationListProperty property, |
| AXObject::AXObjectVector& children) const { |
| HeapVector<Member<Element>> elements; |
| if (!HasAOMPropertyOrARIAAttribute(property, elements)) |
| return; |
| AXObjectCacheImpl& cache = AXObjectCache(); |
| for (const auto& element : elements) { |
| if (AXObject* child = cache.Get(element)) { |
| // Only aria-labelledby and aria-describedby can target hidden elements. |
| if (!child) |
| continue; |
| if (child->AccessibilityIsIgnored() && |
| property != AOMRelationListProperty::kLabeledBy && |
| property != AOMRelationListProperty::kDescribedBy) { |
| continue; |
| } |
| children.push_back(child); |
| } |
| } |
| } |
| |
| static Element* SiblingWithAriaRole(String role, Node* node) { |
| Node* parent = LayoutTreeBuilderTraversal::Parent(*node); |
| if (!parent) |
| return nullptr; |
| |
| for (Node* sibling = LayoutTreeBuilderTraversal::FirstChild(*parent); sibling; |
| sibling = LayoutTreeBuilderTraversal::NextSibling(*sibling)) { |
| auto* element = DynamicTo<Element>(sibling); |
| if (!element) |
| continue; |
| const AtomicString& sibling_aria_role = |
| AccessibleNode::GetPropertyOrARIAAttribute(element, |
| AOMStringProperty::kRole); |
| if (EqualIgnoringASCIICase(sibling_aria_role, role)) |
| return element; |
| } |
| |
| return nullptr; |
| } |
| |
| Element* AXNodeObject::MenuItemElementForMenu() const { |
| if (AriaRoleAttribute() != ax::mojom::blink::Role::kMenu) |
| return nullptr; |
| |
| return SiblingWithAriaRole("menuitem", GetNode()); |
| } |
| |
| void AXNodeObject::Init(AXObject* parent) { |
| #if DCHECK_IS_ON() |
| DCHECK(!initialized_); |
| initialized_ = true; |
| #endif |
| AXObject::Init(parent); |
| |
| DCHECK(role_ == native_role_ || role_ == aria_role_) |
| << "Role must be either the cached native role or cached aria role: " |
| << "\n* Final role: " << role_ << "\n* Native role: " << native_role_ |
| << "\n* Aria role: " << aria_role_ << "\n* Node: " << GetNode(); |
| |
| DCHECK(node_ || (GetLayoutObject() && |
| AXObjectCacheImpl::IsRelevantPseudoElementDescendant( |
| *GetLayoutObject()))) |
| << "Nodeless AXNodeObject can only exist inside a pseudo element: " |
| << GetLayoutObject(); |
| } |
| |
| void AXNodeObject::Detach() { |
| #if defined(AX_FAIL_FAST_BUILD) |
| SANITIZER_CHECK(!is_adding_children_) |
| << "Cannot detach |this| during AddChildren(): " << GetNode(); |
| #endif |
| AXObject::Detach(); |
| node_ = nullptr; |
| } |
| |
| bool AXNodeObject::IsAXNodeObject() const { |
| return true; |
| } |
| |
| bool AXNodeObject::IsControl() const { |
| Node* node = GetNode(); |
| if (!node) |
| return false; |
| |
| auto* element = DynamicTo<Element>(node); |
| return ((element && element->IsFormControlElement()) || |
| AXObject::IsARIAControl(AriaRoleAttribute())); |
| } |
| |
| bool AXNodeObject::IsAutofillAvailable() const { |
| // Autofill suggestion availability is stored in AXObjectCache. |
| WebAXAutofillSuggestionAvailability suggestion_availability = |
| AXObjectCache().GetAutofillSuggestionAvailability(AXObjectID()); |
| return suggestion_availability == |
| WebAXAutofillSuggestionAvailability::kAutofillAvailable; |
| } |
| |
| bool AXNodeObject::IsDefault() const { |
| if (IsDetached()) |
| return false; |
| |
| // Checks for any kind of disabled, including aria-disabled. |
| if (Restriction() == kRestrictionDisabled || |
| RoleValue() != ax::mojom::blink::Role::kButton) { |
| return false; |
| } |
| |
| // Will only match :default pseudo class if it's the first default button in |
| // a form. |
| return GetElement()->MatchesDefaultPseudoClass(); |
| } |
| |
| bool AXNodeObject::IsFieldset() const { |
| return IsA<HTMLFieldSetElement>(GetNode()); |
| } |
| |
| bool AXNodeObject::IsHovered() const { |
| if (Node* node = GetNode()) |
| return node->IsHovered(); |
| return false; |
| } |
| |
| bool AXNodeObject::IsImageButton() const { |
| return IsNativeImage() && IsButton(); |
| } |
| |
| bool AXNodeObject::IsInputImage() const { |
| auto* html_input_element = DynamicTo<HTMLInputElement>(GetNode()); |
| if (html_input_element && RoleValue() == ax::mojom::blink::Role::kButton) { |
| return html_input_element->FormControlType() == |
| FormControlType::kInputImage; |
| } |
| |
| return false; |
| } |
| |
| bool AXNodeObject::IsLineBreakingObject() const { |
| // According to Blink Editing, objects without an associated DOM node such as |
| // pseudo-elements and list bullets, are never considered as paragraph |
| // boundaries. |
| if (IsDetached() || !GetNode()) |
| return false; |
| |
| // Presentational objects should not contribute any of their semantic meaning |
| // to the accessibility tree, including to its text representation. |
| if (IsPresentational()) |
| return false; |
| |
| // `IsEnclosingBlock` includes all elements with display block, inline block, |
| // table related, flex, grid, list item, flow-root, webkit-box, and display |
| // contents. This is the same function used by Blink > Editing for determining |
| // paragraph boundaries, i.e. line breaking objects. |
| if (IsEnclosingBlock(GetNode())) |
| return true; |
| |
| // Not all <br> elements have an associated layout object. They might be |
| // "visibility: hidden" or within a display locked region. We need to check |
| // their DOM node first. |
| if (IsA<HTMLBRElement>(GetNode())) |
| return true; |
| |
| const LayoutObject* layout_object = GetLayoutObject(); |
| if (!layout_object) |
| return AXObject::IsLineBreakingObject(); |
| |
| if (layout_object->IsBR()) |
| return true; |
| |
| // LayoutText objects could include a paragraph break in their text. This can |
| // only occur if line breaks are preserved and a newline character is present |
| // in their collapsed text. Text collapsing removes all whitespace found in |
| // the HTML file, but a special style rule could be used to preserve line |
| // breaks. |
| // |
| // The best example is the <pre> element: |
| // <pre>Line 1 |
| // Line 2</pre> |
| if (const LayoutText* layout_text = DynamicTo<LayoutText>(layout_object)) { |
| const ComputedStyle& style = layout_object->StyleRef(); |
| if (layout_text->HasNonCollapsedText() && style.ShouldPreserveBreaks() && |
| layout_text->PlainText().find('\n') != WTF::kNotFound) { |
| return true; |
| } |
| } |
| |
| // Rely on the ARIA role to figure out if this object is line breaking. |
| return AXObject::IsLineBreakingObject(); |
| } |
| |
| bool AXNodeObject::IsLoaded() const { |
| if (!GetDocument()) |
| return false; |
| |
| if (!GetDocument()->IsLoadCompleted()) |
| return false; |
| |
| // Check for a navigation API single-page app navigation in progress. |
| if (auto* window = GetDocument()->domWindow()) { |
| if (window->navigation()->HasNonDroppedOngoingNavigation()) |
| return false; |
| } |
| |
| return true; |
| } |
| |
| bool AXNodeObject::IsMultiSelectable() const { |
| switch (RoleValue()) { |
| case ax::mojom::blink::Role::kGrid: |
| case ax::mojom::blink::Role::kTreeGrid: |
| case ax::mojom::blink::Role::kTree: |
| case ax::mojom::blink::Role::kListBox: |
| case ax::mojom::blink::Role::kTabList: { |
| bool multiselectable = false; |
| if (HasAOMPropertyOrARIAAttribute(AOMBooleanProperty::kMultiselectable, |
| multiselectable)) { |
| return multiselectable; |
| } |
| break; |
| } |
| default: |
| break; |
| } |
| |
| auto* html_select_element = DynamicTo<HTMLSelectElement>(GetNode()); |
| return html_select_element && html_select_element->IsMultiple(); |
| } |
| |
| bool AXNodeObject::IsNativeImage() const { |
| Node* node = GetNode(); |
| if (!node) |
| return false; |
| |
| if (IsA<HTMLImageElement>(*node) || IsA<HTMLPlugInElement>(*node)) |
| return true; |
| |
| if (const auto* input = DynamicTo<HTMLInputElement>(*node)) |
| return input->FormControlType() == FormControlType::kInputImage; |
| |
| return false; |
| } |
| |
| bool AXNodeObject::IsOffScreen() const { |
| if (IsDetached()) |
| return false; |
| DCHECK(GetNode()); |
| // Differs fromAXLayoutObject::IsOffScreen() in that there is no bounding box. |
| // However, we know that if it is display-locked that is an indicator that it |
| // is currently offscreen, and will likely be onscreen once scrolled to. |
| return DisplayLockUtilities::IsDisplayLockedPreventingPaint(GetNode()); |
| } |
| |
| bool AXNodeObject::IsProgressIndicator() const { |
| return RoleValue() == ax::mojom::blink::Role::kProgressIndicator; |
| } |
| |
| bool AXNodeObject::IsSlider() const { |
| return RoleValue() == ax::mojom::blink::Role::kSlider; |
| } |
| |
| bool AXNodeObject::IsSpinButton() const { |
| return RoleValue() == ax::mojom::blink::Role::kSpinButton; |
| } |
| |
| bool AXNodeObject::IsNativeSlider() const { |
| if (const auto* input = DynamicTo<HTMLInputElement>(GetNode())) |
| return input->FormControlType() == FormControlType::kInputRange; |
| return false; |
| } |
| |
| bool AXNodeObject::IsNativeSpinButton() const { |
| if (const auto* input = DynamicTo<HTMLInputElement>(GetNode())) |
| return input->FormControlType() == FormControlType::kInputNumber; |
| return false; |
| } |
| |
| bool AXNodeObject::IsEmbeddingElement() const { |
| return ui::IsEmbeddingElement(native_role_); |
| } |
| |
| bool AXNodeObject::IsClickable() const { |
| // Determine whether the element is clickable either because there is a |
| // mouse button handler or because it has a native element where click |
| // performs an action. Disabled nodes are never considered clickable. |
| // Note: we can't call |node->WillRespondToMouseClickEvents()| because that |
| // triggers a style recalc and can delete this. |
| |
| // Treat mouse button listeners on the |window|, |document| as if they're on |
| // the |documentElement|. |
| if (GetNode() == GetDocument()->documentElement()) { |
| return GetNode()->HasAnyEventListeners( |
| event_util::MouseButtonEventTypes()) || |
| GetDocument()->HasAnyEventListeners( |
| event_util::MouseButtonEventTypes()) || |
| GetDocument()->domWindow()->HasAnyEventListeners( |
| event_util::MouseButtonEventTypes()); |
| } |
| |
| // Look for mouse listeners only on element nodes, e.g. skip text nodes. |
| const Element* element = GetElement(); |
| if (!element) |
| return false; |
| |
| if (IsDisabled()) |
| return false; |
| |
| if (element->HasAnyEventListeners(event_util::MouseButtonEventTypes())) |
| return true; |
| |
| if (HasContentEditableAttributeSet()) |
| return true; |
| |
| // Certain user-agent shadow DOM elements are expected to be clickable but |
| // they do not have event listeners attached or a clickable native role. We |
| // whitelist them here. |
| if (element->ShadowPseudoId() == |
| shadow_element_names::kPseudoCalendarPickerIndicator) { |
| return true; |
| } |
| |
| // Only use native roles. For ARIA elements, require a click listener. |
| return ui::IsClickable(native_role_); |
| } |
| |
| bool AXNodeObject::IsFocused() const { |
| if (!GetDocument()) |
| return false; |
| |
| // A web area is represented by the Document node in the DOM tree, which isn't |
| // focusable. Check instead if the frame's selection controller is focused. |
| if (IsWebArea() && |
| GetDocument()->GetFrame()->Selection().FrameIsFocusedAndActive()) { |
| return true; |
| } |
| |
| Element* focused_element = GetDocument()->FocusedElement(); |
| return focused_element && focused_element == GetElement(); |
| } |
| |
| AccessibilitySelectedState AXNodeObject::IsSelected() const { |
| if (!GetNode() || !GetLayoutObject() || !IsSubWidget()) |
| return kSelectedStateUndefined; |
| |
| // The aria-selected attribute overrides automatic behaviors. |
| bool is_selected; |
| if (HasAOMPropertyOrARIAAttribute(AOMBooleanProperty::kSelected, is_selected)) |
| return is_selected ? kSelectedStateTrue : kSelectedStateFalse; |
| |
| // The selection should only follow the focus when the aria-selected attribute |
| // is marked as required or implied for this element in the ARIA specs. |
| // If this object can't follow the focus, then we can't say that it's selected |
| // nor that it's not. |
| if (!ui::IsSelectRequiredOrImplicit(RoleValue())) |
| return kSelectedStateUndefined; |
| |
| // Selection follows focus, but ONLY in single selection containers, and only |
| // if aria-selected was not present to override. |
| return IsSelectedFromFocus() ? kSelectedStateTrue : kSelectedStateFalse; |
| } |
| |
| bool AXNodeObject::IsSelectedFromFocusSupported() const { |
| // The selection should only follow the focus when the aria-selected attribute |
| // is marked as required or implied for this element in the ARIA specs. |
| // If this object can't follow the focus, then we can't say that it's selected |
| // nor that it's not. |
| // TODO(crbug.com/1143483): Consider allowing more roles. |
| if (!ui::IsSelectRequiredOrImplicit(RoleValue())) |
| return false; |
| |
| // https://www.w3.org/TR/wai-aria-1.1/#aria-selected |
| // Any explicit assignment of aria-selected takes precedence over the implicit |
| // selection based on focus. |
| bool is_selected; |
| if (HasAOMPropertyOrARIAAttribute(AOMBooleanProperty::kSelected, is_selected)) |
| return false; |
| |
| // Selection follows focus only when in a single selection container. |
| const AXObject* container = ContainerWidget(); |
| if (!container || container->IsMultiSelectable()) |
| return false; |
| |
| // TODO(crbug.com/1143451): https://www.w3.org/TR/wai-aria-1.1/#aria-selected |
| // If any DOM element in the widget is explicitly marked as selected, the user |
| // agent MUST NOT convey implicit selection for the widget. |
| return true; |
| } |
| |
| // In single selection containers, selection follows focus unless aria_selected |
| // is set to false. This is only valid for a subset of elements. |
| bool AXNodeObject::IsSelectedFromFocus() const { |
| if (!IsSelectedFromFocusSupported()) |
| return false; |
| |
| // A tab item can also be selected if it is associated to a focused tabpanel |
| // via the aria-labelledby attribute. |
| if (IsTabItem() && IsTabItemSelected()) |
| return true; |
| |
| // If this object is not accessibility focused, then it is not selected from |
| // focus. |
| AXObject* focused_object = AXObjectCache().FocusedObject(); |
| if (focused_object != this && |
| (!focused_object || focused_object->ActiveDescendant() != this)) |
| return false; |
| |
| return true; |
| } |
| |
| bool AXNodeObject::IsTabItemSelected() const { |
| if (!IsTabItem() || !GetLayoutObject()) |
| return false; |
| |
| Node* node = GetNode(); |
| if (!node || !node->IsElementNode()) |
| return false; |
| |
| // The ARIA spec says a tab item can also be selected if it is aria-labeled by |
| // a tabpanel that has keyboard focus inside of it, or if a tabpanel in its |
| // aria-controls list has KB focus inside of it. |
| AXObject* focused_element = AXObjectCache().FocusedObject(); |
| if (!focused_element) |
| return false; |
| |
| HeapVector<Member<Element>> elements; |
| if (!HasAOMPropertyOrARIAAttribute(AOMRelationListProperty::kControls, |
| elements)) |
| return false; |
| |
| for (const auto& element : elements) { |
| AXObject* tab_panel = AXObjectCache().Get(element); |
| |
| // A tab item should only control tab panels. |
| if (!tab_panel || |
| tab_panel->RoleValue() != ax::mojom::blink::Role::kTabPanel) { |
| continue; |
| } |
| |
| AXObject* check_focus_element = focused_element; |
| // Check if the focused element is a descendant of the element controlled by |
| // the tab item. |
| while (check_focus_element) { |
| if (tab_panel == check_focus_element) |
| return true; |
| check_focus_element = check_focus_element->ParentObject(); |
| } |
| } |
| |
| return false; |
| } |
| |
| AXRestriction AXNodeObject::Restriction() const { |
| Element* elem = GetElement(); |
| if (!elem) |
| return kRestrictionNone; |
| |
| // An <optgroup> is not exposed directly in the AX tree. |
| if (IsA<HTMLOptGroupElement>(elem)) |
| return kRestrictionNone; |
| |
| // According to ARIA, all elements of the base markup can be disabled. |
| // According to CORE-AAM, any focusable descendant of aria-disabled |
| // ancestor is also disabled. |
| if (IsDisabled()) |
| return kRestrictionDisabled; |
| |
| // Only editable fields can be marked @readonly (unlike @aria-readonly). |
| auto* text_area_element = DynamicTo<HTMLTextAreaElement>(*elem); |
| if (text_area_element && text_area_element->IsReadOnly()) |
| return kRestrictionReadOnly; |
| if (const auto* input = DynamicTo<HTMLInputElement>(*elem)) { |
| if (input->IsTextField() && input->IsReadOnly()) |
| return kRestrictionReadOnly; |
| } |
| |
| // Check aria-readonly if supported by current role. |
| bool is_read_only; |
| if (SupportsARIAReadOnly() && |
| HasAOMPropertyOrARIAAttribute(AOMBooleanProperty::kReadOnly, |
| is_read_only)) { |
| // ARIA overrides other readonly state markup. |
| return is_read_only ? kRestrictionReadOnly : kRestrictionNone; |
| } |
| |
| // If a grid cell does not have it's own ARIA input restriction, |
| // fall back on parent grid's readonly state. |
| // See ARIA specification regarding grid/treegrid and readonly. |
| if (IsTableCellLikeRole()) { |
| AXObject* row = ParentObjectUnignored(); |
| if (row && row->IsTableRowLikeRole()) { |
| AXObject* table = row->ParentObjectUnignored(); |
| if (table && table->IsTableLikeRole() && |
| (table->RoleValue() == ax::mojom::blink::Role::kGrid || |
| table->RoleValue() == ax::mojom::blink::Role::kTreeGrid)) { |
| if (table->Restriction() == kRestrictionReadOnly) |
| return kRestrictionReadOnly; |
| } |
| } |
| } |
| |
| // This is a node that is not readonly and not disabled. |
| return kRestrictionNone; |
| } |
| |
| AccessibilityExpanded AXNodeObject::IsExpanded() const { |
| if (!SupportsARIAExpanded()) |
| return kExpandedUndefined; |
| |
| auto* element = GetElement(); |
| if (!element) |
| return kExpandedUndefined; |
| |
| if (auto* button = DynamicTo<HTMLButtonElement>(element)) { |
| if (auto* select_list = button->OwnerSelectList()) { |
| return select_list->open() ? kExpandedExpanded : kExpandedCollapsed; |
| } |
| } |
| |
| if (RoleValue() == ax::mojom::blink::Role::kComboBoxSelect && |
| IsA<HTMLSelectElement>(*element)) { |
| return To<HTMLSelectElement>(element)->PopupIsVisible() |
| ? kExpandedExpanded |
| : kExpandedCollapsed; |
| } |
| |
| // For form controls that act as triggering elements for popovers of type |
| // kAuto, then set aria-expanded=false when the popover is hidden, and |
| // aria-expanded=true when it is showing. |
| if (auto* form_control = DynamicTo<HTMLFormControlElement>(element)) { |
| if (auto popover = form_control->popoverTargetElement().popover; |
| popover && popover->PopoverType() == PopoverValueType::kAuto) { |
| return popover->popoverOpen() ? kExpandedExpanded : kExpandedCollapsed; |
| } |
| } |
| |
| if (IsA<HTMLSummaryElement>(*element)) { |
| if (element->parentNode() && |
| IsA<HTMLDetailsElement>(element->parentNode())) { |
| return To<Element>(element->parentNode()) |
| ->FastHasAttribute(html_names::kOpenAttr) |
| ? kExpandedExpanded |
| : kExpandedCollapsed; |
| } |
| } |
| |
| bool expanded = false; |
| if (HasAOMPropertyOrARIAAttribute(AOMBooleanProperty::kExpanded, expanded)) { |
| return expanded ? kExpandedExpanded : kExpandedCollapsed; |
| } |
| |
| return kExpandedUndefined; |
| } |
| |
| bool AXNodeObject::IsRequired() const { |
| auto* form_control = DynamicTo<HTMLFormControlElement>(GetNode()); |
| if (form_control && form_control->IsRequired()) |
| return true; |
| |
| if (AOMPropertyOrARIAAttributeIsTrue(AOMBooleanProperty::kRequired)) |
| return true; |
| |
| return false; |
| } |
| |
| bool AXNodeObject::CanvasHasFallbackContent() const { |
| if (IsDetached()) |
| return false; |
| Node* node = GetNode(); |
| return IsA<HTMLCanvasElement>(node) && node->hasChildren(); |
| } |
| |
| int AXNodeObject::HeadingLevel() const { |
| // headings can be in block flow and non-block flow |
| Node* node = GetNode(); |
| if (!node) |
| return 0; |
| |
| if (RoleValue() == ax::mojom::blink::Role::kHeading) { |
| uint32_t level; |
| if (HasAOMPropertyOrARIAAttribute(AOMUIntProperty::kLevel, level)) { |
| if (level >= 1 && level <= 9) |
| return level; |
| } |
| } |
| |
| auto* element = DynamicTo<HTMLElement>(node); |
| if (!element) |
| return 0; |
| |
| if (element->HasTagName(html_names::kH1Tag)) |
| return 1; |
| |
| if (element->HasTagName(html_names::kH2Tag)) |
| return 2; |
| |
| if (element->HasTagName(html_names::kH3Tag)) |
| return 3; |
| |
| if (element->HasTagName(html_names::kH4Tag)) |
| return 4; |
| |
| if (element->HasTagName(html_names::kH5Tag)) |
| return 5; |
| |
| if (element->HasTagName(html_names::kH6Tag)) |
| return 6; |
| |
| if (RoleValue() == ax::mojom::blink::Role::kHeading) |
| return kDefaultHeadingLevel; |
| |
| return 0; |
| } |
| |
| unsigned AXNodeObject::HierarchicalLevel() const { |
| Element* element = GetElement(); |
| if (!element) |
| return 0; |
| |
| uint32_t level; |
| if (HasAOMPropertyOrARIAAttribute(AOMUIntProperty::kLevel, level)) { |
| if (level >= 1) |
| return level; |
| } |
| |
| // Helper lambda for calculating hierarchical levels by counting ancestor |
| // nodes that match a target role. |
| auto accumulateLevel = [&](int initial_level, |
| ax::mojom::blink::Role target_role) { |
| int level = initial_level; |
| for (AXObject* parent = ParentObject(); parent; |
| parent = parent->ParentObject()) { |
| if (parent->RoleValue() == target_role) |
| level++; |
| } |
| return level; |
| }; |
| |
| switch (RoleValue()) { |
| case ax::mojom::blink::Role::kComment: |
| // Comment: level is based on counting comment ancestors until the root. |
| return accumulateLevel(1, ax::mojom::blink::Role::kComment); |
| case ax::mojom::blink::Role::kListItem: |
| level = accumulateLevel(0, ax::mojom::blink::Role::kList); |
| // When level count is 0 due to this list item not having an ancestor of |
| // Role::kList, not nested in list groups, this list item has a level |
| // of 1. |
| return level == 0 ? 1 : level; |
| case ax::mojom::blink::Role::kTabList: |
| return accumulateLevel(1, ax::mojom::blink::Role::kTabList); |
| case ax::mojom::blink::Role::kTreeItem: { |
| // Hierarchy leveling starts at 1, to match the aria-level spec. |
| // We measure tree hierarchy by the number of groups that the item is |
| // within. |
| level = 1; |
| for (AXObject* parent = ParentObject(); parent; |
| parent = parent->ParentObject()) { |
| ax::mojom::blink::Role parent_role = parent->RoleValue(); |
| if (parent_role == ax::mojom::blink::Role::kGroup) |
| level++; |
| else if (parent_role == ax::mojom::blink::Role::kTree) |
| break; |
| } |
| return level; |
| } |
| default: |
| return 0; |
| } |
| } |
| |
| String AXNodeObject::AutoComplete() const { |
| // Check cache for auto complete state. |
| if (AXObjectCache().GetAutofillSuggestionAvailability(AXObjectID()) == |
| WebAXAutofillSuggestionAvailability::kAutocompleteAvailable) { |
| return "list"; |
| } |
| |
| if (IsAtomicTextField() || IsARIATextField()) { |
| const AtomicString& aria_auto_complete = |
| GetAOMPropertyOrARIAAttribute(AOMStringProperty::kAutocomplete) |
| .LowerASCII(); |
| // Illegal values must be passed through, according to CORE-AAM. |
| if (!aria_auto_complete.IsNull()) |
| return aria_auto_complete == "none" ? String() : aria_auto_complete; |
| } |
| |
| if (auto* input = DynamicTo<HTMLInputElement>(GetNode())) { |
| if (input->DataList()) |
| return "list"; |
| } |
| |
| return String(); |
| } |
| |
| // TODO(nektar): Consider removing this method in favor of |
| // AXInlineTextBox::GetDocumentMarkers, or add document markers to the tree data |
| // instead of nodes objects. |
| void AXNodeObject::SerializeMarkerAttributes(ui::AXNodeData* node_data) const { |
| if (!GetNode() || !GetDocument() || !GetDocument()->View()) |
| return; |
| |
| auto* text_node = DynamicTo<Text>(GetNode()); |
| if (!text_node) |
| return; |
| |
| std::vector<int32_t> marker_types; |
| std::vector<int32_t> highlight_types; |
| std::vector<int32_t> marker_starts; |
| std::vector<int32_t> marker_ends; |
| |
| // First use ARIA markers for spelling/grammar if available. |
| std::optional<DocumentMarker::MarkerType> aria_marker_type = |
| GetAriaSpellingOrGrammarMarker(); |
| if (aria_marker_type) { |
| AXRange range = AXRange::RangeOfContents(*this); |
| marker_types.push_back(ToAXMarkerType(aria_marker_type.value())); |
| marker_starts.push_back(range.Start().TextOffset()); |
| marker_ends.push_back(range.End().TextOffset()); |
| } |
| |
| DocumentMarkerController& marker_controller = GetDocument()->Markers(); |
| const DocumentMarker::MarkerTypes markers_used_by_accessibility( |
| DocumentMarker::kSpelling | DocumentMarker::kGrammar | |
| DocumentMarker::kTextMatch | DocumentMarker::kActiveSuggestion | |
| DocumentMarker::kSuggestion | DocumentMarker::kTextFragment | |
| DocumentMarker::kCustomHighlight); |
| const DocumentMarkerVector markers = |
| marker_controller.MarkersFor(*text_node, markers_used_by_accessibility); |
| for (const DocumentMarker* marker : markers) { |
| if (aria_marker_type == marker->GetType()) |
| continue; |
| |
| const Position start_position(*GetNode(), marker->StartOffset()); |
| const Position end_position(*GetNode(), marker->EndOffset()); |
| if (!start_position.IsValidFor(*GetDocument()) || |
| !end_position.IsValidFor(*GetDocument())) { |
| continue; |
| } |
| |
| int32_t highlight_type = |
| static_cast<int32_t>(ax::mojom::blink::HighlightType::kNone); |
| if (marker->GetType() == DocumentMarker::kCustomHighlight) { |
| const auto& highlight_marker = To<CustomHighlightMarker>(*marker); |
| highlight_type = |
| ToAXHighlightType(highlight_marker.GetHighlight()->type()); |
| } |
| |
| marker_types.push_back(ToAXMarkerType(marker->GetType())); |
| highlight_types.push_back(static_cast<int32_t>(highlight_type)); |
| auto start_pos = |
| AXPosition::FromPosition(start_position, TextAffinity::kDownstream, |
| AXPositionAdjustmentBehavior::kMoveLeft); |
| auto end_pos = |
| AXPosition::FromPosition(end_position, TextAffinity::kDownstream, |
| AXPositionAdjustmentBehavior::kMoveRight); |
| marker_starts.push_back(start_pos.TextOffset()); |
| marker_ends.push_back(end_pos.TextOffset()); |
| } |
| |
| if (marker_types.empty()) |
| return; |
| |
| node_data->AddIntListAttribute( |
| ax::mojom::blink::IntListAttribute::kMarkerTypes, marker_types); |
| node_data->AddIntListAttribute( |
| ax::mojom::blink::IntListAttribute::kHighlightTypes, highlight_types); |
| node_data->AddIntListAttribute( |
| ax::mojom::blink::IntListAttribute::kMarkerStarts, marker_starts); |
| node_data->AddIntListAttribute( |
| ax::mojom::blink::IntListAttribute::kMarkerEnds, marker_ends); |
| } |
| |
| AXObject* AXNodeObject::InPageLinkTarget() const { |
| if (!IsLink() || !GetDocument()) |
| return AXObject::InPageLinkTarget(); |
| |
| const Element* anchor = AnchorElement(); |
| if (!anchor) |
| return AXObject::InPageLinkTarget(); |
| |
| KURL link_url = anchor->HrefURL(); |
| if (!link_url.IsValid()) |
| return AXObject::InPageLinkTarget(); |
| |
| KURL document_url = GetDocument()->Url(); |
| if (!document_url.IsValid() || |
| !EqualIgnoringFragmentIdentifier(document_url, link_url)) { |
| return AXObject::InPageLinkTarget(); |
| } |
| |
| String fragment = link_url.FragmentIdentifier(); |
| TreeScope& tree_scope = anchor->GetTreeScope(); |
| Node* target = tree_scope.FindAnchor(fragment); |
| AXObject* ax_target = AXObjectCache().Get(target); |
| if (!ax_target || !IsPotentialInPageLinkTarget(*ax_target->GetNode())) |
| return AXObject::InPageLinkTarget(); |
| |
| #if DCHECK_IS_ON() |
| // Link targets always have an element, unless it is the document itself, |
| // e.g. via <a href="#">. |
| DCHECK(ax_target->IsWebArea() || ax_target->GetElement()) |
| << "The link target is expected to be a document or an element: " |
| << ax_target->ToString(true, true) << "\n* URL fragment = " << fragment; |
| #endif |
| |
| // Usually won't be ignored, but could be e.g. if aria-hidden. |
| if (ax_target->AccessibilityIsIgnored()) |
| return nullptr; |
| |
| return ax_target; |
| } |
| |
| const AtomicString& AXNodeObject::EffectiveTarget() const { |
| // The "target" attribute defines the target browser context and is supported |
| // on <a>, <area>, <base>, and <form>. Valid values are: "frame_name", "self", |
| // "blank", "top", and "parent", where "frame_name" is the value of the "name" |
| // attribute on any enclosing iframe. |
| // |
| // <area> is a subclass of <a>, while <base> provides the document's base |
| // target that any <a>'s or any <area>'s target can override. |
| // `HtmlAnchorElement::GetEffectiveTarget()` will take <base> into account. |
| // |
| // <form> is out of scope, because it affects the target to which the form is |
| // submitted, and could also be overridden by a "formTarget" attribute on e.g. |
| // a form's submit button. However, screen reader users have no need to know |
| // to which target (browser context) a form would be submitted. |
| const auto* anchor = DynamicTo<HTMLAnchorElement>(GetNode()); |
| if (anchor) { |
| const AtomicString self_value("_self"); |
| const AtomicString& effective_target = anchor->GetEffectiveTarget(); |
| if (effective_target != self_value) { |
| return anchor->GetEffectiveTarget(); |
| } |
| } |
| return AXObject::EffectiveTarget(); |
| } |
| |
| AccessibilityOrientation AXNodeObject::Orientation() const { |
| const AtomicString& aria_orientation = |
| GetAOMPropertyOrARIAAttribute(AOMStringProperty::kOrientation); |
| AccessibilityOrientation orientation = kAccessibilityOrientationUndefined; |
| if (EqualIgnoringASCIICase(aria_orientation, "horizontal")) |
| orientation = kAccessibilityOrientationHorizontal; |
| else if (EqualIgnoringASCIICase(aria_orientation, "vertical")) |
| orientation = kAccessibilityOrientationVertical; |
| |
| switch (RoleValue()) { |
| case ax::mojom::blink::Role::kListBox: |
| case ax::mojom::blink::Role::kMenu: |
| case ax::mojom::blink::Role::kScrollBar: |
| case ax::mojom::blink::Role::kTree: |
| if (orientation == kAccessibilityOrientationUndefined) |
| orientation = kAccessibilityOrientationVertical; |
| |
| return orientation; |
| case ax::mojom::blink::Role::kMenuBar: |
| case ax::mojom::blink::Role::kSlider: |
| case ax::mojom::blink::Role::kSplitter: |
| case ax::mojom::blink::Role::kTabList: |
| case ax::mojom::blink::Role::kToolbar: |
| if (orientation == kAccessibilityOrientationUndefined) |
| orientation = kAccessibilityOrientationHorizontal; |
| |
| return orientation; |
| case ax::mojom::blink::Role::kComboBoxGrouping: |
| case ax::mojom::blink::Role::kComboBoxMenuButton: |
| case ax::mojom::blink::Role::kRadioGroup: |
| case ax::mojom::blink::Role::kTreeGrid: |
| return orientation; |
| default: |
| return AXObject::Orientation(); |
| } |
| } |
| |
| // According to the standard, the figcaption should only be the first or |
| // last child: https://html.spec.whatwg.org/#the-figcaption-element |
| AXObject* AXNodeObject::GetChildFigcaption() const { |
| AXObject* child = FirstChildIncludingIgnored(); |
| if (!child) |
| return nullptr; |
| if (child->RoleValue() == ax::mojom::blink::Role::kFigcaption) |
| return child; |
| |
| child = LastChildIncludingIgnored(); |
| if (child->RoleValue() == ax::mojom::blink::Role::kFigcaption) |
| return child; |
| |
| return nullptr; |
| } |
| |
| AXObject::AXObjectVector AXNodeObject::RadioButtonsInGroup() const { |
| AXObjectVector radio_buttons; |
| if (!node_ || RoleValue() != ax::mojom::blink::Role::kRadioButton) |
| return radio_buttons; |
| |
| if (auto* node_radio_button = DynamicTo<HTMLInputElement>(node_.Get())) { |
| HeapVector<Member<HTMLInputElement>> html_radio_buttons = |
| FindAllRadioButtonsWithSameName(node_radio_button); |
| for (HTMLInputElement* radio_button : html_radio_buttons) { |
| AXObject* ax_radio_button = AXObjectCache().Get(radio_button); |
| if (ax_radio_button) |
| radio_buttons.push_back(ax_radio_button); |
| } |
| return radio_buttons; |
| } |
| |
| // If the immediate parent is a radio group, return all its children that are |
| // radio buttons. |
| AXObject* parent = ParentObjectUnignored(); |
| if (parent && parent->RoleValue() == ax::mojom::blink::Role::kRadioGroup) { |
| for (AXObject* child : parent->UnignoredChildren()) { |
| DCHECK(child); |
| if (child->RoleValue() == ax::mojom::blink::Role::kRadioButton && |
| child->AccessibilityIsIncludedInTree()) { |
| radio_buttons.push_back(child); |
| } |
| } |
| } |
| |
| return radio_buttons; |
| } |
| |
| // static |
| HeapVector<Member<HTMLInputElement>> |
| AXNodeObject::FindAllRadioButtonsWithSameName(HTMLInputElement* radio_button) { |
| HeapVector<Member<HTMLInputElement>> all_radio_buttons; |
| if (!radio_button || |
| radio_button->FormControlType() != FormControlType::kInputRadio) { |
| return all_radio_buttons; |
| } |
| |
| constexpr bool kTraverseForward = true; |
| constexpr bool kTraverseBackward = false; |
| HTMLInputElement* first_radio_button = radio_button; |
| do { |
| radio_button = RadioInputType::NextRadioButtonInGroup(first_radio_button, |
| kTraverseBackward); |
| if (radio_button) |
| first_radio_button = radio_button; |
| } while (radio_button); |
| |
| HTMLInputElement* next_radio_button = first_radio_button; |
| |