Bug 1137481 - Adjust the Heartbeat UI and add a Learn More link. r=MattN, a=sledru
authorAlessio Placitelli <alessio.placitelli@gmail.com>
Fri, 27 Feb 2015 08:37:00 -0500
changeset 258244 94de32e773b8
parent 258243 bfe014dd05ef
child 258245 a9d533ac9ff4
push id4627
push userryanvm@gmail.com
push date2015-04-03 19:22 +0000
treeherdermozilla-beta@5f5a4c5a7e02 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMattN, sledru
bugs1137481
milestone38.0
Bug 1137481 - Adjust the Heartbeat UI and add a Learn More link. r=MattN, a=sledru
browser/components/uitour/UITour-lib.js
browser/components/uitour/UITour.jsm
browser/components/uitour/test/browser_UITour_heartbeat.js
browser/themes/shared/UITour.inc.css
--- a/browser/components/uitour/UITour-lib.js
+++ b/browser/components/uitour/UITour-lib.js
@@ -93,22 +93,25 @@ if (typeof Mozilla == 'undefined') {
   };
 
 	Mozilla.UITour.registerPageID = function(pageID) {
 		_sendEvent('registerPageID', {
 			pageID: pageID
 		});
 	};
 
-	Mozilla.UITour.showHeartbeat = function(message, thankyouMessage, flowId, engagementURL) {
+	Mozilla.UITour.showHeartbeat = function(message, thankyouMessage, flowId, engagementURL,
+																					learnMoreLabel, learnMoreURL) {
 		_sendEvent('showHeartbeat', {
 			message: message,
 			thankyouMessage: thankyouMessage,
 			flowId: flowId,
-			engagementURL: engagementURL
+			engagementURL: engagementURL,
+			learnMoreLabel: learnMoreLabel,
+			learnMoreURL: learnMoreURL,
 		});
 	};
 
 	Mozilla.UITour.showHighlight = function(target, effect) {
 		_sendEvent('showHighlight', {
 			target: target,
 			effect: effect
 		});
--- a/browser/components/uitour/UITour.jsm
+++ b/browser/components/uitour/UITour.jsm
@@ -427,18 +427,17 @@ this.UITour = {
         }
 
         if (typeof data.flowId !== "string" || data.flowId === "") {
           log.error("showHeartbeat: Invalid flowId specified.");
           break;
         }
 
         // Finally show the Heartbeat UI.
-        this.showHeartbeat(window, messageManager, data.message, data.thankyouMessage, data.flowId,
-                           data.engagementURL);
+        this.showHeartbeat(window, messageManager, data);
         break;
       }
 
       case "showHighlight": {
         let targetPromise = this.getTarget(window, data.target);
         targetPromise.then(target => {
           if (!target.node) {
             log.error("UITour: Target could not be resolved: " + data.target);
@@ -1016,46 +1015,51 @@ this.UITour = {
 
   resetTheme: function() {
     LightweightThemeManager.resetPreview();
   },
 
   /**
    * Show the Heartbeat UI to request user feedback. This function reports back to the
    * caller using |notify|. The notification event name reflects the current status the UI
-   * is in (either "Heartbeat:NotificationOffered", "Heartbeat:NotificationClosed" or
-   * "Heartbeat:Voted"). When a "Heartbeat:Voted" event is notified the data payload contains
-   * a |score| field which holds the rating picked by the user.
+   * is in (either "Heartbeat:NotificationOffered", "Heartbeat:NotificationClosed",
+   * "Heartbeat:LearnMore" or "Heartbeat:Voted"). When a "Heartbeat:Voted" event is notified
+   * the data payload contains a |score| field which holds the rating picked by the user.
    * Please note that input parameters are already validated by the caller.
    *
    * @param aChromeWindow
    *        The chrome window that the heartbeat notification is displayed in.
    * @param aMessageManager
    *        The message manager to communicate with the API caller.
-   * @param aMessage
+   * @param {Object} aOptions Options object.
+   * @param {String} aOptions.message
    *        The message, or question, to display on the notification.
-   * @param aThankyouMessage
+   * @param {String} aOptions.thankyouMessage
    *        The thank you message to display after user votes.
-   * @param aFlowId
+   * @param {String} aOptions.flowId
    *        An identifier for this rating flow. Please note that this is only used to
    *        identify the notification box.
-   * @param [aEngagementURL]
+   * @param {String} [aOptions.engagementURL=null]
    *        The engagement URL to open in a new tab once user has voted. If this is null
    *        or invalid, no new tab is opened.
+   * @param {String} [aOptions.learnMoreLabel=null]
+   *        The label of the learn more link. No link will be shown if this is null.
+   * @param {String} [aOptions.learnMoreURL=null]
+   *        The learn more URL to open when clicking on the learn more link. No learn more
+   *        will be shown if this is an invalid URL.
    */
-  showHeartbeat: function(aChromeWindow, aMessageManager, aMessage, aThankyouMessage, aFlowId,
-                          aEngagementURL = null) {
+  showHeartbeat: function(aChromeWindow, aMessageManager, aOptions) {
     let nb = aChromeWindow.document.getElementById("high-priority-global-notificationbox");
 
     // Create the notification. Prefix its ID to decrease the chances of collisions.
-    let notice = nb.appendNotification(aMessage, "heartbeat-" + aFlowId,
-      "chrome://branding/content/icon64.png", nb.PRIORITY_INFO_HIGH, null, function() {
+    let notice = nb.appendNotification(aOptions.message, "heartbeat-" + aOptions.flowId,
+      "chrome://browser/skin/heartbeat-icon.svg", nb.PRIORITY_INFO_HIGH, null, function() {
         // Let the consumer know the notification bar was closed. This also happens
         // after voting.
-        this.notify("Heartbeat:NotificationClosed", { flowId: aFlowId, timestamp: Date.now() });
+        this.notify("Heartbeat:NotificationClosed", { flowId: aOptions.flowId, timestamp: Date.now() });
     }.bind(this));
 
     // Get the elements we need to style.
     let messageImage =
       aChromeWindow.document.getAnonymousElementByAttribute(notice, "anonid", "messageImage");
     let messageText =
       aChromeWindow.document.getAnonymousElementByAttribute(notice, "anonid", "messageText");
 
@@ -1077,44 +1081,43 @@ this.UITour = {
       ratingElement.id = "star" + starIndex;
       ratingElement.setAttribute("data-score", starIndex);
 
       // Add the click handler.
       ratingElement.addEventListener("click", function (evt) {
         let rating = Number(evt.target.getAttribute("data-score"), 10);
 
         // Let the consumer know user voted.
-        this.notify("Heartbeat:Voted", { flowId: aFlowId, score: rating, timestamp: Date.now() });
+        this.notify("Heartbeat:Voted", { flowId: aOptions.flowId, score: rating, timestamp: Date.now() });
 
-        // Display the Heart and make it pulse twice.
-        notice.image = "chrome://browser/skin/heartbeat-icon.svg";
-        notice.label = aThankyouMessage;
+        // Make the heartbeat icon pulse twice.
+        notice.label = aOptions.thankyouMessage;
         messageImage.classList.remove("pulse-onshow");
         messageImage.classList.add("pulse-twice");
 
         // Remove all the children of the notice (rating container
         // and the flex).
         while (notice.firstChild) {
           notice.removeChild(notice.firstChild);
         }
 
         // Make sure that we have a valid URL. If we haven't, do not open the engagement page.
         let engagementURL = null;
         try {
-          engagementURL = new URL(aEngagementURL);
+          engagementURL = new URL(aOptions.engagementURL);
         } catch (error) {
           log.error("showHeartbeat: Invalid URL specified.");
         }
 
         // Just open the engagement tab if we have a valid engagement URL.
         if (engagementURL) {
           // Append the score data to the engagement URL.
           engagementURL.searchParams.append("type", "stars");
           engagementURL.searchParams.append("score", rating);
-          engagementURL.searchParams.append("flowid", aFlowId);
+          engagementURL.searchParams.append("flowid", aOptions.flowId);
 
           // Open the engagement URL in a new tab.
           aChromeWindow.gBrowser.selectedTab =
             aChromeWindow.gBrowser.addTab(engagementURL.toString(), {
               owner: aChromeWindow.gBrowser.selectedTab,
               relatedToCurrent: true
             });
         }
@@ -1131,27 +1134,47 @@ this.UITour = {
 
     frag.appendChild(ratingContainer);
 
     // Make sure the stars are not pushed to the right by the spacer.
     let rightSpacer = aChromeWindow.document.createElement("spacer");
     rightSpacer.flex = 20;
     frag.appendChild(rightSpacer);
 
+    messageText.flex = 0; // Collapse the space before the stars.
     let leftSpacer = messageText.nextSibling;
     leftSpacer.flex = 0;
 
+    // Make sure that we have a valid learn more URL.
+    let learnMoreURL = null;
+    try {
+      learnMoreURL = new URL(aOptions.learnMoreURL);
+    } catch (error) {
+      log.error("showHeartbeat: Invalid learnMore URL specified.");
+    }
+
+    // Add the learn more link.
+    if (aOptions.learnMoreLabel && learnMoreURL) {
+      let learnMore = aChromeWindow.document.createElement("label");
+      learnMore.className = "text-link";
+      learnMore.href = learnMoreURL.toString();
+      learnMore.setAttribute("value", aOptions.learnMoreLabel);
+      learnMore.addEventListener("click", () => this.notify("Heartbeat:LearnMore",
+        { flowId: aOptions.flowId, timestamp: Date.now() }));
+      frag.appendChild(learnMore);
+    }
+
     // Append the fragment and apply the styling.
     notice.appendChild(frag);
     notice.classList.add("heartbeat");
     messageImage.classList.add("heartbeat", "pulse-onshow");
     messageText.classList.add("heartbeat");
 
     // Let the consumer know the notification was shown.
-    this.notify("Heartbeat:NotificationOffered", { flowId: aFlowId, timestamp: Date.now() });
+    this.notify("Heartbeat:NotificationOffered", { flowId: aOptions.flowId, timestamp: Date.now() });
   },
 
   /**
    * The node to which a highlight or notification(-popup) is anchored is sometimes
    * obscured because it may be inside an overflow menu. This function should figure
    * that out and offer the overflow chevron as an alternative.
    *
    * @param {Node} aAnchor The element that's supposed to be the anchor
--- a/browser/components/uitour/test/browser_UITour_heartbeat.js
+++ b/browser/components/uitour/test/browser_UITour_heartbeat.js
@@ -9,38 +9,57 @@ let gContentWindow;
 let notificationBox = document.getElementById("high-priority-global-notificationbox");
 
 Components.utils.import("resource:///modules/UITour.jsm");
 
 function test() {
   UITourTest();
 }
 
+function getHeartbeatNotification(aId) {
+  // UITour.jsm prefixes the notification box ID with "heartbeat-" to prevent collisions.
+  return notificationBox.getNotificationWithValue("heartbeat-" + aId);
+}
+
 /**
  * Simulate a click on a rating element in the Heartbeat notification.
  *
  * @param aId
  *        The id of the notification box.
  * @param aScore
  *        The score related to the rating element we want to click on.
  */
 function simulateVote(aId, aScore) {
-  // UITour.jsm prefixes the notification box ID with "heartbeat-" to prevent collisions.
-  let notification = notificationBox.getNotificationWithValue("heartbeat-" + aId);
+  let notification = getHeartbeatNotification(aId);
 
   let ratingContainer = notification.childNodes[0];
   ok(ratingContainer, "The notification has a valid rating container.");
 
   let ratingElement = ratingContainer.getElementsByAttribute("data-score", aScore);
   ok(ratingElement[0], "The rating container contains the requested rating element.");
 
   ratingElement[0].click();
 }
 
 /**
+ * Simulate a click on the learn-more link.
+ *
+ * @param aId
+ *        The id of the notification box.
+ */
+function clickLearnMore(aId) {
+  let notification = getHeartbeatNotification(aId);
+
+  let learnMoreLabel = notification.childNodes[2];
+  ok(learnMoreLabel, "The notification has a valid learn more label.");
+
+  learnMoreLabel.click();
+}
+
+/**
  * Remove the notification box.
  *
  * @param aId
  *        The id of the notification box to remove.
  */
 function cleanUpNotification(aId) {
   let notification = notificationBox.getNotificationWithValue("heartbeat-" + aId);
   notificationBox.removeNotification(notification);
@@ -225,10 +244,53 @@ let tests = [
         }
         default:
           // We are not expecting other states for this test.
           ok(false, "Unexpected notification received: " + aEventName);
       }
     });
 
     gContentAPI.showHeartbeat("How would you rate Firefox?", "Thank you!", flowId, engagementURL);
+  },
+
+  /**
+   * Test that the learn more link is displayed and that the page is correctly opened when
+   * clicking on it.
+   */
+  function test_heartbeat_learnmore(done) {
+    let dummyURL = "http://example.com";
+    let flowId = "ui-ratefirefox-" + Math.random();
+    let originalTabCount = gBrowser.tabs.length;
+    const expectedTabCount = originalTabCount + 1;
+
+    gContentAPI.observe(function (aEventName, aData) {
+      switch (aEventName) {
+        case "Heartbeat:NotificationOffered": {
+          info("'Heartbeat:Offered' notification received (timestamp " + aData.timestamp.toString() + ").");
+          ok(Number.isFinite(aData.timestamp), "Timestamp must be a number.");
+          // The UI was just shown. Simulate a click on the learn more link.
+          clickLearnMore(flowId);
+          break;
+        }
+        case "Heartbeat:LearnMore": {
+          info("'Heartbeat:LearnMore' notification received (timestamp " + aData.timestamp.toString() + ").");
+          ok(Number.isFinite(aData.timestamp), "Timestamp must be a number.");
+          cleanUpNotification(flowId);
+          break;
+        }
+        case "Heartbeat:NotificationClosed": {
+          info("'Heartbeat:NotificationClosed' notification received (timestamp " + aData.timestamp.toString() + ").");
+          ok(Number.isFinite(aData.timestamp), "Timestamp must be a number.");
+          is(gBrowser.tabs.length, expectedTabCount, "Learn more URL should open in a new tab.");
+          gBrowser.removeCurrentTab();
+          done();
+          break;
+        }
+        default:
+          // We are not expecting other states for this test.
+          ok(false, "Unexpected notification received: " + aEventName);
+      }
+    });
+
+    gContentAPI.showHeartbeat("How would you rate Firefox?", "Thank you!", flowId, dummyURL,
+                              "What is this?", dummyURL);
   }
 ];
--- a/browser/themes/shared/UITour.inc.css
+++ b/browser/themes/shared/UITour.inc.css
@@ -146,23 +146,39 @@
 
 #UITourTooltipButtons > button.button-primary:not(:active):hover {
   background-color: rgb(105,173,61);
 }
 
 /* Notification overrides for Heartbeat UI */
 
 notification.heartbeat {
-  background-color: #F1F1F1;
 %ifdef XP_MACOSX
   background-image: linear-gradient(-179deg, #FBFBFB 0%, #EBEBEB 100%);
+%else
+  background-color: #F1F1F1;
 %endif
-  box-shadow: 0px 1px 0px 0px rgba(0,0,0,0.35);
+  border-bottom: 1px solid #C1C1C1;
+  height: 40px;
 }
 
+/* In themes/osx/global/notification.css the close icon is inverted because notifications
+   on OSX are usually dark. Heartbeat is light, so override that behaviour. */
+
+%ifdef XP_MACOSX
+notification.heartbeat[type="info"] .close-icon:not(:hover) {
+  -moz-image-region: rect(0, 16px, 16px, 0px) !important;
+}
+@media (min-resolution: 2dppx) {
+  notification.heartbeat[type="info"] .close-icon:not(:hover) {
+    -moz-image-region: rect(0, 32px, 32px, 0px) !important;
+  }
+}
+%endif
+
 @keyframes pulse-onshow {
  0% {
    opacity: 0;
    transform: scale(1.0);
  }
  25% {
    opacity: 1;
    transform: scale(1.1);
@@ -187,46 +203,62 @@ notification.heartbeat {
  }
  100% {
    transform: scale(1);
  }
 }
 
 .messageText.heartbeat {
   color: #333333;
-  font-weight: normal;
-  font-family: "Lucida Grande", Segoe, Ubuntu;
-  font-size: 14px;
-  line-height: 16px;
   text-shadow: none;
+  -moz-margin-start: 0px;
+  /* The !important is required to override OSX default style. */
+  -moz-margin-end: 12px !important;
 }
 
 .messageImage.heartbeat {
-  width: 36px;
-  height: 36px;
-  -moz-margin-end: 10px;
+  width: 24px;
+  height: 24px;
+  -moz-margin-start: 8px;
+  -moz-margin-end: 8px;
 }
 
 .messageImage.heartbeat.pulse-onshow {
   animation-name: pulse-onshow;
   animation-duration: 1.5s;
   animation-iteration-count: 1;
   animation-timing-function: cubic-bezier(.7,1.8,.9,1.1);
 }
 
 .messageImage.heartbeat.pulse-twice {
   animation-name: pulse-twice;
   animation-duration: 1s;
   animation-iteration-count: 2;
   animation-timing-function: linear;
 }
 
+/* Learn More link styles */
+.heartbeat > .text-link {
+  color: #0095DD;
+  -moz-margin-start: 0px;
+}
+
+.heartbeat > .text-link:hover {
+  color: #008ACB;
+  text-decoration: none;
+}
+
+.heartbeat > .text-link:hover:active {
+  color: #006B9D;
+}
+
 /* Heartbeat UI Rating Star Classes */
 .heartbeat > #star-rating-container {
   display: -moz-box;
+  margin-bottom: 4px;
 }
 
 .heartbeat > #star-rating-container > #star5 {
   -moz-box-ordinal-group: 5;
 }
 
 .heartbeat > #star-rating-container > #star4 {
   -moz-box-ordinal-group: 4;
@@ -238,19 +270,18 @@ notification.heartbeat {
 
 .heartbeat > #star-rating-container > #star2 {
   -moz-box-ordinal-group: 2;
 }
 
 .heartbeat > #star-rating-container > .star-x  {
   background: url("chrome://browser/skin/heartbeat-star-off.svg");
   cursor: pointer;
-  width: 24px;
-  height: 24px;
+  /* Overrides the -moz-margin-end for all platforms defined in the .plain class */
+  -moz-margin-end: 4px !important;
+  width: 16px;
+  height: 16px;
 }
 
 .heartbeat > #star-rating-container > .star-x:hover,
 .heartbeat > #star-rating-container > .star-x:hover ~ .star-x {
   background: url("chrome://browser/skin/heartbeat-star-lit.svg");
-  cursor: pointer;
-  width: 24px;
-  height: 24px;
 }