Commit 56e69a64 authored by Adam Ettenberger's avatar Adam Ettenberger Committed by Chromium LUCI CQ

Invalidate aria-hidden subtree to update accessibility ignored and invisible states

This CL addresses a Blink Accessibility invalidation gap, when a node's
aria-hidden state changes, its blink accessibility subtree must also be
invalidated so that the accessibility ignored and invisible states are
serialized to the browser process.

This is important because we rely on these states being accurate in the
browser process, and we need need to receive the state changes through
AXEventGenerator in order to send appropriate platform accessibility
events, such as structure change events (add/remove child, hide/show,
or 'children-changed').

Adding AXObjectCacheImpl::HandleAriaHiddenChangedWithCleanLayout which
handles this invalidation, and immediately handles
ChildrenChangedWithCleanLayout on the parent of the modified node,
and increments |modification_count_| to invalidate AXObject cached
values. e.g. AccessibilityIsIgnored(), and IsInertOrAriaHidden().

I needed to also modify BrowserAccessibilityWin::CanFireEvents to allow
events to process for the corner case outlined in Bug:1159660.
Because the node becoming ignored was the only child, the parent became
a leaf node, and the node removed would return false for CanFireEvents
because now it "IsChildOfLeaf()". I added a condition to check if the
ignored state changed this frame, and if so, allow events to process.
BrowserAccessibilityManagerWin filters platform events appropriately
for nodes that are ignored.

Bug: 1159660
Change-Id: I82e2368c09d4855fdb834600aed7ac61d1883c65
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2612365
Commit-Queue: Adam Ettenberger <Adam.Ettenberger@microsoft.com>
Reviewed-by: default avatarKurt Catti-Schmidt <kschmi@microsoft.com>
Reviewed-by: default avatarJacques Newman <janewman@microsoft.com>
Reviewed-by: default avatarAaron Leventhal <aleventhal@chromium.org>
Cr-Commit-Position: refs/heads/master@{#846165}
parent b4a2fcee
......@@ -514,7 +514,7 @@ void BrowserAccessibilityManagerWin::FireWinAccessibilityEvent(
// state may show / hide a popup by exposing it to the tree or not.
// Also include focus events since a node may become visible at the same time
// it receives focus It's never good to suppress a po
if (base::Contains(ignored_changed_nodes_, node)) {
if (IsIgnoredChangedNode(node)) {
switch (win_event_type) {
case EVENT_OBJECT_HIDE:
case EVENT_OBJECT_SHOW:
......@@ -541,6 +541,12 @@ void BrowserAccessibilityManagerWin::FireWinAccessibilityEvent(
::NotifyWinEvent(win_event_type, hwnd, OBJID_CLIENT, child_id);
}
bool BrowserAccessibilityManagerWin::IsIgnoredChangedNode(
const BrowserAccessibility* node) const {
return base::Contains(ignored_changed_nodes_,
const_cast<BrowserAccessibility*>(node));
}
void BrowserAccessibilityManagerWin::FireUiaAccessibilityEvent(
LONG uia_event,
BrowserAccessibility* node) {
......@@ -551,7 +557,7 @@ void BrowserAccessibilityManagerWin::FireUiaAccessibilityEvent(
// Suppress events when |IGNORED_CHANGED| except for MenuClosed / MenuOpen
// since a change in the ignored state may show / hide a popup by exposing
// it to the tree or not.
if (base::Contains(ignored_changed_nodes_, node)) {
if (IsIgnoredChangedNode(node)) {
switch (uia_event) {
case UIA_MenuClosedEventId:
case UIA_MenuOpenedEventId:
......@@ -577,7 +583,7 @@ void BrowserAccessibilityManagerWin::FireUiaPropertyChangedEvent(
// Suppress events when |IGNORED_CHANGED| with the exception for firing
// UIA_AriaPropertiesPropertyId-hidden event on non-text node marked as
// ignored.
if (node->IsIgnored() || base::Contains(ignored_changed_nodes_, node)) {
if (node->IsIgnored() || IsIgnoredChangedNode(node)) {
if (uia_property != UIA_AriaPropertiesPropertyId || node->IsText())
return;
}
......@@ -603,7 +609,7 @@ void BrowserAccessibilityManagerWin::FireUiaStructureChangedEvent(
if (!ShouldFireEventForNode(node))
return;
// Suppress events when |IGNORED_CHANGED| except for related structure changes
if (base::Contains(ignored_changed_nodes_, node)) {
if (IsIgnoredChangedNode(node)) {
switch (change_type) {
case StructureChangeType_ChildRemoved:
case StructureChangeType_ChildAdded:
......
......@@ -36,6 +36,7 @@ class CONTENT_EXPORT BrowserAccessibilityManagerWin
// BrowserAccessibilityManager methods
void UserIsReloading() override;
BrowserAccessibility* GetFocus() const override;
bool IsIgnoredChangedNode(const BrowserAccessibility* node) const;
bool CanFireEvents() const override;
gfx::Rect GetViewBoundsInScreenCoordinates() const override;
......
......@@ -5,6 +5,7 @@
#include "content/browser/accessibility/browser_accessibility_win.h"
#include "content/browser/accessibility/browser_accessibility_manager.h"
#include "content/browser/accessibility/browser_accessibility_manager_win.h"
#include "content/browser/accessibility/browser_accessibility_state_impl.h"
#include "ui/base/win/atl_module.h"
......@@ -47,6 +48,13 @@ bool BrowserAccessibilityWin::CanFireEvents() const {
if (!IsIgnored() && GetCollapsedMenuListPopUpButtonAncestor())
return true;
// If the node changed its ignored state this frame then some events should be
// allowed, such as hide/show/structure events. If a node with no siblings
// changes aria-hidden value, this would affect whether it would be considered
// a "child of leaf" node which affects BrowserAccessibility::CanFireEvents.
if (manager()->ToBrowserAccessibilityManagerWin()->IsIgnoredChangedNode(this))
return true;
return BrowserAccessibility::CanFireEvents();
}
......
......@@ -574,6 +574,25 @@ IN_PROC_BROWSER_TEST_P(DumpAccessibilityEventsTest,
RunEventTest(FILE_PATH_LITERAL("aria-hidden-descendants.html"));
}
IN_PROC_BROWSER_TEST_P(DumpAccessibilityEventsTest,
AccessibilityEventsAriaHiddenSingleDescendant) {
RunEventTest(FILE_PATH_LITERAL("aria-hidden-single-descendant.html"));
}
IN_PROC_BROWSER_TEST_P(
DumpAccessibilityEventsTest,
AccessibilityEventsAriaHiddenSingleDescendantDisplayNone) {
RunEventTest(
FILE_PATH_LITERAL("aria-hidden-single-descendant-display-none.html"));
}
IN_PROC_BROWSER_TEST_P(
DumpAccessibilityEventsTest,
AccessibilityEventsAriaHiddenSingleDescendantVisibilityHidden) {
RunEventTest(FILE_PATH_LITERAL(
"aria-hidden-single-descendant-visibility-hidden.html"));
}
IN_PROC_BROWSER_TEST_P(DumpAccessibilityEventsTest,
AccessibilityEventsAriaHiddenDescendantsAlreadyIgnored) {
RunEventTest(
......
......@@ -739,6 +739,24 @@ IN_PROC_BROWSER_TEST_P(DumpAccessibilityTreeTest,
RunAriaTest(FILE_PATH_LITERAL("aria-hidden-descendants.html"));
}
IN_PROC_BROWSER_TEST_P(DumpAccessibilityTreeTest,
AccessibilityAriaHiddenSingleDescendant) {
RunAriaTest(FILE_PATH_LITERAL("aria-hidden-single-descendant.html"));
}
IN_PROC_BROWSER_TEST_P(DumpAccessibilityTreeTest,
AccessibilityAriaHiddenSingleDescendantDisplayNone) {
RunAriaTest(
FILE_PATH_LITERAL("aria-hidden-single-descendant-display-none.html"));
}
IN_PROC_BROWSER_TEST_P(
DumpAccessibilityTreeTest,
AccessibilityAriaHiddenSingleDescendantVisibilityHidden) {
RunAriaTest(FILE_PATH_LITERAL(
"aria-hidden-single-descendant-visibility-hidden.html"));
}
IN_PROC_BROWSER_TEST_P(DumpAccessibilityTreeTest,
AccessibilityAriaHiddenDescendantTabindexChange) {
RunAriaTest(FILE_PATH_LITERAL("aria-hidden-descendant-tabindex-change.html"));
......
rootWebArea
++genericContainer ignored
++++genericContainer ignored
++++++genericContainer invisible
++++++group name='Done'
<!--
@WAIT-FOR:Done
-->
<html>
<body>
<div style="display: none">
<div class="test-case" aria-hidden="false">
<button>expect invisible subtree</button>
</div>
<div class="test-case" aria-hidden="true">
<button>expect invisible subtree</button>
</div>
</div>
<div role="group" id="test-status" aria-label="running"></div>
<script>
setTimeout(() => {
document.querySelectorAll('.test-case[aria-hidden]').forEach((element) => {
let hidden = element.getAttribute('aria-hidden') == 'true';
element.setAttribute('aria-hidden', !hidden);
});
}, 500);
setTimeout(() => {
document.getElementById('test-status').setAttribute('aria-label', 'Done');
}, 1000);
</script>
</body>
</html>
rootWebArea
++genericContainer ignored
++++genericContainer ignored
++++++genericContainer ignored invisible
++++++++button invisible name='expect invisible subtree'
++++++++++staticText ignored invisible name='expect invisible subtree'
++++++++++++inlineTextBox ignored invisible name='expect invisible subtree'
++++++genericContainer
++++++++button name='expect visible subtree'
++++++++++staticText name='expect visible subtree'
++++++group name='Done'
rootWebArea
++genericContainer ignored
++++genericContainer ignored
++++++genericContainer ignored invisible
++++++++genericContainer ignored invisible
++++++++++button ignored invisible name='expect invisible subtree'
++++++++++++staticText ignored invisible name='expect invisible subtree'
++++++++genericContainer invisible
++++++++++button ignored invisible name='expect invisible subtree'
++++++++genericContainer ignored invisible
++++++++++button invisible name='expect invisible subtree'
++++++++++++staticText ignored invisible name='expect invisible subtree'
++++++++++++++inlineTextBox ignored invisible name='expect invisible subtree'
++++++++genericContainer
++++++++++button name='expect visible subtree'
++++++++++++staticText name='expect visible subtree'
++++++group name='Done'
<!--
@WAIT-FOR:Done
-->
<html>
<body>
<div style="visibility: hidden">
<div class="test-case" aria-hidden="false">
<button>expect invisible subtree</button>
</div>
<div class="test-case" aria-hidden="true">
<button>expect invisible subtree</button>
</div>
<div class="test-case" aria-hidden="false" style="visibility: visible">
<button>expect invisible subtree</button>
</div>
<div class="test-case" aria-hidden="true" style="visibility: visible">
<button>expect visible subtree</button>
</div>
</div>
<div role="group" id="test-status" aria-label="running"></div>
<script>
setTimeout(() => {
document.querySelectorAll('.test-case[aria-hidden]').forEach((element) => {
let hidden = element.getAttribute('aria-hidden') == 'true';
element.setAttribute('aria-hidden', !hidden);
});
}, 500);
setTimeout(() => {
document.getElementById('test-status').setAttribute('aria-label', 'Done');
}, 1000);
</script>
</body>
</html>
<!--
@WAIT-FOR:Done
-->
<html>
<body>
<div class="test-case" aria-hidden="false">
<button>expect invisible subtree</button>
</div>
<div class="test-case" aria-hidden="true">
<button>expect visible subtree</button>
</div>
<div role="group" id="test-status" aria-label="running"></div>
<script>
setTimeout(() => {
document.querySelectorAll('.test-case[aria-hidden]').forEach((element) => {
let hidden = element.getAttribute('aria-hidden') == 'true';
element.setAttribute('aria-hidden', !hidden);
});
}, 500);
setTimeout(() => {
document.getElementById('test-status').setAttribute('aria-label', 'Done');
}, 1000);
</script>
</body>
</html>
CHILDREN-CHANGED:REMOVE index:0 CHILD:(role=ROLE_HEADING) role=ROLE_TOOL_BAR ENABLED,HORIZONTAL,SENSITIVE,SHOWING,VISIBLE
CHILDREN-CHANGED:REMOVE index:0 CHILD:(role=ROLE_SECTION) role=ROLE_HEADING ENABLED,SENSITIVE,SHOWING,VISIBLE
CHILDREN-CHANGED:REMOVE index:0 CHILD:(role=ROLE_SECTION) role=ROLE_SECTION ENABLED,SENSITIVE,SHOWING,VISIBLE
......@@ -2,6 +2,8 @@
@WIN-DENY:IA2_EVENT_TEXT_INSERTED*
@WIN-DENY:IA2_EVENT_TEXT_REMOVED*
@WIN-DENY:EVENT_OBJECT_REORDER*
@UIA-WIN-DENY:AriaProperties*
@UIA-WIN-DENY:StructureChanged/ChildrenReordered*
-->
<html>
<body>
......
AriaProperties changed on role=banner, name=Banner
AriaProperties changed on role=toolbar
StructureChanged/ChildAdded on role=banner, name=Banner
StructureChanged/ChildRemoved on role=toolbar
StructureChanged/ChildrenReordered on role=toolbar
<!--
@WIN-DENY:IA2_EVENT_TEXT_INSERTED*
@WIN-DENY:IA2_EVENT_TEXT_REMOVED*
@WIN-DENY:EVENT_OBJECT_REORDER*
@UIA-WIN-DENY:StructureChanged/ChildrenReordered*
-->
<html>
<body>
<div style="display: none">
<div class="test-case" aria-hidden="false">
<button>expect invisible subtree</button>
</div>
<div class="test-case" aria-hidden="true">
<button>expect invisible subtree</button>
</div>
</div>
<script>
function go() {
document.querySelectorAll('.test-case[aria-hidden]').forEach((element) => {
let hidden = element.getAttribute('aria-hidden') == 'true';
element.setAttribute('aria-hidden', !hidden);
}
</script>
</body>
</html>
AriaProperties changed on role=document
StructureChanged/ChildRemoved on role=document
=== Start Continuation ===
AriaProperties changed on role=group
StructureChanged/ChildAdded on role=group
EVENT_OBJECT_HIDE on <div.test-case> role=ROLE_SYSTEM_GROUPING INVISIBLE
EVENT_OBJECT_STATECHANGE on <button> role=ROLE_SYSTEM_PUSHBUTTON name="expect invisible subtree" INVISIBLE,FOCUSABLE
=== Start Continuation ===
EVENT_OBJECT_SHOW on <div.test-case> role=ROLE_SYSTEM_GROUPING
EVENT_OBJECT_STATECHANGE on <button> role=ROLE_SYSTEM_PUSHBUTTON name="expect visible subtree" FOCUSABLE
AriaProperties changed on role=document
StructureChanged/ChildRemoved on role=document
=== Start Continuation ===
AriaProperties changed on role=document
StructureChanged/ChildAdded on role=document
=== Start Continuation ===
AriaProperties changed on role=document
StructureChanged/ChildRemoved on role=document
=== Start Continuation ===
AriaProperties changed on role=group
StructureChanged/ChildAdded on role=group
EVENT_OBJECT_HIDE on <div.test-case> role=ROLE_SYSTEM_GROUPING INVISIBLE
=== Start Continuation ===
EVENT_OBJECT_SHOW on <div.test-case> role=ROLE_SYSTEM_GROUPING INVISIBLE
=== Start Continuation ===
EVENT_OBJECT_HIDE on <div.test-case> role=ROLE_SYSTEM_GROUPING INVISIBLE
EVENT_OBJECT_STATECHANGE on <button> role=ROLE_SYSTEM_PUSHBUTTON name="expect invisible subtree" INVISIBLE,FOCUSABLE
=== Start Continuation ===
EVENT_OBJECT_SHOW on <div.test-case> role=ROLE_SYSTEM_GROUPING
EVENT_OBJECT_STATECHANGE on <button> role=ROLE_SYSTEM_PUSHBUTTON name="expect visible subtree" FOCUSABLE
<!--
@WIN-DENY:IA2_EVENT_TEXT_INSERTED*
@WIN-DENY:IA2_EVENT_TEXT_REMOVED*
@WIN-DENY:EVENT_OBJECT_REORDER*
@UIA-WIN-DENY:StructureChanged/ChildrenReordered*
-->
<html>
<body>
<div style="visibility: hidden">
<div class="test-case" aria-hidden="false">
<button>expect invisible subtree</button>
</div>
<div class="test-case" aria-hidden="true">
<button>expect invisible subtree</button>
</div>
<div class="test-case" aria-hidden="false" style="visibility: visible">
<button>expect invisible subtree</button>
</div>
<div class="test-case" aria-hidden="true" style="visibility: visible">
<button>expect visible subtree</button>
</div>
</div>
<script>
var current_pass = 0;
var test_cases = document.querySelectorAll('.test-case[aria-hidden]');
function run_test_case(element) {
let hidden = element.getAttribute('aria-hidden') == 'true';
element.setAttribute('aria-hidden', !hidden);
}
function go() {
run_test_case(test_cases.item(current_pass++));
return current_pass < test_cases.length;
}
</script>
</body>
</html>
<!--
@WIN-DENY:IA2_EVENT_TEXT_INSERTED*
@WIN-DENY:IA2_EVENT_TEXT_REMOVED*
@WIN-DENY:EVENT_OBJECT_REORDER*
@UIA-WIN-DENY:StructureChanged/ChildrenReordered*
-->
<html>
<body>
<div class="test-case" aria-hidden="false">
<button>expect invisible subtree</button>
</div>
<div class="test-case" aria-hidden="true">
<button>expect visible subtree</button>
</div>
<script>
var current_pass = 0;
var test_cases = document.querySelectorAll('.test-case[aria-hidden]');
function run_test_case(element) {
let hidden = element.getAttribute('aria-hidden') == 'true';
element.setAttribute('aria-hidden', !hidden);
}
function go() {
run_test_case(test_cases.item(current_pass++));
return current_pass < test_cases.length;
}
</script>
</body>
</html>
AriaProperties changed on role=tree
StructureChanged/ChildRemoved on role=tree
StructureChanged/ChildrenReordered on role=tree
StructureChanged/ChildrenReordered on role=tree
StructureChanged/ChildrenReordered on role=tree
StructureChanged/ChildrenReordered on role=tree
=== Start Continuation ===
AriaProperties changed on role=article
StructureChanged/ChildAdded on role=article
StructureChanged/ChildrenReordered on role=tree
StructureChanged/ChildrenReordered on role=treeitem, name=grandchild1
StructureChanged/ChildrenReordered on role=treeitem, name=grandchild2
StructureChanged/ChildrenReordered on role=treeitem, name=grandchild3
......@@ -5,4 +5,7 @@ EVENT_OBJECT_REORDER on <div> role=ROLE_SYSTEM_OUTLINEITEM name="grandchild2" IN
EVENT_OBJECT_REORDER on <div> role=ROLE_SYSTEM_OUTLINEITEM name="grandchild3" INVISIBLE,FOCUSABLE level=2
=== Start Continuation ===
EVENT_OBJECT_REORDER on <div#tree> role=ROLE_SYSTEM_OUTLINE IA2_STATE_VERTICAL
EVENT_OBJECT_SHOW on <div#article> role=ROLE_SYSTEM_DOCUMENT
\ No newline at end of file
EVENT_OBJECT_REORDER on <div> role=ROLE_SYSTEM_OUTLINEITEM name="grandchild1" FOCUSABLE level=2 PosInSet=1 SetSize=3
EVENT_OBJECT_REORDER on <div> role=ROLE_SYSTEM_OUTLINEITEM name="grandchild2" FOCUSABLE level=2 PosInSet=2 SetSize=3
EVENT_OBJECT_REORDER on <div> role=ROLE_SYSTEM_OUTLINEITEM name="grandchild3" FOCUSABLE level=2 PosInSet=3 SetSize=3
EVENT_OBJECT_SHOW on <div#article> role=ROLE_SYSTEM_DOCUMENT
......@@ -2004,6 +2004,58 @@ void AXObjectCacheImpl::HandleRoleChangeWithCleanLayout(Node* node) {
}
}
void AXObjectCacheImpl::HandleAriaHiddenChangedWithCleanLayout(Node* node) {
if (!node)
return;
SCOPED_DISALLOW_LIFECYCLE_TRANSITION(node->GetDocument());
DCHECK(!node->GetDocument().NeedsLayoutTreeUpdateForNode(*node));
AXObject* obj = GetOrCreate(node);
if (!obj)
return;
// https://www.w3.org/TR/wai-aria-1.1/#aria-hidden
// An element is considered hidden if it, or any of its ancestors are not
// rendered or have their aria-hidden attribute value set to true.
AXObject* parent = obj->ParentObject();
if (parent) {
// If the parent is inert or aria-hidden, then the subtree will be
// ignored and changing aria-hidden will have no effect.
// |IsInertOrAriaHidden| returns true if the element or one of its
// ancestors is either inert or within an aria-hidden subtree.
if (parent->IsInertOrAriaHidden())
return;
// If the parent is 'display: none', then the subtree will be ignored and
// changing aria-hidden will have no effect.
if (parent->GetLayoutObject()) {
// For elements with layout objects we can get their style directly.
if (parent->GetLayoutObject()->Style()->Display() == EDisplay::kNone)
return;
} else if (Element* parent_element = parent->GetElement()) {
// No layout object: must ensure computed style.
const ComputedStyle* parent_style = parent_element->EnsureComputedStyle();
if (!parent_style || parent_style->IsEnsuredInDisplayNone())
return;
}
// Unlike AXObject's |IsVisible| or |IsHiddenViaStyle| this method does not
// consider 'visibility: [hidden|collapse]', because while the visibility
// property is inherited it can be overridden by any descendant by providing
// 'visibility: visible' so it would be safest to invalidate the subtree in
// such a case.
}
// Changing the aria hidden state should trigger recomputing all
// cached values even if it doesn't result in a notification, because
// it affects accessibility ignored state.
modification_count_++;
// Invalidate the subtree because aria-hidden affects the
// accessibility ignored state for the entire subtree.
MarkAXObjectDirty(obj, /*subtree=*/true);
ChildrenChangedWithCleanLayout(node->parentNode());
}
void AXObjectCacheImpl::HandleAttributeChanged(const QualifiedName& attr_name,
Element* element) {
DCHECK(element);
......@@ -2073,7 +2125,7 @@ void AXObjectCacheImpl::HandleAttributeChangedWithCleanLayout(
} else if (attr_name == html_names::kAriaExpandedAttr) {
HandleAriaExpandedChangeWithCleanLayout(element);
} else if (attr_name == html_names::kAriaHiddenAttr) {
ChildrenChangedWithCleanLayout(element->parentNode());
HandleAriaHiddenChangedWithCleanLayout(element);
} else if (attr_name == html_names::kAriaInvalidAttr) {
MarkElementDirty(element, false);
} else if (attr_name == html_names::kAriaErrormessageAttr) {
......
......@@ -217,6 +217,7 @@ class MODULES_EXPORT AXObjectCacheImpl
void HandleActiveDescendantChangedWithCleanLayout(Node*);
void HandleRoleChangeWithCleanLayout(Node*);
void HandleAriaHiddenChangedWithCleanLayout(Node*);
void HandleAriaExpandedChangeWithCleanLayout(Node*);
void HandleAriaSelectedChangedWithCleanLayout(Node*);
void HandleNodeLostFocusWithCleanLayout(Node*);
......
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