blob: be384a2ab32696dd9e99ff252f82d90319f8d6eb [file] [log] [blame]
// Copyright 2019 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 './xf_button.js';
import './xf_circular_progress.js';
import {assert} from 'chrome://resources/js/assert.m.js';
import {util} from '../../common/js/util.js';
import {DisplayPanel} from './xf_display_panel.js';
/**
* A panel to display the status or progress of a file operation.
* @extends HTMLElement
*/
export class PanelItem extends HTMLElement {
constructor() {
super();
const host = document.createElement('template');
host.innerHTML = this.constructor.template_;
this.attachShadow({mode: 'open'}).appendChild(host.content.cloneNode(true));
/** @private {Element} */
this.indicator_ = this.shadowRoot.querySelector('#indicator');
/**
* TODO(crbug.com/947388) make this a closure enum.
* @const
*/
this.panelTypeDefault = -1;
this.panelTypeProgress = 0;
this.panelTypeSummary = 1;
this.panelTypeDone = 2;
this.panelTypeError = 3;
this.panelTypeInfo = 4;
this.panelTypeFormatProgress = 5;
this.panelTypeSyncProgress = 6;
/** @private {number} */
this.panelType_ = this.panelTypeDefault;
/** @private @type {?function(Event)} */
this.onclick = this.onClicked_.bind(this);
/** @public {?DisplayPanel} */
this.parent = null;
/**
* Callback that signals events happening in the panel (e.g. click).
* @private @type {!function(*)}
*/
this.signal_ = console.log;
/**
* User specific data, used as a reference to persist any custom
* data that the panel user may want to use in the signal callback.
* e.g. holding the file name(s) used in a copy operation.
* @type {string|Object}
*/
this.userData = null;
}
/**
* Static getter for the custom element template.
* @private
* @return {string}
*/
static get template_() {
return `<style>
.xf-panel-item {
align-items: center;
background-color: rgba(0,0,0,0);
border-radius: 4px;
display: flex;
flex-direction: row;
height: 68px;
width: 400px;
}
.xf-button {
height: 36px;
width: 36px;
}
.xf-panel-text {
flex: 1;
font: 13px Roboto;
line-height: 20px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.xf-panel-label-text {
outline: none;
}
:host([panel-type='3']) .xf-panel-label-text {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
white-space: normal;
}
:host([panel-type='3']) .xf-linebreaker {
display: none;
}
.xf-panel-label-text {
color: var(--google-grey-900);
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.xf-panel-secondary-text {
color: var(--google-grey-700);
}
:host(:not([detailed-panel])) .xf-padder-4 {
width: 4px;
}
:host(:not([detailed-panel])) .xf-padder-16 {
width: 16px;
}
:host(:not([detailed-panel])) .xf-grow-padder {
width: 24px;
}
xf-circular-progress {
padding: 16px;
}
:host(:not([detailed-summary])) iron-icon {
height: 36px;
padding: 16px;
width: 36px;
}
// TODO(crbug.com/947388) Use '--goog' prefixed CSS varables.
.xf-success {
color: rgb(52, 168, 83);
}
.xf-failure {
color: rgb(234, 67, 53);
}
:host([panel-type='0']) .xf-panel-item {
height: var(--progress-height);
padding-top: var(--progress-padding-top);
padding-bottom: var(--progress-padding-bottom);
}
:host([detailed-panel]:not([detailed-summary])) .xf-panel-text {
margin-inline-end: 24px;
margin-inline-start: 24px;
}
:host([detailed-panel][panel-type='2']) .xf-panel-secondary-text {
color: var(--google-green-600);
}
:host([detailed-panel]:not([detailed-summary])) xf-button {
margin-inline-end: 12px;
}
:host([detailed-panel]:not([detailed-summary])) #indicator {
display: none;
}
:host([detailed-summary][data-category='collapsed'])
.xf-panel-item {
width: 236px;
}
:host([detailed-summary]) .xf-panel-text {
align-items: center;
display: flex;
height: 48px;
max-width: unset;
width: 100%;
}
:host([detailed-summary]) #indicator {
margin-inline-start: 22px;
padding: 0;
}
:host([detailed-summary][data-category='collapsed']) #indicator {
margin-inline-end: 20px;
min-width: 28px;
}
:host([detailed-summary][data-category='expanded']) #indicator {
margin-inline-end: 18px;
min-width: 32px;
}
:host([detailed-summary]) #primary-action {
align-items: center;
display: flex;
height: 48px;
justify-content: center;
margin-inline-end: 10px;
margin-inline-start: auto;
width: 48px;
}
:host([detailed-panel]) .xf-padder-4 {
display: none;
}
:host([detailed-panel]) .xf-padder-16 {
display: none;
}
:host([detailed-panel]) .xf-grow-padder {
display: none;
}
</style>
<div class='xf-panel-item'>
<xf-circular-progress id='indicator'>
</xf-circular-progress>
<div class='xf-panel-text' role='alert' tabindex='0'>
<span class='xf-panel-label-text'>
</span>
<br class='xf-linebreaker'/>
</div>
<div class='xf-grow-padder'></div>
<xf-button id='secondary-action' tabindex='-1'>
</xf-button>
<div id='button-gap' class='xf-padder-4'></div>
<xf-button id='primary-action' tabindex='-1'>
</xf-button>
<div class='xf-padder-16'></div>
</div>`;
}
/**
* Remove an element from the panel using it's id.
* @return {?Element}
* @private
*/
removePanelElementById_(id) {
const element = this.shadowRoot.querySelector(id);
if (element) {
element.remove();
}
return element;
}
/**
* Sets up the different panel types. Panels have per-type configuration
* templates, but can be further customized using individual attributes.
* @param {number} type The enumerated panel type to set up.
* @private
*/
setPanelType(type) {
this.setAttribute('detailed-panel', 'detailed-panel');
if (this.panelType_ === type) {
return;
}
// Remove the indicators/buttons that can change.
this.removePanelElementById_('#indicator');
let element = this.removePanelElementById_('#primary-action');
if (element) {
element.onclick = null;
}
element = this.removePanelElementById_('#secondary-action');
if (element) {
element.onclick = null;
}
// Mark the indicator as empty so it recreates on setAttribute.
this.setAttribute('indicator', 'empty');
const buttonSpacer = this.shadowRoot.querySelector('#button-gap');
// Default the text host to use an alert role.
const textHost = assert(this.shadowRoot.querySelector('.xf-panel-text'));
textHost.setAttribute('role', 'alert');
// Setup the panel configuration for the panel type.
// TOOD(crbug.com/947388) Simplify this switch breaking out common cases.
/** @type {?Element} */
let primaryButton = null;
/** @type {?Element} */
let secondaryButton = null;
switch (type) {
case this.panelTypeProgress:
this.setAttribute('indicator', 'progress');
secondaryButton = document.createElement('xf-button');
secondaryButton.id = 'secondary-action';
secondaryButton.onclick = assert(this.onclick);
secondaryButton.dataset.category = 'cancel';
secondaryButton.setAttribute('aria-label', '$i18n{CANCEL_LABEL}');
buttonSpacer.insertAdjacentElement('afterend', secondaryButton);
break;
case this.panelTypeSummary:
this.setAttribute('indicator', 'largeprogress');
primaryButton = document.createElement('xf-button');
primaryButton.id = 'primary-action';
primaryButton.dataset.category = 'expand';
primaryButton.setAttribute(
'aria-label', '$i18n{FEEDBACK_EXPAND_LABEL}');
// Remove the 'alert' role to stop screen readers repeatedly
// reading each progress update.
textHost.setAttribute('role', '');
buttonSpacer.insertAdjacentElement('afterend', primaryButton);
break;
case this.panelTypeDone:
this.setAttribute('indicator', 'status');
this.setAttribute('status', 'success');
primaryButton = document.createElement('xf-button');
primaryButton.id = 'primary-action';
primaryButton.onclick = assert(this.onclick);
primaryButton.dataset.category = 'dismiss';
buttonSpacer.insertAdjacentElement('afterend', primaryButton);
break;
case this.panelTypeError:
this.setAttribute('indicator', 'status');
this.setAttribute('status', 'failure');
this.primaryText = '$i18n{FILE_ERROR_GENERIC}';
this.secondaryText = '';
secondaryButton = document.createElement('xf-button');
secondaryButton.id = 'secondary-action';
secondaryButton.onclick = assert(this.onclick);
secondaryButton.dataset.category = 'dismiss';
buttonSpacer.insertAdjacentElement('afterend', secondaryButton);
break;
case this.panelTypeInfo:
break;
case this.panelTypeFormatProgress:
this.setAttribute('indicator', 'status');
this.setAttribute('status', 'hard-drive');
break;
case this.panelTypeSyncProgress:
this.setAttribute('indicator', 'progress');
break;
}
this.panelType_ = type;
}
/**
* Registers this instance to listen to these attribute changes.
* @private
*/
static get observedAttributes() {
return [
'count',
'errormark',
'indicator',
'panel-type',
'primary-text',
'progress',
'secondary-text',
'status',
];
}
/**
* Callback triggered by the browser when our attribute values change.
* @param {string} name Attribute that's changed.
* @param {?string} oldValue Old value of the attribute.
* @param {?string} newValue New value of the attribute.
* @private
*/
attributeChangedCallback(name, oldValue, newValue) {
/** @type {Element} */
let indicator = null;
/** @type {Element} */
let textNode;
// TODO(adanilo) Chop out each attribute handler into a function.
switch (name) {
case 'count':
if (this.indicator_) {
this.indicator_.setAttribute('label', newValue || '');
}
break;
case 'errormark':
if (this.indicator_) {
this.indicator_.setAttribute('errormark', newValue || '');
}
break;
case 'indicator':
// Get rid of any existing indicator
const oldIndicator = this.shadowRoot.querySelector('#indicator');
if (oldIndicator) {
oldIndicator.remove();
}
switch (newValue) {
case 'progress':
case 'largeprogress':
indicator = document.createElement('xf-circular-progress');
if (newValue === 'largeprogress') {
indicator.setAttribute('radius', '14');
} else {
indicator.setAttribute('radius', '10');
}
break;
case 'status':
indicator = document.createElement('iron-icon');
const status = this.getAttribute('status');
if (status) {
indicator.setAttribute('icon', `files36:${status}`);
}
break;
}
this.indicator_ = indicator;
if (indicator) {
const itemRoot = this.shadowRoot.querySelector('.xf-panel-item');
indicator.setAttribute('id', 'indicator');
itemRoot.prepend(indicator);
}
break;
case 'panel-type':
this.setPanelType(Number(newValue));
if (this.parent && this.parent.updateSummaryPanel) {
this.parent.updateSummaryPanel();
}
break;
case 'progress':
if (this.indicator_) {
this.indicator_.progress = Number(newValue);
if (this.parent && this.parent.updateProgress) {
this.parent.updateProgress();
}
}
break;
case 'status':
if (this.indicator_) {
this.indicator_.setAttribute('icon', `files36:${newValue}`);
}
break;
case 'primary-text':
textNode = this.shadowRoot.querySelector('.xf-panel-label-text');
if (textNode) {
textNode.textContent = newValue;
// Set the aria labels for the activity and cancel button.
this.setAttribute('aria-label', /** @type {string} */ (newValue));
}
break;
case 'secondary-text':
textNode = this.shadowRoot.querySelector('.xf-panel-secondary-text');
if (!textNode) {
const parent = this.shadowRoot.querySelector('.xf-panel-text');
if (!parent) {
return;
}
textNode = document.createElement('span');
textNode.setAttribute('class', 'xf-panel-secondary-text');
parent.appendChild(textNode);
}
// Remove the secondary text node if the text is empty
if (newValue == '') {
textNode.remove();
} else {
textNode.textContent = newValue;
}
break;
}
}
/**
* DOM connected.
* @private
*/
connectedCallback() {
this.onclick = this.onClicked_.bind(this);
// Set click event handler references.
let button = this.shadowRoot.querySelector('#primary-action');
if (button) {
button.onclick = this.onclick;
}
button = this.shadowRoot.querySelector('#secondary-action');
if (button) {
button.onclick = this.onclick;
}
}
/**
* DOM disconnected.
* @private
*/
disconnectedCallback() {
// Replace references to any signal callback.
this.signal_ = console.log;
// Clear click event handler references.
let button = this.shadowRoot.querySelector('#primary-action');
if (button) {
button.onclick = null;
}
button = this.shadowRoot.querySelector('#secondary-action');
if (button) {
button.onclick = null;
}
this.onclick = null;
}
/**
* Handles 'click' events from our sub-elements and sends
* signals to the |signal_| callback if needed.
* @param {?Event} event
* @private
*/
onClicked_(event) {
event.stopImmediatePropagation();
event.preventDefault();
// Ignore clicks on the panel item itself.
if (event.target === this) {
return;
}
const id = assert(event.target.dataset.category);
this.signal_(id);
}
/**
* Sets the callback that triggers signals from events on the panel.
* @param {?function(*)} signal
*/
set signalCallback(signal) {
this.signal_ = signal || console.log;
}
/**
* Set the visibility of the error marker.
* @param {string} visibility Visibility value being set.
*/
set errorMarkerVisibility(visibility) {
this.setAttribute('errormark', visibility);
}
/**
* Getter for the visibility of the error marker.
*/
get errorMarkerVisibility() {
// If we have an indicator on the panel, then grab the
// visibility value from that.
if (this.indicator_) {
return this.indicator_.errorMarkerVisibility;
}
// If there's no indicator on the panel just return the
// value of any attribute as a fallback.
return this.getAttribute('errormark');
}
/**
* Setter to set the indicator type.
* @param {string} indicator Progress (optionally large) or status.
*/
set indicator(indicator) {
this.setAttribute('indicator', indicator);
}
/**
* Getter for the progress indicator.
*/
get indicator() {
return this.getAttribute('indicator');
}
/**
* Setter to set the success/failure indication.
* @param {string} status Status value being set.
*/
set status(status) {
this.setAttribute('status', status);
}
/**
* Getter for the success/failure indication.
*/
get status() {
return this.getAttribute('status');
}
/**
* Setter to set the progress property, sent to any child indicator.
* @param {string} progress Progress value being set.
* @public
*/
set progress(progress) {
this.setAttribute('progress', progress);
}
/**
* Getter for the progress indicator percentage.
*/
get progress() {
return this.indicator_.progress || 0;
}
/**
* Setter to set the primary text on the panel.
* @param {string} text Text to be shown.
*/
set primaryText(text) {
this.setAttribute('primary-text', text);
}
/**
* Getter for the primary text on the panel.
* @return {string}
*/
get primaryText() {
return this.getAttribute('primary-text');
}
/**
* Setter to set the secondary text on the panel.
* @param {string} text Text to be shown.
*/
set secondaryText(text) {
this.setAttribute('secondary-text', text);
}
/**
* Getter for the secondary text on the panel.
* @return {string}
*/
get secondaryText() {
return this.getAttribute('secondary-text');
}
/**
* Setter to set the panel type.
* @param {number} type Enum value for the panel type.
*/
set panelType(type) {
this.setAttribute('panel-type', type);
}
/**
* Getter for the panel type.
* TODO(crbug.com/947388) Add closure annotations to getters.
*/
get panelType() {
return this.panelType_;
}
/**
* Getter for the primary action button.
*/
get primaryButton() {
return this.shadowRoot.querySelector('#primary-action');
}
/**
* Getter for the secondary action button.
*/
get secondaryButton() {
return this.shadowRoot.querySelector('#secondary-action');
}
/**
* Getter for the panel text div.
*/
get textDiv() {
return this.shadowRoot.querySelector('.xf-panel-text');
}
/**
* Setter to replace the default aria-label on any close button.
* @param {string} text Text to set for the 'aria-label'.
*/
set closeButtonAriaLabel(text) {
const action = this.shadowRoot.querySelector('#secondary-action');
if (action && action.dataset.category === 'cancel') {
action.setAttribute('aria-label', text);
}
}
}
window.customElements.define('xf-panel-item', PanelItem);
//# sourceURL=//ui/file_manager/file_manager/foreground/elements/xf_panel_item.js