Bug 1560359 - Add keyboard support for the menu on about:logins. r=sfoster,yzen
authorJared Wein <jwein@mozilla.com>
Wed, 03 Jul 2019 03:34:39 +0000
changeset 540711 f9bbfd8ef2b0cce32506daebfa787bdb00d8eb8c
parent 540710 2bfbb6633d3a12f097cab9f1c40f54288395d27c
child 540712 7512f4258344882296a97896d4c45fa44b2a38d3
push id11529
push userarchaeopteryx@coole-files.de
push dateThu, 04 Jul 2019 15:22:33 +0000
treeherdermozilla-beta@ebb510a784b8 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssfoster, yzen
bugs1560359
milestone69.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 1560359 - Add keyboard support for the menu on about:logins. r=sfoster,yzen Differential Revision: https://phabricator.services.mozilla.com/D35830
browser/components/aboutlogins/content/aboutLogins.html
browser/components/aboutlogins/content/components/menu-button.js
browser/components/aboutlogins/tests/chrome/test_menu_button.html
--- a/browser/components/aboutlogins/content/aboutLogins.html
+++ b/browser/components/aboutlogins/content/aboutLogins.html
@@ -144,19 +144,17 @@
     <template id="menu-button-template">
       <link rel="stylesheet" href="chrome://global/skin/in-content/common.css">
       <link rel="stylesheet" href="chrome://browser/content/aboutlogins/common.css">
       <link rel="stylesheet" href="chrome://browser/content/aboutlogins/components/menu-button.css">
       <button class="menu-button alternate-button"></button>
       <ul class="menu" role="menu" hidden>
         <button role="menuitem" class="menuitem-button menuitem-import alternate-button" hidden data-supported-platforms="Win32" data-event-name="AboutLoginsImport"></button>
         <button role="menuitem" class="menuitem-button menuitem-preferences alternate-button" data-event-name="AboutLoginsOpenPreferences"></button>
-        </li>
-        <li role="menuitem" class="menuitem">
-          <button class="menuitem-button menuitem-feedback alternate-button" data-event-name="AboutLoginsOpenFeedback"></button>
+        <button role="menuitem" class="menuitem-button menuitem-feedback alternate-button" data-event-name="AboutLoginsOpenFeedback"></button>
       </ul>
     </template>
 
     <template id="copy-to-clipboard-button-template">
       <link rel="stylesheet" href="chrome://global/skin/in-content/common.css">
       <link rel="stylesheet" href="chrome://browser/content/aboutlogins/components/copy-to-clipboard-button.css">
       <button class="copy-button">
         <span class="copied-button-text"></span>
--- a/browser/components/aboutlogins/content/components/menu-button.js
+++ b/browser/components/aboutlogins/content/components/menu-button.js
@@ -19,17 +19,19 @@ export default class MenuButton extends 
       if (supportedPlatforms.includes(navigator.platform)) {
         menuitem.hidden = false;
       }
     }
 
     this._menu = this.shadowRoot.querySelector(".menu");
     this._menuButton = this.shadowRoot.querySelector(".menu-button");
 
+    this.addEventListener("blur", this);
     this._menuButton.addEventListener("click", this);
+    this.addEventListener("keydown", this, true);
 
     super.connectedCallback();
   }
 
   static get reflectedFluentIDs() {
     return [
       "button-title",
       "menuitem-import",
@@ -49,16 +51,25 @@ export default class MenuButton extends 
     }
 
     this._menuButton.setAttribute("title", this.getAttribute(attrName));
     return true;
   }
 
   handleEvent(event) {
     switch (event.type) {
+      case "blur": {
+        if (event.relatedTarget &&
+            event.relatedTarget.closest(".menu") == this._menu) {
+          // Only hide the menu if focus has left the menu-button.
+          return;
+        }
+        this._hideMenu();
+        break;
+      }
       case "click": {
         // Skip the catch-all event listener if it was the menu-button
         // that was clicked on.
         if (event.currentTarget == document.documentElement &&
             event.target == this &&
             event.originalTarget == this._menuButton) {
           return;
         }
@@ -71,19 +82,50 @@ export default class MenuButton extends 
             bubbles: true,
           }));
           this._hideMenu();
           break;
         }
         this._toggleMenu();
         break;
       }
+      case "keydown": {
+        this._handleKeyDown(event);
+      }
     }
   }
 
+  _handleKeyDown(event) {
+    if (event.key == "Enter") {
+      event.preventDefault();
+      this._toggleMenu();
+    } else if (event.key == "Escape") {
+      this._hideMenu();
+      this._menuButton.focus();
+    }
+
+    if (!event.key.startsWith("Arrow")) {
+      return;
+    }
+
+    let activeMenuitem = this.shadowRoot.activeElement ||
+                         this._menu.querySelector(".menuitem-button:not([hidden])");
+
+    let newlyFocusedItem = null;
+    if (event.key == "ArrowDown") {
+      newlyFocusedItem = activeMenuitem.nextElementSibling;
+    } else if (event.key == "ArrowUp") {
+      newlyFocusedItem = activeMenuitem.previousElementSibling;
+    }
+    if (!newlyFocusedItem) {
+      return;
+    }
+    newlyFocusedItem.focus();
+  }
+
   _hideMenu() {
     this._menu.hidden = true;
     document.documentElement.removeEventListener("click", this, true);
   }
 
   _showMenu() {
     this._menu.hidden = false;
 
--- a/browser/components/aboutlogins/tests/chrome/test_menu_button.html
+++ b/browser/components/aboutlogins/tests/chrome/test_menu_button.html
@@ -43,43 +43,65 @@ add_task(async function setup() {
 
 add_task(async function test_menu_open_close() {
   is(document.activeElement, gMenuButton, "menu-button should be focused to start the test");
 
   let menu = gMenuButton.shadowRoot.querySelector(".menu");
   is(true, menu.hidden, "menu should be hidden before pressing 'space'");
   sendKey("SPACE");
   await new Promise(resolve => requestAnimationFrame(resolve));
-  is(false, menu.hidden, "menu should be visible after pressing 'space'");
+  ok(!menu.hidden, "menu should be visible after pressing 'space'");
+
+  sendKey("ESCAPE");
+  await new Promise(resolve => requestAnimationFrame(resolve));
+  ok(menu.hidden, "menu should be hidden after pressing 'escape'");
+  is(gMenuButton.shadowRoot.activeElement, gMenuButton.shadowRoot.querySelector(".menu-button"),
+    "the .menu-button should be focused after closing the menu via keyboard");
+
+  sendKey("RETURN");
+  await new Promise(resolve => requestAnimationFrame(resolve));
+  ok(!menu.hidden, "menu should be visible after pressing 'return'");
 
-  let feedbackItem = gMenuButton.shadowRoot.querySelector(".menuitem-feedback");
-  ok(!feedbackItem.matches(":focus"), ".menuitem-feedback should not be focused before tabbing to it");
-  // The Import menuitem is only visible on Windows, where we will need a second Tab
-  // press to get to the Feedback item.
-  let tabs = navigator.platform == "Win32" ? 2 : 1;
-  while (tabs--) {
+  let firstVisibleItem = gMenuButton.shadowRoot.querySelector(".menuitem-button:not([hidden])");
+  ok(!firstVisibleItem.matches(":focus"), "the first item should not be focused before tabbing to it");
+  sendKey("TAB");
+  await SimpleTest.promiseWaitForCondition(() => firstVisibleItem.matches(":focus"),
+    "waiting for firstVisibleItem to get focus");
+  ok(firstVisibleItem.matches(":focus"), "firstVisibleItem should be focused after tabbing to it");
+  synthesizeKey("VK_TAB", { shiftKey: true });
+  await SimpleTest.promiseWaitForCondition(() => !firstVisibleItem.matches(":focus"),
+    "waiting for firstVisibleItem to lose focus");
+  ok(!firstVisibleItem.matches(":focus"), "firstVisibleItem should lose focus after tabbing away from it");
+  sendKey("TAB");
+  await SimpleTest.promiseWaitForCondition(() => firstVisibleItem.matches(":focus"),
+    "waiting for firstVisibleItem to get focus again");
+  ok(firstVisibleItem.matches(":focus"), "firstVisibleItem should be focused after tabbing to it again");
+
+  if (navigator.platform == "Win32") {
+    // The Import menuitem is only visible on Windows, where we will need another Tab
+    // press to get to the Preferences item.
+    let preferencesItem = gMenuButton.shadowRoot.querySelector(".menuitem-preferences");
+    sendKey("DOWN");
+    await SimpleTest.promiseWaitForCondition(() => preferencesItem.matches(":focus"),
+      "waiting for preferencesItem to gain focus");
+    ok(preferencesItem.matches(":focus"), `.menuitem-preferences should be now be focused (DOWN)`);
+    sendKey("UP");
+    await SimpleTest.promiseWaitForCondition(() => !preferencesItem.matches(":focus"),
+      `waiting for preferencesItem to lose focus (UP)`);
+    ok(!preferencesItem.matches(":focus"), `.menuitem-preferences should lose focus after pressing up`);
+
     sendKey("TAB");
+    await SimpleTest.promiseWaitForCondition(() => preferencesItem.matches(":focus"),
+      "waiting for preferencesItem to get focus");
+    ok(preferencesItem.matches(":focus"), ".menuitem-preferences should be focused after tabbing to it");
   }
 
-  await SimpleTest.promiseWaitForCondition(() => feedbackItem.matches(":focus"),
-                                           "waiting for feedbackItem to get focus");
-  ok(feedbackItem.matches(":focus"), ".menuitem-feedback should be focused after tabbing to it");
-
-  let preferencesItem = gMenuButton.shadowRoot.querySelector(".menuitem-preferences");
-  ok(!preferencesItem.matches(":focus"), ".menuitem-preferences should not be focused before tabbing to it");
-  // We will need a third Tab press to get to the Preferences item.
-  sendKey("TAB");
-
-  await SimpleTest.promiseWaitForCondition(() => preferencesItem.matches(":focus"),
-    "waiting for preferencesItem to get focus");
-  ok(preferencesItem.matches(":focus"), ".menuitem-preferences should be focused after tabbing to it");
-
   let openPreferencesEvent = null;
-  is(false, menu.hidden, "menu should be visible before pressing 'space' on .menuitem-preferences");
+  ok(!menu.hidden, "menu should be visible before pressing 'space' on .menuitem-preferences");
   window.addEventListener("AboutLoginsOpenPreferences", event => openPreferencesEvent = event);
   sendKey("SPACE");
   ok(openPreferencesEvent, "AboutLoginsOpenPreferences event should be dispatched after pressing 'space' on .menuitem-preferences");
-  is(true, menu.hidden, "menu should be hidden after pressing 'space' on .menuitem-preferences");
+  ok(menu.hidden, "menu should be hidden after pressing 'space' on .menuitem-preferences");
 });
 </script>
 
 </body>
 </html>