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
--- 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
@@ -600,16 +600,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,
@@ -686,16 +687,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);
}
@@ -807,16 +809,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 {