Commit 2cf475dd authored by Tommy Steimel's avatar Tommy Steimel Committed by Chromium LUCI CQ

NTP: Allow modules to instantiate the header

This CL removes the module header from the wrapper and instead allows
the modules to instantiate it instead. This makes the title and actions
properties of the ModuleDescriptor obsolete, so this CL also removes
those.

Bug: 1152205, 1152216
Change-Id: Ib524a59c8c8a7cc69e130b31a8557b903233ab95
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2570390Reviewed-by: default avatarTibor Goldschwendt <tiborg@chromium.org>
Commit-Queue: Tommy Steimel <steimel@chromium.org>
Cr-Commit-Position: refs/heads/master@{#834424}
parent 716ba3cf
...@@ -349,7 +349,7 @@ ...@@ -349,7 +349,7 @@
<dom-if if="[[lazyRender_]]" restamp> <dom-if if="[[lazyRender_]]" restamp>
<template> <template>
<cr-toast id="dismissModuleToast" duration="10000"> <cr-toast id="dismissModuleToast" duration="10000">
<div id="dismissModuleToastMessage">[[dismissModuleToastMessage_]]</div> <div id="dismissModuleToastMessage">[[dismissedModuleData_.message]]</div>
<cr-button id="undoDismissModuleButton" <cr-button id="undoDismissModuleButton"
aria-label="$i18n{undoDescription}" aria-label="$i18n{undoDescription}"
on-click="onUndoDismissModuleButtonClick_"> on-click="onUndoDismissModuleButtonClick_">
......
...@@ -248,21 +248,15 @@ class AppElement extends PolymerElement { ...@@ -248,21 +248,15 @@ class AppElement extends PolymerElement {
moduleDescriptors_: Object, moduleDescriptors_: Object,
/** /**
* The <ntp-module-wrapper> element of the last dismissed module. * Data about the most recently dismissed module.
* @type {?Element} * @type {?{id: string, element: !Element, message: string,
* restoreCallback: function()}}
* @private * @private
*/ */
dismissedModuleWrapper_: { dismissedModuleData_: {
type: Object, type: Object,
value: null, value: null,
}, },
/**
* The message shown in the toast when a module is dismissed.
* @type {string}
* @private
*/
dismissModuleToastMessage_: String,
}; };
} }
...@@ -885,19 +879,25 @@ class AppElement extends PolymerElement { ...@@ -885,19 +879,25 @@ class AppElement extends PolymerElement {
} }
/** /**
* @param {!CustomEvent<string>} e Event notifying a module was dismissed. * @param {!CustomEvent<{message: string, restoreCallback: function()}>} e
* Contains the message to show in the toast. * Event notifying a module was dismissed. Contains the message to show in
* the toast.
* @private * @private
*/ */
onDismissModule_(e) { onDismissModule_(e) {
this.dismissedModuleWrapper_ = /** @type {!Element} */ (e.target); this.dismissedModuleData_ = {
id: $$(this, '#modules').itemForElement(e.target).id,
element: /** @type {!Element} */ (e.target),
message: loadTimeData.getStringF(
'dismissModuleToastMessage', e.detail.message),
restoreCallback: e.detail.restoreCallback,
};
this.dismissedModuleData_.element.hidden = true;
// Notify the user. // Notify the user.
this.dismissModuleToastMessage_ = e.detail;
$$(this, '#dismissModuleToast').show(); $$(this, '#dismissModuleToast').show();
// Notify the backend. // Notify the backend.
this.pageHandler_.onDismissModule( this.pageHandler_.onDismissModule(this.dismissedModuleData_.id);
this.dismissedModuleWrapper_.descriptor.id);
} }
/** /**
...@@ -905,14 +905,15 @@ class AppElement extends PolymerElement { ...@@ -905,14 +905,15 @@ class AppElement extends PolymerElement {
*/ */
onUndoDismissModuleButtonClick_() { onUndoDismissModuleButtonClick_() {
// Restore the module. // Restore the module.
this.dismissedModuleWrapper_.restore(); this.dismissedModuleData_.restoreCallback();
this.dismissedModuleData_.element.hidden = false;
// Notify the user. // Notify the user.
$$(this, '#dismissModuleToast').hide(); $$(this, '#dismissModuleToast').hide();
// Notify the backend. // Notify the backend.
this.pageHandler_.onRestoreModule( this.pageHandler_.onRestoreModule(this.dismissedModuleData_.id);
this.dismissedModuleWrapper_.descriptor.id);
this.dismissedModuleWrapper_ = null; this.dismissedModuleData_ = null;
} }
/** /**
......
...@@ -29,6 +29,12 @@ js_library("module_wrapper") { ...@@ -29,6 +29,12 @@ js_library("module_wrapper") {
] ]
} }
js_library("module_header") {
deps = [
"//third_party/polymer/v3_0/components-chromium/polymer:polymer_bundled",
]
}
js_library("module_registry") { js_library("module_registry") {
deps = [ deps = [
":module_descriptor", ":module_descriptor",
...@@ -37,7 +43,10 @@ js_library("module_registry") { ...@@ -37,7 +43,10 @@ js_library("module_registry") {
} }
html_to_js("web_components_local") { html_to_js("web_components_local") {
js_files = [ "module_wrapper.js" ] js_files = [
"module_header.js",
"module_wrapper.js",
]
} }
group("web_components") { group("web_components") {
...@@ -76,6 +85,7 @@ if (optimize_webui) { ...@@ -76,6 +85,7 @@ if (optimize_webui) {
in_folder = target_gen_dir in_folder = target_gen_dir
out_folder = "$target_gen_dir/$preprocess_folder" out_folder = "$target_gen_dir/$preprocess_folder"
in_files = [ in_files = [
"module_header.js",
"module_wrapper.js", "module_wrapper.js",
"dummy/module.js", "dummy/module.js",
"task_module/module.js", "task_module/module.js",
......
<style> <style>
:host {
width: 100%;
}
cr-grid { cr-grid {
--cr-grid-gap: 8px; --cr-grid-gap: 8px;
} }
#moduleContent {
align-items: center;
display: flex;
height: 260px;
justify-content: center;
}
.tile-item { .tile-item {
color: var(--cr-primary-text-color); color: var(--cr-primary-text-color);
height: 120px; height: 120px;
...@@ -13,11 +24,14 @@ ...@@ -13,11 +24,14 @@
border-radius: 8px; border-radius: 8px;
} }
</style> </style>
<cr-grid id="tiles" columns="3"> <ntp-module-header module-id="[[id]]">[[title]]</ntp-module-header>
<template id="tileList" is="dom-repeat" items="[[tiles]]"> <div id="moduleContent">
<div class="tile-item" title="[[item.label]]"> <cr-grid id="tiles" columns="3">
<img is="ntp-img" auto-src="[[item.imageUrl]]"></img> <template id="tileList" is="dom-repeat" items="[[tiles]]">
<span>[[item.value]]</span> <div class="tile-item" title="[[item.label]]">
</div> <img is="ntp-img" auto-src="[[item.imageUrl]]"></img>
</template> <span>[[item.value]]</span>
</cr-grid> </div>
</template>
</cr-grid>
</div>
...@@ -6,6 +6,7 @@ import 'chrome://resources/cr_elements/cr_grid/cr_grid.js'; ...@@ -6,6 +6,7 @@ import 'chrome://resources/cr_elements/cr_grid/cr_grid.js';
import '../../img.js'; import '../../img.js';
import '../../strings.m.js'; import '../../strings.m.js';
import '../module_header.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.m.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 {html, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
...@@ -29,8 +30,14 @@ class DummyModuleElement extends PolymerElement { ...@@ -29,8 +30,14 @@ class DummyModuleElement extends PolymerElement {
static get properties() { static get properties() {
return { return {
/** @type {!string} */
id: String,
/** @type {!Array<!foo.mojom.FooDataItem>} */ /** @type {!Array<!foo.mojom.FooDataItem>} */
tiles: Array, tiles: Array,
/** @type {!string} */
title: String,
}; };
} }
...@@ -48,18 +55,26 @@ class DummyModuleElement extends PolymerElement { ...@@ -48,18 +55,26 @@ class DummyModuleElement extends PolymerElement {
customElements.define(DummyModuleElement.is, DummyModuleElement); customElements.define(DummyModuleElement.is, DummyModuleElement);
/**
* @param {!string} id
* @param {!string} titleId
* @return {!DummyModuleElement}
*/
function createDummyElement(id, titleId) {
const element = new DummyModuleElement();
element.id = id;
element.title = loadTimeData.getString(titleId);
return element;
}
/** @type {!ModuleDescriptor} */ /** @type {!ModuleDescriptor} */
export const dummyDescriptor = new ModuleDescriptor( export const dummyDescriptor = new ModuleDescriptor(
/*id=*/ 'dummy', /*id=*/ 'dummy',
/*heightPx=*/ 260, () => Promise.resolve({ /*heightPx=*/ 314,
element: new DummyModuleElement(), () => Promise.resolve(createDummyElement('dummy', 'modulesDummyTitle')));
title: loadTimeData.getString('modulesDummyTitle'),
}));
/** @type {!ModuleDescriptor} */ /** @type {!ModuleDescriptor} */
export const dummyDescriptor2 = new ModuleDescriptor( export const dummyDescriptor2 = new ModuleDescriptor(
/*id=*/ 'dummy2', /*id=*/ 'dummy2',
/*heightPx=*/ 260, () => Promise.resolve({ /*heightPx=*/ 314,
element: new DummyModuleElement(), () => Promise.resolve(createDummyElement('dummy2', 'modulesDummy2Title')));
title: loadTimeData.getString('modulesDummy2Title'),
}));
...@@ -3,9 +3,6 @@ ...@@ -3,9 +3,6 @@
// found in the LICENSE file. // found in the LICENSE file.
/** /**
* @returns {Promise<?{ * @returns {Promise<?HTMLElement>}
* element: !HTMLElement,
* title: string,
* }>}
*/ */
window.loadKaleidoscopeModule = () => {}; window.loadKaleidoscopeModule = () => {};
...@@ -13,7 +13,7 @@ import {ModuleDescriptor} from '../module_descriptor.js'; ...@@ -13,7 +13,7 @@ import {ModuleDescriptor} from '../module_descriptor.js';
/** @type {!ModuleDescriptor} */ /** @type {!ModuleDescriptor} */
export const kaleidoscopeDescriptor = new ModuleDescriptor( export const kaleidoscopeDescriptor = new ModuleDescriptor(
/*id=*/ 'kaleidoscope', /*id=*/ 'kaleidoscope',
/*heightPx=*/ 330, /*heightPx=*/ 384,
async () => { async () => {
return loadKaleidoscopeModule(); return loadKaleidoscopeModule();
}, },
......
...@@ -10,20 +10,7 @@ import {BrowserProxy} from '../browser_proxy.js'; ...@@ -10,20 +10,7 @@ import {BrowserProxy} from '../browser_proxy.js';
*/ */
/** /**
* @typedef {{ * @typedef {function(): !Promise<?HTMLElement>}
* info: (function()|undefined),
* dismiss: (function():string|undefined),
* restore: (function()|undefined),
* }}
*/
let Actions;
/**
* @typedef {function(): !Promise<?{
* element: !HTMLElement,
* title: string,
* actions: (undefined|Actions),
* }>}
*/ */
let InitializeModuleCallback; let InitializeModuleCallback;
...@@ -38,14 +25,10 @@ export class ModuleDescriptor { ...@@ -38,14 +25,10 @@ export class ModuleDescriptor {
this.id_ = id; this.id_ = id;
/** @private {number} */ /** @private {number} */
this.heightPx_ = heightPx; this.heightPx_ = heightPx;
/** @private {?string} */
this.title_ = null;
/** @private {HTMLElement} */ /** @private {HTMLElement} */
this.element_ = null; this.element_ = null;
/** @private {!InitializeModuleCallback} */ /** @private {!InitializeModuleCallback} */
this.initializeCallback_ = initializeCallback; this.initializeCallback_ = initializeCallback;
/** @private {?Actions} */
this.actions_ = null;
} }
/** @return {string} */ /** @return {string} */
...@@ -58,29 +41,16 @@ export class ModuleDescriptor { ...@@ -58,29 +41,16 @@ export class ModuleDescriptor {
return this.heightPx_; return this.heightPx_;
} }
/** @return {?string} */
get title() {
return this.title_;
}
/** @return {?HTMLElement} */ /** @return {?HTMLElement} */
get element() { get element() {
return this.element_; return this.element_;
} }
/** @return {?Actions} */
get actions() {
return this.actions_;
}
async initialize() { async initialize() {
const info = await this.initializeCallback_(); this.element_ = await this.initializeCallback_();
if (!info) { if (!this.element_) {
return; return;
} }
this.title_ = info.title;
this.element_ = info.element;
this.actions_ = info.actions || null;
BrowserProxy.getInstance().handler.onModuleLoaded( BrowserProxy.getInstance().handler.onModuleLoaded(
this.id_, BrowserProxy.getInstance().now()); this.id_, BrowserProxy.getInstance().now());
} }
......
<style include="cr-icons">
:host {
align-items: center;
display: flex;
height: 22px;
margin: 16px;
}
#title {
color: var(--cr-primary-text-color);
font-size: 15px;
}
#headerSpacer {
flex-grow: 1;
}
#infoButton {
--cr-icon-image: url(./icons/info.svg);
}
#dismissButton {
--cr-icon-button-margin-start: 4px;
}
</style>
<span id="title"><slot></slot></span>
<div id="headerSpacer"></div>
<template is="dom-if" if="[[showInfoButton]]">
<cr-icon-button id="infoButton" title="$i18n{moduleInfoButtonTitle}"
on-click="onInfoButtonClick_">
</cr-icon-button>
</template>
<template is="dom-if" if="[[showDismissButton]]">
<cr-icon-button id="dismissButton" title="$i18n{moduleDismissButtonTitle}"
class="icon-clear" on-click="onDismissButtonClick_">
</cr-icon-button>
</template>
// 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 {assert} from 'chrome://resources/js/assert.m.js';
import {html, microTask, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {BrowserProxy} from '../browser_proxy.js';
/** @fileoverview Element that displays a header inside a module. */
class ModuleHeaderElement extends PolymerElement {
static get is() {
return 'ntp-module-header';
}
static get template() {
return html`{__html_template__}`;
}
static get properties() {
return {
/**
* The ID of the module.
* @type {!string}
*/
moduleId: String,
/**
* The title to be displayed.
* @type {!string}
*/
title: String,
/**
* True if the header should display an info button.
* @type {boolean}
*/
showInfoButton: {
type: Boolean,
value: false,
},
/**
* True if the header should display a dismiss button.
* @type {boolean}
*/
showDismissButton: {
type: Boolean,
value: false,
},
};
}
ready() {
super.ready();
const observer = new IntersectionObserver(([{intersectionRatio}]) => {
if (intersectionRatio >= .5) {
observer.disconnect();
BrowserProxy.getInstance().handler.onModuleImpression(
this.moduleId, BrowserProxy.getInstance().now());
}
}, {threshold: .5});
// Calling observe will immediately invoke the callback. If the header is
// fully shown when the page loads, the first callback invocation will
// happen before the header has dimensions. For this reason, we start
// observing after the element has had a chance to be rendered.
microTask.run(() => {
observer.observe(this);
});
}
/** @private */
onInfoButtonClick_() {
this.dispatchEvent(new CustomEvent('info-button-click', {bubbles: true}));
}
/** @private */
onDismissButtonClick_() {
this.dispatchEvent(
new CustomEvent('dismiss-button-click', {bubbles: true}));
}
}
customElements.define(ModuleHeaderElement.is, ModuleHeaderElement);
<style include="cr-icons"> <style>
:host { :host {
background-color: var(--ntp-background-override-color); background-color: var(--ntp-background-override-color);
border: solid var(--ntp-border-color) 1px; border: solid var(--ntp-border-color) 1px;
...@@ -8,48 +8,10 @@ ...@@ -8,48 +8,10 @@
overflow: hidden; overflow: hidden;
} }
#header {
align-items: center;
display: flex;
height: 22px;
margin: 16px;
}
#title {
color: var(--cr-primary-text-color);
font-size: 15px;
}
#headerSpacer {
flex-grow: 1;
}
#infoButton {
--cr-icon-image: url(./icons/info.svg);
}
#dismissButton {
--cr-icon-button-margin-start: 4px;
}
#moduleElement { #moduleElement {
align-items: center; align-items: center;
display: flex; display: flex;
justify-content: center; justify-content: center;
} }
</style> </style>
<div id="header">
<span id="title">[[descriptor.title]]</span>
<div id="headerSpacer"></div>
<template is="dom-if" if="[[descriptor.actions.info]]">
<cr-icon-button id="infoButton" title="$i18n{moduleInfoButtonTitle}"
on-click="onInfoButtonClick_">
</cr-icon-button>
</template>
<template is="dom-if" if="[[descriptor.actions.dismiss]]">
<cr-icon-button id="dismissButton" title="$i18n{moduleDismissButtonTitle}"
class="icon-clear" on-click="onDismissButtonClick_">
</cr-icon-button>
</template>
</div>
<div id="moduleElement"></div> <div id="moduleElement"></div>
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import {assert} from 'chrome://resources/js/assert.m.js'; import {assert} from 'chrome://resources/js/assert.m.js';
import {html, microTask, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js'; import {html, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {BrowserProxy} from '../browser_proxy.js'; import {BrowserProxy} from '../browser_proxy.js';
import {ModuleDescriptor} from './module_descriptor.js'; import {ModuleDescriptor} from './module_descriptor.js';
...@@ -38,49 +38,13 @@ class ModuleWrapperElement extends PolymerElement { ...@@ -38,49 +38,13 @@ class ModuleWrapperElement extends PolymerElement {
assert(!oldValue); assert(!oldValue);
this.$.moduleElement.appendChild(this.descriptor.element); this.$.moduleElement.appendChild(this.descriptor.element);
this.$.moduleElement.style.height = `${this.descriptor.heightPx}px`; this.$.moduleElement.style.height = `${this.descriptor.heightPx}px`;
const observer = new IntersectionObserver(([{intersectionRatio}]) => {
if (intersectionRatio >= .5) {
observer.disconnect();
BrowserProxy.getInstance().handler.onModuleImpression(
this.descriptor.id, BrowserProxy.getInstance().now());
}
}, {threshold: .5});
// Calling observe will immediately invoke the callback. If the header is
// fully shown when the page loads, the first callback invocation will
// happen before the header has dimensions. For this reason, we start
// observing after the element has had a chance to be rendered.
microTask.run(() => {
observer.observe(this.$.header);
});
// Log at most one usage per module per NTP page load. This is possible, // Log at most one usage per module per NTP page load. This is possible,
// if a user opens a link in a new tab. // if a user opens a link in a new tab.
this.descriptor.element.addEventListener('usage', () => { this.descriptor.element.addEventListener('usage', () => {
BrowserProxy.getInstance().handler.onModuleUsage(this.descriptor.id); BrowserProxy.getInstance().handler.onModuleUsage(this.descriptor.id);
}, {once: true}); }, {once: true});
} }
/** @private */
onInfoButtonClick_() {
this.descriptor.actions.info();
}
/** @private */
onDismissButtonClick_() {
this.hidden = true;
const message = this.descriptor.actions.dismiss();
this.dispatchEvent(new CustomEvent('dismiss-module', {
bubbles: true,
composed: true,
detail: message,
}));
}
restore() {
this.hidden = false;
if (this.descriptor.actions.restore) {
this.descriptor.actions.restore();
}
}
} }
customElements.define(ModuleWrapperElement.is, ModuleWrapperElement); customElements.define(ModuleWrapperElement.is, ModuleWrapperElement);
<style include="cr-hidden-style"> <style include="cr-hidden-style">
:host { :host {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
}
#moduleContent {
box-sizing: border-box; box-sizing: border-box;
display: block; display: block;
height: 100%; flex-grow: 1;
padding-inline-end: 15px; padding-inline-end: 15px;
padding-inline-start: 15px; padding-inline-start: 15px;
width: 100%; width: 100%;
...@@ -170,31 +177,38 @@ ...@@ -170,31 +177,38 @@
text-decoration: none; text-decoration: none;
} }
</style> </style>
<div id="taskItems"> <ntp-module-header module-id="[[id]]"
<template is="dom-repeat" id="taskItemsRepeat" items="[[task.taskItems]]" show-info-button on-info-button-click="onInfoButtonClick_"
on-dom-change="onDomChange_"> show-dismiss-button on-dismiss-button-click="onDismissButtonClick_">
<a class="task-item" href="[[item.targetUrl.url]]" [[task.title]]
on-click="onTaskItemClick_" on-auxclick="onTaskItemClick_"> </ntp-module-header>
<div class="image-background"> <div id="moduleContent">
<div class="image-container"> <div id="taskItems">
<img is="ntp-img" auto-src="[[item.imageUrl.url]]"></img> <template is="dom-repeat" id="taskItemsRepeat" items="[[task.taskItems]]"
on-dom-change="onDomChange_">
<a class="task-item" href="[[item.targetUrl.url]]"
on-click="onTaskItemClick_" on-auxclick="onTaskItemClick_">
<div class="image-background">
<div class="image-container">
<img is="ntp-img" auto-src="[[item.imageUrl.url]]"></img>
</div>
</div> </div>
</div> <div class="price" hidden$="[[!item.price]]">[[item.price]]</div>
<div class="price" hidden$="[[!item.price]]">[[item.price]]</div> <div class="name" title="[[item.name]]">[[item.name]]</div>
<div class="name" title="[[item.name]]">[[item.name]]</div> <div class="info">[[item.info]]</div>
<div class="info">[[item.info]]</div> </a>
</a> </template>
</template> </div>
</div> <div id="relatedSearches">
<div id="relatedSearches"> <template is="dom-repeat" id="relatedSearchesRepeat"
<template is="dom-repeat" id="relatedSearchesRepeat" items="[[task.relatedSearches]]" on-dom-change="onDomChange_">
items="[[task.relatedSearches]]" on-dom-change="onDomChange_"> <a class="pill" href="[[item.targetUrl.url]]" on-click="onPillClick_"
<a class="pill" href="[[item.targetUrl.url]]" on-click="onPillClick_" on-auxclick="onPillClick_">
on-auxclick="onPillClick_"> <div class="loupe"></div>
<div class="loupe"></div> <div class="search-text">[[item.text]]</div>
<div class="search-text">[[item.text]]</div> </a>
</a> </template>
</template> </div>
</div> </div>
<template is="dom-if" if="[[showInfoDialog]]" restamp> <template is="dom-if" if="[[showInfoDialog]]" restamp>
<cr-dialog show-on-attach> <cr-dialog show-on-attach>
......
...@@ -3,9 +3,9 @@ ...@@ -3,9 +3,9 @@
// found in the LICENSE file. // found in the LICENSE file.
import '../../img.js'; import '../../img.js';
import '../module_header.js';
import 'chrome://resources/cr_elements/hidden_style_css.m.js'; import 'chrome://resources/cr_elements/hidden_style_css.m.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 {html, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {ModuleDescriptor} from '../module_descriptor.js'; import {ModuleDescriptor} from '../module_descriptor.js';
import {TaskModuleHandlerProxy} from './task_module_handler_proxy.js'; import {TaskModuleHandlerProxy} from './task_module_handler_proxy.js';
...@@ -27,6 +27,9 @@ class TaskModuleElement extends PolymerElement { ...@@ -27,6 +27,9 @@ class TaskModuleElement extends PolymerElement {
static get properties() { static get properties() {
return { return {
/** @type {string} */
id: String,
/** @type {!taskModule.mojom.TaskModuleType} */ /** @type {!taskModule.mojom.TaskModuleType} */
taskModuleType: { taskModuleType: {
type: Number, type: Number,
...@@ -81,11 +84,36 @@ class TaskModuleElement extends PolymerElement { ...@@ -81,11 +84,36 @@ class TaskModuleElement extends PolymerElement {
this.dispatchEvent(new Event('usage', {bubbles: true, composed: true})); this.dispatchEvent(new Event('usage', {bubbles: true, composed: true}));
} }
/** @private */
onInfoButtonClick_() {
this.showInfoDialog = true;
}
/** @private */ /** @private */
onCloseClick_() { onCloseClick_() {
this.showInfoDialog = false; this.showInfoDialog = false;
} }
/** @private */
onDismissButtonClick_() {
TaskModuleHandlerProxy.getInstance().handler.dismissTask(
this.taskModuleType, this.task.name);
this.dispatchEvent(new CustomEvent('dismiss-module', {
bubbles: true,
composed: true,
detail: {
message: this.task.name,
restoreCallback: this.onRestore_.bind(this),
},
}));
}
/** @private */
onRestore_() {
TaskModuleHandlerProxy.getInstance().handler.restoreTask(
this.taskModuleType, this.task.name);
}
/** @private */ /** @private */
onDomChange_() { onDomChange_() {
if (!this.intersectionObserver_) { if (!this.intersectionObserver_) {
...@@ -106,8 +134,8 @@ class TaskModuleElement extends PolymerElement { ...@@ -106,8 +134,8 @@ class TaskModuleElement extends PolymerElement {
customElements.define(TaskModuleElement.is, TaskModuleElement); customElements.define(TaskModuleElement.is, TaskModuleElement);
/** @return {!Promise<?{element: !HTMLElement, title: string}>} */ /** @return {!Promise<?HTMLElement>} */
async function createModule(taskModuleType) { async function createModule(id, taskModuleType) {
const {task} = const {task} =
await TaskModuleHandlerProxy.getInstance().handler.getPrimaryTask( await TaskModuleHandlerProxy.getInstance().handler.getPrimaryTask(
taskModuleType); taskModuleType);
...@@ -117,34 +145,20 @@ async function createModule(taskModuleType) { ...@@ -117,34 +145,20 @@ async function createModule(taskModuleType) {
const element = new TaskModuleElement(); const element = new TaskModuleElement();
element.taskModuleType = taskModuleType; element.taskModuleType = taskModuleType;
element.task = task; element.task = task;
return { element.id = id;
element: element, return element;
title: task.title,
actions: {
info: () => {
element.showInfoDialog = true;
},
dismiss: () => {
TaskModuleHandlerProxy.getInstance().handler.dismissTask(
taskModuleType, task.name);
return loadTimeData.getStringF('dismissModuleToastMessage', task.name);
},
restore: () => {
TaskModuleHandlerProxy.getInstance().handler.restoreTask(
taskModuleType, task.name);
},
},
};
} }
/** @type {!ModuleDescriptor} */ /** @type {!ModuleDescriptor} */
export const recipeTasksDescriptor = new ModuleDescriptor( export const recipeTasksDescriptor = new ModuleDescriptor(
/*id=*/ 'recipe_tasks', /*id=*/ 'recipe_tasks',
/*heightPx=*/ 206, /*heightPx=*/ 260,
createModule.bind(null, taskModule.mojom.TaskModuleType.kRecipe)); createModule.bind(
null, 'recipe_tasks', taskModule.mojom.TaskModuleType.kRecipe));
/** @type {!ModuleDescriptor} */ /** @type {!ModuleDescriptor} */
export const shoppingTasksDescriptor = new ModuleDescriptor( export const shoppingTasksDescriptor = new ModuleDescriptor(
/*id=*/ 'shopping_tasks', /*id=*/ 'shopping_tasks',
/*heightPx=*/ 270, /*heightPx=*/ 324,
createModule.bind(null, taskModule.mojom.TaskModuleType.kShopping)); createModule.bind(
null, 'shopping_tasks', taskModule.mojom.TaskModuleType.kShopping));
...@@ -480,12 +480,10 @@ suite('NewTabPageAppTest', () => { ...@@ -480,12 +480,10 @@ suite('NewTabPageAppTest', () => {
{ {
id: 'foo', id: 'foo',
element: document.createElement('div'), element: document.createElement('div'),
title: 'Foo Title',
}, },
{ {
id: 'bar', id: 'bar',
element: document.createElement('div'), element: document.createElement('div'),
title: 'Bar Title',
} }
]); ]);
$$(app, 'ntp-middle-slot-promo') $$(app, 'ntp-middle-slot-promo')
...@@ -507,45 +505,43 @@ suite('NewTabPageAppTest', () => { ...@@ -507,45 +505,43 @@ suite('NewTabPageAppTest', () => {
test('modules can be dismissed and restored', async () => { test('modules can be dismissed and restored', async () => {
// Arrange. // Arrange.
let dismissCalled = false;
let restoreCalled = false; let restoreCalled = false;
const moduleElement = document.createElement('div');
// Act. // Act.
moduleResolver.resolve([{ moduleResolver.resolve([{
id: 'foo', id: 'foo',
element: document.createElement('div'), element: moduleElement,
title: 'Foo Title',
actions: {
dismiss: () => {
dismissCalled = true;
return 'Foo was removed';
},
restore: () => {
restoreCalled = true;
},
}
}]); }]);
await flushTasks(); // Wait for module descriptor resolution. await flushTasks(); // Wait for module descriptor resolution.
// Assert. // Assert.
const modules = app.shadowRoot.querySelectorAll('ntp-module-wrapper'); const modules = app.shadowRoot.querySelectorAll('ntp-module-wrapper');
assertEquals(1, modules.length); assertEquals(1, modules.length);
assertNotStyle($$(modules[0], '#dismissButton'), 'display', 'none');
assertFalse($$(app, '#dismissModuleToast').open); assertFalse($$(app, '#dismissModuleToast').open);
// Act. // Act.
$$(modules[0], '#dismissButton').click(); moduleElement.dispatchEvent(new CustomEvent('dismiss-module', {
bubbles: true,
composed: true,
detail: {
message: 'Foo',
restoreCallback: _ => {
restoreCalled = true;
},
},
}));
await flushTasks(); await flushTasks();
// Assert. // Assert.
assertTrue($$(app, '#dismissModuleToast').open); assertTrue($$(app, '#dismissModuleToast').open);
assertEquals( assertEquals(
'Foo was removed', 'Removed Foo',
$$(app, '#dismissModuleToastMessage').textContent.trim()); $$(app, '#dismissModuleToastMessage').textContent.trim());
assertNotStyle($$(app, '#undoDismissModuleButton'), 'display', 'none'); assertNotStyle($$(app, '#undoDismissModuleButton'), 'display', 'none');
assertTrue(dismissCalled);
assertEquals( assertEquals(
'foo', await testProxy.handler.whenCalled('onDismissModule')); 'foo', await testProxy.handler.whenCalled('onDismissModule'));
assertFalse(restoreCalled);
// Act. // Act.
$$(app, '#undoDismissModuleButton').click(); $$(app, '#undoDismissModuleButton').click();
......
...@@ -12,10 +12,7 @@ suite('NewTabPageModulesModuleRegistryTest', () => { ...@@ -12,10 +12,7 @@ suite('NewTabPageModulesModuleRegistryTest', () => {
const bazModule = document.createElement('div'); const bazModule = document.createElement('div');
const bazModuleResolver = new PromiseResolver(); const bazModuleResolver = new PromiseResolver();
ModuleRegistry.getInstance().registerModules([ ModuleRegistry.getInstance().registerModules([
new ModuleDescriptor('foo', 100, () => Promise.resolve({ new ModuleDescriptor('foo', 100, () => Promise.resolve(fooModule)),
element: fooModule,
title: 'Foo Title',
})),
new ModuleDescriptor('bar', 200, () => null), new ModuleDescriptor('bar', 200, () => null),
new ModuleDescriptor('baz', 300, () => bazModuleResolver.promise), new ModuleDescriptor('baz', 300, () => bazModuleResolver.promise),
]); ]);
...@@ -23,21 +20,16 @@ suite('NewTabPageModulesModuleRegistryTest', () => { ...@@ -23,21 +20,16 @@ suite('NewTabPageModulesModuleRegistryTest', () => {
// Act. // Act.
const modulesPromise = ModuleRegistry.getInstance().initializeModules(); const modulesPromise = ModuleRegistry.getInstance().initializeModules();
// Delayed promise resolution to test async module instantiation. // Delayed promise resolution to test async module instantiation.
bazModuleResolver.resolve({ bazModuleResolver.resolve(bazModule);
element: bazModule,
title: 'Baz Title',
});
const modules = await modulesPromise; const modules = await modulesPromise;
// Assert. // Assert.
assertEquals(2, modules.length); assertEquals(2, modules.length);
assertEquals('foo', modules[0].id); assertEquals('foo', modules[0].id);
assertEquals(100, modules[0].heightPx); assertEquals(100, modules[0].heightPx);
assertEquals('Foo Title', modules[0].title);
assertDeepEquals(fooModule, modules[0].element); assertDeepEquals(fooModule, modules[0].element);
assertEquals('baz', modules[1].id); assertEquals('baz', modules[1].id);
assertEquals(300, modules[1].heightPx); assertEquals(300, modules[1].heightPx);
assertEquals('Baz Title', modules[1].title);
assertDeepEquals(bazModule, modules[1].element); assertDeepEquals(bazModule, modules[1].element);
}); });
}); });
...@@ -22,12 +22,10 @@ suite('NewTabPageModulesModuleWrapperTest', () => { ...@@ -22,12 +22,10 @@ suite('NewTabPageModulesModuleWrapperTest', () => {
moduleWrapper.descriptor = { moduleWrapper.descriptor = {
id: 'foo', id: 'foo',
heightPx: 100, heightPx: 100,
title: 'Foo Title',
element: moduleElement, element: moduleElement,
}; };
// Assert. // Assert.
assertEquals('Foo Title', moduleWrapper.$.title.textContent);
assertEquals(100, $$(moduleWrapper, '#moduleElement').offsetHeight); assertEquals(100, $$(moduleWrapper, '#moduleElement').offsetHeight);
assertDeepEquals( assertDeepEquals(
moduleElement, $$(moduleWrapper, '#moduleElement').children[0]); moduleElement, $$(moduleWrapper, '#moduleElement').children[0]);
...@@ -38,14 +36,12 @@ suite('NewTabPageModulesModuleWrapperTest', () => { ...@@ -38,14 +36,12 @@ suite('NewTabPageModulesModuleWrapperTest', () => {
moduleWrapper.descriptor = { moduleWrapper.descriptor = {
id: 'foo', id: 'foo',
heightPx: 100, heightPx: 100,
title: 'Foo Title',
element: moduleElement, element: moduleElement,
}; };
assertThrows(() => { assertThrows(() => {
moduleWrapper.descriptor = { moduleWrapper.descriptor = {
id: 'foo', id: 'foo',
heightPx: 100, heightPx: 100,
title: 'Foo Title',
element: moduleElement, element: moduleElement,
}; };
}); });
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
import {shoppingTasksDescriptor, TaskModuleHandlerProxy} from 'chrome://new-tab-page/new_tab_page.js'; import {shoppingTasksDescriptor, TaskModuleHandlerProxy} from 'chrome://new-tab-page/new_tab_page.js';
import {TestBrowserProxy} from 'chrome://test/test_browser_proxy.m.js'; import {TestBrowserProxy} from 'chrome://test/test_browser_proxy.m.js';
import {eventToPromise} from 'chrome://test/test_util.m.js'; import {eventToPromise, flushTasks} from 'chrome://test/test_util.m.js';
suite('NewTabPageModulesTaskModuleTest', () => { suite('NewTabPageModulesTaskModuleTest', () => {
/** /**
...@@ -180,25 +180,30 @@ suite('NewTabPageModulesTaskModuleTest', () => { ...@@ -180,25 +180,30 @@ suite('NewTabPageModulesTaskModuleTest', () => {
}; };
testProxy.handler.setResultFor('getPrimaryTask', Promise.resolve({task})); testProxy.handler.setResultFor('getPrimaryTask', Promise.resolve({task}));
// Arrange.
// Act.
await shoppingTasksDescriptor.initialize(); await shoppingTasksDescriptor.initialize();
const moduleElement = shoppingTasksDescriptor.element;
// Assert. document.body.append(moduleElement);
assertEquals('function', typeof shoppingTasksDescriptor.actions.dismiss); await flushTasks();
assertEquals('function', typeof shoppingTasksDescriptor.actions.restore);
// Act. // Act.
const toastMessage = shoppingTasksDescriptor.actions.dismiss(); const waitForDismissEvent = eventToPromise('dismiss-module', moduleElement);
const dismissButton =
moduleElement.shadowRoot.querySelector('ntp-module-header')
.shadowRoot.querySelector('#dismissButton');
dismissButton.click();
const dismissEvent = await waitForDismissEvent;
const toastMessage = dismissEvent.detail.message;
const restoreCallback = dismissEvent.detail.restoreCallback;
// Assert. // Assert.
assertEquals('Removed Hello world', toastMessage); assertEquals('Hello world', toastMessage);
assertDeepEquals( assertDeepEquals(
[taskModule.mojom.TaskModuleType.kShopping, 'Hello world'], [taskModule.mojom.TaskModuleType.kShopping, 'Hello world'],
await testProxy.handler.whenCalled('dismissTask')); await testProxy.handler.whenCalled('dismissTask'));
// Act. // Act.
shoppingTasksDescriptor.actions.restore(); restoreCallback();
// Assert. // Assert.
assertDeepEquals( assertDeepEquals(
......
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