coral-spectrum/coral-component-progress/src/scripts/Progress.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 base from '../templates/base';
- import {commons, transform, validate} from '../../../coral-utils';
- import {Decorator} from '../../../coral-decorator';
-
- /**
- Enumeration for {@link Progress} sizes.
-
- @typedef {Object} ProgressSizeEnum
-
- @property {String} SMALL
- A small progress bar.
- @property {String} MEDIUM
- A default medium progress bar.
- @property {String} LARGE
- Not supported. Falls back to MEDIUM.
- */
- const size = {
- SMALL: 'S',
- MEDIUM: 'M',
- LARGE: 'L'
- };
-
- /**
- Enumeration for {@link Progress} label positions.
-
- @typedef {Object} ProgressLabelPositionEnum
-
- @property {String} LEFT
- Show the label to the left of the bar.
- @property {String} SIDE
- Show the label to the side of the bar.
- @property {String} RIGHT
- Not supported. Falls back to LEFT.
- @property {String} BOTTOM
- Not supported. Falls back to LEFT.
- */
- const labelPosition = {
- LEFT: 'left',
- RIGHT: 'right',
- SIDE: 'side',
- BOTTOM: 'bottom'
- };
-
- // Base classname
- const CLASSNAME = '_coral-BarLoader';
-
- /**
- @class Coral.Progress
- @classdesc A Progress component to indicate progress of processes.
- @htmltag coral-progress
- @extends {HTMLElement}
- @extends {BaseComponent}
- */
- const Progress = Decorator(class extends BaseComponent(HTMLElement) {
- /** @ignore */
- constructor() {
- super();
-
- // Prepare templates
- this._elements = {
- // Fetch or create the content content zone element
- label: this.querySelector('coral-progress-label') || document.createElement('coral-progress-label')
- };
- base.call(this._elements);
-
- // Watch for label changes
- this._observer = new MutationObserver(this._toggleLabelVisibility.bind(this));
- this._observer.observe(this._elements.label, {
- characterData: true,
- childList: true,
- subtree: true
- });
- }
-
- /**
- The current progress in percent.
-
- @type {Number}
- @default 0
- @emits {coral-progress:change}
- @htmlattribute value
- @htmlattributereflected
- */
- get value() {
- return this.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);
-
- if (!this.indeterminate) {
- this._elements.status.style.width = `${this.value}%`;
-
- // ARIA: Reflect value for screenreaders
- this.setAttribute('aria-valuenow', this._value);
-
- if (this.showPercent) {
- // Only update label text in percent mode
- this._setPercentage(`${this._value}%`);
- }
- } else {
- this._elements.status.style.width = '';
- }
-
- this.trigger('coral-progress:change');
- }
-
- /**
- 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 vertical and text size of this progress bar. To adjust the width, simply set the CSS width property.
- See {@link ProgressSizeEnum}.
-
- @type {String}
- @default ProgressSizeEnum.MEDIUM
- @htmlattribute size
- @htmlattributereflected size
- */
- 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);
-
- this.classList.toggle(`${CLASSNAME}--small`, this._size === size.SMALL);
- }
-
- /**
- Boolean attribute to toggle showing progress percent as the label content.
- Default is true.
-
- @type {Boolean}
- @default false
- @htmlattribute showpercent
- */
- get showPercent() {
- return this._showPercent || false;
- }
-
- set showPercent(value) {
- this._showPercent = transform.booleanAttr(value);
- this._reflectAttribute('showpercent', this._showPercent);
-
- if (this._showPercent) {
- const content = this.indeterminate ? '' : `${this.value}%`;
- this._setPercentage(content);
- }
-
- this._toggleLabelVisibility();
- }
-
- /**
- Used to access to the {@link Coral.Progress.Label} element. Keep in mind that the width of a custom label is
- limited for {@link Coral.Progress.labelPosition.LEFT} and {@link Coral.Progress.labelPosition.RIGHT}.
-
- @type {ProgressLabel}
- @contentzone
- */
- get label() {
- return this._getContentZone(this._elements.label);
- }
-
- set label(value) {
- this._setContentZone('label', value, {
- handle: 'label',
- tagName: 'coral-progress-label',
- insert: function (label) {
- label.classList.add(`${CLASSNAME}-label`);
- this.appendChild(label);
- }
- });
- }
-
- /**
- Label position. See {@link ProgressLabelPositionEnum}.
-
- @type {String}
- @default ProgressLabelPositionEnum.LEFT
- @htmlattribute labelposition
- @htmlattributereflected
- */
- get labelPosition() {
- return this._labelPosition || labelPosition.LEFT;
- }
-
- set labelPosition(value) {
- value = transform.string(value).toLowerCase();
- this._labelPosition = validate.enumeration(labelPosition)(value) && value || labelPosition.LEFT;
- this._reflectAttribute('labelposition', this._labelPosition);
-
- this.classList.toggle('_coral-BarLoader--sideLabel', this._labelPosition === labelPosition.SIDE);
-
- const elements = this.labelPosition === labelPosition.SIDE ? ['label', 'bar', 'percentage'] : ['label', 'percentage', 'bar'];
- // @spectrum should be supported with classes
- elements.forEach((el, i) => {
- this._elements[el].style.order = i;
- });
-
- this._toggleLabelVisibility();
- }
-
- /** @ignore */
- _toggleLabelVisibility() {
- const percentage = this._elements.percentage;
- const label = this._elements.label;
- const isSidePositioned = this.labelPosition === labelPosition.SIDE;
-
- // Handle percentage
- if (this.showPercent) {
- percentage.style.visibility = 'visible';
- percentage.setAttribute('aria-hidden', 'false');
-
- if (isSidePositioned) {
- percentage.hidden = false;
- }
- } else {
- percentage.style.visibility = 'hidden';
- percentage.setAttribute('aria-hidden', 'true');
-
- if (isSidePositioned) {
- percentage.hidden = true;
- }
- }
-
- // Handle label
- if (label.textContent.length > 0) {
- label.style.visibility = 'visible';
- label.setAttribute('aria-hidden', 'false');
-
- if (isSidePositioned) {
- label.hidden = false;
- }
-
- if (!this.showPercent) {
- // Update the value for accessibility as it was cleared when the label was hidden
- this.setAttribute('aria-valuetext', label.textContent);
- }
- } else {
- label.style.visibility = 'hidden';
- label.setAttribute('aria-hidden', 'true');
-
- if (isSidePositioned) {
- label.hidden = true;
- }
-
- // Remove the value for accessibility so the screenreader knows we're unlabelled
- this.removeAttribute('aria-valuetext');
- }
- }
-
- /** @ignore */
- _setPercentage(content) {
- this._elements.percentage.textContent = content;
-
- // ARIA
- this[this.showPercent ? 'removeAttribute' : 'setAttribute']('aria-valuetext', content);
- }
-
- get _contentZones() {
- return {'coral-progress-label': 'label'};
- }
-
- /**
- Returns {@link Progress} label position options.
-
- @return {ProgressLabelPositionEnum}
- */
- static get labelPosition() {
- return labelPosition;
- }
-
- /**
- Returns {@link Progress} sizes.
-
- @return {ProgressSizeEnum}
- */
- static get size() {
- return size;
- }
-
- static get _attributePropertyMap() {
- return commons.extend(super._attributePropertyMap, {
- showpercent: 'showPercent',
- labelposition: 'labelPosition'
- });
- }
-
- /** @ignore */
- static get observedAttributes() {
- return super.observedAttributes.concat([
- 'value',
- 'indeterminate',
- 'size',
- 'showpercent',
- 'labelposition'
- ]);
- }
-
- /** @ignore */
- attributeChangedCallback(name, oldValue, value) {
- if (name === 'indeterminate' && transform.booleanAttr(value)) {
- // Remember current value in case indeterminate is toggled
- this._oldValue = this._value || 0;
- }
-
- super.attributeChangedCallback(name, oldValue, value);
- }
-
- /** @ignore */
- render() {
- super.render();
-
- this.classList.add(CLASSNAME);
-
- // Default reflected attributes
- if (!this._value) {
- this.value = this.value;
- }
- if (!this._size) {
- this.size = size.MEDIUM;
- }
- if (!this._labelPosition) {
- this.labelPosition = labelPosition.LEFT;
- }
-
- // Create a fragment
- const fragment = document.createDocumentFragment();
-
- const templateHandleNames = ['bar', 'percentage'];
-
- // Render the template
- fragment.appendChild(this._elements.percentage);
- fragment.appendChild(this._elements.bar);
-
- const label = this._elements.label;
-
- // Remove it so we can process children
- if (label.parentNode) {
- label.parentNode.removeChild(label);
- }
-
- // Move any remaining elements into the content sub-component
- while (this.firstChild) {
- const child = this.firstChild;
- if (child.nodeType === Node.TEXT_NODE ||
- child.nodeType === Node.ELEMENT_NODE && templateHandleNames.indexOf(child.getAttribute('handle')) === -1) {
- // Add non-template elements to the label
- label.appendChild(child);
- } else {
- // Remove anything else
- this.removeChild(child);
- }
- }
-
- // Add the frag to the component
- this.appendChild(fragment);
-
- // Assign the content zone
- this.label = label;
-
- // Toggle label based on content
- this._toggleLabelVisibility();
-
- // ARIA
- this.setAttribute('role', 'progressbar');
- this.setAttribute('aria-valuenow', '0');
- this.setAttribute('aria-valuemin', '0');
- this.setAttribute('aria-valuemax', '100');
- }
-
- /**
- Triggered when the {@link Progress} value is changed.
-
- @typedef {CustomEvent} coral-progress:change
- */
- });
-
- export default Progress;