Commit 6488736e authored by Noel Gordon's avatar Noel Gordon Committed by Commit Bot

[toast] FilesApp toaster should use cr-element <cr-toast>

Rewrite the toaster in terms of <cr-toast> which supports animating the
toast on and off the screen using CSS transitions effects, has in-built
ARIA support, and provides <slot>s for inserting the toast text element
<div> and action <cr-button>.

Styling this component for ChromeOS files-ng: <cr-toast> mostly does it
for us with little extra work. We only need to control toast min-height
and number of lines shown (at most 2), a few margins and one background
color. Delete all our CSS that <cr-toast> now does for us.

Elevation +2 shadows are the <cr-toast> default internal style and flex
display too. Users must add justify-content: space-between to keep text
pinned to the left and the action button pinned to the right.

The default <cr-toast> position is 24px,24px from the lower-left corner
so CSS move it to the lower-right corner in LTR per FilesApp usage.

The toast duration is controlled by that <cr-toast> attribute: add that
attribute in markup and use the same value as before: 5000 ms.

Update the externs file definition (it was out-of-date) and add TODO to
fix <cr-toast> iron-a11y-announcer BUILD.gn deps issue. Remove ES6 lint
exceptions, add comments, add closure markup.

Fix UI test sharing tests: <cr-toast> fails to work in that system. Add
<files-toast> mock to workaround that failure.

Test: browser_tests --gtest_filter="FilemanagerJsTest*FilesToast"
Bug: 1002391, 1033660
Change-Id: I06113a6bcdc564bc374df559a3830a2a9df031f5
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2208166
Commit-Queue: Noel Gordon <noel@chromium.org>
Reviewed-by: default avatarAlex Danilo <adanilo@chromium.org>
Reviewed-by: default avatarLuciano Pacheco <lucmult@chromium.org>
Cr-Commit-Position: refs/heads/master@{#770964}
parent b5056af5
...@@ -17,25 +17,17 @@ class FilesToast extends PolymerElement { ...@@ -17,25 +17,17 @@ class FilesToast extends PolymerElement {
* @type {boolean} * @type {boolean}
*/ */
this.visible; this.visible;
/**
* @type {number}
*/
this.timeout;
} }
hide() {}
/** /**
* @param {string} text * @param {string} text
* @param {{text: string, callback: function()}=} opt_action * @param {{text: string, callback: function()}=} opt_action
*/ */
show(text, opt_action) {} show(text, opt_action) {}
/**
* @return {!Promise}
*/
hide() {}
} }
class FilesQuickView extends PolymerElement {} class FilesQuickView extends PolymerElement {}
class FilesMetadataBox extends PolymerElement {} class FilesMetadataBox extends PolymerElement {}
...@@ -103,7 +103,13 @@ js_library("files_spinner") { ...@@ -103,7 +103,13 @@ js_library("files_spinner") {
# TODO(tapted): Move this to //ui/file_manager/base. # TODO(tapted): Move this to //ui/file_manager/base.
js_library("files_toast") { js_library("files_toast") {
visibility += [ "//ui/file_manager/gallery/*" ] visibility += [ "//ui/file_manager/gallery/*" ]
deps = [ "//ui/webui/resources/cr_elements/cr_button:cr_button" ] deps = [
# TODO(crbug.com/1033660) Remove this line once CL:2144427 follow-up adds
# an iron-a11y-announcer deps to the js_library('cr_toast') rule.
"//third_party/polymer/v1_0/components-chromium/iron-a11y-announcer:iron-a11y-announcer-extracted",
"//ui/webui/resources/cr_elements/cr_button:cr_button",
"//ui/webui/resources/cr_elements/cr_toast:cr_toast",
]
} }
# TODO(tapted): Move this to //ui/file_manager/base. # TODO(tapted): Move this to //ui/file_manager/base.
......
...@@ -7,59 +7,51 @@ ...@@ -7,59 +7,51 @@
<link rel="import" href="chrome://resources/cr_elements/shared_style_css.html"> <link rel="import" href="chrome://resources/cr_elements/shared_style_css.html">
<link rel="import" href="chrome://resources/cr_elements/cr_button/cr_button.html"> <link rel="import" href="chrome://resources/cr_elements/cr_button/cr_button.html">
<link rel="import" href="chrome://resources/cr_elements/cr_toast/cr_toast.html">
<dom-module id="files-toast"> <dom-module id="files-toast">
<template> <template>
<style> <style>
.container[hidden] { /*
display: none; * TODO(files-ng): apply max-width to the container when long button text
} * labels like "Manage Linux Files" are i18n translated to "Manage". Use
* max-width: 336px; for files-ng.
*/
.container { .container {
align-items: center; --cr-toast-background: var(--google-grey-900-white-4-percent);
background: var(--google-grey-900-white-4-percent); justify-content: space-between;
border-radius: 4px;
bottom: 24px;
display: flex;
/*
* TODO(files-ng): apply max-width when long button labels like "Manage
* Linux Files" are i18n translated to "Manage". Use max-width: 336px;
*/
min-height: 48px; min-height: 48px;
min-width: 256px; min-width: 256px;
position: absolute; padding: 0;
} }
:host-context(html[dir='ltr']) .container { :host-context(:root[dir=ltr]) .container {
right: 24px; left: unset;
right: 0;
} }
:host-context(html[dir='rtl']) .container { :host-context(:root[dir=rtl]) .container {
left: 24px; left: 0;
right: unset;
} }
.text { .text {
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
color: white;
display: -webkit-box; display: -webkit-box;
line-height: 20px; line-height: 20px;
margin: 14px 16px;
overflow: hidden; overflow: hidden;
padding-inline-end: 16px;
padding-inline-start: 16px;
white-space: normal;
} }
.action { .action {
border: none; margin: 0 8px !important;
color: var(--google-blue-300);
margin: 0 8px;
} }
</style> </style>
<div class="container" id="container" hidden> <cr-toast class="container" id="container" duration="5000">
<div class="text" id="text"></div> <div class="text" id="text"></div>
<cr-button class="action" id="action" on-tap="onActionTapped_"></cr-button> <cr-button class="action" id="action" on-tap="onActionTapped_"></cr-button>
</div> </cr-toast>
</template> </template>
</dom-module> </dom-module>
......
...@@ -3,29 +3,31 @@ ...@@ -3,29 +3,31 @@
// found in the LICENSE file. // found in the LICENSE file.
/** /**
* @typedef {{text:string, callback:(function()|undefined)}} * @typedef {{
* text:string,
* callback:(function()|undefined)
* }}
*/ */
// eslint-disable-next-line no-var let FilesToastAction;
var FilesToastAction;
/** /**
* @typedef {{text:string, action:(FilesToastAction|undefined)}} * @typedef {{
* text:string,
* action:(FilesToastAction|undefined)
* }}
*/ */
// eslint-disable-next-line no-var let FilesToastData;
var FilesToastData;
/** /**
* Files Toast. * Files Toast.
* *
* This toast is shown at the bottom-right in ltr (bottom-left in rtl). * The toast is shown at the bottom-right in LTR, bottom-left in RTL. Usage:
* *
* Usage:
* toast.show('Toast without action.'); * toast.show('Toast without action.');
* toast.show('Toast with action', {text: 'ACTION', callback:function() {}}); * toast.show('Toast with action', {text: 'Action', callback:function(){}});
* toast.hide(); * toast.hide();
*/ */
// eslint-disable-next-line no-var const FilesToast = Polymer({
var FilesToast = Polymer({
is: 'files-toast', is: 'files-toast',
properties: { properties: {
...@@ -34,16 +36,9 @@ var FilesToast = Polymer({ ...@@ -34,16 +36,9 @@ var FilesToast = Polymer({
readOnly: true, readOnly: true,
value: false, value: false,
}, },
duration: {
type: Number,
value: 5000, /* ms */
}
}, },
/** created() {
* Initialize member variables.
*/
created: function() {
/** /**
* @private {?FilesToastAction} * @private {?FilesToastAction}
*/ */
...@@ -53,124 +48,80 @@ var FilesToast = Polymer({ ...@@ -53,124 +48,80 @@ var FilesToast = Polymer({
* @private {!Array<!FilesToastData>} * @private {!Array<!FilesToastData>}
*/ */
this.queue_ = []; this.queue_ = [];
},
/** attached() {
* @private {Animation} this.$.container.ontransitionend = this.onTransitionEnd_.bind(this);
*/
this.enterAnimationPlayer_ = null;
/**
* @private {Animation}
*/
this.hideAnimationPlayer_ = null;
}, },
/** /**
* Shows toast. If a toast is already shown, this toast will be added to the * Shows toast. If a toast is already shown, add the toast to the pending
* queue and shown when others have completed. * queue. It will shown later when other toasts have completed.
* *
* @param {string} text Text of toast. * @param {string} text Text of toast.
* @param {FilesToastAction=} opt_action Action. Callback * @param {FilesToastAction=} opt_action Action. The |Action.callback| is
* is invoked when user taps an action. * called if the user taps or clicks the action button.
*/ */
show: function(text, opt_action) { show(text, opt_action) {
if (this.visible) { if (this.visible) {
this.queue_.push({text: text, action: opt_action}); this.queue_.push({text: text, action: opt_action});
return; return;
} }
this._setVisible(true);
// Update UI. this._setVisible(true);
this.$.container.removeAttribute('style');
this.$.container.hidden = false;
this.$.text.removeAttribute('style');
this.$.text.innerText = text; this.$.text.innerText = text;
this.action_ = opt_action || null; this.action_ = opt_action || null;
if (this.action_) { if (this.action_) {
this.$.text.setAttribute('style', 'padding-inline-end: 0'); this.$.text.setAttribute('style', 'margin-inline-end: 0');
this.$.action.hidden = false;
this.$.action.innerText = this.action_.text; this.$.action.innerText = this.action_.text;
this.$.action.hidden = false;
} else { } else {
this.$.text.removeAttribute('style');
this.$.action.innerText = '';
this.$.action.hidden = true; this.$.action.hidden = true;
} }
// Make container min-height 68px if the text needs two lines. this.$.container.show();
const style = window.getComputedStyle(this.$.text);
if (parseFloat(style.height) > 1.2 * parseFloat(style.lineHeight)) {
this.$.container.setAttribute('style', 'min-height: 68px');
}
// Perform animation.
this.enterAnimationPlayer_ = this.$.container.animate([
{bottom: '-100px', opacity: 0, offset: 0},
{bottom: '24px', opacity: 1, offset: 1}
], 100 /* ms */);
this.enterAnimationPlayer_.addEventListener('finish', () => {
this.enterAnimationPlayer_ = null;
});
// Set timeout.
setTimeout(this.hide.bind(this), this.duration);
}, },
/** /**
* Handles tap event of action button. * Handles action button tap/click.
*
* @private
*/ */
onActionTapped_: function() { onActionTapped_() {
if (!this.action_ || !this.action_.callback) { if (this.action_ && this.action_.callback) {
return; this.action_.callback();
this.hide();
} }
this.action_.callback();
this.hide();
}, },
/** /**
* Clears toast if it's shown. * Handles the <cr-toast> transitionend event. On a hide transition, show
* @return {!Promise} A promise which is resolved when toast is hidden. * the next queued toast if any.
*
* @private
*/ */
hide: function() { onTransitionEnd_() {
if (!this.visible) { const hide = !this.$.container.hasAttribute('open');
return Promise.resolve();
}
// If it's performing enter animation, wait until it's done and come back
// later.
if (this.enterAnimationPlayer_ && !this.enterAnimationPlayer_.finished) {
return new Promise(resolve => {
// Check that the animation is still playing. Animation can be finished
// between the above condition check and this function call.
if (!this.enterAnimationPlayer_ ||
this.enterAnimationPlayer_.finished) {
resolve();
}
this.enterAnimationPlayer_.addEventListener('finish', resolve);
}).then(this.hide.bind(this));
}
// Start hide animation if it's not performing now.
if (!this.hideAnimationPlayer_) {
this.hideAnimationPlayer_ = this.$.container.animate([
{bottom: '24px', opacity: 1, offset: 0},
{bottom: '-100px', opacity: 0, offset: 1}
], 100 /* ms */);
}
return new Promise(resolve => { if (hide && this.visible) {
this.hideAnimationPlayer_.addEventListener('finish', resolve);
}).then(() => {
this.$.container.hidden = true;
this.hideAnimationPlayer_ = null;
this._setVisible(false); this._setVisible(false);
// Show next in the queue, if any.
if (this.queue_.length > 0) { if (this.queue_.length > 0) {
const next = this.queue_.shift(); const next = this.queue_.shift();
this.show(next.text, next.action); setTimeout(this.show.bind(this), 0, next.text, next.action);
} }
}); }
},
/**
* Hides toast if visible.
*/
hide() {
if (this.visible) {
this.$.container.hide();
}
} }
}); });
...@@ -252,7 +252,48 @@ shareBase.testSharePaths = async ( ...@@ -252,7 +252,48 @@ shareBase.testSharePaths = async (
done(); done();
}; };
const createMockFilesAppToast = () => {
const toast = document.querySelector('#toast');
toast.shadowRoot.innerHTML = `
<div class="container" id="container" hidden>
<div class="text" id="text" hidden></div>
<cr-button class="action" id="action" hidden></cr-button>
</div>
`;
toast.visible = false;
toast.show = (message, action) => {
const host = document.querySelector('#toast');
if (typeof message === 'string') {
const text = host.shadowRoot.querySelector('#text');
text.innerText = message;
text.hidden = false;
} else {
assertTrue(false, 'Invalid <files-toast> message');
return;
}
if (action && action.text && action.callback) {
const button = host.shadowRoot.querySelector('#action');
button.innerText = action.text;
button.hidden = false;
} else {
assertTrue(false, 'Invalid <files-toast> action');
return;
}
console.log('Toasted ' + message);
const container = host.shadowRoot.querySelector('#container');
container.hidden = false;
host.visible = true;
};
};
crostiniShare.testSharePaths = done => { crostiniShare.testSharePaths = done => {
createMockFilesAppToast();
shareBase.testSharePaths( shareBase.testSharePaths(
shareBase.vmNameTermina, shareBase.vmNameSelectorLinux, shareBase.vmNameTermina, shareBase.vmNameSelectorLinux,
shareBase.toastSharedTextLinux, shareBase.toastActionTextLinux, shareBase.toastSharedTextLinux, shareBase.toastActionTextLinux,
...@@ -260,6 +301,7 @@ crostiniShare.testSharePaths = done => { ...@@ -260,6 +301,7 @@ crostiniShare.testSharePaths = done => {
}; };
pluginVmShare.testSharePaths = done => { pluginVmShare.testSharePaths = done => {
createMockFilesAppToast();
shareBase.testSharePaths( shareBase.testSharePaths(
shareBase.vmNamePluginVm, shareBase.vmNameSelectorPluginVm, shareBase.vmNamePluginVm, shareBase.vmNameSelectorPluginVm,
shareBase.toastSharedTextPluginVm, shareBase.toastActionTextPluginVm, shareBase.toastSharedTextPluginVm, shareBase.toastActionTextPluginVm,
......
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