Bug 1354141 - Part 2 - Introduce a new binding for Photon panels that allows for more granular control in behavior and to fork the styles entirely. r?Gijs draft
authorMike de Boer <mdeboer@mozilla.com>
Tue, 25 Apr 2017 17:59:40 +0200
changeset 567816 ca9e5e686b445d3b58ce01a280741e58cc948b67
parent 567815 68a4c5721d9b0a7e249be5806e0002f33565dfa9
child 625779 1800c3f9e68b21205d6019432149acb4def39e0d
push id55711
push usermdeboer@mozilla.com
push dateTue, 25 Apr 2017 16:02:04 +0000
reviewersGijs
bugs1354141
milestone55.0a1
Bug 1354141 - Part 2 - Introduce a new binding for Photon panels that allows for more granular control in behavior and to fork the styles entirely. r?Gijs MozReview-Commit-ID: IfvGbVMAR8V
browser/base/content/browser.css
browser/components/customizableui/PanelMultiView.jsm
browser/components/customizableui/content/panelUI.css
browser/components/customizableui/content/panelUI.inc.xul
browser/components/customizableui/content/panelUI.js
browser/components/customizableui/content/panelUI.xml
browser/themes/shared/customizableui/panelUI.inc.css
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -90,30 +90,38 @@ toolbar[customizable="true"] {
   padding: 0;
   margin: 0;
 }
 
 panelmultiview {
   -moz-binding: url("chrome://browser/content/customizableui/panelUI.xml#panelmultiview");
 }
 
+photonpanelmultiview {
+  -moz-binding: url("chrome://browser/content/customizableui/panelUI.xml#photonpanelmultiview");
+}
+
 panelview {
   -moz-binding: url("chrome://browser/content/customizableui/panelUI.xml#panelview");
   -moz-box-orient: vertical;
 }
 
 .panel-mainview {
   transition: transform var(--panelui-subview-transition-duration);
 }
 
 panelview:not([mainview]):not([current]) {
   transition: visibility 0s linear var(--panelui-subview-transition-duration);
   visibility: collapse;
 }
 
+panelview:not([title]) > .panel-header {
+  display: none;
+}
+
 tabbrowser {
   -moz-binding: url("chrome://browser/content/tabbrowser.xml#tabbrowser");
 }
 
 .tabbrowser-tabs {
   -moz-binding: url("chrome://browser/content/tabbrowser.xml#tabbrowser-tabs");
 }
 
--- a/browser/components/customizableui/PanelMultiView.jsm
+++ b/browser/components/customizableui/PanelMultiView.jsm
@@ -1,16 +1,101 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 this.EXPORTED_SYMBOLS = ["PanelMultiView"];
 
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+/**
+ * Simple implementation of the sliding window pattern; panels are added to a
+ * linked list, in-order, and the currently shown panel is remembered using a
+ * marker. The marker shifts as navigation between panels is continued, where
+ * the panel at index 0 is always the starting point:
+ *           ┌────┬────┬────┬────┐
+ *           │▓▓▓▓│    │    │    │ Start
+ *           └────┴────┴────┴────┘
+ *      ┌────┬────┬────┬────┐
+ *      │    │▓▓▓▓│    │    │      Forward
+ *      └────┴────┴────┴────┘
+ * ┌────┬────┬────┬────┐
+ * │    │    │▓▓▓▓│    │           Forward
+ * └────┴────┴────┴────┘
+ *      ┌────┬────┬────┬────┐
+ *      │    │▓▓▓▓│    │    │      Back
+ *      └────┴────┴────┴────┘
+ */
+class SlidingPanelViews extends Array {
+  constructor() {
+    super();
+    this._marker = 0;
+  }
+
+  get current() {
+    return this._marker;
+  }
+
+  set current(index) {
+    if (index == this._marker) {
+      // Never change a winning team.
+      return;
+    }
+    if (index == -1 || index > (this.length - 1)) {
+      throw new Error(`SlidingPanelViews :: index ${index} out of bounds`);
+    }
+
+    let view = this.splice(index, 1)[0];
+    if (this._marker > index) {
+      // Correct the current marker if the view-to-select was removed somewhere
+      // before it.
+      --this._marker;
+    }
+    // Then add the view-to-select right after the currently selected view.
+    this.splice(++this._marker, 0, view);
+    return this._marker;
+  }
+
+  get currentView() {
+    return this[this._marker];
+  }
+
+  set currentView(view) {
+    // This will throw an error if the view could not be found.
+    this.current = this.indexOf(view);
+  }
+
+  get previousView() {
+    return this[this._marker + 1] || this[this._marker];
+  }
+
+  /**
+   * Going back is an explicit action on the data structure, moving the marker
+   * one step back.
+   *
+   * @return {Array} A list of two items: the newly selected view and the previous one.
+   */
+  back() {
+    if (this._marker > 0)
+      --this._marker;
+    return [this.currentView, this.previousView];
+  }
+
+  clear() {
+    this._marker = 0;
+    this.splice(0, this.length);
+  }
+
+  toJSON() {
+    return `[ ${this.map((view, idx) => idx == this._marker ? `<${view.id}>` : view.id).join(", ")} ]`;
+  }
+}
+
 /**
  * This is the implementation of the panelUI.xml XBL binding, moved to this
  * module, to make it easier to fork the logic for the newer photon structure.
  * Goals are:
  * 1. to make it easier to programmatically extend the list of panels,
  * 2. allow for navigation between panels multiple levels deep and
  * 3. maintain the pre-photon structure with as little effort possible.
  *
@@ -25,17 +110,17 @@ this.PanelMultiView = class {
     return this.node.ownerGlobal;
   }
 
   get _panel() {
     return this.node.parentNode;
   }
 
   get showingSubView() {
-    return this._viewStack.getAttribute("viewtype") == "subview";
+    return this.node.getAttribute("viewtype") == "subview";
   }
   get _mainViewId() {
     return this.node.getAttribute("mainViewId");
   }
   set _mainViewId(val) {
     this.node.setAttribute("mainViewId", val);
     return val;
   }
@@ -67,16 +152,42 @@ this.PanelMultiView = class {
     this.__transitioning = val;
     if (val) {
       this.node.setAttribute("transitioning", "true");
     } else {
       this.node.removeAttribute("transitioning");
     }
   }
 
+  get panelViews() {
+    if (this._panelViews)
+      return this._panelViews;
+
+    this._panelViews = new SlidingPanelViews();
+    this._panelViews.push(...Array.from(this.node.getElementsByTagName("panelview")));
+    return this._panelViews;
+  }
+  get _dwu() {
+    return this.window.QueryInterface(Ci.nsIInterfaceRequestor)
+                      .getInterface(Ci.nsIDOMWindowUtils);
+  }
+  get _dir() {
+    return this.window.getComputedStyle(this.node).direction;
+  }
+  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;
+  }
+
   constructor(xulNode) {
     this.node = xulNode;
 
     this._currentSubView = this._anchorElement = this._subViewObserver = null;
     this._mainViewHeight = 0;
     this.__transitioning = this._ignoreMutations = false;
 
     const {document, window} = this;
@@ -87,112 +198,146 @@ this.PanelMultiView = class {
       document.getAnonymousElementByAttribute(this.node, "anonid", "viewContainer");
     this._mainViewContainer =
       document.getAnonymousElementByAttribute(this.node, "anonid", "mainViewContainer");
     this._subViews =
       document.getAnonymousElementByAttribute(this.node, "anonid", "subViews");
     this._viewStack =
       document.getAnonymousElementByAttribute(this.node, "anonid", "viewStack");
 
-    this._clickCapturer.addEventListener("click", this);
     this._panel.addEventListener("popupshowing", this);
-    this._panel.addEventListener("popupshown", this);
     this._panel.addEventListener("popuphidden", this);
-    this._subViews.addEventListener("overflow", this);
-    this._mainViewContainer.addEventListener("overflow", this);
+    if (this._subViews) {
+      this._panel.addEventListener("popupshown", this);
+      this._clickCapturer.addEventListener("click", this);
+      this._subViews.addEventListener("overflow", this);
+      this._mainViewContainer.addEventListener("overflow", this);
+      this._subViews.addEventListener("overflow", this);
+      this._mainViewContainer.addEventListener("overflow", this);
 
-    // Get a MutationObserver ready to react to subview size changes. We
-    // only attach this MutationObserver when a subview is being displayed.
-    this._subViewObserver = new window.MutationObserver(this._syncContainerWithSubView.bind(this));
-    this._mainViewObserver = new window.MutationObserver(this._syncContainerWithMainView.bind(this));
+      // Get a MutationObserver ready to react to subview size changes. We
+      // only attach this MutationObserver when a subview is being displayed.
+      this._subViewObserver = new window.MutationObserver(this._syncContainerWithSubView.bind(this));
+      this._mainViewObserver = new window.MutationObserver(this._syncContainerWithMainView.bind(this));
+
+      this._mainViewContainer.setAttribute("panelid", this._panel.id);
 
-    this._mainViewContainer.setAttribute("panelid", this._panel.id);
+      if (this._mainView) {
+        this.setMainView(this._mainView);
+      }
+    } else {
+      this.setMainView(this.panelViews.currentView);
+      this.showMainView();
+    }
 
-    if (this._mainView) {
-      this.setMainView(this._mainView);
-    }
     this.node.setAttribute("viewtype", "main");
 
     // Proxy these public properties and methods, as used elsewhere by various
     // parts of the browser, to this instance.
     ["_mainView", "ignoreMutations", "showingSubView"].forEach(property => {
       Object.defineProperty(this.node, property, {
         enumerable: true,
         get: () => this[property],
         set: (val) => this[property] = val
       });
     });
-    ["setHeightToFit", "setMainView", "showMainView", "showSubView"].forEach(method => {
+    ["goBack", "setHeightToFit", "setMainView", "showMainView", "showSubView"].forEach(method => {
       Object.defineProperty(this.node, method, {
         enumerable: true,
         value: (...args) => this[method](...args)
       });
     });
   }
 
   destructor() {
     if (this._mainView) {
       this._mainView.removeAttribute("mainview");
     }
-    this._mainViewObserver.disconnect();
-    this._subViewObserver.disconnect();
+    if (this._subViews) {
+      this._mainViewObserver.disconnect();
+      this._subViewObserver.disconnect();
+      this._subViews.removeEventListener("overflow", this);
+      this._mainViewContainer.removeEventListener("overflow", this);
+      this._clickCapturer.removeEventListener("click", this);
+    } else {
+      this.panelViews.clear();
+    }
     this._panel.removeEventListener("popupshowing", this);
     this._panel.removeEventListener("popupshown", this);
     this._panel.removeEventListener("popuphidden", this);
-    this._subViews.removeEventListener("overflow", this);
-    this._mainViewContainer.removeEventListener("overflow", this);
-    this._clickCapturer.removeEventListener("click", this);
+    this.node = this._clickCapturer = this._viewContainer = this._mainViewContainer =
+      this._subViews = this._viewStack = null;
+  }
 
-    this.node = this.__clickCapturer = this.__viewContainer = this.__mainViewContainer =
-      this.__subViews = this.__viewStack = null;
+  goBack(target) {
+    let [current, previous] = this.panelViews.back();
+    this.showSubView(current, target, previous);
   }
 
   setMainView(aNewMainView) {
-    if (this._mainView) {
+    if (this._subViews && this._mainView) {
       this._mainViewObserver.disconnect();
       this._subViews.appendChild(this._mainView);
       this._mainView.removeAttribute("mainview");
     }
     this._mainViewId = aNewMainView.id;
-    aNewMainView.setAttribute("mainview", "true");
-    this._mainViewContainer.appendChild(aNewMainView);
+    if (this._subViews) {
+      aNewMainView.setAttribute("mainview", "true");
+      this._mainViewContainer.appendChild(aNewMainView);
+    } else {
+      // If the new main view is not yet in the zeroth position, make sure it's
+      // inserted there.
+      if (aNewMainView.parentNode != this._viewStack && this._viewStack.firstChild != aNewMainView) {
+        this._viewStack.insertBefore(aNewMainView, this._viewStack.firstChild);
+      }
+    }
   }
 
   showMainView() {
-    if (this.showingSubView) {
-      let viewNode = this._currentSubView;
-      let evt = new this.window.CustomEvent("ViewHiding", { bubbles: true, cancelable: true });
-      viewNode.dispatchEvent(evt);
+    if (this._subViews) {
+      if (this.showingSubView) {
+        let viewNode = this._currentSubView;
+        let evt = new this.window.CustomEvent("ViewHiding", { bubbles: true, cancelable: true });
+        viewNode.dispatchEvent(evt);
 
-      viewNode.removeAttribute("current");
-      this._currentSubView = null;
+        viewNode.removeAttribute("current");
+        this._currentSubView = null;
 
-      this._subViewObserver.disconnect();
+        this._subViewObserver.disconnect();
 
-      this._setViewContainerHeight(this._mainViewHeight);
+        this._setViewContainerHeight(this._mainViewHeight);
 
-      this.node.setAttribute("viewtype", "main");
+        this.node.setAttribute("viewtype", "main");
+      }
+
+      this._shiftMainView();
+    } else {
+      this.showSubView(this._mainViewId);
     }
-
-    this._shiftMainView();
   }
 
-  showSubView(aViewId, aAnchor) {
+  showSubView(aViewId, aAnchor, aReverse = false) {
     const {document, window} = this;
     window.Task.spawn(function*() {
-      let viewNode = this.node.querySelector("#" + aViewId);
+      // Support passing in the node directly.
+      let viewNode = typeof aViewId == "string" ? this.node.querySelector("#" + aViewId) : aViewId;
       if (!viewNode) {
         viewNode = document.getElementById(aViewId);
         if (viewNode) {
-          this._subViews.appendChild(viewNode);
+          if (this._subViews) {
+            this._subViews.appendChild(viewNode);
+          } else {
+            this._viewStack.appendChild(viewNode);
+            this.panelViews.push(viewNode);
+          }
         } else {
           throw new Error(`Subview ${aViewId} doesn't exist!`);
         }
       }
-      viewNode.setAttribute("current", true);
+
       // Emit the ViewShowing event so that the widget definition has a chance
       // to lazily populate the subview with things.
       let detail = {
         blockers: new Set(),
         addBlocker(aPromise) {
           this.blockers.add(aPromise);
         },
       };
@@ -201,52 +346,162 @@ this.PanelMultiView = class {
       viewNode.dispatchEvent(evt);
 
       let cancel = evt.defaultPrevented;
       if (detail.blockers.size) {
         try {
           let results = yield window.Promise.all(detail.blockers);
           cancel = cancel || results.some(val => val === false);
         } catch (e) {
-          Components.utils.reportError(e);
+          Cu.reportError(e);
           cancel = true;
         }
       }
 
       if (cancel) {
         return;
       }
 
+      let previousViewNode = aReverse || this._currentSubView;
       this._currentSubView = viewNode;
+      let playTransition = (!!previousViewNode && previousViewNode != viewNode);
+
+      let dwu, previousRect;
+      if (playTransition) {
+        dwu = this._dwu;
+        previousRect = previousViewNode.__lastKnownBoundingRect =
+          dwu.getBoundsWithoutFlushing(previousViewNode);
+      }
+
+      viewNode.setAttribute("current", true);
 
       // Now we have to transition the panel. There are a few parts to this:
       //
       // 1) The main view content gets shifted so that the center of the anchor
       //    node is at the left-most edge of the panel.
       // 2) The subview deck slides in so that it takes up almost all of the
       //    panel.
       // 3) If the subview is taller then the main panel contents, then the panel
       //    must grow to meet that new height. Otherwise, it must shrink.
       //
       // All three of these actions make use of CSS transformations, so they
       // should all occur simultaneously.
       this.node.setAttribute("viewtype", "subview");
-      this._shiftMainView(aAnchor);
+
+      if (this._subViews) {
+        this._shiftMainView(aAnchor);
+
+        this._mainViewHeight = this._viewStack.clientHeight;
+
+        let newHeight = this._heightOfSubview(viewNode, this._subViews);
+        this._setViewContainerHeight(newHeight);
 
-      this._mainViewHeight = this._viewStack.clientHeight;
+        this._subViewObserver.observe(viewNode, {
+          attributes: true,
+          characterData: true,
+          childList: true,
+          subtree: true
+        });
+      } else {
+        // Sliding the next subview in means that the previous panelview stays
+        // where it is and the active panelview slides in from the left in LTR
+        // mode, right in RTL mode.
+        if (playTransition) {
+          let onTransitionEnd = () => {
+            let evt = new window.CustomEvent("ViewHiding", { bubbles: true, cancelable: true });
+            previousViewNode.dispatchEvent(evt);
+            previousViewNode.removeAttribute("current");
+          };
 
-      let newHeight = this._heightOfSubview(viewNode, this._subViews);
-      this._setViewContainerHeight(newHeight);
+          // There's absolutely no need to show off our epic animation skillz when
+          // the panel's not even open.
+          if (this._panel.state != "open") {
+            onTransitionEnd();
+            return;
+          }
+
+          if (aAnchor)
+            aAnchor.setAttribute("open", true);
+          this._viewContainer.style.height = previousRect.height + "px";
+          this._viewContainer.style.width = previousRect.width + "px";
 
-      this._subViewObserver.observe(viewNode, {
-        attributes: true,
-        characterData: true,
-        childList: true,
-        subtree: true
-      });
+          this._transitioning = true;
+          this._viewContainer.setAttribute("transition-reverse", aReverse);
+          // Wait until after the first paint to ensure setting 'current=true'
+          // has taken full effect; we want to correctly measure rects using
+          // `dwu.getBoundsWithoutFlushing`.
+          window.addEventListener("MozAfterPaint", () => {
+            let viewRect = dwu.getBoundsWithoutFlushing(viewNode);
+            // Due to the views being inside a stack, the next view is stretched
+            // to be be same size as the currently visible view, except when it's
+            // larger. In other words: smaller views are reported to be a different
+            // size, regardless whether we flush layout.
+            // We can at least make sure that this only happens to use once and
+            // use the cached value, if we have it (see above).
+            if (viewRect.width == previousRect.width && viewRect.height == previousRect.height) {
+              viewRect = viewNode.__lastKnownBoundingRect || viewRect;
+            }
+            let nodeToAnimate = aReverse ? previousViewNode : viewNode;
+            let rectToAnimate = (aReverse ? previousRect : viewRect);
+            let movementX = Math.max(rectToAnimate.width, previousRect.width);
+
+            if (!aReverse) {
+              // We set the margin here to make sure the view is positioned next
+              // to the view that is currently visible. The animation is taken
+              // care of by transitioning the `transform: translateX()` property
+              // instead.
+              // Once the transition finished, we clean both properties up.
+              viewNode.style.marginInlineStart = `${previousRect.width}px`;
+            }
+
+            // Set the viewContainer dimensions to make sure only the current view
+            // is visible.
+            this._viewContainer.style.height = viewRect.height + "px";
+            this._viewContainer.style.width = viewRect.width + "px";
+
+            // Set the transition style and listen for its end to clean up and
+            // make sure the box sizing becomes dynamic again.
+            nodeToAnimate.style.transition = "transform ease-" + (aReverse ? "in" : "out") +
+              " var(--panelui-subview-transition-duration)";
+            nodeToAnimate.addEventListener("transitionend", () => {
+              onTransitionEnd();
+              this._transitioning = false;
+
+              // Take another breather, just like before, to wait for the 'current'
+              // attribute removal to take effect. This prevents a flicker.
+              // The cleanup we do here doesn't affect the display anymore, so
+              // we're not too fussed about the timing here.
+              window.addEventListener("MozAfterPaint", () => {
+                nodeToAnimate.style.removeProperty("transform");
+                nodeToAnimate.style.removeProperty("transition");
+                nodeToAnimate.style.removeProperty("width");
+                this._viewContainer.style.removeProperty("height");
+                this._viewContainer.style.removeProperty("width");
+                if (!aReverse)
+                  viewNode.style.removeProperty("margin-inline-start");
+                if (aAnchor)
+                  aAnchor.removeAttribute("open");
+
+                this._viewContainer.removeAttribute("transition-reverse");
+
+                if (!aReverse)
+                  viewNode.style.removeProperty("margin-inline-start");
+              }, { once: true });
+            }, { once: true });
+
+            // The 'magic' part: build up the amount of pixels to move right or left.
+            let moveToLeft = (this._dir == "rtl" && !aReverse) || (this._dir == "ltr" && aReverse);
+            let moveX = (moveToLeft ? "" : "-") + movementX;
+            nodeToAnimate.style.transform = "translateX(" + moveX + "px)";
+            // We're setting the width property to prevent flickering during the
+            // sliding animation with smaller views.
+            nodeToAnimate.style.width = movementX + "px";
+          }, { once: true });
+        }
+      }
     }.bind(this));
   }
 
   _setViewContainerHeight(aHeight) {
     let container = this._viewContainer;
     this._transitioning = true;
 
     let onTransitionEnd = () => {
@@ -301,51 +556,53 @@ this.PanelMultiView = class {
     }
     switch (aEvent.type) {
       case "click":
         if (aEvent.originalTarget == this._clickCapturer) {
           this.showMainView();
         }
         break;
       case "overflow":
-        if (aEvent.target.localName == "vbox") {
+        if (this._subViews && aEvent.target.localName == "vbox") {
           // Resize the right view on the next tick.
-          if (this.showingSubView) {
+          if (this._subViews && this.showingSubView) {
             this.window.setTimeout(this._syncContainerWithSubView.bind(this), 0);
           } else if (!this.transitioning) {
             this.window.setTimeout(this._syncContainerWithMainView.bind(this), 0);
           }
         }
         break;
       case "popupshowing":
         this.node.setAttribute("panelopen", "true");
         // Bug 941196 - The panel can get taller when opening a subview. Disabling
         // autoPositioning means that the panel won't jump around if an opened
         // subview causes the panel to exceed the dimensions of the screen in the
         // direction that the panel originally opened in. This property resets
         // every time the popup closes, which is why we have to set it each time.
         this._panel.autoPosition = false;
-        this._syncContainerWithMainView();
 
-        this._mainViewObserver.observe(this._mainView, {
-          attributes: true,
-          characterData: true,
-          childList: true,
-          subtree: true
-        });
-
+        if (this._subViews) {
+          this._syncContainerWithMainView();
+          this._mainViewObserver.observe(this._mainView, {
+            attributes: true,
+            characterData: true,
+            childList: true,
+            subtree: true
+          });
+        }
         break;
       case "popupshown":
         this._setMaxHeight();
         break;
       case "popuphidden":
         this.node.removeAttribute("panelopen");
         this._mainView.style.removeProperty("height");
         this.showMainView();
-        this._mainViewObserver.disconnect();
+        if (this._subViews)
+          this._mainViewObserver.disconnect();
         break;
     }
   }
 
   _shouldSetPosition() {
     return this.node.getAttribute("nosubviews") == "true";
   }
 
--- a/browser/components/customizableui/content/panelUI.css
+++ b/browser/components/customizableui/content/panelUI.css
@@ -21,11 +21,29 @@
   transform: translateX(0);
   overflow-y: auto;
 }
 
 .panel-subviews[panelopen] {
   transition: transform var(--panelui-subview-transition-duration);
 }
 
-.panel-viewcontainer[panelopen]:-moz-any(:not([viewtype="main"]),[transitioning="true"]) {
-  transition: height var(--panelui-subview-transition-duration);
+.panel-viewcontainer[panelopen]:-moz-any(:not([viewtype="main"]),[transitioning]) {
+  transition-property: width, height;
+  transition-timing-function: ease-in;
+  transition-duration: var(--panelui-subview-transition-duration);
+}
+
+.panel-viewcontainer[panelopen]:-moz-any(:not([viewtype="main"]),[transitioning])[transition-reverse] {
+  transition-timing-function: ease-out;
 }
+
+/* START photon adjustments */
+
+photonpanelmultiview > .panel-viewcontainer > .panel-viewstack {
+  overflow: visible;
+}
+
+photonpanelmultiview[transitioning] {
+  pointer-events: none;
+}
+
+/* END photon adjustments */
--- a/browser/components/customizableui/content/panelUI.inc.xul
+++ b/browser/components/customizableui/content/panelUI.inc.xul
@@ -486,24 +486,24 @@
                      hidden="true">
     <popupnotificationcontent id="update-restart-notification-content" orient="vertical">
       <description id="update-restart-description">&updateRestart.message;</description>
     </popupnotificationcontent>
   </popupnotification>
 </panel>
 
 <panel id="appMenu-popup"
-       class="cui-widget-panel"
+       class="cui-widget-panel photon-panel"
        role="group"
        type="arrow"
        hidden="true"
        flip="slide"
        position="bottomcenter topright"
        noautofocus="true">
-  <panelmultiview id="appMenu-multiView" mainViewId="appMenu-mainView">
+  <photonpanelmultiview id="appMenu-multiView" mainViewId="appMenu-mainView">
     <panelview id="appMenu-mainView" class="cui-widget-panelview PanelUI-subView">
       <vbox class="panel-subview-body">
         <toolbarbutton id="appMenu-new-window-button"
                        class="subviewbutton subviewbutton-iconic"
                        label="&newNavigatorCmd.label;"
                        key="key_newNavigator"
                        command="cmd_newNavigator"/>
         <toolbarbutton id="appMenu-private-window-button"
@@ -534,12 +534,44 @@
                        label="&printButton.label;"
                        key="printKb"
 #ifdef XP_MACOSX
                        command="cmd_print"
 #else
                        command="cmd_printPreview"
 #endif
                        />
+        <toolbarbutton id="appMenu-subiew-button"
+                       class="subviewbutton subviewbutton-nav"
+                       label="Go to no. 2"
+                       onclick="document.getBindingParent(this).showSubView('appMenu-second', this)"/>
       </vbox>
     </panelview>
-  </panelmultiview>
+    <panelview id="appMenu-second" title="Second View" class="cui-widget-panelview PanelUI-subView">
+      <vbox class="panel-subview-body">
+        <toolbarbutton id="appMenu-sampleButton1"
+                       class="subviewbutton subviewbutton-nav"
+                       label="Sample Button 1"
+                       onclick="PanelUI.showSubView('appMenu-third', this)"/>
+        <toolbarbutton id="appMenu-sampleButton2"
+                       class="subviewbutton"
+                       label="Le Sample Button 2 With Le Very Long Label"/>
+        <toolbarseparator/>
+        <toolbarbutton id="appMenu-sampleButton3"
+                       class="subviewbutton"
+                       label="Sample Button 3"/>
+        <toolbarbutton id="appMenu-sampleFooter"
+                       class="panel-subview-footer subviewbutton"
+                       label="Le Footer"/>
+      </vbox>
+    </panelview>
+    <panelview id="appMenu-third" title="Third View" class="cui-widget-panelview PanelUI-subView">
+      <vbox class="panel-subview-body">
+        <toolbarbutton id="appMenu-sampleButton4"
+                       class="subviewbutton"
+                       label="Sample Button 4"/>
+        <toolbarbutton id="appMenu-sampleFooter2"
+                       class="panel-subview-footer subviewbutton"
+                       label="Le Footer, Part Deux"/>
+      </vbox>
+    </panelview>
+  </photonpanelmultiview>
 </panel>
--- a/browser/components/customizableui/content/panelUI.js
+++ b/browser/components/customizableui/content/panelUI.js
@@ -436,17 +436,17 @@ const PanelUI = {
       return;
     }
 
     if (!aAnchor) {
       Cu.reportError("Expected an anchor when opening subview with id: " + aViewId);
       return;
     }
 
-    let container = aAnchor.closest("panelmultiview");
+    let container = aAnchor.closest("panelmultiview") || aAnchor.closest("photonpanelmultiview");
     if (container) {
       container.showSubView(aViewId, aAnchor);
     } else if (!aAnchor.open) {
       aAnchor.open = true;
 
       let tempPanel = document.createElement("panel");
       tempPanel.setAttribute("type", "arrow");
       tempPanel.setAttribute("id", "customizationui-widget-panel");
--- a/browser/components/customizableui/content/panelUI.xml
+++ b/browser/components/customizableui/content/panelUI.xml
@@ -1,25 +1,30 @@
 <?xml version="1.0"?>
 <!-- This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
+<!DOCTYPE bindings [
+  <!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd">
+  %browserDTD;
+]>
+
 <bindings id="browserPanelUIBindings"
           xmlns="http://www.mozilla.org/xbl"
           xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
           xmlns:xbl="http://www.mozilla.org/xbl">
 
   <binding id="panelmultiview">
     <resources>
       <stylesheet src="chrome://browser/content/customizableui/panelUI.css"/>
     </resources>
     <content>
       <xul:box anonid="viewContainer" class="panel-viewcontainer" xbl:inherits="panelopen,viewtype,transitioning">
-        <xul:stack anonid="viewStack" xbl:inherits="viewtype,transitioning" viewtype="main" class="panel-viewstack">
+        <xul:stack anonid="viewStack" xbl:inherits="viewtype,transitioning" class="panel-viewstack">
           <xul:vbox anonid="mainViewContainer" class="panel-mainview" xbl:inherits="viewtype"/>
 
           <!-- Used to capture click events over the PanelUI-mainView if we're in
                subview mode. That way, any click on the PanelUI-mainView causes us
                to revert to the mainView mode, whereupon PanelUI-click-capture then
                allows click events to go through it. -->
           <xul:vbox anonid="clickCapturer" class="panel-clickcapturer"/>
 
@@ -41,21 +46,40 @@
        ]]></constructor>
  
        <destructor><![CDATA[
         this.instance.destructor();
        ]]></destructor>
      </implementation>
   </binding>
 
+  <binding id="photonpanelmultiview" extends="chrome://browser/content/customizableui/panelUI.xml#panelmultiview">
+    <content>
+      <xul:box anonid="viewContainer" class="panel-viewcontainer" xbl:inherits="panelopen,transitioning">
+        <xul:stack anonid="viewStack" xbl:inherits="transitioning" class="panel-viewstack">
+          <children includes="panelview"/>
+        </xul:stack>
+      </xul:box>
+    </content>
+  </binding>
+
   <binding id="panelview">
+    <content>
+      <xul:box class="panel-header">
+        <xul:toolbarbutton class="subviewbutton subviewbutton-iconic subviewbutton-back"
+                           tooltip="&backCmd.label;"
+                           onclick="document.getBindingParent(this).panelMultiView.goBack()"/>
+        <xul:label xbl:inherits="value=title"/>
+      </xul:box>
+      <children/>
+    </content>
     <implementation>
       <property name="panelMultiView" readonly="true">
         <getter><![CDATA[
-          if (this.parentNode.localName != "panelmultiview") {
+          if (!this.parentNode.localName.endsWith("panelmultiview")) {
             return document.getBindingParent(this.parentNode);
           }
 
           return this.parentNode;
         ]]></getter>
       </property>
     </implementation>
   </binding>
--- a/browser/themes/shared/customizableui/panelUI.inc.css
+++ b/browser/themes/shared/customizableui/panelUI.inc.css
@@ -322,16 +322,24 @@ panelview:not([mainview]) .toolbarbutton
   text-align: start;
   display: -moz-box;
 }
 
 .cui-widget-panel > .panel-arrowcontainer > .panel-arrowcontent {
   padding: 4px 0;
 }
 
+/* START photonpanelview adjustments */
+
+photonpanelmultiview panelview {
+  background: var(--arrowpanel-background);
+}
+
+/* END photonpanelview adjustments */
+
 .cui-widget-panel.cui-widget-panelWithFooter > .panel-arrowcontainer > .panel-arrowcontent {
   padding-bottom: 0;
 }
 
 #PanelUI-contents {
   display: block;
   flex: 1 0 auto;
   margin-left: auto;