Commit 97c6133d authored by Tibor Goldschwendt's avatar Tibor Goldschwendt Committed by Commit Bot

[webui][ntp] Support animated doodles and image doodle clicks

Animated doodles show a static preview image first and an animated image
(e.g. a gif) on click. Clicking the animation of an animated doodle or
the image of a simple doodle opens a doodle-associated URL in a new
tab/window.

To support displaying the animated image (which is external content) this
CL redefines the chrome-untrusted://new-tab-page/image?<url> helper that
now behaves like an img tag. The previous
chrome-untrusted://new-tab-page/image?<url> moved to
chrome-untrusted://new-tab-page/background_image?<url>.

Bug: 1039910
Change-Id: I57a6e0ca07271e5b9a055791a545d031d8694dc1
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2134695Reviewed-by: default avatarAlex Gough <ajgo@chromium.org>
Reviewed-by: default avatarEsmael Elmoslimany <aee@chromium.org>
Commit-Queue: Tibor Goldschwendt <tiborg@chromium.org>
Cr-Commit-Position: refs/heads/master@{#756776}
parent 3b3edd9e
...@@ -53,7 +53,7 @@ ...@@ -53,7 +53,7 @@
} }
#logo { #logo {
margin-bottom: 38px; margin-bottom: 8px;
} }
ntp-fakebox { ntp-fakebox {
......
...@@ -163,7 +163,7 @@ class AppElement extends PolymerElement { ...@@ -163,7 +163,7 @@ class AppElement extends PolymerElement {
if (!this.theme_ || !this.theme_.backgroundImageUrl) { if (!this.theme_ || !this.theme_.backgroundImageUrl) {
return ''; return '';
} }
return `image?${this.theme_.backgroundImageUrl.url}`; return `background_image?${this.theme_.backgroundImageUrl.url}`;
} }
/** /**
......
...@@ -30,6 +30,11 @@ export class BrowserProxy { ...@@ -30,6 +30,11 @@ export class BrowserProxy {
window.location.href = href; window.location.href = href;
} }
/** @param {string} url */
open(url) {
window.open(url, '_blank');
}
/** /**
* @param {function()} callback * @param {function()} callback
* @param {number} duration * @param {number} duration
......
...@@ -40,7 +40,7 @@ ...@@ -40,7 +40,7 @@
<div class="tile" tabindex="0" title="[[item.label]]" role="button" <div class="tile" tabindex="0" title="[[item.label]]" role="button"
on-click="onCollectionClick_"> on-click="onCollectionClick_">
<ntp-untrusted-iframe class="image" <ntp-untrusted-iframe class="image"
path="image?[[item.previewImageUrl.url]]"> path="background_image?[[item.previewImageUrl.url]]">
</ntp-untrusted-iframe> </ntp-untrusted-iframe>
<div class="label">[[item.label]]</div> <div class="label">[[item.label]]</div>
</div> </div>
...@@ -52,7 +52,7 @@ ...@@ -52,7 +52,7 @@
<template> <template>
<div class="tile" tabindex="0" title="[[item.label]]" role="button"> <div class="tile" tabindex="0" title="[[item.label]]" role="button">
<ntp-untrusted-iframe class="image" <ntp-untrusted-iframe class="image"
path="image?[[item.previewImageUrl.url]]"> path="background_image?[[item.previewImageUrl.url]]">
</ntp-untrusted-iframe> </ntp-untrusted-iframe>
</div> </div>
</template> </template>
......
<style> <style include="cr-hidden-style">
:host(:not([hidden])) { :host {
display: inline-block; display: inline-block;
min-height: 230px;
} }
#singleColoredLogo, #singleColoredLogo,
...@@ -21,8 +22,32 @@ ...@@ -21,8 +22,32 @@
background-image: url(chrome://resources/images/google_logo.svg); background-image: url(chrome://resources/images/google_logo.svg);
} }
#imageContainer {
cursor: pointer;
display: grid;
height: 230px;
width: 500px;
}
#imageContainer > * {
grid-column-start: 1;
grid-row-start: 1;
}
#image {
max-height: 100%;
max-width: 100%;
}
#animation {
height: 100%;
pointer-events: none;
width: 100%;
}
#iframe { #iframe {
height: var(--height, 200px); height: var(--height, 200px);
margin-bottom: 30px;
transition-duration: var(--duration, 100ms); transition-duration: var(--duration, 100ms);
transition-property: height, width; transition-property: height, width;
width: var(--width, 500px); width: var(--width, 500px);
...@@ -35,7 +60,17 @@ ...@@ -35,7 +60,17 @@
<div id="multiColoredLogo" hidden="[[singleColored]]"></div> <div id="multiColoredLogo" hidden="[[singleColored]]"></div>
</div> </div>
<div id="doodle"> <div id="doodle">
<img id="image" src="[[imageUrl_]]" hidden="[[!imageUrl_]]"></img> <div id="imageContainer" hidden="[[!doodle_.content.imageDoodle]]"
on-click="onImageClick_">
<!-- The static image is always visible and the animated image is stacked
on top of the static image so that there is no flicker when starting
the animation. -->
<img id="image" src="[[imageUrl_]]">
</img>
<ntp-untrusted-iframe id="animation" path="[[animationUrl_]]"
hidden="[[!showAnimation_]]">
</ntp-untrusted-iframe>
</div>
<ntp-untrusted-iframe id="iframe" path="[[iframeUrl_]]" <ntp-untrusted-iframe id="iframe" path="[[iframeUrl_]]"
hidden="[[!iframeUrl_]]" hidden="[[!iframeUrl_]]"
style="--duration: [[valueOrUnset_(duration_)]]; style="--duration: [[valueOrUnset_(duration_)]];
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'chrome://resources/cr_elements/hidden_style_css.m.js';
import 'chrome://resources/polymer/v3_0/iron-pages/iron-pages.js'; import 'chrome://resources/polymer/v3_0/iron-pages/iron-pages.js';
import './untrusted_iframe.js'; import './untrusted_iframe.js';
...@@ -60,6 +61,18 @@ class LogoElement extends PolymerElement { ...@@ -60,6 +61,18 @@ class LogoElement extends PolymerElement {
type: String, type: String,
}, },
/** @private */
showAnimation_: {
type: Boolean,
value: false,
},
/** @private */
animationUrl_: {
computed: 'computeAnimationUrl_(doodle_)',
type: String,
},
/** @private */ /** @private */
iframeUrl_: { iframeUrl_: {
computed: 'computeIframeUrl_(doodle_)', computed: 'computeIframeUrl_(doodle_)',
...@@ -134,13 +147,41 @@ class LogoElement extends PolymerElement { ...@@ -134,13 +147,41 @@ class LogoElement extends PolymerElement {
return 'logo'; return 'logo';
} }
/**
* Called when a simple or animated doodle was clicked. Starts animation if
* clicking preview image of animated doodle. Otherwise, opens
* doodle-associated URL in new tab/window.
* @private
*/
onImageClick_() {
if (!this.showAnimation_ && this.doodle_ &&
this.doodle_.content.imageDoodle.animationUrl) {
this.showAnimation_ = true;
return;
}
BrowserProxy.getInstance().open(
this.doodle_.content.imageDoodle.onClickUrl.url);
}
/** /**
* @return {string} * @return {string}
* @private * @private
*/ */
computeImageUrl_() { computeImageUrl_() {
return (this.doodle_ && this.doodle_.content.image) ? return (this.doodle_ && this.doodle_.content.imageDoodle &&
this.doodle_.content.image : this.doodle_.content.imageDoodle.imageUrl) ?
this.doodle_.content.imageDoodle.imageUrl.url :
'';
}
/**
* @return {string}
* @private
*/
computeAnimationUrl_() {
return (this.doodle_ && this.doodle_.content.imageDoodle &&
this.doodle_.content.imageDoodle.animationUrl) ?
`image?${this.doodle_.content.imageDoodle.animationUrl.url}` :
''; '';
} }
......
...@@ -107,6 +107,9 @@ ...@@ -107,6 +107,9 @@
file="untrusted/promo.js" type="chrome_html" compress="gzip" /> file="untrusted/promo.js" type="chrome_html" compress="gzip" />
<structure name="IDR_NEW_TAB_PAGE_UNTRUSTED_IMAGE_HTML" <structure name="IDR_NEW_TAB_PAGE_UNTRUSTED_IMAGE_HTML"
file="untrusted/image.html" type="chrome_html" compress="gzip" /> file="untrusted/image.html" type="chrome_html" compress="gzip" />
<structure name="IDR_NEW_TAB_PAGE_UNTRUSTED_BACKGROUND_IMAGE_HTML"
file="untrusted/background_image.html" type="chrome_html"
compress="gzip" />
<structure name="IDR_NEW_TAB_PAGE_UNTRUSTED_IFRAME_HTML" <structure name="IDR_NEW_TAB_PAGE_UNTRUSTED_IFRAME_HTML"
file="untrusted/iframe.html" type="chrome_html" compress="gzip" /> file="untrusted/iframe.html" type="chrome_html" compress="gzip" />
</structures> </structures>
......
<!doctype html>
<html>
<head>
<style>
html,
body {
height: 100%;
width: 100%;
}
body {
background-image: url($i18nRaw{url});
background-position: center;
background-size: cover;
margin: 0
}
</style>
</head>
<body></body>
</html>
...@@ -9,12 +9,16 @@ ...@@ -9,12 +9,16 @@
} }
body { body {
background-image: url($i18nRaw{url});
background-position: center;
background-size: cover;
margin: 0 margin: 0
} }
img {
max-height: 100%;
max-width: 100%;
}
</style> </style>
</head> </head>
<body></body> <body>
<img src="$i18nRaw{url}"></img>
</body>
</html> </html>
...@@ -111,10 +111,20 @@ struct Theme { ...@@ -111,10 +111,20 @@ struct Theme {
ThemeInfo info; ThemeInfo info;
}; };
// The contents of simple and animated doodles.
struct ImageDoodleContent {
// Doodle image encoded as data URL.
url.mojom.Url image_url;
// URL opened in new tab when the doodle is clicked.
url.mojom.Url on_click_url;
// URL pointing to animated content (e.g. gif). Only set for animated doodles.
url.mojom.Url? animation_url;
};
// The contents of a doodle. // The contents of a doodle.
union DoodleContent { union DoodleContent {
// Doodle image encoded as data URL. Set for static and animated doodles. // Set for simple and animated doodles.
string image; ImageDoodleContent image_doodle;
// URL pointing to doodle page. Set for interactive doodles. // URL pointing to doodle page. Set for interactive doodles.
url.mojom.Url url; url.mojom.Url url;
}; };
......
...@@ -418,27 +418,30 @@ void NewTabPageHandler::OnLogoAvailable( ...@@ -418,27 +418,30 @@ void NewTabPageHandler::OnLogoAvailable(
return; return;
} }
auto doodle = new_tab_page::mojom::Doodle::New(); auto doodle = new_tab_page::mojom::Doodle::New();
switch (logo->metadata.type) { if (logo->metadata.type == search_provider_logos::LogoType::SIMPLE ||
case search_provider_logos::LogoType::SIMPLE: logo->metadata.type == search_provider_logos::LogoType::ANIMATED) {
case search_provider_logos::LogoType::ANIMATED: { if (!logo->encoded_image) {
if (!logo->encoded_image) {
std::move(callback).Run(nullptr);
return;
}
std::string base64;
base::Base64Encode(logo->encoded_image->data(), &base64);
auto data_url =
base::StringPrintf("data:%s;base64,%s",
logo->metadata.mime_type.c_str(), base64.c_str());
doodle->content = new_tab_page::mojom::DoodleContent::NewImage(data_url);
} break;
case search_provider_logos::LogoType::INTERACTIVE:
doodle->content = new_tab_page::mojom::DoodleContent::NewUrl(
logo->metadata.full_page_url);
break;
default:
std::move(callback).Run(nullptr); std::move(callback).Run(nullptr);
return; return;
}
auto image_doodle_content = new_tab_page::mojom::ImageDoodleContent::New();
std::string base64;
base::Base64Encode(logo->encoded_image->data(), &base64);
image_doodle_content->image_url = GURL(base::StringPrintf(
"data:%s;base64,%s", logo->metadata.mime_type.c_str(), base64.c_str()));
image_doodle_content->on_click_url = logo->metadata.on_click_url;
if (logo->metadata.type == search_provider_logos::LogoType::ANIMATED) {
image_doodle_content->animation_url = logo->metadata.animated_url;
}
doodle->content = new_tab_page::mojom::DoodleContent::NewImageDoodle(
std::move(image_doodle_content));
} else if (logo->metadata.type ==
search_provider_logos::LogoType::INTERACTIVE) {
doodle->content = new_tab_page::mojom::DoodleContent::NewUrl(
logo->metadata.full_page_url);
} else {
std::move(callback).Run(nullptr);
return;
} }
std::move(callback).Run(std::move(doodle)); std::move(callback).Run(std::move(doodle));
} }
...@@ -98,21 +98,17 @@ void UntrustedSource::StartDataRequest( ...@@ -98,21 +98,17 @@ void UntrustedSource::StartDataRequest(
bundle.LoadDataResourceBytes(IDR_NEW_TAB_PAGE_UNTRUSTED_PROMO_JS)); bundle.LoadDataResourceBytes(IDR_NEW_TAB_PAGE_UNTRUSTED_PROMO_JS));
return; return;
} }
if (path == "image" && url_param.is_valid() && if ((path == "image" || path == "background_image" || path == "iframe") &&
url_param.SchemeIs(url::kHttpsScheme)) { url_param.is_valid() && url_param.SchemeIs(url::kHttpsScheme)) {
ui::TemplateReplacements replacements; ui::TemplateReplacements replacements;
replacements["url"] = url_param.spec(); replacements["url"] = url_param.spec();
std::string html = int resource_id =
FormatTemplate(IDR_NEW_TAB_PAGE_UNTRUSTED_IMAGE_HTML, replacements); (path == "image")
std::move(callback).Run(base::RefCountedString::TakeString(&html)); ? IDR_NEW_TAB_PAGE_UNTRUSTED_IMAGE_HTML
return; : (path == "background_image")
} ? IDR_NEW_TAB_PAGE_UNTRUSTED_BACKGROUND_IMAGE_HTML
if (path == "iframe" && url_param.is_valid() && : IDR_NEW_TAB_PAGE_UNTRUSTED_IFRAME_HTML;
url_param.SchemeIs(url::kHttpsScheme)) { std::string html = FormatTemplate(resource_id, replacements);
ui::TemplateReplacements replacements;
replacements["url"] = url_param.spec();
std::string html =
FormatTemplate(IDR_NEW_TAB_PAGE_UNTRUSTED_IFRAME_HTML, replacements);
std::move(callback).Run(base::RefCountedString::TakeString(&html)); std::move(callback).Run(base::RefCountedString::TakeString(&html));
return; return;
} }
...@@ -149,7 +145,7 @@ bool UntrustedSource::ShouldServiceRequest( ...@@ -149,7 +145,7 @@ bool UntrustedSource::ShouldServiceRequest(
const std::string path = url.path().substr(1); const std::string path = url.path().substr(1);
return path == "one-google-bar" || path == "one_google_bar.js" || return path == "one-google-bar" || path == "one_google_bar.js" ||
path == "promo" || path == "promo.js" || path == "image" || path == "promo" || path == "promo.js" || path == "image" ||
path == "iframe"; path == "background_image" || path == "iframe";
} }
void UntrustedSource::OnOneGoogleBarDataUpdated() { void UntrustedSource::OnOneGoogleBarDataUpdated() {
......
...@@ -21,6 +21,17 @@ class Profile; ...@@ -21,6 +21,17 @@ class Profile;
// from outside the chromium codebase. The chrome-untrusted://new-tab-page/* // from outside the chromium codebase. The chrome-untrusted://new-tab-page/*
// sources can only be embedded in the chrome://new-tab-page by using an // sources can only be embedded in the chrome://new-tab-page by using an
// <iframe>. // <iframe>.
//
// Offers the following helpers to embed content into chrome://new-tab-page in a
// generalized way:
// * chrome-untrusted://new-tab-page/image?<url>: Behaves like an img element
// with src set to <url>.
// * chrome-untrusted://new-tab-page/background_image?<url>: Behaves like an
// element that has <url> set as the background image, such that the image
// will cover the entire element.
// * chrome-untrusted://new-tab-page/iframe?<url>: Behaves like an iframe with
// src set to <url>.
// Each of those helpers only accept HTTPS URLs.
class UntrustedSource : public content::URLDataSource, class UntrustedSource : public content::URLDataSource,
public OneGoogleBarServiceObserver, public OneGoogleBarServiceObserver,
public PromoServiceObserver { public PromoServiceObserver {
......
...@@ -129,7 +129,8 @@ suite('NewTabPageAppTest', () => { ...@@ -129,7 +129,8 @@ suite('NewTabPageAppTest', () => {
assertNotStyle(app.$.backgroundImage, 'display', 'none'); assertNotStyle(app.$.backgroundImage, 'display', 'none');
assertNotStyle(app.$.backgroundGradient, 'display', 'none'); assertNotStyle(app.$.backgroundGradient, 'display', 'none');
assertNotStyle(app.$.backgroundImageAttribution, 'text-shadow', 'none'); assertNotStyle(app.$.backgroundImageAttribution, 'text-shadow', 'none');
assertEquals(app.$.backgroundImage.path, 'image?https://img.png'); assertEquals(
app.$.backgroundImage.path, 'background_image?https://img.png');
assertFalse(app.$.logo.doodleAllowed); assertFalse(app.$.logo.doodleAllowed);
}); });
......
...@@ -57,7 +57,8 @@ suite('NewTabPageCustomizeBackgroundsTest', () => { ...@@ -57,7 +57,8 @@ suite('NewTabPageCustomizeBackgroundsTest', () => {
assertEquals(tiles.length, 1); assertEquals(tiles.length, 1);
assertEquals(tiles[0].getAttribute('title'), 'col_0'); assertEquals(tiles[0].getAttribute('title'), 'col_0');
assertEquals( assertEquals(
tiles[0].querySelector('.image').path, 'image?https://col_0.jpg'); tiles[0].querySelector('.image').path,
'background_image?https://col_0.jpg');
}); });
test('clicking collection selects collection', async function() { test('clicking collection selects collection', async function() {
...@@ -112,7 +113,7 @@ suite('NewTabPageCustomizeBackgroundsTest', () => { ...@@ -112,7 +113,7 @@ suite('NewTabPageCustomizeBackgroundsTest', () => {
assertEquals(tiles.length, 1); assertEquals(tiles.length, 1);
assertEquals( assertEquals(
tiles[0].querySelector('.image').path, tiles[0].querySelector('.image').path,
'image?https://example.com/image.png'); 'background_image?https://example.com/image.png');
}); });
test('Going back shows collections', async function() { test('Going back shows collections', async function() {
......
...@@ -32,15 +32,37 @@ suite('NewTabPageLogoTest', () => { ...@@ -32,15 +32,37 @@ suite('NewTabPageLogoTest', () => {
BrowserProxy.instance_ = testProxy; BrowserProxy.instance_ = testProxy;
}); });
test('setting static, animated doodle shows image', async () => { test('setting simple doodle shows image', async () => {
// Act. // Act.
const logo = await createLogo({content: {image: 'data:foo'}}); const logo = await createLogo(
{content: {imageDoodle: {imageUrl: {url: 'data:foo'}}}});
// Assert. // Assert.
assertNotStyle(logo.$.doodle, 'display', 'none'); assertNotStyle(logo.$.doodle, 'display', 'none');
assertStyle(logo.$.logo, 'display', 'none'); assertStyle(logo.$.logo, 'display', 'none');
assertEquals(logo.$.image.src, 'data:foo'); assertEquals(logo.$.image.src, 'data:foo');
assertNotStyle(logo.$.image, 'display', 'none'); assertNotStyle(logo.$.image, 'display', 'none');
assertStyle(logo.$.animation, 'display', 'none');
assertStyle(logo.$.iframe, 'display', 'none');
});
test('setting animated doodle shows image', async () => {
// Act.
const logo = await createLogo({
content: {
imageDoodle: {
imageUrl: {url: 'data:foo'},
animationUrl: {url: 'https://foo.com'},
}
}
});
// Assert.
assertNotStyle(logo.$.doodle, 'display', 'none');
assertStyle(logo.$.logo, 'display', 'none');
assertEquals(logo.$.image.src, 'data:foo');
assertNotStyle(logo.$.image, 'display', 'none');
assertStyle(logo.$.animation, 'display', 'none');
assertStyle(logo.$.iframe, 'display', 'none'); assertStyle(logo.$.iframe, 'display', 'none');
}); });
...@@ -53,7 +75,7 @@ suite('NewTabPageLogoTest', () => { ...@@ -53,7 +75,7 @@ suite('NewTabPageLogoTest', () => {
assertStyle(logo.$.logo, 'display', 'none'); assertStyle(logo.$.logo, 'display', 'none');
assertEquals(logo.$.iframe.path, 'iframe?https://foo.com'); assertEquals(logo.$.iframe.path, 'iframe?https://foo.com');
assertNotStyle(logo.$.iframe, 'display', 'none'); assertNotStyle(logo.$.iframe, 'display', 'none');
assertStyle(logo.$.image, 'display', 'none'); assertStyle(logo.$.imageContainer, 'display', 'none');
}); });
test('disallowing doodle shows logo', async () => { test('disallowing doodle shows logo', async () => {
...@@ -160,4 +182,65 @@ suite('NewTabPageLogoTest', () => { ...@@ -160,4 +182,65 @@ suite('NewTabPageLogoTest', () => {
assertEquals(logo.$.iframe.offsetHeight, height); assertEquals(logo.$.iframe.offsetHeight, height);
assertEquals(logo.$.iframe.offsetWidth, width); assertEquals(logo.$.iframe.offsetWidth, width);
}); });
test('clicking simple doodle opens link', async () => {
// Arrange.
const logo = await createLogo({
content: {
imageDoodle: {
imageUrl: {url: 'data:foo'},
onClickUrl: {url: 'https://foo.com'},
}
}
});
// Act.
logo.$.image.click();
const url = await testProxy.whenCalled('open');
// Assert.
assertEquals(url, 'https://foo.com');
});
test('clicking image of animated doodle starts animation', async () => {
// Arrange.
const logo = await createLogo({
content: {
imageDoodle: {
imageUrl: {url: 'data:foo'},
animationUrl: {url: 'https://foo.com'},
}
}
});
// Act.
logo.$.image.click();
// Assert.
assertEquals(testProxy.getCallCount('open'), 0);
assertNotStyle(logo.$.image, 'display', 'none');
assertNotStyle(logo.$.animation, 'display', 'none');
assertEquals(logo.$.animation.path, 'image?https://foo.com');
});
test('clicking animation of animated doodle opens link', async () => {
// Arrange.
const logo = await createLogo({
content: {
imageDoodle: {
imageUrl: {url: 'data:foo'},
animationUrl: {url: 'https://foo.com'},
onClickUrl: {url: 'https://bar.com'},
}
}
});
logo.$.image.click();
// Act.
logo.$.animation.click();
const url = await testProxy.whenCalled('open');
// Assert.
assertEquals(url, 'https://bar.com');
});
}); });
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment