--- a/browser/components/customizableui/PanelMultiView.jsm
+++ b/browser/components/customizableui/PanelMultiView.jsm
@@ -190,21 +190,16 @@ this.PanelMultiView = class extends this
}
/**
* @return {Promise} showSubView() returns a promise, which is kept here for
* random access.
*/
get currentShowPromise() {
return this._currentShowPromise || Promise.resolve();
}
- get _keyNavigationMap() {
- if (!this.__keyNavigationMap)
- this.__keyNavigationMap = new Map();
- return this.__keyNavigationMap;
- }
connect() {
this.knownViews = new Set(Array.from(
this.node.getElementsByTagName("panelview"),
node => PanelView.forNode(node)));
this.openViews = [];
this._mainViewHeight = 0;
this.__transitioning = false;
@@ -292,32 +287,27 @@ this.PanelMultiView = class extends this
for (let subview of subviews) {
// XBL lists the 'children' XBL element explicitly. :-(
if (subview.nodeName != "children")
this._panelViewCache.appendChild(subview);
}
}
goBack() {
+ if (this.openViews.length < 2) {
+ // This may be called by keyboard navigation or external code when only
+ // the main view is open.
+ return;
+ }
+
let previous = this.openViews.pop().node;
let current = this._currentSubView;
return this.showSubView(current, null, previous);
}
- /**
- * Checks whether it is possible to navigate backwards currently. Returns
- * false if this is the panelmultiview's mainview, true otherwise.
- *
- * @param {panelview} view View to check, defaults to the currently active view.
- * @return {Boolean}
- */
- _canGoBack(view = this._currentSubView) {
- return view.id != this._mainViewId;
- }
-
showMainView() {
if (!this.node || !this._mainViewId)
return Promise.resolve();
return this.showSubView(this._mainView);
}
/**
@@ -432,17 +422,17 @@ this.PanelMultiView = class extends this
}
}
// Now we have to transition the panel. If we've got an older transition
// still running, make sure to clean it up.
await this._cleanupTransitionPhase();
if (playTransition) {
await this._transitionViews(previousViewNode, viewNode, reverse, previousRect, aAnchor);
- this._updateKeyboardFocus(viewNode);
+ nextPanelView.focusSelectedElement();
} else {
this.hideAllViewsExcept(nextPanelView);
}
})().catch(e => Cu.reportError(e));
return this._currentShowPromise;
}
/**
@@ -617,24 +607,25 @@ this.PanelMultiView = class extends this
if (!details || !this.node)
return;
let {phase, previousViewNode, viewNode, reverse, resolve, listener, cancelListener, anchor} = details;
if (details == this._transitionDetails)
this._transitionDetails = null;
let nextPanelView = PanelView.forNode(viewNode);
+ let prevPanelView = PanelView.forNode(previousViewNode);
// Do the things we _always_ need to do whenever the transition ends or is
// interrupted.
this.hideAllViewsExcept(nextPanelView);
previousViewNode.removeAttribute("in-transition");
viewNode.removeAttribute("in-transition");
if (reverse)
- this._resetKeyNavigation(previousViewNode);
+ prevPanelView.clearNavigation();
if (anchor)
anchor.removeAttribute("open");
if (phase >= TRANSITION_PHASES.START) {
this._panel.removeAttribute("width");
this._panel.removeAttribute("height");
// Myeah, panel layout auto-resizing is a funky thing. We'll wait
@@ -716,20 +707,23 @@ this.PanelMultiView = class extends this
handleEvent(aEvent) {
if (aEvent.type.startsWith("popup") && aEvent.target != this._panel) {
// Shouldn't act on e.g. context menus being shown from within the panel.
return;
}
switch (aEvent.type) {
case "keydown":
- this._keyNavigation(aEvent);
+ if (!this._transitioning) {
+ PanelView.forNode(this._currentSubView)
+ .keyNavigation(aEvent, this._dir);
+ }
break;
case "mousemove":
- this._resetKeyNavigation();
+ this.openViews.forEach(panelView => panelView.clearNavigation());
break;
case "popupshowing": {
this.node.setAttribute("panelopen", "true");
if (!this.node.hasAttribute("disablekeynav")) {
this.window.addEventListener("keydown", this);
this._panel.addEventListener("mousemove", this);
}
break;
@@ -767,221 +761,33 @@ this.PanelMultiView = class extends this
if (panelView.nodeName != "children") {
panelView.__lastKnownBoundingRect = null;
panelView.style.removeProperty("min-width");
panelView.style.removeProperty("max-width");
}
}
this.window.removeEventListener("keydown", this);
this._panel.removeEventListener("mousemove", this);
- this._resetKeyNavigation();
+ this.openViews.forEach(panelView => panelView.clearNavigation());
this.openViews = [];
// Clear the main view size caches. The dimensions could be different
// when the popup is opened again, e.g. through touch mode sizing.
this._mainViewHeight = 0;
this._mainViewWidth = 0;
this._viewContainer.style.removeProperty("min-height");
this._viewStack.style.removeProperty("max-height");
this._viewContainer.style.removeProperty("min-width");
this._viewContainer.style.removeProperty("max-width");
this.dispatchCustomEvent("PanelMultiViewHidden");
break;
}
}
}
-
- /**
- * Based on going up or down, select the previous or next focusable button
- * in the current view.
- *
- * @param {Object} navMap the navigation keyboard map object for the view
- * @param {Array} buttons an array of focusable buttons to select an item from.
- * @param {Boolean} isDown whether we're going down (true) or up (false) in this view.
- *
- * @return {DOMNode} the button we selected.
- */
- _updateSelectedKeyNav(navMap, buttons, isDown) {
- let lastSelected = navMap.selected && navMap.selected.get();
- let newButton = null;
- let maxIdx = buttons.length - 1;
- if (lastSelected) {
- let buttonIndex = buttons.indexOf(lastSelected);
- if (buttonIndex != -1) {
- // Buttons may get selected whilst the panel is shown, so add an extra
- // check here.
- do {
- buttonIndex = buttonIndex + (isDown ? 1 : -1);
- } while (buttons[buttonIndex] && buttons[buttonIndex].disabled);
- if (isDown && buttonIndex > maxIdx)
- buttonIndex = 0;
- else if (!isDown && buttonIndex < 0)
- buttonIndex = maxIdx;
- newButton = buttons[buttonIndex];
- } else {
- // The previously selected item is no longer selectable. Find the next item:
- let allButtons = lastSelected.closest("panelview").getElementsByTagName("toolbarbutton");
- let maxAllButtonIdx = allButtons.length - 1;
- let allButtonIndex = allButtons.indexOf(lastSelected);
- while (allButtonIndex >= 0 && allButtonIndex <= maxAllButtonIdx) {
- allButtonIndex++;
- // Check if the next button is in the list of focusable buttons.
- buttonIndex = buttons.indexOf(allButtons[allButtonIndex]);
- if (buttonIndex != -1) {
- // If it is, just use that button if we were going down, or the previous one
- // otherwise. If this was the first button, newButton will end up undefined,
- // which is fine because we'll fall back to using the last button at the
- // bottom of this method.
- newButton = buttons[isDown ? buttonIndex : buttonIndex - 1];
- break;
- }
- }
- }
- }
-
- // If we couldn't find something, select the first or last item:
- if (!newButton) {
- newButton = buttons[isDown ? 0 : maxIdx];
- }
- navMap.selected = Cu.getWeakReference(newButton);
- return newButton;
- }
-
- /**
- * 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) {
- if (this._transitioning)
- return;
-
- let view = this._currentSubView;
- let navMap = this._keyNavigationMap.get(view);
- if (!navMap) {
- navMap = {};
- this._keyNavigationMap.set(view, navMap);
- }
-
- let buttons = navMap.buttons;
- if (!buttons || !buttons.length) {
- buttons = navMap.buttons = PanelView.forNode(view).getNavigableElements();
- // Set the 'tabindex' attribute on the buttons to make sure they're focussable.
- for (let button of buttons) {
- if (!button.classList.contains("subviewbutton-back") &&
- !button.hasAttribute("tabindex")) {
- button.setAttribute("tabindex", 0);
- }
- }
- }
- if (!buttons.length)
- return;
-
- let stop = () => {
- event.stopPropagation();
- event.preventDefault();
- };
-
- let keyCode = event.code;
- switch (keyCode) {
- case "ArrowDown":
- case "ArrowUp":
- case "Tab": {
- stop();
- let isDown = (keyCode == "ArrowDown") ||
- (keyCode == "Tab" && !event.shiftKey);
- let button = this._updateSelectedKeyNav(navMap, buttons, isDown);
- button.focus();
- break;
- }
- case "ArrowLeft":
- case "ArrowRight": {
- stop();
- let dir = this._dir;
- if ((dir == "ltr" && keyCode == "ArrowLeft") ||
- (dir == "rtl" && keyCode == "ArrowRight")) {
- if (this._canGoBack(view))
- this.goBack();
- break;
- }
- // If the current button is _not_ one that points to a subview, pressing
- // the arrow key shouldn't do anything.
- if (!navMap.selected || !navMap.selected.get() ||
- !navMap.selected.get().classList.contains("subviewbutton-nav")) {
- break;
- }
- // Fall-through...
- }
- case "Space":
- case "Enter": {
- let button = navMap.selected && navMap.selected.get();
- if (!button)
- break;
- stop();
-
- // Unfortunately, 'tabindex' doesn't execute the default action, so
- // we explicitly do this here.
- // We are sending a command event and then a click event.
- // This is done in order to mimic a "real" mouse click event.
- // The command event executes the action, then the click event closes the menu.
- button.doCommand();
- let clickEvent = new event.target.ownerGlobal.MouseEvent("click", {"bubbles": true});
- button.dispatchEvent(clickEvent);
- break;
- }
- }
- }
-
- /**
- * Clear all traces of keyboard navigation happening right now.
- *
- * @param {panelview} view View to reset the key navigation attributes of.
- * If no view is passed, all navigation attributes for
- * this panelmultiview are cleared.
- */
- _resetKeyNavigation(view) {
- let viewToBlur = view || this._currentSubView;
- let navMap = this._keyNavigationMap.get(viewToBlur);
- if (navMap && navMap.selected && navMap.selected.get()) {
- navMap.selected.get().blur();
- }
-
- // We clear the entire key navigation map ONLY if *no* view was passed in.
- // This happens e.g. when the popup is hidden completely, or the user moves
- // their mouse.
- // If a view is passed in, we just delete the map for that view. This happens
- // when going back from a view (which resets the map for that view only)
- if (view) {
- this._keyNavigationMap.delete(view);
- } else {
- this._keyNavigationMap.clear();
- }
- }
-
- /**
- * Focus the last selected element in the view, if any.
- *
- * @param {panelview} view the view in which to update keyboard focus.
- */
- _updateKeyboardFocus(view) {
- let navMap = this._keyNavigationMap.get(view);
- if (navMap && navMap.selected && navMap.selected.get()) {
- navMap.selected.get().focus();
- }
- }
};
/**
* This is associated to <panelview> elements.
*/
this.PanelView = class extends this.AssociatedToNode {
/**
* The "mainview" attribute is set before the panel is opened when this view
@@ -1139,9 +945,188 @@ this.PanelView = class extends this.Asso
getNavigableElements() {
let buttons = Array.from(this.node.querySelectorAll(".subviewbutton:not([disabled])"));
let dwu = this._dwu;
return buttons.filter(button => {
let bounds = dwu.getBoundsWithoutFlushing(button);
return bounds.width > 0 && bounds.height > 0;
});
}
+
+ /**
+ * Element that is currently selected with the keyboard, or null if no element
+ * is selected. Since the reference is held weakly, it can become null or
+ * undefined at any time.
+ *
+ * The element is usually, but not necessarily, in the "buttons" property
+ * which in turn is initialized from the getNavigableElements list.
+ */
+ get selectedElement() {
+ return this._selectedElement && this._selectedElement.get();
+ }
+ set selectedElement(value) {
+ if (!value) {
+ delete this._selectedElement;
+ } else {
+ this._selectedElement = Cu.getWeakReference(value);
+ }
+ }
+
+ /**
+ * Based on going up or down, select the previous or next focusable button.
+ *
+ * @param {Boolean} isDown whether we're going down (true) or up (false).
+ *
+ * @return {DOMNode} the button we selected.
+ */
+ moveSelection(isDown) {
+ let buttons = this.buttons;
+ let lastSelected = this.selectedElement;
+ let newButton = null;
+ let maxIdx = buttons.length - 1;
+ if (lastSelected) {
+ let buttonIndex = buttons.indexOf(lastSelected);
+ if (buttonIndex != -1) {
+ // Buttons may get selected whilst the panel is shown, so add an extra
+ // check here.
+ do {
+ buttonIndex = buttonIndex + (isDown ? 1 : -1);
+ } while (buttons[buttonIndex] && buttons[buttonIndex].disabled);
+ if (isDown && buttonIndex > maxIdx)
+ buttonIndex = 0;
+ else if (!isDown && buttonIndex < 0)
+ buttonIndex = maxIdx;
+ newButton = buttons[buttonIndex];
+ } else {
+ // The previously selected item is no longer selectable. Find the next item:
+ let allButtons = lastSelected.closest("panelview").getElementsByTagName("toolbarbutton");
+ let maxAllButtonIdx = allButtons.length - 1;
+ let allButtonIndex = allButtons.indexOf(lastSelected);
+ while (allButtonIndex >= 0 && allButtonIndex <= maxAllButtonIdx) {
+ allButtonIndex++;
+ // Check if the next button is in the list of focusable buttons.
+ buttonIndex = buttons.indexOf(allButtons[allButtonIndex]);
+ if (buttonIndex != -1) {
+ // If it is, just use that button if we were going down, or the previous one
+ // otherwise. If this was the first button, newButton will end up undefined,
+ // which is fine because we'll fall back to using the last button at the
+ // bottom of this method.
+ newButton = buttons[isDown ? buttonIndex : buttonIndex - 1];
+ break;
+ }
+ }
+ }
+ }
+
+ // If we couldn't find something, select the first or last item:
+ if (!newButton) {
+ newButton = buttons[isDown ? 0 : maxIdx];
+ }
+ this.selectedElement = newButton;
+ return newButton;
+ }
+
+ /**
+ * 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
+ * @param {String} dir
+ * Direction for arrow navigation, either "ltr" or "rtl".
+ */
+ keyNavigation(event, dir) {
+ let buttons = this.buttons;
+ if (!buttons || !buttons.length) {
+ buttons = this.buttons = this.getNavigableElements();
+ // Set the 'tabindex' attribute on the buttons to make sure they're focussable.
+ for (let button of buttons) {
+ if (!button.classList.contains("subviewbutton-back") &&
+ !button.hasAttribute("tabindex")) {
+ button.setAttribute("tabindex", 0);
+ }
+ }
+ }
+ if (!buttons.length)
+ return;
+
+ let stop = () => {
+ event.stopPropagation();
+ event.preventDefault();
+ };
+
+ let keyCode = event.code;
+ switch (keyCode) {
+ case "ArrowDown":
+ case "ArrowUp":
+ case "Tab": {
+ stop();
+ let isDown = (keyCode == "ArrowDown") ||
+ (keyCode == "Tab" && !event.shiftKey);
+ let button = this.moveSelection(isDown);
+ button.focus();
+ break;
+ }
+ case "ArrowLeft":
+ case "ArrowRight": {
+ stop();
+ if ((dir == "ltr" && keyCode == "ArrowLeft") ||
+ (dir == "rtl" && keyCode == "ArrowRight")) {
+ this.node.panelMultiView.goBack();
+ break;
+ }
+ // If the current button is _not_ one that points to a subview, pressing
+ // the arrow key shouldn't do anything.
+ let button = this.selectedElement;
+ if (!button || !button.classList.contains("subviewbutton-nav")) {
+ break;
+ }
+ // Fall-through...
+ }
+ case "Space":
+ case "Enter": {
+ let button = this.selectedElement;
+ if (!button)
+ break;
+ stop();
+
+ // Unfortunately, 'tabindex' doesn't execute the default action, so
+ // we explicitly do this here.
+ // We are sending a command event and then a click event.
+ // This is done in order to mimic a "real" mouse click event.
+ // The command event executes the action, then the click event closes the menu.
+ button.doCommand();
+ let clickEvent = new event.target.ownerGlobal.MouseEvent("click", {"bubbles": true});
+ button.dispatchEvent(clickEvent);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Focus the last selected element in the view, if any.
+ */
+ focusSelectedElement() {
+ let selected = this.selectedElement;
+ if (selected) {
+ selected.focus();
+ }
+ }
+
+ /**
+ * Clear all traces of keyboard navigation happening right now.
+ */
+ clearNavigation() {
+ delete this.buttons;
+ let selected = this.selectedElement;
+ if (selected) {
+ selected.blur();
+ this.selectedElement = null;
+ }
+ }
};