Commit 31ad4379 authored by Alice Boxhall's avatar Alice Boxhall Committed by Commit Bot

Include display:contents elements in accessibility tree

Bug: 835455
Change-Id: If3f4eacb975a8ee50a66bb787a55d257633cfb1e
Reviewed-on: https://chromium-review.googlesource.com/c/1242572
Commit-Queue: Alice Boxhall <aboxhall@chromium.org>
Reviewed-by: default avatarDominic Mazzoni <dmazzoni@chromium.org>
Cr-Commit-Position: refs/heads/master@{#609515}
parent 8f34427c
......@@ -2,6 +2,5 @@ rootWebArea pageLocation=(0, 0)
++genericContainer pageLocation=(0, 0)
++++staticText pageLocation=(0, 0) name='Before'
++++++inlineTextBox pageLocation=(0, 0) name='Before'
++++genericContainer pageLocation=(100, 0)
++++++staticText pageLocation=(100, 0) name='After'
++++++++inlineTextBox pageLocation=(100, 0) name='After'
++++staticText pageLocation=(100, 0) name='After'
++++++inlineTextBox pageLocation=(100, 0) name='After'
\ No newline at end of file
rootWebArea
++genericContainer
++++labelText
++++++checkBox name='Checkbox Title' checkedState=false
++++checkBox name='Checkbox Title' checkedState=false
\ No newline at end of file
AXWebArea
++AXGroup
++++AXGroup
++++++AXCheckBox AXTitle='Checkbox Title' AXValue='0'
++++AXCheckBox AXTitle='Checkbox Title' AXValue='0'
<!DOCTYPE html>
<script src="../resources/testharness.js"></script>
<script src="../resources/testharnessreport.js"></script>
<style>
.hideAllContainers .container { display: none; }
</style>
<template id="template">
<slot role="note" name="one"></slot>
<slot name="two"></slot>
</template>
<div class="container">
<div style="display: contents" id="div">Boring old div</div>
<div style="display: contents" role="heading" id="role-heading">Heading</div>
<button style="display: contents" id="button">Clear form</button>
<a href="#" style="display: contents" id="link">Click here</a>
<div id="shadow-host">
<div slot="one">Hello</div>
<div slot="two">Goodbye</div>
</div>
</div>
<script>
let shadowHost = document.getElementById('shadow-host');
let shadowRoot = shadowHost.attachShadow({mode: 'open'});
let template = document.getElementById('template');
shadowRoot.appendChild(template.content.cloneNode(true));
</script>
<script>
test(function(t)
{
let axDiv = accessibilityController.accessibleElementById('div');
assert_not_equals(axDiv, undefined);
assert_equals(axDiv.role, 'AXRole: AXGenericContainer');
}, 'Elements with display: contents should appear in the accessibility tree.');
test(function(t)
{
let axHeading = accessibilityController.accessibleElementById('role-heading');
assert_not_equals(axHeading, undefined);
assert_equals(axHeading.role, 'AXRole: AXHeading');
}, 'Elements with display: contents should have ARIA roles respected.');
test(function(t)
{
let axButton = accessibilityController.accessibleElementById('button');
assert_not_equals(axButton, undefined);
assert_equals(axButton.role, 'AXRole: AXButton');
let axLink = accessibilityController.accessibleElementById('link');
assert_not_equals(axLink, undefined);
assert_equals(axLink.role, 'AXRole: AXLink');
}, 'Elements with display: contents should have native roles respected.');
test(function(t)
{
let axShadowHost = accessibilityController.accessibleElementById('shadow-host');
assert_equals(axShadowHost.childrenCount, 2);
let axSlotWithRole = axShadowHost.childAtIndex(0);
assert_equals(axSlotWithRole.role, 'AXRole: AXNote');
let axSlotWithoutRole = axShadowHost.childAtIndex(1);
assert_equals(axSlotWithoutRole.role, 'AXRole: AXGenericContainer');
}, '<slot> elements should appear in the accessibility tree, and have ARIA roles respected');
</script>
<script>
if (window.testRunner)
document.body.className = "hideAllContainers";
</script>
......@@ -514,9 +514,6 @@ bool AXLayoutObject::IsSelectedFromFocus() const {
AXObjectInclusion AXLayoutObject::DefaultObjectInclusion(
IgnoredReasons* ignored_reasons) const {
// The following cases can apply to any element that's a subclass of
// AXLayoutObject.
if (!layout_object_) {
if (ignored_reasons)
ignored_reasons->push_back(IgnoredReason(kAXNotRendered));
......@@ -537,20 +534,6 @@ AXObjectInclusion AXLayoutObject::DefaultObjectInclusion(
return AXObject::DefaultObjectInclusion(ignored_reasons);
}
bool HasAriaAttribute(Element* element) {
if (!element)
return false;
AttributeCollection attributes = element->AttributesWithoutUpdate();
for (const Attribute& attr : attributes) {
// Attributes cache their uppercase names.
if (attr.GetName().LocalNameUpper().StartsWith("ARIA-"))
return true;
}
return false;
}
static bool HasLineBox(const LayoutBlockFlow& block_flow) {
if (!block_flow.IsLayoutNGMixin())
return block_flow.FirstLineBox();
......@@ -590,56 +573,40 @@ bool AXLayoutObject::ComputeAccessibilityIsIgnored(
DCHECK(initialized_);
#endif
if (!layout_object_)
if (!layout_object_) {
if (ignored_reasons)
ignored_reasons->push_back(IgnoredReason(kAXNotRendered));
return true;
}
// Check first if any of the common reasons cause this element to be ignored.
// Then process other use cases that need to be applied to all the various
// roles that AXLayoutObjects take on.
AXObjectInclusion decision = DefaultObjectInclusion(ignored_reasons);
if (decision == kIncludeObject)
AXObjectInclusion defaultInclusion = DefaultObjectInclusion(ignored_reasons);
if (defaultInclusion == kIncludeObject)
return false;
if (decision == kIgnoreObject)
return true;
if (layout_object_->IsAnonymousBlock() && !IsEditable())
if (defaultInclusion == kIgnoreObject)
return true;
// Ignore continuations, since those are essentially duplicate copies
// of inline nodes with blocks inside.
if (layout_object_->IsElementContinuation())
AXObjectInclusion semanticInclusion =
ShouldIncludeBasedOnSemantics(ignored_reasons);
if (semanticInclusion == kIncludeObject)
return false;
if (semanticInclusion == kIgnoreObject)
return true;
// If this element is within a parent that cannot have children, it should not
// be exposed.
if (IsDescendantOfLeafNode()) {
if (layout_object_->IsAnonymousBlock() && !IsEditable()) {
if (ignored_reasons)
ignored_reasons->push_back(
IgnoredReason(kAXAncestorIsLeafNode, LeafNodeAncestor()));
ignored_reasons->push_back(IgnoredReason(kAXUninteresting));
return true;
}
if (RoleValue() == ax::mojom::Role::kIgnored) {
// Ignore continuations, since those are essentially duplicate copies
// of inline nodes with blocks inside.
if (layout_object_->IsElementContinuation()) {
if (ignored_reasons)
ignored_reasons->push_back(IgnoredReason(kAXUninteresting));
return true;
}
if (HasInheritedPresentationalRole()) {
if (ignored_reasons) {
const AXObject* inherits_from = InheritsPresentationalRoleFrom();
if (inherits_from == this)
ignored_reasons->push_back(IgnoredReason(kAXPresentational));
else
ignored_reasons->push_back(
IgnoredReason(kAXInheritsPresentation, inherits_from));
}
return true;
}
if (IsTableLikeRole() || IsTableRowLikeRole() || IsTableCellLikeRole())
return false;
// A LayoutEmbeddedContent is an iframe element or embedded object element or
// something like that. We don't want to ignore those.
if (layout_object_->IsLayoutEmbeddedContent())
......@@ -660,145 +627,11 @@ bool AXLayoutObject::ComputeAccessibilityIsIgnored(
return false;
}
// Find out if this element is inside of a label element. If so, it may be
// ignored because it's the label for a checkbox or radio button.
AXObject* control_object = CorrespondingControlForLabelElement();
if (control_object && control_object->IsCheckboxOrRadio() &&
control_object->NameFromLabelElement()) {
if (ignored_reasons) {
HTMLLabelElement* label = LabelElementContainer();
if (label && label != GetNode()) {
AXObject* label_ax_object = AXObjectCache().GetOrCreate(label);
ignored_reasons->push_back(
IgnoredReason(kAXLabelContainer, label_ax_object));
}
ignored_reasons->push_back(IgnoredReason(kAXLabelFor, control_object));
}
return true;
}
if (layout_object_->IsBR())
return false;
if (CanSetFocusAttribute() && GetNode() && !IsHTMLBodyElement(GetNode()))
return false;
if (IsLink())
return false;
if (IsInPageLinkTarget())
return false;
// 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::DefaultActionVerb::kClickAncestor|, it will look for
// the clickable ancestor and it expects to find one.
if (IsClickable())
return false;
if (layout_object_->IsText()) {
if (CanIgnoreTextAsEmpty()) {
if (ignored_reasons)
ignored_reasons->push_back(IgnoredReason(kAXEmptyText));
return true;
}
return false;
}
if (IsHeading())
return false;
if (IsLandmarkRelated())
return false;
// Header and footer tags may also be exposed as landmark roles but not
// always.
if (GetNode() &&
(GetNode()->HasTagName(kHeaderTag) || GetNode()->HasTagName(kFooterTag)))
return false;
// all controls are accessible
if (IsControl())
return false;
if (AriaRoleAttribute() != ax::mojom::Role::kUnknown)
return false;
// don't ignore labels, because they serve as TitleUIElements
Node* node = layout_object_->GetNode();
if (IsHTMLLabelElement(node))
return false;
// Anything that is content editable should not be ignored.
// However, one cannot just call node->hasEditableStyle() since that will ask
// if its parents are also editable. Only the top level content editable
// region should be exposed.
if (HasContentEditableAttributeSet())
return false;
if (RoleValue() == ax::mojom::Role::kAbbr)
return false;
// List items play an important role in defining the structure of lists. They
// should not be ignored.
if (RoleValue() == ax::mojom::Role::kListItem)
return false;
if (RoleValue() == ax::mojom::Role::kBlockquote)
return false;
if (RoleValue() == ax::mojom::Role::kDialog)
return false;
if (RoleValue() == ax::mojom::Role::kFigcaption)
return false;
if (RoleValue() == ax::mojom::Role::kFigure)
return false;
if (RoleValue() == ax::mojom::Role::kContentDeletion)
return false;
if (RoleValue() == ax::mojom::Role::kContentInsertion)
return false;
if (RoleValue() == ax::mojom::Role::kDetails)
return false;
if (RoleValue() == ax::mojom::Role::kMark)
return false;
if (RoleValue() == ax::mojom::Role::kMath)
return false;
if (RoleValue() == ax::mojom::Role::kMeter)
return false;
if (RoleValue() == ax::mojom::Role::kRuby)
return false;
if (RoleValue() == ax::mojom::Role::kSplitter)
return false;
if (RoleValue() == ax::mojom::Role::kTime)
return false;
if (RoleValue() == ax::mojom::Role::kProgressIndicator)
return false;
// if this element has aria attributes on it, it should not be ignored.
if (HasGlobalARIAAttribute())
return false;
if (IsImage())
return false;
if (IsCanvas()) {
if (CanvasHasFallbackContent())
return false;
const auto* canvas = ToLayoutHTMLCanvasOrNull(layout_object_);
const auto* canvas = ToLayoutHTMLCanvasOrNull(GetLayoutObject());
if (canvas &&
(canvas->Size().Height() <= 1 || canvas->Size().Width() <= 1)) {
if (ignored_reasons)
......@@ -810,29 +643,20 @@ bool AXLayoutObject::ComputeAccessibilityIsIgnored(
// to decide.
}
if (IsWebArea() || layout_object_->IsListMarkerIncludingNG())
return false;
// 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.
//
// These checks are simplified in the interest of execution speed;
// for example, any element having an alt attribute will make it
// not ignored, rather than just images.
if (HasAriaAttribute(GetElement()) || !GetAttribute(kAltAttr).IsEmpty() ||
!GetAttribute(kTitleAttr).IsEmpty())
if (layout_object_->IsBR())
return false;
// <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 (IsHTMLSpanElement(node)) {
if (layout_object_->IsText()) {
if (CanIgnoreTextAsEmpty()) {
if (ignored_reasons)
ignored_reasons->push_back(IgnoredReason(kAXUninteresting));
ignored_reasons->push_back(IgnoredReason(kAXEmptyText));
return true;
}
return false;
}
if (IsWebArea() || layout_object_->IsListMarkerIncludingNG())
return false;
// Positioned elements and scrollable containers are important for
// determining bounding boxes.
......
......@@ -30,6 +30,8 @@
#include <math.h>
#include "base/debug/stack_trace.h"
#include "third_party/blink/renderer/core/aom/accessible_node.h"
#include "third_party/blink/renderer/core/dom/element.h"
#include "third_party/blink/renderer/core/dom/flat_tree_traversal.h"
......@@ -132,23 +134,53 @@ AXObject* AXNodeObject::ActiveDescendant() {
return ax_descendant;
}
bool AXNodeObject::ComputeAccessibilityIsIgnored(
IgnoredReasons* ignored_reasons) const {
#if DCHECK_IS_ON()
// Double-check that an AXObject is never accessed before
// it's been initialized.
DCHECK(initialized_);
#endif
bool HasAriaAttribute(Element* element) {
if (!element)
return false;
AttributeCollection attributes = element->AttributesWithoutUpdate();
for (const Attribute& attr : attributes) {
// Attributes cache their uppercase names.
if (attr.GetName().LocalNameUpper().StartsWith("ARIA-"))
return true;
}
return false;
}
AXObjectInclusion AXNodeObject::ShouldIncludeBasedOnSemantics(
IgnoredReasons* ignored_reasons) const {
// If this element is within a parent that cannot have children, it should not
// be exposed.
if (IsDescendantOfLeafNode()) {
if (ignored_reasons)
ignored_reasons->push_back(
IgnoredReason(kAXAncestorIsLeafNode, LeafNodeAncestor()));
return true;
return kIgnoreObject;
}
if (RoleValue() == ax::mojom::Role::kIgnored) {
if (ignored_reasons)
ignored_reasons->push_back(IgnoredReason(kAXUninteresting));
return kIgnoreObject;
}
if (HasInheritedPresentationalRole()) {
if (ignored_reasons) {
const AXObject* inherits_from = InheritsPresentationalRoleFrom();
if (inherits_from == this) {
ignored_reasons->push_back(IgnoredReason(kAXPresentational));
} else {
ignored_reasons->push_back(
IgnoredReason(kAXInheritsPresentation, inherits_from));
}
}
return kIgnoreObject;
}
if (IsTableLikeRole() || IsTableRowLikeRole() || IsTableCellLikeRole())
return kIncludeObject;
// Ignore labels that are already referenced by a control.
AXObject* control_object = CorrespondingControlForLabelElement();
if (control_object && control_object->IsCheckboxOrRadio() &&
......@@ -163,24 +195,140 @@ bool AXNodeObject::ComputeAccessibilityIsIgnored(
ignored_reasons->push_back(IgnoredReason(kAXLabelFor, control_object));
}
return true;
return kIgnoreObject;
}
Element* element = GetNode()->IsElementNode() ? ToElement(GetNode())
: GetNode()->parentElement();
if (!GetLayoutObject() && (!element || !element->IsInCanvasSubtree()) &&
!AOMPropertyOrARIAAttributeIsFalse(AOMBooleanProperty::kHidden)) {
if (CanSetFocusAttribute() && GetNode() && !IsHTMLBodyElement(GetNode()))
return kIncludeObject;
if (IsLink() || IsInPageLinkTarget())
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::DefaultActionVerb::kClickAncestor|, it will look for
// the clickable ancestor and it expects to find one.
if (IsClickable())
return kIncludeObject;
if (IsHeading() || IsLandmarkRelated())
return kIncludeObject;
// Header and footer tags may also be exposed as landmark roles but not
// always.
if (GetNode() &&
(GetNode()->HasTagName(kHeaderTag) || GetNode()->HasTagName(kFooterTag)))
return kIncludeObject;
// All controls are accessible.
if (IsControl())
return kIncludeObject;
// Anything with an explicit ARIA role should be included.
if (AriaRoleAttribute() != ax::mojom::Role::kUnknown)
return kIncludeObject;
// Don't ignore labels, because they serve as TitleUIElements.
Node* node = GetNode();
if (IsHTMLLabelElement(node))
return kIncludeObject;
// Anything that is content editable should not be ignored.
// However, one cannot just call node->hasEditableStyle() since that will ask
// if its parents are also editable. Only the top level content editable
// region should be exposed.
if (HasContentEditableAttributeSet())
return kIncludeObject;
static const std::set<ax::mojom::Role> always_included_computed_roles = {
ax::mojom::Role::kAbbr,
ax::mojom::Role::kBlockquote,
ax::mojom::Role::kContentDeletion,
ax::mojom::Role::kContentInsertion,
ax::mojom::Role::kDetails,
ax::mojom::Role::kDialog,
ax::mojom::Role::kFigcaption,
ax::mojom::Role::kFigure,
ax::mojom::Role::kListItem,
ax::mojom::Role::kMark,
ax::mojom::Role::kMath,
ax::mojom::Role::kMeter,
ax::mojom::Role::kProgressIndicator,
ax::mojom::Role::kRuby,
ax::mojom::Role::kSplitter,
ax::mojom::Role::kTime,
};
if (always_included_computed_roles.find(RoleValue()) !=
always_included_computed_roles.end())
return kIncludeObject;
// If this element has aria attributes on it, it should not be ignored.
if (HasGlobalARIAAttribute())
return kIncludeObject;
if (IsImage())
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.
//
// These checks are simplified in the interest of execution speed;
// for example, any element having an alt attribute will make it
// not ignored, rather than just images.
if (HasAriaAttribute(GetElement()) || !GetAttribute(kAltAttr).IsEmpty() ||
!GetAttribute(kTitleAttr).IsEmpty())
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 (node && IsHTMLSpanElement(node)) {
if (ignored_reasons)
ignored_reasons->push_back(IgnoredReason(kAXNotRendered));
return true;
ignored_reasons->push_back(IgnoredReason(kAXUninteresting));
return kIgnoreObject;
}
return kDefaultBehavior;
}
bool AXNodeObject::ComputeAccessibilityIsIgnored(
IgnoredReasons* ignored_reasons) const {
#if DCHECK_IS_ON()
// Double-check that an AXObject is never accessed before
// it's been initialized.
DCHECK(initialized_);
#endif
if (GetLayoutObject()) {
if (role_ == ax::mojom::Role::kUnknown) {
if (ignored_reasons)
ignored_reasons->push_back(IgnoredReason(kAXUninteresting));
return true;
}
return false;
}
Element* element = GetNode()->IsElementNode() ? ToElement(GetNode())
: GetNode()->parentElement();
if (!element)
return true;
if (element->IsInCanvasSubtree())
return ShouldIncludeBasedOnSemantics(ignored_reasons) == kIgnoreObject;
if (AOMPropertyOrARIAAttributeIsFalse(AOMBooleanProperty::kHidden))
return false;
if (element->HasDisplayContentsStyle()) {
if (ShouldIncludeBasedOnSemantics(ignored_reasons) == kIncludeObject)
return false;
}
if (ignored_reasons)
ignored_reasons->push_back(IgnoredReason(kAXNotRendered));
return true;
}
static bool IsListElement(Node* node) {
......@@ -2099,9 +2247,12 @@ void AXNodeObject::GetRelativeBounds(AXObject** out_container,
}
}
// If it's in a canvas but doesn't have an explicit rect, get the bounding
// rect of its children.
if (GetNode()->parentElement()->IsInCanvasSubtree()) {
Element* element = GetElement();
// If it's in a canvas but doesn't have an explicit rect, or has display:
// contents set, get the bounding rect of its children.
if ((GetNode()->parentElement() &&
GetNode()->parentElement()->IsInCanvasSubtree()) ||
(element && element->HasDisplayContentsStyle())) {
Vector<FloatRect> rects;
for (Node& child : NodeTraversal::ChildrenOf(*GetNode())) {
if (child.IsHTMLElement()) {
......
......@@ -55,6 +55,8 @@ class MODULES_EXPORT AXNodeObject : public AXObject {
// The accessibility role, not taking ARIA into account.
ax::mojom::Role native_role_;
AXObjectInclusion ShouldIncludeBasedOnSemantics(
IgnoredReasons* = nullptr) const;
bool ComputeAccessibilityIsIgnored(IgnoredReasons* = nullptr) const override;
const AXObject* InheritsPresentationalRoleFrom() const override;
ax::mojom::Role DetermineAccessibilityRole() override;
......
......@@ -1011,8 +1011,12 @@ void AXObjectCacheImpl::HandlePossibleRoleChange(Node* node) {
if (!node)
return; // Virtual AOM node.
AXObject* obj = Get(node);
if (!obj && IsHTMLSelectElement(node))
obj = GetOrCreate(node);
// Invalidate the current object and make the parent reconsider its children.
if (AXObject* obj = Get(node)) {
if (obj) {
// Save parent for later use.
AXObject* parent = obj->ParentObject();
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment