Bug 1242852 - (part 1) making top dev tools toolbar keyboard accessible. r=bgrins
☠☠ backed out by 0a7b395dbfca ☠ ☠
authorYura Zenevich <yzenevich@mozilla.com>
Fri, 08 Apr 2016 10:47:43 -0400
changeset 332347 61e35a492b0dcba877d68690fe91f584c3383f08
parent 332346 6c0267e552613c591e95d64540b89eb202d5418e
child 332348 16747842afff7736323440f7c27e4901525c4aef
push id1146
push userCallek@gmail.com
push dateMon, 25 Jul 2016 16:35:44 +0000
treeherdermozilla-release@a55778f9cd5a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbgrins
bugs1242852, 100644
milestone48.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 1242852 - (part 1) making top dev tools toolbar keyboard accessible. r=bgrins MozReview-Commit-ID: MPMzYnbZOM --- devtools/client/framework/test/browser.ini | 2 + .../test/browser_toolbox_keyboard_navigation.js | 88 ++++++++++++++++++++++ devtools/client/framework/toolbox.js | 67 ++++++++++++++++ devtools/client/themes/toolbars.css | 3 + 4 files changed, 160 insertions(+) create mode 100644 devtools/client/framework/test/browser_toolbox_keyboard_navigation.js
devtools/client/framework/test/browser.ini
devtools/client/framework/test/browser_toolbox_keyboard_navigation.js
devtools/client/framework/toolbox.js
devtools/client/themes/toolbars.css
--- a/devtools/client/framework/test/browser.ini
+++ b/devtools/client/framework/test/browser.ini
@@ -36,16 +36,18 @@ support-files =
 [browser_target_remote.js]
 [browser_target_support.js]
 [browser_toolbox_custom_host.js]
 [browser_toolbox_dynamic_registration.js]
 [browser_toolbox_getpanelwhenready.js]
 [browser_toolbox_highlight.js]
 [browser_toolbox_hosts.js]
 [browser_toolbox_hosts_size.js]
+[browser_toolbox_keyboard_navigation.js]
+skip-if = os == "mac" # Full keyboard navigation on OSX only works if Full Keyboard Access setting is set to All Control in System Keyboard Preferences
 [browser_toolbox_minimize.js]
 skip-if = true # Bug 1177463 - Temporarily hide the minimize button
 [browser_toolbox_options.js]
 [browser_toolbox_options_disable_buttons.js]
 [browser_toolbox_options_disable_cache-01.js]
 [browser_toolbox_options_disable_cache-02.js]
 [browser_toolbox_options_disable_js.js]
 [browser_toolbox_options_enable_serviceworkers_testing.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_keyboard_navigation.js
@@ -0,0 +1,88 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests keyboard navigation of devtools tabbar.
+
+const TEST_URL =
+  "data:text/html;charset=utf8,test page for toolbar keyboard navigation";
+
+function containsFocus(aDoc, aElm) {
+  let elm = aDoc.activeElement;
+  while (elm) {
+    if (elm === aElm) { return true; }
+    elm = elm.parentNode;
+  }
+  return false;
+}
+
+function testFocus(aDoc, aToolbar, aElm) {
+  let id = aElm.id;
+  is(aToolbar.getAttribute("aria-activedescendant"), id,
+    `Active descendant is set to a new control: ${id}`);
+  is(aDoc.activeElement.id, id, "New control is focused");
+}
+
+add_task(function*() {
+  info("Create a test tab and open the toolbox");
+  let toolbox = yield openNewTabAndToolbox(TEST_URL, "webconsole");
+  let doc = toolbox.doc;
+
+  let toolbar = doc.querySelector(".devtools-tabbar");
+  let toolbarControls = [...toolbar.querySelectorAll(
+    ".devtools-tab, toolbarbutton")].filter(elm =>
+      !elm.hidden && doc.defaultView.getComputedStyle(elm).getPropertyValue(
+        "display") !== "none");
+
+  // Put the keyboard focus onto the first toolbar control.
+  toolbarControls[0].focus();
+  ok(containsFocus(doc, toolbar), "Focus is within the toolbar");
+
+  // Move the focus away from toolbar to a next focusable element.
+  EventUtils.synthesizeKey("VK_TAB", {});
+  ok(!containsFocus(doc, toolbar), "Focus is outside of the toolbar");
+
+  // Move the focus back to the toolbar.
+  EventUtils.synthesizeKey("VK_TAB", { shiftKey: true });
+  ok(containsFocus(doc, toolbar), "Focus is within the toolbar again");
+
+  // Move through the toolbar forward using the right arrow key.
+  for (let i = 0; i < toolbarControls.length; ++i) {
+    testFocus(doc, toolbar, toolbarControls[i]);
+    if (i < toolbarControls.length - 1) {
+      EventUtils.synthesizeKey("VK_RIGHT", {});
+    }
+  }
+
+  // Move the focus away from toolbar to a next focusable element.
+  EventUtils.synthesizeKey("VK_TAB", {});
+  ok(!containsFocus(doc, toolbar), "Focus is outside of the toolbar");
+
+  // Move the focus back to the toolbar.
+  EventUtils.synthesizeKey("VK_TAB", { shiftKey: true });
+  ok(containsFocus(doc, toolbar), "Focus is within the toolbar again");
+
+  // Move through the toolbar backward using the left arrow key.
+  for (let i = toolbarControls.length - 1; i >= 0; --i) {
+    testFocus(doc, toolbar, toolbarControls[i]);
+    if (i > 0) { EventUtils.synthesizeKey("VK_LEFT", {}); }
+  }
+
+  // Move focus to the 3rd (non-first) toolbar control.
+  let expectedFocusedControl = toolbarControls[2];
+  EventUtils.synthesizeKey("VK_RIGHT", {});
+  EventUtils.synthesizeKey("VK_RIGHT", {});
+  testFocus(doc, toolbar, expectedFocusedControl);
+
+  // Move the focus away from toolbar to a next focusable element.
+  EventUtils.synthesizeKey("VK_TAB", {});
+  ok(!containsFocus(doc, toolbar), "Focus is outside of the toolbar");
+
+  // Move the focus back to the toolbar, ensure we land on the last active
+  // descendant control.
+  EventUtils.synthesizeKey("VK_TAB", { shiftKey: true });
+  testFocus(doc, toolbar, expectedFocusedControl);
+});
--- a/devtools/client/framework/toolbox.js
+++ b/devtools/client/framework/toolbox.js
@@ -408,16 +408,17 @@ Toolbox.prototype = {
       this._addHostListeners();
       this._registerOverlays();
       if (this._hostOptions && this._hostOptions.zoom === false) {
         this._disableZoomKeys();
       } else {
         this._addZoomKeys();
         this._loadInitialZoom();
       }
+      this._setToolbarKeyboardNavigation();
 
       this.webconsolePanel = this.doc.querySelector("#toolbox-panel-webconsole");
       this.webconsolePanel.height = Services.prefs.getIntPref(SPLITCONSOLE_HEIGHT_PREF);
       this.webconsolePanel.addEventListener("resize", this._saveSplitConsoleHeight);
 
       let buttonsPromise = this._buildButtons();
 
       this._pingTelemetry();
@@ -902,16 +903,82 @@ Toolbox.prototype = {
    */
   _buildTabs: function() {
     for (let definition of gDevTools.getToolDefinitionArray()) {
       this._buildTabForTool(definition);
     }
   },
 
   /**
+   * Sets up keyboard navigation with and within the dev tools toolbar.
+   */
+  _setToolbarKeyboardNavigation() {
+    let toolbar = this.doc.querySelector(".devtools-tabbar");
+    // Set and track aria-activedescendant to indicate which control is
+    // currently focused within the toolbar (for accessibility purposes).
+    toolbar.addEventListener("focus", event => {
+      let { target, rangeParent } = event;
+      let control, controlID = toolbar.getAttribute("aria-activedescendant");
+
+      if (controlID) {
+        control = this.doc.getElementById(controlID);
+      }
+      if (rangeParent || !control) {
+        // If range parent is present, the focused is moved within the toolbar,
+        // simply updating aria-activedescendant. Or if aria-activedescendant is
+        // not available, set it to target.
+        toolbar.setAttribute("aria-activedescendant", target.id);
+      } else {
+        // When range parent is not present, we focused into the toolbar, move
+        // focus to current aria-activedescendant.
+        event.preventDefault();
+        control.focus();
+      }
+    }, true)
+
+    toolbar.addEventListener("keypress", event => {
+      let { key, target } = event;
+      let win = this.doc.defaultView;
+      let elm, type;
+      if (key === "Tab") {
+        // Tabbing when toolbar or its contents are focused should move focus to
+        // next/previous focusable element relative to toolbar itself.
+        if (event.shiftKey) {
+          elm = toolbar;
+          type = Services.focus.MOVEFOCUS_BACKWARD;
+        } else {
+          // To move focus to next element following the toolbar, relative
+          // element needs to be the last element in its subtree.
+          let last = toolbar.lastChild;
+          while (last && last.lastChild) {
+            last = last.lastChild;
+          }
+          elm = last;
+          type = Services.focus.MOVEFOCUS_FORWARD;
+        }
+      } else if (key === "ArrowLeft") {
+        // Using left arrow key inside toolbar should move focus to previous
+        // toolbar control.
+        elm = target;
+        type = Services.focus.MOVEFOCUS_BACKWARD;
+      } else if (key === "ArrowRight") {
+        // Using right arrow key inside toolbar should move focus to next
+        // toolbar control.
+        elm = target;
+        type = Services.focus.MOVEFOCUS_FORWARD;
+      } else {
+        // Ignore all other keys.
+        return;
+      }
+      event.preventDefault();
+      Services.focus.moveFocus(win, elm, type, 0);
+    });
+  },
+
+  /**
    * Add buttons to the UI as specified in the devtools.toolbox.toolbarSpec pref
    */
   _buildButtons: function() {
     if (!this.target.isAddon) {
       this._buildPickerButton();
     }
 
     this.setToolboxButtonsVisibility();
--- a/devtools/client/themes/toolbars.css
+++ b/devtools/client/themes/toolbars.css
@@ -607,16 +607,17 @@
 
 /* Toolbox - moved from toolbox.css.
  * Rules that apply to the global toolbox like command buttons,
  * devtools tabs, docking buttons, etc. */
 
 #toolbox-controls > toolbarbutton,
 #toolbox-dock-buttons > toolbarbutton {
   -moz-appearance: none;
+  -moz-user-focus: normal;
   border: none;
   margin: 0 4px;
   min-width: 16px;
   width: 16px;
 }
 
 #toolbox-controls > toolbarbutton > .toolbarbutton-text,
 #toolbox-dock-buttons > toolbarbutton > .toolbarbutton-text,
@@ -693,16 +694,17 @@
 
 .command-button {
   -moz-appearance: none;
   border: none;
   padding: 0 8px;
   margin: 0;
   width: 32px;
   position: relative;
+  -moz-user-focus: normal;
 }
 
 .command-button:hover {
   background-color: hsla(206,37%,4%,.2);
 }
 .command-button:hover:active, .command-button[checked=true]:not(:hover) {
   background-color: hsla(206,37%,4%,.4);
 }
@@ -814,16 +816,17 @@
   min-height: 24px;
   max-width: 100px;
   margin: 0;
   padding: 0;
   border-style: solid;
   border-width: 0;
   -moz-border-start-width: 1px;
   -moz-box-align: center;
+  -moz-user-focus: normal;
 }
 
 .theme-dark .devtools-tab {
   color: var(--theme-body-color-alt);
   border-color: #42484f;
 }
 
 .theme-light .devtools-tab {