Bug 1508364 - New UI for Private Browsing with Search r=andreio

MozReview-Commit-ID: 4WSGpL5Gvde

Differential Revision: https://phabricator.services.mozilla.com/D16854

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Ricky Rosario 2019-01-21 20:14:26 +00:00
parent 11b304b442
commit bbd0796d2c
10 changed files with 405 additions and 5 deletions

View File

@ -973,6 +973,9 @@ pref("security.certerrors.recordEventTelemetry", true);
// Whether to start the private browsing mode at application startup
pref("browser.privatebrowsing.autostart", false);
// Whether to show the new private browsing UI with in-content search box.
pref("browser.privatebrowsing.searchUI", true);
// Whether the bookmark panel should be shown when bookmarking a page.
pref("browser.bookmarks.editDialog.showForNewBookmarks", true);

View File

@ -7,11 +7,13 @@
var EXPORTED_SYMBOLS = ["AboutPrivateBrowsingHandler"];
ChromeUtils.import("resource://gre/modules/remotepagemanager/RemotePageManagerParent.jsm");
ChromeUtils.import("resource://gre/modules/Services.jsm");
var AboutPrivateBrowsingHandler = {
_topics: [
"DontShowIntroPanelAgain",
"OpenPrivateWindow",
"SearchHandoff",
],
init() {
@ -40,6 +42,67 @@ var AboutPrivateBrowsingHandler = {
win.ContentBlocking.dontShowIntroPanelAgain();
break;
}
case "SearchHandoff": {
let searchAlias = "";
let searchAliases = Services.search.defaultEngine.wrappedJSObject.__internalAliases;
if (searchAliases && searchAliases.length > 0) {
searchAlias = `${searchAliases[0]} `;
}
let urlBar = aMessage.target.browser.ownerGlobal.gURLBar;
let isFirstChange = true;
if (!aMessage.data || !aMessage.data.text) {
urlBar.hiddenFocus();
} else {
// Pass the provided text to the awesomebar. Prepend the @engine shortcut.
urlBar.search(`${searchAlias}${aMessage.data.text}`);
isFirstChange = false;
}
let checkFirstChange = () => {
// Check if this is the first change since we hidden focused. If it is,
// remove hidden focus styles, prepend the search alias and hide the
// in-content search.
if (isFirstChange) {
isFirstChange = false;
urlBar.removeHiddenFocus();
urlBar.search(searchAlias);
aMessage.target.sendAsyncMessage("HideSearch");
urlBar.removeEventListener("compositionstart", checkFirstChange);
urlBar.removeEventListener("paste", checkFirstChange);
}
};
let onKeydown = ev => {
// Check if the keydown will cause a value change.
if (ev.key.length === 1 && !ev.altKey && !ev.ctrlKey && !ev.metaKey) {
checkFirstChange();
}
// If the Esc button is pressed, we are done. Show in-content search and cleanup.
if (ev.key === "Escape") {
onDone();
}
};
let onDone = () => {
// We are done. Show in-content search again and cleanup.
aMessage.target.sendAsyncMessage("ShowSearch");
urlBar.removeHiddenFocus();
urlBar.removeEventListener("keydown", onKeydown);
urlBar.removeEventListener("mousedown", onDone);
urlBar.removeEventListener("blur", onDone);
urlBar.removeEventListener("compositionstart", checkFirstChange);
urlBar.removeEventListener("paste", checkFirstChange);
};
urlBar.addEventListener("keydown", onKeydown);
urlBar.addEventListener("mousedown", onDone);
urlBar.addEventListener("blur", onDone);
urlBar.addEventListener("compositionstart", checkFirstChange);
urlBar.addEventListener("paste", checkFirstChange);
break;
}
}
},
};

View File

@ -92,7 +92,7 @@ let ACTORS = {
child: {
module: "resource:///actors/ContentSearchChild.jsm",
group: "browsers",
matches: ["about:home", "about:newtab", "about:welcome",
matches: ["about:home", "about:newtab", "about:welcome", "about:privatebrowsing",
"chrome://mochitests/content/*"],
events: {
"ContentSearchClient": {capture: true, wantUntrusted: true},

View File

@ -1,5 +1,7 @@
html.private .showNormal,
html.normal .showPrivate,
html.search-ui .dontShowSearch,
html.no-search-ui .showSearch,
body[tpEnabled] .showTpDisabled,
body:not([tpEnabled]) .showTpEnabled {
display: none !important;

View File

@ -5,6 +5,7 @@
/* eslint-env mozilla/frame-script */
const TP_PB_ENABLED_PREF = "privacy.trackingprotection.pbmode.enabled";
const PB_SEARCH_UI_ENABLED_PREF = "browser.privatebrowsing.searchUI";
document.addEventListener("DOMContentLoaded", function() {
if (!RPMIsWindowPrivate()) {
@ -34,4 +35,73 @@ document.addEventListener("DOMContentLoaded", function() {
document.getElementById("tpSubHeader").remove();
document.getElementById("tpSection").remove();
}
let searchUIEnabled = RPMGetBoolPref(PB_SEARCH_UI_ENABLED_PREF);
if (searchUIEnabled) {
setupSearchUI();
}
});
function setupSearchUI() {
// Show the new search UI and hide the old one.
document.documentElement.classList.remove("no-search-ui");
document.documentElement.classList.add("search-ui");
// Setup the private browsing myths link.
document.getElementById("private-browsing-myths").setAttribute("href",
RPMGetFormatURLPref("app.support.baseURL") + "private-browsing-myths");
// Setup the search hand-off box.
let btn = document.getElementById("search-handoff-button");
let editable = document.getElementById("fake-editable");
let HIDE_SEARCH_TOPIC = "HideSearch";
let SHOW_SEARCH_TOPIC = "ShowSearch";
let SEARCH_HANDOFF_TOPIC = "SearchHandoff";
function showSearch() {
btn.classList.remove("focused");
btn.classList.remove("hidden");
RPMRemoveMessageListener(SHOW_SEARCH_TOPIC, showSearch);
}
function hideSearch() {
btn.classList.add("hidden");
}
function handoffSearch(text) {
RPMSendAsyncMessage(SEARCH_HANDOFF_TOPIC, {text});
RPMAddMessageListener(SHOW_SEARCH_TOPIC, showSearch);
if (text) {
hideSearch();
} else {
btn.classList.add("focused");
RPMAddMessageListener(HIDE_SEARCH_TOPIC, hideSearch);
}
}
btn.addEventListener("focus", function() {
handoffSearch();
});
btn.addEventListener("click", function() {
handoffSearch();
});
// Hand-off any text that gets dropped or pasted
editable.addEventListener("drop", function(ev) {
ev.preventDefault();
let text = ev.dataTransfer.getData("text");
if (text) {
handoffSearch(text);
}
});
editable.addEventListener("paste", function(ev) {
ev.preventDefault();
handoffSearch(ev.clipboardData.getData("Text"));
});
// Load contentSearchUI so it sets the search engine icon for us.
// TODO: FIXME. We should eventually refector contentSearchUI to do only what
// we need and have it do the common search handoff work for
// about:newtab and about:privatebrowsing.
let input = document.getElementById("dummy-input");
new window.ContentSearchUIController(input, input.parentNode, "aboutprivatebrowsing", "aboutprivatebrowsing");
}

View File

@ -17,13 +17,14 @@
%aboutPrivateBrowsingDTD;
]>
<html xmlns="http://www.w3.org/1999/xhtml" class="private">
<html xmlns="http://www.w3.org/1999/xhtml" class="private no-search-ui">
<head>
<meta http-equiv="Content-Security-Policy" content="default-src chrome:"/>
<meta http-equiv="Content-Security-Policy" content="default-src chrome: blob:"/>
<link rel="icon" type="image/png" href="chrome://browser/skin/privatebrowsing/favicon.svg"/>
<link rel="stylesheet" href="chrome://browser/content/aboutPrivateBrowsing.css" type="text/css" media="all"/>
<link rel="stylesheet" href="chrome://browser/skin/privatebrowsing/aboutPrivateBrowsing.css" type="text/css" media="all"/>
<script type="application/javascript" src="chrome://browser/content/aboutPrivateBrowsing.js"></script>
<script type="application/javascript" src="chrome://browser/content/contentSearchUI.js"></script>
</head>
<body dir="&locale.dir;">
@ -31,7 +32,7 @@
<button id="startPrivateBrowsing"
class="showNormal"
accesskey="&privatebrowsingpage.openPrivateWindow.accesskey;">&privatebrowsingpage.openPrivateWindow.label;</button>
<div class="showPrivate container">
<div class="showPrivate dontShowSearch container">
<h1 class="title">
<span id="title">&privateBrowsing.title;</span>
</h1>
@ -67,5 +68,27 @@
<p class="about-info">&aboutPrivateBrowsing.learnMore3.before;<a id="learnMore" target="_blank">&aboutPrivateBrowsing.learnMore3.title;</a>&aboutPrivateBrowsing.learnMore3.after;</p>
</section>
</div>
<div class="showPrivate showSearch container">
<div class="logo-and-wordmark">
<div class="logo" />
<div class="wordmark" />
</div>
<div class="search-inner-wrapper">
<button id="search-handoff-button" class="search-handoff-button" title="&aboutPrivateBrowsing.search.placeholder;" tabindex="-1">
<div class="fake-textbox">&aboutPrivateBrowsing.search.placeholder;</div>
<div id="fake-editable" class="fake-editable" tabindex="-1" aria-hidden="true" contenteditable="" />
<div class="fake-caret" />
</button>
<input id="dummy-input" class="dummy-input" type="search" />
</div>
<div class="info">
<h1>&aboutPrivateBrowsing.info.title;</h1>
<p>
&aboutPrivateBrowsing.info.description;
<br/>
<a id="private-browsing-myths">&aboutPrivateBrowsing.info.myths;</a>
</p>
</div>
</div>
</body>
</html>

View File

@ -72,3 +72,95 @@ add_task(async function test_links() {
await BrowserTestUtils.closeWindow(win);
});
/**
* Tests the private-browsing-myths link in "about:privatebrowsing".
*/
add_task(async function test_myths_link() {
Services.prefs.setCharPref("app.support.baseURL", "https://example.com/");
registerCleanupFunction(function() {
Services.prefs.clearUserPref("app.support.baseURL");
});
let { win, tab } = await openAboutPrivateBrowsing();
await testLinkOpensUrl({ win, tab,
elementId: "private-browsing-myths",
expectedUrl: "https://example.com/private-browsing-myths",
});
await BrowserTestUtils.closeWindow(win);
});
function urlBarHasHiddenFocus(win) {
return win.gURLBar.hasAttribute("focused") && win.gURLBar.classList.contains("hidden-focus");
}
function urlBarHasNormalFocus(win) {
return win.gURLBar.hasAttribute("focused") && !win.gURLBar.classList.contains("hidden-focus");
}
/**
* Tests the search hand-off on character keydown in "about:privatebrowsing".
*/
add_task(async function test_search_handoff_on_keydown() {
let { win, tab } = await openAboutPrivateBrowsing();
await ContentTask.spawn(tab, null, async function() {
let btn = content.document.getElementById("search-handoff-button");
btn.click();
ok(btn.classList.contains("focused"), "in-content search has focus styles");
});
ok(urlBarHasHiddenFocus(win), "url bar has hidden focused");
await new Promise(r => EventUtils.synthesizeKey("f", {}, win, r));
await ContentTask.spawn(tab, null, async function() {
ok(content.document.getElementById("search-handoff-button").classList.contains("hidden"),
"in-content search is hidden");
});
ok(urlBarHasNormalFocus(win), "url bar has normal focused");
is(win.gURLBar.value, "@google f", "url bar has search text");
// Hitting ESC should reshow the in-content search
await new Promise(r => EventUtils.synthesizeKey("KEY_Escape", {}, win, r));
await ContentTask.spawn(tab, null, async function() {
ok(!content.document.getElementById("search-handoff-button").classList.contains("hidden"),
"in-content search is not");
});
await BrowserTestUtils.closeWindow(win);
});
/**
* Tests the search hand-off on composition start in "about:privatebrowsing".
*/
add_task(async function test_search_handoff_on_composition_start() {
let { win, tab } = await openAboutPrivateBrowsing();
await ContentTask.spawn(tab, null, async function() {
content.document.getElementById("search-handoff-button").click();
});
ok(urlBarHasHiddenFocus(win), "url bar has hidden focused");
await new Promise(r => EventUtils.synthesizeComposition({type: "compositionstart"}, win, r));
ok(urlBarHasNormalFocus(win), "url bar has normal focused");
await BrowserTestUtils.closeWindow(win);
});
/**
* Tests the search hand-off on paste in "about:privatebrowsing".
*/
add_task(async function test_search_handoff_on_paste() {
let { win, tab } = await openAboutPrivateBrowsing();
await ContentTask.spawn(tab, null, async function() {
content.document.getElementById("search-handoff-button").click();
});
ok(urlBarHasHiddenFocus(win), "url bar has hidden focused");
var helper = SpecialPowers.Cc["@mozilla.org/widget/clipboardhelper;1"]
.getService(SpecialPowers.Ci.nsIClipboardHelper);
helper.copyString("words");
await new Promise(r => EventUtils.synthesizeKey("v", {accelKey: true}, win, r));
ok(urlBarHasNormalFocus(win), "url bar has normal focused");
is(win.gURLBar.value, "@google words", "url bar has search text");
await BrowserTestUtils.closeWindow(win);
});

View File

@ -103,3 +103,148 @@ a.button:hover:active {
color: inherit;
background-color: #6000a1;
}
.logo-and-wordmark {
align-items: center;
display: flex;
justify-content: center;
margin-bottom: 50px;
}
.search-ui .container {
max-width: 768px;
}
.logo {
background: url("chrome://branding/content/icon128.png") no-repeat center center;
background-size: 97px;
display: inline-block;
height: 97px;
width: 97px;
}
.wordmark {
background: url("resource://activity-stream/data/content/assets/firefox-wordmark.svg") no-repeat center center;
background-size: 175px;
-moz-context-properties: fill;
display: inline-block;
fill: #fff;
height: 97px;
margin-inline-start: 15px;
width: 175px;
}
.search-inner-wrapper {
display: flex;
height: 48px;
margin-bottom: 64px;
padding: 0 22px;
}
.search-handoff-button {
background: #fff var(--newtab-search-icon) 12px center no-repeat;
background-size: 24px;
border: solid 1px rgba(249, 249, 250, 0.2);
border-radius: 3px;
box-shadow: 0 1px 4px 0 rgba(12, 12, 13, 0.2), 0 0 0 1px rgba(0, 0, 0, 0.15);
cursor: text;
font-size: 15px;
margin: 0;
padding: 0;
padding-inline-end: 48px;
padding-inline-start: 46px;
position: relative;
opacity: 1;
transition: opacity 500ms;
width: 100%;
}
.search-handoff-button.focused {
border: solid 1px #0060df;
box-shadow: 0 0 0 1px #0060df, 0 0 0 4px rgba(0, 96, 223, 0.3);
}
.search-handoff-button.hidden {
opacity: 0;
visibility: hidden;
}
.search-handoff-button:dir(rtl) {
background-position-x: right 12px;
}
.search-inner-wrapper .search-handoff-button:hover {
background-color: #fff;
}
.search-handoff-button.focused .fake-caret {
display: block;
}
.fake-editable:focus {
outline: none;
caret-color: transparent;
}
.fake-editable {
color: transparent;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.fake-textbox {
color: rgb(12, 12, 13);
opacity: 0.54;
text-align: start;
}
@keyframes caret-animation {
to {
visibility: hidden;
}
}
.fake-caret {
animation: caret-animation 1.3s steps(5, start) infinite;
background: rgb(12, 12, 13);
display: none;
inset-inline-start: 47px;
height: 17px;
position: absolute;
top: 16px;
width: 1px;
}
.dummy-input {
display: none;
}
.info {
background-color: rgba(0, 0, 0, 0.2);
background-image: url("chrome://browser/skin/privatebrowsing/private-browsing.svg");
background-position: left 32px top 20px;
background-repeat: no-repeat;
background-size: 32px;
border-radius: 6px;
letter-spacing: -0.2px;
padding: 20px;
padding-inline-start: 76px;
}
.info:dir(rtl) {
background-position: right 32px top 20px;
}
.info h1 {
font-size: 18px;
font-weight: bold;
line-height: 28px;
}
.info p {
font-size: 15px;
line-height: 25px;
}

View File

@ -16,6 +16,7 @@ class TestAboutPrivateBrowsing(PuppeteerMixin, MarionetteTestCase):
# Use a fake local support URL
support_url = 'about:blank?'
self.marionette.set_pref('app.support.baseURL', support_url)
self.marionette.set_pref('browser.privatebrowsing.searchUI', False)
self.pb_url = support_url + 'private-browsing'

View File

@ -25,7 +25,8 @@ let RPMAccessManager = {
// "sendAsyncMessage": handled within AboutPrivateBrowsingHandler.jsm
// "setBoolPref": handled within AsyncPrefs.jsm and uses the pref
// ["privacy.trackingprotection.pbmode.enabled"],
"getBoolPref": ["privacy.trackingprotection.pbmode.enabled"],
"getBoolPref": ["privacy.trackingprotection.pbmode.enabled",
"browser.privatebrowsing.searchUI"],
"getFormatURLPref": ["privacy.trackingprotection.introURL",
"app.support.baseURL"],
"isWindowPrivate": ["yes"],