Bug 1009116 - Redo resizing architecture of panelmultiview. r=Gijs draft
authorPaolo Amadini <paolo.mozmail@amadzone.org>
Tue, 23 May 2017 17:08:01 +0100
changeset 583036 44afa09af80ecc2a39de9b6fe290d49b036c1418
parent 583035 44a4ab26de2e7f0a95e7c77f955edeef52dcfa83
child 629950 9ad8df7ce014ab0835b62d4292d85ffdc16f60b9
push id60279
push userpaolo.mozmail@amadzone.org
push dateTue, 23 May 2017 16:35:40 +0000
reviewersGijs
bugs1009116
milestone55.0a1
Bug 1009116 - Redo resizing architecture of panelmultiview. r=Gijs The height of the "panelmultiview" binding is now determined by the stack layout code, and doesn't have to be calculated manually via JavaScript anymore. This allows the removal of mutation and overflow observers, and reduces the number of synchronous layouts being made. There is still a workaround included for wrapping blocks not being taken into account in height calculations. MozReview-Commit-ID: 9rrPU5O5hUx
browser/base/content/browser.js
browser/components/controlcenter/content/panel.inc.xul
browser/components/customizableui/PanelMultiView.jsm
browser/components/customizableui/content/panelUI.css
browser/components/customizableui/content/panelUI.xml
browser/components/downloads/content/downloads.js
browser/components/downloads/content/downloadsOverlay.xul
browser/themes/shared/customizableui/panelUI.inc.css
browser/themes/shared/downloads/downloads.inc.css
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -7323,17 +7323,18 @@ var gIdentityHandler = {
     if (sharing)
       this._identityBox.setAttribute("sharing", sharing);
     else
       this._identityBox.removeAttribute("sharing");
 
     this._sharingState = tab._sharingState;
 
     if (this._identityPopup.state == "open") {
-      this._handleHeightChange(() => this.updateSitePermissions());
+      this.updateSitePermissions();
+      this._identityPopupMultiView.descriptionHeightWorkaround();
     }
   },
 
   /**
    * Attempt to provide proper IDN treatment for host names
    */
   getEffectiveHost() {
     if (!this._IDNService)
@@ -7834,30 +7835,16 @@ var gIdentityHandler = {
     if (!this._permissionList.hasChildNodes() &&
         this._permissionReloadHint.hasAttribute("hidden")) {
       this._permissionEmptyHint.removeAttribute("hidden");
     } else {
       this._permissionEmptyHint.setAttribute("hidden", "true");
     }
   },
 
-  _handleHeightChange(aFunction, aWillShowReloadHint) {
-    let heightBefore = getComputedStyle(this._permissionList).height;
-    aFunction();
-    let heightAfter = getComputedStyle(this._permissionList).height;
-    // Showing the reload hint increases the height, we need to account for it.
-    if (aWillShowReloadHint) {
-      heightAfter = parseInt(heightAfter) +
-                    parseInt(getComputedStyle(this._permissionList.nextSibling).height);
-    }
-    let heightChange = parseInt(heightAfter) - parseInt(heightBefore);
-    if (heightChange)
-      this._identityPopupMultiView.setHeightToFit(heightChange);
-  },
-
   _createPermissionItem(aPermission) {
     let container = document.createElement("hbox");
     container.setAttribute("class", "identity-popup-permission-item");
     container.setAttribute("align", "center");
 
     let img = document.createElement("image");
     let classes = "identity-popup-permission-icon " + aPermission.id + "-icon";
     if (aPermission.state == SitePermissions.BLOCK)
@@ -7884,19 +7871,17 @@ var gIdentityHandler = {
     stateLabel.textContent = SitePermissions.getCurrentStateLabel(state, scope);
 
     let button = document.createElement("button");
     button.setAttribute("class", "identity-popup-permission-remove-button");
     let tooltiptext = gNavigatorBundle.getString("permissions.remove.tooltip");
     button.setAttribute("tooltiptext", tooltiptext);
     button.addEventListener("command", () => {
       let browser = gBrowser.selectedBrowser;
-      // Only resize the window if the reload hint was previously hidden.
-      this._handleHeightChange(() => this._permissionList.removeChild(container),
-                               this._permissionReloadHint.hasAttribute("hidden"));
+      this._permissionList.removeChild(container);
       if (aPermission.inUse &&
           ["camera", "microphone", "screen"].includes(aPermission.id)) {
         let windowId = this._sharingState.windowId;
         if (aPermission.id == "screen") {
           windowId = "screen:" + windowId;
         } else {
           // If we set persistent permissions or the sharing has
           // started due to existing persistent permissions, we need
@@ -7917,16 +7902,17 @@ var gIdentityHandler = {
           }
         }
         browser.messageManager.sendAsyncMessage("webrtc:StopSharing", windowId);
         webrtcUI.forgetActivePermissionsFromBrowser(gBrowser.selectedBrowser);
       }
       SitePermissions.remove(gBrowser.currentURI, aPermission.id, browser);
 
       this._permissionReloadHint.removeAttribute("hidden");
+      this._identityPopupMultiView.descriptionHeightWorkaround();
 
       // Set telemetry values for clearing a permission
       let histogram = Services.telemetry.getKeyedHistogramById("WEB_PERMISSION_CLEARED");
 
       let permissionType = 0;
       if (aPermission.state == SitePermissions.ALLOW &&
           aPermission.scope == SitePermissions.SCOPE_PERSISTENT) {
         // 1 : clear permanently allowed permission
--- a/browser/components/controlcenter/content/panel.inc.xul
+++ b/browser/components/controlcenter/content/panel.inc.xul
@@ -11,17 +11,18 @@
        orient="vertical">
 
   <broadcasterset>
     <broadcaster id="identity-popup-mcb-learn-more" class="text-link plain" value="&identity.learnMore;"/>
     <broadcaster id="identity-popup-insecure-login-forms-learn-more" class="text-link plain" value="&identity.learnMore;"/>
   </broadcasterset>
 
   <panelmultiview id="identity-popup-multiView"
-                  mainViewId="identity-popup-mainView">
+                  mainViewId="identity-popup-mainView"
+                  descriptionheightworkaround="true">
     <panelview id="identity-popup-mainView" flex="1">
 
       <!-- Security Section -->
       <hbox id="identity-popup-security" class="identity-popup-section">
         <vbox id="identity-popup-security-content" flex="1">
           <label class="plain">
             <label class="identity-popup-headline identity-popup-host"></label>
             <label class="identity-popup-headline identity-popup-hostless" crop="end"/>
@@ -91,29 +92,29 @@
           <vbox id="identity-popup-permission-list"/>
           <description id="identity-popup-permission-reload-hint">&identity.permissionsReloadHint;</description>
           <description id="identity-popup-permission-empty-hint">&identity.permissionsEmpty;</description>
         </vbox>
       </hbox>
     </panelview>
 
     <!-- Security SubView -->
-    <panelview id="identity-popup-securityView" flex="1">
+    <panelview id="identity-popup-securityView">
       <vbox id="identity-popup-securityView-header">
         <label class="plain">
           <label class="identity-popup-headline identity-popup-host"></label>
           <label class="identity-popup-headline identity-popup-hostless" crop="end"/>
         </label>
         <description class="identity-popup-connection-not-secure"
                      when-connection="not-secure secure-cert-user-overridden">&identity.connectionNotSecure;</description>
         <description class="identity-popup-connection-secure"
                      when-connection="secure secure-ev">&identity.connectionSecure;</description>
       </vbox>
 
-      <vbox id="identity-popup-securityView-body" flex="1">
+      <vbox id="identity-popup-securityView-body" class="panel-view-body-unscrollable">
         <!-- (EV) Certificate Information -->
         <description id="identity-popup-content-verified-by"
                      when-connection="secure-ev">&identity.connectionVerified2;</description>
         <description id="identity-popup-content-owner"
                      when-connection="secure-ev"
                      class="header"/>
         <description id="identity-popup-content-supplemental"
                      when-connection="secure-ev"/>
--- a/browser/components/customizableui/PanelMultiView.jsm
+++ b/browser/components/customizableui/PanelMultiView.jsm
@@ -166,30 +166,16 @@ this.PanelMultiView = class {
   }
   get _mainView() {
     return this._mainViewId ? this.document.getElementById(this._mainViewId) : null;
   }
   get showingSubViewAsMainView() {
     return this.node.getAttribute("mainViewIsSubView") == "true";
   }
 
-  get ignoreMutations() {
-    return this._ignoreMutations;
-  }
-  set ignoreMutations(val) {
-    this._ignoreMutations = val;
-    if (!val && this._panel.state == "open") {
-      if (this.showingSubView) {
-        this._syncContainerWithSubView();
-      } else {
-        this._syncContainerWithMainView();
-      }
-    }
-  }
-
   get _transitioning() {
     return this.__transitioning;
   }
   set _transitioning(val) {
     this.__transitioning = val;
     if (val) {
       this.node.setAttribute("transitioning", "true");
     } else {
@@ -259,23 +245,16 @@ this.PanelMultiView = class {
       // Set CSS-determined attributes now to prevent a layout flush when we do
       // it when transitioning between panels.
       this._dir = cs.direction;
       this.setMainView(this.panelViews.currentView);
       this.showMainView();
     } else {
       this._panel.addEventListener("popupshown", this);
       this._clickCapturer.addEventListener("click", 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));
 
       this._mainViewContainer.setAttribute("panelid", this._panel.id);
 
       if (this._mainView) {
         this.setMainView(this._mainView);
       }
     }
 
@@ -285,35 +264,32 @@ this.PanelMultiView = class {
     // 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
       });
     });
-    ["goBack", "setHeightToFit", "setMainView", "showMainView", "showSubView"].forEach(method => {
+    ["goBack", "descriptionHeightWorkaround", "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");
     }
     if (this.panelViews) {
       this.panelViews.clear();
     } else {
-      this._mainViewObserver.disconnect();
-      this._subViewObserver.disconnect();
-      this._subViews.removeEventListener("overflow", this);
-      this._mainViewContainer.removeEventListener("overflow", this);
       this._clickCapturer.removeEventListener("click", this);
     }
     this._panel.removeEventListener("popupshowing", this);
     this._panel.removeEventListener("popupshown", this);
     this._panel.removeEventListener("popuphidden", this);
     this.node = this._clickCapturer = this._viewContainer = this._mainViewContainer =
       this._subViews = this._viewStack = this.__dwu = null;
   }
@@ -340,17 +316,16 @@ this.PanelMultiView = class {
     if (this.panelViews) {
       // 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);
       }
     } else {
       if (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);
     }
   }
@@ -359,24 +334,21 @@ this.PanelMultiView = class {
     if (this.panelViews) {
       this.showSubView(this._mainViewId);
     } else {
       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;
-
-        this._subViewObserver.disconnect();
-
-        this._setViewContainerHeight(this._mainViewHeight);
-
-        this.node.setAttribute("viewtype", "main");
+        this._transitionHeight(() => {
+          viewNode.removeAttribute("current");
+          this._currentSubView = null;
+          this.node.setAttribute("viewtype", "main");
+        });
       }
 
       this._shiftMainView();
     }
   }
 
   showSubView(aViewId, aAnchor, aPreviousView, aAdopted = false) {
     const {document, window} = this;
@@ -465,19 +437,19 @@ this.PanelMultiView = class {
       //    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");
+      if (this.panelViews && playTransition) {
+        this.node.setAttribute("viewtype", "subview");
 
-      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 = () => {
           evt = new window.CustomEvent("ViewHiding", { bubbles: true, cancelable: true });
           previousViewNode.dispatchEvent(evt);
           previousViewNode.removeAttribute("current");
         };
@@ -587,44 +559,96 @@ this.PanelMultiView = class {
               }, { once: true });
               ++seen;
             }
             if (seen == 2)
               this._viewContainer.removeEventListener("transitionend", listener);
           });
         }, { once: true });
       } else if (!this.panelViews) {
+        this._transitionHeight(() => {
+          viewNode.setAttribute("current", true);
+          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._shiftMainView(aAnchor);
-
-        this._mainViewHeight = this._viewStack.clientHeight;
-
-        let newHeight = this._heightOfSubview(viewNode, this._subViews);
-        this._setViewContainerHeight(newHeight);
-
-        this._subViewObserver.observe(viewNode, {
-          attributes: true,
-          characterData: true,
-          childList: true,
-          subtree: true
-        });
       }
     })();
   }
 
-  _setViewContainerHeight(aHeight) {
-    let container = this._viewContainer;
-    this._transitioning = true;
+  /**
+   * 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.
+   *
+   * @param changeFn
+   *        This synchronous function is called to make the DOM changes
+   *        that will result in a new height of the viewStack.
+   */
+  _transitionHeight(changeFn) {
+    if (this._panel.state != "open") {
+      changeFn();
+      return;
+    }
+
+    // Lock the dimensions of the window that hosts the popup panel. This
+    // in turn constrains the height of the viewContainer.
+    let rect = this._panel.popupBoxObject.getOuterScreenRect();
+    this._panel.setAttribute("width", rect.width);
+    this._panel.setAttribute("height", rect.height);
+
+    // Read the current height of the viewStack. If we are in the middle
+    // of a transition, this is the actual height of the element at this
+    // point in time.
+    let oldHeight = this._dwu.getBoundsWithoutFlushing(this._viewStack).height;
 
-    let onTransitionEnd = () => {
-      container.removeEventListener("transitionend", onTransitionEnd);
-      this._transitioning = false;
-    };
+    // Make the necessary DOM changes, and remove the "height" property of the
+    // viewStack to ensure that we read its final value even if we are in the
+    // middle of a transition. To avoid flickering, we have to prevent the panel
+    // from being painted in this temporary state, which requires a synchronous
+    // layout when reading the new height.
+    this._viewStack.style.removeProperty("height");
+    changeFn();
+    let newHeight = this._viewStack.getBoundingClientRect().height;
+
+    // Now we can allow the popup panel to resize again. This must occur
+    // in the same tick as the code below, but we can do this before
+    // setting the starting height in case the transition is not needed.
+    this._panel.removeAttribute("width");
+    this._panel.removeAttribute("height");
 
-    container.addEventListener("transitionend", onTransitionEnd);
-    container.style.height = `${aHeight}px`;
+    if (oldHeight != newHeight) {
+      // Height transitions can only occur between two numeric values, and
+      // cannot start if the height is not set. In case a transition is
+      // needed, we have to set the height to the old value, then force a
+      // synchronous layout so the panel won't resize unexpectedly.
+      this._viewStack.style.height = oldHeight + "px";
+      this._viewStack.getBoundingClientRect().height;
+
+      // We can now set the new height to start the transition, but
+      // before doing that we set up a listener to reset the height to
+      // "auto" at the end, so that DOM changes made after the
+      // transition ends are still reflected by the height of the panel.
+      let onTransitionEnd = event => {
+        if (event.target != this._viewStack) {
+          return;
+        }
+        this._viewStack.removeEventListener("transitionend", onTransitionEnd);
+        this._viewStack.style.removeProperty("height");
+      };
+      this._viewStack.addEventListener("transitionend", onTransitionEnd);
+      this._viewStack.style.height = newHeight + "px";
+    }
   }
 
   _shiftMainView(aAnchor) {
     if (aAnchor) {
       // We need to find the edge of the anchor, relative to the main panel.
       // Then we need to add half the width of the anchor. This is the target
       // that we need to transition to.
       let anchorRect = aAnchor.getBoundingClientRect();
@@ -670,58 +694,64 @@ this.PanelMultiView = class {
         }
         break;
       case "keydown":
         this._keyNavigation(aEvent);
         break;
       case "mousemove":
         this._resetKeyNavigation();
         break;
-      case "overflow":
-        if (!this.panelViews && aEvent.target.localName == "vbox") {
-          // Resize the right view on the next tick.
-          if (this.showingSubView) {
-            this.window.setTimeout(this._syncContainerWithSubView.bind(this), 0);
-          } else if (!this.transitioning) {
-            this.window.setTimeout(this._syncContainerWithMainView.bind(this), 0);
-          }
-        }
-        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;
-
-        if (!this.panelViews) {
-          this._syncContainerWithMainView();
-          this._mainViewObserver.observe(this._mainView, {
-            attributes: true,
-            characterData: true,
-            childList: true,
-            subtree: true
-          });
-        } else {
+        if (this.panelViews) {
           this.window.addEventListener("keydown", this);
           this._panel.addEventListener("mousemove", this);
         }
         break;
       case "popupshown":
-        this._setMaxHeight();
+        // Now that the main view is visible, we can check the height of the
+        // description elements it contains.
+        this.descriptionHeightWorkaround();
+        // Now that the panel has opened, we can compute the distance from its
+        // anchor to the available margin of the screen, based on whether the
+        // panel actually opened towards the top or the bottom. We use this to
+        // limit its maximum height, which is relevant when opening a subview.
+        let maxHeight;
+        if (this._panel.alignmentPosition.startsWith("before_")) {
+          maxHeight = this._panel.getOuterScreenRect().bottom -
+                      this.window.screen.availTop;
+        } else {
+          maxHeight = this.window.screen.availTop +
+                      this.window.screen.availHeight -
+                      this._panel.getOuterScreenRect().top;
+        }
+        // To go from the maximum height of the panel to the maximum height of
+        // the view stack, we start by subtracting the height of the arrow box.
+        // We don't need to trigger a new layout because this does not change.
+        let arrowBox = this.document.getAnonymousElementByAttribute(
+                                        this._panel, "anonid", "arrowbox");
+        maxHeight -= this._dwu.getBoundsWithoutFlushing(arrowBox).height;
+        // We subtract a fixed margin to account for variable borders. We don't
+        // try to measure this accurately so it's faster, we don't depend on
+        // the arrowpanel structure, and we don't hit rounding errors. Instead,
+        // we use a value that is much greater than the typical borders and
+        // makes sense visually.
+        const EXTRA_MARGIN_PX = 8;
+        this._viewStack.style.maxHeight = (maxHeight - EXTRA_MARGIN_PX) + "px";
         break;
       case "popuphidden":
         this.node.removeAttribute("panelopen");
-        this._mainView.style.removeProperty("height");
         this.showMainView();
-        if (!this.panelViews) {
-          this._mainViewObserver.disconnect();
-        } else {
+        if (this.panelViews) {
           this.window.removeEventListener("keydown", this);
           this._panel.removeEventListener("mousemove", this);
           this._resetKeyNavigation();
         }
         break;
     }
   }
 
@@ -854,138 +884,52 @@ this.PanelMultiView = class {
    */
   _getNavigableElements(view) {
     let buttons = Array.from(view.querySelectorAll(".subviewbutton:not([disabled])"));
     if (this._canGoBack(view))
       buttons.unshift(view.backButton);
     return buttons;
   }
 
-  _shouldSetPosition() {
-    return this.node.getAttribute("nosubviews") == "true";
-  }
-
-  _shouldSetHeight() {
-    return this.node.getAttribute("nosubviews") != "true";
-  }
-
-  _setMaxHeight() {
-    if (!this._shouldSetHeight())
-      return;
-
-    // Ignore the mutation that'll fire when we set the height of
-    // the main view.
-    this.ignoreMutations = true;
-    this._mainView.style.height = this.node.getBoundingClientRect().height + "px";
-    this.ignoreMutations = false;
-  }
-
-  _adjustContainerHeight() {
-    if (!this.ignoreMutations && !this.showingSubView && !this._transitioning) {
-      let height;
-      if (this.showingSubViewAsMainView) {
-        height = this._heightOfSubview(this._mainView);
-      } else {
-        height = this._mainView.scrollHeight;
-      }
-      this._viewContainer.style.height = height + "px";
-    }
-  }
-
-  _syncContainerWithSubView() {
-    // Check that this panel is still alive:
-    if (!this._panel || !this._panel.parentNode) {
-      return;
-    }
-
-    if (!this.ignoreMutations && this.showingSubView) {
-      let newHeight = this._heightOfSubview(this._currentSubView, this._subViews);
-      this._viewContainer.style.height = newHeight + "px";
-    }
-  }
-
-  _syncContainerWithMainView() {
-    // Check that this panel is still alive:
-    if (!this._panel || !this._panel.parentNode) {
+  /**
+   * If the main view or a subview contains wrapping elements, the attribute
+   * "descriptionheightworkaround" should be set on the view to force all the
+   * "description" elements to a fixed height. If the attribute is set and the
+   * visibility, contents, or width of any of these elements changes, this
+   * function should be called to refresh the calculated heights.
+   *
+   * @note While both "label" and "description" elements may contain wrapping
+   *       text, only "description" elements are used that way in panels.
+   *
+   * @param viewNode
+   *        Indicates the node to scan for descendant elements. This is the main
+   *        view if omitted.
+   */
+  descriptionHeightWorkaround(viewNode = this._mainView) {
+    if (!this.node.hasAttribute("descriptionheightworkaround")) {
+      // This view does not require the workaround.
       return;
     }
 
-    if (this._shouldSetPosition()) {
-      this._panel.adjustArrowPosition();
+    // 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 = [];
+    for (let element of viewNode.getElementsByTagName("description")) {
+      element.style.removeProperty("height");
+      items.push({ element });
     }
-
-    if (this._shouldSetHeight()) {
-      this._adjustContainerHeight();
+    // We now read the computed style to store the height of any element that
+    // may contain wrapping text, which will be zero if the element is hidden.
+    // This might trigger a synchronous layout, but if this function was called
+    // from a _transitionHeight callback and there are no description elements
+    // visible, then _transitionHeight will not trigger a layout again.
+    for (let item of items) {
+      item.height = item.element.getBoundingClientRect().height;
+    }
+    // Now we can make all the necessary DOM changes at once.
+    for (let item of items) {
+      if (item.height) {
+        item.element.style.height = item.height + "px";
+      }
     }
   }
-
-  /**
-   * Call this when the height of one of your views (the main view or a
-   * subview) changes and you want the heights of the multiview and panel
-   * to be the same as the view's height.
-   * If the caller can give a hint of the expected height change with the
-   * optional aExpectedChange parameter, it prevents flicker.
-   */
-  setHeightToFit(aExpectedChange) {
-    // Set the max-height to zero, wait until the height is actually
-    // updated, and then remove it.  If it's not removed, weird things can
-    // happen, like widgets in the panel won't respond to clicks even
-    // though they're visible.
-    const {window} = this;
-    let count = 5;
-    let height = window.getComputedStyle(this.node).height;
-    if (aExpectedChange)
-      this.node.style.maxHeight = (parseInt(height, 10) + aExpectedChange) + "px";
-    else
-      this.node.style.maxHeight = "0";
-    let interval = window.setInterval(() => {
-      if (height != window.getComputedStyle(this.node).height || --count == 0) {
-        window.clearInterval(interval);
-        this.node.style.removeProperty("max-height");
-      }
-    }, 0);
-  }
-
-  _heightOfSubview(aSubview, aContainerToCheck) {
-    function getFullHeight(element) {
-      // XXXgijs: unfortunately, scrollHeight rounds values, and there's no alternative
-      // that works with overflow: auto elements. Fortunately for us,
-      // we have exactly 1 (potentially) scrolling element in here (the subview body),
-      // and rounding 1 value is OK - rounding more than 1 and adding them means we get
-      // off-by-1 errors. Now we might be off by a subpixel, but we care less about that.
-      // So, use scrollHeight *only* if the element is vertically scrollable.
-      let height;
-      let elementCS;
-      if (element.scrollTopMax) {
-        height = element.scrollHeight;
-        // Bounding client rects include borders, scrollHeight doesn't:
-        elementCS = win.getComputedStyle(element);
-        height += parseFloat(elementCS.borderTopWidth) +
-                  parseFloat(elementCS.borderBottomWidth);
-      } else {
-        height = element.getBoundingClientRect().height;
-        if (height > 0) {
-          elementCS = win.getComputedStyle(element);
-        }
-      }
-      if (elementCS) {
-        // Include margins - but not borders or paddings because they
-        // were dealt with above.
-        height += parseFloat(elementCS.marginTop) + parseFloat(elementCS.marginBottom);
-      }
-      return height;
-    }
-    let win = aSubview.ownerGlobal;
-    let body = aSubview.querySelector(".panel-subview-body");
-    let height = getFullHeight(body || aSubview);
-    if (body) {
-      let header = aSubview.querySelector(".panel-subview-header");
-      let footer = aSubview.querySelector(".panel-subview-footer");
-      height += (header ? getFullHeight(header) : 0) +
-                (footer ? getFullHeight(footer) : 0);
-    }
-    if (aContainerToCheck) {
-      let containerCS = win.getComputedStyle(aContainerToCheck);
-      height += parseFloat(containerCS.paddingTop) + parseFloat(containerCS.paddingBottom);
-    }
-    return Math.ceil(height);
-  }
 }
--- a/browser/components/customizableui/content/panelUI.css
+++ b/browser/components/customizableui/content/panelUI.css
@@ -1,30 +1,31 @@
 /* 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/. */
 
 .panel-viewstack[viewtype="main"] > .panel-clickcapturer {
   pointer-events: none;
 }
 
-.panel-mainview,
-.panel-viewcontainer,
-.panel-viewstack {
+.panel-viewcontainer {
   overflow: hidden;
 }
 
 .panel-viewstack {
-  position: relative;
+  transition: height var(--panelui-subview-transition-duration);
 }
 
 .panel-subviews {
-  -moz-stack-sizing: ignore;
+  -moz-stack-sizing: ignore-horizontal;
   transform: translateX(0);
-  overflow-y: auto;
+}
+
+.panel-viewstack[viewtype="main"] > .panel-subviews {
+  -moz-stack-sizing: ignore;
 }
 
 .panel-subviews[panelopen] {
   transition: transform var(--panelui-subview-transition-duration);
 }
 
 .panel-viewcontainer[panelopen]:-moz-any(:not([viewtype="main"]),[transitioning]) {
   transition-property: height;
--- a/browser/components/customizableui/content/panelUI.xml
+++ b/browser/components/customizableui/content/panelUI.xml
@@ -13,17 +13,17 @@
           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:vbox anonid="viewContainer" class="panel-viewcontainer" xbl:inherits="panelopen,viewtype,transitioning">
         <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"/>
@@ -32,28 +32,28 @@
                subviews that are not being displayed. We're using this over a deck
                because a deck assumes the size of its largest child, regardless of
                whether or not it is shown. That's not good for our case, since we
                want to allow each subview to be uniquely sized. -->
           <xul:vbox anonid="subViews" class="panel-subviews" xbl:inherits="panelopen">
             <children includes="panelview"/>
           </xul:vbox>
         </xul:stack>
-      </xul:box>
+      </xul:vbox>
     </content>
     <implementation>
       <constructor><![CDATA[
         const {PanelMultiView} = Components.utils.import("resource:///modules/PanelMultiView.jsm", {});
         this.instance = new PanelMultiView(this);
-       ]]></constructor>
- 
-       <destructor><![CDATA[
+      ]]></constructor>
+
+      <destructor><![CDATA[
         this.instance.destructor();
-       ]]></destructor>
-     </implementation>
+      ]]></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>
--- a/browser/components/downloads/content/downloads.js
+++ b/browser/components/downloads/content/downloads.js
@@ -298,30 +298,16 @@ const DownloadsPanel = {
         this.keyFocusing = false;
         break;
       case "keydown":
         this._onKeyDown(aEvent);
         break;
       case "keypress":
         this._onKeyPress(aEvent);
         break;
-      case "popupshown":
-        if (this.setHeightToFitOnShow) {
-          this.setHeightToFitOnShow = false;
-          this.setHeightToFit();
-        }
-        break;
-    }
-  },
-
-  setHeightToFit() {
-    if (this._state == this.kStateShown) {
-      DownloadsBlockedSubview.view.setHeightToFit();
-    } else {
-      this.setHeightToFitOnShow = true;
     }
   },
 
   // Callback functions from DownloadsView
 
   /**
    * Called after data loading finished.
    */
@@ -401,28 +387,25 @@ const DownloadsPanel = {
    * panel has successfully loaded.
    */
   _attachEventListeners() {
     // Handle keydown to support accel-V.
     this.panel.addEventListener("keydown", this);
     // Handle keypress to be able to preventDefault() events before they reach
     // the richlistbox, for keyboard navigation.
     this.panel.addEventListener("keypress", this);
-    // Handle height adjustment on show.
-    this.panel.addEventListener("popupshown", this);
   },
 
   /**
    * Unattach event listeners that were added in _attachEventListeners. This
    * is called automatically on panel termination.
    */
   _unattachEventListeners() {
     this.panel.removeEventListener("keydown", this);
     this.panel.removeEventListener("keypress", this);
-    this.panel.removeEventListener("popupshown", this);
   },
 
   _onKeyPress(aEvent) {
     // Handle unmodified keys only.
     if (aEvent.altKey || aEvent.ctrlKey || aEvent.shiftKey || aEvent.metaKey) {
       return;
     }
 
@@ -853,19 +836,16 @@ const DownloadsView = {
       this._removeViewItem(download);
       if (this._downloads.length >= this.kItemCountLimit) {
         // Reinsert the next item into the panel.
         this._addViewItem(this._downloads[this.kItemCountLimit - 1], false);
       }
     }
 
     this._itemCountChanged();
-
-    // Adjust the panel height if we removed items.
-    DownloadsPanel.setHeightToFit();
   },
 
   /**
    * Associates each richlistitem for a download with its corresponding
    * DownloadsViewItem object.
    */
   _itemsForElements: new Map(),
 
@@ -1540,20 +1520,16 @@ const DownloadsFooter = {
    */
   set showingSummary(aValue) {
     if (this._footerNode) {
       if (aValue) {
         this._footerNode.setAttribute("showingsummary", "true");
       } else {
         this._footerNode.removeAttribute("showingsummary");
       }
-      if (!aValue && this._showingSummary) {
-        // Make sure the panel's height shrinks when the summary is hidden.
-        DownloadsPanel.setHeightToFit();
-      }
       this._showingSummary = aValue;
     }
     return aValue;
   },
 
   /**
    * Element corresponding to the footer of the downloads panel.
    */
--- a/browser/components/downloads/content/downloadsOverlay.xul
+++ b/browser/components/downloads/content/downloadsOverlay.xul
@@ -103,32 +103,30 @@
                   accesskey="&cmd.removeFromHistory.accesskey;"/>
         <menuitem command="downloadsCmd_clearList"
                   label="&cmd.clearList2.label;"
                   accesskey="&cmd.clearList2.accesskey;"/>
       </menupopup>
 
       <panelmultiview id="downloadsPanel-multiView"
                       mainViewId="downloadsPanel-mainView"
-                      align="stretch">
+                      descriptionheightworkaround="true">
 
-        <panelview id="downloadsPanel-mainView"
-                   flex="1"
-                   align="stretch">
-          <richlistbox id="downloadsListBox"
-                       context="downloadsContextMenu"
-                       onmouseover="DownloadsView.onDownloadMouseOver(event);"
-                       onmouseout="DownloadsView.onDownloadMouseOut(event);"
-                       oncontextmenu="DownloadsView.onDownloadContextMenu(event);"
-                       ondragstart="DownloadsView.onDownloadDragStart(event);"/>
-          <description id="emptyDownloads"
-                       mousethrough="always">
-             &downloadsPanelEmpty.label;
-          </description>
-          <spacer flex="1"/>
+        <panelview id="downloadsPanel-mainView">
+          <vbox class="panel-view-body-unscrollable">
+            <richlistbox id="downloadsListBox"
+                         context="downloadsContextMenu"
+                         onmouseover="DownloadsView.onDownloadMouseOver(event);"
+                         onmouseout="DownloadsView.onDownloadMouseOut(event);"
+                         oncontextmenu="DownloadsView.onDownloadContextMenu(event);"
+                         ondragstart="DownloadsView.onDownloadDragStart(event);"/>
+            <description id="emptyDownloads"
+                         mousethrough="always"
+                         value="&downloadsPanelEmpty.label;"/>
+          </vbox>
           <vbox id="downloadsFooter"
                 class="downloadsPanelFooter">
             <stack>
               <hbox id="downloadsSummary"
                     align="center"
                     orient="horizontal"
                     onkeydown="DownloadsSummary.onKeyDown(event);"
                     onclick="DownloadsSummary.onClick(event);">
@@ -154,23 +152,22 @@
                         accesskey="&downloadsHistory.accesskey;"
                         flex="1"
                         oncommand="DownloadsPanel.showDownloadsHistory();"/>
               </hbox>
             </stack>
           </vbox>
         </panelview>
 
-        <panelview id="downloadsPanel-blockedSubview"
-                   orient="vertical"
-                   flex="1">
-          <description id="downloadsPanel-blockedSubview-title"/>
-          <description id="downloadsPanel-blockedSubview-details1"/>
-          <description id="downloadsPanel-blockedSubview-details2"/>
-          <spacer flex="1"/>
+        <panelview id="downloadsPanel-blockedSubview">
+          <vbox class="panel-view-body-unscrollable">
+            <description id="downloadsPanel-blockedSubview-title"/>
+            <description id="downloadsPanel-blockedSubview-details1"/>
+            <description id="downloadsPanel-blockedSubview-details2"/>
+          </vbox>
           <hbox id="downloadsPanel-blockedSubview-buttons"
                 class="downloadsPanelFooter"
                 align="stretch">
             <button id="downloadsPanel-blockedSubview-openButton"
                     class="downloadsPanelFooterButton"
                     command="downloadsCmd_unblockAndOpen"
                     flex="1"/>
             <toolbarseparator/>
--- a/browser/themes/shared/customizableui/panelUI.inc.css
+++ b/browser/themes/shared/customizableui/panelUI.inc.css
@@ -193,26 +193,32 @@
 .panel-viewstack[viewtype="main"] > .panel-subviews:-moz-locale-dir(rtl) {
   transform: translateX(-@menuPanelWidth@);
 }
 
 panelmultiview[nosubviews=true] > .panel-viewcontainer > .panel-viewstack > .panel-subviews {
   display: none;
 }
 
-.panel-viewstack:not([viewtype="main"]) > .panel-mainview > #PanelUI-mainView {
+panelview {
+  -moz-box-orient: vertical;
   -moz-box-flex: 1;
 }
 
 .panel-subview-body {
   overflow-y: auto;
   overflow-x: hidden;
   -moz-box-flex: 1;
 }
 
+.panel-view-body-unscrollable {
+  overflow: hidden;
+  -moz-box-flex: 1;
+}
+
 #PanelUI-popup .panel-subview-body {
   margin: -4px;
   padding: 4px 4px;
 }
 
 .panel-subview-header,
 .subviewbutton.panel-subview-footer {
   box-sizing: border-box;
@@ -220,16 +226,18 @@ panelmultiview[nosubviews=true] > .panel
   padding: 11px 12px;
 }
 
 .panel-subview-header {
   margin: -4px -4px 4px;
   border-bottom: 1px solid var(--panel-separator-color);
   color: GrayText;
   font-variant: small-caps;
+  /* Workaround for min-height not being accounted for in vertical layout. */
+  height: 41px;
 }
 
 .cui-widget-panelview .panel-subview-header,
 photonpanelmultiview .panel-subview-header {
   display: none;
 }
 
 .cui-widget-panelview .subviewbutton.panel-subview-footer {
--- a/browser/themes/shared/downloads/downloads.inc.css
+++ b/browser/themes/shared/downloads/downloads.inc.css
@@ -10,17 +10,16 @@
 %define itemFinished @item@[state="1"]
 %define itemNotFinished @item@:not([state="1"])
 %define itemFocused #downloadsListBox:focus > @item@[selected]
 
 /*** Panel and outer controls ***/
 
 #downloadsPanel > .panel-arrowcontainer > .panel-arrowcontent {
   overflow: hidden;
-  display: block;
 }
 
 #downloadsPanel > .panel-arrowcontainer > .panel-arrowcontent,
 #downloadsPanel-multiView > .panel-viewcontainer > .panel-viewstack > .panel-subviews {
   padding: 0;
 }
 
 #downloadsListBox {