chromium/ui/file_manager/file_manager/foreground/js/ui/banners/warning_banner.ts

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

import {visitURL} from '../../../../common/js/util.js';

import {type AllowedVolumeOrType, Banner, BannerEvent} from './types.js';
import {getTemplate} from './warning_banner.html.js';

/**
 * WarningBanner is a type of banner that is highest priority and is used to
 * showcase potential underlying issues for the filesystem (e.g. low disk space)
 * or that are contextually relevant (e.g. Google Drive is offline).
 *
 * To implement a WarningBanner, extend from this banner and override the
 * allowedVolumes method where you want the warning message shown. The
 * connectedCallback method can be used to set the warning text and an optional
 * link to provide more information. All other configuration elements are
 * optional and can be found documented on the Banner extern.
 *
 * For example the following banner will show when a user navigates to the
 * Downloads volume type:
 *
 *    class ConcreteWarningBanner extends WarningBanner {
 *      allowedVolumes() {
 *        return [{type: VolumeType.DOWNLOADS}];
 *      }
 *    }
 *
 * Create a HTML template with the same file name as the banner and override
 * the text using slots with the content that you want:
 *
 *    <warning-banner>
 *      <span slot="text">Warning Banner text</span>
 *      <cr-button slot="extra-button" href="{{url_to_navigate}}">
 *        Extra button text
 *      </cr-button>
 *    </warning-banner>
 */
export class WarningBanner extends Banner {
  constructor() {
    super();

    const fragment = this.getTemplate();
    this.attachShadow({mode: 'open'}).appendChild(fragment);
  }

  /**
   * Returns the HTML template for the Warning Banner.
   */
  override getTemplate() {
    const template = document.createElement('template');
    template.innerHTML = getTemplate() as unknown as string;
    const fragment = template.content.cloneNode(true);
    return fragment;
  }

  /**
   * Called when the web component is connected to the DOM. This will be called
   * for both the inner warning-banner component and the concrete
   * implementations that extend from it.
   */
  override connectedCallback() {
    // If a WarningBanner subclass overrides the default dismiss button, the
    // button will not exist in the shadowRoot. Add the event listener to the
    // overridden dismiss button first and fall back to the default button if
    // no overridden button.
    const overridenDismissButton =
        this.querySelector('[slot="dismiss-button"]');
    const defaultDismissButton =
        this.shadowRoot!.querySelector('#dismiss-button');
    if (overridenDismissButton) {
      overridenDismissButton.addEventListener(
          'click', this.onDismissClickHandler_.bind(this));
    } else if (defaultDismissButton) {
      defaultDismissButton.addEventListener(
          'click', this.onDismissClickHandler_.bind(this));
    }

    // Attach an onclick handler to the extra-button slot. This enables a new
    // element to leverage the href tag on the element to have a URL opened.
    // TODO(crbug.com/40189485): Add UMA trigger to capture number of extra
    // button clicks.
    const extraButton = this.querySelector('[slot="extra-button"]');
    if (extraButton) {
      extraButton.addEventListener('click', (e) => {
        if (extraButton.getAttribute('href')) {
          visitURL(extraButton.getAttribute('href')!);
        }
        e.preventDefault();
      });
    }
  }

  /**
   * When a WarningBanner is dismissed, do not show it again for another 36
   * hours.
   */
  override hideAfterDismissedDurationSeconds() {
    return 36 * 60 * 60;  // 36 hours, 129,600 seconds.
  }

  /**
   * All banners that inherit this class should override with their own
   * volume types to allow. Setting this explicitly as an empty array ensures
   * banners that don't override this are not shown by default.
   */
  override allowedVolumes(): AllowedVolumeOrType[] {
    return [];
  }

  /**
   * Handler for the dismiss button on click, switches to the custom banner
   * dismissal event to ensure the controller can catch the event.
   */
  private onDismissClickHandler_(_: Event) {
    const parent =
        this.getRootNode() && (this.getRootNode() as ShadowRoot).host;
    let bannerInstance: WarningBanner = this;
    // In the case the warning-banner web component is not the root node (e.g.
    // it is contained within another web component) prefer the outer component.
    if (parent && parent instanceof WarningBanner) {
      bannerInstance = parent;
    }
    this.dispatchEvent(new CustomEvent(
        BannerEvent.BANNER_DISMISSED,
        {bubbles: true, composed: true, detail: {banner: bannerInstance}}));
  }
}

declare global {
  interface HTMLElementTagNameMap {
    'warning-banner': WarningBanner;
  }
}

customElements.define('warning-banner', WarningBanner);