Commit cf5a2fb4 authored by Tibor Goldschwendt's avatar Tibor Goldschwendt Committed by Commit Bot

[ntp][modules] Add generic custom module element

The generic module element implements common UI such as the header and
border. The module supplies the information required to populate the
common UI via its module descriptor. The generic module element also
wraps the local module UI previously instantiated.

Bug: 1110047
Change-Id: I4c2579bb2c0d3e19b9120c03606ad21d30dea529
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2336912
Commit-Queue: Tibor Goldschwendt <tiborg@chromium.org>
Reviewed-by: default avatarMoe Ahmadi <mahmadi@chromium.org>
Cr-Commit-Position: refs/heads/master@{#798161}
parent 21b49e10
......@@ -5613,6 +5613,24 @@ Keep your key file in a safe place. You will need it to create new versions of y
<message name="IDS_NTP_COLORS_DARK_PURPLE" desc="A color option in the customization menu on the New Tab Page.">
Dark purple
</message>
<message name="IDS_NTP_MODULES_DUMMY_NAME" translateable="false" desc="Name shown in the header of the dummy module.">
Dummy
</message>
<message name="IDS_NTP_MODULES_DUMMY_TITLE" translateable="false" desc="Title shown in the header of the dummy module.">
Super Duper Module
</message>
<message name="IDS_NTP_MODULES_DUMMY2_NAME" translateable="false" desc="Name shown in the header of the dummy 2 module.">
Dummy 2
</message>
<message name="IDS_NTP_MODULES_DUMMY2_TITLE" translateable="false" desc="Title shown in the header of the dummy 2 module.">
Even Better Module
</message>
<message name="IDS_NTP_MODULES_KALEIDOSCOPE_NAME" translateable="false" desc="Name shown in the header of the Kaleidoscope module.">
Kaleidoscope
</message>
<message name="IDS_NTP_MODULES_KALEIDOSCOPE_TITLE" desc="Title shown in the header of the Kaleidoscope module.">
Top picks for you
</message>
<!-- Extensions NTP Middle Slot Promo -->
<message name="IDS_EXTENSIONS_PROMO_PERFORMANCE">
......
615a1d35e012617c4de8c15c64087a0a0e010320
\ No newline at end of file
......@@ -49,6 +49,7 @@ js_library("app") {
deps = [
":background_manager",
":browser_proxy",
":module_wrapper",
":modules",
":most_visited",
":one_google_bar_api",
......@@ -240,9 +241,18 @@ js_library("modules") {
]
}
js_library("module_wrapper") {
deps = [
"//third_party/polymer/v3_0/components-chromium/polymer:polymer_bundled",
]
}
js_library("module_registry") {
sources = [ "modules/module_registry.js" ]
deps = [ ":module_descriptor" ]
deps = [
":module_descriptor",
"//ui/webui/resources/js:cr.m",
]
}
html_to_js("web_components_local") {
......@@ -266,6 +276,7 @@ html_to_js("web_components_local") {
"theme_icon.js",
"iframe.js",
"voice_search_overlay.js",
"module_wrapper.js",
]
}
......
......@@ -4,6 +4,19 @@
--ntp-theme-text-color: var(--google-grey-800);
--ntp-theme-text-shadow: none;
--ntp-one-google-bar-height: 56px;
--ntp-search-box-width: 337px;
}
@media (min-width: 560px) {
:host {
--ntp-search-box-width: 449px;
}
}
@media (min-width: 672px) {
:host {
--ntp-search-box-width: 561px;
}
}
@media (prefers-color-scheme: dark) {
......@@ -61,6 +74,12 @@
margin-bottom: 32px;
}
ntp-fakebox,
ntp-realbox,
ntp-module-wrapper {
width: var(--ntp-search-box-width);
}
ntp-realbox {
visibility: hidden;
}
......@@ -84,6 +103,10 @@
width: 100%;
}
ntp-module-wrapper + ntp-module-wrapper {
margin-top: 24px;
}
#customizeButtonSpacer {
flex-grow: 1;
}
......@@ -260,7 +283,9 @@
<ntp-iframe id="promo" hidden$="[[!promoLoaded_]]"
src="chrome-untrusted://new-tab-page/promo">
</ntp-iframe>
<div id="modules"></div>
<template is="dom-repeat" items="[[moduleDescriptors_]]" id="modules">
<ntp-module-wrapper descriptor="[[item]]"></ntp-module-wrapper>
</template>
<a id="backgroundImageAttribution"
href="[[backgroundImageAttributionUrl_]]"
hidden="[[!backgroundImageAttribution1_]]">
......
......@@ -10,6 +10,8 @@ import './iframe.js';
import './fakebox.js';
import './realbox.js';
import './logo.js';
import './module_wrapper.js';
import './modules/modules.js'; // Registers module descriptors.
import 'chrome://resources/cr_elements/cr_button/cr_button.m.js';
import 'chrome://resources/cr_elements/shared_style_css.m.js';
......@@ -22,7 +24,8 @@ import {html, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/poly
import {BackgroundManager} from './background_manager.js';
import {BrowserProxy} from './browser_proxy.js';
import {BackgroundSelection, BackgroundSelectionType} from './customize_dialog.js';
import {registry} from './modules/modules.js';
import {ModuleDescriptor} from './modules/module_descriptor.js';
import {ModuleRegistry} from './modules/module_registry.js';
import {oneGoogleBarApi} from './one_google_bar_api.js';
import {PromoBrowserCommandProxy} from './promo_browser_command_proxy.js';
import {$$, hexColorToSkColor, skColorToRgba} from './utils.js';
......@@ -198,6 +201,9 @@ class AppElement extends PolymerElement {
* @private
*/
lazyRender_: Boolean,
/** @private {!Array<!ModuleDescriptor>} */
moduleDescriptors_: Object,
};
}
......@@ -448,15 +454,12 @@ class AppElement extends PolymerElement {
}
/** @private */
onLazyRendered_() {
async onLazyRendered_() {
if (!loadTimeData.getBoolean('modulesEnabled')) {
return;
}
const container = $$(this, '#modules');
if (!container) {
return;
}
registry.instantiateModules(container);
this.moduleDescriptors_ =
await ModuleRegistry.getInstance().initializeModules();
}
/** @private */
......
......@@ -6,19 +6,6 @@
box-shadow: 0 1px 6px 0 rgba(32, 33, 36, .28);
height: var(--ntp-fakebox-height);
position: relative;
width: 337px;
}
@media (min-width: 560px) {
:host {
width: 449px;
}
}
@media (min-width: 672px) {
:host {
width: 561px;
}
}
:host([hidden_]) {
......
<style>
:host {
background-color: var(--ntp-background-override-color);
border: solid var(--ntp-border-color) 1px;
border-radius: 5px;
}
#header {
margin: 10px;
}
#title {
color: var(--cr-primary-text-color);
}
#name {
color: var(--cr-secondary-text-color);
}
#moduleElement {
align-items: center;
display: flex;
height: 150px;
justify-content: center;
overflow: hidden;
}
</style>
<div id="header">
<span id="title">[[descriptor.title]]</span>
<span id="name"> • [[descriptor.name]]</span>
</div>
<div id="moduleElement"></div>
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {html, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {ModuleDescriptor} from './modules/module_descriptor.js';
/** @fileoverview Element that implements the common module UI. */
class ModuleWrapperElement extends PolymerElement {
static get is() {
return 'ntp-module-wrapper';
}
static get template() {
return html`{__html_template__}`;
}
static get properties() {
return {
/** @type {!ModuleDescriptor} */
descriptor: {
observer: 'onDescriptorChange_',
type: Object,
},
};
}
/** @private */
onDescriptorChange_() {
this.$.moduleElement.innerHTML = '';
this.$.moduleElement.appendChild(this.descriptor.element);
}
}
customElements.define(ModuleWrapperElement.is, ModuleWrapperElement);
......@@ -4,6 +4,7 @@
import '../../grid.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js';
import {html, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {ModuleDescriptor} from '../module_descriptor.js';
......@@ -39,7 +40,16 @@ class DummyModuleElement extends PolymerElement {
customElements.define(DummyModuleElement.is, DummyModuleElement);
/** @type {!ModuleDescriptor} */
export const dummyDescriptor = {
id: 'dummy',
create: () => Promise.resolve(new DummyModuleElement()),
};
export const dummyDescriptor = new ModuleDescriptor(
'dummy', loadTimeData.getString('modulesDummyName'), () => Promise.resolve({
element: new DummyModuleElement(),
title: loadTimeData.getString('modulesDummyTitle'),
}));
/** @type {!ModuleDescriptor} */
export const dummyDescriptor2 = new ModuleDescriptor(
'dummy2', loadTimeData.getString('modulesDummy2Name'),
() => Promise.resolve({
element: new DummyModuleElement(),
title: loadTimeData.getString('modulesDummy2Title'),
}));
......@@ -4,6 +4,7 @@
import '../../grid.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js';
import {html, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {ModuleDescriptor} from '../module_descriptor.js';
......@@ -40,7 +41,9 @@ class KaleidoscopeModuleElement extends PolymerElement {
customElements.define(KaleidoscopeModuleElement.is, KaleidoscopeModuleElement);
/** @type {!ModuleDescriptor} */
export const kaleidoscopeDescriptor = {
id: 'kaleidoscope',
create: () => Promise.resolve(new KaleidoscopeModuleElement()),
};
export const kaleidoscopeDescriptor = new ModuleDescriptor(
'kaleidoscope', loadTimeData.getString('modulesKaleidoscopeName'),
() => Promise.resolve({
element: new KaleidoscopeModuleElement(),
title: loadTimeData.getString('modulesKaleidoscopeTitle'),
}));
......@@ -3,14 +3,54 @@
// found in the LICENSE file.
/**
* @fileoverview Provides the module descriptor type declaration. Each module
* must create a module descriptor and register it at the NTP.
* @fileoverview Provides the module descriptor. Each module must create a
* module descriptor and register it at the NTP.
*/
/**
* @typedef {{
* id: string,
* create: function(): !Promise<!HTMLElement>,
* }}
* @typedef {function(): !Promise<?{
* element: !HTMLElement,
* title: string,
* }>}
*/
export let ModuleDescriptor;
let InitializeModuleCallback;
export class ModuleDescriptor {
/**
* @param {string} id
* @param {string} name
* @param {!InitializeModuleCallback} initializeCallback
*/
constructor(id, name, initializeCallback) {
this.id_ = id;
this.name_ = name;
this.title_ = null;
this.element_ = null;
this.initializeCallback_ = initializeCallback;
}
get id() {
return this.id_;
}
get name() {
return this.name_;
}
get title() {
return this.title_;
}
get element() {
return this.element_;
}
async initialize() {
const info = await this.initializeCallback_();
if (!info) {
return;
}
this.title_ = info.title;
this.element_ = info.element;
}
}
......@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {addSingletonGetter} from 'chrome://resources/js/cr.m.js';
import {ModuleDescriptor} from './module_descriptor.js';
/**
......@@ -10,22 +12,29 @@ import {ModuleDescriptor} from './module_descriptor.js';
*/
export class ModuleRegistry {
/** @param {!Array<!ModuleDescriptor>} moduleDescriptors */
constructor(moduleDescriptors) {
constructor() {
/** @private {!Array<!ModuleDescriptor>} */
this.moduleDescriptors_ = moduleDescriptors;
this.descriptors_ = [];
}
/**
* Registers modules via their descriptors.
* @param {!Array<!ModuleDescriptor>} descriptors
*/
registerModules(descriptors) {
/** @type {!Array<!ModuleDescriptor>} */
this.descriptors_ = descriptors;
}
/**
* Instantiates modules and appends them to |container|.
* @param {!Element} container
* Initializes the modules previously set via |registerModules| and returns
* the initialized descriptors.
* @return {!Promise<!Array<!ModuleDescriptor>>}
*/
async instantiateModules(container) {
(await Promise.all(
this.moduleDescriptors_.map(descriptor => descriptor.create())))
.filter(module => !!module)
.forEach(element => {
container.appendChild(element);
});
async initializeModules() {
await Promise.all(this.descriptors_.map(d => d.initialize()));
return this.descriptors_.filter(descriptor => !!descriptor.element);
}
}
addSingletonGetter(ModuleRegistry);
......@@ -7,15 +7,17 @@
*/
import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js';
import {dummyDescriptor} from './dummy/module.js';
import {dummyDescriptor, dummyDescriptor2} from './dummy/module.js';
import {kaleidoscopeDescriptor} from './kaleidoscope/module.js';
import {ModuleDescriptor} from './module_descriptor.js';
import {ModuleRegistry} from './module_registry.js';
const descriptors = [dummyDescriptor];
/** @type {!Array<!ModuleDescriptor>} */
const descriptors = [dummyDescriptor, dummyDescriptor2];
if (loadTimeData.getBoolean('kaleidoscopeModuleEnabled')) {
descriptors.push(kaleidoscopeDescriptor);
}
/** @type {!ModuleRegistry} */
export const registry = new ModuleRegistry(descriptors);
ModuleRegistry.getInstance().registerModules(descriptors);
......@@ -17,6 +17,7 @@ export {BrowserProxy} from './browser_proxy.js';
export {BackgroundSelectionType} from './customize_dialog.js';
export {dummyDescriptor} from './modules/dummy/module.js';
export {kaleidoscopeDescriptor} from './modules/kaleidoscope/module.js';
export {ModuleDescriptor} from './modules/module_descriptor.js';
export {ModuleRegistry} from './modules/module_registry.js';
export {PromoBrowserCommandProxy} from './promo_browser_command_proxy.js';
export {$$, createScrollBorders, decodeString16, hexColorToSkColor, mojoString16, skColorToRgba} from './utils.js';
......@@ -72,6 +72,9 @@
<include name="IDR_NEW_TAB_PAGE_REALBOX_MATCH_JS"
file="${root_gen_dir}/chrome/browser/resources/new_tab_page/realbox_match.js"
use_base_dir="false" type="BINDATA" compress="false" />
<include name="IDR_NEW_TAB_PAGE_MODULE_WRAPPER_JS"
file="${root_gen_dir}/chrome/browser/resources/new_tab_page/module_wrapper.js"
use_base_dir="false" type="BINDATA" compress="false" />
<include name="IDR_NEW_TAB_PAGE_BROWSER_PROXY_JS"
file="browser_proxy.js" type="BINDATA" compress="false" />
<include name="IDR_NEW_TAB_PAGE_UTILS_JS"
......
......@@ -4,25 +4,12 @@
border-radius: calc(0.5 * var(--ntp-realbox-height));
box-shadow: 0 1px 6px 0 rgba(32, 33, 36, .28);
height: var(--ntp-realbox-height);
width: 337px;
}
:host([matches-are-visible]) {
box-shadow: none;
}
@media (min-width: 560px) {
:host {
width: 449px;
}
}
@media (min-width: 672px) {
:host {
width: 561px;
}
}
#inputWrapper {
height: 100%;
position: relative;
......@@ -30,8 +17,8 @@
input {
background-color: var(--search-box-bg, white);
border-radius: calc(0.5 * var(--ntp-realbox-height));
border: none;
border-radius: calc(0.5 * var(--ntp-realbox-height));
color: var(--search-box-text);
font-size: 16px;
height: 100%;
......
......@@ -174,6 +174,14 @@ content::WebUIDataSource* CreateNewTabPageUiHtmlSource(Profile* profile) {
// Theme.
{"themeCreatedBy", IDS_NEW_TAB_ATTRIBUTION_INTRO},
// Modules.
{"modulesDummyName", IDS_NTP_MODULES_DUMMY_NAME},
{"modulesDummyTitle", IDS_NTP_MODULES_DUMMY_TITLE},
{"modulesDummy2Name", IDS_NTP_MODULES_DUMMY2_NAME},
{"modulesDummy2Title", IDS_NTP_MODULES_DUMMY2_TITLE},
{"modulesKaleidoscopeName", IDS_NTP_MODULES_KALEIDOSCOPE_NAME},
{"modulesKaleidoscopeTitle", IDS_NTP_MODULES_KALEIDOSCOPE_TITLE},
};
AddLocalizedStringsBulk(source, kStrings);
......
......@@ -2,9 +2,10 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {$$, BackgroundManager, BackgroundSelectionType, BrowserProxy, PromoBrowserCommandProxy} from 'chrome://new-tab-page/new_tab_page.js';
import {$$, BackgroundManager, BackgroundSelectionType, BrowserProxy, ModuleRegistry, PromoBrowserCommandProxy} from 'chrome://new-tab-page/new_tab_page.js';
import {isMac} from 'chrome://resources/js/cr.m.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js';
import {PromiseResolver} from 'chrome://resources/js/promise_resolver.m.js';
import {assertNotStyle, assertStyle, createTestProxy, createTheme} from 'chrome://test/new_tab_page/test_support.js';
import {TestBrowserProxy} from 'chrome://test/test_browser_proxy.m.js';
import {eventToPromise, flushTasks} from 'chrome://test/test_util.m.js';
......@@ -25,6 +26,9 @@ suite('NewTabPageAppTest', () => {
*/
let backgroundManager;
/** @type {PromiseResolver} */
let moduleResolver;
suiteSetup(() => {
loadTimeData.overrideValues({
realboxEnabled: false,
......@@ -57,6 +61,10 @@ suite('NewTabPageAppTest', () => {
backgroundManager.setResultFor(
'getBackgroundImageLoadTime', Promise.resolve(0));
BackgroundManager.instance_ = backgroundManager;
const moduleRegistry = TestBrowserProxy.fromClass(ModuleRegistry);
moduleResolver = new PromiseResolver();
moduleRegistry.setResultFor('initializeModules', moduleResolver.promise);
ModuleRegistry.instance_ = moduleRegistry;
app = document.createElement('ntp-app');
document.body.appendChild(app);
......@@ -423,4 +431,36 @@ suite('NewTabPageAppTest', () => {
const {data: commandExecuted} = await eventToPromise('message', window);
assertTrue(commandExecuted);
});
suite('modules', () => {
suiteSetup(() => {
loadTimeData.overrideValues({
modulesEnabled: true,
});
});
test('modules appended to page', async () => {
// Act.
moduleResolver.resolve([
{
id: 'foo',
name: 'Foo',
element: document.createElement('div'),
title: 'Foo Title',
},
{
id: 'bar',
name: 'Bar',
element: document.createElement('div'),
title: 'Bar Title',
}
]);
await flushTasks(); // Wait for module descriptor resolution.
$$(app, '#modules').render();
// Assert.
const modules = app.shadowRoot.querySelectorAll('ntp-module-wrapper');
assertEquals(2, modules.length);
});
});
});
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {$$} from 'chrome://new-tab-page/new_tab_page.js';
suite('NewTabPageModuleWrapperTest', () => {
/** @type {!ModuleWrapperElement} */
let moduleWrapper;
setup(() => {
PolymerTest.clearBody();
moduleWrapper = document.createElement('ntp-module-wrapper');
document.body.appendChild(moduleWrapper);
});
test('renders module descriptor', async () => {
// Arrange.
const moduleElement = document.createElement('div');
// Act.
moduleWrapper.descriptor = {
id: 'foo',
name: 'Foo',
title: 'Foo Title',
element: moduleElement,
};
// Assert.
assertEquals('Foo Title', moduleWrapper.$.title.textContent);
assertEquals(' • Foo', moduleWrapper.$.name.textContent);
assertDeepEquals(
moduleElement, $$(moduleWrapper, '#moduleElement').children[0]);
});
});
......@@ -6,9 +6,14 @@ import {$$, dummyDescriptor} from 'chrome://new-tab-page/new_tab_page.js';
import {isVisible} from 'chrome://test/test_util.m.js';
suite('NewTabPageModulesDummyModuleTest', () => {
setup(() => {
PolymerTest.clearBody();
});
test('creates module', async () => {
// Act.
const module = await dummyDescriptor.create();
await dummyDescriptor.initialize();
const module = dummyDescriptor.element;
document.body.append(module);
module.$.tileList.render();
......
......@@ -8,7 +8,8 @@ import {isVisible} from 'chrome://test/test_util.m.js';
suite('NewTabPageModulesKaleidoscopeModuleTest', () => {
test('creates module', async () => {
// Act.
const module = await kaleidoscopeDescriptor.create();
await kaleidoscopeDescriptor.initialize();
const module = kaleidoscopeDescriptor.element;
document.body.append(module);
module.$.tileList.render();
......
......@@ -2,35 +2,42 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {ModuleRegistry} from 'chrome://new-tab-page/new_tab_page.js';
import {ModuleDescriptor, ModuleRegistry} from 'chrome://new-tab-page/new_tab_page.js';
import {PromiseResolver} from 'chrome://resources/js/promise_resolver.m.js';
suite('NewTabPageModulesModuleRegistryTest', () => {
test('instantiates modules', async () => {
// Arrange.
const fooModule = document.createElement('div');
const bazModule = document.createElement('div');
const registry = new ModuleRegistry([
{
id: 'foo',
create: () => Promise.resolve(fooModule),
},
{
id: 'bar',
create: () => null,
},
{
id: 'baz',
create: () => Promise.resolve(bazModule),
}
const bazModuleResolver = new PromiseResolver();
ModuleRegistry.getInstance().registerModules([
new ModuleDescriptor('foo', 'Foo', () => Promise.resolve({
element: fooModule,
title: 'Foo Title',
})),
new ModuleDescriptor('bar', 'Bar', () => null),
new ModuleDescriptor('baz', 'Baz', () => bazModuleResolver.promise),
]);
const container = document.createElement('div');
// Act.
await registry.instantiateModules(container);
const modulesPromise = ModuleRegistry.getInstance().initializeModules();
// Delayed promise resolution to test async module instantiation.
bazModuleResolver.resolve({
element: bazModule,
title: 'Baz Title',
});
const modules = await modulesPromise;
// Assert.
assertEquals(2, container.children.length);
assertDeepEquals(fooModule, container.children[0]);
assertDeepEquals(bazModule, container.children[1]);
assertEquals(2, modules.length);
assertEquals('foo', modules[0].id);
assertEquals('Foo', modules[0].name);
assertEquals('Foo Title', modules[0].title);
assertDeepEquals(fooModule, modules[0].element);
assertEquals('baz', modules[1].id);
assertEquals('Baz', modules[1].name);
assertEquals('Baz Title', modules[1].title);
assertDeepEquals(bazModule, modules[1].element);
});
});
......@@ -197,6 +197,18 @@ TEST_F('NewTabPageBackgroundManagerTest', 'All', function() {
mocha.run();
});
// eslint-disable-next-line no-var
var NewTabPageModuleWrapperTest = class extends NewTabPageBrowserTest {
/** @override */
get browsePreload() {
return 'chrome://new-tab-page/test_loader.html?module=new_tab_page/module_wrapper_test.js';
}
};
TEST_F('NewTabPageModuleWrapperTest', 'All', function() {
mocha.run();
});
// eslint-disable-next-line no-var
var NewTabPageModulesModuleRegistryTest = class extends NewTabPageBrowserTest {
/** @override */
......
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