chromium/chrome/browser/resources/side_panel/customize_chrome/app.ts

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

import 'chrome://customize-chrome-side-panel.top-chrome/shared/sp_heading.js';
import 'chrome://resources/cr_components/help_bubble/new_badge.js';
import 'chrome://resources/cr_elements/cr_chip/cr_chip.js';
import 'chrome://resources/cr_elements/cr_icon/cr_icon.js';
import 'chrome://resources/cr_elements/cr_page_selector/cr_page_selector.js';
import 'chrome://resources/cr_elements/icons_lit.html.js';
import './appearance.js';
import './cards.js';
import './categories.js';
import './customize_toolbar/toolbar.js';
import './shortcuts.js';
import './themes.js';
import './wallpaper_search/wallpaper_search.js';

import {ColorChangeUpdater} from 'chrome://resources/cr_components/color_change_listener/colors_css_updater.js';
import {HelpBubbleMixinLit} from 'chrome://resources/cr_components/help_bubble/help_bubble_mixin_lit.js';
import {assert} from 'chrome://resources/js/assert.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {CrLitElement} from 'chrome://resources/lit/v3_0/lit.rollup.js';

import {getCss} from './app.css.js';
import {getHtml} from './app.html.js';
import type {AppearanceElement} from './appearance.js';
import type {CategoriesElement} from './categories.js';
import {CustomizeChromeImpression, recordCustomizeChromeImpression} from './common.js';
import type {BackgroundCollection, CustomizeChromePageHandlerInterface} from './customize_chrome.mojom-webui.js';
import {ChromeWebStoreCategory, ChromeWebStoreCollection, CustomizeChromeSection} from './customize_chrome.mojom-webui.js';
import {CustomizeChromeApiProxy} from './customize_chrome_api_proxy.js';
import type {ThemesElement} from './themes.js';

const SECTION_TO_SELECTOR = {
  [CustomizeChromeSection.kAppearance]: '#appearance',
  [CustomizeChromeSection.kShortcuts]: '#shortcuts',
  [CustomizeChromeSection.kModules]: '#modules',
};

const CHANGE_CHROME_THEME_BUTTON_ELEMENT_ID =
    'CustomizeChromeUI::kChangeChromeThemeButtonElementId';

export enum CustomizeChromePage {
  OVERVIEW = 'overview',
  CATEGORIES = 'categories',
  THEMES = 'themes',
  TOOLBAR = 'toolbar',
  WALLPAPER_SEARCH = 'wallpaper-search',
}

const AppElementBase = HelpBubbleMixinLit(CrLitElement);

export interface AppElement {
  $: {
    overviewPage: HTMLDivElement,
    categoriesPage: CategoriesElement,
    themesPage: ThemesElement,
    appearanceElement: AppearanceElement,
  };
}

export class AppElement extends AppElementBase {
  static get is() {
    return 'customize-chrome-app';
  }

  static override get styles() {
    return getCss();
  }

  override render() {
    return getHtml.bind(this)();
  }

  static override get properties() {
    return {
      page_: {type: String},
      modulesEnabled_: {type: Boolean},
      selectedCollection_: {type: Object},
      extensionsCardEnabled_: {type: Boolean},
      wallpaperSearchEnabled_: {type: Boolean},
      toolbarCustomizationEnabled_: {type: Boolean},
      isSourceTabFirstPartyNtp_: {type: Boolean},
    };
  }

  override firstUpdated() {
    ColorChangeUpdater.forDocument().start();
    this.registerHelpBubble(
        CHANGE_CHROME_THEME_BUTTON_ELEMENT_ID,
        ['#appearanceElement', '#editThemeButton']);
  }

  protected page_: CustomizeChromePage = CustomizeChromePage.OVERVIEW;
  protected modulesEnabled_: boolean =
      loadTimeData.getBoolean('modulesEnabled');
  protected selectedCollection_: BackgroundCollection|null = null;
  protected extensionsCardEnabled_: boolean =
      loadTimeData.getBoolean('extensionsCardEnabled');
  protected wallpaperSearchEnabled_: boolean =
      loadTimeData.getBoolean('wallpaperSearchEnabled');
  protected toolbarCustomizationEnabled_: boolean =
      loadTimeData.getBoolean('toolbarCustomizationEnabled');
  protected isSourceTabFirstPartyNtp_: boolean = true;
  private scrollToSectionListenerId_: number|null = null;
  private attachedTabStateUpdatedId_: number|null = null;
  private pageHandler_: CustomizeChromePageHandlerInterface =
      CustomizeChromeApiProxy.getInstance().handler;

  override connectedCallback() {
    super.connectedCallback();
    this.scrollToSectionListenerId_ =
        CustomizeChromeApiProxy.getInstance()
            .callbackRouter.scrollToSection.addListener(
                (section: CustomizeChromeSection) => {
                  if (section === CustomizeChromeSection.kWallpaperSearch) {
                    this.onWallpaperSearchSelect_();
                    return;
                  } else if (section === CustomizeChromeSection.kToolbar) {
                    this.openToolbarCustomizationPage();
                    chrome.metricsPrivate.recordUserAction(
                        'Actions.CustomizeToolbarSidePanel' +
                        '.OpenedFromOutsideCustomizeChrome');
                    return;
                  }
                  const selector = SECTION_TO_SELECTOR[section];
                  const element = this.shadowRoot!.querySelector(selector);
                  if (!element) {
                    return;
                  }
                  this.page_ = CustomizeChromePage.OVERVIEW;
                  element.scrollIntoView({behavior: 'auto'});
                });

    this.attachedTabStateUpdatedId_ =
        CustomizeChromeApiProxy.getInstance()
            .callbackRouter.attachedTabStateUpdated.addListener(
                (isSourceTabFirstPartyNtp: boolean) => {
                  if (this.isSourceTabFirstPartyNtp_ ===
                      isSourceTabFirstPartyNtp) {
                    return;
                  }

                  this.isSourceTabFirstPartyNtp_ = isSourceTabFirstPartyNtp;

                  // Since some pages aren't supported in non first party mode,
                  // change the section back to the overview.
                  if (!this.isSourceTabFirstPartyNtp_ &&
                      !this.pageSupportedOnNonFirstPartyNtps()) {
                    this.page_ = CustomizeChromePage.OVERVIEW;
                  }
                });
    this.pageHandler_.updateAttachedTabState();

    // We wait for load because `scrollIntoView` above requires the page to be
    // laid out.
    window.addEventListener('load', () => {
      CustomizeChromeApiProxy.getInstance().handler.updateScrollToSection();
      // Install observer to log extension cards impression.
      const extensionsCardSectionObserver =
          new IntersectionObserver(entries => {
            assert(entries.length >= 1);
            if (entries[0]!.intersectionRatio >= 0.8) {
              extensionsCardSectionObserver.disconnect();
              this.dispatchEvent(
                  new Event('detect-extensions-card-section-impression'));
              recordCustomizeChromeImpression(
                  CustomizeChromeImpression.EXTENSIONS_CARD_SECTION_DISPLAYED);
            }
          }, {
            threshold: 1.0,
          });
      // Start observing if extension cards are scroll into view.
      if (this.shadowRoot && this.shadowRoot.querySelector('#extensions')) {
        extensionsCardSectionObserver.observe(
            this.shadowRoot!.querySelector('#extensions')!);
      }
    }, {once: true});
  }

  override disconnectedCallback() {
    super.disconnectedCallback();

    assert(this.scrollToSectionListenerId_);
    CustomizeChromeApiProxy.getInstance().callbackRouter.removeListener(
        this.scrollToSectionListenerId_);

    assert(this.attachedTabStateUpdatedId_);
    CustomizeChromeApiProxy.getInstance().callbackRouter.removeListener(
        this.attachedTabStateUpdatedId_);
  }

  protected async onBackClick_() {
    switch (this.page_) {
      case CustomizeChromePage.CATEGORIES:
      case CustomizeChromePage.TOOLBAR:
        this.page_ = CustomizeChromePage.OVERVIEW;
        await this.updateComplete;
        this.$.appearanceElement.focusOnThemeButton();
        break;
      case CustomizeChromePage.THEMES:
      case CustomizeChromePage.WALLPAPER_SEARCH:
        this.page_ = CustomizeChromePage.CATEGORIES;
        await this.updateComplete;
        this.$.categoriesPage.focusOnBackButton();
        break;
    }
  }

  protected async onEditThemeClick_() {
    this.page_ = CustomizeChromePage.CATEGORIES;
    await this.updateComplete;
    this.$.categoriesPage.focusOnBackButton();
  }

  protected async onCollectionSelect_(event:
                                          CustomEvent<BackgroundCollection>) {
    this.selectedCollection_ = event.detail;
    this.page_ = CustomizeChromePage.THEMES;
    await this.updateComplete;
    this.$.themesPage.focusOnBackButton();
  }

  protected async onLocalImageUpload_() {
    this.page_ = CustomizeChromePage.OVERVIEW;
    await this.updateComplete;
    this.$.appearanceElement.focusOnThemeButton();
  }

  protected onWallpaperSearchSelect_() {
    this.page_ = CustomizeChromePage.WALLPAPER_SEARCH;
    const page =
        this.shadowRoot!.querySelector('customize-chrome-wallpaper-search');
    assert(page);
    page.focusOnBackButton();
  }

  protected onCouponsButtonClick_() {
    this.pageHandler_.openChromeWebStoreCategoryPage(
        ChromeWebStoreCategory.kShopping);
  }

  protected onWritingButtonClick_() {
    this.pageHandler_.openChromeWebStoreCollectionPage(
        ChromeWebStoreCollection.kWritingEssentials);
  }

  protected onProductivityButtonClick_() {
    this.pageHandler_.openChromeWebStoreCategoryPage(
        ChromeWebStoreCategory.kWorkflowPlanning);
  }

  protected onChromeWebStoreLinkClick_(e: Event) {
    if ((e.target as HTMLElement).id !== 'chromeWebstoreLink') {
      // Ignore any clicks that are not directly on the <a> element itself. Note
      // that the <a> element is part of a localized string, which is why the
      // listener is added on the parent DOM node.
      return;
    }

    this.pageHandler_.openChromeWebStoreHomePage();
  }

  protected onToolbarCustomizationButtonClick_() {
    this.openToolbarCustomizationPage();
    chrome.metricsPrivate.recordUserAction(
        'Actions.CustomizeToolbarSidePanel.OpenedFromCustomizeChrome');
  }

  private async openToolbarCustomizationPage() {
    this.page_ = CustomizeChromePage.TOOLBAR;
    const page = this.shadowRoot!.querySelector('customize-chrome-toolbar');
    assert(page);
    await this.updateComplete;
    page.focusOnBackButton();
  }

  private pageSupportedOnNonFirstPartyNtps() {
    return this.page_ === CustomizeChromePage.TOOLBAR;
  }
}

declare global {
  interface HTMLElementTagNameMap {
    'customize-chrome-app': AppElement;
  }
}

customElements.define(AppElement.is, AppElement);