Backed out changeset f09b2b387751 (bug 1374749) for frequently failing browser-chrome's browser/components/extensions/test/browser/test-oop-extensions/browser_ext_browserAction_popup_resize.js on macOS. r=backout
authorSebastian Hengst <archaeopteryx@coole-files.de>
Mon, 11 Sep 2017 18:45:35 +0200
changeset 429552 307b5682dec342ab187af9e13ee006c36bd09da8
parent 429551 27cf5225d7e0629db23a1419aefeddc3c3677fb7
child 429553 15128312c02a238ac158589c59fd3ee3e384ce80
child 429608 a57eaa7832b2322039da509b64ecca86f98a5f91
push id7761
push userjlund@mozilla.com
push dateFri, 15 Sep 2017 00:19:52 +0000
treeherdermozilla-beta@c38455951db4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbackout
bugs1374749
milestone57.0a1
backs outf09b2b387751bb86a34b5cfb2bd1eacd31ab1fad
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Backed out changeset f09b2b387751 (bug 1374749) for frequently failing browser-chrome's browser/components/extensions/test/browser/test-oop-extensions/browser_ext_browserAction_popup_resize.js on macOS. r=backout
browser/base/content/browser.css
browser/base/content/test/performance/browser_appmenu_reflows.js
browser/components/customizableui/PanelMultiView.jsm
browser/components/customizableui/content/panelUI.css
browser/components/customizableui/content/panelUI.xml
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -90,21 +90,16 @@ panel[hidden] panelview {
   transition: transform var(--panelui-subview-transition-duration);
 }
 
 panelview:not([mainview]):not([current]) {
   transition: visibility 0s linear var(--panelui-subview-transition-duration);
   visibility: collapse;
 }
 
-photonpanelmultiview panelview:not([current]) {
-  transition: none;
-  visibility: collapse;
-}
-
 panelview[mainview] > .panel-header,
 panelview:not([title]) > .panel-header {
   display: none;
 }
 
 tabbrowser {
   -moz-binding: url("chrome://browser/content/tabbrowser.xml#tabbrowser");
 }
--- 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",
+      "onTransitionEnd@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
@@ -11,23 +11,16 @@ const {classes: Cc, interfaces: Ci, util
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
   "resource://gre/modules/AppConstants.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
   "resource://gre/modules/BrowserUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
   "resource:///modules/CustomizableUI.jsm");
 
-const TRANSITION_PHASES = Object.freeze({
-  START: 1,
-  PREPARE: 2,
-  TRANSITION: 3,
-  END: 4
-});
-
 /**
  * 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
  *           └────┴────┴────┴────┘
@@ -343,18 +336,17 @@ this.PanelMultiView = class {
       this._clickCapturer.removeEventListener("click", this);
     }
     this._panel.removeEventListener("mousemove", this);
     this._panel.removeEventListener("popupshowing", this);
     this._panel.removeEventListener("popupshown", this);
     this._panel.removeEventListener("popuphidden", this);
     this.window.removeEventListener("keydown", this);
     this.node = this._clickCapturer = this._viewContainer = this._mainViewContainer =
-      this._subViews = this._viewStack = this.__dwu = this._panelViewCache =
-        this._transitionDetails = null;
+      this._subViews = this._viewStack = this.__dwu = this._panelViewCache = null;
   }
 
   /**
    * Remove any child subviews into the panelViewCache, to ensure
    * they remain usable even if this panelmultiview instance is removed
    * from the DOM.
    * @param viewNodeContainer the container from which to remove subviews
    */
@@ -426,44 +418,42 @@ this.PanelMultiView = class {
         this.showSubView(this._mainViewId);
       } else {
         this._transitionHeight(() => {
           viewNode.removeAttribute("current");
           this._currentSubView = null;
           this.node.setAttribute("viewtype", "main");
         });
       }
-    } else if (this.panelViews) {
-      this._mainView.setAttribute("current", "true");
     }
 
     if (!this.panelViews) {
       this._shiftMainView();
     }
   }
 
   showSubView(aViewId, aAnchor, aPreviousView) {
+    const {document, window} = this;
     return (async () => {
       // Support passing in the node directly.
       let viewNode = typeof aViewId == "string" ? this.node.querySelector("#" + aViewId) : aViewId;
       if (!viewNode) {
-        viewNode = this.document.getElementById(aViewId);
+        viewNode = 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");
+      let playTransition = (!!previousViewNode && previousViewNode != viewNode);
 
       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
@@ -490,18 +480,17 @@ this.PanelMultiView = class {
         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.
+      // to lazily populate the subview with things.
       let detail = {
         blockers: new Set(),
         addBlocker(promise) {
           this.blockers.add(promise);
         }
       };
       let cancel = this._dispatchViewEvent(viewNode, "ViewShowing", aAnchor, detail);
       if (detail.blockers.size) {
@@ -515,40 +504,163 @@ this.PanelMultiView = class {
       }
 
       this._viewShowing = null;
       if (cancel) {
         return;
       }
 
       this._currentSubView = viewNode;
+      viewNode.setAttribute("current", true);
       if (this.panelViews) {
         if (viewNode.id == this._mainViewId) {
           this.node.setAttribute("viewtype", "main");
         } else {
           this.node.setAttribute("viewtype", "subview");
         }
-        if (!playTransition) {
-          viewNode.setAttribute("current", true);
+        if (!playTransition)
           this.descriptionHeightWorkaround(viewNode);
-        }
       }
 
-      // Now we have to transition the panel.
+      // 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.
       if (this.panelViews && playTransition) {
+        // 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.
+        let onTransitionEnd = () => {
+          this._dispatchViewEvent(previousViewNode, "ViewHiding");
+          previousViewNode.removeAttribute("current");
+          this.descriptionHeightWorkaround(viewNode);
+        };
+
+        // 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);
 
-        await this._transitionViews(previousViewNode, viewNode, reverse, previousRect);
+        // 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);
+
+        this._viewBoundsOffscreen(viewNode, previousRect, viewRect => {
+          this._transitioning = true;
+          if (this._autoResizeWorkaroundTimer)
+            window.clearTimeout(this._autoResizeWorkaroundTimer);
+          this._viewContainer.setAttribute("transition-reverse", reverse);
+          let nodeToAnimate = reverse ? previousViewNode : viewNode;
+
+          if (!reverse) {
+            // 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.
+            nodeToAnimate.style.marginInlineStart = previousRect.width + "px";
+          }
+
+          // Set the transition style and listen for its end to clean up and
+          // make sure the box sizing becomes dynamic again.
+          // Somehow, putting these properties in PanelUI.css doesn't work for
+          // newly shown nodes in a XUL parent node.
+          nodeToAnimate.style.transition = "transform ease-" + (reverse ? "in" : "out") +
+            " var(--panelui-subview-transition-duration)";
+          nodeToAnimate.style.willChange = "transform";
+          nodeToAnimate.style.borderInlineStart = "1px solid var(--panel-separator-color)";
+
+          // Wait until after the first paint to ensure setting 'current=true'
+          // has taken full effect; once both views are visible, we want to
+          // correctly measure rects using `dwu.getBoundsWithoutFlushing`.
+          window.addEventListener("MozAfterPaint", () => {
+            if (this._panel.state != "open") {
+              onTransitionEnd();
+              return;
+            }
+            // Now set the viewContainer dimensions to that of the new view, which
+            // kicks of the height animation.
+            this._viewContainer.style.height = Math.max(viewRect.height, this._mainViewHeight) + "px";
+            this._viewContainer.style.width = viewRect.width + "px";
+            this._panel.removeAttribute("width");
+            this._panel.removeAttribute("height");
 
-        if (aAnchor)
-          aAnchor.removeAttribute("open");
+            // 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 movementX = reverse ? viewRect.width : previousRect.width;
+            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 = viewRect.width + "px";
+
+            this._viewContainer.addEventListener("transitionend", this._transitionEndListener = 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 != nodeToAnimate || ev.propertyName != "transform")
+                return;
+
+              this._viewContainer.removeEventListener("transitionend", this._transitionEndListener);
+              this._transitionEndListener = null;
+              onTransitionEnd();
+              this._transitioning = false;
+              if (reverse) {
+                this._resetKeyNavigation(previousViewNode);
+              }
 
-        this._dispatchViewEvent(viewNode, "ViewShown");
-        this._updateKeyboardFocus(viewNode);
+              // Myeah, panel layout auto-resizing is a funky thing. We'll wait
+              // another few milliseconds to remove the width and height 'fixtures',
+              // to be sure we don't flicker annoyingly.
+              // NB: HACK! Bug 1363756 is there to fix this.
+              this._autoResizeWorkaroundTimer = window.setTimeout(() => {
+                this._viewContainer.style.removeProperty("height");
+                this._viewContainer.style.removeProperty("width");
+              }, 500);
+
+              // Take another breather, just like before, to wait for the 'current'
+              // attribute removal to take effect. This prevents a flicker.
+              // The cleanup we do doesn't affect the display anymore, so we're not
+              // too fussed about the timing here.
+              window.addEventListener("MozAfterPaint", () => {
+                nodeToAnimate.style.removeProperty("border-inline-start");
+                nodeToAnimate.style.removeProperty("transition");
+                nodeToAnimate.style.removeProperty("transform");
+                nodeToAnimate.style.removeProperty("width");
+
+                if (!reverse)
+                  viewNode.style.removeProperty("margin-inline-start");
+                if (aAnchor)
+                  aAnchor.removeAttribute("open");
+
+                this._viewContainer.removeAttribute("transition-reverse");
+
+                this._dispatchViewEvent(viewNode, "ViewShown");
+                this._updateKeyboardFocus(viewNode);
+              }, { once: true });
+            });
+          }, { once: true });
+        });
       } else if (!this.panelViews) {
         this._transitionHeight(() => {
           viewNode.setAttribute("current", true);
           if (viewNode.id == this._mainViewId) {
             this.node.setAttribute("viewtype", "main");
           } else {
             this.node.setAttribute("viewtype", "subview");
           }
@@ -558,208 +670,16 @@ this.PanelMultiView = class {
           this._dispatchViewEvent(viewNode, "ViewShown");
         });
         this._shiftMainView(aAnchor);
       }
     })().catch(e => Cu.reportError(e));
   }
 
   /**
-   * 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.
-   *
-   * @param {panelview} previousViewNode Node that is currently shown as active,
-   *                                     but is about to be transitioned away.
-   * @param {panelview} viewNode         Node that will becode the active view,
-   *                                     after the transition has finished.
-   * @param {Boolean}   reverse          Whether we're navigation back to a
-   *                                     previous view or forward to a next view.
-   * @param {Object}    previousRect     Rect object, with the same structure as
-   *                                     a DOMRect, of the `previousViewNode`.
-   * @param {Function}  callback         Function that will be invoked when the
-   *                                     transition is finished or when the
-   *                                     operation was canceled (early return).
-   */
-  async _transitionViews(previousViewNode, viewNode, reverse, previousRect) {
-    // There's absolutely no need to show off our epic animation skillz when
-    // the panel's not even open.
-    if (this._panel.state != "open") {
-      return;
-    }
-
-    const {window, document} = this;
-
-    if (this._autoResizeWorkaroundTimer)
-      window.clearTimeout(this._autoResizeWorkaroundTimer);
-
-    this._transitionDetails = {
-      phase: TRANSITION_PHASES.START,
-      previousViewNode, viewNode, reverse
-    };
-
-    // 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);
-    } 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);
-    } else {
-      let oldSibling = viewNode.nextSibling || null;
-      this._offscreenViewStack.appendChild(viewNode);
-      viewNode.setAttribute("current", 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;
-
-    // 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
-    // that a translateX() unveils the previous view from the right direction.
-    if (reverse)
-      this._viewStack.style.marginInlineStart = "-" + deltaX + "px";
-
-    // Set the transition style and listen for its end to clean up and make sure
-    // the box sizing becomes dynamic again.
-    // Somehow, putting these properties in PanelUI.css doesn't work for newly
-    // shown nodes in a XUL parent node.
-    this._viewStack.style.transition = "transform var(--animation-easing-function)" +
-      " var(--panelui-subview-transition-duration)";
-    this._viewStack.style.willChange = "transform";
-    deepestNode.style.borderInlineStart = "1px solid var(--panel-separator-color)";
-
-    // Now set the viewContainer dimensions to that of the new view, which
-    // kicks of the height animation.
-    this._viewContainer.style.height = Math.max(viewRect.height, this._mainViewHeight) + "px";
-    this._viewContainer.style.width = viewRect.width + "px";
-    this._panel.removeAttribute("width");
-    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;
-    this._viewStack.style.transform = "translateX(" + (moveToLeft ? "" : "-") + deltaX + "px)";
-
-    await new Promise(resolve => {
-      this._transitionDetails.resolve = resolve;
-      this._viewContainer.addEventListener("transitionend", this._transitionDetails.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;
-        resolve();
-      });
-    });
-
-    this._transitionDetails.phase = TRANSITION_PHASES.END;
-
-    await this._cleanupTransitionPhase();
-  }
-
-  /**
-   * 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)
-      return;
-
-    let {phase, previousViewNode, viewNode, reverse, resolve, listener} = 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");
-    if (reverse)
-      this._resetKeyNavigation(previousViewNode);
-    this.descriptionHeightWorkaround(viewNode);
-
-    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
-      // another few milliseconds to remove the width and height 'fixtures',
-      // to be sure we don't flicker annoyingly.
-      // NB: HACK! Bug 1363756 is there to fix this.
-      this._autoResizeWorkaroundTimer = this.window.setTimeout(() => {
-        if (!this._viewContainer)
-          return;
-        this._viewContainer.style.removeProperty("height");
-        this._viewContainer.style.removeProperty("width");
-      }, 500);
-    }
-    if (phase >= TRANSITION_PHASES.PREPARE) {
-      this._transitioning = false;
-      if (reverse)
-        this._viewStack.style.removeProperty("margin-inline-start");
-      let deepestNode = reverse ? previousViewNode : viewNode;
-      deepestNode.style.removeProperty("border-inline-start");
-      this._viewStack.style.removeProperty("transition");
-    }
-    if (phase >= TRANSITION_PHASES.TRANSITION) {
-      this._viewStack.style.removeProperty("transform");
-      viewNode.style.removeProperty("width");
-      if (listener)
-        this._viewContainer.removeEventListener("transitionend", listener);
-      if (resolve)
-        resolve();
-    }
-    if (phase >= TRANSITION_PHASES.END) {
-      // We force 'display: none' on the previous view node to make sure that it
-      // doesn't cause an annoying flicker whilst resetting the styles above.
-      previousViewNode.style.display = "none";
-      await BrowserUtils.promiseLayoutFlushed(this.document, "layout", () => {});
-      previousViewNode.style.removeProperty("display");
-    }
-  }
-
-  /**
    * Helper method to emit an event on a panelview, whilst also making sure that
    * the correct method is called on CustomizableWidget instances.
    *
    * @param  {panelview} viewNode  Target of the event to dispatch.
    * @param  {String}    eventName Name of the event to dispatch.
    * @param  {DOMNode}   [anchor]  Node where the panel is anchored to. Optional.
    * @param  {Object}    [detail]  Event detail object. Optional.
    * @return {Boolean} `true` if the event was canceled by an event handler, `false`
@@ -779,16 +699,61 @@ this.PanelMultiView = class {
     });
     viewNode.dispatchEvent(evt);
     if (!cancel)
       cancel = evt.defaultPrevented;
     return cancel;
   }
 
   /**
+   * Calculate the correct bounds of a panelview node offscreen to minimize the
+   * amount of paint flashing and keep the stack vs panel layouts from interfering.
+   *
+   * @param {panelview} viewNode Node to measure the bounds of.
+   * @param {Rect}      previousRect Rect representing the previous view
+   *                                 (used to fill in any blanks).
+   * @param {Function}  callback Called when we got the measurements in and pass
+   *                             them on as its first argument.
+   */
+  _viewBoundsOffscreen(viewNode, previousRect, callback) {
+    if (viewNode.__lastKnownBoundingRect) {
+      callback(viewNode.__lastKnownBoundingRect);
+      return;
+    }
+
+    if (viewNode.customRectGetter) {
+      // Can't use Object.assign directly with a DOM Rect object because its properties
+      // aren't enumerable.
+      let {height, width} = previousRect;
+      let rect = Object.assign({height, width}, viewNode.customRectGetter());
+      let {header} = viewNode;
+      if (header) {
+        rect.height += this._dwu.getBoundsWithoutFlushing(header).height;
+      }
+      callback(rect);
+      return;
+    }
+
+    let oldSibling = viewNode.nextSibling || null;
+    this._offscreenViewStack.appendChild(viewNode);
+
+    BrowserUtils.promiseLayoutFlushed(this.document, "layout", () => {
+      return this._dwu.getBoundsWithoutFlushing(viewNode);
+    }).then(viewRect => {
+      try {
+        this._viewStack.insertBefore(viewNode, oldSibling);
+      } catch (ex) {
+        this._viewStack.appendChild(viewNode);
+      }
+
+      callback(viewRect);
+    });
+  }
+
+  /**
    * Applies the height transition for which <panelmultiview> is designed.
    *
    * The height transition involves two elements, the viewContainer and its only
    * immediate child the viewStack. In order for this to work correctly, the
    * viewContainer must have "overflow: hidden;" and the two elements must have
    * no margins or padding. This means that the height of the viewStack is never
    * limited by the viewContainer, but when the height of the container is not
    * constrained it matches the height of the viewStack.
@@ -992,17 +957,20 @@ this.PanelMultiView = class {
       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) {
-          this._cleanupTransitionPhase();
+          if (this._transitionEndListener) {
+            this._viewContainer.removeEventListener("transitionend", this._transitionEndListener);
+            this._transitionEndListener = null;
+          }
           for (let panelView of this._viewStack.children) {
             if (panelView.nodeName != "children") {
               panelView.__lastKnownBoundingRect = null;
               panelView.style.removeProperty("min-width");
               panelView.style.removeProperty("max-width");
             }
           }
           this.window.removeEventListener("keydown", this);
--- a/browser/components/customizableui/content/panelUI.css
+++ b/browser/components/customizableui/content/panelUI.css
@@ -24,21 +24,25 @@
 }
 
 .panel-subviews[panelopen] {
   transition: transform var(--panelui-subview-transition-duration);
 }
 
 .panel-viewcontainer[panelopen]:-moz-any(:not([viewtype="main"]),[transitioning]) {
   transition-property: height;
-  transition-timing-function: var(--animation-easing-function);
+  transition-timing-function: ease-in;
   transition-duration: var(--panelui-subview-transition-duration);
   will-change: height;
 }
 
+.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;
--- a/browser/components/customizableui/content/panelUI.xml
+++ b/browser/components/customizableui/content/panelUI.xml
@@ -49,19 +49,19 @@
         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:box anonid="viewStack" xbl:inherits="transitioning" class="panel-viewstack">
+        <xul:stack anonid="viewStack" xbl:inherits="transitioning" class="panel-viewstack">
           <children includes="panelview"/>
-        </xul:box>
+        </xul:stack>
       </xul:box>
       <xul:box class="panel-viewcontainer offscreen">
         <xul:box anonid="offscreenViewStack" class="panel-viewstack"/>
       </xul:box>
     </content>
   </binding>
 
   <binding id="panelview">