bug 1430701, contents of frameless shadow hosts should be focusable, r=mrbkap
authorOlli Pettay <Olli.Pettay@helsinki.fi>
Fri, 20 Jul 2018 19:29:52 +0300
changeset 821008 b2bf59373df221b444682b00cf4c4478cee797a8
parent 821007 c1af3f6ba68608d93e476f88d25e926ea2624b38
child 821009 b13a396d28ec1567a36f57b67ebf44373af54dd1
push id116993
push usermaglione.k@gmail.com
push dateFri, 20 Jul 2018 18:58:06 +0000
reviewersmrbkap
bugs1430701
milestone63.0a1
bug 1430701, contents of frameless shadow hosts should be focusable, r=mrbkap
dom/base/nsFocusManager.cpp
dom/base/test/file_bug1453693.html
--- a/dom/base/nsFocusManager.cpp
+++ b/dom/base/nsFocusManager.cpp
@@ -3450,16 +3450,34 @@ nsFocusManager::GetNextTabbableContentIn
     }
 
     startContent = owner;
   }
 
   return nullptr;
 }
 
+static nsIContent*
+GetTopLevelHost(nsIContent* aContent)
+{
+  nsIContent* topLevelhost = nullptr;
+  while (aContent) {
+    if (HTMLSlotElement* slot = aContent->GetAssignedSlot()) {
+      aContent = slot;
+    } else if (ShadowRoot* shadowRoot = aContent->GetContainingShadow()) {
+      aContent = shadowRoot->Host();
+      topLevelhost = aContent;
+    } else {
+      aContent = aContent->GetParent();
+    }
+  }
+
+  return topLevelhost;
+}
+
 nsresult
 nsFocusManager::GetNextTabbableContent(nsIPresShell* aPresShell,
                                        nsIContent* aRootContent,
                                        nsIContent* aOriginalStartContent,
                                        nsIContent* aStartContent,
                                        bool aForward,
                                        int32_t aCurrentTabIndex,
                                        bool aIgnoreTabIndex,
@@ -3467,16 +3485,18 @@ nsFocusManager::GetNextTabbableContent(n
                                        nsIContent** aResultContent)
 {
   *aResultContent = nullptr;
 
   nsCOMPtr<nsIContent> startContent = aStartContent;
   if (!startContent)
     return NS_OK;
 
+  nsIContent* currentTopLevelHost = GetTopLevelHost(aStartContent);
+
   LOGCONTENTNAVIGATION("GetNextTabbable: %s", aStartContent);
   LOGFOCUSNAVIGATION(("  tabindex: %d", aCurrentTabIndex));
 
   if (nsDocument::IsShadowDOMEnabled(aRootContent)) {
     // If aStartContent is a shadow host or slot in forward navigation,
     // search in scope owned by aStartContent
     if (aForward && IsHostOrSlot(aStartContent)) {
       nsIContent* contentToFocus =
@@ -3547,18 +3567,17 @@ nsFocusManager::GetNextTabbableContent(n
     nsCOMPtr<nsIFrameEnumerator> frameTraversal;
     nsresult rv = NS_NewFrameTraversal(getter_AddRefs(frameTraversal),
                                        presContext, startFrame,
                                        ePreOrder,
                                        false, // aVisual
                                        false, // aLockInScrollView
                                        true,  // aFollowOOFs
                                        aForDocumentNavigation,  // aSkipPopupChecks
-                                       nsDocument::IsShadowDOMEnabled(aRootContent) // aSkipShadow
-                                       );
+                                       false);
     NS_ENSURE_SUCCESS(rv, rv);
 
     if (iterStartContent == aRootContent) {
       if (!aForward) {
         frameTraversal->Last();
       } else if (aRootContent->IsFocusable()) {
         frameTraversal->Next();
       }
@@ -3572,17 +3591,39 @@ nsFocusManager::GetNextTabbableContent(n
         frameTraversal->Next();
       else
         frameTraversal->Prev();
     }
 
     // Walk frames to find something tabbable matching mCurrentTabIndex
     nsIFrame* frame = static_cast<nsIFrame*>(frameTraversal->CurrentItem());
     while (frame) {
+      // Try to find the topmost Shadow DOM host, since we want to
+      // skip Shadow DOM in frame traversal.
       nsIContent* currentContent = frame->GetContent();
+      nsIContent* oldTopLevelHost = currentTopLevelHost;
+      nsIContent* topLevel = GetTopLevelHost(currentContent);
+      currentTopLevelHost = topLevel;
+      if (topLevel) {
+        if (topLevel == oldTopLevelHost) {
+          // We're within Shadow DOM, continue.
+          do {
+            if (aForward) {
+              frameTraversal->Next();
+            } else {
+              frameTraversal->Prev();
+            }
+            frame = static_cast<nsIFrame*>(frameTraversal->CurrentItem());
+            // For the usage of GetPrevContinuation, see the comment
+            // at the end of while (frame) loop.
+          } while (frame && frame->GetPrevContinuation());
+          continue;
+        }
+        currentContent = topLevel;
+      }
 
       // For document navigation, check if this element is an open panel. Since
       // panels aren't focusable (tabIndex would be -1), we'll just assume that
       // for document navigation, the tabIndex is 0.
       if (aForDocumentNavigation && currentContent && (aCurrentTabIndex == 0) &&
           currentContent->IsXULElement(nsGkAtoms::panel)) {
         nsMenuPopupFrame* popupFrame = do_QueryFrame(frame);
         // Check if the panel is open. Closed panels are ignored since you can't
--- a/dom/base/test/file_bug1453693.html
+++ b/dom/base/test/file_bug1453693.html
@@ -215,23 +215,58 @@
         opener.is(lastFocusTarget, input11, "[nested shadow] Should have focused input element. (7)");
 
         synthesizeKey("KEY_Tab", {shiftKey: true});
         opener.is(lastFocusTarget, button, "[nested shadow] Should have focused button element. (8)");
 
         // Back to beginning, outside of Shadow DOM.
         synthesizeKey("KEY_Tab", {shiftKey: true});
         opener.is(document.activeElement, document.body.firstChild, "body's first child should have focus. (2)");
+
+        host.remove();
+      }
+
+      function testTabbingThroughDisplayContentsHost() {
+        opener.is(document.activeElement, document.body.firstChild, "body's first child should have focus. (1)");
+
+        var host = document.createElement("div");
+        host.id = "host";
+        host.setAttribute("style", "display: contents; border: 1px solid black;");
+        document.body.appendChild(host);
+
+        var sr0 = host.attachShadow({mode: "open"});
+        sr0.innerHTML = "<input id='shadowInput1'><input id='shadowInput2'>";
+        var shadowInput1 = sr0.getElementById("shadowInput1");
+        shadowInput1.onfocus = focusLogger;
+        var shadowInput2 = sr0.getElementById("shadowInput2");
+        shadowInput2.onfocus = focusLogger;
+
+        document.body.offsetLeft;
+
+        synthesizeKey("KEY_Tab");
+        opener.is(lastFocusTarget, shadowInput1, "Should have focused input element. (1)");
+
+        synthesizeKey("KEY_Tab");
+        opener.is(lastFocusTarget, shadowInput2, "Should have focused input element. (2)");
+
+        // Backwards
+        synthesizeKey("KEY_Tab", {shiftKey: true});
+        opener.is(lastFocusTarget, shadowInput1, "Should have focused input element. (3)");
+
+        // Back to beginning, outside of Shadow DOM.
+        synthesizeKey("KEY_Tab", {shiftKey: true});
+        opener.is(document.activeElement, document.body.firstChild, "body's first child should have focus. (2)");
       }
 
       function runTest() {
 
         testTabbingThroughShadowDOMWithTabIndexes();
         testTabbingThroughSimpleShadowDOM();
         testTabbingThroughNestedShadowDOM();
+        testTabbingThroughDisplayContentsHost();
 
         opener.didRunTests();
         window.close();
       }
 
       function init() {
         SimpleTest.waitForFocus(runTest);
       }