Bug 1038997 - Backout newtab changes including layout and styling changes for Fx32. r=adw, a=sylvestre
☠☠ backed out by 5bb486b0f89a ☠ ☠
authorEd Lee <edilee>
Thu, 17 Jul 2014 08:52:00 -0400
changeset 209167 238174823b7c77b998984e3b73c536c5c39488ad
parent 209166 951b15af43a7eb059578e0a2447336d2de5b23e7
child 209168 7da62c51fa9742e1f9590b5a0363199c04271d48
push id494
push userraliiev@mozilla.com
push dateMon, 25 Aug 2014 18:42:16 +0000
treeherdermozilla-release@a3cc3e46b571 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersadw, sylvestre
bugs1038997, 980014, 978338, 991210
milestone32.0a2
Bug 1038997 - Backout newtab changes including layout and styling changes for Fx32. r=adw, a=sylvestre Revert bug 980014, bug 978338, bug 991210; fix up search form
browser/base/content/newtab/grid.js
browser/base/content/newtab/newTab.css
browser/base/content/newtab/newTab.xul
browser/base/content/newtab/page.js
browser/base/content/test/newtab/browser.ini
browser/base/content/test/newtab/browser_newtab_bug1001854.js
browser/base/content/test/newtab/browser_newtab_bug991210.js
browser/base/content/test/newtab/browser_newtab_update.js
browser/base/content/test/newtab/head.js
browser/themes/linux/newtab/newTab.css
browser/themes/osx/newtab/newTab.css
browser/themes/windows/newtab/newTab.css
toolkit/modules/NewTabUtils.jsm
--- a/browser/base/content/newtab/grid.js
+++ b/browser/base/content/newtab/grid.js
@@ -1,21 +1,15 @@
 #ifdef 0
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 #endif
 
 /**
- * Define various fixed dimensions
- */
-const GRID_BOTTOM_EXTRA = 4; // title's line-height extends 4px past the margin
-const GRID_WIDTH_EXTRA = 1; // provide 1px buffer to allow for rounding error
-
-/**
  * This singleton represents the grid that contains all sites.
  */
 let gGrid = {
   /**
    * The DOM node of the grid.
    */
   _node: null,
   get node() this._node,
@@ -32,82 +26,55 @@ let gGrid = {
   get cells() this._cells,
 
   /**
    * All sites contained in the grid's cells. Sites may be empty.
    */
   get sites() [cell.site for each (cell in this.cells)],
 
   // Tells whether the grid has already been initialized.
-  get ready() !!this._ready,
+  get ready() !!this._node,
 
   /**
    * Initializes the grid.
    * @param aSelector The query selector of the grid.
    */
   init: function Grid_init() {
     this._node = document.getElementById("newtab-grid");
     this._createSiteFragment();
-    this._renderGrid();
-    gLinks.populateCache(() => {
-      this._renderSites();
-      this._ready = true;
-    });
-    addEventListener("load", this);
-    addEventListener("resize", this);
-
-    // The document may already be loaded if the user is toggling the page
-    if (document.readyState == "complete") {
-      this.handleEvent({type: "load"});
-    }
+    this._render();
   },
 
   /**
    * Creates a new site in the grid.
    * @param aLink The new site's link.
    * @param aCell The cell that will contain the new site.
    * @return The newly created site.
    */
   createSite: function Grid_createSite(aLink, aCell) {
     let node = aCell.node;
     node.appendChild(this._siteFragment.cloneNode(true));
     return new Site(node.firstElementChild, aLink);
   },
 
   /**
-   * Handles all grid events.
-   */
-  handleEvent: function Grid_handleEvent(aEvent) {
-    switch (aEvent.type) {
-      case "load":
-      case "resize":
-        this._resizeGrid();
-        break;
-    }
-  },
-
-  /**
    * Refreshes the grid and re-creates all sites.
    */
   refresh: function Grid_refresh() {
     // Remove all sites.
     this.cells.forEach(function (cell) {
       let node = cell.node;
       let child = node.firstElementChild;
 
       if (child)
         node.removeChild(child);
     }, this);
 
     // Render the grid again.
-    if (this._shouldRenderGrid()) {
-      this._renderGrid();
-      this._resizeGrid();
-    }
-    this._renderSites();
+    this._render();
   },
 
   /**
    * Locks the grid to block all pointer events.
    */
   lock: function Grid_lock() {
     this.node.setAttribute("locked", "true");
   },
@@ -118,43 +85,39 @@ let gGrid = {
   unlock: function Grid_unlock() {
     this.node.removeAttribute("locked");
   },
 
   /**
    * Creates the newtab grid.
    */
   _renderGrid: function Grid_renderGrid() {
+    let row = document.createElementNS(HTML_NAMESPACE, "div");
     let cell = document.createElementNS(HTML_NAMESPACE, "div");
+    row.classList.add("newtab-row");
     cell.classList.add("newtab-cell");
 
     // Clear the grid
     this._node.innerHTML = "";
 
-    // Creates all the cells up to the maximum
-    for (let i = 0; i < gGridPrefs.gridColumns * gGridPrefs.gridRows; i++) {
-      this._node.appendChild(cell.cloneNode(true));
+    // Creates the structure of one row
+    for (let i = 0; i < gGridPrefs.gridColumns; i++) {
+      row.appendChild(cell.cloneNode(true));
+    }
+    // Creates the grid
+    for (let j = 0; j < gGridPrefs.gridRows; j++) {
+      this._node.appendChild(row.cloneNode(true));
     }
 
     // (Re-)initialize all cells.
     let cellElements = this.node.querySelectorAll(".newtab-cell");
     this._cells = [new Cell(this, cell) for (cell of cellElements)];
   },
 
   /**
-   * Calculate the height for a number of rows up to the maximum rows
-   * @param rows Number of rows defaulting to the max
-   */
-  _computeHeight: function Grid_computeHeight(aRows) {
-    let {gridRows} = gGridPrefs;
-    aRows = aRows === undefined ? gridRows : Math.min(gridRows, aRows);
-    return aRows * this._cellHeight + GRID_BOTTOM_EXTRA;
-  },
-
-  /**
    * Creates the DOM fragment that is re-used when creating sites.
    */
   _createSiteFragment: function Grid_createSiteFragment() {
     let site = document.createElementNS(HTML_NAMESPACE, "div");
     site.classList.add("newtab-site");
     site.setAttribute("draggable", "true");
 
     // Create the site's inner HTML code.
@@ -185,39 +148,26 @@ let gGrid = {
 
     for (let i = 0; i < length; i++) {
       if (links[i])
         this.createSite(links[i], cells[i]);
     }
   },
 
   /**
-   * Make sure the correct number of rows and columns are visible
+   * Renders the grid.
    */
-  _resizeGrid: function Grid_resizeGrid() {
-    // 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._cellHeight = refCell.offsetHeight + this._cellMargin;
-      this._cellWidth = refCell.offsetWidth + this._cellMargin;
+  _render: function Grid_render() {
+    if (this._shouldRenderGrid()) {
+      this._renderGrid();
     }
 
-    let availSpace = document.documentElement.clientHeight - this._cellMargin -
-                     document.querySelector("#newtab-margin-undo-container").offsetHeight -
-                     document.querySelector("#newtab-search-container").offsetHeight;
-    let visibleRows = Math.floor(availSpace / this._cellHeight);
-    this._node.style.height = this._computeHeight() + "px";
-    this._node.style.maxHeight = this._computeHeight(visibleRows) + "px";
-    this._node.style.maxWidth = gGridPrefs.gridColumns * this._cellWidth +
-                                GRID_WIDTH_EXTRA + "px";
-
-    // Resize the search bar.
-    let width = parseFloat(window.getComputedStyle(this._node).width);
-    let visibleCols = Math.floor(width / this._cellWidth);
-    gSearch.setWidth(visibleCols * this._cellWidth - this._cellMargin);
+    this._renderSites();
   },
 
   _shouldRenderGrid : function Grid_shouldRenderGrid() {
+    let rowsLength = this._node.querySelectorAll(".newtab-row").length;
     let cellsLength = this._node.querySelectorAll(".newtab-cell").length;
-    return cellsLength != (gGridPrefs.gridRows * gGridPrefs.gridColumns);
+
+    return (rowsLength != gGridPrefs.gridRows ||
+            cellsLength != (gGridPrefs.gridRows * gGridPrefs.gridColumns));
   }
 };
--- a/browser/base/content/newtab/newTab.css
+++ b/browser/base/content/newtab/newTab.css
@@ -53,21 +53,35 @@ input[type=button] {
 /* MARGINS */
 #newtab-vertical-margin {
   display: -moz-box;
   position: relative;
   -moz-box-flex: 1;
   -moz-box-orient: vertical;
 }
 
+#newtab-margin-top {
+  min-height: 50px;
+  max-height: 80px;
+  -moz-box-flex: 1;
+  -moz-box-align: center;
+  -moz-box-pack: center;
+}
+
 #newtab-margin-undo-container {
   display: -moz-box;
   -moz-box-pack: center;
 }
 
+#newtab-margin-bottom {
+  min-height: 40px;
+  max-height: 100px;
+  -moz-box-flex: 1;
+}
+
 #newtab-horizontal-margin {
   display: -moz-box;
   -moz-box-flex: 1;
 }
 
 #newtab-margin-top,
 #newtab-margin-bottom {
   display: -moz-box;
@@ -78,44 +92,53 @@ input[type=button] {
   -moz-box-flex: 1;
 }
 
 #newtab-margin-bottom {
   -moz-box-flex: 2;
 }
 
 .newtab-side-margin {
-  min-width: 16px;
+  min-width: 40px;
+  max-width: 300px;
   -moz-box-flex: 1;
 }
 
 /* GRID */
 #newtab-grid {
+  display: -moz-box;
   -moz-box-flex: 5;
-  overflow: hidden;
-  text-align: center;
+  -moz-box-orient: vertical;
+  min-width: 600px;
+  min-height: 400px;
   transition: 100ms ease-out;
   transition-property: opacity;
 }
 
 #newtab-grid[page-disabled] {
   opacity: 0;
 }
 
 #newtab-grid[locked],
 #newtab-grid[page-disabled] {
   pointer-events: none;
 }
 
+/* ROWS */
+.newtab-row {
+  display: -moz-box;
+  -moz-box-orient: horizontal;
+  -moz-box-direction: normal;
+  -moz-box-flex: 1;
+}
+
 /* CELLS */
 .newtab-cell {
   display: -moz-box;
-  height: 150px;
-  margin: 16px;
-  width: 243px;
+  -moz-box-flex: 1;
 }
 
 /* SITES */
 .newtab-site {
   position: relative;
   -moz-box-flex: 1;
   transition: 100ms ease-out;
   transition-property: top, left, opacity;
@@ -149,22 +172,20 @@ input[type=button] {
 .newtab-thumbnail[dragged],
 .newtab-link:-moz-focusring > .newtab-thumbnail,
 .newtab-cell:not([ignorehover]) > .newtab-site:hover > .newtab-link > .newtab-thumbnail {
   opacity: 1;
 }
 
 /* TITLES */
 .newtab-title {
-  bottom: -21px;
   position: absolute;
   left: 0;
-  line-height: 21px;
   right: 0;
-  text-align: start;
+  bottom: 0;
   white-space: nowrap;
   overflow: hidden;
   text-overflow: ellipsis;
 }
 
 /* CONTROLS */
 .newtab-control {
   position: absolute;
@@ -250,16 +271,17 @@ input[type=button] {
 
 #newtab-search-container[page-disabled] {
   opacity: 0;
   pointer-events: none;
 }
 
 #newtab-search-form {
   display: -moz-box;
+  -moz-box-flex: 5;
   -moz-box-orient: horizontal;
   -moz-box-align: center;
   height: 44px; /* 32 + 6 logo top "padding" + 6 logo bottom "padding" */
   margin-bottom: 10px; /* 32 - 16 tiles top margin - 6 logo bottom "padding" */
 }
 
 #newtab-search-logo {
   display: -moz-box;
--- a/browser/base/content/newtab/newTab.xul
+++ b/browser/base/content/newtab/newTab.xul
@@ -52,23 +52,27 @@
                       class="newtab-undo-button" />
           <xul:toolbarbutton id="newtab-undo-close-button" tabindex="-1"
                              class="close-icon tabbable"
                              tooltiptext="&newtab.undo.closeTooltip;" />
         </div>
       </div>
 
       <div id="newtab-search-container">
+        <div class="newtab-side-margin"/>
+
         <form id="newtab-search-form" name="searchForm">
           <div id="newtab-search-logo"/>
           <input type="text" name="q" value="" id="newtab-search-text"
                  maxlength="256" dir="auto"/>
           <input id="newtab-search-submit" type="submit"
                  value="&searchEndCap.label;"/>
         </form>
+
+        <div class="newtab-side-margin"/>
       </div>
 
       <div id="newtab-horizontal-margin">
         <div class="newtab-side-margin"/>
 
         <div id="newtab-grid">
         </div>
 
--- a/browser/base/content/newtab/page.js
+++ b/browser/base/content/newtab/page.js
@@ -108,27 +108,29 @@ let gPage = {
     gSearch.init();
 
     if (document.hidden) {
       addEventListener("visibilitychange", this);
     } else {
       this.onPageFirstVisible();
     }
 
-    // Initialize and render the grid.
-    gGrid.init();
+    gLinks.populateCache(function () {
+      // Initialize and render the grid.
+      gGrid.init();
 
-    // Initialize the drop target shim.
-    gDropTargetShim.init();
+      // Initialize the drop target shim.
+      gDropTargetShim.init();
 
 #ifdef XP_MACOSX
-    // Workaround to prevent a delay on MacOSX due to a slow drop animation.
-    document.addEventListener("dragover", this, false);
-    document.addEventListener("drop", this, false);
+      // Workaround to prevent a delay on MacOSX due to a slow drop animation.
+      document.addEventListener("dragover", this, false);
+      document.addEventListener("drop", this, false);
 #endif
+    }.bind(this));
   },
 
   /**
    * Updates the 'page-disabled' attributes of the respective DOM nodes.
    * @param aValue Whether the New Tab Page is enabled or not.
    */
   _updateAttributes: function Page_updateAttributes(aValue) {
     // Set the nodes' states.
--- a/browser/base/content/test/newtab/browser.ini
+++ b/browser/base/content/test/newtab/browser.ini
@@ -12,19 +12,17 @@ support-files =
 [browser_newtab_bug725996.js]
 [browser_newtab_bug734043.js]
 [browser_newtab_bug735987.js]
 skip-if = os == "mac" # Intermittent failures, bug 898317
 [browser_newtab_bug752841.js]
 [browser_newtab_bug765628.js]
 [browser_newtab_bug876313.js]
 [browser_newtab_bug991111.js]
-[browser_newtab_bug991210.js]
 [browser_newtab_bug998387.js]
-[browser_newtab_bug1001854.js]
 [browser_newtab_disable.js]
 [browser_newtab_drag_drop.js]
 [browser_newtab_drag_drop_ext.js]
 [browser_newtab_drop_preview.js]
 [browser_newtab_focus.js]
 [browser_newtab_perwindow_private_browsing.js]
 [browser_newtab_reset.js]
 [browser_newtab_search.js]
deleted file mode 100644
--- a/browser/base/content/test/newtab/browser_newtab_bug1001854.js
+++ /dev/null
@@ -1,20 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-const PRELOAD_PREF = "browser.newtab.preload";
-
-function runTests() {
-  // turn off preload to ensure that a newtab page loads as disabled
-  Services.prefs.setBoolPref(PRELOAD_PREF, false);
-  Services.prefs.setBoolPref(PREF_NEWTAB_ENABLED, false);
-  yield addNewTabPageTab();
-
-  let search = getContentDocument().getElementById("newtab-search-form");
-  is(search.style.width, "", "search form has no width yet");
-
-  NewTabUtils.allPages.enabled = true;
-  isnot(search.style.width, "", "search form has width set");
-
-  // restore original state
-  Services.prefs.clearUserPref(PRELOAD_PREF);
-}
deleted file mode 100644
--- a/browser/base/content/test/newtab/browser_newtab_bug991210.js
+++ /dev/null
@@ -1,41 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-const PRELOAD_PREF = "browser.newtab.preload";
-
-function runTests() {
-  // turn off preload to ensure that a newtab page loads
-  Services.prefs.setBoolPref(PRELOAD_PREF, false);
-
-  // add a test provider that waits for load
-  let afterLoadProvider = {
-    getLinks: function(callback) {
-      this.callback = callback;
-    },
-    addObserver: function() {},
-  };
-  NewTabUtils.links.addProvider(afterLoadProvider);
-
-  // wait until about:newtab loads before calling provider callback
-  addNewTabPageTab();
-  let browser = gWindow.gBrowser.selectedTab.linkedBrowser;
-  yield browser.addEventListener("load", function onLoad() {
-    browser.removeEventListener("load", onLoad, true);
-    // afterLoadProvider.callback has to be called asynchronously to make grid
-    // initilize after "load" event was handled
-    executeSoon(() => afterLoadProvider.callback([]));
-  }, true);
-
-  let {_cellMargin, _cellHeight, _cellWidth, node} = getGrid();
-  isnot(_cellMargin, null, "grid has a computed cell margin");
-  isnot(_cellHeight, null, "grid has a computed cell height");
-  isnot(_cellWidth, null, "grid has a computed cell width");
-  let {height, maxHeight, maxWidth} = node.style;
-  isnot(height, "", "grid has a computed grid height");
-  isnot(maxHeight, "", "grid has a computed grid max-height");
-  isnot(maxWidth, "", "grid has a computed grid max-width");
-
-  // restore original state
-  NewTabUtils.links.removeProvider(afterLoadProvider);
-  Services.prefs.clearUserPref(PRELOAD_PREF);
-}
--- a/browser/base/content/test/newtab/browser_newtab_update.js
+++ b/browser/base/content/test/newtab/browser_newtab_update.js
@@ -1,16 +1,21 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * Checks that newtab is updated as its links change.
  */
 
 function runTests() {
+  if (NewTabUtils.allPages.updateScheduledForHiddenPages) {
+    // Wait for dynamic updates triggered by the previous test to finish.
+    yield whenPagesUpdated(null, true);
+  }
+
   // First, start with an empty page.  setLinks will trigger a hidden page
   // update because it calls clearHistory.  We need to wait for that update to
   // happen so that the next time we wait for a page update below, we catch the
   // right update and not the one triggered by setLinks.
   //
   // Why this weird way of yielding?  First, these two functions don't return
   // promises, they call TestRunner.next when done.  Second, the point at which
   // setLinks is done is independent of when the page update will happen, so
--- a/browser/base/content/test/newtab/head.js
+++ b/browser/base/content/test/newtab/head.js
@@ -8,18 +8,17 @@ Services.prefs.setBoolPref(PREF_NEWTAB_E
 
 let tmp = {};
 Cu.import("resource://gre/modules/Promise.jsm", tmp);
 Cu.import("resource://gre/modules/NewTabUtils.jsm", tmp);
 Cu.import("resource://gre/modules/DirectoryLinksProvider.jsm", tmp);
 Cc["@mozilla.org/moz/jssubscript-loader;1"]
   .getService(Ci.mozIJSSubScriptLoader)
   .loadSubScript("chrome://browser/content/sanitize.js", tmp);
-Cu.import("resource://gre/modules/Timer.jsm", tmp);
-let {Promise, NewTabUtils, Sanitizer, clearTimeout, DirectoryLinksProvider} = tmp;
+let {Promise, NewTabUtils, Sanitizer, DirectoryLinksProvider} = tmp;
 
 let uri = Services.io.newURI("about:newtab", null, null);
 let principal = Services.scriptSecurityManager.getNoAppCodebasePrincipal(uri);
 
 let isMac = ("nsILocalFileMac" in Ci);
 let isLinux = ("@mozilla.org/gnome-gconf-service;1" in Cc);
 let isWindows = ("@mozilla.org/windows-registry-key;1" in Cc);
 let gWindow = window;
@@ -54,23 +53,16 @@ if (gBrowser.contentWindow.innerHeight <
 
 registerCleanupFunction(function () {
   while (gWindow.gBrowser.tabs.length > 1)
     gWindow.gBrowser.removeTab(gWindow.gBrowser.tabs[1]);
 
   if (oldInnerHeight)
     gBrowser.contentWindow.innerHeight = oldInnerHeight;
 
-  // Stop any update timers to prevent unexpected updates in later tests
-  let timer = NewTabUtils.allPages._scheduleUpdateTimeout;
-  if (timer) {
-    clearTimeout(timer);
-    delete NewTabUtils.allPages._scheduleUpdateTimeout;
-  }
-
   Services.prefs.clearUserPref(PREF_NEWTAB_ENABLED);
   Services.prefs.clearUserPref(PREF_NEWTAB_DIRECTORYSOURCE);
 
   return watchLinksChangeOnce();
 });
 
 /**
  * Resolves promise when directory links are downloaded and written to disk
--- a/browser/themes/linux/newtab/newTab.css
+++ b/browser/themes/linux/newtab/newTab.css
@@ -73,28 +73,44 @@
   border: none;
   background: -216px 0 transparent url(chrome://browser/skin/newtab/controls.png);
 }
 
 #newtab-toggle[page-disabled] {
   background-position: -232px 0;
 }
 
+/* ROWS */
+.newtab-row {
+  margin-bottom: 20px;
+}
+
+.newtab-row:last-child {
+  margin-bottom: 0;
+}
+
 /* CELLS */
 .newtab-cell {
+  -moz-margin-end: 20px;
   background-color: rgba(255,255,255,.2);
-  border: 1px solid #dee0e1;
+  border: 1px solid;
+  border-color: rgba(8,22,37,.12) rgba(8,22,37,.14) rgba(8,22,37,.16);
+  border-radius: 1px;
   transition: border-color 100ms ease-out;
 }
 
 .newtab-cell:empty {
   border: 1px dashed;
   border-color: rgba(8,22,37,.15) rgba(8,22,37,.17) rgba(8,22,37,.19);
 }
 
+.newtab-cell:last-child {
+  -moz-margin-end: 0;
+}
+
 .newtab-cell:hover:not(:empty):not([dragged]):not([ignorehover]) {
   border-color: rgba(8,22,37,.25) rgba(8,22,37,.27) rgba(8,22,37,.3);
 }
 
 /* SITES */
 .newtab-site {
   text-decoration: none;
   transition-property: top, left, opacity, box-shadow, background-color;
@@ -122,19 +138,20 @@
 .newtab-site[type=organic] .newtab-thumbnail,
 .newtab-site[type=sponsored] .newtab-thumbnail {
   background-position: center center;
   background-size: auto;
 }
 
 /* TITLES */
 .newtab-title {
-  color: #525c66;
-  font-family: sans-serif;
-  font-size: 13px;
+  padding: 0 8px;
+  background-color: rgba(248,249,251,.95);
+  color: #1f364c;
+  line-height: 24px;
 }
 
 .newtab-site[type=sponsored] .newtab-title {
   -moz-padding-end: 24px;
 }
 
 /* CONTROLS */
 .newtab-control {
--- a/browser/themes/osx/newtab/newTab.css
+++ b/browser/themes/osx/newtab/newTab.css
@@ -77,28 +77,44 @@
 
 @media (min-resolution: 2dppx) {
   #newtab-toggle {
     background-image: url(chrome://browser/skin/newtab/controls@2x.png);
     background-size: 296px;
   }
 }
 
+/* ROWS */
+.newtab-row {
+  margin-bottom: 20px;
+}
+
+.newtab-row:last-child {
+  margin-bottom: 0;
+}
+
 /* CELLS */
 .newtab-cell {
+  -moz-margin-end: 20px;
   background-color: rgba(255,255,255,.2);
-  border: 1px solid #dee0e1;
+  border: 1px solid;
+  border-color: rgba(8,22,37,.12) rgba(8,22,37,.14) rgba(8,22,37,.16);
+  border-radius: 1px;
   transition: border-color 100ms ease-out;
 }
 
 .newtab-cell:empty {
   border: 1px dashed;
   border-color: rgba(8,22,37,.15) rgba(8,22,37,.17) rgba(8,22,37,.19);
 }
 
+.newtab-cell:last-child {
+  -moz-margin-end: 0;
+}
+
 .newtab-cell:hover:not(:empty):not([dragged]):not([ignorehover]) {
   border-color: rgba(8,22,37,.25) rgba(8,22,37,.27) rgba(8,22,37,.3);
 }
 
 /* SITES */
 .newtab-site {
   text-decoration: none;
   transition-property: top, left, opacity, box-shadow, background-color;
@@ -126,19 +142,20 @@
 .newtab-site[type=organic] .newtab-thumbnail,
 .newtab-site[type=sponsored] .newtab-thumbnail {
   background-position: center center;
   background-size: auto;
 }
 
 /* TITLES */
 .newtab-title {
-  color: #525c66;
-  font-family: Lucida Grande;
-  font-size: 13px;
+  padding: 0 8px;
+  background-color: rgba(248,249,251,.95);
+  color: #1f364c;
+  line-height: 24px;
 }
 
 .newtab-site[type=sponsored] .newtab-title {
   -moz-padding-end: 24px;
 }
 
 /* CONTROLS */
 .newtab-control {
--- a/browser/themes/windows/newtab/newTab.css
+++ b/browser/themes/windows/newtab/newTab.css
@@ -76,28 +76,44 @@
   border: none;
   background: -216px 0 transparent url(chrome://browser/skin/newtab/controls.png);
 }
 
 #newtab-toggle[page-disabled] {
   background-position: -232px 0;
 }
 
+/* ROWS */
+.newtab-row {
+  margin-bottom: 20px;
+}
+
+.newtab-row:last-child {
+  margin-bottom: 0;
+}
+
 /* CELLS */
 .newtab-cell {
+  -moz-margin-end: 20px;
   background-color: rgba(255,255,255,.2);
-  border: 1px solid #dee0e1;
+  border: 1px solid;
+  border-color: rgba(8,22,37,.12) rgba(8,22,37,.14) rgba(8,22,37,.16);
+  border-radius: 1px;
   transition: border-color 100ms ease-out;
 }
 
 .newtab-cell:empty {
   border: 1px dashed;
   border-color: rgba(8,22,37,.15) rgba(8,22,37,.17) rgba(8,22,37,.19);
 }
 
+.newtab-cell:last-child {
+  -moz-margin-end: 0;
+}
+
 .newtab-cell:hover:not(:empty):not([dragged]):not([ignorehover]) {
   border-color: rgba(8,22,37,.25) rgba(8,22,37,.27) rgba(8,22,37,.3);
 }
 
 /* SITES */
 .newtab-site {
   text-decoration: none;
   transition-property: top, left, opacity, box-shadow, background-color;
@@ -125,19 +141,20 @@
 .newtab-site[type=organic] .newtab-thumbnail,
 .newtab-site[type=sponsored] .newtab-thumbnail {
   background-position: center center;
   background-size: auto;
 }
 
 /* TITLES */
 .newtab-title {
-  color: #525c66;
-  font-family: Segoe UI;
-  font-size: 13px;
+  padding: 0 8px;
+  background-color: rgba(248,249,251,.95);
+  color: #1f364c;
+  line-height: 24px;
 }
 
 .newtab-site[type=sponsored] .newtab-title {
   -moz-padding-end: 24px;
 }
 
 /* CONTROLS */
 .newtab-control {
--- a/toolkit/modules/NewTabUtils.jsm
+++ b/toolkit/modules/NewTabUtils.jsm
@@ -276,16 +276,20 @@ let AllPages = {
     if (!this._scheduleUpdateTimeout) {
       this._scheduleUpdateTimeout = Timer.setTimeout(() => {
         delete this._scheduleUpdateTimeout;
         this.update(null, true);
       }, SCHEDULE_UPDATE_TIMEOUT_MS);
     }
   },
 
+  get updateScheduledForHiddenPages() {
+    return !!this._scheduleUpdateTimeout;
+  },
+
   /**
    * Implements the nsIObserver interface to get notified when the preference
    * value changes or when a new copy of a page thumbnail is available.
    */
   observe: function AllPages_observe(aSubject, aTopic, aData) {
     if (aTopic == "nsPref:changed") {
       // Clear the cached value.
       this._enabled = null;