Commit 749a93ee authored by Tibor Goldschwendt's avatar Tibor Goldschwendt Committed by Commit Bot

[webui][ntp] Add background image previews to customize dialog

This includes adding title navigation to the customize dialog to switch
between collections and images.

NOTE: This does not embed actual images. Will do this in a follow up CL.

Bug: 1032328
Change-Id: I3f9931aca673590dcd05dd4e4fa6dcf046aee3f1
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2076554
Commit-Queue: Tibor Goldschwendt <tiborg@chromium.org>
Reviewed-by: default avatarEsmael Elmoslimany <aee@chromium.org>
Reviewed-by: default avatarAlex Gough <ajgo@chromium.org>
Cr-Commit-Position: refs/heads/master@{#746634}
parent f836de5d
<style>
#collections {
--ntp-grid-gap: 8px;
#container {
padding: 4px;
}
ntp-grid {
--ntp-grid-gap: 8px;
}
.tile {
cursor: pointer;
outline-width: 0;
......@@ -36,10 +39,11 @@
min-height: 30px;
}
</style>
<ntp-grid id="collections" columns="3">
<dom-repeat items="[[collections_]]">
<ntp-grid id="collections" columns="3" hidden="[[selectedCollection]]">
<dom-repeat id="collectionsRepeat" items="[[collections_]]">
<template>
<div class="tile" tabindex="0" title="[[item.label]]" role="button">
<div class="tile" tabindex="0" title="[[item.label]]" role="button"
on-click="onCollectionClick_">
<!-- TODO(crbug.com/1032328): Show image in an iframe. -->
<div class="image">[[item.previewImageUrl.url]]</div>
<div class="label">[[item.label]]</div>
......@@ -47,3 +51,13 @@
</template>
</dom-repeat>
</ntp-grid>
<ntp-grid id="images" columns="3" hidden="[[!selectedCollection]]">
<dom-repeat items="[[images_]]">
<template>
<div class="tile" tabindex="0" title="[[item.label]]" role="button">
<!-- TODO(crbug.com/1032328): Show image in an iframe. -->
<div class="image">[[item.previewImageUrl.url]]</div>
</div>
</template>
</dom-repeat>
</ntp-grid>
......@@ -19,8 +19,19 @@ class CustomizeBackgroundsElement extends PolymerElement {
static get properties() {
return {
/** @private {newTabPage.mojom.BackgroundCollection} */
selectedCollection: {
notify: true,
observer: 'onSelectedCollectionChange_',
type: Object,
value: null,
},
/** @private {!Array<!newTabPage.mojom.BackgroundCollection>} */
collections_: Array,
/** @private {!Array<!newTabPage.mojom.BackgroundImage>} */
images_: Array,
};
}
......@@ -31,6 +42,33 @@ class CustomizeBackgroundsElement extends PolymerElement {
this.collections_ = collections;
});
}
/**
* @param {!Event} e
* @private
*/
onCollectionClick_(e) {
this.selectedCollection = this.$.collectionsRepeat.itemForElement(e.target);
}
/** @private */
async onSelectedCollectionChange_() {
this.images_ = [];
if (!this.selectedCollection) {
return;
}
const collectionId = this.selectedCollection.id;
const {images} =
await BrowserProxy.getInstance().handler.getBackgroundImages(
collectionId);
// We check the IDs match since the user may have already moved to a
// different collection before the results come back.
if (!this.selectedCollection ||
this.selectedCollection.id !== collectionId) {
return;
}
this.images_ = images;
}
}
customElements.define(
......
<style>
<style include="cr-icons">
::part(dialog) {
min-width: 800px;
}
......@@ -9,6 +9,7 @@
div[slot=title] {
align-items: center;
color: var(--ntp-primary-text-color);
display: flex;
flex-direction: row;
height: 80px;
......@@ -120,15 +121,29 @@
#themesIcon {
-webkit-mask-image: url(icons/colors.svg);
}
#backButton {
--cr-icon-button-fill-color: var(--ntp-primary-text-color);
margin-inline-end: 4px;
/* So that the arrow aligns with the grid. */
margin-inline-start: -12px;
}
</style>
<cr-dialog id="dialog" show-on-attach>
<div slot="title">
<div id="leftTitleSpacer"></div>
<div id="title">$i18n{customizeThisPage}</div>
<div id="title">
<div id="titleText" hidden="[[showTitleNavigation_]]">
$i18n{customizeThisPage}
</div>
<div id="titleNavigation" hidden="[[!showTitleNavigation_]]">
<cr-icon-button id="backButton" class="icon-arrow-back"
on-click="onBackClick_" title="$i18n{backButton}">
</cr-icon-button>
[[selectedCollection_.label]]
</div>
</div>
</div>
<!-- TODO(crbug.com/1040256): Currently, the sidebar scrolls in sync with the
page content area. Fix, so that the page content can scroll
separately. -->
<div slot="body">
<div id="menuContainer">
<div id="menu">
......@@ -153,7 +168,8 @@
<div id="pagesContainer">
<div id="pages">
<iron-pages selected="[[selectedPage_]]" attr-for-selected="page-name">
<ntp-customize-backgrounds page-name="backgrounds">
<ntp-customize-backgrounds id="backgrounds" page-name="backgrounds"
selected-collection="{{selectedCollection_}}">
</ntp-customize-backgrounds>
<ntp-customize-shortcuts page-name="shortcuts">
</ntp-customize-shortcuts>
......
......@@ -39,6 +39,17 @@ class CustomizeDialogElement extends PolymerElement {
value: 'backgrounds',
observer: 'onSelectedPageChange_',
},
/** @private {newTabPage.mojom.BackgroundCollection} */
selectedCollection_: Object,
/** @private */
showTitleNavigation_: {
type: Boolean,
computed:
'computeShowTitleNavigation_(selectedPage_, selectedCollection_)',
value: false,
},
};
}
......@@ -98,6 +109,16 @@ class CustomizeDialogElement extends PolymerElement {
onSelectedPageChange_() {
this.$.pages.scrollTop = 0;
}
/** @private */
computeShowTitleNavigation_() {
return this.selectedPage_ === 'backgrounds' && this.selectedCollection_;
}
/** @private */
onBackClick_() {
this.selectedCollection_ = null;
}
}
customElements.define(CustomizeDialogElement.is, CustomizeDialogElement);
<style>
:host {
--ntp-grid-gap: 0px;
display: block;
}
#grid {
......
......@@ -29,12 +29,22 @@ struct ThemeColors {
// A collection of background images.
struct BackgroundCollection {
// Collection identifier.
string id;
// Localized string of the collection name.
string label;
// URL to a preview image for the collection. Can point to untrusted content.
url.mojom.Url preview_image_url;
};
// A background image in a collection.
struct BackgroundImage {
// Localized string of extra image info.
string label;
// URL to a preview of the image. Can point to untrusted content.
url.mojom.Url preview_image_url;
};
// A predefined theme provided by Chrome. Created from data embedded in the
// Chrome binary.
struct ChromeTheme {
......@@ -127,6 +137,8 @@ interface PageHandler {
RevertThemeChanges();
// Returns the collections of background images.
GetBackgroundCollections() => (array<BackgroundCollection> collections);
// Returns the images of a collection identified by |collection_id|.
GetBackgroundImages(string collection_id) => (array<BackgroundImage> images);
};
// WebUI-side handler for requests from the browser.
......
......@@ -204,7 +204,7 @@ void NewTabPageHandler::UpdateMostVisitedTile(
void NewTabPageHandler::GetBackgroundCollections(
GetBackgroundCollectionsCallback callback) {
if (!ntp_background_service_) {
if (!ntp_background_service_ || background_collections_callback_) {
std::move(callback).Run(
std::vector<new_tab_page::mojom::BackgroundCollectionPtr>());
return;
......@@ -213,6 +213,23 @@ void NewTabPageHandler::GetBackgroundCollections(
ntp_background_service_->FetchCollectionInfo();
}
void NewTabPageHandler::GetBackgroundImages(
const std::string& collection_id,
GetBackgroundImagesCallback callback) {
if (background_images_callback_) {
std::move(background_images_callback_)
.Run(std::vector<new_tab_page::mojom::BackgroundImagePtr>());
}
if (!ntp_background_service_) {
std::move(callback).Run(
std::vector<new_tab_page::mojom::BackgroundImagePtr>());
return;
}
images_request_collection_id_ = collection_id;
background_images_callback_ = std::move(callback);
ntp_background_service_->FetchCollectionImageInfo(collection_id);
}
void NewTabPageHandler::NtpThemeChanged(const NtpTheme& ntp_theme) {
page_->SetTheme(MakeTheme(ntp_theme));
}
......@@ -248,6 +265,7 @@ void NewTabPageHandler::OnCollectionInfoAvailable() {
std::vector<new_tab_page::mojom::BackgroundCollectionPtr> collections;
for (const auto& info : ntp_background_service_->collection_info()) {
auto collection = new_tab_page::mojom::BackgroundCollection::New();
collection->id = info.collection_id;
collection->label = info.collection_name;
collection->preview_image_url = GURL(info.preview_image_url);
collections.push_back(std::move(collection));
......@@ -255,7 +273,25 @@ void NewTabPageHandler::OnCollectionInfoAvailable() {
std::move(background_collections_callback_).Run(std::move(collections));
}
void NewTabPageHandler::OnCollectionImagesAvailable() {}
void NewTabPageHandler::OnCollectionImagesAvailable() {
if (!background_images_callback_) {
return;
}
std::vector<new_tab_page::mojom::BackgroundImagePtr> images;
if (ntp_background_service_->collection_images().empty()) {
std::move(background_images_callback_).Run(std::move(images));
}
auto collection_id =
ntp_background_service_->collection_images()[0].collection_id;
for (const auto& info : ntp_background_service_->collection_images()) {
DCHECK(info.collection_id == collection_id);
auto image = new_tab_page::mojom::BackgroundImage::New();
image->preview_image_url = GURL(info.thumbnail_image_url);
image->label = !info.attribution.empty() ? info.attribution[0] : "";
images.push_back(std::move(image));
}
std::move(background_images_callback_).Run(std::move(images));
}
void NewTabPageHandler::OnNextCollectionImageAvailable() {}
......
......@@ -59,6 +59,8 @@ class NewTabPageHandler : public content::WebContentsObserver,
void RevertThemeChanges() override;
void GetBackgroundCollections(
GetBackgroundCollectionsCallback callback) override;
void GetBackgroundImages(const std::string& collection_id,
GetBackgroundImagesCallback callback) override;
private:
// InstantServiceObserver:
......@@ -76,6 +78,8 @@ class NewTabPageHandler : public content::WebContentsObserver,
NtpBackgroundService* ntp_background_service_;
GURL last_blacklisted_;
GetBackgroundCollectionsCallback background_collections_callback_;
std::string images_request_collection_id_;
GetBackgroundImagesCallback background_images_callback_;
mojo::Remote<new_tab_page::mojom::Page> page_;
mojo::Receiver<new_tab_page::mojom::PageHandler> receiver_;
......
......@@ -73,6 +73,7 @@ content::WebUIDataSource* CreateNewTabPageUiHtmlSource(Profile* profile) {
{"urlField", IDS_NTP_CUSTOM_LINKS_URL},
// Customize button and dialog.
{"backButton", IDS_ACCNAME_BACK},
{"backgroundsMenuItem", IDS_NTP_CUSTOMIZE_MENU_BACKGROUND_LABEL},
{"cancelButton", IDS_CANCEL},
{"colorPickerLabel", IDS_NTP_CUSTOMIZE_COLOR_PICKER_LABEL},
......@@ -80,17 +81,17 @@ content::WebUIDataSource* CreateNewTabPageUiHtmlSource(Profile* profile) {
{"customizeThisPage", IDS_NTP_CUSTOM_BG_CUSTOMIZE_NTP_LABEL},
{"defaultThemeLabel", IDS_NTP_CUSTOMIZE_DEFAULT_LABEL},
{"doneButton", IDS_DONE},
{"shortcutsMenuItem", IDS_NTP_CUSTOMIZE_MENU_SHORTCUTS_LABEL},
{"themesMenuItem", IDS_NTP_CUSTOMIZE_MENU_COLOR_LABEL},
{"thirdPartyThemeDescription", IDS_NTP_CUSTOMIZE_3PT_THEME_DESC},
{"uninstallThirdPartyThemeButton", IDS_NTP_CUSTOMIZE_3PT_THEME_UNINSTALL},
{"hideShortcuts", IDS_NTP_CUSTOMIZE_HIDE_SHORTCUTS_LABEL},
{"hideShortcutsDesc", IDS_NTP_CUSTOMIZE_HIDE_SHORTCUTS_DESC},
{"mostVisited", IDS_NTP_CUSTOMIZE_MOST_VISITED_LABEL},
{"myShortcuts", IDS_NTP_CUSTOMIZE_MY_SHORTCUTS_LABEL},
{"shortcutsCurated", IDS_NTP_CUSTOMIZE_MY_SHORTCUTS_DESC},
{"shortcutsMenuItem", IDS_NTP_CUSTOMIZE_MENU_SHORTCUTS_LABEL},
{"shortcutsOption", IDS_NTP_CUSTOMIZE_MENU_SHORTCUTS_LABEL},
{"shortcutsSuggested", IDS_NTP_CUSTOMIZE_MOST_VISITED_DESC},
{"themesMenuItem", IDS_NTP_CUSTOMIZE_MENU_COLOR_LABEL},
{"thirdPartyThemeDescription", IDS_NTP_CUSTOMIZE_3PT_THEME_DESC},
{"uninstallThirdPartyThemeButton", IDS_NTP_CUSTOMIZE_3PT_THEME_UNINSTALL},
// Voice search.
{"audioError", IDS_NEW_TAB_VOICE_AUDIO_ERROR},
......
......@@ -5,55 +5,136 @@
import 'chrome://new-tab-page/customize_backgrounds.js';
import {BrowserProxy} from 'chrome://new-tab-page/browser_proxy.js';
import {createTestProxy} from 'chrome://test/new_tab_page/test_support.js';
import {flushTasks} from 'chrome://test/test_util.m.js';
import {assertNotStyle, assertStyle, createTestProxy} from 'chrome://test/new_tab_page/test_support.js';
import {flushTasks, isVisible} from 'chrome://test/test_util.m.js';
function createCollection(id = 0, label = '', url = '') {
return {id: id, label: label, previewImageUrl: {url: url}};
}
suite('NewTabPageCustomizeBackgroundsTest', () => {
/** @type {TestProxy} */
let testProxy;
/** @type {newTabPage.mojom.PageHandlerRemote} */
let handler;
async function createCustomizeBackgrounds() {
const customizeBackgrounds =
document.createElement('ntp-customize-backgrounds');
document.body.appendChild(customizeBackgrounds);
await handler.whenCalled('getBackgroundCollections');
await flushTasks();
return customizeBackgrounds;
}
setup(() => {
PolymerTest.clearBody();
testProxy = createTestProxy();
const testProxy = createTestProxy();
handler = testProxy.handler;
handler.setResultFor('getBackgroundCollections', Promise.resolve({
collections: [],
}));
handler.setResultFor('getBackgroundImages', Promise.resolve({
images: [],
}));
BrowserProxy.instance_ = testProxy;
});
test('creating element shows background collection tiles', async () => {
// Arrange.
const collections = [
{
label: 'collection_0',
previewImageUrl: {url: 'https://example.com/image_0.jpg'},
},
{
label: 'collection_1',
previewImageUrl: {url: 'https://example.com/image_1.jpg'},
},
];
const getBackgroundCollectionsCalled =
testProxy.handler.whenCalled('getBackgroundCollections');
testProxy.handler.setResultFor('getBackgroundCollections', Promise.resolve({
collections: collections,
const collection = createCollection(0, 'col_0', 'https://col_0.jpg');
handler.setResultFor('getBackgroundCollections', Promise.resolve({
collections: [collection],
}));
// Act.
const customizeBackgrounds =
document.createElement('ntp-customize-backgrounds');
document.body.appendChild(customizeBackgrounds);
await getBackgroundCollectionsCalled;
await flushTasks();
const customizeBackgrounds = await createCustomizeBackgrounds();
// Assert.
const tiles = customizeBackgrounds.shadowRoot.querySelectorAll('.tile');
assertEquals(tiles.length, 2);
assertEquals(tiles[0].getAttribute('title'), 'collection_0');
assertEquals(tiles[1].getAttribute('title'), 'collection_1');
assertTrue(isVisible(customizeBackgrounds.$.collections));
assertStyle(customizeBackgrounds.$.images, 'display', 'none');
const tiles =
customizeBackgrounds.shadowRoot.querySelectorAll('#collections .tile');
assertEquals(tiles.length, 1);
assertEquals(tiles[0].getAttribute('title'), 'col_0');
assertEquals(
tiles[0].querySelector('.image').textContent.trim(),
'https://example.com/image_0.jpg');
'https://col_0.jpg');
});
test('clicking collection selects collection', async function() {
// Arrange.
const collection = createCollection();
handler.setResultFor('getBackgroundCollections', Promise.resolve({
collections: [collection],
}));
const customizeBackgrounds = await createCustomizeBackgrounds();
// Act.
customizeBackgrounds.shadowRoot.querySelector('#collections .tile').click();
// Assert.
assertDeepEquals(customizeBackgrounds.selectedCollection, collection);
});
test('setting collection requests images', async function() {
// Arrange.
const customizeBackgrounds = await createCustomizeBackgrounds();
// Act.
customizeBackgrounds.selectedCollection = createCollection();
// Assert.
assertFalse(isVisible(customizeBackgrounds.$.collections));
await handler.whenCalled('getBackgroundImages');
});
test('Loading images shows image tiles', async function() {
// Arrange.
const image = {
label: 'image_0',
previewImageUrl: {url: 'https://example.com/image.png'},
};
handler.setResultFor('getBackgroundImages', Promise.resolve({
images: [image],
}));
const customizeBackgrounds = await createCustomizeBackgrounds();
customizeBackgrounds.selectedCollection = createCollection(0);
// Act.
const id = await handler.whenCalled('getBackgroundImages');
await flushTasks();
// Assert.
assertEquals(id, 0);
assertFalse(isVisible(customizeBackgrounds.$.collections));
assertTrue(isVisible(customizeBackgrounds.$.images));
const tiles =
customizeBackgrounds.shadowRoot.querySelectorAll('#images .tile');
assertEquals(tiles.length, 1);
assertEquals(
tiles[1].querySelector('.image').textContent.trim(),
'https://example.com/image_1.jpg');
tiles[0].querySelector('.image').textContent.trim(),
'https://example.com/image.png');
});
test('Going back shows collections', async function() {
// Arrange.
const image = {
label: 'image_0',
previewImageUrl: {url: 'https://example.com/image.png'},
};
const customizeBackgrounds = await createCustomizeBackgrounds();
handler.setResultFor('getBackgroundImages', Promise.resolve({
images: [image],
}));
customizeBackgrounds.selectedCollection = createCollection();
await flushTasks();
// Act.
customizeBackgrounds.selectedCollection = null;
await flushTasks();
// Assert.
assertNotStyle(customizeBackgrounds.$.collections, 'display', 'none');
assertStyle(customizeBackgrounds.$.images, 'display', 'none');
});
});
......@@ -6,7 +6,7 @@ import 'chrome://new-tab-page/customize_dialog.js';
import {BrowserProxy} from 'chrome://new-tab-page/browser_proxy.js';
import {createTestProxy} from 'chrome://test/new_tab_page/test_support.js';
import {flushTasks, waitAfterNextRender} from 'chrome://test/test_util.m.js';
import {flushTasks, isVisible, waitAfterNextRender} from 'chrome://test/test_util.m.js';
suite('NewTabPageCustomizeDialogTest', () => {
/** @type {!CustomizeDialogElement} */
......@@ -53,4 +53,46 @@ suite('NewTabPageCustomizeDialogTest', () => {
assertEquals(shownPages.length, 1);
assertEquals(shownPages[0].getAttribute('page-name'), 'themes');
});
suite('scroll borders', () => {
/**
* @param {!HTMLElement} container
* @private
*/
async function testScrollBorders(container) {
const assertHidden = el => {
assertTrue(el.matches('[scroll-border]:not([show])'));
};
const assertShown = el => {
assertTrue(el.matches('[scroll-border][show]'));
};
const {firstElementChild: top, lastElementChild: bottom} = container;
const scrollableElement = top.nextSibling;
const dialogBody =
customizeDialog.shadowRoot.querySelector('div[slot=body]');
const heightWithBorders = `${scrollableElement.scrollHeight + 2}px`;
dialogBody.style.height = heightWithBorders;
assertHidden(top);
assertHidden(bottom);
dialogBody.style.height = '50px';
await waitAfterNextRender();
assertHidden(top);
assertShown(bottom);
scrollableElement.scrollTop = 1;
await waitAfterNextRender();
assertShown(top);
assertShown(bottom);
scrollableElement.scrollTop = scrollableElement.scrollHeight;
await waitAfterNextRender();
assertShown(top);
assertHidden(bottom);
dialogBody.style.height = heightWithBorders;
await waitAfterNextRender();
assertHidden(top);
assertHidden(bottom);
}
test('menu', () => testScrollBorders(customizeDialog.$.menuContainer));
test('pages', () => testScrollBorders(customizeDialog.$.pagesContainer));
});
});
......@@ -32,6 +32,17 @@ export function assertStyle(element, name, expected) {
assertEquals(expected, actual);
}
/**
* Asserts the computed style for an element is not value.
* @param {!HTMLElement} element The element.
* @param {string} name The name of the style to assert.
* @param {string} not The value the style should not be.
*/
export function assertNotStyle(element, name, not) {
const actual = window.getComputedStyle(element).getPropertyValue(name).trim();
assertNotEquals(not, actual);
}
/**
* Asserts that an element is focused.
* @param {!HTMLElement} element The element to test.
......
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