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 {