Bug 1401991 - Ensure that we don't hide panelviews that are already reparented to another multi-view and ensure to hide other panels consistently. r?Gijs draft
authorMike de Boer <mdeboer@mozilla.com>
Fri, 29 Sep 2017 13:51:51 +0200
changeset 672591 148210aa078791816f409e0f98fbbf970267de18
parent 672576 778767346dacb8cede38ce5c9749b2e2343e0c86
child 672596 81b9c6950b61b493d04b645df5d86a3cd444b8d1
push id82285
push usermdeboer@mozilla.com
push dateFri, 29 Sep 2017 11:57:42 +0000
reviewersGijs
bugs1401991
milestone58.0a1
Bug 1401991 - Ensure that we don't hide panelviews that are already reparented to another multi-view and ensure to hide other panels consistently. r?Gijs * Harden the new `hideAllViewsExcept()` to not do erroneous things if called when the binding is already gone. * Generalize things into `hideAllViewsExcept(thisOne)`: - Clear `_viewShowing` in there and do the descriptionHeightWorkaround thing in there too, - For Photon panels, do all the 'current' attribute setting in there. To show a panel during transition, I introduced the 'in-transition' attribute. * I had to make sure not to over-eagerly dispatch 'ViewShowing' events, because that confuses some, * Move the temporary panel handling, which contains an ephemeral panelmultiview instance, internally. This cleans up the hacky, duplicate PanelUI.js code nicely. * Keep a local copy of `_transitionDetails` to ensure it's still there after transition, * Harden `_cleanupTransitionPhase()` to only clear the phase that belongs to a specific transition, _if_ that's passed in as an argument. This resolves any potential raciness that might occur when `showSubView()` is called again mid-transition. * Skip the UITour element visibility check when it's inside a panelview, because too many things need to happen and that check is too simple to be useful in that case. MozReview-Commit-ID: 5HpJKs1Ny5j
browser/base/content/browser.css
browser/base/content/test/performance/browser_appmenu_reflows.js
browser/components/customizableui/PanelMultiView.jsm
browser/components/customizableui/content/panelUI.js
browser/components/customizableui/test/browser_981305_separator_insertion.js
browser/components/customizableui/test/browser_981418-widget-onbeforecreated-handler.js
browser/components/customizableui/test/browser_remote_tabs_button.js
browser/components/customizableui/test/head.js
browser/components/extensions/ext-browserAction.js
browser/components/uitour/UITour.jsm
browser/themes/shared/customizableui/panelUI.inc.css
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -85,22 +85,22 @@ panel[hidden] photonpanelmultiview,
 panel[hidden] panelview {
   -moz-binding: none;
 }
 
 .panel-mainview {
   transition: transform var(--panelui-subview-transition-duration);
 }
 
-panelview:not([mainview]):not([current]) {
+panelview:not([mainview]):not([current]):not([in-transition]) {
   transition: visibility 0s linear var(--panelui-subview-transition-duration);
   visibility: collapse;
 }
 
-photonpanelmultiview panelview:not([current]) {
+photonpanelmultiview panelview:not([current]):not([in-transition]) {
   transition: none;
   visibility: collapse;
 }
 
 panelview[mainview] > .panel-header,
 panelview:not([title]) > .panel-header {
   display: none;
 }
--- a/browser/base/content/test/performance/browser_appmenu_reflows.js
+++ b/browser/base/content/test/performance/browser_appmenu_reflows.js
@@ -63,17 +63,17 @@ const EXPECTED_APPMENU_SUBVIEW_REFLOWS =
    * correct. Unfortunately this requires 2 sync reflows.
    *
    * If we add more views where this is necessary, we may need to duplicate
    * these expected reflows further.
    */
   {
     stack: [
       "descriptionHeightWorkaround@resource:///modules/PanelMultiView.jsm",
-      "_cleanupTransitionPhase@resource:///modules/PanelMultiView.jsm",
+      "hideAllViewsExcept@resource:///modules/PanelMultiView.jsm",
     ],
 
     times: 2, // This number should only ever go down - never up.
   },
 
   /**
    * Please don't add anything new!
    */
--- a/browser/components/customizableui/PanelMultiView.jsm
+++ b/browser/components/customizableui/PanelMultiView.jsm
@@ -186,16 +186,25 @@ this.PanelMultiView = class {
     this.__transitioning = val;
     if (val) {
       this.node.setAttribute("transitioning", "true");
     } else {
       this.node.removeAttribute("transitioning");
     }
   }
 
+  /**
+   * @return {Boolean} |true| when the 'ephemeral' attribute is set, which means
+   *                   that this instance should be ready to be thrown away at
+   *                   any time.
+   */
+  get _ephemeral() {
+    return this.node.hasAttribute("ephemeral");
+  }
+
   get panelViews() {
     // If there's a dedicated subViews container, we're not in the right binding
     // to use SlidingPanelViews.
     if (this._subViews)
       return null;
 
     if (this._panelViews)
       return this._panelViews;
@@ -212,34 +221,40 @@ this.PanelMultiView = class {
   }
   get _screenManager() {
     if (this.__screenManager)
       return this.__screenManager;
     return this.__screenManager = Cc["@mozilla.org/gfx/screenmanager;1"]
                                     .getService(Ci.nsIScreenManager);
   }
   /**
-   * Getter that returns the currently visible subview OR the subview that is
-   * about to be shown whilst a 'ViewShowing' event is being dispatched.
-   *
-   * @return {panelview}
+   * @return {panelview} the currently visible subview OR the subview that is
+   *                     about to be shown whilst a 'ViewShowing' event is being
+   *                     dispatched.
    */
   get current() {
     return this._viewShowing || this._currentSubView
   }
   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;
   }
+  /**
+   * @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;
   }
   get _multiLineElementsMap() {
     if (!this.__multiLineElementsMap)
       this.__multiLineElementsMap = new WeakMap();
@@ -311,29 +326,33 @@ this.PanelMultiView = class {
     });
     ["goBack", "descriptionHeightWorkaround", "setMainView", "showMainView",
      "showSubView"].forEach(method => {
       Object.defineProperty(this.node, method, {
         enumerable: true,
         value: (...args) => this[method](...args)
       });
     });
-    Object.defineProperty(this.node, "current", {
-      enumerable: true,
-      get: () => this.current
+    ["current", "currentShowPromise"].forEach(property => {
+      Object.defineProperty(this.node, property, {
+        enumerable: true,
+        get: () => this[property]
+      });
     });
   }
 
   destructor() {
     // Guard against re-entrancy.
     if (!this.node)
       return;
 
-    if (this._mainView) {
-      let mainView = this._mainView;
+    if (this._ephemeral)
+      this.hideAllViewsExcept(null);
+    let mainView = this._mainView;
+    if (mainView) {
       if (this._panelViewCache)
         this._panelViewCache.appendChild(mainView);
       mainView.removeAttribute("mainview");
     }
     if (this._subViews)
       this._moveOutKids(this._subViews);
 
     if (this.panelViews) {
@@ -390,20 +409,23 @@ this.PanelMultiView = class {
   /**
    * 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 != this._mainView;
+    return view.id != this._mainViewId;
   }
 
   setMainView(aNewMainView) {
+    if (!aNewMainView)
+      return;
+
     if (this._mainView) {
       if (!this.panelViews)
         this._subViews.appendChild(this._mainView);
       this._mainView.removeAttribute("mainview");
     }
     this._mainViewId = aNewMainView.id;
     aNewMainView.setAttribute("mainview", "true");
     if (this.panelViews) {
@@ -413,65 +435,88 @@ this.PanelMultiView = class {
         this._viewStack.insertBefore(aNewMainView, this._viewStack.firstChild);
       }
     } else {
       this._mainViewContainer.appendChild(aNewMainView);
     }
   }
 
   showMainView() {
+    if (!this._mainViewId)
+      return Promise.resolve();
+
+    if (this.panelViews)
+      return this.showSubView(this._mainView);
+
     if (this.showingSubView) {
       let viewNode = this._currentSubView;
       this._dispatchViewEvent(viewNode, "ViewHiding");
-      if (this.panelViews) {
+      this._transitionHeight(() => {
         viewNode.removeAttribute("current");
-        this.showSubView(this._mainViewId);
-      } else {
-        this._transitionHeight(() => {
-          viewNode.removeAttribute("current");
-          this._currentSubView = null;
-          this.node.setAttribute("viewtype", "main");
-        });
-      }
-    } else if (this.panelViews) {
-      // Make sure to hide all subviews, except for the mainView.
-      let mainView = this._mainView;
-      for (let panelview of this._panelViews) {
-        if (panelview == mainView)
-          panelview.setAttribute("current", true);
-        else
-          panelview.removeAttribute("current");
-      }
-      this.node.setAttribute("viewtype", "main");
+        this._currentSubView = null;
+        this.node.setAttribute("viewtype", "main");
+      });
     }
 
-    if (!this.panelViews) {
-      this._shiftMainView();
+    this._shiftMainView();
+    return Promise.resolve();
+  }
+
+  /**
+   * Ensures that all the panelviews, that are currently part of this instance,
+   * are hidden, except one specifically.
+   *
+   * @param {panelview} [theOne] The panelview DOM node to ensure is visible.
+   *                             Optional.
+   */
+  hideAllViewsExcept(theOne = null) {
+    for (let panelview of this._panelViews) {
+      // When the panelview was already reparented, don't interfere any more.
+      if (panelview == theOne || !this.node || panelview.panelMultiView != this.node)
+        continue;
+      if (panelview.hasAttribute("current"))
+        this._dispatchViewEvent(panelview, "ViewHiding");
+      panelview.removeAttribute("current");
     }
+
+    this._viewShowing = null;
+
+    if (!this.node || !theOne)
+      return;
+
+    this._currentSubView = theOne;
+    if (!theOne.hasAttribute("current")) {
+      theOne.setAttribute("current", true);
+      this.descriptionHeightWorkaround(theOne);
+      this._dispatchViewEvent(theOne, "ViewShown");
+    }
+    this.node.setAttribute("viewtype", (theOne.id == this._mainViewId) ? "main" : "subview");
   }
 
   showSubView(aViewId, aAnchor, aPreviousView) {
-    return (async () => {
+    this._currentShowPromise = (async () => {
       // Support passing in the node directly.
       let viewNode = typeof aViewId == "string" ? this.node.querySelector("#" + aViewId) : aViewId;
       if (!viewNode) {
         viewNode = this.document.getElementById(aViewId);
         if (viewNode) {
           this._placeSubView(viewNode);
         } else {
           throw new Error(`Subview ${aViewId} doesn't exist!`);
         }
       } else if (viewNode.parentNode == this._panelViewCache) {
         this._placeSubView(viewNode);
       }
 
       let reverse = !!aPreviousView;
       let previousViewNode = aPreviousView || this._currentSubView;
-      let playTransition = (!!previousViewNode && previousViewNode != viewNode &&
-        this._panel.state == "open");
+      // If the panelview to show is the same as the previous one, the 'ViewShowing'
+      // event has already been dispatched. Don't do it twice.
+      let showingSameView = viewNode == previousViewNode;
+      let playTransition = (!!previousViewNode && !showingSameView && this._panel.state == "open");
 
       let dwu, previousRect;
       if (playTransition || this.panelViews) {
         dwu = this._dwu;
         previousRect = previousViewNode.__lastKnownBoundingRect =
           dwu.getBoundsWithoutFlushing(previousViewNode);
         if (this.panelViews) {
           // Here go the measures that have the same caching lifetime as the width
@@ -487,88 +532,88 @@ this.PanelMultiView = class {
           if (!this._mainViewHeight) {
             this._mainViewHeight = previousRect.height;
             this._viewContainer.style.minHeight = this._mainViewHeight + "px";
           }
         }
       }
 
       this._viewShowing = viewNode;
+      // Because the 'mainview' attribute may be out-of-sync, due to view node
+      // reparenting in combination with ephemeral PanelMultiView instances,
+      // this is the best place to correct it (just before showing).
+      if (viewNode.id == this._mainViewId)
+        viewNode.setAttribute("mainview", true);
+      else
+        viewNode.removeAttribute("mainview");
 
       // Make sure that new panels always have a title set.
       if (this.panelViews && aAnchor) {
         if (!viewNode.hasAttribute("title"))
           viewNode.setAttribute("title", aAnchor.getAttribute("label"));
         viewNode.classList.add("PanelUI-subView");
       }
       if (this.panelViews && this._mainViewWidth)
         viewNode.style.maxWidth = viewNode.style.minWidth = this._mainViewWidth + "px";
 
-      // Emit the ViewShowing event so that the widget definition has a chance
-      // to lazily populate the subview with things or perhaps even cancel this
-      // whole operation.
-      let detail = {
-        blockers: new Set(),
-        addBlocker(promise) {
-          this.blockers.add(promise);
-        }
-      };
-      let cancel = this._dispatchViewEvent(viewNode, "ViewShowing", aAnchor, detail);
-      if (detail.blockers.size) {
-        try {
-          let results = await Promise.all(detail.blockers);
-          cancel = cancel || results.some(val => val === false);
-        } catch (e) {
-          Cu.reportError(e);
-          cancel = true;
+      if (!showingSameView || !viewNode.hasAttribute("current")) {
+        // Emit the ViewShowing event so that the widget definition has a chance
+        // to lazily populate the subview with things or perhaps even cancel this
+        // whole operation.
+        let detail = {
+          blockers: new Set(),
+          addBlocker(promise) {
+            this.blockers.add(promise);
+          }
+        };
+        let cancel = this._dispatchViewEvent(viewNode, "ViewShowing", aAnchor, detail);
+        if (detail.blockers.size) {
+          try {
+            let results = await Promise.all(detail.blockers);
+            cancel = cancel || results.some(val => val === false);
+          } catch (e) {
+            Cu.reportError(e);
+            cancel = true;
+          }
         }
-      }
 
-      this._viewShowing = null;
-      if (cancel) {
-        return;
-      }
-
-      this._currentSubView = viewNode;
-      if (this.panelViews) {
-        if (viewNode.id == this._mainViewId) {
-          this.node.setAttribute("viewtype", "main");
-        } else {
-          this.node.setAttribute("viewtype", "subview");
-        }
-        // If we've got an older transition still running, make sure to clean it up.
-        await this._cleanupTransitionPhase();
-        if (!playTransition) {
-          viewNode.setAttribute("current", true);
-          this.descriptionHeightWorkaround(viewNode);
+        if (cancel) {
+          this._viewShowing = null;
+          return;
         }
       }
 
       // Now we have to transition the panel.
-      if (this.panelViews && playTransition) {
-        await this._transitionViews(previousViewNode, viewNode, reverse, previousRect, aAnchor);
-
-        this._dispatchViewEvent(viewNode, "ViewShown");
-        this._updateKeyboardFocus(viewNode);
-      } else if (!this.panelViews) {
+      if (this.panelViews) {
+        // 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);
+        } else {
+          this.hideAllViewsExcept(viewNode);
+        }
+      } else {
+        this._currentSubView = viewNode;
         this._transitionHeight(() => {
           viewNode.setAttribute("current", true);
           if (viewNode.id == this._mainViewId) {
             this.node.setAttribute("viewtype", "main");
           } else {
             this.node.setAttribute("viewtype", "subview");
           }
           // Now that the subview is visible, we can check the height of the
           // description elements it contains.
           this.descriptionHeightWorkaround(viewNode);
           this._dispatchViewEvent(viewNode, "ViewShown");
         });
         this._shiftMainView(aAnchor);
       }
     })().catch(e => Cu.reportError(e));
+    return this._currentShowPromise;
   }
 
   /**
    * Apply a transition to 'slide' from the currently active view to the next
    * one.
    * 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.
@@ -591,65 +636,69 @@ this.PanelMultiView = class {
       return;
     }
 
     const {window, document} = this;
 
     if (this._autoResizeWorkaroundTimer)
       window.clearTimeout(this._autoResizeWorkaroundTimer);
 
-    this._transitionDetails = {
+    let details = this._transitionDetails = {
       phase: TRANSITION_PHASES.START,
       previousViewNode, viewNode, reverse, anchor
     };
 
     if (anchor)
       anchor.setAttribute("open", "true");
 
+    // Since we're going to show two subview at the same time, don't abuse the
+    // 'current' attribute, since it's needed for other state-keeping, but use
+    // a separate 'in-transition' attribute instead.
+    previousViewNode.setAttribute("in-transition", true);
     // Set the viewContainer dimensions to make sure only the current view is
     // visible.
     this._viewContainer.style.height = Math.max(previousRect.height, this._mainViewHeight) + "px";
     this._viewContainer.style.width = previousRect.width + "px";
     // Lock the dimensions of the window that hosts the popup panel.
     let rect = this._panel.popupBoxObject.getOuterScreenRect();
     this._panel.setAttribute("width", rect.width);
     this._panel.setAttribute("height", rect.height);
 
     let viewRect;
     if (viewNode.__lastKnownBoundingRect) {
       viewRect = viewNode.__lastKnownBoundingRect;
-      viewNode.setAttribute("current", true);
+      viewNode.setAttribute("in-transition", true);
     } else if (viewNode.customRectGetter) {
       // Can't use Object.assign directly with a DOM Rect object because its properties
       // aren't enumerable.
       let {height, width} = previousRect;
       viewRect = Object.assign({height, width}, viewNode.customRectGetter());
       let {header} = viewNode;
       if (header) {
         viewRect.height += this._dwu.getBoundsWithoutFlushing(header).height;
       }
-      viewNode.setAttribute("current", true);
+      viewNode.setAttribute("in-transition", true);
     } else {
       let oldSibling = viewNode.nextSibling || null;
       this._offscreenViewStack.appendChild(viewNode);
-      viewNode.setAttribute("current", true);
+      viewNode.setAttribute("in-transition", true);
 
       viewRect = await BrowserUtils.promiseLayoutFlushed(this.document, "layout", () => {
         return this._dwu.getBoundsWithoutFlushing(viewNode);
       });
 
       try {
         this._viewStack.insertBefore(viewNode, oldSibling);
       } catch (ex) {
         this._viewStack.appendChild(viewNode);
       }
     }
 
     this._transitioning = true;
-    this._transitionDetails.phase = TRANSITION_PHASES.PREPARE;
+    details.phase = TRANSITION_PHASES.PREPARE;
 
     // The 'magic' part: build up the amount of pixels to move right or left.
     let moveToLeft = (this._dir == "rtl" && !reverse) || (this._dir == "ltr" && reverse);
     let deltaX = previousRect.width;
     let deepestNode = reverse ? previousViewNode : viewNode;
 
     // With a transition when navigating backwards - user hits the 'back'
     // button - we need to make sure that the views are positioned in a way
@@ -674,57 +723,58 @@ this.PanelMultiView = class {
     this._panel.removeAttribute("height");
     // We're setting the width property to prevent flickering during the
     // sliding animation with smaller views.
     viewNode.style.width = viewRect.width + "px";
 
     await BrowserUtils.promiseLayoutFlushed(document, "layout", () => {});
 
     // Kick off the transition!
-    this._transitionDetails.phase = TRANSITION_PHASES.TRANSITION;
+    details.phase = TRANSITION_PHASES.TRANSITION;
     this._viewStack.style.transform = "translateX(" + (moveToLeft ? "" : "-") + deltaX + "px)";
 
     await new Promise(resolve => {
-      this._transitionDetails.resolve = resolve;
-      this._viewContainer.addEventListener("transitionend", this._transitionDetails.listener = ev => {
+      details.resolve = resolve;
+      this._viewContainer.addEventListener("transitionend", details.listener = ev => {
         // It's quite common that `height` on the view container doesn't need
         // to transition, so we make sure to do all the work on the transform
         // transition-end, because that is guaranteed to happen.
         if (ev.target != this._viewStack || ev.propertyName != "transform")
           return;
-        this._viewContainer.removeEventListener("transitionend", this._transitionDetails.listener);
-        delete this._transitionDetails.listener;
+        this._viewContainer.removeEventListener("transitionend", details.listener);
+        delete details.listener;
         resolve();
       });
     });
 
-    this._transitionDetails.phase = TRANSITION_PHASES.END;
+    details.phase = TRANSITION_PHASES.END;
 
-    await this._cleanupTransitionPhase();
+    await this._cleanupTransitionPhase(details);
   }
 
   /**
    * Attempt to clean up the attributes and properties set by `_transitionViews`
    * above. Which attributes and properties depends on the phase the transition
    * was left from - normally that'd be `TRANSITION_PHASES.END`.
    */
-  async _cleanupTransitionPhase() {
-    if (!this._transitionDetails)
+  async _cleanupTransitionPhase(details = this._transitionDetails) {
+    // Make sure to only clean up a phase from the most recent transition.
+    if (!this._transitionDetails || details != this._transitionDetails)
       return;
 
     let {phase, previousViewNode, viewNode, reverse, resolve, listener, anchor} = this._transitionDetails;
     this._transitionDetails = null;
 
     // Do the things we _always_ need to do whenever the transition ends or is
     // interrupted.
-    this._dispatchViewEvent(previousViewNode, "ViewHiding");
-    previousViewNode.removeAttribute("current");
+    this.hideAllViewsExcept(viewNode);
+    previousViewNode.removeAttribute("in-transition");
+    viewNode.removeAttribute("in-transition");
     if (reverse)
       this._resetKeyNavigation(previousViewNode);
-    this.descriptionHeightWorkaround(viewNode);
 
     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
@@ -910,17 +960,17 @@ this.PanelMultiView = class {
         }
         break;
       case "keydown":
         this._keyNavigation(aEvent);
         break;
       case "mousemove":
         this._resetKeyNavigation();
         break;
-      case "popupshowing":
+      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;
         if (this.panelViews && !this.node.hasAttribute("disablekeynav")) {
@@ -966,44 +1016,47 @@ this.PanelMultiView = class {
         this._viewStack.style.maxHeight = maxHeight + "px";
 
         // When using block-in-box layout inside a scrollable frame, like in the
         // main menu contents scroller, if we allow the contents to scroll then
         // it will not cause its container to expand. Thus, we layout first
         // without any scrolling (using "display: flex;"), and only if the view
         // exceeds the available space we set the height explicitly and enable
         // scrolling.
-        if (this._mainView.hasAttribute("blockinboxworkaround")) {
+        let mainView = this._mainView;
+        if (mainView && mainView.hasAttribute("blockinboxworkaround")) {
           let blockInBoxWorkaround = () => {
             let mainViewHeight =
-                this._dwu.getBoundsWithoutFlushing(this._mainView).height;
+                this._dwu.getBoundsWithoutFlushing(mainView).height;
             if (mainViewHeight > maxHeight) {
-              this._mainView.style.height = maxHeight + "px";
-              this._mainView.setAttribute("exceeding", "true");
+              mainView.style.height = maxHeight + "px";
+              mainView.setAttribute("exceeding", "true");
             }
           };
           // On Windows, we cannot measure the full height of the main view
           // until it is visible. Unfortunately, this causes a visible jump when
           // the view needs to scroll, but there is no easy way around this.
           if (AppConstants.platform == "win") {
             // We register a "once" listener so we don't need to store the value
             // of maxHeight elsewhere on the object.
             this._panel.addEventListener("popupshown", blockInBoxWorkaround,
                                          { once: true });
           } else {
             blockInBoxWorkaround();
           }
         }
         break;
+      }
       case "popupshown":
         // Now that the main view is visible, we can check the height of the
         // description elements it contains.
-        this.descriptionHeightWorkaround();
+        if (!this.panelViews)
+          this.descriptionHeightWorkaround();
         break;
-      case "popuphidden":
+      case "popuphidden": {
         // WebExtensions consumers can hide the popup from viewshowing, or
         // mid-transition, which disrupts our state:
         this._viewShowing = null;
         this._transitioning = false;
         this.node.removeAttribute("panelopen");
         this.showMainView();
         if (this.panelViews) {
           for (let panelView of this._viewStack.children) {
@@ -1024,22 +1077,24 @@ this.PanelMultiView = class {
           this._viewContainer.style.removeProperty("min-height");
           this._viewStack.style.removeProperty("max-height");
           this._viewContainer.style.removeProperty("min-width");
           this._viewContainer.style.removeProperty("max-width");
         }
 
         // Always try to layout the panel normally when reopening it. This is
         // also the layout that will be used in customize mode.
-        if (this._mainView.hasAttribute("blockinboxworkaround")) {
-          this._mainView.style.removeProperty("height");
-          this._mainView.removeAttribute("exceeding");
+        let mainView = this._mainView;
+        if (mainView && mainView.hasAttribute("blockinboxworkaround")) {
+          mainView.style.removeProperty("height");
+          mainView.removeAttribute("exceeding");
         }
         this._dispatchViewEvent(this.node, "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
@@ -1252,17 +1307,17 @@ this.PanelMultiView = class {
    *
    * This may trigger a synchronous layout.
    *
    * @param viewNode
    *        Indicates the node to scan for descendant elements. This is the main
    *        view if omitted.
    */
   descriptionHeightWorkaround(viewNode = this._mainView) {
-    if (!viewNode.hasAttribute("descriptionheightworkaround")) {
+    if (!viewNode || !viewNode.hasAttribute("descriptionheightworkaround")) {
       // This view does not require the workaround.
       return;
     }
 
     // We batch DOM changes together in order to reduce synchronous layouts.
     // First we reset any change we may have made previously. The first time
     // this is called, and in the best case scenario, this has no effect.
     let items = [];
--- a/browser/components/customizableui/content/panelUI.js
+++ b/browser/components/customizableui/content/panelUI.js
@@ -414,79 +414,65 @@ const PanelUI = {
       tempPanel.setAttribute("viewId", aViewId);
       if (aAnchor.getAttribute("tabspecific")) {
         tempPanel.setAttribute("tabspecific", true);
       }
       if (this._disableAnimations) {
         tempPanel.setAttribute("animate", "false");
       }
       tempPanel.setAttribute("context", "");
+      tempPanel.setAttribute("photon", true);
       document.getElementById(CustomizableUI.AREA_NAVBAR).appendChild(tempPanel);
       // If the view has a footer, set a convenience class on the panel.
       tempPanel.classList.toggle("cui-widget-panelWithFooter",
                                  viewNode.querySelector(".panel-subview-footer"));
 
+      // If the panelview is already selected in another PanelMultiView instance
+      // as a subview, make sure to properly hide it there.
+      let oldMultiView = viewNode.panelMultiView;
+      if (oldMultiView && oldMultiView.current == viewNode) {
+        await oldMultiView.showMainView();
+      }
+
+      let viewShown = false;
+      let listener = () => viewShown = true;
+      viewNode.addEventListener("ViewShown", listener, {once: true});
+
       let multiView = document.createElement("photonpanelmultiview");
       multiView.setAttribute("id", "customizationui-widget-multiview");
-      multiView.setAttribute("nosubviews", "true");
       multiView.setAttribute("viewCacheId", "appMenu-viewCache");
-      tempPanel.setAttribute("photon", true);
       multiView.setAttribute("mainViewId", viewNode.id);
-      multiView.appendChild(viewNode);
+      multiView.setAttribute("ephemeral", true);
+      document.getElementById("appMenu-viewCache").appendChild(viewNode);
       tempPanel.appendChild(multiView);
       viewNode.classList.add("cui-widget-panelview");
 
-      let viewShown = false;
       let panelRemover = () => {
         viewNode.classList.remove("cui-widget-panelview");
         if (viewShown) {
           CustomizableUI.removePanelCloseListeners(tempPanel);
           tempPanel.removeEventListener("popuphidden", panelRemover);
-
-          let currentView = multiView.current || viewNode;
-          let evt = new CustomEvent("ViewHiding", {detail: currentView});
-          currentView.dispatchEvent(evt);
         }
         aAnchor.open = false;
 
         // Ensure we run the destructor:
         multiView.instance.destructor();
 
         tempPanel.remove();
       };
 
-      // 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);
-        },
-      };
-
-      let evt = new CustomEvent("ViewShowing", { bubbles: true, cancelable: true, detail });
-      viewNode.dispatchEvent(evt);
+      // Wait until all the tasks needed to show a view are done.
+      await multiView.currentShowPromise;
 
-      let cancel = evt.defaultPrevented;
-      if (detail.blockers.size) {
-        try {
-          let results = await Promise.all(detail.blockers);
-          cancel = cancel || results.some(val => val === false);
-        } catch (e) {
-          Components.utils.reportError(e);
-          cancel = true;
-        }
-      }
-
-      if (cancel) {
+      if (!viewShown) {
+        viewNode.removeEventListener("ViewShown", listener);
         panelRemover();
         return;
       }
 
-      viewShown = true;
       CustomizableUI.addPanelCloseListeners(tempPanel);
       tempPanel.addEventListener("popuphidden", panelRemover);
 
       let anchor = this._getPanelAnchor(aAnchor);
 
       if (aAnchor != anchor && aAnchor.id) {
         anchor.setAttribute("consumeanchor", aAnchor.id);
       }
@@ -534,17 +520,18 @@ const PanelUI = {
 
     let highlights = await NewTabUtils.activityStreamLinks.getHighlights({
       // As per bug 1402023, hard-coded limit, until Activity Stream develops a
       // richer list.
       numItems: 6,
       withFavicons: true
     });
     // If there's nothing to display, or the panel is already hidden, get out.
-    if (!highlights.length || viewNode.panelMultiView.getAttribute("panelopen") != "true") {
+    let multiView = viewNode.panelMultiView;
+    if (!highlights.length || (multiView && multiView.getAttribute("panelopen") != "true")) {
       this._loadingRecentHighlights = false;
       return;
     }
 
     container.hidden = container.previousSibling.hidden =
       container.previousSibling.previousSibling.hidden = false;
     let fragment = document.createDocumentFragment();
     for (let highlight of highlights) {
--- a/browser/components/customizableui/test/browser_981305_separator_insertion.js
+++ b/browser/components/customizableui/test/browser_981305_separator_insertion.js
@@ -30,39 +30,40 @@ function checkSeparatorInsertion(menuId,
     insertTempItemsIntoMenu(menu);
 
     CustomizableUI.addWidgetToArea(buttonId, CustomizableUI.AREA_FIXED_OVERFLOW_PANEL);
 
     await waitForOverflowButtonShown();
 
     await document.getElementById("nav-bar").overflowable.show();
 
+    let subview = document.getElementById(subviewId);
     let button = document.getElementById(buttonId);
     button.click();
+    await BrowserTestUtils.waitForEvent(subview, "ViewShown");
 
-    await BrowserTestUtils.waitForEvent(PanelUI.overflowPanel, "ViewShown");
-    let subview = document.getElementById(subviewId);
-    ok(subview.firstChild, "Subview should have a kid");
-    is(subview.firstChild.localName, "toolbarbutton", "There should be no separators to start with");
+    let subviewBody = subview.firstChild;
+    ok(subviewBody.firstChild, "Subview should have a kid");
+    is(subviewBody.firstChild.localName, "toolbarbutton", "There should be no separators to start with");
 
-    for (let kid of subview.children) {
+    for (let kid of subviewBody.children) {
       if (kid.localName == "menuseparator") {
         ok(kid.previousSibling && kid.previousSibling.localName != "menuseparator",
            "Separators should never have another separator next to them, and should never be the first node.");
       }
     }
 
     let panelHiddenPromise = promiseOverflowHidden(window);
     PanelUI.overflowPanel.hidePopup();
     await panelHiddenPromise;
 
     CustomizableUI.reset();
   };
 }
 
-add_task(checkSeparatorInsertion("menuWebDeveloperPopup", "developer-button", "PanelUI-developerItems"));
+add_task(checkSeparatorInsertion("menuWebDeveloperPopup", "developer-button", "PanelUI-developer"));
 
 registerCleanupFunction(function() {
   for (let el of tempElements) {
     el.remove();
   }
   tempElements = null;
 });
--- a/browser/components/customizableui/test/browser_981418-widget-onbeforecreated-handler.js
+++ b/browser/components/customizableui/test/browser_981418-widget-onbeforecreated-handler.js
@@ -3,89 +3,62 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 /* eslint-disable mozilla/no-arbitrary-setTimeout */
 
 "use strict";
 const kWidgetId = "test-981418-widget-onbeforecreated";
 
 // Should be able to add broken view widget
 add_task(async function testAddOnBeforeCreatedWidget() {
-  let viewShownDeferred = Promise.defer();
   let onBeforeCreatedCalled = false;
   let widgetSpec = {
     id: kWidgetId,
     type: "view",
     viewId: kWidgetId + "idontexistyet",
     onBeforeCreated(doc) {
       let view = doc.createElement("panelview");
       view.id = kWidgetId + "idontexistyet";
-      document.getElementById("PanelUI-multiView").appendChild(view);
+      document.getElementById("appMenu-viewCache").appendChild(view);
       onBeforeCreatedCalled = true;
-    },
-    onViewShowing() {
-      viewShownDeferred.resolve();
     }
   };
 
-  let noError = true;
-  try {
-    CustomizableUI.createWidget(widgetSpec);
-    CustomizableUI.addWidgetToArea(kWidgetId, CustomizableUI.AREA_NAVBAR);
-  } catch (ex) {
-    Cu.reportError(ex);
-    noError = false;
-  }
-  ok(noError, "Should not throw an exception trying to add the widget.");
+  CustomizableUI.createWidget(widgetSpec);
+  CustomizableUI.addWidgetToArea(kWidgetId, CustomizableUI.AREA_NAVBAR);
+
   ok(onBeforeCreatedCalled, "onBeforeCreated should have been called");
 
   let widgetNode = document.getElementById(kWidgetId);
+  let viewNode = document.getElementById(kWidgetId + "idontexistyet");
   ok(widgetNode, "Widget should exist");
-  if (widgetNode) {
-    try {
-      widgetNode.click();
-
-      let tempPanel = document.getElementById("customizationui-widget-panel");
-      let panelShownPromise = promisePanelElementShown(window, tempPanel);
+  ok(viewNode, "Panelview should exist");
+  widgetNode.click();
 
-      let shownTimeout = setTimeout(() => viewShownDeferred.reject("Panel not shown within 20s"), 20000);
-      await viewShownDeferred.promise;
-      await panelShownPromise;
-      clearTimeout(shownTimeout);
-      ok(true, "Found view shown");
+  let tempPanel = document.getElementById("customizationui-widget-panel");
+  let panelShownPromise = promisePanelElementShown(window, tempPanel);
 
-      let panelHiddenPromise = promisePanelElementHidden(window, tempPanel);
-      tempPanel.hidePopup();
-      await panelHiddenPromise;
-
-      CustomizableUI.addWidgetToArea(kWidgetId, CustomizableUI.AREA_FIXED_OVERFLOW_PANEL);
-      await waitForOverflowButtonShown();
-      await document.getElementById("nav-bar").overflowable.show();
+  await Promise.all([
+    BrowserTestUtils.waitForEvent(viewNode, "ViewShown"),
+    panelShownPromise
+  ]);
 
-      viewShownDeferred = Promise.defer();
-      widgetNode.click();
+  let panelHiddenPromise = promisePanelElementHidden(window, tempPanel);
+  tempPanel.hidePopup();
+  await panelHiddenPromise;
 
-      shownTimeout = setTimeout(() => viewShownDeferred.reject("Panel not shown within 20s"), 20000);
-      await viewShownDeferred.promise;
-      clearTimeout(shownTimeout);
-      ok(true, "Found view shown");
+  CustomizableUI.addWidgetToArea(kWidgetId, CustomizableUI.AREA_FIXED_OVERFLOW_PANEL);
+  await waitForOverflowButtonShown();
+  await document.getElementById("nav-bar").overflowable.show();
 
-      let panelHidden = promiseOverflowHidden(window);
-      PanelUI.overflowPanel.hidePopup();
-      await panelHidden;
-    } catch (ex) {
-      ok(false, "Unexpected exception (like a timeout for one of the yields) " +
-                "when testing view widget.");
-    }
-  }
+  widgetNode.click();
+
+  await BrowserTestUtils.waitForEvent(viewNode, "ViewShown");
 
-  noError = true;
-  try {
-    CustomizableUI.destroyWidget(kWidgetId);
-  } catch (ex) {
-    Cu.reportError(ex);
-    noError = false;
-  }
-  ok(noError, "Should not throw an exception trying to remove the broken view widget.");
+  let panelHidden = promiseOverflowHidden(window);
+  PanelUI.overflowPanel.hidePopup();
+  await panelHidden;
+
+  CustomizableUI.destroyWidget(kWidgetId);
 });
 
 add_task(async function asyncCleanup() {
   await resetCustomization();
 });
--- a/browser/components/customizableui/test/browser_remote_tabs_button.js
+++ b/browser/components/customizableui/test/browser_remote_tabs_button.js
@@ -2,17 +2,17 @@
  * 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";
 
 let syncService = {};
 Components.utils.import("resource://services-sync/service.js", syncService);
 const service = syncService.Service;
-Components.utils.import("resource://services-sync/UIState.jsm");
+const {UIState} = Components.utils.import("resource://services-sync/UIState.jsm", {});
 
 let getState;
 let originalSync;
 let syncWasCalled = false;
 
 // TODO: This test should probably be re-written, we don't really test much here.
 add_task(async function testSyncRemoteTabsButtonFunctionality() {
   info("Test the Sync Remote Tabs button in the panel");
--- a/browser/components/customizableui/test/head.js
+++ b/browser/components/customizableui/test/head.js
@@ -311,22 +311,22 @@ function isOverflowOpen() {
 }
 
 function subviewShown(aSubview) {
   return new Promise((resolve, reject) => {
     let win = aSubview.ownerGlobal;
     let timeoutId = win.setTimeout(() => {
       reject("Subview (" + aSubview.id + ") did not show within 20 seconds.");
     }, 20000);
-    function onViewShowing(e) {
-      aSubview.removeEventListener("ViewShowing", onViewShowing);
+    function onViewShown(e) {
+      aSubview.removeEventListener("ViewShown", onViewShown);
       win.clearTimeout(timeoutId);
       resolve();
     }
-    aSubview.addEventListener("ViewShowing", onViewShowing);
+    aSubview.addEventListener("ViewShown", onViewShown);
   });
 }
 
 function subviewHidden(aSubview) {
   return new Promise((resolve, reject) => {
     let win = aSubview.ownerGlobal;
     let timeoutId = win.setTimeout(() => {
       reject("Subview (" + aSubview.id + ") did not hide within 20 seconds.");
--- a/browser/components/extensions/ext-browserAction.js
+++ b/browser/components/extensions/ext-browserAction.js
@@ -145,17 +145,17 @@ this.browserAction = class extends Exten
       localized: false,
 
       onBeforeCreated: document => {
         let view = document.createElementNS(XUL_NS, "panelview");
         view.id = this.viewId;
         view.setAttribute("flex", "1");
         view.setAttribute("extension", true);
 
-        document.getElementById("PanelUI-multiView").appendChild(view);
+        document.getElementById("appMenu-viewCache").appendChild(view);
 
         if (this.extension.hasPermission("menus") ||
             this.extension.hasPermission("contextMenus")) {
           document.addEventListener("popupshowing", this);
         }
       },
 
       onDestroyed: document => {
--- a/browser/components/uitour/UITour.jsm
+++ b/browser/components/uitour/UITour.jsm
@@ -1073,25 +1073,26 @@ this.UITour = {
    */
   async _ensureTarget(aChromeWindow, aTarget) {
     let shouldOpenAppMenu = false;
     let shouldOpenPageActionPanel = false;
     if (this.targetIsInAppMenu(aTarget)) {
       shouldOpenAppMenu = true;
     } else if (this.targetIsInPageActionPanel(aTarget)) {
       shouldOpenPageActionPanel = true;
-      // Ensure the panel visibility so as to ensure the visibility of
-      // the target element inside the panel otherwise
-      // we would be rejected in the below `isElementVisible` checking.
+      // Ensure the panel visibility so as to ensure the visibility of the target
+      // element inside the panel otherwise we would be rejected in the below
+      // `isElementVisible` checking.
       aChromeWindow.BrowserPageActions.panelNode.hidden = false;
     }
 
-    // Prevent showing a panel at an undefined position.
-    if (!this.isElementVisible(aTarget.node)) {
-      return Promise.reject(`_ensureTarget: Reject the ${aTarget.name} target since it isn't visible.`);
+    // Prevent showing a panel at an undefined position, but when it's tucked
+    // away inside a panel, we skip this check.
+    if (!aTarget.node.closest("panelview") && !this.isElementVisible(aTarget.node)) {
+      return Promise.reject(`_ensureTarget: Reject the ${aTarget.name || aTarget.targetName} target since it isn't visible.`);
     }
 
     let menuToOpen = null;
     let menuClosePromises = [];
     if (shouldOpenAppMenu) {
       menuToOpen = "appMenu";
       menuClosePromises.push(this._setMenuStateForAnnotation(aChromeWindow, false, "pageActionPanel"));
     } else if (shouldOpenPageActionPanel) {
--- a/browser/themes/shared/customizableui/panelUI.inc.css
+++ b/browser/themes/shared/customizableui/panelUI.inc.css
@@ -189,20 +189,16 @@
 .panel-viewstack[viewtype="main"] > .panel-subviews {
   transform: translateX(@menuPanelWidth@);
 }
 
 .panel-viewstack[viewtype="main"] > .panel-subviews:-moz-locale-dir(rtl) {
   transform: translateX(-@menuPanelWidth@);
 }
 
-panelmultiview[nosubviews=true] > .panel-viewcontainer > .panel-viewstack > .panel-subviews {
-  display: none;
-}
-
 panelview {
   -moz-box-orient: vertical;
   -moz-box-flex: 1;
 }
 
 .panel-subview-body {
   overflow-y: auto;
   overflow-x: hidden;