chromium/ui/file_manager/file_manager/foreground/elements/xf_circular_progress.ts

// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import {getTemplate} from './xf_circular_progress.html.js';

const MAX_PROGRESS = 100.0;

/**
 * Definition of a circular progress indicator custom element.
 * The element supports two attributes for control - 'radius' and 'progress'.
 * Default radius for the element is 10px, use the DOM API
 *   element.setAttribute('radius', '12px'); to set the radius to 12px;
 * Default progress is 0, progress is a value from 0 to 100, use
 *   element.setAttribute('progress', '50'); to set progress to half complete
 * or alternately, set the 'element.progress' JS property for the same result.
 */
export class CircularProgress extends HTMLElement {
  private fullCircle_ = 63;
  private progress_ = 0.0;

  /**
   * The visual indicator for the progress is accomplished by changing the
   * stroke-dasharray SVG attribute on the top circle. The stroke-dasharray
   * is calculated by using the circumference of the circle as the 100%
   * length and then setting the dash length to match the percentage of
   * the set 'progress_' value.
   */
  private indicator_: SVGElement;
  private errormark_: SVGElement;
  private label_: SVGElement;

  constructor() {
    super();

    const template = document.createElement('template');
    template.innerHTML = getTemplate() as unknown as string;
    const fragment = template.content.cloneNode(true);
    this.attachShadow({mode: 'open'}).appendChild(fragment);

    this.indicator_ = this.shadowRoot!.querySelector<SVGElement>('.top')!;
    this.errormark_ = this.shadowRoot!.querySelector<SVGElement>('.errormark')!;
    this.label_ = this.shadowRoot!.querySelector<SVGElement>('.label')!;
  }

  static get is() {
    return 'xf-circular-progress' as const;
  }

  /**
   * Registers this instance to listen to these attribute changes.
   */
  static get observedAttributes() {
    return [
      'errormark',
      'label',
      'progress',
      'radius',
    ];
  }

  /**
   * Sets the indicators progress position.
   * @param progress A value between 0 and MAX_PROGRESS to indicate.
   */
  setProgress(progress: number): number {
    // Clamp progress to 0 .. MAX_PROGRESS.
    progress = Math.min(Math.max(progress, 0), MAX_PROGRESS);
    const value = (progress / MAX_PROGRESS) * this.fullCircle_;
    this.indicator_?.setAttribute(
        'stroke-dasharray', value + ' ' + this.fullCircle_);
    return progress;
  }

  /**
   * Sets the position of the error indicator.
   * The error indicator is used by the summary panel. Its position is aligned
   * with the top-right square that contains the progress circle itself.
   * @param radius The radius of the progress circle.
   * @param strokeWidth The width of the progress circle stroke.
   */
  private setErrorPosition_(radius: number, strokeWidth: number) {
    const center = 18;
    const x = center + radius + (strokeWidth / 2) - 4;
    const y = center - radius - (strokeWidth / 2) + 4;
    this.errormark_.setAttribute('cx', x.toString());
    this.errormark_.setAttribute('cy', y.toString());
  }

  /**
   * Callback triggered by the browser when our attribute values change.
   * TODO(crbug.com/40620728) Add unit tests to exercise attribute edge cases.
   * @param name Attribute that's changed.
   * @param oldValue Old value of the attribute.
   * @param newValue New value of the attribute.
   */
  attributeChangedCallback(
      name: string, oldValue: null|string, newValue: null|string) {
    if (oldValue === newValue) {
      return;
    }
    switch (name) {
      case 'errormark':
        this.errormark_.setAttribute('visibility', newValue || '');
        break;
      case 'label':
        this.label_.textContent = newValue;
        break;
      case 'radius':
        if (!newValue) {
          break;
        }
        const radius = Number(newValue);
        // Restrict the allowed size to what fits in our area.
        if (radius < 0 || radius > 16.5) {
          return;
        }
        let strokeWidth = 3;
        if (radius > 10) {
          const circles = this.shadowRoot?.querySelector('#circles');
          circles?.setAttribute('stroke-width', '4');
          strokeWidth = 4;
        }
        // Position the error indicator relative to the progress circle.
        this.setErrorPosition_(radius, strokeWidth);
        // Calculate the circumference for the progress dash length.
        this.fullCircle_ = Math.PI * 2 * radius;
        const bottom = this.shadowRoot?.querySelector('.bottom');
        bottom?.setAttribute('r', radius.toString());
        this.indicator_.setAttribute('r', radius.toString());
        this.setProgress(this.progress_);
        break;
      case 'progress':
        const progress = Number(newValue);
        this.progress_ = this.setProgress(progress);
        break;
    }
  }

  /**
   * Getter for the visibility of the error marker.
   */
  get errorMarkerVisibility(): string {
    return this.errormark_.getAttribute('visibility') || '';
  }

  /**
   * Set the visibility of the error marker.
   * @param visibility Visibility value being set.
   */
  set errorMarkerVisibility(visibility: string) {
    // Reflect the progress property into the attribute.
    this.setAttribute('errormark', visibility);
  }

  /**
   * Getter for the current state of the progress indication.
   */
  get progress(): string {
    return this.progress_.toString();
  }

  /**
   * Sets the progress position between 0 and 100.0.
   * @param progress Progress value being set.
   */
  set progress(progress: string) {
    // Reflect the progress property into the attribute.
    this.setAttribute('progress', progress);
  }

  /**
   * Set the text label in the centre of the progress indicator.
   * This is used to indicate multiple operations in progress.
   * @param label Text to place inside the circle.
   */
  set label(label: string) {
    this.setAttribute('label', label);
  }
}

declare global {
  interface HTMLElementTagNameMap {
    [CircularProgress.is]: CircularProgress;
  }
}

window.customElements.define(CircularProgress.is, CircularProgress);