Bug 1242852 - (part 2) making inspector toolbar keyboard accessible.
MozReview-Commit-ID: 4Kr9uXRS4P6
---
devtools/client/inspector/breadcrumbs.js | 36 +++++++++++++++
devtools/client/inspector/inspector-search.js | 9 +++-
devtools/client/inspector/test/browser.ini | 2 +
.../browser_inspector_breadcrumbs_keyboard_trap.js | 52 ++++++++++++++++++++++
.../inspector/test/browser_inspector_search-05.js | 4 +-
.../test/browser_inspector_search-navigation.js | 8 ++--
.../test/browser_inspector_search_keyboard_trap.js | 48 ++++++++++++++++++++
devtools/client/inspector/test/head.js | 15 +++++++
8 files changed, 168 insertions(+), 6 deletions(-)
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,29 @@ 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);
+ }
+ },
+
+ 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 +389,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 +433,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
@@ -271,26 +271,33 @@ SelectorAutocompleter.prototype = {
/**
* Handles keypresses inside the input box.
*/
_onSearchKeypress: function(event) {
let query = this.searchBox.value;
switch(event.keyCode) {
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();
}
break;
+ case event.DOM_VK_TAB:
+ // When tab is pressed with focus on searchbox: do not change its value
+ // even if the popup is open, close the popup, do not prevent the
+ // default to avoid a keyboard trap.
+ this.hidePopup();
+ this.emit("processing-done");
+ return;
+
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 =
Math.max(0, this.searchPopup.itemCount - 2);
}
else {
--- a/devtools/client/inspector/test/browser.ini
+++ b/devtools/client/inspector/test/browser.ini
@@ -29,16 +29,17 @@ support-files =
doc_inspector_search-svg.html
doc_inspector_select-last-selected-01.html
doc_inspector_select-last-selected-02.html
head.js
[browser_inspector_breadcrumbs.js]
[browser_inspector_breadcrumbs_highlight_hover.js]
[browser_inspector_breadcrumbs_keybinding.js]
+[browser_inspector_breadcrumbs_keyboard_trap.js]
[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]
@@ -105,14 +106,15 @@ 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_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,52 @@
+/* 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";
+
+const TEST_URL = URL_ROOT + "doc_inspector_breadcrumbs.html";
+
+/**
+ * Test data has the format of:
+ * [
+ * {Boolean} flag, indicating if breadcrumbs contains focus
+ * {String} key event's key
+ * {?Object} Optional event data such as shiftKey, etc
+ * ]
+ *
+ */
+const TEST_DATA = [
+ // Move the focus away from breadcrumbs to a next focusable element.
+ [ false, "VK_TAB", { } ],
+ // Move the focus back to the breadcrumbs.
+ [ true, "VK_TAB", { shiftKey: true } ],
+ // Move the focus back away from breadcrumbs to a previous focusable element.
+ [ false, "VK_TAB", { shiftKey: true } ],
+ // Move the focus back to the breadcrumbs.
+ [ true, "VK_TAB", { } ]
+];
+
+add_task(function*() {
+ let { inspector } = yield openInspectorForURL(TEST_URL);
+ let doc = inspector.panelDoc;
+
+ info("Selecting the test node");
+ 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]");
+ button.click();
+
+ // Ensure a breadcrumb is focused.
+ is(doc.activeElement, button, "Focus is on selected breadcrumb");
+
+ for (let [focused, ...key] of TEST_DATA) {
+ EventUtils.synthesizeKey(...key);
+ if (focused) {
+ is(doc.activeElement, button, "Focus is on selected breadcrumb");
+ } else {
+ yield inspector.once("breadcrumbs-navigation-cancelled");
+ ok(!containsFocus(doc, container), "Focus is outside of breadcrumbs");
+ }
+ }
+});
--- a/devtools/client/inspector/test/browser_inspector_search-05.js
+++ b/devtools/client/inspector/test/browser_inspector_search-05.js
@@ -29,19 +29,19 @@ add_task(function* () {
info("Enter # to search for all ids");
let processingDone = once(inspector.searchSuggestions, "processing-done");
EventUtils.synthesizeKey("#", {}, inspector.panelWin);
yield processingDone;
info("Wait for search query to complete");
yield inspector.searchSuggestions._lastQuery;
- info("Press tab to fill the search input with the first suggestion");
+ info("Press enter to fill the search input with the first suggestion");
processingDone = once(inspector.searchSuggestions, "processing-done");
- EventUtils.synthesizeKey("VK_TAB", {}, inspector.panelWin);
+ EventUtils.synthesizeKey("VK_RETURN", {}, inspector.panelWin);
yield processingDone;
info("Press enter and expect a new selection");
let onSelect = inspector.once("inspector-updated");
EventUtils.synthesizeKey("VK_RETURN", {}, inspector.panelWin);
yield onSelect;
yield checkCorrectButton(inspector, "#iframe-1");
--- a/devtools/client/inspector/test/browser_inspector_search-navigation.js
+++ b/devtools/client/inspector/test/browser_inspector_search-navigation.js
@@ -10,36 +10,38 @@
const KEY_STATES = [
["d", "d"],
["i", "di"],
["v", "div"],
[".", "div."],
["VK_UP", "div.c1"],
["VK_DOWN", "div.l1"],
["VK_DOWN", "div.l1"],
+ ["VK_UP", "div.l1"],
+ ["VK_TAB", "div.l1"],
["VK_BACK_SPACE", "div.l"],
- ["VK_TAB", "div.l1"],
+ ["VK_RETURN", "div.l1"],
[" ", "div.l1 "],
["VK_UP", "div.l1 div"],
["VK_UP", "div.l1 div"],
[".", "div.l1 div."],
- ["VK_TAB", "div.l1 div.c1"],
+ ["VK_RETURN", "div.l1 div.c1"],
["VK_BACK_SPACE", "div.l1 div.c"],
["VK_BACK_SPACE", "div.l1 div."],
["VK_BACK_SPACE", "div.l1 div"],
["VK_BACK_SPACE", "div.l1 di"],
["VK_BACK_SPACE", "div.l1 d"],
["VK_BACK_SPACE", "div.l1 "],
["VK_UP", "div.l1 div"],
["VK_BACK_SPACE", "div.l1 di"],
["VK_BACK_SPACE", "div.l1 d"],
["VK_BACK_SPACE", "div.l1 "],
["VK_UP", "div.l1 div"],
["VK_UP", "div.l1 div"],
- ["VK_TAB", "div.l1 div"],
+ ["VK_RETURN", "div.l1 div"],
["VK_BACK_SPACE", "div.l1 di"],
["VK_BACK_SPACE", "div.l1 d"],
["VK_BACK_SPACE", "div.l1 "],
["VK_DOWN", "div.l1 div"],
["VK_DOWN", "div.l1 span"],
["VK_DOWN", "div.l1 span"],
["VK_BACK_SPACE", "div.l1 spa"],
["VK_BACK_SPACE", "div.l1 sp"],
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_search_keyboard_trap.js
@@ -0,0 +1,48 @@
+/* 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";
+
+const TEST_URL = URL_ROOT + "doc_inspector_search.html";
+
+/**
+ * Test data has the format of:
+ * [
+ * {Boolean} flag, indicating if search box contains focus
+ * {Array} list of keys that include key code and optional event data
+ * (shiftKey, etc)
+ * ]
+ *
+ */
+const TEST_DATA = [
+ // Move focus to a next focusable element.
+ [ false, [ "VK_TAB", {} ] ],
+ // Move focus back to searchbox.
+ [ true, [ "VK_TAB", { shiftKey: true } ] ],
+ // Open popup and then tab away to the a next focusable element.
+ [ false, [ "c", {} ], [ "VK_TAB", {} ] ],
+ // Move focus back to searchbox.
+ [ true, [ "VK_TAB", { 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 [focused, ...keys] of TEST_DATA) {
+ for (let key of keys) {
+ let done = !focused ?
+ inspector.searchSuggestions.once("processing-done") : Promise.resolve();
+ EventUtils.synthesizeKey(...key);
+ 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
@@ -603,8 +603,23 @@ 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} aDoc document with active element in question
+ * @param {DOMNode} aElm element tested on focus containment
+ * @return {Boolean}
+ */
+function containsFocus(aDoc, aElm) {
+ let elm = aDoc.activeElement;
+ while (elm) {
+ if (elm === aElm) { return true; }
+ elm = elm.parentNode;
+ }
+ return false;
+}