From 85b489310e6288aa70eb3482a4a46f3f4173fe7c Mon Sep 17 00:00:00 2001 From: Marina Samuel Date: Wed, 13 May 2015 12:23:45 -0400 Subject: [PATCH] Bug 1138818 - Part 1 - Onboarding UI without the tile images. r=adw --- browser/app/profile/firefox.js | 3 + browser/base/content/newtab/intro.js | 200 +++++++++++++++- browser/base/content/newtab/newTab.css | 213 ++++++++++++++++++ browser/base/content/newtab/newTab.xul | 29 +++ .../test/newtab/browser_newtab_intro.js | 107 +++++++-- 5 files changed, 524 insertions(+), 28 deletions(-) diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index 51db411d6b45..24d9a3a994b7 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -1632,6 +1632,9 @@ pref("browser.newtab.preload", true); // Remembers if the about:newtab intro has been shown pref("browser.newtabpage.introShown", false); +// Remembers if the about:newtab update intro has been shown +pref("browser.newtabpage.updateIntroShown", false); + // Toggles the content of 'about:newtab'. Shows the grid when enabled. pref("browser.newtabpage.enabled", true); diff --git a/browser/base/content/newtab/intro.js b/browser/base/content/newtab/intro.js index f94c255e26d3..3644b0cc87cc 100644 --- a/browser/base/content/newtab/intro.js +++ b/browser/base/content/newtab/intro.js @@ -5,13 +5,72 @@ #endif const PREF_INTRO_SHOWN = "browser.newtabpage.introShown"; +const PREF_UPDATE_INTRO_SHOWN = "browser.newtabpage.updateIntroShown"; + +// These consts indicate the type of intro/onboarding we show. +const WELCOME = "welcome"; +const UPDATE = "update"; + +// The maximum paragraph ID listed for 'newtab.intro.paragraph' +// strings in newTab.properties +const MAX_PARAGRAPH_ID = 9; + +const NUM_INTRO_PAGES = 3; let gIntro = { + _enUSStrings: { + "newtab.intro.paragraph2": "In order to provide this service, Mozilla collects and uses certain analytics information relating to your use of the tiles in accordance with our %1$S.", + "newtab.intro.paragraph4": "You can turn off this feature by clicking the gear (%1$S) button and selecting 'Show blank page' in the %2$S menu.", + "newtab.intro.paragraph5": "New Tab will show the sites you visit most frequently, along with sites we think might be of interest to you. To get started, you'll see several sites from Mozilla.", + "newtab.intro.paragraph6": "You can %1$S or %2$S any site by using the controls available on rollover.", + "newtab.intro.paragraph7": "Some of the sites you will see may be suggested by Mozilla and may be sponsored by a Mozilla partner. We'll always indicate which sites are sponsored.", + "newtab.intro.paragraph8": "Firefox will only show sites that most closely match your interests on the Web. %1$S", + "newtab.intro.paragraph9": "Now when you open New Tab, you'll also see sites we think might be interesting to you.", + "newtab.intro.controls": "New Tab Controls", + "newtab.learn.link2": "More about New Tab", + "newtab.privacy.link2": "About your privacy", + "newtab.intro.remove": "remove", + "newtab.intro.pin": "pin", + "newtab.intro.header.welcome": "Welcome to New Tab on %1$S", + "newtab.intro.header.update": "New Tab got an update!", + "newtab.intro.skip": "Skip this", + "newtab.intro.continue": "Continue tour", + "newtab.intro.back": "Back", + "newtab.intro.next": "Next", + "newtab.intro.gotit": "Got it!", + "newtab.intro.firefox": "Firefox!" + }, + _nodeIDSuffixes: [ "panel", "what", + "mask", + "modal", + "numerical-progress", + "text", + "buttons", + "header", + "footer" ], + /** + * The paragraphs & buttons to show on each page in the intros. + * + * _introPages.welcome and _introPages.update contain an array of + * indices of paragraphs to be used to lookup text in _paragraphs + * for each page of the intro. + * + * Similarly, _introPages.buttons is used to lookup text for buttons + * on each page of the intro. + */ + _introPages: { + "welcome": [[0,1],[2,3],[4,5]], + "update": [[6,5],[4,3],[0,1]], + "buttons": [["skip", "continue"],["back", "next"],["back", "gotit"]] + }, + + _paragraphs: [], + _nodes: {}, init: function() { @@ -19,22 +78,149 @@ let gIntro = { this._nodes[idSuffix] = document.getElementById("newtab-intro-" + idSuffix); } - this._nodes.panel.addEventListener("popupshowing", e => this._setUpPanel()); - this._nodes.panel.addEventListener("popuphidden", e => this._hidePanel()); - this._nodes.what.addEventListener("click", e => this.showPanel()); + if (DirectoryLinksProvider.locale != "en-US") { + this._nodes.what.style.display = "block"; + this._nodes.panel.addEventListener("popupshowing", e => this._setUpPanel()); + this._nodes.panel.addEventListener("popuphidden", e => this._hidePanel()); + this._nodes.what.addEventListener("click", e => this.showPanel()); + } + }, + + _goToPage: function(pageNum) { + this._currPage = pageNum; + + this._nodes["numerical-progress"].innerHTML = `${this._bold(pageNum + 1)} / ${NUM_INTRO_PAGES}`; + this._nodes["numerical-progress"].setAttribute("page", pageNum); + + // Set the paragraphs + let paragraphNodes = this._nodes.text.getElementsByTagName("p"); + let paragraphIDs = this._introPages[this._onboardingType][pageNum]; + paragraphIDs.forEach((arg, index) => { + paragraphNodes[index].innerHTML = this._paragraphs[arg]; + }); + + // Set the buttons + let buttonNodes = this._nodes.buttons.getElementsByTagName("input"); + let buttonIDs = this._introPages.buttons[pageNum]; + buttonIDs.forEach((arg, index) => { + buttonNodes[index].setAttribute("value", this._newTabString("intro." + arg)); + }); + }, + + _bold: function(str) { + return `${str}` + }, + + _link: function(url, text) { + return `${text}`; + }, + + _span: function(text, className) { + return `${text}`; + }, + + _exitIntro: function() { + this._nodes.mask.style.opacity = 0; + this._nodes.mask.addEventListener("transitionend", () => { + this._nodes.mask.style.display = "none"; + }); + }, + + _back: function() { + if (this._currPage == 0) { + // We're on the first page so 'back' means exit. + this._exitIntro(); + return; + } + this._goToPage(this._currPage - 1); + }, + + _next: function() { + if (this._currPage == (NUM_INTRO_PAGES - 1)) { + // We're on the last page so 'next' means exit. + this._exitIntro(); + return; + } + this._goToPage(this._currPage + 1); + }, + + _generateParagraphs: function() { + let customizeIcon = ''; + + let substringMappings = { + "2": [this._link(TILES_PRIVACY_LINK, newTabString("privacy.link"))], + "4": [customizeIcon, this._bold(this._newTabString("intro.controls"))], + "6": [this._bold(this._newTabString("intro.remove")), this._bold(this._newTabString("intro.pin"))], + "7": [this._link(TILES_INTRO_LINK, newTabString("learn.link"))], + "8": [this._link(TILES_INTRO_LINK, newTabString("learn.link"))] + } + + for (let i = 1; i <= MAX_PARAGRAPH_ID; i++) { + try { + this._paragraphs.push(this._newTabString("intro.paragraph" + i, substringMappings[i])) + } catch (ex) { + // Paragraph with this ID doesn't exist so continue + } + } + }, + + _newTabString: function(str, substrArr) { + let regExp = /%[0-9]\$S/g; + let paragraph = this._enUSStrings["newtab." + str]; + + if (!paragraph) { + throw new Error("Paragraph doesn't exist"); + } + + let matches; + while ((matches = regExp.exec(paragraph)) !== null) { + let match = matches[0]; + let index = match.charAt(1); // Get the digit in the regExp. + paragraph = paragraph.replace(match, substrArr[index - 1]); + } + return paragraph; }, showIfNecessary: function() { if (!Services.prefs.getBoolPref(PREF_INTRO_SHOWN)) { - Services.prefs.setBoolPref(PREF_INTRO_SHOWN, true); + this._onboardingType = WELCOME; + this.showPanel(); + } else if (!Services.prefs.getBoolPref(PREF_UPDATE_INTRO_SHOWN) && DirectoryLinksProvider.locale == "en-US") { + this._onboardingType = UPDATE; this.showPanel(); } + Services.prefs.setBoolPref(PREF_INTRO_SHOWN, true); + Services.prefs.setBoolPref(PREF_UPDATE_INTRO_SHOWN, true); }, showPanel: function() { - // Point the panel at the 'what' link - this._nodes.panel.hidden = false; - this._nodes.panel.openPopup(this._nodes.what); + if (DirectoryLinksProvider.locale != "en-US") { + // Point the panel at the 'what' link + this._nodes.panel.hidden = false; + this._nodes.panel.openPopup(this._nodes.what); + return; + } + + this._nodes.mask.style.display = "block"; + this._nodes.mask.style.opacity = 1; + + if (!this._paragraphs.length) { + // It's our first time showing the panel. Do some initial setup + this._generateParagraphs(); + } + this._goToPage(0); + + // Header text + let boldSubstr = this._onboardingType == WELCOME ? this._span(this._newTabString("intro.firefox"), "bold") : ""; + this._nodes.header.innerHTML = this._newTabString("intro.header." + this._onboardingType, [boldSubstr]); + + // Footer links + let footerLinkNodes = this._nodes.footer.getElementsByTagName("li"); + [this._link(TILES_INTRO_LINK, this._newTabString("learn.link2")), + this._link(TILES_PRIVACY_LINK, this._newTabString("privacy.link2")), + ].forEach((arg, index) => { + footerLinkNodes[index].innerHTML = arg; + }); }, _setUpPanel: function() { diff --git a/browser/base/content/newtab/newTab.css b/browser/base/content/newtab/newTab.css index 5724ec8391bf..dd06ada072ea 100644 --- a/browser/base/content/newtab/newTab.css +++ b/browser/base/content/newtab/newTab.css @@ -55,6 +55,7 @@ input[type=button] { position: absolute; right: 70px; top: 20px; + display: none; } #newtab-intro-what:-moz-locale-dir(rtl) { @@ -624,3 +625,215 @@ input[type=button] { font: message-box; font-size: 16px; } + +/** + * Onboarding styling + */ + + #newtab-intro-mask { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: #424F5A; + z-index:102; + background-color: rgba(66,79,90,0.95); + transition: opacity .5s linear; + overflow: auto; + display: none; +} + +#newtab-intro-modal { + font-family: "Helvetica"; + width: 700px; + height: 500px; + position: fixed; + left: 0; + right: 0; + top: 0; + bottom: 0; + margin: auto; + background: linear-gradient(#FFFFFF, #F9F9F9); + box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.7); + border-radius: 8px 8px 0px 0px; +} + +#newtab-intro-header { + font-size: 28px; + color: #737980; + text-align: center; + top: 50px; + position: relative; + border-bottom: 2px solid #E0DFE0; + padding-bottom: 10px; + width: 600px; + display: block; + margin: 0px auto; + font-weight: 100; +} + +#newtab-intro-header .bold { + font-weight: 500; + color: #343F48; +} + +#newtab-intro-footer { + width: 100%; + height: 55px; + margin: 0px auto; + display: block; + position: absolute; + bottom: 0px; + background-color: white; + box-shadow: 0 -1px 4px -1px #EBEBEB; + text-align: center; + vertical-align: middle; + line-height: 55px; +} + +#newtab-intro-footer > ul { + list-style-type: none; + margin: 0px; + padding: 0px; +} + +#newtab-intro-footer > ul > li { + display: inline; + padding-left: 10px; + padding-right: 10px; +} + +#newtab-intro-footer > ul > li > a { + text-decoration: none; + color: #4A90E2; +} + +#newtab-intro-footer > ul > li > a:visited { + color: #171F26; +} + +#newtab-intro-footer > ul > :first-child { + border-right: solid 1px #C1C1C1; +} + +#newtab-intro-body { + height: 300px; + position: relative; + display: block; + top: 50px; + margin: 25px 50px 30px; +} + +#newtab-intro-content > * { + display: inline-block; +} + +#newtab-intro-content { + height: 210px; + position: relative; +} + +#newtab-intro-buttons { + height: 90px; + text-align: center; + vertical-align: middle; + line-height: 90px; +} + +#newtab-intro-tile { + width: 290px; + height: 100%; +} + +#newtab-intro-text { + height: 100%; + width: 270px; + right: 0px; + position: absolute; + font-size: 14px; + line-height: 20px; +} + +#newtab-intro-text > p { + margin: 0 0 1em 0; +} + +#newtab-intro-text .newtab-control { + background-size: 18px auto; + height: 18px; + width: 18px; + vertical-align: middle; + opacity: 1; + position: inherit; +} + +#newtab-intro-buttons > input { + width: 150px; + height: 50px; + margin: 0px 5px; + vertical-align: bottom; + border-radius: 2px; + border: solid 1px #2C72E2; + background-color: #FFFFFF; + color: #4A90E2; + -moz-user-focus: normal; +} + +#newtab-intro-buttons > input[default] { + background-color: #4A90E2; + color: #FFFFFF; +} + +#newtab-intro-buttons > input:hover { + background-color: #2C72E2; + color: #FFFFFF; +} + +#newtab-intro-progress { + position: absolute; + width: 100%; +} + +#newtab-intro-numerical-progress { + text-align: center; + top: 15px; + position: relative; + font-size: 12px; + color: #424F5A; +} + +#newtab-intro-graphical-progress { + text-align: left; + border-radius: 1.5px; + overflow: hidden; + position: relative; + margin: 10px auto 0px; + height: 3px; + top: 8px; + width: 35px; + background-color: #DCDCDC; +} + +#indicator { + position: absolute; + top: 0px; + left: 0px; + display: inline-block; + width: 0%; + height: 4px; + background: none repeat scroll 0% 0% #FF9500; + transition: width 0.3s ease-in-out 0s; +} + +#newtab-intro-numerical-progress[page="0"] + #newtab-intro-graphical-progress > #indicator { + width: 33%; +} + +#newtab-intro-numerical-progress[page="1"] + #newtab-intro-graphical-progress > #indicator { + width: 66%; +} + +#newtab-intro-numerical-progress[page="2"] + #newtab-intro-graphical-progress > #indicator { + width: 100%; +} diff --git a/browser/base/content/newtab/newTab.xul b/browser/base/content/newtab/newTab.xul index 0f66a06ce921..0c2aae495798 100644 --- a/browser/base/content/newtab/newTab.xul +++ b/browser/base/content/newtab/newTab.xul @@ -58,6 +58,35 @@ +
+
+
+
+
+ +
+
+
+
+
+
+
+

+

+
+
+ + +
+
+ +
+
+
diff --git a/browser/base/content/test/newtab/browser_newtab_intro.js b/browser/base/content/test/newtab/browser_newtab_intro.js index a855232c601f..3c25ed260a43 100644 --- a/browser/base/content/test/newtab/browser_newtab_intro.js +++ b/browser/base/content/test/newtab/browser_newtab_intro.js @@ -2,50 +2,115 @@ http://creativecommons.org/publicdomain/zero/1.0/ */ const INTRO_PREF = "browser.newtabpage.introShown"; +const UPDATE_INTRO_PREF = "browser.newtabpage.updateIntroShown"; const PRELOAD_PREF = "browser.newtab.preload"; function runTests() { let origIntro = Services.prefs.getBoolPref(INTRO_PREF); + let origUpdateIntro = Services.prefs.getBoolPref(UPDATE_INTRO_PREF); let origPreload = Services.prefs.getBoolPref(PRELOAD_PREF); registerCleanupFunction(_ => { Services.prefs.setBoolPref(INTRO_PREF, origIntro); + Services.prefs.setBoolPref(INTRO_PREF, origUpdateIntro); Services.prefs.setBoolPref(PRELOAD_PREF, origPreload); }); // Test with preload false Services.prefs.setBoolPref(INTRO_PREF, false); + Services.prefs.setBoolPref(UPDATE_INTRO_PREF, false); Services.prefs.setBoolPref(PRELOAD_PREF, false); - let panel; - function maybeWaitForPanel() { - // If already open, no need to wait - if (panel.state == "open") { - executeSoon(TestRunner.next); - return; - } - - // We're expecting the panel to open, so wait for it - panel.addEventListener("popupshown", TestRunner.next); - isnot(panel.state, "open", "intro panel can be slow to show"); - } - + let intro; yield addNewTabPageTab(); - panel = getContentDocument().getElementById("newtab-intro-panel"); - yield maybeWaitForPanel(); - is(panel.state, "open", "intro automatically shown on first opening"); + intro = getContentDocument().getElementById("newtab-intro-mask"); + is(intro.style.opacity, 1, "intro automatically shown on first opening"); + is(getContentDocument().getElementById("newtab-intro-header").innerHTML, + 'Welcome to New Tab on Firefox!', "we show the first-run intro."); is(Services.prefs.getBoolPref(INTRO_PREF), true, "newtab remembers that the intro was shown"); + is(Services.prefs.getBoolPref(UPDATE_INTRO_PREF), true, "newtab avoids showing update if intro was shown"); yield addNewTabPageTab(); - panel = getContentDocument().getElementById("newtab-intro-panel"); - is(panel.state, "closed", "intro not shown on second opening"); + intro = getContentDocument().getElementById("newtab-intro-mask"); + is(intro.style.opacity, 0, "intro not shown on second opening"); // Test with preload true Services.prefs.setBoolPref(INTRO_PREF, false); Services.prefs.setBoolPref(PRELOAD_PREF, true); yield addNewTabPageTab(); - panel = getContentDocument().getElementById("newtab-intro-panel"); - yield maybeWaitForPanel(); - is(panel.state, "open", "intro automatically shown on preloaded opening"); + intro = getContentDocument().getElementById("newtab-intro-mask"); + is(intro.style.opacity, 1, "intro automatically shown on preloaded opening"); + is(getContentDocument().getElementById("newtab-intro-header").innerHTML, + 'Welcome to New Tab on Firefox!', "we show the first-run intro."); is(Services.prefs.getBoolPref(INTRO_PREF), true, "newtab remembers that the intro was shown"); + is(Services.prefs.getBoolPref(UPDATE_INTRO_PREF), true, "newtab avoids showing update if intro was shown"); + + // Test with first run true but update false + Services.prefs.setBoolPref(UPDATE_INTRO_PREF, false); + + yield addNewTabPageTab(); + intro = getContentDocument().getElementById("newtab-intro-mask"); + is(intro.style.opacity, 1, "intro automatically shown on preloaded opening"); + is(getContentDocument().getElementById("newtab-intro-header").innerHTML, + "New Tab got an update!", "we show the update intro."); + is(Services.prefs.getBoolPref(INTRO_PREF), true, "INTRO_PREF stays true"); + is(Services.prefs.getBoolPref(UPDATE_INTRO_PREF), true, "newtab remembers that the update intro was show"); + + // Test clicking the 'next' and 'back' buttons. + let buttons = getContentDocument().getElementById("newtab-intro-buttons").getElementsByTagName("input"); + let progress = getContentDocument().getElementById("newtab-intro-numerical-progress"); + let back = buttons[0]; + let next = buttons[1]; + + is(progress.getAttribute("page"), 0, "we are on the first page"); + is(intro.style.opacity, 1, "intro visible"); + + + let createMutationObserver = function(fcn) { + return new Promise(resolve => { + let observer = new MutationObserver(function(mutations) { + fcn(); + observer.disconnect(); + resolve(); + }); + let config = { attributes: true, attributeFilter: ["style"], childList: true }; + observer.observe(progress, config); + }); + } + + let p = createMutationObserver(function() { + is(progress.getAttribute("page"), 1, "we get to the 2nd page"); + is(intro.style.opacity, 1, "intro visible"); + }); + next.click(); + yield p.then(TestRunner.next); + + p = createMutationObserver(function() { + is(progress.getAttribute("page"), 2, "we get to the 3rd page"); + is(intro.style.opacity, 1, "intro visible"); + }); + next.click(); + yield p.then(TestRunner.next); + + p = createMutationObserver(function() { + is(progress.getAttribute("page"), 1, "go back to 2nd page"); + is(intro.style.opacity, 1, "intro visible"); + }); + back.click(); + yield p.then(TestRunner.next); + + p = createMutationObserver(function() { + is(progress.getAttribute("page"), 0, "go back to 1st page"); + is(intro.style.opacity, 1, "intro visible"); + }); + back.click(); + yield p.then(TestRunner.next); + + + p = createMutationObserver(function() { + is(progress.getAttribute("page"), 0, "another back will 'skip tutorial'"); + is(intro.style.opacity, 0, "intro exited"); + }); + back.click(); + p.then(TestRunner.next); }