Bug 1126188: Show suggested tile explanation text under a suggested tile. r=adw
authorMarina Samuel <msamuel@mozilla.com>
Fri, 20 Mar 2015 16:39:09 -0400
changeset 263724 41d6e4de79be313d4b7e411a3a28f170e8ac0de0
parent 263723 f8455aca5a0137049749d5075c8df68609e19f4f
child 263725 648e3888d9bde4602931496d3c439965dcc7a788
push id4718
push userraliiev@mozilla.com
push dateMon, 11 May 2015 18:39:53 +0000
treeherdermozilla-beta@c20c4ef55f08 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersadw
bugs1126188
milestone39.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1126188: Show suggested tile explanation text under a suggested tile. r=adw
browser/base/content/newtab/grid.js
browser/base/content/newtab/newTab.css
browser/base/content/newtab/sites.js
browser/base/content/test/newtab/browser_newtab_drag_drop.js
browser/base/content/test/newtab/browser_newtab_drag_drop_ext.js
browser/locales/en-US/chrome/browser/newTab.properties
browser/modules/DirectoryLinksProvider.jsm
browser/themes/shared/newtab/newTab.inc.css
--- a/browser/base/content/newtab/grid.js
+++ b/browser/base/content/newtab/grid.js
@@ -163,17 +163,18 @@ let gGrid = {
       '  <span class="newtab-thumbnail"/>' +
       '  <span class="newtab-thumbnail enhanced-content"/>' +
       '  <span class="newtab-title"/>' +
       '</a>' +
       '<input type="button" title="' + newTabString("pin") + '"' +
       '       class="newtab-control newtab-control-pin"/>' +
       '<input type="button" title="' + newTabString("block") + '"' +
       '       class="newtab-control newtab-control-block"/>' +
-      '<span class="newtab-sponsored">' + newTabString("sponsored.button") + '</span>';
+      '<span class="newtab-sponsored">' + newTabString("sponsored.button") + '</span>' +
+      '<span class="newtab-suggested"/>';
 
     this._siteFragment = document.createDocumentFragment();
     this._siteFragment.appendChild(site);
   },
 
   /**
    * Make sure the correct number of rows and columns are visible
    */
@@ -184,17 +185,18 @@ let gGrid = {
     // Same goes for the grid if that's not ready yet.
     if (!this.isDocumentLoaded || !this._ready) {
       return;
     }
 
     // Save the cell's computed height/width including margin and border
     if (this._cellMargin === undefined) {
       let refCell = document.querySelector(".newtab-cell");
-      this._cellMargin = parseFloat(getComputedStyle(refCell).marginTop) * 2;
+      this._cellMargin = parseFloat(getComputedStyle(refCell).marginTop) +
+        parseFloat(getComputedStyle(refCell).marginBottom);
       this._cellHeight = refCell.offsetHeight + this._cellMargin;
       this._cellWidth = refCell.offsetWidth + this._cellMargin;
     }
 
     let availSpace = document.documentElement.clientHeight - this._cellMargin -
                      document.querySelector("#newtab-search-container").offsetHeight;
     let visibleRows = Math.floor(availSpace / this._cellHeight);
     this._node.style.height = this._computeHeight() + "px";
--- a/browser/base/content/newtab/newTab.css
+++ b/browser/base/content/newtab/newTab.css
@@ -154,17 +154,17 @@ input[type=button] {
 #newtab-grid[page-disabled] {
   pointer-events: none;
 }
 
 /* CELLS */
 .newtab-cell {
   display: -moz-box;
   height: 180px;
-  margin: 20px 10px;
+  margin: 20px 10px 85px;
   width: 290px;
 }
 
 /* SITES */
 .newtab-site {
   position: relative;
   -moz-box-flex: 1;
   transition: 100ms ease-out;
@@ -188,30 +188,55 @@ input[type=button] {
   left: 0;
   top: 0;
   right: 0;
   bottom: 0;
 }
 
 /* TITLES */
 .newtab-sponsored,
-.newtab-title {
-  bottom: -26px;
+.newtab-title,
+.newtab-suggested {
   overflow: hidden;
   position: absolute;
   right: 0;
   text-align: center;
+}
+
+.newtab-sponsored,
+.newtab-title {
+  bottom: -26px;
   white-space: nowrap;
+  text-overflow: ellipsis;
+  font-size: 13px;
+}
+
+.newtab-suggested {
+  border: 1px solid #dcdcdc;
+  border-radius: 2px;
+  cursor: pointer;
+  font-size: 12px;
+  height: 17px;
+  line-height: 17px;
+  margin-bottom: -1px;
+  padding: 2px 8px;
+  display: none;
+  margin-left: auto;
+  margin-right: auto;
+  left: 0;
+  top: 215px;
+}
+
+.newtab-suggested-bounds {
+  max-height: 51px; /* 51 / 17 = 3 lines maximum */
 }
 
 .newtab-title {
-  font-size: 13px;
   left: 0;
   padding-top: 14px;
-  text-overflow: ellipsis;
 }
 
 .newtab-sponsored {
   border: 1px solid #dcdcdc;
   border-radius: 2px;
   cursor: pointer;
   display: none;
   font-family: Arial;
@@ -232,16 +257,20 @@ input[type=button] {
   left: 0;
   right: auto;
 }
 
 .newtab-site:-moz-any([type=enhanced], [type=sponsored]) .newtab-sponsored {
   display: block;
 }
 
+.newtab-site[type=related] .newtab-suggested {
+  display: table;
+}
+
 .sponsored-explain,
 .sponsored-explain a {
   color: white;
 }
 
 .sponsored-explain {
   background-color: rgba(51, 51, 51, 0.95);
   border-bottom-left-radius: 6px;
--- a/browser/base/content/newtab/sites.js
+++ b/browser/base/content/newtab/sites.js
@@ -126,16 +126,22 @@ Site.prototype = {
     let tooltip = (title == url ? title : title + "\n" + url);
 
     let link = this._querySelector(".newtab-link");
     link.setAttribute("title", tooltip);
     link.setAttribute("href", url);
     this._querySelector(".newtab-title").textContent = title;
     this.node.setAttribute("type", this.link.type);
 
+    if (this.link.targetedSite) {
+      let targetedSite = `<strong> ${this.link.targetedSite} </strong>`;
+      this._querySelector(".newtab-suggested").innerHTML =
+        `<div class='newtab-suggested-bounds'> ${newTabString("suggested.button", [targetedSite])} </div>`;
+    }
+
     if (this.isPinned())
       this._updateAttributes(true);
     // Capture the page if the thumbnail is missing, which will cause page.js
     // to be notified and call our refreshThumbnail() method.
     this.captureIfMissing();
     // but still display whatever thumbnail might be available now.
     this.refreshThumbnail();
   },
@@ -172,33 +178,40 @@ Site.prototype = {
 
       if (this.link.type != link.type) {
         this.node.setAttribute("type", "enhanced");
         this.enhancedId = link.directoryId;
       }
     }
   },
 
+  _ignoreHoverEvents: function(element) {
+    element.addEventListener("mouseover", () => {
+      this.cell.node.setAttribute("ignorehover", "true");
+    });
+    element.addEventListener("mouseout", () => {
+      this.cell.node.removeAttribute("ignorehover");
+    });
+  },
+
   /**
    * Adds event handlers for the site and its buttons.
    */
   _addEventHandlers: function Site_addEventHandlers() {
     // Register drag-and-drop event handlers.
     this._node.addEventListener("dragstart", this, false);
     this._node.addEventListener("dragend", this, false);
     this._node.addEventListener("mouseover", this, false);
 
-    // Specially treat the sponsored icon to prevent regular hover effects
+    // Specially treat the sponsored icon & suggested explanation
+    // text to prevent regular hover effects
     let sponsored = this._querySelector(".newtab-sponsored");
-    sponsored.addEventListener("mouseover", () => {
-      this.cell.node.setAttribute("ignorehover", "true");
-    });
-    sponsored.addEventListener("mouseout", () => {
-      this.cell.node.removeAttribute("ignorehover");
-    });
+    let suggested = this._querySelector(".newtab-suggested");
+    this._ignoreHoverEvents(sponsored);
+    this._ignoreHoverEvents(suggested);
   },
 
   /**
    * Speculatively opens a connection to the current site.
    */
   _speculativeConnect: function Site_speculativeConnect() {
     let sc = Services.io.QueryInterface(Ci.nsISpeculativeConnect);
     let uri = Services.io.newURI(this.url, null, null);
@@ -263,16 +276,22 @@ Site.prototype = {
       }
     }
     // Handle sponsored explanation link click
     else if (target.parentElement.classList.contains("sponsored-explain")) {
       action = "sponsored_link";
     }
     // Only handle primary clicks for the remaining targets
     else if (button == 0) {
+      if (target.parentElement.classList.contains("newtab-suggested") ||
+          target.classList.contains("newtab-suggested")) {
+        // Suggested explanation text should do nothing when clicked and
+        // the link in the suggested explanation should act as default.
+        return;
+      }
       aEvent.preventDefault();
       if (target.classList.contains("newtab-control-block")) {
         this.block();
         action = "block";
       }
       else if (target.classList.contains("sponsored-explain") ||
                target.classList.contains("newtab-sponsored")) {
         this._toggleSponsored();
--- a/browser/base/content/test/newtab/browser_newtab_drag_drop.js
+++ b/browser/base/content/test/newtab/browser_newtab_drag_drop.js
@@ -55,18 +55,18 @@ function runTests() {
   // pinned sites should not be pushed out of the grid (unless there are only
   // pinned ones left on the grid)
   yield setLinks("0,1,2,3,4,5,6,7,8");
   setPinnedLinks(",,,,,,,7,8");
 
   yield addNewTabPageTab();
   checkGrid("0,1,2,3,4,5,6,7p,8p");
 
-  yield simulateDrop(2, 8);
-  checkGrid("0,1,3,4,5,6,7p,8p,2p");
+  yield simulateDrop(2, 5);
+  checkGrid("0,1,3,4,5,2p,6,7p,8p");
 
   // make sure that pinned sites are re-positioned correctly
   yield setLinks("0,1,2,3,4,5,6,7,8");
   setPinnedLinks("0,1,2,,,5");
 
   yield addNewTabPageTab();
   checkGrid("0p,1p,2p,3,4,5p,6,7,8");
 
--- a/browser/base/content/test/newtab/browser_newtab_drag_drop_ext.js
+++ b/browser/base/content/test/newtab/browser_newtab_drag_drop_ext.js
@@ -30,29 +30,29 @@ function runTests() {
   yield setLinks("0,1,2,3,4,5,6,7,8");
   setPinnedLinks(",,,,,,,7,8");
 
   yield addNewTabPageTab();
   checkGrid("0,1,2,3,4,5,6,7p,8p");
 
   // force the grid to be small enough that a pinned cell could be pushed out
   Services.prefs.setIntPref(PREF_NEWTAB_COLUMNS, 3);
-  yield simulateExternalDrop(7);
-  checkGrid("0,1,2,3,4,5,7p,99p,8p");
+  yield simulateExternalDrop(5);
+  checkGrid("0,1,2,3,4,99p,5,7p,8p");
 
   // drag a new site beneath a pinned cell and make sure the pinned cell is
   // not moved
   yield setLinks("0,1,2,3,4,5,6,7,8");
   setPinnedLinks(",,,,,,,,8");
 
   yield addNewTabPageTab();
   checkGrid("0,1,2,3,4,5,6,7,8p");
 
-  yield simulateExternalDrop(7);
-  checkGrid("0,1,2,3,4,5,6,99p,8p");
+  yield simulateExternalDrop(5);
+  checkGrid("0,1,2,3,4,99p,5,6,8p");
 
   // drag a new site onto a block of pinned sites and make sure they're shifted
   // around accordingly
   yield setLinks("0,1,2,3,4,5,6,7,8");
   setPinnedLinks("0,1,2,,,,,,");
 
   yield addNewTabPageTab();
   checkGrid("0p,1p,2p");
--- a/browser/locales/en-US/chrome/browser/newTab.properties
+++ b/browser/locales/en-US/chrome/browser/newTab.properties
@@ -4,16 +4,21 @@
 
 newtab.pin=Pin this site at its current position
 newtab.unpin=Unpin this site
 newtab.block=Remove this site
 # LOCALIZATION NOTE(newtab.sponsored.button): This text appears for sponsored
 # and enhanced tiles on the same line as the tile's title, so prefer short
 # strings to avoid overlap. This string should be uppercase.
 newtab.sponsored.button=SPONSORED
+# LOCALIZATION NOTE(newtab.suggested.button): %1$S will be replaced inline by
+# one of the user's top 100 sites that triggered this suggested tile.
+# This text appears for suggested tiles under the tile's title, so prefer short
+# strings to avoid truncating important text.
+newtab.suggested.button=Suggested for %1$S visitors
 # LOCALIZATION NOTE(newtab.sponsored.explain): %1$S will be replaced inline by
 # the (X) block icon. %2$S will be replaced by an active link using string
 # newtab.learn.link as text.
 newtab.sponsored.explain=This tile is being shown to you on behalf of a Mozilla partner. You can remove it at any time by clicking the %1$S button. %2$S
 # LOCALIZATION NOTE(newtab.enhanced.explain): %1$S will be replaced inline by
 # the gear icon used to customize the new tab window. %2$S will be replaced by
 # an active link using string newtab.learn.link as text.
 newtab.enhanced.explain=A Mozilla partner has visually enhanced this tile, replacing the screenshot. You can turn off enhanced tiles by clicking the %1$S button for your preferences. %2$S
--- a/browser/modules/DirectoryLinksProvider.jsm
+++ b/browser/modules/DirectoryLinksProvider.jsm
@@ -558,34 +558,49 @@ let DirectoryLinksProvider = {
     }
 
     // Create a flat list of all possible links we can show as related.
     // Note that many top sites may map to the same related links, but we only
     // want to count each related link once (based on url), thus possibleLinks is a map
     // from url to relatedLink. Thus, each link has an equal chance of being chosen at
     // random from flattenedLinks if it appears only once.
     let possibleLinks = new Map();
+    let targetedSites = new Map();
     this._topSitesWithRelatedLinks.forEach(topSiteWithRelatedLink => {
       let relatedLinksMap = this._relatedLinks.get(topSiteWithRelatedLink);
       relatedLinksMap.forEach((relatedLink, url) => {
         possibleLinks.set(url, relatedLink);
+
+        // Keep a map of URL to targeted sites. We later use this to show the user
+        // what site they visited to trigger this suggestion.
+        if (!targetedSites.get(url)) {
+          targetedSites.set(url, []);
+        }
+        targetedSites.get(url).push(topSiteWithRelatedLink);
       })
     });
     let flattenedLinks = [...possibleLinks.values()];
 
     // Choose our related link at random
     let relatedIndex = Math.floor(Math.random() * flattenedLinks.length);
     let chosenRelatedLink = flattenedLinks[relatedIndex];
 
     // Show the new directory tile.
     this._callObservers("onLinkChanged", {
       url: chosenRelatedLink.url,
+      title: chosenRelatedLink.title,
       frecency: RELATED_FRECENCY,
       lastVisitDate: chosenRelatedLink.lastVisitDate,
       type: "related",
+
+      // Choose the first site a user has visited as the target. In the future,
+      // this should be the site with the highest frecency. However, we currently
+      // store frecency by URL not by site.
+      targetedSite: targetedSites.get(chosenRelatedLink.url).length ?
+        targetedSites.get(chosenRelatedLink.url)[0] : null
     });
     return chosenRelatedLink;
    },
 
   /**
    * Return the object to its pre-init state
    */
   reset: function DirectoryLinksProvider_reset() {
--- a/browser/themes/shared/newtab/newTab.inc.css
+++ b/browser/themes/shared/newtab/newTab.inc.css
@@ -117,35 +117,40 @@
   background-origin: padding-box;
   background-clip: padding-box;
   background-repeat: no-repeat;
   background-size: cover;
   border-radius: inherit;
   transition: opacity 100ms ease-out;
 }
 
-.newtab-site:hover .newtab-thumbnail.enhanced-content {
+.newtab-cell:not([ignorehover]) .newtab-site:hover .newtab-thumbnail.enhanced-content {
   opacity: 0;
 }
 
 .newtab-site[type=affiliate] .newtab-thumbnail,
 .newtab-site[type=enhanced] .newtab-thumbnail,
 .newtab-site[type=organic] .newtab-thumbnail,
 .newtab-site[type=sponsored] .newtab-thumbnail {
   background-position: center center;
   background-size: auto;
 }
 
 /* TITLES */
 #newtab-intro-what,
 .newtab-sponsored,
-.newtab-title {
+.newtab-title,
+.newtab-suggested  {
   color: #5c5c5c;
 }
 
+.newtab-suggested {
+  background-color: white;
+}
+
 .newtab-site:hover .newtab-title {
   color: #222;
 }
 
 .newtab-site[pinned] .newtab-title {
   padding: 0 15px;
 }