Commit 011f4ec7 authored by Christopher Lam's avatar Christopher Lam Committed by Commit Bot

[App Management] Push content of expandable-app-list to clients.

This CL pushes the per-row contents of expandable-app-list to being
specified at the client layer, rather than branching with dom-ifs
internally.

Bug: 906508
Change-Id: Icc3b324a408a0a0dd2d487745eda3793e910f143
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1481135
Commit-Queue: calamity <calamity@chromium.org>
Reviewed-by: default avatarAlan Cutter <alancutter@chromium.org>
Cr-Commit-Position: refs/heads/master@{#637968}
parent b5f87d5b
...@@ -116,7 +116,9 @@ if (!is_android) { ...@@ -116,7 +116,9 @@ if (!is_android) {
js_library("expandable_app_list") { js_library("expandable_app_list") {
deps = [ deps = [
":app_item", ":app_item",
":constants",
":store_client", ":store_client",
"//third_party/polymer/v1_0/components-chromium/iron-collapse:iron-collapse-extracted",
] ]
} }
......
<link rel="import" href="chrome://resources/html/polymer.html"> <link rel="import" href="chrome://resources/html/polymer.html">
<link rel="import" href="app_item.html"> <link rel="import" href="app_item.html">
<link rel="import" href="constants.html">
<link rel="import" href="shared_style.html"> <link rel="import" href="shared_style.html">
<link rel="import" href="store_client.html"> <link rel="import" href="store_client.html">
<link rel="import" href="permission_toggle.html"> <link rel="import" href="permission_toggle.html">
...@@ -10,68 +11,25 @@ ...@@ -10,68 +11,25 @@
<dom-module id="app-management-expandable-app-list"> <dom-module id="app-management-expandable-app-list">
<template> <template>
<style include="app-management-shared-css"> <style include="app-management-shared-css">
.app-management-item-arrow {
margin-inline-end: 8px;
padding: 12px;
}
#app-list-title { #app-list-title {
padding: 16px 24px; padding: 16px 24px;
} }
app-management-permission-toggle { #collapse {
margin-inline-end: 24px; display: block;
min-height: var(--collapsed-height);
overflow: hidden;
} }
</style> </style>
<!-- TODO(ceciliani) Avoid using dom-if, and use slot by getting |items|
from dom-repeat -->
<!-- TODO(calamity) Make a more generic polymer element for expandable
list. -->
<div class="card-container"> <div class="card-container">
<div id="app-list-title" class="header-text">[[listTitle]]</div> <div id="app-list-title" class="header-text">[[listTitle]]</div>
<template is="dom-repeat" items="[[displayedApps]]"> <iron-collapse id="collapse">
<template is="dom-if" if="[[!notificationsViewSelected_()]]"> <slot></slot>
<app-management-app-item app="[[item]]">
<paper-icon-button-light slot="right-content"
class="subpage-arrow app-management-item-arrow" actionable>
<button></button>
</paper-icon-button-light>
</app-management-app-item>
</template>
<template is="dom-if" if="[[notificationsViewSelected_()]]">
<app-management-app-item app="[[item]]">
<app-management-permission-toggle slot="right-content"
app="[[item]]"
permission-type="CONTENT_SETTINGS_TYPE_NOTIFICATIONS">
</app-management-permission-toggle>
</app-management-app-item>
</template>
</template>
<iron-collapse opened="[[listExpanded_]]">
<template is="dom-repeat" items="[[collapsedApps]]">
<template is="dom-if" if="[[!notificationsViewSelected_()]]">
<app-management-app-item app="[[item]]">
<paper-icon-button-light slot="right-content"
class="subpage-arrow app-management-item-arrow" actionable>
<button></button>
</paper-icon-button-light>
</app-management-app-item>
</template>
<template is="dom-if" if="[[notificationsViewSelected_()]]">
<app-management-app-item app="[[item]]">
<app-management-permission-toggle slot="right-content"
app="[[item]]"
permission-type="CONTENT_SETTINGS_TYPE_NOTIFICATIONS">
</app-management-permission-toggle>
</app-management-app-item>
</template>
</template>
</iron-collapse> </iron-collapse>
<div id="expander-row" class="expander-list-row" <div id="expander-row" class="expander-list-row"
on-click="toggleListExpanded_"> on-click="toggleListExpanded_">
<span>[[moreAppsString_(collapsedApps.length,listExpanded_)]]</span> <span>[[moreAppsString_(apps.length, listExpanded_)]]</span>
<paper-icon-button-light class="expand-button"> <paper-icon-button-light class="expand-button">
<button> <button>
<iron-icon icon="[[getCollapsedIcon_(listExpanded_)]]"> <iron-icon icon="[[getCollapsedIcon_(listExpanded_)]]">
......
...@@ -2,79 +2,112 @@ ...@@ -2,79 +2,112 @@
// 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.
/**
* This is an expanding container for a list of apps that shows some items by
* default and can be expanded to show more.
*
* Note: The implementation assumes children are all the same height.
*
* Example usage:
* <app-management-expandable-app-list apps="[[appsList]]">
* <template is="dom-repeat" items="[[appsList]]" as="app" notify-dom-change>
* <app-management-app-item app="[[app]]"></app-management-app-item>
* </template>
* </app-management-expandable-app-list>
*/
Polymer({ Polymer({
is: 'app-management-expandable-app-list', is: 'app-management-expandable-app-list',
behaviors: [
app_management.StoreClient,
],
properties: { properties: {
/**
* @private {Page}
*/
currentPage_: {
type: Object,
observer: 'onViewChanged_',
},
/**
* List of apps displayed before expanding the app list.
* @type {Array<App>}
*/
displayedApps: Array,
/** /**
* Title of the expandable list. * Title of the expandable list.
* @type {String} * @type {String}
*/ */
listTitle: String, listTitle: {
type: String,
value: '',
observer: 'onListTitleChanged_',
},
/** /** The number of apps to collapse down to. */
* List of apps displayed after expanding app list. collapsedSize: {
* @type {Array<App>} type: Number,
*/ value: NUMBER_OF_APPS_DISPLAYED_DEFAULT,
collapsedApps: {
type: Array,
observer: 'onAppsChanged_',
}, },
/** /** @private {boolean} */
* @private {boolean} listExpanded_: {
*/ type: Boolean,
listExpanded_: Boolean, observer: 'onListExpandedChanged_',
},
},
listeners: {
'dom-change': 'onDomChange_',
}, },
attached: function() { attached: function() {
this.watch('currentPage_', state => state.currentPage); // Hide on reattach.
this.listExpanded_ = false;
this.$.collapse.hide();
this.updateFromStore(); // Recalculate child heights on reattach.
this.onDomChange_();
}, },
/** /** @private */
* @private onAppsChanged_: function(change) {},
*/
onAppsChanged_: function() {
this.$['expander-row'].hidden = this.collapsedApps.length === 0;
},
/** /** @private */
* Collapse the list when changing a page if it is open so that list is always onListTitleChanged_() {
* collapsed when entering the page.
* @private
*/
onViewChanged_: function() {
this.$['app-list-title'].hidden = !this.listTitle; this.$['app-list-title'].hidden = !this.listTitle;
this.listExpanded_ = false;
}, },
/** /** @private */
* @private onDomChange_: function() {
*/ let collapsedHeight = 0;
let numChildren = 0;
for (const child of this.$.collapse.getContentChildren()) {
// Wait until we have an actual child element rather than just the
// dom-repeat.
if (child.tagName == 'DOM-REPEAT' || child.tagName == 'TEMPLATE') {
continue;
}
if (numChildren < this.collapsedSize) {
collapsedHeight += child.offsetHeight;
}
numChildren++;
}
this.style.setProperty(
'--collapsed-height', String(collapsedHeight) + 'px');
this.$['expander-row'].hidden = numChildren <= this.collapsedSize;
},
/** @private */
toggleListExpanded_: function() { toggleListExpanded_: function() {
this.listExpanded_ = !this.listExpanded_; this.listExpanded_ = !this.listExpanded_;
}, },
/** @private */
onListExpandedChanged_() {
// TODO(calamity): Hiding should display:none after the animation to prevent
// tabbing into hidden items.
const collapse = this.$.collapse;
// Since iron-collapse does not support a 'min-height' property, we force it
// to animate to the collapsed height.
if (this.listExpanded_) {
// Reset the opened state, or show won't work.
collapse.hide();
collapse.show();
} else {
// This technically leaves the collapse open.
collapse.updateSize('var(--collapsed-height)', true);
}
},
/** /**
* @param {boolean} listExpanded * @param {boolean} listExpanded
* @return {string} * @return {string}
...@@ -91,11 +124,8 @@ Polymer({ ...@@ -91,11 +124,8 @@ Polymer({
* @private * @private
*/ */
moreAppsString_: function(numApps, listExpanded) { moreAppsString_: function(numApps, listExpanded) {
return listExpanded ? loadTimeData.getString('lessApps') : return listExpanded ?
loadTimeData.getStringF('moreApps', numApps); loadTimeData.getString('lessApps') :
}, loadTimeData.getStringF('moreApps', numApps - this.collapsedSize);
notificationsViewSelected_: function() {
return this.currentPage_.pageType === PageType.NOTIFICATIONS;
}, },
}); });
...@@ -107,6 +107,9 @@ cr.define('app_management', function() { ...@@ -107,6 +107,9 @@ cr.define('app_management', function() {
await this.page.$.flushForTesting(); await this.page.$.flushForTesting();
} }
}; };
/** @type {number} */
this.guid = 0;
} }
async getApps() { async getApps() {
...@@ -170,12 +173,16 @@ cr.define('app_management', function() { ...@@ -170,12 +173,16 @@ cr.define('app_management', function() {
openNativeSettings(appId) {} openNativeSettings(appId) {}
/** /**
* @param {string} id * @param {string} optId
* @param {Object=} optConfig * @param {Object=} optConfig
* @return {!Promise<!App>}
*/ */
async addApp(id, optConfig) { async addApp(optId, optConfig) {
this.page.onAppAdded(FakePageHandler.createApp(id, optConfig)); optId = optId || String(this.guid++);
const app = FakePageHandler.createApp(optId, optConfig);
this.page.onAppAdded(app);
await this.$.flushForTesting(); await this.$.flushForTesting();
return app;
} }
/** /**
......
...@@ -38,11 +38,23 @@ ...@@ -38,11 +38,23 @@
justify-content: space-between; justify-content: space-between;
padding: 0 24px; padding: 0 24px;
} }
.app-management-item-arrow {
margin-inline-end: 8px;
padding: 12px;
}
</style> </style>
<app-management-expandable-app-list <app-management-expandable-app-list
displayed-apps="[[displayedApps_]]" apps="[[appsList]]"
collapsed-apps="[[collapsedApps_]]"
list-title="$i18n{appListTitle}"> list-title="$i18n{appListTitle}">
<template is="dom-repeat" items="[[appsList]]" as="app" notify-dom-change>
<app-management-app-item app="[[app]]">
<paper-icon-button-light slot="right-content"
class="subpage-arrow app-management-item-arrow" actionable>
<button></button>
</paper-icon-button-light>
</app-management-app-item>
</template>
</app-management-expandable-app-list> </app-management-expandable-app-list>
<div class="card-container"> <div class="card-container">
......
...@@ -22,16 +22,7 @@ Polymer({ ...@@ -22,16 +22,7 @@ Polymer({
* List of apps displayed before expanding the app list. * List of apps displayed before expanding the app list.
* @private {Array<App>} * @private {Array<App>}
*/ */
displayedApps_: { appsList: {
type: Array,
value: () => [],
},
/**
* List of apps displayed after expanding app list.
* @private {Array<App>}
*/
collapsedApps_: {
type: Array, type: Array,
value: () => [], value: () => [],
}, },
...@@ -56,10 +47,7 @@ Polymer({ ...@@ -56,10 +47,7 @@ Polymer({
* @private * @private
*/ */
onAppsChanged_: function() { onAppsChanged_: function() {
const appList = Object.values(this.apps_); this.appsList = Object.values(this.apps_);
this.displayedApps_ = appList.slice(0, NUMBER_OF_APPS_DISPLAYED_DEFAULT);
this.collapsedApps_ =
appList.slice(NUMBER_OF_APPS_DISPLAYED_DEFAULT, appList.length);
}, },
/** @private */ /** @private */
......
...@@ -12,6 +12,10 @@ ...@@ -12,6 +12,10 @@
margin-inline-start: 0; margin-inline-start: 0;
} }
app-management-permission-toggle {
margin-inline-end: 24px;
}
#notification-view-header { #notification-view-header {
align-items: center; align-items: center;
display: flex; display: flex;
...@@ -35,8 +39,17 @@ ...@@ -35,8 +39,17 @@
<div id="notification-title" class="page-title">$i18n{notifications}</div> <div id="notification-title" class="page-title">$i18n{notifications}</div>
</div> </div>
<app-management-expandable-app-list <app-management-expandable-app-list
displayed-apps="[[displayedApps_]]" apps="[[appsList_]]"
collapsed-apps="[[collapsedApps_]]"> collapsed-size="[[getCollapsedSize_(appsList_)]]">
<template is="dom-repeat" items="[[appsList_]]"
as="app" notify-dom-change>
<app-management-app-item app="[[app]]">
<app-management-permission-toggle slot="right-content"
app="[[app]]"
permission-type="CONTENT_SETTINGS_TYPE_NOTIFICATIONS">
</app-management-permission-toggle>
</app-management-app-item>
</template>
</app-management-expandable-app-list> </app-management-expandable-app-list>
</template> </template>
<script src="notifications_view.js"></script> <script src="notifications_view.js"></script>
......
...@@ -18,12 +18,18 @@ Polymer({ ...@@ -18,12 +18,18 @@ Polymer({
observer: 'onAppsChanged_', observer: 'onAppsChanged_',
}, },
/** @private {!Array<!App>} */
appsList_: {
type: Array,
computed: 'calculateAppsList_(allowed_.*, blocked_.*)',
},
/** /**
* List of apps with notification permission * List of apps with notification permission
* displayed before expanding the app list. * displayed before expanding the app list.
* @private {!Array<App>} * @private {!Array<App>}
*/ */
displayedApps_: { allowed_: {
type: Array, type: Array,
value: () => [], value: () => [],
}, },
...@@ -33,18 +39,10 @@ Polymer({ ...@@ -33,18 +39,10 @@ Polymer({
* displayed after expanding app list. * displayed after expanding app list.
* @private {!Array<App>} * @private {!Array<App>}
*/ */
collapsedApps_: { blocked_: {
type: Array, type: Array,
value: () => [], value: () => [],
}, },
/**
* @private {boolean}
*/
listExpanded_: {
type: Boolean,
value: false,
},
}, },
attached: function() { attached: function() {
...@@ -66,15 +64,10 @@ Polymer({ ...@@ -66,15 +64,10 @@ Polymer({
*/ */
onViewLoaded_: function() { onViewLoaded_: function() {
const state = this.getState(); const state = this.getState();
this.displayedApps_ = this.allowed_ =
Array.from(state.notifications.allowedIds, id => state.apps[id]); Array.from(state.notifications.allowedIds, id => state.apps[id]);
this.collapsedApps_ = this.blocked_ =
Array.from(state.notifications.blockedIds, id => state.apps[id]); Array.from(state.notifications.blockedIds, id => state.apps[id]);
if (this.displayedApps_.length === 0) {
this.displayedApps_ = this.collapsedApps_;
this.collapsedApps_ = [];
}
}, },
/** /**
...@@ -85,10 +78,8 @@ Polymer({ ...@@ -85,10 +78,8 @@ Polymer({
*/ */
onAppsChanged_() { onAppsChanged_() {
const unhandledAppIds = new Set(Object.keys(this.apps_)); const unhandledAppIds = new Set(Object.keys(this.apps_));
this.displayedApps_ = this.allowed_ = this.updateAppList_(this.allowed_, unhandledAppIds);
this.updateAppList_(this.displayedApps_, unhandledAppIds); this.blocked_ = this.updateAppList_(this.blocked_, unhandledAppIds);
this.collapsedApps_ =
this.updateAppList_(this.collapsedApps_, unhandledAppIds);
// If any new apps have been added, append them to the appropriate list. // If any new apps have been added, append them to the appropriate list.
for (const appId of unhandledAppIds) { for (const appId of unhandledAppIds) {
...@@ -100,13 +91,29 @@ Polymer({ ...@@ -100,13 +91,29 @@ Polymer({
} }
if (allowed === OptionalBool.kTrue) { if (allowed === OptionalBool.kTrue) {
this.displayedApps_.push(app); this.push('allowed_', app);
} else { } else {
this.collapsedApps_.push(app); this.push('blocked_', app);
} }
} }
}, },
/**
* @private
* @return {!Array<!App>}
*/
calculateAppsList_() {
return this.allowed_.concat(this.blocked_);
},
/**
* @private
* @return {number}
*/
getCollapsedSize_() {
return this.allowed_.length || this.blocked_.length;
},
/** /**
* Creates a new list of apps with the same order as the original appList, * Creates a new list of apps with the same order as the original appList,
* but using the updated apps from this.apps_. As each app is added to the * but using the updated apps from this.apps_. As each app is added to the
...@@ -129,7 +136,6 @@ Polymer({ ...@@ -129,7 +136,6 @@ Polymer({
/** @private */ /** @private */
onClickBackButton_: function() { onClickBackButton_: function() {
this.listExpanded_ = false;
if (!window.history.state) { if (!window.history.state) {
this.dispatch(app_management.actions.changePage(PageType.MAIN)); this.dispatch(app_management.actions.changePage(PageType.MAIN));
} else { } else {
......
...@@ -7,22 +7,25 @@ ...@@ -7,22 +7,25 @@
suite('<app-management-main-view>', function() { suite('<app-management-main-view>', function() {
let mainView; let mainView;
let fakeHandler; let fakeHandler;
let appIdCounter; let store;
/** /**
* @param {number} numApps * @param {number} numApps
*/ */
async function addApps(numApps) { async function addApps(numApps) {
for (let i = 0; i < numApps; i++) { for (let i = 0; i < numApps; i++) {
await fakeHandler.addApp((appIdCounter++).toString()); await fakeHandler.addApp();
} }
} }
setup(function() { function getAppItems() {
appIdCounter = 0; return mainView.$$('app-management-expandable-app-list')
.querySelectorAll('app-management-app-item');
}
setup(function() {
fakeHandler = setupFakeHandler(); fakeHandler = setupFakeHandler();
replaceStore(); store = replaceStore();
mainView = document.createElement('app-management-main-view'); mainView = document.createElement('app-management-main-view');
replaceBody(mainView); replaceBody(mainView);
...@@ -30,41 +33,31 @@ suite('<app-management-main-view>', function() { ...@@ -30,41 +33,31 @@ suite('<app-management-main-view>', function() {
test('simple app addition', async function() { test('simple app addition', async function() {
// Ensure there is no apps initially // Ensure there is no apps initially
expectEquals( expectEquals(0, getAppItems().length);
0,
mainView.$$('app-management-expandable-app-list')
.root.querySelectorAll('app-management-app-item')
.length);
const appId = '1'; const app = await fakeHandler.addApp();
await fakeHandler.addApp(appId);
let appItems = mainView.$$('app-management-expandable-app-list') let appItems = getAppItems();
.root.querySelectorAll('app-management-app-item');
expectEquals(1, appItems.length); expectEquals(1, appItems.length);
expectEquals(app.id, appItems[0].app.id);
expectEquals(appId, appItems[0].app.id); store.setReducersEnabled(false);
appItems[0].click();
const expected = app_management.actions.changePage(PageType.DETAIL, app.id);
assertDeepEquals(expected, store.lastAction);
}); });
test('more apps bar visibility', async function() { test('more apps bar visibility', async function() {
// The more apps bar shouldn't appear when there are 4 apps. // The more apps bar shouldn't appear when there are 4 apps.
await addApps(4); await addApps(NUMBER_OF_APPS_DISPLAYED_DEFAULT);
expectEquals( expectEquals(NUMBER_OF_APPS_DISPLAYED_DEFAULT, getAppItems().length);
4,
mainView.$$('app-management-expandable-app-list')
.root.querySelectorAll('app-management-app-item')
.length);
expectTrue(mainView.$$('app-management-expandable-app-list') expectTrue(mainView.$$('app-management-expandable-app-list')
.$['expander-row'] .$['expander-row']
.hidden); .hidden);
// The more apps bar appears when there are 5 apps. // The more apps bar appears when there are 5 apps.
await addApps(1); await addApps(1);
expectEquals( expectEquals(NUMBER_OF_APPS_DISPLAYED_DEFAULT + 1, getAppItems().length);
5,
mainView.$$('app-management-expandable-app-list')
.root.querySelectorAll('app-management-app-item')
.length);
expectFalse(mainView.$$('app-management-expandable-app-list') expectFalse(mainView.$$('app-management-expandable-app-list')
.$['expander-row'] .$['expander-row']
.hidden); .hidden);
......
...@@ -19,9 +19,8 @@ suite('<app-management-router>', function() { ...@@ -19,9 +19,8 @@ suite('<app-management-router>', function() {
setup(async function() { setup(async function() {
fakeHandler = setupFakeHandler(); fakeHandler = setupFakeHandler();
store = new app_management.TestStore(); store = replaceStore();
await fakeHandler.addApp('1'); await fakeHandler.addApp('1');
store.replaceSingleton();
router = document.createElement('app-management-router'); router = document.createElement('app-management-router');
replaceBody(router); replaceBody(router);
}); });
......
...@@ -27,13 +27,14 @@ function setupFakeHandler() { ...@@ -27,13 +27,14 @@ function setupFakeHandler() {
} }
/** /**
* Replace the app management store instance with a new, empty store. * Replace the app management store instance with a new, empty TestStore.
* @return {app_management.TestStore}
*/ */
function replaceStore() { function replaceStore() {
app_management.Store.instance_ = new app_management.Store(); let store = new app_management.TestStore();
store.setReducersEnabled(true);
app_management.Store.getInstance().init( store.replaceSingleton();
app_management.util.createEmptyState()); return store;
} }
/** /**
......
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