Bug 1453693 - Ensure sequential focus navigation works in Shadow DOM and add some tests, r=mrbkap
authorOlli Pettay <Olli.Pettay@helsinki.fi>
Sat, 21 Apr 2018 22:35:30 +0300
changeset 1491735 a39def43cc5153c4debecffc305510d084029f73
parent 1491734 9dcde577687cd5519b2156728f033c322e142a51
child 1491736 514ff6e1dc4e9001e76010ea10d7011593367d8f
push id266529
push useropettay@mozilla.com
push dateSat, 21 Apr 2018 20:56:30 +0000
treeherdertry@514ff6e1dc4e [default view] [failures only]
reviewersmrbkap
bugs1453693
milestone61.0a1
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,133 @@
+<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);
+
+        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);
+
+        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,17 @@ 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]
 [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>