diff options
Diffstat (limited to 'chromium/third_party/blink/renderer/modules/accessibility/ax_node_object.cc')
-rw-r--r-- | chromium/third_party/blink/renderer/modules/accessibility/ax_node_object.cc | 656 |
1 files changed, 387 insertions, 269 deletions
diff --git a/chromium/third_party/blink/renderer/modules/accessibility/ax_node_object.cc b/chromium/third_party/blink/renderer/modules/accessibility/ax_node_object.cc index 4455cbe96ab..04d036b75a4 100644 --- a/chromium/third_party/blink/renderer/modules/accessibility/ax_node_object.cc +++ b/chromium/third_party/blink/renderer/modules/accessibility/ax_node_object.cc @@ -30,11 +30,13 @@ #include <math.h> #include <memory> +#include <queue> #include <algorithm> #include "third_party/abseil-cpp/absl/types/optional.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" @@ -48,6 +50,7 @@ #include "third_party/blink/renderer/core/dom/qualified_name.h" #include "third_party/blink/renderer/core/dom/shadow_root.h" #include "third_party/blink/renderer/core/dom/text.h" +#include "third_party/blink/renderer/core/editing/editing_utilities.h" #include "third_party/blink/renderer/core/editing/markers/document_marker_controller.h" #include "third_party/blink/renderer/core/editing/position.h" #include "third_party/blink/renderer/core/events/event_util.h" @@ -92,7 +95,6 @@ #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_file_upload_control.h" -#include "third_party/blink/renderer/core/layout/layout_image.h" #include "third_party/blink/renderer/core/layout/layout_inline.h" #include "third_party/blink/renderer/core/layout/layout_object.h" #include "third_party/blink/renderer/core/layout/layout_table.h" @@ -105,6 +107,7 @@ #include "third_party/blink/renderer/core/page/page.h" #include "third_party/blink/renderer/core/style/computed_style_constants.h" #include "third_party/blink/renderer/core/svg/svg_element.h" +#include "third_party/blink/renderer/core/svg/svg_title_element.h" #include "third_party/blink/renderer/modules/accessibility/ax_image_map_link.h" #include "third_party/blink/renderer/modules/accessibility/ax_inline_text_box.h" #include "third_party/blink/renderer/modules/accessibility/ax_layout_object.h" @@ -120,19 +123,54 @@ #include "third_party/blink/renderer/platform/text/text_direction.h" #include "third_party/blink/renderer/platform/weborigin/kurl.h" #include "third_party/blink/renderer/platform/wtf/text/string_builder.h" +#include "ui/accessibility/ax_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" namespace { -blink::HTMLMapElement* GetMapForImage(blink::LayoutObject* layout_object) { - blink::LayoutImage* layout_image = - blink::DynamicTo<blink::LayoutImage>(layout_object); - if (!layout_image) - return nullptr; +// 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; + } - return layout_image->ImageMap(); + // <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) { @@ -165,8 +203,6 @@ blink::AXObject* GetDOMTableAXAncestor(blink::Node* node, if (ax_object && !IsNeutralWithinTable(ax_object)) return ax_object; } - - return nullptr; } enum class AXAction { @@ -224,6 +260,32 @@ TextDecorationStyleToAXTextDecorationStyle( 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.IsEmpty()) + 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(); +} + } // namespace namespace blink { @@ -346,6 +408,21 @@ AXObjectInclusion AXNodeObject::ShouldIncludeBasedOnSemantics( 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 (IsTableLikeRole() || IsTableRowLikeRole() || IsTableCellLikeRole()) return kIncludeObject; @@ -353,7 +430,7 @@ AXObjectInclusion AXNodeObject::ShouldIncludeBasedOnSemantics( if (!IsA<HTMLBodyElement>(node) && CanSetFocusAttribute()) return kIncludeObject; - if (IsLink() || IsInPageLinkTarget()) + if (IsLink()) return kIncludeObject; // A click handler might be placed on an otherwise ignored non-empty block @@ -363,7 +440,7 @@ AXObjectInclusion AXNodeObject::ShouldIncludeBasedOnSemantics( if (IsClickable()) return kIncludeObject; - if (IsHeading() || IsLandmarkRelated()) + if (IsHeading()) return kIncludeObject; // Header and footer tags may also be exposed as landmark roles but not @@ -381,6 +458,7 @@ AXObjectInclusion AXNodeObject::ShouldIncludeBasedOnSemantics( 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. absl::optional<String> alt_text = GetCSSAltText(GetNode()); @@ -401,24 +479,54 @@ AXObjectInclusion AXNodeObject::ShouldIncludeBasedOnSemantics( static const HashSet<ax::mojom::blink::Role> always_included_computed_roles = { ax::mojom::blink::Role::kAbbr, + ax::mojom::blink::Role::kApplication, + ax::mojom::blink::Role::kArticle, + ax::mojom::blink::Role::kBanner, ax::mojom::blink::Role::kBlockquote, + ax::mojom::blink::Role::kComplementary, ax::mojom::blink::Role::kContentDeletion, + ax::mojom::blink::Role::kContentInfo, ax::mojom::blink::Role::kContentInsertion, - ax::mojom::blink::Role::kDetails, ax::mojom::blink::Role::kDescriptionList, ax::mojom::blink::Role::kDescriptionListDetail, ax::mojom::blink::Role::kDescriptionListTerm, + ax::mojom::blink::Role::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::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::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::kTime, }; @@ -427,17 +535,6 @@ AXObjectInclusion AXNodeObject::ShouldIncludeBasedOnSemantics( always_included_computed_roles.end()) return kIncludeObject; - // Avoid double speech. The ruby text describes pronunciation of the ruby - // base, and generally produces redundant screen reader output. Expose it only - // as a description on the <ruby> element so that screen reader users can - // toggle it on/off as with other descriptions/annotations. - if (RoleValue() == ax::mojom::blink::Role::kRubyAnnotation || - (RoleValue() == ax::mojom::blink::Role::kStaticText && ParentObject() && - ParentObject()->RoleValue() == - ax::mojom::blink::Role::kRubyAnnotation)) { - return kIgnoreObject; - } - // 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 @@ -457,6 +554,10 @@ AXObjectInclusion AXNodeObject::ShouldIncludeBasedOnSemantics( return kIgnoreObject; } + // Process potential in-page link targets + if (IsPotentialInPageLinkTarget(*element)) + 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. @@ -482,7 +583,14 @@ AXObjectInclusion AXNodeObject::ShouldIncludeBasedOnSemantics( return kDefaultBehavior; } +// static absl::optional<String> AXNodeObject::GetCSSAltText(const Node* node) { + // 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 (!node || !node->GetComputedStyle() || node->GetComputedStyle()->ContentBehavesAsNormal()) { return absl::nullopt; @@ -535,7 +643,8 @@ bool AXNodeObject::ComputeAccessibilityIsIgnored( DCHECK(!GetLayoutObject()); if (DisplayLockUtilities::NearestLockedExclusiveAncestor(*GetNode())) { - if (DisplayLockUtilities::ShouldIgnoreNodeDueToDisplayLock( + if (IsAriaHidden() || + DisplayLockUtilities::ShouldIgnoreNodeDueToDisplayLock( *GetNode(), DisplayLockActivationReason::kAccessibility)) { if (ignored_reasons) ignored_reasons->push_back(IgnoredReason(kAXNotRendered)); @@ -718,18 +827,17 @@ ax::mojom::blink::Role AXNodeObject::NativeRoleIgnoringAria() const { return RoleFromLayoutObjectOrNode(); } - if (GetCSSAltText(GetNode())) { + if (GetNode()->IsPseudoElement() && GetCSSAltText(GetNode())) { const ComputedStyle* style = GetNode()->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) Should it still be possible to use an ARIA role here? // 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 a child image, because - // CanHaveChildren() returns false for an image. + // 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; @@ -742,23 +850,17 @@ ax::mojom::blink::Role AXNodeObject::NativeRoleIgnoringAria() const { if (IsA<HTMLImageElement>(GetNode())) return ax::mojom::blink::Role::kImage; - // <a href> or <svg:a xlink:href> - if (GetNode()->IsLink()) { - // |HTMLAnchorElement| sets isLink only when it has kHrefAttr. - return ax::mojom::blink::Role::kLink; + // <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; + return ax::mojom::blink::Role::kGenericContainer; } - if (IsA<HTMLPortalElement>(*GetNode())) { + if (IsA<HTMLPortalElement>(*GetNode())) return ax::mojom::blink::Role::kPortal; - } - - if (IsA<HTMLAnchorElement>(*GetNode())) { - // We assume that an anchor element is LinkRole if it has event listeners - // even though it doesn't have kHrefAttr. - if (IsClickable()) - return ax::mojom::blink::Role::kLink; - return ax::mojom::blink::Role::kAnchor; - } if (IsA<HTMLButtonElement>(*GetNode())) return ButtonRoleType(); @@ -1178,28 +1280,57 @@ bool AXNodeObject::IsInputImage() const { return false; } -// It is not easily possible to find out if an element is the target of an -// in-page link. -// As a workaround, we check if the element is a sectioning element with an ID, -// or an anchor with a name. -bool AXNodeObject::IsInPageLinkTarget() const { - auto* element = DynamicTo<Element>(node_.Get()); - if (!element) +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; - // We exclude elements that are in the shadow DOM. - if (element->ContainingShadowRoot()) + + // Presentational objects should not contribute any of their semantic meaning + // to the accessibility tree, including to its text representation. + if (IsPresentational()) return false; - if (auto* anchor = DynamicTo<HTMLAnchorElement>(element)) { - return anchor->HasName() || anchor->HasID(); - } + // `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; - if (element->HasID() && - (IsLandmarkRelated() || IsA<HTMLSpanElement>(element) || - IsA<HTMLDivElement>(element))) { + // 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.PreserveNewline() && + layout_text->PlainText().find('\n') != WTF::kNotFound) { + return true; + } } - return false; + + // Rely on the ARIA role to figure out if this object is line breaking. + return AXObject::IsLineBreakingObject(); } bool AXNodeObject::IsLoaded() const { @@ -1330,7 +1461,7 @@ AccessibilitySelectedState AXNodeObject::IsSelected() const { // is marked as required or implied for this element in the ARIA specs. // If this object can't follow the focus, then we can't say that it's selected // nor that it's not. - if (!SelectionShouldFollowFocus()) + if (!ui::IsSelectRequiredOrImplicit(RoleValue())) return kSelectedStateUndefined; // Selection follows focus, but ONLY in single selection containers, and only @@ -1338,21 +1469,43 @@ AccessibilitySelectedState AXNodeObject::IsSelected() const { 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 (!SelectionShouldFollowFocus()) + 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 kSelectedStateTrue; - - // If not a single selection container, selection does not follow focus. - AXObject* container = ContainerWidget(); - if (!container || container->IsMultiSelectable()) - return false; + return true; // If this object is not accessibility focused, then it is not selected from // focus. @@ -1361,25 +1514,7 @@ bool AXNodeObject::IsSelectedFromFocus() const { (!focused_object || focused_object->ActiveDescendant() != this)) return false; - // In single selection container and accessibility focused => true if - // aria-selected wasn't used as an override. - bool is_selected; - return !HasAOMPropertyOrARIAAttribute(AOMBooleanProperty::kSelected, - is_selected); -} - -// Returns true if the node's aria-selected attribute should be set to true -// when the node is focused. This is true for only a subset of roles. -bool AXNodeObject::SelectionShouldFollowFocus() const { - switch (RoleValue()) { - case ax::mojom::blink::Role::kListBoxOption: - case ax::mojom::blink::Role::kMenuListOption: - case ax::mojom::blink::Role::kTab: - return true; - default: - break; - } - return false; + return true; } bool AXNodeObject::IsTabItemSelected() const { @@ -1629,8 +1764,6 @@ unsigned AXNodeObject::HierarchicalLevel() const { default: return 0; } - - return 0; } String AXNodeObject::AutoComplete() const { @@ -1722,7 +1855,7 @@ void AXNodeObject::SerializeMarkerAttributes(ui::AXNodeData* node_data) const { } AXObject* AXNodeObject::InPageLinkTarget() const { - if (!IsAnchor() || !GetDocument()) + if (!IsLink() || !GetDocument()) return AXObject::InPageLinkTarget(); const Element* anchor = AnchorElement(); @@ -1742,11 +1875,23 @@ AXObject* AXNodeObject::InPageLinkTarget() const { String fragment = link_url.FragmentIdentifier(); TreeScope& tree_scope = anchor->GetTreeScope(); Node* target = tree_scope.FindAnchor(fragment); - if (!target) + AXObject* ax_target = AXObjectCache().GetOrCreate(target); + if (!ax_target || !IsPotentialInPageLinkTarget(*ax_target->GetNode())) return AXObject::InPageLinkTarget(); - // If the target is not in the accessibility tree, get the first unignored - // sibling. - return AXObjectCache().FirstAccessibleObjectFromNode(target); + +#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; } AccessibilityOrientation AXNodeObject::Orientation() const { @@ -1871,7 +2016,36 @@ ax::mojom::blink::WritingDirection AXNodeObject::GetTextDirection() const { return AXNodeObject::GetTextDirection(); } +ax::mojom::blink::TextPosition AXNodeObject::GetTextPositionFromAria() const { + // Check for role="subscript" or role="superscript" on the element, or if + // static text, on the containing element. + AXObject* obj = nullptr; + if (RoleValue() == ax::mojom::blink::Role::kStaticText) + obj = ParentObject(); + else if (RoleValue() == ax::mojom::blink::Role::kGenericContainer) + obj = const_cast<AXNodeObject*>(this); // May have role=sub/superscript. + + if (obj) { + const AtomicString& aria_role = + obj->GetAOMPropertyOrARIAAttribute(AOMStringProperty::kRole); + if (aria_role == "subscript") + return ax::mojom::blink::TextPosition::kSubscript; + if (aria_role == "superscript") + return ax::mojom::blink::TextPosition::kSuperscript; + } + + return ax::mojom::blink::TextPosition::kNone; +} + ax::mojom::blink::TextPosition AXNodeObject::GetTextPosition() const { + if (GetNode()) { + // role="subscript" and role="superscript" don't use an internal role, they + // just return a TextPosition here. + const auto& text_position = GetTextPositionFromAria(); + if (text_position != ax::mojom::blink::TextPosition::kNone) + return text_position; + } + if (!GetLayoutObject()) return AXObject::GetTextPosition(); @@ -2325,8 +2499,10 @@ bool AXNodeObject::MaxValueForRange(float* out_value) const { } // In ARIA 1.1, default value of scrollbar, separator and slider - // for aria-valuemax were changed to 100. + // for aria-valuemax were changed to 100. This change was made for + // progressbar in ARIA 1.2. switch (AriaRoleAttribute()) { + case ax::mojom::blink::Role::kProgressIndicator: case ax::mojom::blink::Role::kScrollBar: case ax::mojom::blink::Role::kSplitter: case ax::mojom::blink::Role::kSlider: { @@ -2358,8 +2534,10 @@ bool AXNodeObject::MinValueForRange(float* out_value) const { } // In ARIA 1.1, default value of scrollbar, separator and slider - // for aria-valuemin were changed to 0. + // for aria-valuemin were changed to 0. This change was made for + // progressbar in ARIA 1.2. switch (AriaRoleAttribute()) { + case ax::mojom::blink::Role::kProgressIndicator: case ax::mojom::blink::Role::kScrollBar: case ax::mojom::blink::Role::kSplitter: case ax::mojom::blink::Role::kSlider: { @@ -2404,18 +2582,8 @@ bool AXNodeObject::StepValueForRange(float* out_value) const { } KURL AXNodeObject::Url() const { - if (IsAnchor()) { - const Element* anchor = AnchorElement(); - - if (const auto* html_anchor = DynamicTo<HTMLAnchorElement>(anchor)) { - return html_anchor->Href(); - } - - // Some non-HTML elements, most notably SVG <a> elements, can act as - // links/anchors. - if (anchor) - return anchor->HrefURL(); - } + if (IsLink()) // <area>, <link>, <html:a> or <svg:a> + return GetElement()->HrefURL(); if (IsWebArea() && GetDocument()) return GetDocument()->Url(); @@ -2482,7 +2650,7 @@ String AXNodeObject::GetValueForControl() const { // We don't retrieve the element's value attribute on purpose. The value // attribute might be sanitized and might be different from what is actually // displayed inside the <select> element on screen. - return select_element->InnerElement().innerText(); + return select_element->InnerElement().GetInnerTextWithoutUpdate(); } if (IsAtomicTextField()) { @@ -2712,8 +2880,10 @@ bool AXNodeObject::HasContentEditableAttributeSet() const { if (!html_element) return false; - String normalized_value = html_element->contentEditable(); - return normalized_value == "true" || normalized_value == "plaintext-only"; + ContentEditableType normalized_value = + html_element->contentEditableNormalized(); + return normalized_value == ContentEditableType::kContentEditable || + normalized_value == ContentEditableType::kPlaintextOnly; } // Returns the nearest block-level LayoutBlockFlow ancestor @@ -2802,12 +2972,13 @@ String AXNodeObject::GetName(ax::mojom::blink::NameFrom& name_from, return name; } -String AXNodeObject::TextAlternative(bool recursive, - bool in_aria_labelled_by_traversal, - AXObjectSet& visited, - ax::mojom::blink::NameFrom& name_from, - AXRelatedObjectVector* related_objects, - NameSources* name_sources) const { +String AXNodeObject::TextAlternative( + bool recursive, + const AXObject* aria_label_or_description_root, + AXObjectSet& visited, + ax::mojom::blink::NameFrom& name_from, + AXRelatedObjectVector* related_objects, + NameSources* name_sources) const { // If nameSources is non-null, relatedObjects is used in filling it in, so it // must be non-null as well. if (name_sources) @@ -2848,7 +3019,7 @@ String AXNodeObject::TextAlternative(bool recursive, // Step 2C from: http://www.w3.org/TR/accname-aam-1.1 -- aria-label. String text_alternative = AriaTextAlternative( - recursive, in_aria_labelled_by_traversal, visited, name_from, + recursive, aria_label_or_description_root, visited, name_from, related_objects, name_sources, &found_text_alternative); if (found_text_alternative && !name_sources) return text_alternative; @@ -2864,7 +3035,7 @@ String AXNodeObject::TextAlternative(bool recursive, return text_alternative; // Step 2F / 2G from: http://www.w3.org/TR/accname-aam-1.1 -- from content. - if (in_aria_labelled_by_traversal || SupportsNameFromContents(recursive)) { + if (aria_label_or_description_root || SupportsNameFromContents(recursive)) { Node* node = GetNode(); if (!IsA<HTMLSelectElement>(node)) { // Avoid option descendant text name_from = ax::mojom::blink::NameFrom::kContents; @@ -2891,7 +3062,7 @@ String AXNodeObject::TextAlternative(bool recursive, } } - // Step 2H from: http://www.w3.org/TR/accname-aam-1.1 + // Step 2I from: http://www.w3.org/TR/accname-aam-1.1 name_from = ax::mojom::blink::NameFrom::kTitle; if (name_sources) { name_sources->push_back(NameSource(found_text_alternative, kTitleAttr)); @@ -2981,7 +3152,7 @@ static bool ShouldInsertSpaceBetweenObjectsIfNeeded( String AXNodeObject::TextFromDescendants(AXObjectSet& visited, bool recursive) const { if (!CanHaveChildren()) - return recursive ? String() : GetElement()->innerText(); + return recursive ? String() : GetElement()->GetInnerTextWithoutUpdate(); StringBuilder accumulated_text; AXObject* previous = nullptr; @@ -2991,10 +3162,9 @@ String AXNodeObject::TextFromDescendants(AXObjectSet& visited, AXObjectVector children; HeapVector<Member<AXObject>> owned_children; - AXObjectCache().GetAriaOwnedChildren(this, owned_children); + AXObjectCache().ValidatedAriaOwnedChildren(this, owned_children); // TODO(aleventhal) Why isn't this just using cached children? - AXNodeObject* parent = const_cast<AXNodeObject*>(this); for (Node* child = LayoutTreeBuilderTraversal::FirstChild(*node_); child; child = LayoutTreeBuilderTraversal::NextSibling(*child)) { auto* child_text_node = DynamicTo<Text>(child); @@ -3003,7 +3173,7 @@ String AXNodeObject::TextFromDescendants(AXObjectSet& visited, // skip over empty text nodes continue; } - AXObject* child_obj = AXObjectCache().GetOrCreate(child, parent); + AXObject* child_obj = AXObjectCache().GetOrCreate(child); if (child_obj && !AXObjectCache().IsAriaOwned(child_obj)) children.push_back(child_obj); } @@ -3016,7 +3186,7 @@ String AXNodeObject::TextFromDescendants(AXObjectSet& visited, break; // Need to add 1 because the root naming node is in the list. // Don't recurse into children that are explicitly hidden. - // Note that we don't call IsInertOrAriaHidden because that would return + // Note that we don't call IsInert()/IsAriaHidden because they would return // true if any ancestor is hidden, but we need to be able to compute the // accessible name of object inside hidden subtrees (for example, if // aria-labelledby points to an object that's hidden). @@ -3032,7 +3202,7 @@ String AXNodeObject::TextFromDescendants(AXObjectSet& visited, result = child->TextFromDescendants(visited, true); } else { result = - RecursiveTextAlternative(*child, false, visited, child_name_from); + RecursiveTextAlternative(*child, nullptr, visited, child_name_from); } if (!result.IsEmpty() && previous && accumulated_text.length() && @@ -3127,7 +3297,7 @@ bool AXNodeObject::IsRedundantLabel(HTMLLabelElement* label) { void AXNodeObject::GetRelativeBounds(AXObject** out_container, FloatRect& out_bounds_in_container, - SkMatrix44& out_container_transform, + skia::Matrix44& out_container_transform, bool* clips_children) const { if (GetLayoutObject()) { AXObject::GetRelativeBounds(out_container, out_bounds_in_container, @@ -3337,39 +3507,42 @@ int AXNodeObject::TextOffsetInFormattingContext(int offset) const { inline_offset_mapping->GetMappingUnitsForLayoutObject(*layout_obj); if (mapping_units.empty()) return AXObject::TextOffsetInFormattingContext(offset); - return int{mapping_units.front().TextContentStart()} + offset; + return static_cast<int>(mapping_units.front().TextContentStart()) + offset; } // // Inline text boxes. // -void AXNodeObject::LoadInlineTextBoxesRecursive() { - if (ui::CanHaveInlineTextBoxChildren(RoleValue())) { - if (children_.IsEmpty()) { - // We only need to add inline textbox children if they aren't present. - // Although some platforms (e.g. Android), load inline text boxes - // on subtrees that may later be stale, once they are stale, the old - // inline text boxes are cleared because SetNeedsToUpdateChildren() - // calls ClearChildren(). - AddInlineTextBoxChildren(/*force*/ true); - children_dirty_ = false; // Avoid adding these children twice. +void AXNodeObject::LoadInlineTextBoxes() { + std::queue<AXID> work_queue; + work_queue.push(AXObjectID()); + + while (!work_queue.empty()) { + AXObject* work_obj = AXObjectCache().ObjectFromAXID(work_queue.front()); + work_queue.pop(); + if (!work_obj || !work_obj->AccessibilityIsIncludedInTree()) + continue; + + if (ui::CanHaveInlineTextBoxChildren(work_obj->RoleValue())) { + if (work_obj->CachedChildrenIncludingIgnored().IsEmpty()) { + // We only need to add inline textbox children if they aren't present. + // Although some platforms (e.g. Android), load inline text boxes + // on subtrees that may later be stale, once they are stale, the old + // inline text boxes are cleared because SetNeedsToUpdateChildren() + // calls ClearChildren(). + work_obj->ForceAddInlineTextBoxChildren(); + } + } else { + for (const auto& child : work_obj->ChildrenIncludingIgnored()) + work_queue.push(child->AXObjectID()); } - return; } +} - for (const auto& child : ChildrenIncludingIgnored()) { - // TODO(https://crbug.com/1200244) Downgrade these to DCHECKs. - CHECK(child) << "Child has been destroyed before attempted use, parent is: " - << ToString(true, true); - CHECK(!child->IsDetached()) - << "Child has been detached before attempted use, parent is: " - << ToString(true, true); - CHECK(!IsDetached()) << "Parent was detached while attempting to load " - "child text boxes, parent is: " - << ToString(true, true); - child->LoadInlineTextBoxesRecursive(); - } +void AXNodeObject::ForceAddInlineTextBoxChildren() { + AddInlineTextBoxChildren(true /*force*/); + children_dirty_ = false; // Avoid adding these children twice. } void AXNodeObject::AddInlineTextBoxChildren(bool force) { @@ -3425,7 +3598,7 @@ void AXNodeObject::AddValidationMessageChild() { } void AXNodeObject::AddImageMapChildren() { - HTMLMapElement* map = GetMapForImage(GetLayoutObject()); + HTMLMapElement* map = GetMapForImage(GetNode()); if (!map) return; @@ -3450,16 +3623,6 @@ void AXNodeObject::AddImageMapChildren() { // Get the primary image, which is the first image using this map. HTMLImageElement* primary_image_element = map->ImageElement(); - DCHECK(primary_image_element); -#if DCHECK_IS_ON() - // Prove that this is the same as getting the first image using this map. - String usemap_selector = "img[usemap=\""; - usemap_selector = usemap_selector + usemap + "\"]"; - Element* first_image_with_this_usemap = - GetDocument()->QuerySelector(AtomicString(usemap_selector)); - DCHECK(primary_image_element) << "No match for " << usemap_selector; - DCHECK_EQ(primary_image_element, first_image_with_this_usemap); -#endif // Is this the primary image for this map? if (primary_image_element != curr_image_element) { @@ -3469,7 +3632,7 @@ void AXNodeObject::AddImageMapChildren() { AXObjectCache().GetOrCreate(primary_image_element); if (ax_primary_image && ax_primary_image->ChildCountIncludingIgnored() == 0 && - Traversal<HTMLAreaElement>::FirstWithin(*map)) { + NodeTraversal::FirstChild(*map)) { // The primary image still needs to add the area children, and there's at // least one to add. AXObjectCache().ChildrenChanged(primary_image_element); @@ -3478,28 +3641,24 @@ void AXNodeObject::AddImageMapChildren() { } // Yes, this is the primary image. - HTMLAreaElement* first_area = Traversal<HTMLAreaElement>::FirstWithin(*map); - if (first_area) { - // If the <area> children were part of a different parent, notify that - // parent that its children have changed. - if (AXObject* ax_preexisting = AXObjectCache().Get(first_area)) { - if (AXObject* ax_previous_parent = ax_preexisting->CachedParentObject()) { - if (ax_previous_parent != this) { - DCHECK(ax_previous_parent->GetNode()); - AXObjectCache().ChildrenChangedWithCleanLayout( - ax_previous_parent->GetNode(), ax_previous_parent); - ax_previous_parent->ClearChildren(); - } - } - } - // Add the area children to |this|. - for (HTMLAreaElement& area : - Traversal<HTMLAreaElement>::DescendantsOf(*map)) { - // Add an <area> element for this child if it has a link and is visible. - AddChildAndCheckIncluded(AXObjectCache().GetOrCreate(&area, this)); + // If the children were part of a different parent, notify that parent that + // its children have changed. + if (AXObject* ax_previous_parent = AXObjectCache().GetAXImageForMap(*map)) { + if (ax_previous_parent != this) { + DCHECK(ax_previous_parent->GetNode()); + AXObjectCache().ChildrenChangedWithCleanLayout( + ax_previous_parent->GetNode(), ax_previous_parent); + ax_previous_parent->ClearChildren(); } } + + // Add the children to |this|. + Node* child = LayoutTreeBuilderTraversal::FirstChild(*map); + while (child) { + AddChildAndCheckIncluded(AXObjectCache().GetOrCreate(child, this)); + child = LayoutTreeBuilderTraversal::NextSibling(*child); + } } void AXNodeObject::AddPopupChildren() { @@ -3598,7 +3757,7 @@ void AXNodeObject::AddAccessibleNodeChildren() { void AXNodeObject::AddOwnedChildren() { AXObjectVector owned_children; - AXObjectCache().GetAriaOwnedChildren(this, owned_children); + AXObjectCache().ValidatedAriaOwnedChildren(this, owned_children); DCHECK(owned_children.size() == 0 || AXRelationCache::IsValidOwner(this)) << "This object is not allowed to use aria-owns, but it is.\n" @@ -3741,17 +3900,17 @@ void AXNodeObject::CheckValidChild(AXObject* child) { Node* child_node = child->GetNode(); - // An HTML image can only have area children. - DCHECK(!IsA<HTMLImageElement>(GetNode()) || IsA<HTMLAreaElement>(child_node)) - << "Image elements can only have area children, but this one has:\n" - << child->ToString(true, true); - // <area> children should only be added via AddImageMapChildren(), as the - // children of an <image usemap>, and never alone or as children of a <map>. - DCHECK(IsA<HTMLImageElement>(GetNode()) || !IsA<HTMLAreaElement>(child_node)) - << "Area elements can only be added by image parents: " - << child->ToString(true, true) << " had a parent of " - << ToString(true, true); + // descendants of an <image usemap> -- never alone or as children of a <map>. + if (IsA<HTMLAreaElement>(child_node)) { + AXObject* ancestor = this; + while (ancestor && !IsA<HTMLImageElement>(ancestor->GetNode())) + ancestor = ancestor->CachedParentObject(); + DCHECK(ancestor && IsA<HTMLImageElement>(ancestor->GetNode())) + << "Area elements can only be added by image parents: " + << child->ToString(true, true) << " had a parent of " + << ToString(true, true); + } // An option or popup for a <select size=1> must only be added via an // overridden AddChildren() on AXMenuList/AXMenuListPopup. @@ -3840,10 +3999,15 @@ void AXNodeObject::InsertChild(AXObject* child, int new_index = index; for (wtf_size_t i = 0; i < length; ++i) { if (children[i]->IsDetached()) { - CHECK(false) << "Cannot add a detached child: " - << "\n* Child: " << children[i]->ToString(true, true) - << "\n* Parent: " << child->ToString(true, true) - << "\n* Grandparent: " << ToString(true, true); + // TODO(accessibility) Restore to CHECK(). +#if defined(AX_FAIL_FAST_BUILD) + SANITIZER_NOTREACHED() + << "Cannot add a detached child: " + << "\n* Child: " << children[i]->ToString(true, true) + << "\n* Parent: " << child->ToString(true, true) + << "\n* Grandparent: " << ToString(true, true); +#endif + continue; } // If the child was owned, it will be added elsewhere as a direct // child of the object owning it. @@ -3903,9 +4067,6 @@ bool AXNodeObject::CanHaveChildren() const { // Note: these can have AXInlineTextBox children, but when adding them, we // also check AXObjectCache().InlineTextBoxAccessibilityEnabled(). return true; - case ax::mojom::blink::Role::kImage: - // Can turn into an image map if gains children later. - return GetNode() && GetNode()->IsLink(); default: break; } @@ -3916,44 +4077,10 @@ bool AXNodeObject::CanHaveChildren() const { if (blink::EnclosingTextControl(GetNode())) return true; - switch (AriaRoleAttribute()) { - case ax::mojom::blink::Role::kImage: - return false; - case ax::mojom::blink::Role::kCheckBox: - case ax::mojom::blink::Role::kListBoxOption: - case ax::mojom::blink::Role::kMath: // role="math" is flat, unlike <math> - case ax::mojom::blink::Role::kMenuListOption: - case ax::mojom::blink::Role::kMenuItem: - case ax::mojom::blink::Role::kMenuItemCheckBox: - case ax::mojom::blink::Role::kMenuItemRadio: - case ax::mojom::blink::Role::kPopUpButton: - case ax::mojom::blink::Role::kProgressIndicator: - case ax::mojom::blink::Role::kRadioButton: - case ax::mojom::blink::Role::kScrollBar: - case ax::mojom::blink::Role::kSlider: - case ax::mojom::blink::Role::kSplitter: - case ax::mojom::blink::Role::kSwitch: - case ax::mojom::blink::Role::kTab: - case ax::mojom::blink::Role::kToggleButton: { - // These roles have ChildrenPresentational: true in the ARIA spec. - // We used to remove/prune all descendants of them, but that removed - // useful content if the author didn't follow the spec perfectly, for - // example if they wanted a complex radio button with a textfield child. - // We are now only pruning these if there is a single text child, - // otherwise the subtree is exposed. The ChildrenPresentational rule - // is thus useful for authoring/verification tools but does not break - // complex widget implementations. - // Similarly, when content is inside a contenteditable, it does not make - // sense to hide it, since the user can interact with it. - // TODO(accessibility) Does it make sense to hide any of this content even - // in non-editable content? - Element* element = GetElement(); - return element && - (HasEditableStyle(*element) || !element->HasOneTextChild()); - } - default: - break; - } + // ARIA roles with childrenPresentational:true in the ARIA spec expose + // their contents to the browser side, allowing platforms to decide whether + // to make them a leaf, ensuring that focusable content cannot be hidden, + // and improving stability in Blink. return true; } @@ -3987,6 +4114,8 @@ Element* AXNodeObject::ActionElement() const { return element; Element* anchor = AnchorElement(); + if (anchor && !anchor->IsLiveLink()) + anchor = nullptr; // Non-interactive link target like <a name>. Element* click_element = MouseButtonListener(); if (!anchor || (click_element && click_element->IsDescendantOf(anchor))) return click_element; @@ -3994,24 +4123,16 @@ Element* AXNodeObject::ActionElement() const { } Element* AXNodeObject::AnchorElement() const { - Node* node = GetNode(); - if (!node) - return nullptr; - - AXObjectCacheImpl& cache = AXObjectCache(); - - // search up the DOM tree for an anchor element - // NOTE: this assumes that any non-image with an anchor is an - // HTMLAnchorElement - for (; node; node = node->parentNode()) { - if (IsA<HTMLAnchorElement>(*node)) - return To<Element>(node); - - if (LayoutObject* layout_object = node->GetLayoutObject()) { - AXObject* ax_object = cache.GetOrCreate(layout_object); - if (ax_object && ax_object->IsAnchor()) - return To<Element>(node); + // Search up the DOM tree for an anchor. This can be anything that has the + // linked state, such as an HTMLAnchorElement or role=link/doc-backlink. + const AXObject* current = this; + while (current) { + if (current->IsLink()) { + DCHECK(current->GetElement()) + << "An AXObject* that is a link should always have an element."; + return current->GetElement(); } + current = current->ParentObject(); } return nullptr; @@ -4359,11 +4480,14 @@ void AXNodeObject::HandleActiveDescendantChanged() { // Mark this node dirty. AXEventGenerator will automatically infer // that the active descendant changed. - AXObjectCache().MarkAXObjectDirtyWithCleanLayout(this, false); + AXObjectCache().MarkAXObjectDirtyWithCleanLayout(this); } } AXObject* AXNodeObject::ErrorMessage() const { + if (GetInvalidState() == ax::mojom::blink::InvalidState::kFalse) + return nullptr; + // Check for aria-errormessage. Element* existing_error_message = GetAOMPropertyOrARIAAttribute(AOMRelationProperty::kErrorMessage); @@ -4675,7 +4799,7 @@ String AXNodeObject::NativeTextAlternative( AXObject* figcaption_ax_object = AXObjectCache().GetOrCreate(figcaption); if (figcaption_ax_object) { text_alternative = - RecursiveTextAlternative(*figcaption_ax_object, false, visited); + RecursiveTextAlternative(*figcaption_ax_object, nullptr, visited); if (related_objects) { local_related_objects.push_back( @@ -4738,7 +4862,7 @@ String AXNodeObject::NativeTextAlternative( AXObject* caption_ax_object = AXObjectCache().GetOrCreate(caption); if (caption_ax_object) { text_alternative = - RecursiveTextAlternative(*caption_ax_object, false, visited); + RecursiveTextAlternative(*caption_ax_object, nullptr, visited); if (related_objects) { local_related_objects.push_back( MakeGarbageCollected<NameSourceRelatedObject>(caption_ax_object, @@ -4797,7 +4921,7 @@ String AXNodeObject::NativeTextAlternative( AXObject* title_ax_object = AXObjectCache().GetOrCreate(title); if (title_ax_object && !visited.Contains(title_ax_object)) { text_alternative = - RecursiveTextAlternative(*title_ax_object, false, visited); + RecursiveTextAlternative(*title_ax_object, nullptr, visited); if (related_objects) { local_related_objects.push_back( MakeGarbageCollected<NameSourceRelatedObject>(title_ax_object, @@ -4832,7 +4956,7 @@ String AXNodeObject::NativeTextAlternative( // Avoid an infinite loop if (legend_ax_object && !visited.Contains(legend_ax_object)) { text_alternative = - RecursiveTextAlternative(*legend_ax_object, false, visited); + RecursiveTextAlternative(*legend_ax_object, nullptr, visited); if (related_objects) { local_related_objects.push_back( @@ -5077,7 +5201,7 @@ String AXNodeObject::Description( if (ruby_annotation_ax_object) { AXObjectSet visited; description = - RecursiveTextAlternative(*ruby_annotation_ax_object, true, visited); + RecursiveTextAlternative(*ruby_annotation_ax_object, this, visited); if (related_objects) { related_objects->push_back( MakeGarbageCollected<NameSourceRelatedObject>( @@ -5110,7 +5234,7 @@ String AXNodeObject::Description( if (caption_ax_object) { AXObjectSet visited; description = - RecursiveTextAlternative(*caption_ax_object, false, visited); + RecursiveTextAlternative(*caption_ax_object, nullptr, visited); if (related_objects) { related_objects->push_back( MakeGarbageCollected<NameSourceRelatedObject>(caption_ax_object, @@ -5214,15 +5338,9 @@ String AXNodeObject::Placeholder(ax::mojom::blink::NameFrom name_from) const { String AXNodeObject::Title(ax::mojom::blink::NameFrom name_from) const { if (name_from == ax::mojom::blink::NameFrom::kTitle) - return String(); + return String(); // Already exposed the title in the name field. - if (const auto* element = GetElement()) { - String title = element->title(); - if (!title.IsEmpty()) - return title; - } - - return String(); + return GetTitle(GetElement()); } String AXNodeObject::PlaceholderFromNativeAttribute() const { |