diff --git a/devtools/client/inspector/breadcrumbs.js b/devtools/client/inspector/breadcrumbs.js index 57e3361c5bfe..7048cee36844 100644 --- a/devtools/client/inspector/breadcrumbs.js +++ b/devtools/client/inspector/breadcrumbs.js @@ -10,6 +10,7 @@ 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; @@ -72,6 +73,7 @@ HTMLBreadcrumbs.prototype = { 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 = []; @@ -290,6 +292,24 @@ HTMLBreadcrumbs.prototype = { 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(); } }, @@ -379,6 +399,26 @@ HTMLBreadcrumbs.prototype = { 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)); @@ -403,6 +443,7 @@ HTMLBreadcrumbs.prototype = { 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(); diff --git a/devtools/client/inspector/inspector-search.js b/devtools/client/inspector/inspector-search.js index 45bdcb7fc339..ae185f5ca14a 100644 --- a/devtools/client/inspector/inspector-search.js +++ b/devtools/client/inspector/inspector-search.js @@ -288,6 +288,12 @@ SelectorAutocompleter.prototype = { 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; diff --git a/devtools/client/inspector/test/browser.ini b/devtools/client/inspector/test/browser.ini index 9002643d7dac..fda09f02c35c 100644 --- a/devtools/client/inspector/test/browser.ini +++ b/devtools/client/inspector/test/browser.ini @@ -43,6 +43,8 @@ support-files = [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] @@ -121,6 +123,7 @@ skip-if = (e10s && debug) # Bug 1250058 - Docshell leak on debug e10s [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] diff --git a/devtools/client/inspector/test/browser_inspector_breadcrumbs_keyboard_trap.js b/devtools/client/inspector/test/browser_inspector_breadcrumbs_keyboard_trap.js new file mode 100644 index 000000000000..5ed1c4aa502f --- /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"); + } + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_search_keyboard_trap.js b/devtools/client/inspector/test/browser_inspector_search_keyboard_trap.js new file mode 100644 index 000000000000..5cf8b3614374 --- /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"); + } +}); diff --git a/devtools/client/inspector/test/head.js b/devtools/client/inspector/test/head.js index cdc2ade01655..f4bb01f20017 100644 --- a/devtools/client/inspector/test/head.js +++ b/devtools/client/inspector/test/head.js @@ -655,3 +655,20 @@ function waitForClipboard(setup, expected) { 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; +}