Bug 1407228 - Collapsed sections should be animated when they are reopened. r=r1cky draft
authorAndrei Oprea <andrei.br92@gmail.com>
Thu, 30 Nov 2017 18:01:49 +0100
changeset 705785 6e14e6462e27ac1b1325eec9dfb1362869c3452b
parent 704525 bff979e2ef09d34ff2dd042dca9d2fca559eb4fd
child 742454 416d4934092c36b870fb98fc1293ae8470314718
push id91578
push userbmo:andrei.br92@gmail.com
push dateThu, 30 Nov 2017 17:03:03 +0000
reviewersr1cky
bugs1407228
milestone58.0
Bug 1407228 - Collapsed sections should be animated when they are reopened. r=r1cky MozReview-Commit-ID: KNCWY7Dxc7S
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
--- 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);
@@ -1246,21 +1245,21 @@ section.top-sites:not(.collapsed):hover 
     @media (min-width: 224px) {
       .collapsible-section .section-disclaimer button {
         position: relative; } }
     @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);
@@ -1246,21 +1245,21 @@ section.top-sites:not(.collapsed):hover 
     @media (min-width: 224px) {
       .collapsible-section .section-disclaimer button {
         position: relative; } }
     @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);
@@ -1246,21 +1245,21 @@ section.top-sites:not(.collapsed):hover 
     @media (min-width: 224px) {
       .collapsible-section .section-disclaimer button {
         position: relative; } }
     @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 = {