Bug 1453693 - Ensure sequential focus navigation works in Shadow DOM and add some tests, r=mrbkap
authorOlli Pettay <Olli.Pettay@helsinki.fi>
Sun, 22 Apr 2018 14:25:38 +0300
changeset 414997 116a49e26695aab8fb9c5b241da2067c3c7cd8a8
parent 414996 6c222e89103d4c70c58bf1955ab81cef8e8b62e1
child 414998 ebdbae0764b8b7dcd221351d357ba55db1320e11
push id33885
push usertoros@mozilla.com
push dateSun, 22 Apr 2018 22:12:17 +0000
treeherdermozilla-central@378a8a64401f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmrbkap
bugs1453693
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 1453693 - Ensure sequential focus navigation works in Shadow DOM and add some tests, r=mrbkap
dom/base/nsFocusManager.cpp
dom/base/nsFocusManager.h
dom/base/test/file_bug1453693.html
dom/base/test/mochitest.ini
dom/base/test/test_bug1453693.html
--- a/dom/base/nsFocusManager.cpp
+++ b/dom/base/nsFocusManager.cpp
@@ -3214,16 +3214,34 @@ nsFocusManager::FindOwner(nsIContent* aC
 
 bool
 nsFocusManager::IsHostOrSlot(nsIContent* aContent)
 {
   return aContent->GetShadowRoot() || // shadow host
          aContent->IsHTMLElement(nsGkAtoms::slot); // slot
 }
 
+int32_t
+nsFocusManager::HostOrSlotTabIndexValue(nsIContent* aContent)
+{
+  MOZ_ASSERT(IsHostOrSlot(aContent));
+
+  const nsAttrValue* attrVal =
+    aContent->AsElement()->GetParsedAttr(nsGkAtoms::tabindex);
+  if (!attrVal) {
+    return 0;
+  }
+
+  if (attrVal->Type() == nsAttrValue::eInteger) {
+    return attrVal->GetIntegerValue();
+  }
+
+  return -1;
+}
+
 nsIContent*
 nsFocusManager::GetNextTabbableContentInScope(nsIContent* aOwner,
                                               nsIContent* aStartContent,
                                               bool aForward,
                                               int32_t aCurrentTabIndex,
                                               bool aIgnoreTabIndex,
                                               bool aSkipOwner)
 {
@@ -3325,17 +3343,17 @@ nsFocusManager::GetNextTabbableContentIn
       return contentToFocus;
     }
 
     // If not found in shadow DOM, search from the shadow host in light DOM
     if (!owner->IsInShadowTree()) {
       MOZ_ASSERT(owner->GetShadowRoot());
 
       *aStartContent = owner;
-      owner->IsFocusable(aCurrentTabIndex);
+      *aCurrentTabIndex = HostOrSlotTabIndexValue(owner);
       break;
     }
 
     startContent = owner;
   }
 
   return nullptr;
 }
@@ -3495,16 +3513,39 @@ nsFocusManager::GetNextTabbableContent(n
                                         aResultContent);
             if (NS_SUCCEEDED(rv) && *aResultContent) {
               return rv;
             }
           }
         }
       }
 
+      // As of now, 2018/04/12, sequential focus navigation is still
+      // in the obsolete Shadow DOM specification.
+      // http://w3c.github.io/webcomponents/spec/shadow/#sequential-focus-navigation
+      // "if ELEMENT is focusable, a shadow host, or a slot element,
+      //  append ELEMENT to NAVIGATION-ORDER."
+      // 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,
+                                          aForward ? 1 : 0, aIgnoreTabIndex,
+                                          true /* aSkipOwner */);
+          if (contentToFocus) {
+            NS_ADDREF(*aResultContent = contentToFocus);
+            return NS_OK;
+          }
+        }
+      }
+
       // TabIndex not set defaults to 0 for form elements, anchors and other
       // elements that are normally focusable. Tabindex defaults to -1
       // for elements that are not normally focusable.
       // The returned computed tabindex from IsFocusable() is as follows:
       //          < 0 not tabbable at all
       //          == 0 in normal tab order (last after positive tabindexed items)
       //          > 0 can be tabbed to in the order specified by this value
       int32_t tabIndex;
--- a/dom/base/nsFocusManager.h
+++ b/dom/base/nsFocusManager.h
@@ -439,16 +439,23 @@ protected:
   nsIContent* FindOwner(nsIContent* aContent);
 
   /**
    * Returns true if aContent is a shadow host or slot
    */
   bool IsHostOrSlot(nsIContent* aContent);
 
   /**
+   * Host and Slot elements need to be handled as if they had tabindex 0 even
+   * when they don't have the attribute. This is a helper method to get the right
+   * value for focus navigation.
+   */
+  int32_t HostOrSlotTabIndexValue(nsIContent* aContent);
+
+  /**
    * 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.
    *
    * aForward should be true for forward navigation or false for backward
new file mode 100644
--- /dev/null
+++ b/dom/base/test/file_bug1453693.html
@@ -0,0 +1,137 @@
+<html>
+  <head>
+    <title>Test for Bug 1453693</title>
+    <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+    <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+    <script>
+
+      var lastFocusTarget;
+      function focusLogger(event) {
+        lastFocusTarget = event.target;
+        console.log(event.target + " under " + event.target.parentNode);
+      }
+
+      function testTabbingThroughShadowDOMWithTabIndexes() {
+        var anchor = document.createElement("a");
+        anchor.onfocus = focusLogger;
+        anchor.href = "#";
+        anchor.textContent = "in light DOM";
+        document.body.appendChild(anchor);
+
+        var host = document.createElement("div");
+        document.body.appendChild(host);
+
+        var sr = host.attachShadow({mode: "open"});
+        var shadowAnchor = anchor.cloneNode(false);
+        shadowAnchor.onfocus = focusLogger;
+        shadowAnchor.textContent = "in shadow DOM";
+        sr.appendChild(shadowAnchor);
+        var shadowInput = document.createElement("input");
+        shadowInput.onfocus = focusLogger;
+        shadowInput.tabIndex = 1;
+        sr.appendChild(shadowInput);
+
+        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);
+
+        document.body.offsetLeft;
+
+        synthesizeKey("KEY_Tab");
+        opener.is(lastFocusTarget, input, "Should have focused input element. (3)");
+        synthesizeKey("KEY_Tab");
+        opener.is(lastFocusTarget, anchor, "Should have focused anchor element. (3)");
+        synthesizeKey("KEY_Tab");
+        opener.is(lastFocusTarget, shadowInput, "Should have focused input element in shadow DOM. (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(lastFocusTarget, shadowInput, "Should have focused input element in shadow DOM. (4)");
+        synthesizeKey("KEY_Tab", {shiftKey: true});
+        opener.is(lastFocusTarget, anchor, "Should have focused anchor element. (4)");
+        synthesizeKey("KEY_Tab", {shiftKey: true});
+        opener.is(lastFocusTarget, input, "Should have focused input element. (4)");
+
+        document.body.innerHTML = null;
+      }
+
+      function testTabbingThroughSimpleShadowDOM() {
+        var anchor = document.createElement("a");
+        anchor.onfocus = focusLogger;
+        anchor.href = "#";
+        anchor.textContent = "in light DOM";
+        document.body.appendChild(anchor);
+        anchor.focus();
+
+        var host = document.createElement("div");
+        document.body.appendChild(host);
+
+        var sr = host.attachShadow({mode: "open"});
+        var shadowAnchor = anchor.cloneNode(false);
+        shadowAnchor.onfocus = focusLogger;
+        shadowAnchor.textContent = "in shadow DOM";
+        sr.appendChild(shadowAnchor);
+        var shadowInput = document.createElement("input");
+        shadowInput.onfocus = focusLogger;
+        sr.appendChild(shadowInput);
+
+        var input = document.createElement("input");
+        input.onfocus = focusLogger;
+        document.body.appendChild(input);
+
+        var input2 = document.createElement("input");
+        input2.onfocus = focusLogger;
+        document.body.appendChild(input2);
+
+        document.body.offsetLeft;
+
+        synthesizeKey("KEY_Tab");
+        opener.is(lastFocusTarget, shadowAnchor, "Should have focused anchor element in shadow DOM.");
+        synthesizeKey("KEY_Tab");
+        opener.is(lastFocusTarget, shadowInput, "Should have focused input element in shadow DOM.");
+        synthesizeKey("KEY_Tab");
+        opener.is(lastFocusTarget, input, "Should have focused input element.");
+        synthesizeKey("KEY_Tab");
+        opener.is(lastFocusTarget, input2, "Should have focused input[2] element.");
+
+        // Backwards
+        synthesizeKey("KEY_Tab", {shiftKey: true});
+        opener.is(lastFocusTarget, input, "Should have focused input element. (2)");
+        synthesizeKey("KEY_Tab", {shiftKey: true});
+        opener.is(lastFocusTarget, shadowInput, "Should have focused input element in shadow DOM. (2)");
+        synthesizeKey("KEY_Tab", {shiftKey: true});
+        opener.is(lastFocusTarget, shadowAnchor, "Should have focused anchor element in shadow DOM. (2)");
+        synthesizeKey("KEY_Tab", {shiftKey: true});
+        opener.is(lastFocusTarget, anchor, "Should have focused anchor element. (2)");
+      }
+
+      function runTest() {
+
+        testTabbingThroughShadowDOMWithTabIndexes();
+        testTabbingThroughSimpleShadowDOM();
+
+        opener.didRunTests();
+        window.close();
+      }
+
+      function init() {
+        SimpleTest.waitForFocus(runTest);
+      }
+    </script>
+    <style>
+    </style>
+  </head>
+  <body onload="init()">
+  </body>
+</html>
--- a/dom/base/test/mochitest.ini
+++ b/dom/base/test/mochitest.ini
@@ -121,16 +121,17 @@ support-files =
   file_bug769117.html
   file_bug782342.txt
   file_bug787778.sjs
   file_bug869432.eventsource
   file_bug869432.eventsource^headers^
   file_bug907892.html
   file_bug945152.jar
   file_bug1274806.html
+  file_bug1453693.html
   file_domwindowutils_animation.html
   file_general_document.html
   file_history_document_open.html
   file_htmlserializer_1.html
   file_htmlserializer_1_bodyonly.html
   file_htmlserializer_1_format.html
   file_htmlserializer_1_linebreak.html
   file_htmlserializer_1_links.html
@@ -607,16 +608,18 @@ skip-if = toolkit == 'android'
 [test_bug1318303.html]
 [test_bug1375050.html]
 [test_bug1381710.html]
 [test_bug1384661.html]
 [test_bug1399605.html]
 [test_bug1404385.html]
 [test_bug1406102.html]
 [test_bug1421568.html]
+[test_bug1453693.html]
+skip-if = os == "mac"
 [test_caretPositionFromPoint.html]
 [test_change_policy.html]
 [test_clearTimeoutIntervalNoArg.html]
 [test_constructor-assignment.html]
 [test_constructor.html]
 [test_copyimage.html]
 subsuite = clipboard
 skip-if = toolkit == 'android' #bug 904183
new file mode 100644
--- /dev/null
+++ b/dom/base/test/test_bug1453693.html
@@ -0,0 +1,40 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1453693
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test for Bug 1453693</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+  <script type="application/javascript">
+
+  /** Test for Bug 1453693 **/
+
+  SimpleTest.waitForExplicitFinish();
+
+  function init() {
+    SpecialPowers.pushPrefEnv(
+      {
+        "set": [["dom.webcomponents.shadowdom.enabled", true]]
+      },
+      runTests);
+  }
+
+  function runTests() {
+    win = window.open("file_bug1453693.html", "", "width=300, height=300");
+  }
+
+  function didRunTests() {
+    setTimeout("SimpleTest.finish()");
+  }
+
+  SimpleTest.waitForFocus(init);
+
+  </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1453693">Mozilla Bug 1453693</a>
+</body>
+</html>