Bug 1242852 - (part 2) making inspector toolbar keyboard accessible. r=gl
MozReview-Commit-ID: BmLtydkQao7
---
devtools/client/inspector/breadcrumbs.js | 41 ++++++++++
devtools/client/inspector/inspector-search.js | 6 ++
devtools/client/inspector/test/browser.ini | 3 +
.../browser_inspector_breadcrumbs_keyboard_trap.js | 79 ++++++++++++++++++
.../test/browser_inspector_search_keyboard_trap.js | 93 ++++++++++++++++++++++
devtools/client/inspector/test/head.js | 17 ++++
6 files changed, 239 insertions(+)
create mode 100644 devtools/client/inspector/test/browser_inspector_breadcrumbs_keyboard_trap.js
create mode 100644 devtools/client/inspector/test/browser_inspector_search_keyboard_trap.js
--- a/devtools/client/inspector/breadcrumbs.js
+++ b/devtools/client/inspector/breadcrumbs.js
@@ -5,16 +5,17 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {Cu, Ci} = require("chrome");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
const Services = require("Services");
const promise = require("promise");
+const FocusManager = Services.focus;
const ENSURE_SELECTION_VISIBLE_DELAY = 50; // ms
const ELLIPSIS = Services.prefs.getComplexValue("intl.ellipsis", Ci.nsIPrefLocalizedString).data;
const MAX_LABEL_LENGTH = 40;
const LOW_PRIORITY_ELEMENTS = {
"HEAD": true,
"BASE": true,
"BASEFONT": true,
@@ -67,16 +68,17 @@ HTMLBreadcrumbs.prototype = {
"<box id='breadcrumb-separator-after'></box>" +
"<box id='breadcrumb-separator-normal'></box>";
this.container.parentNode.appendChild(this.separators);
this.container.addEventListener("mousedown", this, true);
this.container.addEventListener("keypress", this, true);
this.container.addEventListener("mouseover", this, true);
this.container.addEventListener("mouseleave", this, true);
+ this.container.addEventListener("focus", this, true);
// We will save a list of already displayed nodes in this array.
this.nodeHierarchy = [];
// Last selected node in nodeHierarchy.
this.currentIndex = -1;
// By default, hide the arrows. We let the <scrollbox> show them
@@ -285,16 +287,34 @@ HTMLBreadcrumbs.prototype = {
if (event.type == "mousedown" && event.button == 0) {
this.handleMouseDown(event);
} else if (event.type == "keypress" && this.selection.isElementNode()) {
this.handleKeyPress(event);
} else if (event.type == "mouseover") {
this.handleMouseOver(event);
} else if (event.type == "mouseleave") {
this.handleMouseLeave(event);
+ } else if (event.type == "focus") {
+ this.handleFocus(event);
+ }
+ },
+
+ /**
+ * Focus event handler. When breadcrumbs container gets focus, if there is an
+ * already selected breadcrumb, move focus to it.
+ * @param {DOMEvent} event.
+ */
+ handleFocus: function(event) {
+ let control = this.container.querySelector(
+ ".breadcrumbs-widget-item[checked]");
+ if (control && control !== event.target) {
+ // If we already have a selected breadcrumb and focus target is not it,
+ // move focus to selected breadcrumb.
+ event.preventDefault();
+ control.focus();
}
},
/**
* On click and hold, open the siblings menu.
* @param {DOMEvent} event.
*/
handleMouseDown: function(event) {
@@ -374,16 +394,36 @@ HTMLBreadcrumbs.prototype = {
whatToShow: Ci.nsIDOMNodeFilter.SHOW_ELEMENT
});
break;
case this.chromeWin.KeyEvent.DOM_VK_DOWN:
navigate = this.walker.nextSibling(this.selection.nodeFront, {
whatToShow: Ci.nsIDOMNodeFilter.SHOW_ELEMENT
});
break;
+ case this.chromeWin.KeyEvent.DOM_VK_TAB:
+ // Tabbing when breadcrumbs or its contents are focused should move
+ // focus to next/previous focusable element relative to breadcrumbs
+ // themselves.
+ let elm, type;
+ if (event.shiftKey) {
+ elm = this.container;
+ type = FocusManager.MOVEFOCUS_BACKWARD;
+ } else {
+ // To move focus to next element following the breadcrumbs, relative
+ // element needs to be the last element in breadcrumbs' subtree.
+ let last = this.container.lastChild;
+ while (last && last.lastChild) {
+ last = last.lastChild;
+ }
+ elm = last;
+ type = FocusManager.MOVEFOCUS_FORWARD;
+ }
+ FocusManager.moveFocus(this.chromeWin, elm, type, 0);
+ break;
}
return navigate.then(node => this.navigateTo(node));
});
event.preventDefault();
event.stopPropagation();
},
@@ -398,16 +438,17 @@ HTMLBreadcrumbs.prototype = {
this.inspector.off("markupmutation", this.update);
this.container.removeEventListener("underflow", this.onscrollboxreflow, false);
this.container.removeEventListener("overflow", this.onscrollboxreflow, false);
this.container.removeEventListener("mousedown", this, true);
this.container.removeEventListener("keypress", this, true);
this.container.removeEventListener("mouseover", this, true);
this.container.removeEventListener("mouseleave", this, true);
+ this.container.removeEventListener("focus", this, true);
this.empty();
this.separators.remove();
this.onscrollboxreflow = null;
this.container = null;
this.separators = null;
this.nodeHierarchy = null;
--- a/devtools/client/inspector/inspector-search.js
+++ b/devtools/client/inspector/inspector-search.js
@@ -283,16 +283,22 @@ SelectorAutocompleter.prototype = {
case event.DOM_VK_RETURN:
case event.DOM_VK_TAB:
if (this.searchPopup.isOpen &&
this.searchPopup.getItemAtIndex(this.searchPopup.itemCount - 1)
.preLabel == query) {
this.searchPopup.selectedIndex = this.searchPopup.itemCount - 1;
this.searchBox.value = this.searchPopup.selectedItem.label;
this.hidePopup();
+ } else if (!this.searchPopup.isOpen && event.keyCode === event.DOM_VK_TAB) {
+ // When tab is pressed with focus on searchbox and closed popup,
+ // do not prevent the default to avoid a keyboard trap and move focus
+ // to next/previous element.
+ this.emit("processing-done");
+ return;
}
break;
case event.DOM_VK_UP:
if (this.searchPopup.isOpen && this.searchPopup.itemCount > 0) {
this.searchPopup.focus();
if (this.searchPopup.selectedIndex == this.searchPopup.itemCount - 1) {
this.searchPopup.selectedIndex =
--- a/devtools/client/inspector/test/browser.ini
+++ b/devtools/client/inspector/test/browser.ini
@@ -38,16 +38,18 @@ support-files =
!/devtools/client/shared/test/test-actor-registry.js
[browser_inspector_addNode_01.js]
[browser_inspector_addNode_02.js]
[browser_inspector_addNode_03.js]
[browser_inspector_breadcrumbs.js]
[browser_inspector_breadcrumbs_highlight_hover.js]
[browser_inspector_breadcrumbs_keybinding.js]
+[browser_inspector_breadcrumbs_keyboard_trap.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_inspector_breadcrumbs_menu.js]
[browser_inspector_breadcrumbs_mutations.js]
[browser_inspector_delete-selected-node-01.js]
[browser_inspector_delete-selected-node-02.js]
[browser_inspector_delete-selected-node-03.js]
[browser_inspector_destroy-after-navigation.js]
[browser_inspector_destroy-before-ready.js]
[browser_inspector_expand-collapse.js]
@@ -116,15 +118,16 @@ skip-if = (e10s && debug) # Bug 1250058
[browser_inspector_remove-iframe-during-load.js]
[browser_inspector_search-01.js]
[browser_inspector_search-02.js]
[browser_inspector_search-03.js]
[browser_inspector_search-04.js]
[browser_inspector_search-05.js]
[browser_inspector_search-06.js]
[browser_inspector_search-07.js]
+[browser_inspector_search_keyboard_trap.js]
[browser_inspector_search-reserved.js]
[browser_inspector_search-selection.js]
[browser_inspector_select-docshell.js]
[browser_inspector_select-last-selected.js]
[browser_inspector_search-navigation.js]
[browser_inspector_sidebarstate.js]
[browser_inspector_switch-to-inspector-on-pick.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_breadcrumbs_keyboard_trap.js
@@ -0,0 +1,79 @@
+/* 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";
+
+// Test ability to tab to and away from breadcrumbs using keyboard.
+
+const TEST_URL = URL_ROOT + "doc_inspector_breadcrumbs.html";
+
+/**
+ * Test data has the format of:
+ * {
+ * desc {String} description for better logging
+ * focused {Boolean} flag, indicating if breadcrumbs contain focus
+ * key {String} key event's key
+ * options {?Object} optional event data such as shiftKey, etc
+ * }
+ */
+const TEST_DATA = [
+ {
+ desc: "Move the focus away from breadcrumbs to a next focusable element",
+ focused: false,
+ key: "VK_TAB",
+ options: { }
+ },
+ {
+ desc: "Move the focus back to the breadcrumbs",
+ focused: true,
+ key: "VK_TAB",
+ options: { shiftKey: true }
+ },
+ {
+ desc: "Move the focus back away from breadcrumbs to a previous focusable element",
+ focused: false,
+ key: "VK_TAB",
+ options: { shiftKey: true }
+ },
+ {
+ desc: "Move the focus back to the breadcrumbs",
+ focused: true,
+ key: "VK_TAB",
+ options: { }
+ }
+];
+
+add_task(function*() {
+ let { toolbox, inspector } = yield openInspectorForURL(TEST_URL);
+ let doc = inspector.panelDoc;
+
+ yield selectNode("#i2", inspector);
+
+ info("Clicking on the corresponding breadcrumbs node to focus it");
+ let container = doc.getElementById("inspector-breadcrumbs");
+
+ let button = container.querySelector("button[checked]");
+ let onHighlight = toolbox.once("node-highlight");
+ button.click();
+ yield onHighlight;
+
+ // Ensure a breadcrumb is focused.
+ is(doc.activeElement, button, "Focus is on selected breadcrumb");
+
+ for (let { desc, focused, key, options } of TEST_DATA) {
+ info(desc);
+
+ let onUpdated;
+ if (!focused) {
+ onUpdated = inspector.once("breadcrumbs-navigation-cancelled");
+ }
+ EventUtils.synthesizeKey(key, options);
+ if (focused) {
+ is(doc.activeElement, button, "Focus is on selected breadcrumb");
+ } else {
+ yield onUpdated;
+ ok(!containsFocus(doc, container), "Focus is outside of breadcrumbs");
+ }
+ }
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_search_keyboard_trap.js
@@ -0,0 +1,93 @@
+/* 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";
+
+// Test ability to tab to and away from inspector search using keyboard.
+
+const TEST_URL = URL_ROOT + "doc_inspector_search.html";
+
+/**
+ * Test data has the format of:
+ * {
+ * desc {String} description for better logging
+ * focused {Boolean} flag, indicating if search box contains focus
+ * keys: {Array} list of keys that include key code and optional
+ * event data (shiftKey, etc)
+ * }
+ *
+ */
+const TEST_DATA = [
+ {
+ desc: "Move focus to a next focusable element",
+ focused: false,
+ keys: [
+ {
+ key: "VK_TAB",
+ options: { }
+ }
+ ]
+ },
+ {
+ desc: "Move focus back to searchbox",
+ focused: true,
+ keys: [
+ {
+ key: "VK_TAB",
+ options: { shiftKey: true }
+ }
+ ]
+ },
+ {
+ desc: "Open popup and then tab away (2 times) to the a next focusable element",
+ focused: false,
+ keys: [
+ {
+ key: "d",
+ options: { }
+ },
+ {
+ key: "VK_TAB",
+ options: { }
+ },
+ {
+ key: "VK_TAB",
+ options: { }
+ }
+ ]
+ },
+ {
+ desc: "Move focus back to searchbox",
+ focused: true,
+ keys: [
+ {
+ key: "VK_TAB",
+ options: { shiftKey: true }
+ }
+ ]
+ }
+];
+
+add_task(function*() {
+ let { inspector } = yield openInspectorForURL(TEST_URL);
+ let { searchBox } = inspector;
+ let doc = inspector.panelDoc;
+
+ yield selectNode("#b1", inspector);
+ yield focusSearchBoxUsingShortcut(inspector.panelWin);
+
+ // Ensure a searchbox is focused.
+ ok(containsFocus(doc, searchBox), "Focus is in a searchbox");
+
+ for (let { desc, focused, keys } of TEST_DATA) {
+ info(desc);
+ for (let { key, options } of keys) {
+ let done = !focused ?
+ inspector.searchSuggestions.once("processing-done") : Promise.resolve();
+ EventUtils.synthesizeKey(key, options);
+ yield done;
+ }
+ is(containsFocus(doc, searchBox), focused, "Focus is set correctly");
+ }
+});
--- a/devtools/client/inspector/test/head.js
+++ b/devtools/client/inspector/test/head.js
@@ -650,8 +650,25 @@ function waitForStyleEditor(toolbox, hre
* @return a promise that resolves when the expected string has been found or
* the validator function has returned true, rejects otherwise.
*/
function waitForClipboard(setup, expected) {
let def = promise.defer();
SimpleTest.waitForClipboard(expected, setup, def.resolve, def.reject);
return def.promise;
}
+
+/**
+ * Checks if document's active element is within the given element.
+ * @param {HTMLDocument} doc document with active element in question
+ * @param {DOMNode} container element tested on focus containment
+ * @return {Boolean}
+ */
+function containsFocus(doc, container) {
+ let elm = doc.activeElement;
+ while (elm) {
+ if (elm === container) {
+ return true;
+ }
+ elm = elm.parentNode;
+ }
+ return false;
+}