Bug 1354146 - add support for keyboard navigation inside panel views. r?Gijs
MozReview-Commit-ID: JQ2uezgfZOH
--- a/browser/components/customizableui/PanelMultiView.jsm
+++ b/browser/components/customizableui/PanelMultiView.jsm
@@ -205,30 +205,40 @@ this.PanelMultiView = class {
if (this._panelViews)
return this._panelViews;
this._panelViews = new SlidingPanelViews();
this._panelViews.push(...this.node.getElementsByTagName("panelview"));
return this._panelViews;
}
+ get _du() {
+ if (this.__du)
+ return this.__du;
+ return this.__du = Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
+ }
get _dwu() {
return this.window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
}
get _currentSubView() {
return this.panelViews ? this.panelViews.currentView : this.__currentSubView;
}
set _currentSubView(panel) {
if (this.panelViews)
this.panelViews.currentView = panel;
else
this.__currentSubView = panel;
return panel;
}
+ get _keyNavigationMap() {
+ if (!this.__keyNavigationMap)
+ this.__keyNavigationMap = new Map();
+ return this.__keyNavigationMap;
+ }
constructor(xulNode) {
this.node = xulNode;
this._currentSubView = this._anchorElement = this._subViewObserver = null;
this._mainViewHeight = 0;
this.__transitioning = this._ignoreMutations = false;
@@ -635,16 +645,22 @@ this.PanelMultiView = class {
return;
}
switch (aEvent.type) {
case "click":
if (aEvent.originalTarget == this._clickCapturer) {
this.showMainView();
}
break;
+ case "keydown":
+ this._keyNavigation(aEvent);
+ break;
+ case "mousemove":
+ this._resetKeyNavigation();
+ break;
case "overflow":
if (!this.panelViews && aEvent.target.localName == "vbox") {
// Resize the right view on the next tick.
if (this.showingSubView) {
this.window.setTimeout(this._syncContainerWithSubView.bind(this), 0);
} else if (!this.transitioning) {
this.window.setTimeout(this._syncContainerWithMainView.bind(this), 0);
}
@@ -662,31 +678,158 @@ this.PanelMultiView = class {
if (!this.panelViews) {
this._syncContainerWithMainView();
this._mainViewObserver.observe(this._mainView, {
attributes: true,
characterData: true,
childList: true,
subtree: true
});
+ } else {
+ this.window.addEventListener("keydown", this);
+ this._panel.addEventListener("mousemove", this);
}
break;
case "popupshown":
this._setMaxHeight();
break;
case "popuphidden":
this.node.removeAttribute("panelopen");
this._mainView.style.removeProperty("height");
this.showMainView();
- if (!this.panelViews)
+ if (!this.panelViews) {
this._mainViewObserver.disconnect();
+ } else {
+ this.window.removeEventListener("keydown", this);
+ this._panel.removeEventListener("mousemove", this);
+ this._resetKeyNavigation();
+ }
break;
}
}
+ /**
+ * Allow for navigating subview buttons using the arrow keys and the Enter key.
+ * The Up and Down keys can be used to navigate the list up and down and the
+ * Enter, Right or Left - depending on the text direction - key can be used to
+ * simulate a click on the currently selected button.
+ * The Right or Left key - depending on the text direction - can be used to
+ * navigate to the previous view, functioning as a shortcut for the view's
+ * back button.
+ * Thus, in LTR mode:
+ * - The Right key functions the same as the Enter key, simulating a click
+ * - The Left key triggers a navigation back to the previous view.
+ *
+ * @param {KeyEvent} event
+ */
+ _keyNavigation(event) {
+ let view = this._currentSubView;
+ let navMap = this._keyNavigationMap.get(view);
+ if (!navMap) {
+ navMap = {};
+ this._keyNavigationMap.set(view, {});
+ }
+ let buttons = this._getNavigableElements(view);
+ if (!buttons.length)
+ return;
+
+ let stop = () => {
+ event.stopPropagation();
+ event.preventDefault();
+ };
+ let du = this._du;
+ let idx;
+
+ switch (event.keyCode) {
+ case event.DOM_VK_DOWN:
+ case event.DOM_VK_UP: {
+ let isDown = (event.keyCode == event.DOM_VK_DOWN);
+ stop();
+ let maxIdx = buttons.length - 1;
+ idx = isDown ? 0 : maxIdx;
+ let prev = -1;
+ if (typeof navMap.selected == "number") {
+ prev = navMap.selected;
+ if (isDown) {
+ idx = ++navMap.selected;
+ if (idx > maxIdx)
+ idx = 0;
+ } else {
+ idx = --navMap.selected;
+ if (idx < 0)
+ idx = maxIdx;
+ }
+ }
+ if (prev > -1)
+ du.removePseudoClassLock(buttons[prev], ":hover");
+ du.addPseudoClassLock(buttons[idx], ":hover");
+ navMap.selected = idx;
+ break;
+ }
+ case event.DOM_VK_RIGHT:
+ case event.DOM_VK_LEFT: {
+ let dir = this._dir;
+ if ((dir == "rtl" && event.keyCode == event.DOM_VK_RIGHT) ||
+ (dir == "ltr" && event.keyCode == event.DOM_VK_LEFT)) {
+ let canGoBack = !!view.getAttribute("title");
+ if (canGoBack)
+ this.goBack(view.backButton);
+ break;
+ }
+ // Fall-through...
+ }
+ case event.DOM_VK_RETURN: {
+ idx = navMap.selected;
+ if (!buttons[idx])
+ break;
+ stop();
+ du.removePseudoClassLock(buttons[idx], ":hover");
+ delete navMap.selected;
+ buttons[idx].click();
+ break;
+ }
+ }
+ }
+
+ /**
+ * Clear all traces of keyboard navigation happening right now.
+ */
+ _resetKeyNavigation() {
+ let view = this._currentSubView;
+ let navMap = this._keyNavigationMap.get(view);
+ this._keyNavigationMap.clear();
+ if (!navMap)
+ return;
+
+ let buttons = this._getNavigableElements(view);
+ if (!buttons.length)
+ return;
+
+ let idx = navMap.selected;
+ if (!buttons[idx])
+ return;
+ this._du.removePseudoClassLock(buttons[idx], ":hover");
+ this._keyNavigationMap.clear();
+ }
+
+ /**
+ * Retrieve the button elements from a view node that can be used for navigation
+ * using the keyboard; enabled buttons and the back button, if visible.
+ *
+ * @param {nsIDOMNode} view
+ * @return {Array}
+ */
+ _getNavigableElements(view) {
+ let buttons = Array.from(view.querySelectorAll(".subviewbutton:not([disabled])"));
+ let canGoBack = !!view.getAttribute("title");
+ if (canGoBack)
+ buttons.unshift(view.backButton);
+ return buttons;
+ }
+
_shouldSetPosition() {
return this.node.getAttribute("nosubviews") == "true";
}
_shouldSetHeight() {
return this.node.getAttribute("nosubviews") != "true";
}
--- a/browser/components/customizableui/content/panelUI.xml
+++ b/browser/components/customizableui/content/panelUI.xml
@@ -59,28 +59,32 @@
</xul:stack>
</xul:box>
</content>
</binding>
<binding id="panelview">
<content>
<xul:box class="panel-header" anonid="header">
- <xul:toolbarbutton class="subviewbutton subviewbutton-iconic subviewbutton-back"
+ <xul:toolbarbutton anonid="back"
+ class="subviewbutton subviewbutton-iconic subviewbutton-back"
closemenu="none"
tooltip="&backCmd.label;"
onclick="document.getBindingParent(this).panelMultiView.goBack()"/>
<xul:label xbl:inherits="value=title"/>
</xul:box>
<children/>
</content>
<implementation>
<property name="header"
readonly="true"
onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'header');"/>
+ <property name="backButton"
+ readonly="true"
+ onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'back');"/>
<property name="panelMultiView" readonly="true">
<getter><![CDATA[
if (!this.parentNode.localName.endsWith("panelmultiview")) {
return document.getBindingParent(this.parentNode);
}
return this.parentNode;
]]></getter>