Bug 1374749 - Animate the panelviews differently to make it look as if the view to show is pushing the previous view out of the panel. r?Paolo draft
authorMike de Boer <mdeboer@mozilla.com>
Fri, 08 Sep 2017 11:12:26 +0100
changeset 661389 f19f3c6156f8106c4200b5404b40a90f2162717a
parent 661103 b4c1ad9565ee9d00d96501c4a83083daf25c1413
child 730545 b21034b86d2c063a34fcf104109bdeb4dc1b102f
push id78731
push userpaolo.mozmail@amadzone.org
push dateFri, 08 Sep 2017 10:13:04 +0000
reviewersPaolo
bugs1374749
milestone57.0a1
Bug 1374749 - Animate the panelviews differently to make it look as if the view to show is pushing the previous view out of the panel. r?Paolo MozReview-Commit-ID: 5u2GJhXexMO
browser/base/content/browser.css
browser/components/customizableui/PanelMultiView.jsm
browser/components/customizableui/content/panelUI.css
browser/components/customizableui/content/panelUI.xml
toolkit/themes/osx/global/global.css
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -90,16 +90,21 @@ 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/components/customizableui/PanelMultiView.jsm
+++ b/browser/components/customizableui/PanelMultiView.jsm
@@ -418,30 +418,31 @@ 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 = document.getElementById(aViewId);
+        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);
       }
@@ -480,17 +481,18 @@ 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.
+      // 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) {
@@ -504,17 +506,16 @@ 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)
           this.descriptionHeightWorkaround(viewNode);
@@ -527,140 +528,26 @@ this.PanelMultiView = class {
       // 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);
 
-        // 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");
+        await this._transitionViews(previousViewNode, viewNode, reverse, previousRect);
 
-            // 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);
-              }
+        if (aAnchor)
+          aAnchor.removeAttribute("open");
 
-              // 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 });
-        });
+        this._dispatchViewEvent(viewNode, "ViewShown");
+        this._updateKeyboardFocus(viewNode);
       } 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");
           }
@@ -670,16 +557,171 @@ 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);
+
+    // 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);
+      this.descriptionHeightWorkaround(viewNode);
+    } 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);
+      this.descriptionHeightWorkaround(viewNode);
+    } else {
+      let oldSibling = viewNode.nextSibling || null;
+      this._offscreenViewStack.appendChild(viewNode);
+      viewNode.setAttribute("current", true);
+      this.descriptionHeightWorkaround(viewNode);
+
+      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;
+
+    // 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._viewStack.style.transform = "translateX(" + (moveToLeft ? "" : "-") + deltaX + "px)";
+
+    await new Promise(resolve => {
+      this._transitionEndResolve = resolve;
+      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 != this._viewStack || ev.propertyName != "transform")
+          return;
+        this._viewContainer.removeEventListener("transitionend", this._transitionEndListener);
+        this._transitionEndListener = null;
+        resolve();
+      });
+    });
+
+    this._transitioning = false;
+
+    this._viewStack.style.removeProperty("margin-inline-start");
+    this._viewStack.style.removeProperty("transition");
+    this._viewStack.style.removeProperty("transform");
+    deepestNode.style.removeProperty("border-inline-start");
+    viewNode.style.removeProperty("width");
+
+    this._dispatchViewEvent(previousViewNode, "ViewHiding");
+    previousViewNode.removeAttribute("current");
+
+    previousViewNode.style.display = "none";
+    await BrowserUtils.promiseLayoutFlushed(document, "layout", () => {});
+    previousViewNode.style.removeProperty("display");
+
+    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(() => {
+      this._viewContainer.style.removeProperty("height");
+      this._viewContainer.style.removeProperty("width");
+    }, 500);
+
+    if (reverse) {
+      this._resetKeyNavigation(previousViewNode);
+    }
+  }
+
+  /**
    * 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`
@@ -699,61 +741,16 @@ 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.
@@ -960,16 +957,17 @@ this.PanelMultiView = class {
         this._viewShowing = null;
         this._transitioning = false;
         this.node.removeAttribute("panelopen");
         this.showMainView();
         if (this.panelViews) {
           if (this._transitionEndListener) {
             this._viewContainer.removeEventListener("transitionend", this._transitionEndListener);
             this._transitionEndListener = null;
+            this._transitionEndResolve();
           }
           for (let panelView of this._viewStack.children) {
             if (panelView.nodeName != "children") {
               panelView.__lastKnownBoundingRect = null;
               panelView.style.removeProperty("min-width");
               panelView.style.removeProperty("max-width");
             }
           }
--- a/browser/components/customizableui/content/panelUI.css
+++ b/browser/components/customizableui/content/panelUI.css
@@ -24,25 +24,21 @@
 }
 
 .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: ease-in;
+  transition-timing-function: var(--animation-easing-function);
   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:stack anonid="viewStack" xbl:inherits="transitioning" class="panel-viewstack">
+        <xul:box anonid="viewStack" xbl:inherits="transitioning" class="panel-viewstack">
           <children includes="panelview"/>
-        </xul:stack>
+        </xul:box>
       </xul:box>
       <xul:box class="panel-viewcontainer offscreen">
         <xul:box anonid="offscreenViewStack" class="panel-viewstack"/>
       </xul:box>
     </content>
   </binding>
 
   <binding id="panelview">
--- a/toolkit/themes/osx/global/global.css
+++ b/toolkit/themes/osx/global/global.css
@@ -12,17 +12,17 @@
 
 menulist > menupopup {
   -moz-binding: url("chrome://global/content/bindings/popup.xml#popup-scrollbars");
 }
 
 /* ::::: Variables ::::: */
 :root {
   --arrowpanel-padding: 16px;
-  --arrowpanel-background: linear-gradient(hsla(0,0%,99%,1), hsla(0,0%,99%,.975) 10%, hsla(0,0%,98%,.975));
+  --arrowpanel-background: hsl(0,0%,99%);
   --arrowpanel-color: hsl(0,0%,10%);
   --arrowpanel-border-color: hsla(210,4%,10%,.05);
   --arrowpanel-border-radius: 3.5px;
   --focus-ring-box-shadow: @focusRingShadow@;
 }
 
 /* ::::: root elements ::::: */