ExamplesPlaygroundReference Source

coral-spectrum/coral-component-progress/src/scripts/Progress.js

  1. /**
  2. * Copyright 2019 Adobe. All rights reserved.
  3. * This file is licensed to you under the Apache License, Version 2.0 (the "License");
  4. * you may not use this file except in compliance with the License. You may obtain a copy
  5. * of the License at http://www.apache.org/licenses/LICENSE-2.0
  6. *
  7. * Unless required by applicable law or agreed to in writing, software distributed under
  8. * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
  9. * OF ANY KIND, either express or implied. See the License for the specific language
  10. * governing permissions and limitations under the License.
  11. */
  12.  
  13. import {BaseComponent} from '../../../coral-base-component';
  14. import base from '../templates/base';
  15. import {commons, transform, validate} from '../../../coral-utils';
  16. import {Decorator} from '../../../coral-decorator';
  17.  
  18. /**
  19. Enumeration for {@link Progress} sizes.
  20.  
  21. @typedef {Object} ProgressSizeEnum
  22.  
  23. @property {String} SMALL
  24. A small progress bar.
  25. @property {String} MEDIUM
  26. A default medium progress bar.
  27. @property {String} LARGE
  28. Not supported. Falls back to MEDIUM.
  29. */
  30. const size = {
  31. SMALL: 'S',
  32. MEDIUM: 'M',
  33. LARGE: 'L'
  34. };
  35.  
  36. /**
  37. Enumeration for {@link Progress} label positions.
  38.  
  39. @typedef {Object} ProgressLabelPositionEnum
  40.  
  41. @property {String} LEFT
  42. Show the label to the left of the bar.
  43. @property {String} SIDE
  44. Show the label to the side of the bar.
  45. @property {String} RIGHT
  46. Not supported. Falls back to LEFT.
  47. @property {String} BOTTOM
  48. Not supported. Falls back to LEFT.
  49. */
  50. const labelPosition = {
  51. LEFT: 'left',
  52. RIGHT: 'right',
  53. SIDE: 'side',
  54. BOTTOM: 'bottom'
  55. };
  56.  
  57. // Base classname
  58. const CLASSNAME = '_coral-BarLoader';
  59.  
  60. /**
  61. @class Coral.Progress
  62. @classdesc A Progress component to indicate progress of processes.
  63. @htmltag coral-progress
  64. @extends {HTMLElement}
  65. @extends {BaseComponent}
  66. */
  67. const Progress = Decorator(class extends BaseComponent(HTMLElement) {
  68. /** @ignore */
  69. constructor() {
  70. super();
  71.  
  72. // Prepare templates
  73. this._elements = {
  74. // Fetch or create the content content zone element
  75. label: this.querySelector('coral-progress-label') || document.createElement('coral-progress-label')
  76. };
  77. base.call(this._elements);
  78.  
  79. // Watch for label changes
  80. this._observer = new MutationObserver(this._toggleLabelVisibility.bind(this));
  81. this._observer.observe(this._elements.label, {
  82. characterData: true,
  83. childList: true,
  84. subtree: true
  85. });
  86. }
  87.  
  88. /**
  89. The current progress in percent.
  90.  
  91. @type {Number}
  92. @default 0
  93. @emits {coral-progress:change}
  94. @htmlattribute value
  95. @htmlattributereflected
  96. */
  97. get value() {
  98. return this.indeterminate ? 0 : this._value || 0;
  99. }
  100.  
  101. set value(value) {
  102. value = transform.number(value) || 0;
  103.  
  104. // Stay within bounds
  105. if (value > 100) {
  106. value = 100;
  107. } else if (value < 0) {
  108. value = 0;
  109. }
  110.  
  111. this._value = value;
  112. this._reflectAttribute('value', this._value);
  113.  
  114. if (!this.indeterminate) {
  115. this._elements.status.style.width = `${this.value}%`;
  116.  
  117. // ARIA: Reflect value for screenreaders
  118. this.setAttribute('aria-valuenow', this._value);
  119.  
  120. if (this.showPercent) {
  121. // Only update label text in percent mode
  122. this._setPercentage(`${this._value}%`);
  123. }
  124. } else {
  125. this._elements.status.style.width = '';
  126. }
  127.  
  128. this.trigger('coral-progress:change');
  129. }
  130.  
  131. /**
  132. Whether to hide the current value and show an animation. Set to true for operations whose progress cannot be
  133. determined.
  134.  
  135. @type {Boolean}
  136. @default false
  137. @htmlattribute indeterminate
  138. @htmlattributereflected
  139. */
  140. get indeterminate() {
  141. return this._indeterminate || false;
  142. }
  143.  
  144. set indeterminate(value) {
  145. this._indeterminate = transform.booleanAttr(value);
  146. this._reflectAttribute('indeterminate', this._indeterminate);
  147.  
  148. if (this._indeterminate) {
  149. this.classList.add(`${CLASSNAME}--indeterminate`);
  150.  
  151. // ARIA: Remove attributes
  152. this.removeAttribute('aria-valuenow');
  153. this.removeAttribute('aria-valuemin');
  154. this.removeAttribute('aria-valuemax');
  155.  
  156. this.value = 0;
  157. } else {
  158. this.classList.remove(`${CLASSNAME}--indeterminate`);
  159.  
  160. // ARIA: Add attributes
  161. this.setAttribute('aria-valuemin', '0');
  162. this.setAttribute('aria-valuemax', '100');
  163.  
  164. this.value = this._oldValue;
  165. }
  166. }
  167.  
  168. /**
  169. The vertical and text size of this progress bar. To adjust the width, simply set the CSS width property.
  170. See {@link ProgressSizeEnum}.
  171.  
  172. @type {String}
  173. @default ProgressSizeEnum.MEDIUM
  174. @htmlattribute size
  175. @htmlattributereflected size
  176. */
  177. get size() {
  178. return this._size || size.MEDIUM;
  179. }
  180.  
  181. set size(value) {
  182. value = transform.string(value).toUpperCase();
  183. this._size = validate.enumeration(size)(value) && value || size.MEDIUM;
  184. this._reflectAttribute('size', this._size);
  185.  
  186. this.classList.toggle(`${CLASSNAME}--small`, this._size === size.SMALL);
  187. }
  188.  
  189. /**
  190. Boolean attribute to toggle showing progress percent as the label content.
  191. Default is true.
  192.  
  193. @type {Boolean}
  194. @default false
  195. @htmlattribute showpercent
  196. */
  197. get showPercent() {
  198. return this._showPercent || false;
  199. }
  200.  
  201. set showPercent(value) {
  202. this._showPercent = transform.booleanAttr(value);
  203. this._reflectAttribute('showpercent', this._showPercent);
  204.  
  205. if (this._showPercent) {
  206. const content = this.indeterminate ? '' : `${this.value}%`;
  207. this._setPercentage(content);
  208. }
  209.  
  210. this._toggleLabelVisibility();
  211. }
  212.  
  213. /**
  214. Used to access to the {@link Coral.Progress.Label} element. Keep in mind that the width of a custom label is
  215. limited for {@link Coral.Progress.labelPosition.LEFT} and {@link Coral.Progress.labelPosition.RIGHT}.
  216.  
  217. @type {ProgressLabel}
  218. @contentzone
  219. */
  220. get label() {
  221. return this._getContentZone(this._elements.label);
  222. }
  223.  
  224. set label(value) {
  225. this._setContentZone('label', value, {
  226. handle: 'label',
  227. tagName: 'coral-progress-label',
  228. insert: function (label) {
  229. label.classList.add(`${CLASSNAME}-label`);
  230. this.appendChild(label);
  231. }
  232. });
  233. }
  234.  
  235. /**
  236. Label position. See {@link ProgressLabelPositionEnum}.
  237.  
  238. @type {String}
  239. @default ProgressLabelPositionEnum.LEFT
  240. @htmlattribute labelposition
  241. @htmlattributereflected
  242. */
  243. get labelPosition() {
  244. return this._labelPosition || labelPosition.LEFT;
  245. }
  246.  
  247. set labelPosition(value) {
  248. value = transform.string(value).toLowerCase();
  249. this._labelPosition = validate.enumeration(labelPosition)(value) && value || labelPosition.LEFT;
  250. this._reflectAttribute('labelposition', this._labelPosition);
  251.  
  252. this.classList.toggle('_coral-BarLoader--sideLabel', this._labelPosition === labelPosition.SIDE);
  253.  
  254. const elements = this.labelPosition === labelPosition.SIDE ? ['label', 'bar', 'percentage'] : ['label', 'percentage', 'bar'];
  255. // @spectrum should be supported with classes
  256. elements.forEach((el, i) => {
  257. this._elements[el].style.order = i;
  258. });
  259.  
  260. this._toggleLabelVisibility();
  261. }
  262.  
  263. /** @ignore */
  264. _toggleLabelVisibility() {
  265. const percentage = this._elements.percentage;
  266. const label = this._elements.label;
  267. const isSidePositioned = this.labelPosition === labelPosition.SIDE;
  268.  
  269. // Handle percentage
  270. if (this.showPercent) {
  271. percentage.style.visibility = 'visible';
  272. percentage.setAttribute('aria-hidden', 'false');
  273.  
  274. if (isSidePositioned) {
  275. percentage.hidden = false;
  276. }
  277. } else {
  278. percentage.style.visibility = 'hidden';
  279. percentage.setAttribute('aria-hidden', 'true');
  280.  
  281. if (isSidePositioned) {
  282. percentage.hidden = true;
  283. }
  284. }
  285.  
  286. // Handle label
  287. if (label.textContent.length > 0) {
  288. label.style.visibility = 'visible';
  289. label.setAttribute('aria-hidden', 'false');
  290.  
  291. if (isSidePositioned) {
  292. label.hidden = false;
  293. }
  294.  
  295. if (!this.showPercent) {
  296. // Update the value for accessibility as it was cleared when the label was hidden
  297. this.setAttribute('aria-valuetext', label.textContent);
  298. }
  299. } else {
  300. label.style.visibility = 'hidden';
  301. label.setAttribute('aria-hidden', 'true');
  302.  
  303. if (isSidePositioned) {
  304. label.hidden = true;
  305. }
  306.  
  307. // Remove the value for accessibility so the screenreader knows we're unlabelled
  308. this.removeAttribute('aria-valuetext');
  309. }
  310. }
  311.  
  312. /** @ignore */
  313. _setPercentage(content) {
  314. this._elements.percentage.textContent = content;
  315.  
  316. // ARIA
  317. this[this.showPercent ? 'removeAttribute' : 'setAttribute']('aria-valuetext', content);
  318. }
  319.  
  320. get _contentZones() {
  321. return {'coral-progress-label': 'label'};
  322. }
  323.  
  324. /**
  325. Returns {@link Progress} label position options.
  326.  
  327. @return {ProgressLabelPositionEnum}
  328. */
  329. static get labelPosition() {
  330. return labelPosition;
  331. }
  332.  
  333. /**
  334. Returns {@link Progress} sizes.
  335.  
  336. @return {ProgressSizeEnum}
  337. */
  338. static get size() {
  339. return size;
  340. }
  341.  
  342. static get _attributePropertyMap() {
  343. return commons.extend(super._attributePropertyMap, {
  344. showpercent: 'showPercent',
  345. labelposition: 'labelPosition'
  346. });
  347. }
  348.  
  349. /** @ignore */
  350. static get observedAttributes() {
  351. return super.observedAttributes.concat([
  352. 'value',
  353. 'indeterminate',
  354. 'size',
  355. 'showpercent',
  356. 'labelposition'
  357. ]);
  358. }
  359.  
  360. /** @ignore */
  361. attributeChangedCallback(name, oldValue, value) {
  362. if (name === 'indeterminate' && transform.booleanAttr(value)) {
  363. // Remember current value in case indeterminate is toggled
  364. this._oldValue = this._value || 0;
  365. }
  366.  
  367. super.attributeChangedCallback(name, oldValue, value);
  368. }
  369.  
  370. /** @ignore */
  371. render() {
  372. super.render();
  373.  
  374. this.classList.add(CLASSNAME);
  375.  
  376. // Default reflected attributes
  377. if (!this._value) {
  378. this.value = this.value;
  379. }
  380. if (!this._size) {
  381. this.size = size.MEDIUM;
  382. }
  383. if (!this._labelPosition) {
  384. this.labelPosition = labelPosition.LEFT;
  385. }
  386.  
  387. // Create a fragment
  388. const fragment = document.createDocumentFragment();
  389.  
  390. const templateHandleNames = ['bar', 'percentage'];
  391.  
  392. // Render the template
  393. fragment.appendChild(this._elements.percentage);
  394. fragment.appendChild(this._elements.bar);
  395.  
  396. const label = this._elements.label;
  397.  
  398. // Remove it so we can process children
  399. if (label.parentNode) {
  400. label.parentNode.removeChild(label);
  401. }
  402.  
  403. // Move any remaining elements into the content sub-component
  404. while (this.firstChild) {
  405. const child = this.firstChild;
  406. if (child.nodeType === Node.TEXT_NODE ||
  407. child.nodeType === Node.ELEMENT_NODE && templateHandleNames.indexOf(child.getAttribute('handle')) === -1) {
  408. // Add non-template elements to the label
  409. label.appendChild(child);
  410. } else {
  411. // Remove anything else
  412. this.removeChild(child);
  413. }
  414. }
  415.  
  416. // Add the frag to the component
  417. this.appendChild(fragment);
  418.  
  419. // Assign the content zone
  420. this.label = label;
  421.  
  422. // Toggle label based on content
  423. this._toggleLabelVisibility();
  424.  
  425. // ARIA
  426. this.setAttribute('role', 'progressbar');
  427. this.setAttribute('aria-valuenow', '0');
  428. this.setAttribute('aria-valuemin', '0');
  429. this.setAttribute('aria-valuemax', '100');
  430. }
  431.  
  432. /**
  433. Triggered when the {@link Progress} value is changed.
  434.  
  435. @typedef {CustomEvent} coral-progress:change
  436. */
  437. });
  438.  
  439. export default Progress;