AX: aria-modal nodes wrapped in aria-hidden are not honored
authorcfleizach@apple.com <cfleizach@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 29 Jun 2020 18:54:16 +0000 (18:54 +0000)
committercfleizach@apple.com <cfleizach@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 29 Jun 2020 18:54:16 +0000 (18:54 +0000)
https://bugs.webkit.org/show_bug.cgi?id=212849
<rdar://problem/64047019>

Reviewed by Zalan Bujtas.

Source/WebCore:

Test: accessibility/aria-modal-in-aria-hidden.html

If aria-modal was wrapped inside aria-hidden, we were still processing that as the modal node.
Fixing that uncovered a host of very finicky issues related to aria-modal.
1. We were processing modal status immediately instead of after a delay, so visibility requirements were not correct.
2. In handleModalChange: We were processing multiple modal nodes perhaps incorrectly (the spec doesn't account for multiple modal nodes).
     - had to update a test to turn off modal status before adding a new modal node
3. Changed the modal node to a WeakPtr
4. In isNodeAriaVisible: We stopped processing for visibile with aria-hidden as soon as we hit a renderable block, but that means it won't account
   for nodes higher in the tree with aria-hidden.
5. In handleAttributeChange: if aria-hidden changes, we should update modal status if needed.
6. In focusModalNodeTimerFired: we need to verify the element is still live, otherwise it can lead to a crash.

* accessibility/AXObjectCache.cpp:
(WebCore::AXObjectCache::AXObjectCache):
(WebCore::AXObjectCache::findModalNodes):
(WebCore::AXObjectCache::currentModalNode):
(WebCore::AXObjectCache::modalNode):
(WebCore::AXObjectCache::remove):
(WebCore::AXObjectCache::deferModalChange):
(WebCore::AXObjectCache::focusModalNodeTimerFired):
(WebCore::AXObjectCache::handleAttributeChange):
(WebCore::AXObjectCache::handleModalChange):
(WebCore::AXObjectCache::prepareForDocumentDestruction):
(WebCore::AXObjectCache::performDeferredCacheUpdate):
(WebCore::isNodeAriaVisible):
* accessibility/AXObjectCache.h:
(WebCore::AXObjectCache::handleModalChange):
(WebCore::AXObjectCache::deferModalChange):
* accessibility/AccessibilityRenderObject.cpp:
(WebCore::AccessibilityRenderObject::firstChild const):
(WebCore::AccessibilityRenderObject::lastChild const):

LayoutTests:

* accessibility/aria-hidden-negates-no-visibility-expected.txt:
* accessibility/aria-hidden-negates-no-visibility.html:
* accessibility/aria-modal-in-aria-hidden-expected.txt: Added.
* accessibility/aria-modal-in-aria-hidden.html: Added.
* accessibility/aria-modal.html:
* accessibility/mac/aria-modal-auto-focus.html:

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@263673 268f45cc-cd09-0410-ab3c-d52691b4dbfc

LayoutTests/ChangeLog
LayoutTests/accessibility/aria-hidden-negates-no-visibility-expected.txt
LayoutTests/accessibility/aria-hidden-negates-no-visibility.html
LayoutTests/accessibility/aria-modal-in-aria-hidden-expected.txt [new file with mode: 0644]
LayoutTests/accessibility/aria-modal-in-aria-hidden.html [new file with mode: 0644]
LayoutTests/accessibility/aria-modal.html
LayoutTests/accessibility/mac/aria-modal-auto-focus.html
Source/WebCore/ChangeLog
Source/WebCore/accessibility/AXObjectCache.cpp
Source/WebCore/accessibility/AXObjectCache.h
Source/WebCore/accessibility/AccessibilityRenderObject.cpp

index 4af1464..810b862 100644 (file)
@@ -1,3 +1,18 @@
+2020-06-29  Chris Fleizach  <cfleizach@apple.com>
+
+        AX: aria-modal nodes wrapped in aria-hidden are not honored
+        https://bugs.webkit.org/show_bug.cgi?id=212849
+        <rdar://problem/64047019>
+
+        Reviewed by Zalan Bujtas.
+
+        * accessibility/aria-hidden-negates-no-visibility-expected.txt:
+        * accessibility/aria-hidden-negates-no-visibility.html:
+        * accessibility/aria-modal-in-aria-hidden-expected.txt: Added.
+        * accessibility/aria-modal-in-aria-hidden.html: Added.
+        * accessibility/aria-modal.html:
+        * accessibility/mac/aria-modal-auto-focus.html:
+
 2020-06-29  Karl Rackler  <rackler@apple.com>
 
         Remove expectation for imported/w3c/web-platform-tests/css/css-transitions/before-load-001.html as it is passing. 
index bb5ecfb..cad243a 100644 (file)
@@ -2,7 +2,6 @@ heading1.2
 
 
 
-
 This tests ensures that aria-hidden=false will allow a node to be in the AX hierarchy even if it's not rendered or visible
 
 On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
@@ -18,7 +17,7 @@ PASS parent.childAtIndex(1).isEqual(heading3) is true
 PASS heading4.role is 'AXRole: AXHeading'
 PASS parent.childAtIndex(2).isEqual(heading4) is true
 Textfield Title: AXTitle: 
-PASS video.childrenCount is 0
+PASS !video || video.childrenCount == 0 is true
 PASS successfullyParsed is true
 
 TEST COMPLETE
index 474b609..40b3f74 100644 (file)
 <div hidden aria-hidden="false" id="hiddenDiv">HiddenText1</div>
 <input type="text" aria-labelledby="hiddenDiv" id="textFieldWithHiddenLabeller">
 
+<div>
 <div aria-hidden="false">
-<video id="video">
+<video hidden id="video">
 Hidden content
 </video>
 </div>
+</div>
 
 <p id="description"></p>
 <div id="console"></div>
@@ -63,7 +65,7 @@ Hidden content
         
         // aria-hidden="false" need to be on each parent, including rendered parents.
         var video = accessibilityController.accessibleElementById("video");
-        shouldBe("video.childrenCount", "0");
+        shouldBeTrue("!video || video.childrenCount == 0");
     }
 
 </script>
diff --git a/LayoutTests/accessibility/aria-modal-in-aria-hidden-expected.txt b/LayoutTests/accessibility/aria-modal-in-aria-hidden-expected.txt
new file mode 100644 (file)
index 0000000..4f3afdf
--- /dev/null
@@ -0,0 +1,14 @@
+This tests that when something is aria-modal inside an aria-hidden it is ignored.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+PASS accessibilityController.accessibleElementById('bgContent').isIgnored is false
+PASS !backgroundContent || !backgroundContent.isValid() is true
+PASS accessibilityController.accessibleElementById('bgContent').isIgnored is false
+PASS successfullyParsed is true
+
+TEST COMPLETE
+Other page content with a dummy focusable element
+
+Just an example.
diff --git a/LayoutTests/accessibility/aria-modal-in-aria-hidden.html b/LayoutTests/accessibility/aria-modal-in-aria-hidden.html
new file mode 100644 (file)
index 0000000..561de37
--- /dev/null
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
+<html>
+<head>
+<script src="../resources/js-test-pre.js"></script>
+</head>
+
+<body id="body">
+
+<div id="bg">
+<p id="bgContent">Other page content with <a href="#">a dummy focusable element</a></p>
+</div>
+
+<div id="container">
+    <div role="dialog" aria-modal="false" id="box">
+        <h3 id="myDialog">Just an example.</h3>
+    </div>
+</div>
+
+
+<script>
+
+    description("This tests that when something is aria-modal inside an aria-hidden it is ignored.");
+    window.jsTestIsAsync = true;
+
+    if (window.accessibilityController) {
+        shouldBeFalse("accessibilityController.accessibleElementById('bgContent').isIgnored");
+        
+        document.getElementById("box").setAttribute("aria-modal", "true");
+
+        var backgroundContent = accessibilityController.accessibleElementById('bgContent');
+        shouldBeTrue("!backgroundContent || !backgroundContent.isValid()");
+
+        document.getElementById("container").setAttribute("aria-hidden", "true");
+
+        setTimeout(function() {
+            shouldBeFalse("accessibilityController.accessibleElementById('bgContent').isIgnored");
+            finishJSTest();
+        }, 0);
+    }    
+
+</script>
+
+
+<script src="../resources/js-test-post.js"></script>
+</body>
+</html>
index c0a4b67..787e5e1 100644 (file)
@@ -21,7 +21,7 @@
 <script>
 
     description("This tests that aria-modal on dialog makes other elements inert.");
-
+    jsTestIsAsync = true;
     if (window.accessibilityController) {
         // Background should be unaccessible after loading, since the 
         // dialog is displayed and aria-modal=true.
         document.getElementById("box").setAttribute("aria-hidden", "true");
         shouldBeTrue("backgroundAccessible()");
         document.getElementById("box").setAttribute("aria-hidden", "false");
-        shouldBeFalse("backgroundAccessible()");
+        setTimeout(function() {
+            shouldBeFalse("backgroundAccessible()");
         
-        // Test modal dialog is removed from DOM tree.
-        var dialog = document.getElementById("box");
-        dialog.parentNode.removeChild(dialog);
-        shouldBeTrue("backgroundAccessible()");
+            // Test modal dialog is removed from DOM tree.
+            var dialog = document.getElementById("box");
+            dialog.parentNode.removeChild(dialog);
+            shouldBeTrue("backgroundAccessible()");
+            finishJSTest();
+         }, 0);
     }
     
     function backgroundAccessible() {
@@ -82,4 +85,4 @@
 
 <script src="../resources/js-test-post.js"></script>
 </body>
-</html>
\ No newline at end of file
+</html>
index 16c2654..5e26f55 100644 (file)
             shouldBeTrue("newBtn.isFocused");
             
             // 2. Click the new button, dialog2 shows and focus should move to the close button.
+            // Set aria-modal to false on the previous modal object (we shouldn't have two modals in play).
             document.getElementById("new").click();
+            document.getElementById("box").ariaModal = false;
             setTimeout(function(){ 
                 closeBtn = accessibilityController.accessibleElementById("close");
                 shouldBeTrue("closeBtn.isFocused");
                   
                 // 3. Click the close button, dialog2 closes and focus should go back to the
-                // first focusable child of dialog1.
+                // first focusable child of dialog1, which we now need to add aria-modal back to.
                 document.getElementById("close").click();
+                document.getElementById("box").ariaModal = true;
                 setTimeout(function(){
                     okBtn = accessibilityController.accessibleElementById("ok");
                     shouldBeTrue("okBtn.isFocused");
index da3c224..5999d10 100644 (file)
@@ -1,3 +1,44 @@
+2020-06-29  Chris Fleizach  <cfleizach@apple.com>
+
+        AX: aria-modal nodes wrapped in aria-hidden are not honored
+        https://bugs.webkit.org/show_bug.cgi?id=212849
+        <rdar://problem/64047019>
+
+        Reviewed by Zalan Bujtas.
+
+        Test: accessibility/aria-modal-in-aria-hidden.html
+
+        If aria-modal was wrapped inside aria-hidden, we were still processing that as the modal node.
+        Fixing that uncovered a host of very finicky issues related to aria-modal.
+        1. We were processing modal status immediately instead of after a delay, so visibility requirements were not correct.
+        2. In handleModalChange: We were processing multiple modal nodes perhaps incorrectly (the spec doesn't account for multiple modal nodes). 
+             - had to update a test to turn off modal status before adding a new modal node
+        3. Changed the modal node to a WeakPtr
+        4. In isNodeAriaVisible: We stopped processing for visibile with aria-hidden as soon as we hit a renderable block, but that means it won't account
+           for nodes higher in the tree with aria-hidden.
+        5. In handleAttributeChange: if aria-hidden changes, we should update modal status if needed.
+        6. In focusModalNodeTimerFired: we need to verify the element is still live, otherwise it can lead to a crash.
+
+        * accessibility/AXObjectCache.cpp:
+        (WebCore::AXObjectCache::AXObjectCache):
+        (WebCore::AXObjectCache::findModalNodes):
+        (WebCore::AXObjectCache::currentModalNode):
+        (WebCore::AXObjectCache::modalNode):
+        (WebCore::AXObjectCache::remove):
+        (WebCore::AXObjectCache::deferModalChange):
+        (WebCore::AXObjectCache::focusModalNodeTimerFired):
+        (WebCore::AXObjectCache::handleAttributeChange):
+        (WebCore::AXObjectCache::handleModalChange):
+        (WebCore::AXObjectCache::prepareForDocumentDestruction):
+        (WebCore::AXObjectCache::performDeferredCacheUpdate):
+        (WebCore::isNodeAriaVisible):
+        * accessibility/AXObjectCache.h:
+        (WebCore::AXObjectCache::handleModalChange):
+        (WebCore::AXObjectCache::deferModalChange):
+        * accessibility/AccessibilityRenderObject.cpp:
+        (WebCore::AccessibilityRenderObject::firstChild const):
+        (WebCore::AccessibilityRenderObject::lastChild const):
+
 2020-06-29  Youenn Fablet  <youenn@apple.com>
 
         Support MediaRecorder.onstart
index 8506f1c..cab00b9 100644 (file)
@@ -220,7 +220,7 @@ AXObjectCache::AXObjectCache(Document& document)
     , m_passwordNotificationPostTimer(*this, &AXObjectCache::passwordNotificationPostTimerFired)
     , m_liveRegionChangedPostTimer(*this, &AXObjectCache::liveRegionChangedNotificationPostTimerFired)
     , m_focusModalNodeTimer(*this, &AXObjectCache::focusModalNodeTimerFired)
-    , m_currentModalNode(nullptr)
+    , m_currentModalElement(nullptr)
     , m_performCacheUpdateTimer(*this, &AXObjectCache::performCacheUpdateTimerFired)
 {
     ASSERT(isMainThread());
@@ -252,39 +252,39 @@ void AXObjectCache::findModalNodes()
         if (!equalLettersIgnoringASCIICase(element->attributeWithoutSynchronization(aria_modalAttr), "true"))
             continue;
 
-        m_modalNodesSet.add(element);
+        m_modalElementsSet.add(element);
     }
 
     m_modalNodesInitialized = true;
 }
 
-Node* AXObjectCache::currentModalNode()
+Element* AXObjectCache::currentModalNode()
 {
     // There might be multiple nodes with aria-modal=true set.
     // We use this function to pick the one we want.
-    m_currentModalNode = nullptr;
-    if (m_modalNodesSet.isEmpty())
+    m_currentModalElement = nullptr;
+    if (m_modalElementsSet.isEmpty())
         return nullptr;
 
     // If any of the modal nodes contains the keyboard focus, we want to pick that one.
     // If not, we want to pick the last visible dialog in the DOM.
     RefPtr<Element> focusedElement = document().focusedElement();
-    RefPtr<Node> lastVisible;
-    for (auto& node : m_modalNodesSet) {
-        if (isNodeVisible(node)) {
-            if (focusedElement && focusedElement->isDescendantOf(node)) {
-                m_currentModalNode = node;
+    RefPtr<Element> lastVisible;
+    for (auto& element : m_modalElementsSet) {
+        if (isNodeVisible(element)) {
+            if (focusedElement && focusedElement->isDescendantOf(element)) {
+                m_currentModalElement = makeWeakPtr(element);
                 break;
             }
 
-            lastVisible = node;
+            lastVisible = element;
         }
     }
 
-    if (!m_currentModalNode)
-        m_currentModalNode = lastVisible.get();
+    if (!m_currentModalElement)
+        m_currentModalElement = makeWeakPtr(lastVisible.get());
 
-    return m_currentModalNode;
+    return m_currentModalElement.get();
 }
 
 bool AXObjectCache::isNodeVisible(Node* node) const
@@ -306,23 +306,21 @@ bool AXObjectCache::isNodeVisible(Node* node) const
     return true;
 }
 
+// This function returns the valid aria modal node.
 Node* AXObjectCache::modalNode()
 {
-    // This function returns the valid aria modal node.
-    if (!m_modalNodesInitialized) {
+    if (!m_modalNodesInitialized)
         findModalNodes();
-        return currentModalNode();
-    }
 
-    if (m_modalNodesSet.isEmpty())
+    if (m_modalElementsSet.isEmpty())
         return nullptr;
 
     // Check the cached current valid aria modal node first.
     // Usually when one dialog sets aria-modal=true, that dialog is the one we want.
-    if (isNodeVisible(m_currentModalNode))
-        return m_currentModalNode;
+    if (isNodeVisible(m_currentModalElement.get()))
+        return m_currentModalElement.get();
 
-    // Recompute the valid aria modal node when m_currentModalNode is null or hidden.
+    // Recompute the valid aria modal node when m_currentModalElement is null or hidden.
     return currentModalNode();
 }
 
@@ -874,6 +872,7 @@ void AXObjectCache::remove(Node& node)
     if (is<Element>(node)) {
         m_deferredTextFormControlValue.remove(downcast<Element>(&node));
         m_deferredAttributeChange.remove(downcast<Element>(&node));
+        m_modalElementsSet.remove(downcast<Element>(&node));
     }
     m_deferredChildrenChangedNodeList.remove(&node);
     m_deferredTextChangedList.remove(&node);
@@ -890,11 +889,6 @@ void AXObjectCache::remove(Node& node)
     removeNodeForUse(node);
 
     remove(m_nodeObjectMapping.take(&node));
-
-    if (m_currentModalNode == &node)
-        m_currentModalNode = nullptr;
-    m_modalNodesSet.remove(&node);
-
     remove(node.renderer());
 }
 
@@ -1188,6 +1182,13 @@ void AXObjectCache::deferFocusedUIElementChangeIfNeeded(Node* oldNode, Node* new
     } else
         handleFocusedUIElementChanged(oldNode, newNode);
 }
+
+void AXObjectCache::deferModalChange(Element* element)
+{
+    m_deferredModalChangedList.add(element);
+    if (!m_performCacheUpdateTimer.isActive())
+        m_performCacheUpdateTimer.startOneShot(0_s);
+}
     
 void AXObjectCache::handleFocusedUIElementChanged(Node* oldNode, Node* newNode)
 {
@@ -1586,15 +1587,19 @@ void AXObjectCache::focusModalNode()
 
 void AXObjectCache::focusModalNodeTimerFired()
 {
-    if (!m_currentModalNode)
+    if (!m_document.hasLivingRenderTree())
+        return;
+
+    Ref<Document> protectedDocument(m_document);
+    if (!nodeAndRendererAreValid(m_currentModalElement.get()) || !isNodeVisible(m_currentModalElement.get()))
         return;
     
     // Don't set focus if we are already focusing onto some element within
     // the dialog.
-    if (m_currentModalNode->contains(document().focusedElement()))
+    if (m_currentModalElement->contains(document().focusedElement()))
         return;
     
-    if (AccessibilityObject* currentModalNodeObject = getOrCreate(m_currentModalNode)) {
+    if (AccessibilityObject* currentModalNodeObject = getOrCreate(m_currentModalElement.get())) {
         if (AccessibilityObject* focusable = firstFocusableChild(currentModalNodeObject))
             focusable->setFocused(true);
     }
@@ -1692,12 +1697,17 @@ void AXObjectCache::handleAttributeChange(const QualifiedName& attrName, Element
         selectedChildrenChanged(element);
     else if (attrName == aria_expandedAttr)
         handleAriaExpandedChange(element);
-    else if (attrName == aria_hiddenAttr)
+    else if (attrName == aria_hiddenAttr) {
         childrenChanged(element->parentNode(), element);
+        if (m_currentModalElement && m_currentModalElement->isDescendantOf(element)) {
+            m_modalNodesInitialized = false;
+            deferModalChange(m_currentModalElement.get());
+        }
+    }
     else if (attrName == aria_invalidAttr)
         postNotification(element, AXObjectCache::AXInvalidStatusChanged);
     else if (attrName == aria_modalAttr)
-        handleModalChange(element);
+        deferModalChange(element);
     else if (attrName == aria_currentAttr)
         postNotification(element, AXObjectCache::AXCurrentChanged);
     else if (attrName == aria_disabledAttr)
@@ -1712,12 +1722,9 @@ void AXObjectCache::handleAttributeChange(const QualifiedName& attrName, Element
         postNotification(element, AXObjectCache::AXAriaAttributeChanged);
 }
 
-void AXObjectCache::handleModalChange(Node* node)
+void AXObjectCache::handleModalChange(Element& element)
 {
-    if (!is<Element>(node))
-        return;
-
-    if (!nodeHasRole(node, "dialog") && !nodeHasRole(node, "alertdialog"))
+    if (!nodeHasRole(&element, "dialog") && !nodeHasRole(&element, "alertdialog"))
         return;
 
     stopCachingComputedObjectAttributes();
@@ -1725,18 +1732,19 @@ void AXObjectCache::handleModalChange(Node* node)
     if (!m_modalNodesInitialized)
         findModalNodes();
 
-    if (equalLettersIgnoringASCIICase(downcast<Element>(*node).attributeWithoutSynchronization(aria_modalAttr), "true")) {
-        // Add the newly modified node to the modal nodes set, and set it to be the current valid aria modal node.
+    if (equalLettersIgnoringASCIICase(element.attributeWithoutSynchronization(aria_modalAttr), "true")) {
+        // Add the newly modified node to the modal nodes set.
         // We will recompute the current valid aria modal node in modalNode() when this node is not visible.
-        m_modalNodesSet.add(node);
-        m_currentModalNode = node;
+        m_modalElementsSet.add(&element);
     } else {
-        // Remove the node from the modal nodes set. There might be other visible modal nodes, so we recompute here.
-        m_modalNodesSet.remove(node);
-        currentModalNode();
+        // Remove the node from the modal nodes set.
+        m_modalElementsSet.remove(&element);
     }
 
-    if (m_currentModalNode)
+    // Find new active modal node.
+    currentModalNode();
+
+    if (m_currentModalElement)
         focusModalNode();
 
     startCachingComputedObjectAttributesUntilTreeMutates();
@@ -3035,13 +3043,13 @@ void AXObjectCache::prepareForDocumentDestruction(const Document& document)
 {
     HashSet<Node*> nodesToRemove;
     filterListForRemoval(m_textMarkerNodes, document, nodesToRemove);
-    filterListForRemoval(m_modalNodesSet, document, nodesToRemove);
+    filterListForRemoval(m_modalElementsSet, document, nodesToRemove);
     filterListForRemoval(m_deferredTextChangedList, document, nodesToRemove);
     filterListForRemoval(m_deferredChildrenChangedNodeList, document, nodesToRemove);
     filterMapForRemoval(m_deferredTextFormControlValue, document, nodesToRemove);
     filterMapForRemoval(m_deferredAttributeChange, document, nodesToRemove);
     filterVectorPairForRemoval(m_deferredFocusedNodeChange, document, nodesToRemove);
-
+    
     for (auto* node : nodesToRemove)
         remove(*node);
 }
@@ -3110,6 +3118,10 @@ void AXObjectCache::performDeferredCacheUpdate()
         handleFocusedUIElementChanged(deferredFocusedChangeContext.first, deferredFocusedChangeContext.second);
     m_deferredFocusedNodeChange.clear();
 
+    for (auto& deferredModalChangedElement : m_deferredModalChangedList)
+        handleModalChange(deferredModalChangedElement);
+    m_deferredModalChangedList.clear();
+
     platformPerformDeferredCacheUpdate();
 }
     
@@ -3302,15 +3314,16 @@ bool isNodeAriaVisible(Node* node)
             const AtomString& ariaHiddenValue = downcast<Element>(*testNode).attributeWithoutSynchronization(aria_hiddenAttr);
             if (equalLettersIgnoringASCIICase(ariaHiddenValue, "true"))
                 return false;
-            
+
+            // We should break early when it gets to the body.
+            if (testNode->hasTagName(bodyTag))
+                break;
+
             bool ariaHiddenFalse = equalLettersIgnoringASCIICase(ariaHiddenValue, "false");
             if (!testNode->renderer() && !ariaHiddenFalse)
                 return false;
             if (!ariaHiddenFalsePresent && ariaHiddenFalse)
                 ariaHiddenFalsePresent = true;
-            // We should break early when it gets to a rendered object.
-            if (testNode->renderer())
-                break;
         }
     }
     
index 1cf2d68..2fe46e2 100644 (file)
@@ -190,6 +190,7 @@ public:
     void updateCacheAfterNodeIsAttached(Node*);
 
     void deferFocusedUIElementChangeIfNeeded(Node* oldFocusedNode, Node* newFocusedNode);
+    void deferModalChange(Element*);
     void handleScrolledToAnchor(const Node* anchorNode);
     void handleScrollbarUpdate(ScrollView*);
     
@@ -462,10 +463,10 @@ private:
 
     // aria-modal related
     void findModalNodes();
-    Node* currentModalNode();
+    Element* currentModalNode();
     bool isNodeVisible(Node*) const;
-    void handleModalChange(Node*);
-
+    void handleModalChange(Element&);
+    
     Document& m_document;
     const Optional<PageIdentifier> m_pageID; // constant for object's lifetime.
     HashMap<AXID, RefPtr<AccessibilityObject>> m_objects;
@@ -490,8 +491,10 @@ private:
     ListHashSet<RefPtr<AccessibilityObject>> m_liveRegionObjectsSet;
 
     Timer m_focusModalNodeTimer;
-    Node* m_currentModalNode;
-    ListHashSet<Node*> m_modalNodesSet;
+    WeakPtr<Element> m_currentModalElement;
+    // Multiple aria-modals behavior is undefined by spec. We keep them sorted based on DOM order here.
+    // If that changes to require only one aria-modal we could change this to a WeakHashSet, or discard the set completely.
+    ListHashSet<Element*> m_modalElementsSet;
     bool m_modalNodesInitialized { false };
 
     Timer m_performCacheUpdateTimer;
@@ -502,6 +505,7 @@ private:
     WeakHashSet<Element> m_deferredSelectedChildredChangedList;
     ListHashSet<RefPtr<AXCoreObject>> m_deferredChildrenChangedList;
     ListHashSet<Node*> m_deferredChildrenChangedNodeList;
+    WeakHashSet<Element> m_deferredModalChangedList;
     HashMap<Element*, String> m_deferredTextFormControlValue;
     HashMap<Element*, QualifiedName> m_deferredAttributeChange;
     Vector<std::pair<Node*, Node*>> m_deferredFocusedNodeChange;
@@ -573,7 +577,8 @@ inline void AXObjectCache::frameLoadingEventNotification(Frame*, AXLoadingEvent)
 inline void AXObjectCache::frameLoadingEventPlatformNotification(AccessibilityObject*, AXLoadingEvent) { }
 inline void AXObjectCache::handleActiveDescendantChanged(Node*) { }
 inline void AXObjectCache::handleAriaExpandedChange(Node*) { }
-inline void AXObjectCache::handleModalChange(Node*) { }
+inline void AXObjectCache::handleModalChange(Element*) { }
+inline void AXObjectCache::deferModalChange(Element*) { }
 inline void AXObjectCache::handleAriaRoleChanged(Node*) { }
 inline void AXObjectCache::deferAttributeChangeIfNeeded(const QualifiedName&, Element*) { }
 inline void AXObjectCache::handleAttributeChange(const QualifiedName&, Element*) { }
index 62249a3..7370685 100644 (file)
@@ -228,7 +228,8 @@ AccessibilityObject* AccessibilityRenderObject::firstChild() const
     if (!firstChild && !canHaveChildren())
         return AccessibilityNodeObject::firstChild();
 
-    return axObjectCache()->getOrCreate(firstChild);
+    auto objectCache = axObjectCache();
+    return objectCache ? objectCache->getOrCreate(firstChild) : nullptr;
 }
 
 AccessibilityObject* AccessibilityRenderObject::lastChild() const
@@ -241,7 +242,8 @@ AccessibilityObject* AccessibilityRenderObject::lastChild() const
     if (!lastChild && !canHaveChildren())
         return AccessibilityNodeObject::lastChild();
 
-    return axObjectCache()->getOrCreate(lastChild);
+    auto objectCache = axObjectCache();
+    return objectCache ? objectCache->getOrCreate(lastChild) : nullptr;
 }
 
 static inline RenderInline* startOfContinuations(RenderObject& renderer)