AX: expose focusable elements even if element or ancestor has aria-hidden=true
https://bugs.webkit.org/show_bug.cgi?id=220534
<rdar://problem/71865875>
Reviewed by Zalan Bujtas.
Source/WebCore:
ARIA states that if an item is focused, then it should override aria-hidden status.
https://github.com/w3c/aria/pull/1387/files
Test: accessibility/focusable-inside-hidden.html
* accessibility/AXObjectCache.cpp:
(WebCore::isNodeAriaVisible):
* accessibility/AccessibilityObject.cpp:
(WebCore::AccessibilityObject::isAXHidden const):
(WebCore::AccessibilityObject::setIsIgnoredFromParentDataForChild):
LayoutTests:
* accessibility/focusable-inside-hidden-expected.txt: Added.
* accessibility/focusable-inside-hidden.html: Added.
git-svn-id: http://svn.webkit.org/repository/webkit/trunk@272390 268f45cc-cd09-0410-ab3c-d52691b4dbfc
diff --git a/LayoutTests/ChangeLog b/LayoutTests/ChangeLog
index 1e325ce..939ab59 100644
--- a/LayoutTests/ChangeLog
+++ b/LayoutTests/ChangeLog
@@ -1,3 +1,14 @@
+2021-02-04 Chris Fleizach <cfleizach@apple.com>
+
+ AX: expose focusable elements even if element or ancestor has aria-hidden=true
+ https://bugs.webkit.org/show_bug.cgi?id=220534
+ <rdar://problem/71865875>
+
+ Reviewed by Zalan Bujtas.
+
+ * accessibility/focusable-inside-hidden-expected.txt: Added.
+ * accessibility/focusable-inside-hidden.html: Added.
+
2021-02-04 Myles C. Maxfield <mmaxfield@apple.com>
Supplementary code points (U+10000 - U+10FFFF) are not shaped correctly in the fast text codepath
diff --git a/LayoutTests/accessibility/focusable-inside-hidden-expected.txt b/LayoutTests/accessibility/focusable-inside-hidden-expected.txt
new file mode 100644
index 0000000..bb43783
--- /dev/null
+++ b/LayoutTests/accessibility/focusable-inside-hidden-expected.txt
@@ -0,0 +1,15 @@
+This tests that a focusable object inside an aria-hidden is in the hieararchy only after it gains focus.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+PASS !button || !button.isValid is true
+PASS button.description is 'AXDescription: BUTTON'
+PASS button.description is 'AXDescription: BUTTON'
+PASS !button || !button.isValid is true
+PASS successfullyParsed is true
+
+TEST COMPLETE
+a
+test
+
diff --git a/LayoutTests/accessibility/focusable-inside-hidden.html b/LayoutTests/accessibility/focusable-inside-hidden.html
new file mode 100644
index 0000000..abf7419
--- /dev/null
+++ b/LayoutTests/accessibility/focusable-inside-hidden.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
+<html>
+<head>
+<script src="../resources/js-test-pre.js"></script>
+<script src="../resources/accessibility-helper.js"></script>
+</head>
+
+<body id="body">
+
+<div id="otherbutton" tabindex="0">a</div>
+
+<div aria-hidden="true">
+ <p id="backgroundContent">
+ <div tabindex="0" id="button" role="button" aria-label="BUTTON">test</div>
+ </p>
+</div>
+
+<script>
+ description("This tests that a focusable object inside an aria-hidden is in the hieararchy only after it gains focus.");
+
+ if (window.accessibilityController) {
+ jsTestIsAsync = true;
+
+ // By default, if it doesn't have focus it should be hidden because its inside aria-hidden.
+ var button = accessibilityController.accessibleElementById("button");
+ shouldBeTrue("!button || !button.isValid");
+
+ // Gain focus and this element should be visible to AX.
+ document.getElementById("button").focus();
+
+ setTimeout(function() {
+ button = accessibilityController.focusedElement;
+ shouldBe("button.description", "'AXDescription: BUTTON'");
+
+ button = accessibilityController.accessibleElementById("button");
+ shouldBe("button.description", "'AXDescription: BUTTON'");
+
+ // Lose focus and this element should be hidden again.
+ document.getElementById("otherbutton").focus();
+ setTimeout(function() {
+ button = accessibilityController.accessibleElementById("button");
+ shouldBeTrue("!button || !button.isValid");
+ finishJSTest();
+ }, 1);
+ }, 1);
+ }
+</script>
+<script src="../resources/js-test-post.js"></script>
+</body>
+</html>
diff --git a/Source/WebCore/ChangeLog b/Source/WebCore/ChangeLog
index f1c8856..fcd6e5c 100644
--- a/Source/WebCore/ChangeLog
+++ b/Source/WebCore/ChangeLog
@@ -1,3 +1,22 @@
+2021-02-04 Chris Fleizach <cfleizach@apple.com>
+
+ AX: expose focusable elements even if element or ancestor has aria-hidden=true
+ https://bugs.webkit.org/show_bug.cgi?id=220534
+ <rdar://problem/71865875>
+
+ Reviewed by Zalan Bujtas.
+
+ ARIA states that if an item is focused, then it should override aria-hidden status.
+ https://github.com/w3c/aria/pull/1387/files
+
+ Test: accessibility/focusable-inside-hidden.html
+
+ * accessibility/AXObjectCache.cpp:
+ (WebCore::isNodeAriaVisible):
+ * accessibility/AccessibilityObject.cpp:
+ (WebCore::AccessibilityObject::isAXHidden const):
+ (WebCore::AccessibilityObject::setIsIgnoredFromParentDataForChild):
+
2021-02-04 Myles C. Maxfield <mmaxfield@apple.com>
Supplementary code points (U+10000 - U+10FFFF) are not shaped correctly in the fast text codepath
diff --git a/Source/WebCore/accessibility/AXObjectCache.cpp b/Source/WebCore/accessibility/AXObjectCache.cpp
index 010d950..65bbf03 100644
--- a/Source/WebCore/accessibility/AXObjectCache.cpp
+++ b/Source/WebCore/accessibility/AXObjectCache.cpp
@@ -1827,6 +1827,12 @@
obj->notifyIfIgnoredValueChanged();
}
+void AXObjectCache::recomputeIsIgnored(Node* node)
+{
+ if (AccessibilityObject* obj = get(node))
+ obj->notifyIfIgnoredValueChanged();
+}
+
void AXObjectCache::startCachingComputedObjectAttributesUntilTreeMutates()
{
if (!m_computedObjectAttributeCache)
@@ -3151,8 +3157,12 @@
handleAttributeChange(deferredAttributeChangeContext.value, deferredAttributeChangeContext.key);
m_deferredAttributeChange.clear();
- for (auto& deferredFocusedChangeContext : m_deferredFocusedNodeChange)
+ for (auto& deferredFocusedChangeContext : m_deferredFocusedNodeChange) {
handleFocusedUIElementChanged(deferredFocusedChangeContext.first, deferredFocusedChangeContext.second);
+ // Recompute isIgnored after a focus change in case that altered visibility.
+ recomputeIsIgnored(deferredFocusedChangeContext.first);
+ recomputeIsIgnored(deferredFocusedChangeContext.second);
+ }
m_deferredFocusedNodeChange.clear();
for (auto& deferredModalChangedElement : m_deferredModalChangedList)
@@ -3349,6 +3359,10 @@
if (!node)
return false;
+ // If an element is focused, it should not be hidden.
+ if (is<Element>(*node) && downcast<Element>(*node).focused())
+ return true;
+
// ARIA Node visibility is controlled by aria-hidden
// 1) if aria-hidden=true, the whole subtree is hidden
// 2) if aria-hidden=false, and the object is rendered, there's no effect
diff --git a/Source/WebCore/accessibility/AXObjectCache.h b/Source/WebCore/accessibility/AXObjectCache.h
index 2be41b1..9f0208f 100644
--- a/Source/WebCore/accessibility/AXObjectCache.h
+++ b/Source/WebCore/accessibility/AXObjectCache.h
@@ -193,7 +193,8 @@
Node* modalNode();
void deferAttributeChangeIfNeeded(const QualifiedName&, Element*);
- void recomputeIsIgnored(RenderObject* renderer);
+ void recomputeIsIgnored(RenderObject*);
+ void recomputeIsIgnored(Node*);
#if ENABLE(ACCESSIBILITY)
WEBCORE_EXPORT static void enableAccessibility();
diff --git a/Source/WebCore/accessibility/AccessibilityObject.cpp b/Source/WebCore/accessibility/AccessibilityObject.cpp
index 9f7f7a7..06689a2 100644
--- a/Source/WebCore/accessibility/AccessibilityObject.cpp
+++ b/Source/WebCore/accessibility/AccessibilityObject.cpp
@@ -3197,8 +3197,11 @@
// http://www.w3.org/TR/wai-aria/terms#def_hidden
bool AccessibilityObject::isAXHidden() const
{
+ if (isFocused())
+ return false;
+
return Accessibility::findAncestor<AccessibilityObject>(*this, true, [] (const AccessibilityObject& object) {
- return equalLettersIgnoringASCIICase(object.getAttribute(aria_hiddenAttr), "true");
+ return equalLettersIgnoringASCIICase(object.getAttribute(aria_hiddenAttr), "true") && !object.isFocused();
}) != nullptr;
}
@@ -3567,7 +3570,7 @@
AccessibilityIsIgnoredFromParentData result = AccessibilityIsIgnoredFromParentData(this);
if (!m_isIgnoredFromParentData.isNull()) {
- result.isAXHidden = m_isIgnoredFromParentData.isAXHidden || equalLettersIgnoringASCIICase(child->getAttribute(aria_hiddenAttr), "true");
+ result.isAXHidden = (m_isIgnoredFromParentData.isAXHidden || equalLettersIgnoringASCIICase(child->getAttribute(aria_hiddenAttr), "true")) && !child->isFocused();
result.isPresentationalChildOfAriaRole = m_isIgnoredFromParentData.isPresentationalChildOfAriaRole || ariaRoleHasPresentationalChildren();
result.isDescendantOfBarrenParent = m_isIgnoredFromParentData.isDescendantOfBarrenParent || !canHaveChildren();
} else {