Bug 1272011 - improving keyboard accessibility for the inspector breadcrumbs. r=bgrins
MozReview-Commit-ID: A18Ul4HlrlT
--- a/devtools/client/inspector/breadcrumbs.js
+++ b/devtools/client/inspector/breadcrumbs.js
@@ -6,17 +6,16 @@
"use strict";
/* eslint-disable mozilla/reject-some-requires */
const {Ci} = require("chrome");
/* eslint-enable mozilla/reject-some-requires */
const Services = require("Services");
const promise = require("promise");
-const FocusManager = Services.focus;
const {waitForTick} = require("devtools/shared/DevToolsUtils");
const ELLIPSIS = Services.prefs.getComplexValue(
"intl.ellipsis",
Ci.nsIPrefLocalizedString).data;
const MAX_LABEL_LENGTH = 40;
const NS_XHTML = "http://www.w3.org/1999/xhtml";
@@ -354,25 +353,26 @@ HTMLBreadcrumbs.prototype = {
this.outer.addEventListener("mouseout", this, true);
this.outer.addEventListener("focus", this, true);
this.shortcuts = new KeyShortcuts({ window: this.chromeWin, target: this.outer });
this.handleShortcut = this.handleShortcut.bind(this);
this.shortcuts.on("Right", this.handleShortcut);
this.shortcuts.on("Left", this.handleShortcut);
- this.shortcuts.on("Tab", this.handleShortcut);
- this.shortcuts.on("Shift+Tab", this.handleShortcut);
// We will save a list of already displayed nodes in this array.
this.nodeHierarchy = [];
// Last selected node in nodeHierarchy.
this.currentIndex = -1;
+ // Used to build a unique breadcrumb button Id.
+ this.breadcrumbsWidgetItemId = 0;
+
this.update = this.update.bind(this);
this.updateSelectors = this.updateSelectors.bind(this);
this.selection.on("new-node-front", this.update);
this.selection.on("pseudoclass", this.updateSelectors);
this.selection.on("attribute-changed", this.updateSelectors);
this.inspector.on("markupmutation", this.update);
this.update();
},
@@ -485,41 +485,35 @@ HTMLBreadcrumbs.prototype = {
} else if (event.type == "mouseout") {
this.handleMouseOut(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.
+ * Focus event handler. When breadcrumbs container gets focus,
+ * aria-activedescendant needs to be updated to currently selected
+ * breadcrumb. Ensures that the focus stays on the container at all times.
* @param {DOMEvent} event.
*/
handleFocus: function (event) {
- let control = this.container.querySelector(
- ".breadcrumbs-widget-item[checked]");
- if (!this.suspendFocus && 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();
- }
- this.suspendFocus = false;
+ event.stopPropagation();
+
+ this.outer.setAttribute("aria-activedescendant",
+ this.nodeHierarchy[this.currentIndex].button.id);
+
+ this.outer.focus();
},
/**
* On click navigate to the correct node.
* @param {DOMEvent} event.
*/
handleClick: function (event) {
- // When clicking a button temporarily suspend the behaviour that refocuses
- // the currently selected button, to prevent flicking back to that button
- // See Bug 1272011
- this.suspendFocus = true;
let target = event.originalTarget;
if (target.tagName == "button") {
target.onBreadcrumbsClick();
}
},
/**
* On mouse over, highlight the corresponding content DOM Node.
@@ -552,38 +546,27 @@ HTMLBreadcrumbs.prototype = {
if (!this.selection.isElementNode()) {
return;
}
event.preventDefault();
event.stopPropagation();
this.keyPromise = (this.keyPromise || promise.resolve(null)).then(() => {
+ let currentnode;
if (name === "Left" && this.currentIndex != 0) {
- let node = this.nodeHierarchy[this.currentIndex - 1].node;
- return this.selection.setNodeFront(node, "breadcrumbs");
+ currentnode = this.nodeHierarchy[this.currentIndex - 1];
} else if (name === "Right" && this.currentIndex < this.nodeHierarchy.length - 1) {
- let node = this.nodeHierarchy[this.currentIndex + 1].node;
- return this.selection.setNodeFront(node, "breadcrumbs");
- } else if (name === "Tab") {
- // 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;
- }
- FocusManager.moveFocus(this.chromeWin, last, FocusManager.MOVEFOCUS_FORWARD, 0);
- } else if (name === "Shift+Tab") {
- // Tabbing when breadcrumbs or its contents are focused should move focus to
- // previous focusable element relative to breadcrumbs themselves.
- let elt = this.container;
- FocusManager.moveFocus(this.chromeWin, elt, FocusManager.MOVEFOCUS_BACKWARD, 0);
+ currentnode = this.nodeHierarchy[this.currentIndex + 1];
+ } else {
+ return null;
}
- return null;
+ this.outer.setAttribute("aria-activedescendant", currentnode.button.id);
+ return this.selection.setNodeFront(currentnode.node, "breadcrumbs");
});
},
/**
* Remove nodes and clean up.
*/
destroy: function () {
this.selection.off("new-node-front", this.update);
@@ -669,17 +652,19 @@ HTMLBreadcrumbs.prototype = {
* Build a button representing the node.
* @param {NodeFront} node The node from the page.
* @return {DOMNode} The <button> for this node.
*/
buildButton: function (node) {
let button = this.chromeDoc.createElementNS(NS_XHTML, "button");
button.appendChild(this.prettyPrintNodeAsXHTML(node));
button.className = "breadcrumbs-widget-item";
+ button.id = "breadcrumbs-widget-item-" + this.breadcrumbsWidgetItemId++;
+ button.setAttribute("tabindex", "-1");
button.setAttribute("title", this.prettyPrintNodeAsText(node));
button.onclick = () => {
button.focus();
};
button.onBreadcrumbsClick = () => {
this.selection.setNodeFront(node, "breadcrumbs");
--- a/devtools/client/inspector/inspector.xul
+++ b/devtools/client/inspector/inspector.xul
@@ -49,17 +49,18 @@
title="&inspectorEyeDropper.label;"
class="devtools-button command-button-invertable" />
<div xmlns="http://www.w3.org/1999/xhtml"
id="inspector-sidebar-toggle-box" />
</html:div>
<vbox flex="1" id="markup-box">
</vbox>
<html:div id="inspector-breadcrumbs-toolbar" class="devtools-toolbar">
- <html:div id="inspector-breadcrumbs" class="breadcrumbs-widget-container"/>
+ <html:div id="inspector-breadcrumbs" class="breadcrumbs-widget-container"
+ role="group" aria-label="&inspectorBreadcrumbsGroup;" tabindex="0" />
</html:div>
</vbox>
<splitter class="devtools-side-splitter"/>
<vbox id="inspector-sidebar-container">
<!-- Specify the XHTML namespace explicitly
otherwise the layout is broken. -->
<div xmlns="http://www.w3.org/1999/xhtml"
id="inspector-sidebar"
--- a/devtools/client/inspector/test/browser_inspector_breadcrumbs_keybinding.js
+++ b/devtools/client/inspector/test/browser_inspector_breadcrumbs_keybinding.js
@@ -57,12 +57,15 @@ add_task(function* () {
}
EventUtils.synthesizeKey(key, {});
yield onUpdated;
let newNodeFront = yield getNodeFront(newSelection, inspector);
is(newNodeFront, inspector.selection.nodeFront,
"The current selection is correct");
+ is(container.getAttribute("aria-activedescendant"),
+ container.querySelector("button[checked]").id,
+ "aria-activedescendant is set correctly");
currentSelection = newSelection;
}
});
--- a/devtools/client/inspector/test/browser_inspector_breadcrumbs_keyboard_trap.js
+++ b/devtools/client/inspector/test/browser_inspector_breadcrumbs_keyboard_trap.js
@@ -56,24 +56,28 @@ add_task(function* () {
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");
+ is(doc.activeElement, container, "Focus is on selected breadcrumb");
+ is(container.getAttribute("aria-activedescendant"), button.id,
+ "aria-activedescendant is set correctly");
for (let { desc, focused, key, options } of TEST_DATA) {
info(desc);
EventUtils.synthesizeKey(key, options);
// Wait until the keyPromise promise resolves.
yield breadcrumbs.keyPromise;
if (focused) {
- is(doc.activeElement, button, "Focus is on selected breadcrumb");
+ is(doc.activeElement, container, "Focus is on selected breadcrumb");
} else {
ok(!containsFocus(doc, container), "Focus is outside of breadcrumbs");
}
+ is(container.getAttribute("aria-activedescendant"), button.id,
+ "aria-activedescendant is set correctly");
}
});
--- a/devtools/client/locales/en-US/inspector.dtd
+++ b/devtools/client/locales/en-US/inspector.dtd
@@ -14,9 +14,14 @@
the inspector toolbar for the button that lets users add elements to the
DOM (as children of the currently selected element). -->
<!ENTITY inspectorAddNode.label "Create New Node">
<!ENTITY inspectorAddNode.accesskey "C">
<!-- LOCALIZATION NOTE (inspectorEyeDropper.label): A string displayed as the tooltip of
a button in the inspector which toggles the Eyedropper tool -->
-<!ENTITY inspectorEyeDropper.label "Grab a color from the page">
\ No newline at end of file
+<!ENTITY inspectorEyeDropper.label "Grab a color from the page">
+
+<!-- LOCALIZATION NOTE (inspectorBreadcrumbsGroup): A string visible only to a
+ screen reader and is used to label (using aria-label attribute) a container
+ for inspector breadcrumbs -->
+<!ENTITY inspectorBreadcrumbsGroup "Breadcrumbs">
--- a/devtools/client/themes/widgets.css
+++ b/devtools/client/themes/widgets.css
@@ -244,25 +244,16 @@
min-width: 65px;
margin: 0;
padding: 0 8px 0 20px;
border: none;
outline: none;
color: hsl(210,30%,85%);
}
-.breadcrumbs-widget-item:-moz-focusring {
- outline: none;
-}
-
-.breadcrumbs-widget-item[checked]:-moz-focusring > .button-box {
- outline: var(--theme-focus-outline);
- outline-offset: -1px;
-}
-
.breadcrumbs-widget-item > .button-box {
border: none;
padding-top: 0;
padding-bottom: 0;
}
:root[platform="win"] .breadcrumbs-widget-item:-moz-focusring > .button-box {
border-width: 0;