coral-spectrum/coral-component-wait/src/scripts/Wait.js
/**
* Copyright 2019 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
import {BaseComponent} from '../../../coral-base-component';
import {transform, validate} from '../../../coral-utils';
import base from '../templates/base';
import {Decorator} from '../../../coral-decorator';
/**
Enumeration for {@link Wait} variants.
@typedef {Object} WaitVariantEnum
@property {String} DEFAULT
The default variant.
@property {String} DOTS
Not supported. Falls back to DEFAULT.
*/
const variant = {
DEFAULT: 'default',
DOTS: 'dots'
};
/**
Enumeration for {@link Wait} sizes.
@typedef {Object} WaitSizeEnum
@property {String} SMALL
A small wait indicator.
@property {String} MEDIUM
A medium wait indicator. This is the default size.
@property {String} LARGE
A large wait indicator.
*/
const size = {
SMALL: 'S',
MEDIUM: 'M',
LARGE: 'L'
};
// the waits's base classname
const CLASSNAME = '_coral-CircleLoader';
/**
@class Coral.Wait
@classdesc A Wait component to be used to indicate a process that is in-progress for an indefinite amount of time.
When the time is known, {@link Progress} should be used instead.
@htmltag coral-wait
@extends {HTMLElement}
@extends {BaseComponent}
*/
const Wait = Decorator(class extends BaseComponent(HTMLElement) {
constructor() {
super();
// Prepare templates
this._elements = {};
base.call(this._elements);
}
/**
The size of the wait indicator. Currently 'S' (the default), 'M' and 'L' are available.
See {@link WaitSizeEnum}.
@type {String}
@default WaitSizeEnum.MEDIUM
@htmlattribute size
@htmlattributereflected
*/
get size() {
return this._size || size.MEDIUM;
}
set size(value) {
value = transform.string(value).toUpperCase();
this._size = validate.enumeration(size)(value) && value || size.MEDIUM;
this._reflectAttribute('size', this._size);
// large css change
this.classList.toggle(`${CLASSNAME}--large`, this._size === size.LARGE);
// small css change
this.classList.toggle(`${CLASSNAME}--small`, this._size === size.SMALL);
}
/**
Whether the component is centered or not. The container needs to have the style <code>position: relative</code>
for the centering to work correctly.
@type {Boolean}
@default false
@htmlattribute centered
@htmlattributereflected
*/
get centered() {
return this._centered || false;
}
set centered(value) {
this._centered = transform.booleanAttr(value);
this._reflectAttribute('centered', this._centered);
this.classList.toggle(`${CLASSNAME}--centered`, this._centered);
}
/**
The wait's variant. See {@link WaitVariantEnum}.
@type {String}
@default WaitVariantEnum.DEFAULT
@htmlattribute variant
@htmlattributereflected
*/
get variant() {
return this._variant || variant.DEFAULT;
}
set variant(value) {
value = transform.string(value).toLowerCase();
this._variant = validate.enumeration(variant)(value) && value || variant.DEFAULT;
this._reflectAttribute('variant', this._variant);
}
/**
Whether to hide the current value and show an animation. Set to true for operations whose progress cannot be
determined.
@type {Boolean}
@default false
@htmlattribute indeterminate
@htmlattributereflected
*/
get indeterminate() {
return this._indeterminate || false;
}
set indeterminate(value) {
this._indeterminate = transform.booleanAttr(value);
this._reflectAttribute('indeterminate', this._indeterminate);
if (this._indeterminate) {
this.classList.add(`${CLASSNAME}--indeterminate`);
// ARIA: Remove attributes
this.removeAttribute('aria-valuenow');
this.removeAttribute('aria-valuemin');
this.removeAttribute('aria-valuemax');
this.value = 0;
} else {
this.classList.remove(`${CLASSNAME}--indeterminate`);
// ARIA: Add attributes
this.setAttribute('aria-valuemin', '0');
this.setAttribute('aria-valuemax', '100');
this.value = this._oldValue;
}
}
/**
The current progress in percent. If no value is set on initialization, wait is forced into indeterminate state.
@type {Number}
@default 0
@emits {coral-wait:change}
@htmlattribute value
@htmlattributereflected
*/
get value() {
return this.hasAttribute('indeterminate') ? 0 : this._value || 0;
}
set value(value) {
value = transform.number(value) || 0;
// Stay within bounds
if (value > 100) {
value = 100;
} else if (value < 0) {
value = 0;
}
this._value = value;
this._reflectAttribute('value', this._value);
const subMask1 = this._elements.subMask1;
const subMask2 = this._elements.subMask2;
if (!this.hasAttribute('indeterminate')) {
let angle;
if (value > 0 && value <= 50) {
angle = -180 + value / 50 * 180;
subMask1.style.transform = `rotate(${angle}deg)`;
subMask2.style.transform = 'rotate(-180deg)';
} else if (value > 50) {
angle = -180 + (value - 50) / 50 * 180;
subMask1.style.transform = 'rotate(0deg)';
subMask2.style.transform = `rotate(${angle}deg)`;
} else {
subMask1.style.transform = '';
subMask2.style.transform = '';
}
// ARIA: Reflect value for screenreaders
this.setAttribute('aria-valuenow', this._value);
this.setAttribute('aria-valuemin', '0');
this.setAttribute('aria-valuemax', '100');
} else {
subMask1.style.transform = '';
subMask2.style.transform = '';
}
this.trigger('coral-wait:change');
}
/**
Returns {@link Wait} sizes.
@return {WaitSizeEnum}
*/
static get size() {
return size;
}
/**
Returns {@link Wait} variants.
@return {WaitVariantEnum}
*/
static get variant() {
return variant;
}
/** @ignore */
static get observedAttributes() {
return super.observedAttributes.concat(['size', 'centered', 'variant', 'value', 'indeterminate']);
}
/** @ignore */
attributeChangedCallback(name, oldValue, value) {
if (name === 'indeterminate' && transform.booleanAttr(value)) {
// Remember current value in case indeterminate is toggled back
this._oldValue = this._value || 0;
}
super.attributeChangedCallback(name, oldValue, value);
}
/** @ignore */
render() {
super.render();
this.classList.add(CLASSNAME);
// ARIA
this.setAttribute('role', 'progressbar');
// Default reflected attributes
if (!this._size) {
this.size = size.MEDIUM;
}
if (!this._variant) {
this.variant = variant.DEFAULT;
}
// If no value is specified, indeterminate is set
if (!this._value) {
this.indeterminate = true;
}
// Centering reads the size
if (this.centered) {
this.centered = this.centered;
}
// Support cloneNode
const template = this.querySelectorAll('._coral-CircleLoader-track, ._coral-CircleLoader-fills');
for (let i = 0 ; i < template.length ; i++) {
template[i].remove();
}
// Render template
this.appendChild(this._elements.track);
this.appendChild(this._elements.fills);
}
/**
Triggered when the {@link Wait} value is changed.
@typedef {CustomEvent} coral-wait:change
*/
});
export default Wait;