Bug 1407228 - Collapsed sections should be animated when they are reopened. r=r1cky, a=gchang
authorEd Lee <edilee@mozilla.com>
Sat, 02 Dec 2017 16:14:51 -0700
changeset 445109 699b253725efc849c4587b8c961f65f3e97d0bce
parent 445108 18524c7a40b32c1bbc5d47bf8f0fa8c80fdd0cb1
child 445110 89e5df69214e2970045b29bc16e6611642f2f3ee
push id1618
push userCallek@gmail.com
push dateThu, 11 Jan 2018 17:45:48 +0000
treeherdermozilla-release@882ca853e05a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersr1cky, gchang
bugs1407228
milestone58.0
Bug 1407228 - Collapsed sections should be animated when they are reopened. r=r1cky, a=gchang MozReview-Commit-ID: B8WVq8h6uAa
browser/extensions/activity-stream/css/activity-stream-linux.css
browser/extensions/activity-stream/css/activity-stream-mac.css
browser/extensions/activity-stream/css/activity-stream-windows.css
browser/extensions/activity-stream/data/content/activity-stream.bundle.js
browser/extensions/activity-stream/install.rdf.in
--- a/browser/extensions/activity-stream/css/activity-stream-linux.css
+++ b/browser/extensions/activity-stream/css/activity-stream-linux.css
@@ -578,18 +578,17 @@ section.top-sites:not(.collapsed):hover 
       offset-inline-start: auto;
       offset-inline-end: 0; } }
 
 .sections-list .section-empty-state {
   width: 100%;
   height: 266px;
   display: flex;
   border: 1px solid #D7D7DB;
-  border-radius: 3px;
-  margin-bottom: 16px; }
+  border-radius: 3px; }
   .sections-list .section-empty-state .empty-state {
     margin: auto;
     max-width: 350px; }
     .sections-list .section-empty-state .empty-state .empty-state-icon {
       background-size: 50px 50px;
       background-repeat: no-repeat;
       background-position: center;
       fill: rgba(12, 12, 13, 0.6);
@@ -1247,21 +1246,21 @@ section.top-sites:not(.collapsed):hover 
     .collapsible-section .section-disclaimer button:hover:not(.dismiss) {
       box-shadow: 0 0 0 5px #D7D7DB;
       transition: box-shadow 150ms; }
     @media (min-width: 416px) {
       .collapsible-section .section-disclaimer button {
         position: absolute; } }
 
 .collapsible-section .section-body {
-  max-height: 1100px;
   margin: 0 -7px;
   padding: 0 7px; }
   .collapsible-section .section-body.animating {
-    overflow: hidden; }
+    overflow: hidden;
+    pointer-events: none; }
 
 .collapsible-section.animation-enabled .section-title .icon-arrowhead-down,
 .collapsible-section.animation-enabled .section-title .icon-arrowhead-forward {
   transition: transform 0.5s cubic-bezier(0.07, 0.95, 0, 1); }
 
 .collapsible-section.animation-enabled .section-body {
   transition: max-height 0.5s cubic-bezier(0.07, 0.95, 0, 1); }
 
--- a/browser/extensions/activity-stream/css/activity-stream-mac.css
+++ b/browser/extensions/activity-stream/css/activity-stream-mac.css
@@ -578,18 +578,17 @@ section.top-sites:not(.collapsed):hover 
       offset-inline-start: auto;
       offset-inline-end: 0; } }
 
 .sections-list .section-empty-state {
   width: 100%;
   height: 266px;
   display: flex;
   border: 1px solid #D7D7DB;
-  border-radius: 3px;
-  margin-bottom: 16px; }
+  border-radius: 3px; }
   .sections-list .section-empty-state .empty-state {
     margin: auto;
     max-width: 350px; }
     .sections-list .section-empty-state .empty-state .empty-state-icon {
       background-size: 50px 50px;
       background-repeat: no-repeat;
       background-position: center;
       fill: rgba(12, 12, 13, 0.6);
@@ -1247,21 +1246,21 @@ section.top-sites:not(.collapsed):hover 
     .collapsible-section .section-disclaimer button:hover:not(.dismiss) {
       box-shadow: 0 0 0 5px #D7D7DB;
       transition: box-shadow 150ms; }
     @media (min-width: 416px) {
       .collapsible-section .section-disclaimer button {
         position: absolute; } }
 
 .collapsible-section .section-body {
-  max-height: 1100px;
   margin: 0 -7px;
   padding: 0 7px; }
   .collapsible-section .section-body.animating {
-    overflow: hidden; }
+    overflow: hidden;
+    pointer-events: none; }
 
 .collapsible-section.animation-enabled .section-title .icon-arrowhead-down,
 .collapsible-section.animation-enabled .section-title .icon-arrowhead-forward {
   transition: transform 0.5s cubic-bezier(0.07, 0.95, 0, 1); }
 
 .collapsible-section.animation-enabled .section-body {
   transition: max-height 0.5s cubic-bezier(0.07, 0.95, 0, 1); }
 
--- a/browser/extensions/activity-stream/css/activity-stream-windows.css
+++ b/browser/extensions/activity-stream/css/activity-stream-windows.css
@@ -578,18 +578,17 @@ section.top-sites:not(.collapsed):hover 
       offset-inline-start: auto;
       offset-inline-end: 0; } }
 
 .sections-list .section-empty-state {
   width: 100%;
   height: 266px;
   display: flex;
   border: 1px solid #D7D7DB;
-  border-radius: 3px;
-  margin-bottom: 16px; }
+  border-radius: 3px; }
   .sections-list .section-empty-state .empty-state {
     margin: auto;
     max-width: 350px; }
     .sections-list .section-empty-state .empty-state .empty-state-icon {
       background-size: 50px 50px;
       background-repeat: no-repeat;
       background-position: center;
       fill: rgba(12, 12, 13, 0.6);
@@ -1247,21 +1246,21 @@ section.top-sites:not(.collapsed):hover 
     .collapsible-section .section-disclaimer button:hover:not(.dismiss) {
       box-shadow: 0 0 0 5px #D7D7DB;
       transition: box-shadow 150ms; }
     @media (min-width: 416px) {
       .collapsible-section .section-disclaimer button {
         position: absolute; } }
 
 .collapsible-section .section-body {
-  max-height: 1100px;
   margin: 0 -7px;
   padding: 0 7px; }
   .collapsible-section .section-body.animating {
-    overflow: hidden; }
+    overflow: hidden;
+    pointer-events: none; }
 
 .collapsible-section.animation-enabled .section-title .icon-arrowhead-down,
 .collapsible-section.animation-enabled .section-title .icon-arrowhead-forward {
   transition: transform 0.5s cubic-bezier(0.07, 0.95, 0, 1); }
 
 .collapsible-section.animation-enabled .section-body {
   transition: max-height 0.5s cubic-bezier(0.07, 0.95, 0, 1); }
 
--- a/browser/extensions/activity-stream/data/content/activity-stream.bundle.js
+++ b/browser/extensions/activity-stream/data/content/activity-stream.bundle.js
@@ -981,16 +981,19 @@ const VISIBILITY_CHANGE_EVENT = "visibil
 
 function getFormattedMessage(message) {
   return typeof message === "string" ? React.createElement(
     "span",
     null,
     message
   ) : React.createElement(FormattedMessage, message);
 }
+function getCollapsed(props) {
+  return props.Prefs.values[props.prefName];
+}
 
 class Info extends React.PureComponent {
   constructor(props) {
     super(props);
     this.onInfoEnter = this.onInfoEnter.bind(this);
     this.onInfoLeave = this.onInfoLeave.bind(this);
     this.onManageClick = this.onManageClick.bind(this);
     this.state = { infoActive: false };
@@ -1108,27 +1111,38 @@ class Disclaimer extends React.PureCompo
   }
 }
 
 const DisclaimerIntl = injectIntl(Disclaimer);
 
 class CollapsibleSection extends React.PureComponent {
   constructor(props) {
     super(props);
+    this.onBodyMount = this.onBodyMount.bind(this);
     this.onInfoEnter = this.onInfoEnter.bind(this);
     this.onInfoLeave = this.onInfoLeave.bind(this);
     this.onHeaderClick = this.onHeaderClick.bind(this);
     this.onTransitionEnd = this.onTransitionEnd.bind(this);
     this.enableOrDisableAnimation = this.enableOrDisableAnimation.bind(this);
     this.state = { enableAnimation: true, isAnimating: false, infoActive: false };
   }
 
   componentWillMount() {
     this.props.document.addEventListener(VISIBILITY_CHANGE_EVENT, this.enableOrDisableAnimation);
   }
+  componentWillUpdate(nextProps) {
+    // Check if we're about to go from expanded to collapsed
+    if (!getCollapsed(this.props) && getCollapsed(nextProps)) {
+      // This next line forces a layout flush of the section body, which has a
+      // max-height style set, so that the upcoming collapse animation can
+      // animate from that height to the collapsed height. Without this, the
+      // update is coalesced and there's no animation from no-max-height to 0.
+      this.sectionBody.scrollHeight; // eslint-disable-line no-unused-expressions
+    }
+  }
   componentWillUnmount() {
     this.props.document.removeEventListener(VISIBILITY_CHANGE_EVENT, this.enableOrDisableAnimation);
   }
   enableOrDisableAnimation() {
     // Only animate the collapse/expand for visible tabs.
     const visible = this.props.document.visibilityState === VISIBLE;
     if (this.state.enableAnimation !== visible) {
       this.setState({ enableAnimation: visible });
@@ -1136,43 +1150,53 @@ class CollapsibleSection extends React.P
   }
   _setInfoState(nextActive) {
     // Take a truthy value to conditionally change the infoActive state.
     const infoActive = !!nextActive;
     if (infoActive !== this.state.infoActive) {
       this.setState({ infoActive });
     }
   }
+  onBodyMount(node) {
+    this.sectionBody = node;
+  }
   onInfoEnter() {
     // We're getting focus or hover, so info state should be true if not yet.
     this._setInfoState(true);
   }
   onInfoLeave(event) {
     // We currently have an active (true) info state, so keep it true only if we
     // have a related event target that is contained "within" the current target
     // (section-info-option) as itself or a descendant. Set to false otherwise.
     this._setInfoState(event && event.relatedTarget && (event.relatedTarget === event.currentTarget || event.relatedTarget.compareDocumentPosition(event.currentTarget) & Node.DOCUMENT_POSITION_CONTAINS));
   }
   onHeaderClick() {
-    this.setState({ isAnimating: true });
-    this.props.dispatch(ac.SetPref(this.props.prefName, !this.props.Prefs.values[this.props.prefName]));
+    // Get the current height of the body so max-height transitions can work
+    this.setState({
+      isAnimating: true,
+      maxHeight: `${this.sectionBody.scrollHeight}px`
+    });
+    this.props.dispatch(ac.SetPref(this.props.prefName, !getCollapsed(this.props)));
   }
-  onTransitionEnd() {
-    this.setState({ isAnimating: false });
+  onTransitionEnd(event) {
+    // Only update the animating state for our own transition (not a child's)
+    if (event.target === event.currentTarget) {
+      this.setState({ isAnimating: false });
+    }
   }
   renderIcon() {
     const icon = this.props.icon;
     if (icon && icon.startsWith("moz-extension://")) {
       return React.createElement("span", { className: "icon icon-small-spacer", style: { "background-image": `url('${icon}')` } });
     }
     return React.createElement("span", { className: `icon icon-small-spacer icon-${icon || "webextension"}` });
   }
   render() {
-    const isCollapsed = this.props.Prefs.values[this.props.prefName];
-    const { enableAnimation, isAnimating } = this.state;
+    const isCollapsed = getCollapsed(this.props);
+    const { enableAnimation, isAnimating, maxHeight } = this.state;
     const { id, infoOption, eventSource, disclaimer } = this.props;
     const disclaimerPref = `section.${id}.showDisclaimer`;
     const needsDisclaimer = disclaimer && this.props.Prefs.values[disclaimerPref];
 
     return React.createElement(
       "section",
       { className: `collapsible-section ${this.props.className}${enableAnimation ? " animation-enabled" : ""}${isCollapsed ? " collapsed" : ""}` },
       React.createElement(
@@ -1188,17 +1212,21 @@ class CollapsibleSection extends React.P
             this.props.title,
             React.createElement("span", { className: `icon ${isCollapsed ? "icon-arrowhead-forward" : "icon-arrowhead-down"}` })
           )
         ),
         infoOption && React.createElement(InfoIntl, { infoOption: infoOption, dispatch: this.props.dispatch })
       ),
       React.createElement(
         "div",
-        { className: `section-body${isAnimating ? " animating" : ""}`, onTransitionEnd: this.onTransitionEnd },
+        {
+          className: `section-body${isAnimating ? " animating" : ""}`,
+          onTransitionEnd: this.onTransitionEnd,
+          ref: this.onBodyMount,
+          style: isAnimating && !isCollapsed ? { maxHeight } : null },
         needsDisclaimer && React.createElement(DisclaimerIntl, { disclaimerPref: disclaimerPref, disclaimer: disclaimer, eventSource: eventSource, dispatch: this.props.dispatch }),
         this.props.children
       )
     );
   }
 }
 
 CollapsibleSection.defaultProps = {
--- a/browser/extensions/activity-stream/install.rdf.in
+++ b/browser/extensions/activity-stream/install.rdf.in
@@ -3,17 +3,17 @@
 #filter substitution
 
 <RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:em="http://www.mozilla.org/2004/em-rdf#">
   <Description about="urn:mozilla:install-manifest">
     <em:id>activity-stream@mozilla.org</em:id>
     <em:type>2</em:type>
     <em:bootstrap>true</em:bootstrap>
     <em:unpack>false</em:unpack>
-    <em:version>2017.11.29.1212-78117350</em:version>
+    <em:version>2017.12.02.1393-6aa2a91e</em:version>
     <em:name>Activity Stream</em:name>
     <em:description>A rich visual history feed and a reimagined home page make it easier than ever to find exactly what you're looking for in Firefox.</em:description>
     <em:multiprocessCompatible>true</em:multiprocessCompatible>
 
     <em:targetApplication>
       <Description>
         <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
         <em:minVersion>@MOZ_APP_VERSION@</em:minVersion>