Bug 1430020, let sequential focus navigation in shadow DOM enter iframes, r=mrbkap
authorOlli Pettay <Olli.Pettay@helsinki.fi>
Sat, 28 Apr 2018 16:07:06 +0300
changeset 472260 5bde59ad4082e94f3deea06808152b3ad0077876
parent 472259 996616295128d61343f50270fe79b3ee49b9579b
child 472261 128fff8050cc13ebcddbc1adab9eae0a6f8057cf
child 472266 08f68e2c892cadc4035ecbfbf3529f32d40f1fd9
push id1728
push userjlund@mozilla.com
push dateMon, 18 Jun 2018 21:12:27 +0000
treeherdermozilla-release@c296fde26f5f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmrbkap
bugs1430020
milestone61.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1430020, let sequential focus navigation in shadow DOM enter iframes, r=mrbkap
dom/base/nsContentUtils.cpp
dom/base/nsFocusManager.cpp
dom/base/nsFocusManager.h
dom/base/test/file_bug1453693.html
--- a/dom/base/nsContentUtils.cpp
+++ b/dom/base/nsContentUtils.cpp
@@ -6688,19 +6688,17 @@ nsContentUtils::IsFocusedContent(const n
   nsFocusManager* fm = nsFocusManager::GetFocusManager();
 
   return fm && fm->GetFocusedElement() == aContent;
 }
 
 bool
 nsContentUtils::IsSubDocumentTabbable(nsIContent* aContent)
 {
-  //XXXsmaug Shadow DOM spec issue!
-  //         We may need to change this to GetComposedDoc().
-  nsIDocument* doc = aContent->GetUncomposedDoc();
+  nsIDocument* doc = aContent->GetComposedDoc();
   if (!doc) {
     return false;
   }
 
   // If the subdocument lives in another process, the frame is
   // tabbable.
   if (EventStateManager::IsRemoteTarget(aContent)) {
     return true;
--- a/dom/base/nsFocusManager.cpp
+++ b/dom/base/nsFocusManager.cpp
@@ -3234,19 +3234,21 @@ nsFocusManager::HostOrSlotTabIndexValue(
   }
 
   return -1;
 }
 
 nsIContent*
 nsFocusManager::GetNextTabbableContentInScope(nsIContent* aOwner,
                                               nsIContent* aStartContent,
+                                              nsIContent* aOriginalStartContent,
                                               bool aForward,
                                               int32_t aCurrentTabIndex,
                                               bool aIgnoreTabIndex,
+                                              bool aForDocumentNavigation,
                                               bool aSkipOwner)
 {
   // Return shadow host at first for forward navigation if its tabindex
   // is non-negative
   bool skipOwner = aSkipOwner || !aOwner->GetShadowRoot();
   if (!skipOwner && (aForward && aOwner == aStartContent)) {
     int32_t tabIndex = 0;
     aOwner->IsFocusable(&tabIndex);
@@ -3356,24 +3358,43 @@ nsFocusManager::GetNextTabbableContentIn
             }
           }
         }
 
         continue;
       }
 
       if (!IsHostOrSlot(iterContent)) {
+        nsCOMPtr<nsIContent> elementInFrame;
+        bool checkSubDocument = true;
+        if (aForDocumentNavigation &&
+            TryDocumentNavigation(iterContent, &checkSubDocument,
+                                  getter_AddRefs(elementInFrame))) {
+          return elementInFrame;
+        }
+        if (!checkSubDocument) {
+          continue;
+        }
+
+        if (TryToMoveFocusToSubDocument(iterContent, aOriginalStartContent,
+                                        aForward, aForDocumentNavigation,
+                                        getter_AddRefs(elementInFrame))) {
+          return elementInFrame;
+        }
+
         // Found content to focus
         return iterContent;
       }
 
       // Search in scope owned by iterContent
       nsIContent* contentToFocus =
-        GetNextTabbableContentInScope(iterContent, iterContent, aForward,
+        GetNextTabbableContentInScope(iterContent, iterContent,
+                                      aOriginalStartContent, aForward,
                                       aForward ? 1 : 0, aIgnoreTabIndex,
+                                      aForDocumentNavigation,
                                       false /* aSkipOwner */);
       if (contentToFocus) {
         return contentToFocus;
       }
     };
 
     // If already at lowest priority tab (0), end search completely.
     // A bit counterintuitive but true, tabindex order goes 1, 2, ... 32767, 0
@@ -3397,30 +3418,33 @@ nsFocusManager::GetNextTabbableContentIn
   }
 
   return nullptr;
 }
 
 nsIContent*
 nsFocusManager::GetNextTabbableContentInAncestorScopes(
   nsIContent** aStartContent,
+  nsIContent* aOriginalStartContent,
   bool aForward,
   int32_t* aCurrentTabIndex,
-  bool aIgnoreTabIndex)
+  bool aIgnoreTabIndex,
+  bool aForDocumentNavigation)
 {
   nsIContent* startContent = *aStartContent;
   while (1) {
     nsIContent* owner = FindOwner(startContent);
     MOZ_ASSERT(owner, "focus navigation scope owner not in document");
 
     int32_t tabIndex = 0;
     startContent->IsFocusable(&tabIndex);
     nsIContent* contentToFocus =
-      GetNextTabbableContentInScope(owner, startContent, aForward,
-                                    tabIndex, aIgnoreTabIndex,
+      GetNextTabbableContentInScope(owner, startContent, aOriginalStartContent,
+                                    aForward, tabIndex, aIgnoreTabIndex,
+                                    aForDocumentNavigation,
                                     false /* aSkipOwner */);
     if (contentToFocus) {
       return contentToFocus;
     }
 
     // If not found in shadow DOM, search from the shadow host in light DOM
     if (!owner->IsInShadowTree()) {
       MOZ_ASSERT(owner->GetShadowRoot());
@@ -3456,33 +3480,38 @@ nsFocusManager::GetNextTabbableContent(n
   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 =
-        GetNextTabbableContentInScope(aStartContent, aStartContent, aForward,
+        GetNextTabbableContentInScope(aStartContent, aStartContent,
+                                      aOriginalStartContent, aForward,
                                       aForward ? 1 : 0, aIgnoreTabIndex,
+                                      aForDocumentNavigation,
                                       true /* aSkipOwner */);
       if (contentToFocus) {
         NS_ADDREF(*aResultContent = contentToFocus);
         return NS_OK;
       }
     }
 
     // If aStartContent is not in scope owned by aRootContent
     // (e.g., aStartContent is already in shadow DOM),
     // search from scope including aStartContent
     if (aRootContent != FindOwner(aStartContent)) {
       nsIContent* contentToFocus =
-        GetNextTabbableContentInAncestorScopes(&aStartContent, aForward,
+        GetNextTabbableContentInAncestorScopes(&aStartContent,
+                                               aOriginalStartContent,
+                                               aForward,
                                                &aCurrentTabIndex,
-                                               aIgnoreTabIndex);
+                                               aIgnoreTabIndex,
+                                               aForDocumentNavigation);
       if (contentToFocus) {
         NS_ADDREF(*aResultContent = contentToFocus);
         return NS_OK;
       }
     }
 
     // If we reach here, it means no next tabbable content in shadow DOM.
     // We need to continue searching in light DOM, starting at the shadow host
@@ -3604,18 +3633,20 @@ nsFocusManager::GetNextTabbableContent(n
       // and later in "For each element ELEMENT in NAVIGATION-ORDER: "
       // hosts and slots are handled before other elements.
       if (currentContent && nsDocument::IsShadowDOMEnabled(currentContent) &&
           IsHostOrSlot(currentContent)) {
         int32_t tabIndex = HostOrSlotTabIndexValue(currentContent);
         if (tabIndex >= 0 &&
             (aIgnoreTabIndex || aCurrentTabIndex == tabIndex)) {
           nsIContent* contentToFocus =
-            GetNextTabbableContentInScope(currentContent, currentContent, aForward,
+            GetNextTabbableContentInScope(currentContent, currentContent,
+                                          aOriginalStartContent, aForward,
                                           aForward ? 1 : 0, aIgnoreTabIndex,
+                                          aForDocumentNavigation,
                                           true /* aSkipOwner */);
           if (contentToFocus) {
             NS_ADDREF(*aResultContent = contentToFocus);
             return NS_OK;
           }
         }
       }
 
@@ -3662,64 +3693,31 @@ nsFocusManager::GetNextTabbableContent(n
           TabParent* remote = TabParent::GetFrom(currentContent);
           if (remote) {
             remote->NavigateByKey(aForward, aForDocumentNavigation);
             return NS_SUCCESS_DOM_NO_OPERATION;
           }
 
           // Next, for document navigation, check if this a non-remote child document.
           bool checkSubDocument = true;
-          if (aForDocumentNavigation) {
-            Element* docRoot = GetRootForChildDocument(currentContent);
-            if (docRoot) {
-              // If GetRootForChildDocument returned something then call
-              // FocusFirst to find the root or first element to focus within
-              // the child document. If this is a frameset though, skip this and
-              // fall through to the checkSubDocument block below to iterate into
-              // the frameset's frames and locate the first focusable frame.
-              if (!docRoot->IsHTMLElement(nsGkAtoms::frameset)) {
-                return FocusFirst(docRoot, aResultContent);
-              }
-            } else {
-              // Set checkSubDocument to false, as this was neither a frame
-              // type element or a child document that was focusable.
-              checkSubDocument = false;
-            }
+          if (aForDocumentNavigation &&
+              TryDocumentNavigation(currentContent, &checkSubDocument,
+                                    aResultContent)) {
+            return NS_OK;
           }
 
           if (checkSubDocument) {
             // found a node with a matching tab index. Check if it is a child
             // frame. If so, navigate into the child frame instead.
-            nsIDocument* doc = currentContent->GetComposedDoc();
-            NS_ASSERTION(doc, "content not in document");
-            nsIDocument* subdoc = doc->GetSubDocumentFor(currentContent);
-            if (subdoc && !subdoc->EventHandlingSuppressed()) {
-              if (aForward) {
-                // when tabbing forward into a frame, return the root
-                // frame so that the canvas becomes focused.
-                nsCOMPtr<nsPIDOMWindowOuter> subframe = subdoc->GetWindow();
-                if (subframe) {
-                  *aResultContent = GetRootForFocus(subframe, subdoc, false, true);
-                  if (*aResultContent) {
-                    NS_ADDREF(*aResultContent);
-                    return NS_OK;
-                  }
-                }
-              }
-              Element* rootElement = subdoc->GetRootElement();
-              nsIPresShell* subShell = subdoc->GetShell();
-              if (rootElement && subShell) {
-                rv = GetNextTabbableContent(subShell, rootElement,
-                                            aOriginalStartContent, rootElement,
-                                            aForward, (aForward ? 1 : 0),
-                                            false, aForDocumentNavigation, aResultContent);
-                NS_ENSURE_SUCCESS(rv, rv);
-                if (*aResultContent)
-                  return NS_OK;
-              }
+            if (TryToMoveFocusToSubDocument(currentContent,
+                                            aOriginalStartContent,
+                                            aForward, aForDocumentNavigation,
+                                            aResultContent)) {
+              MOZ_ASSERT(*aResultContent);
+              return NS_OK;
             }
             // otherwise, use this as the next content node to tab to, unless
             // this was the element we started on. This would happen for
             // instance on an element with child frames, where frame navigation
             // could return the original element again. In that case, just skip
             // it. Also, if the next content node is the root content, then
             // return it. This latter case would happen only if someone made a
             // popup focusable.
@@ -3733,18 +3731,20 @@ nsFocusManager::GetNextTabbableContent(n
 
               if (nsDocument::IsShadowDOMEnabled(aRootContent)) {
                 // If currentContent is a shadow host in backward
                 // navigation, search in scope owned by currentContent
                 if (!aForward && currentContent->GetShadowRoot()) {
                   nsIContent* contentToFocus =
                     GetNextTabbableContentInScope(currentContent,
                                                   currentContent,
+                                                  aOriginalStartContent,
                                                   aForward, aForward ? 1 : 0,
                                                   aIgnoreTabIndex,
+                                                  aForDocumentNavigation,
                                                   true /* aSkipOwner */);
                   if (contentToFocus) {
                     NS_ADDREF(*aResultContent = contentToFocus);
                     return NS_OK;
                   }
                 }
               }
 
@@ -3798,16 +3798,83 @@ nsFocusManager::GetNextTabbableContent(n
     // continue looking for next highest priority tabindex
     aCurrentTabIndex = GetNextTabIndex(aRootContent, aCurrentTabIndex, aForward);
     startContent = iterStartContent = aRootContent;
   }
 
   return NS_OK;
 }
 
+bool
+nsFocusManager::TryDocumentNavigation(nsIContent* aCurrentContent,
+                                      bool* aCheckSubDocument,
+                                      nsIContent** aResultContent)
+{
+  *aCheckSubDocument = true;
+  Element* docRoot = GetRootForChildDocument(aCurrentContent);
+  if (docRoot) {
+    // If GetRootForChildDocument returned something then call
+    // FocusFirst to find the root or first element to focus within
+    // the child document. If this is a frameset though, skip this and
+    // fall through to normal tab navigation to iterate into
+    // the frameset's frames and locate the first focusable frame.
+    if (!docRoot->IsHTMLElement(nsGkAtoms::frameset)) {
+      *aCheckSubDocument = false;
+      Unused << FocusFirst(docRoot, aResultContent);
+      return *aResultContent != nullptr;
+    }
+  } else {
+    // Set aCheckSubDocument to false, as this was neither a frame
+    // type element or a child document that was focusable.
+    *aCheckSubDocument = false;
+  }
+
+  return false;
+}
+
+bool
+nsFocusManager::TryToMoveFocusToSubDocument(nsIContent* aCurrentContent,
+                                            nsIContent* aOriginalStartContent,
+                                            bool aForward,
+                                            bool aForDocumentNavigation,
+                                            nsIContent** aResultContent)
+{
+  nsIDocument* doc = aCurrentContent->GetComposedDoc();
+  NS_ASSERTION(doc, "content not in document");
+  nsIDocument* subdoc = doc->GetSubDocumentFor(aCurrentContent);
+  if (subdoc && !subdoc->EventHandlingSuppressed()) {
+    if (aForward) {
+      // when tabbing forward into a frame, return the root
+      // frame so that the canvas becomes focused.
+      nsCOMPtr<nsPIDOMWindowOuter> subframe = subdoc->GetWindow();
+      if (subframe) {
+        *aResultContent = GetRootForFocus(subframe, subdoc, false, true);
+        if (*aResultContent) {
+          NS_ADDREF(*aResultContent);
+          return true;
+        }
+      }
+    }
+    Element* rootElement = subdoc->GetRootElement();
+    nsIPresShell* subShell = subdoc->GetShell();
+    if (rootElement && subShell) {
+      nsresult rv = GetNextTabbableContent(subShell, rootElement,
+                                           aOriginalStartContent, rootElement,
+                                           aForward, (aForward ? 1 : 0),
+                                           false, aForDocumentNavigation,
+                                           aResultContent);
+      NS_ENSURE_SUCCESS(rv, false);
+      if (*aResultContent) {
+        return true;
+      }
+    }
+  }
+  return false;
+}
+
 nsIContent*
 nsFocusManager::GetNextTabbableMapArea(bool aForward,
                                        int32_t aCurrentTabIndex,
                                        Element* aImageContent,
                                        nsIContent* aStartContent)
 {
   nsAutoString useMap;
   aImageContent->GetAttr(kNameSpaceID_None, nsGkAtoms::usemap, useMap);
--- a/dom/base/nsFocusManager.h
+++ b/dom/base/nsFocusManager.h
@@ -457,67 +457,83 @@ protected:
   /**
    * Retrieve the next tabbable element in scope owned by aOwner, using
    * focusability and tabindex to determine the tab order.
    *
    * aOwner is the owner of scope to search in.
    *
    * aStartContent is the starting point for this call of this method.
    *
+   * aOriginalStartContent is the initial starting point for sequential
+   * navigation.
+   *
    * aForward should be true for forward navigation or false for backward
    * navigation.
    *
    * aCurrentTabIndex is the current tabindex.
    *
    * aIgnoreTabIndex to ignore the current tabindex and find the element
    * irrespective or the tab index.
    *
+   * aForDocumentNavigation informs whether we're navigating only through
+   * documents.
+   *
    * aSkipOwner to skip owner while searching. The flag is set when caller is
    * |GetNextTabbableContent| in order to let caller handle owner.
    *
    * NOTE:
    *   Consider the method searches downwards in flattened subtree
    *   rooted at aOwner.
    */
   nsIContent* GetNextTabbableContentInScope(nsIContent* aOwner,
                                             nsIContent* aStartContent,
+                                            nsIContent* aOriginalStartContent,
                                             bool aForward,
                                             int32_t aCurrentTabIndex,
                                             bool aIgnoreTabIndex,
+                                            bool aForDocumentNavigation,
                                             bool aSkipOwner);
 
   /**
    * Retrieve the next tabbable element in scope including aStartContent
    * and the scope's ancestor scopes, using focusability and tabindex to
    * determine the tab order.
    *
    * aStartContent an in/out paremeter. It as input is the starting point
    * for this call of this method; as output it is the shadow host in
    * light DOM if the next tabbable element is not found in shadow DOM,
    * in order to continue searching in light DOM.
    *
+   * aOriginalStartContent is the initial starting point for sequential
+   * navigation.
+   *
    * aForward should be true for forward navigation or false for backward
    * navigation.
    *
    * aCurrentTabIndex returns tab index of shadow host in light DOM if the
    * next tabbable element is not found in shadow DOM, in order to continue
    * searching in light DOM.
    *
    * aIgnoreTabIndex to ignore the current tabindex and find the element
    * irrespective or the tab index.
    *
+   * aForDocumentNavigation informs whether we're navigating only through
+   * documents.
+   *
    * NOTE:
    *   Consider the method searches upwards in all shadow host- or slot-rooted
    *   flattened subtrees that contains aStartContent as non-root, except
    *   the flattened subtree rooted at shadow host in light DOM.
    */
   nsIContent* GetNextTabbableContentInAncestorScopes(nsIContent** aStartContent,
+                                                     nsIContent* aOriginalStartContent,
                                                      bool aForward,
                                                      int32_t* aCurrentTabIndex,
-                                                     bool aIgnoreTabIndex);
+                                                     bool aIgnoreTabIndex,
+                                                     bool aForDocumentNavigation);
 
   /**
    * Retrieve the next tabbable element within a document, using focusability
    * and tabindex to determine the tab order. The element is returned in
    * aResultContent.
    *
    * aRootContent is the root node -- nodes above this will not be examined.
    * Typically this will be the root node of a document, but could also be
@@ -629,16 +645,26 @@ private:
   // wrong..
   static void NotifyFocusStateChange(nsIContent* aContent,
                                      nsIContent* aContentToFocus,
                                      bool aWindowShouldShowFocusRing,
                                      bool aGettingFocus);
 
   void SetFocusedWindowInternal(nsPIDOMWindowOuter* aWindow);
 
+  bool TryDocumentNavigation(nsIContent* aCurrentContent,
+                             bool* aCheckSubDocument,
+                             nsIContent** aResultContent);
+
+  bool TryToMoveFocusToSubDocument(nsIContent* aCurrentContent,
+                                   nsIContent* aOriginalStartContent,
+                                   bool aForward,
+                                   bool aForDocumentNavigation,
+                                   nsIContent** aResultContent);
+
   // the currently active and front-most top-most window
   nsCOMPtr<nsPIDOMWindowOuter> mActiveWindow;
 
   // the child or top-level window that is currently focused. This window will
   // either be the same window as mActiveWindow or a descendant of it.
   // Except during shutdown use SetFocusedWindowInternal to set mFocusedWindow!
   nsCOMPtr<nsPIDOMWindowOuter> mFocusedWindow;
 
--- a/dom/base/test/file_bug1453693.html
+++ b/dom/base/test/file_bug1453693.html
@@ -32,16 +32,21 @@
         sr.appendChild(shadowInput);
 
         var shadowDate = document.createElement("input");
         shadowDate.type = "date";
         shadowDate.onfocus = focusLogger;
         shadowDate.tabIndex = 1;
         sr.appendChild(shadowDate);
 
+        var shadowIframe = document.createElement("iframe");
+        shadowIframe.tabIndex = 1;
+        sr.appendChild(shadowIframe);
+        shadowIframe.contentDocument.body.innerHTML = "<input>";
+
         var input = document.createElement("input");
         input.onfocus = focusLogger;
         input.tabIndex = 1;
         document.body.appendChild(input);
 
         var input2 = document.createElement("input");
         input2.onfocus = focusLogger;
         document.body.appendChild(input2);
@@ -56,24 +61,40 @@
         opener.is(lastFocusTarget, shadowInput, "Should have focused input element in shadow DOM. (3)");
         synthesizeKey("KEY_Tab");
         opener.is(lastFocusTarget, shadowDate, "Should have focused date element in shadow DOM. (3)");
         synthesizeKey("KEY_Tab");
         opener.is(lastFocusTarget, shadowDate, "Should have focused date element in shadow DOM. (3)");
         synthesizeKey("KEY_Tab");
         opener.is(lastFocusTarget, shadowDate, "Should have focused date element in shadow DOM. (3)");
         synthesizeKey("KEY_Tab");
+        opener.is(shadowIframe.contentDocument.activeElement,
+                  shadowIframe.contentDocument.documentElement,
+                  "Should have focused document element in shadow iframe. (3)");
+        synthesizeKey("KEY_Tab");
+        opener.is(shadowIframe.contentDocument.activeElement,
+                  shadowIframe.contentDocument.body.firstChild,
+                  "Should have focused input element in shadow iframe. (3)");
+        synthesizeKey("KEY_Tab");
         opener.is(lastFocusTarget, shadowAnchor, "Should have focused anchor element in shadow DOM. (3)");
         synthesizeKey("KEY_Tab");
         opener.is(lastFocusTarget, input2, "Should have focused input[2] element. (3)");
 
         // Backwards
         synthesizeKey("KEY_Tab", {shiftKey: true});
         opener.is(lastFocusTarget, shadowAnchor, "Should have focused anchor element in shadow DOM. (4)");
         synthesizeKey("KEY_Tab", {shiftKey: true});
+        opener.is(shadowIframe.contentDocument.activeElement,
+                  shadowIframe.contentDocument.body.firstChild,
+                  "Should have focused input element in shadow iframe. (4)");
+        synthesizeKey("KEY_Tab", {shiftKey: true});
+        opener.is(shadowIframe.contentDocument.activeElement,
+                  shadowIframe.contentDocument.documentElement,
+                  "Should have focused document element in shadow iframe. (4)");
+        synthesizeKey("KEY_Tab", {shiftKey: true});
         opener.is(lastFocusTarget, shadowDate, "Should have focused date element in shadow DOM. (4)");
         synthesizeKey("KEY_Tab", {shiftKey: true});
         opener.is(lastFocusTarget, shadowDate, "Should have focused date element in shadow DOM. (4)");
         synthesizeKey("KEY_Tab", {shiftKey: true});
         opener.is(lastFocusTarget, shadowDate, "Should have focused date element in shadow DOM. (4)");
         synthesizeKey("KEY_Tab", {shiftKey: true});
         opener.is(lastFocusTarget, shadowInput, "Should have focused input element in shadow DOM. (4)");
         synthesizeKey("KEY_Tab", {shiftKey: true});