chromium/chrome/browser/resources/ash/settings/os_a11y_page/change_dictation_locale_dialog.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.

/**
 * @fileoverview This dialog holds a Dictation locale selection pane that
 * allows a user to pick their locale for Dictation's speech recognition.
 */

import 'chrome://resources/ash/common/cr_elements/cr_button/cr_button.js';
import 'chrome://resources/ash/common/cr_elements/cr_dialog/cr_dialog.js';
import 'chrome://resources/ash/common/cr_elements/cr_search_field/cr_search_field.js';
import 'chrome://resources/polymer/v3_0/iron-flex-layout/iron-flex-layout-classes.js';
import 'chrome://resources/polymer/v3_0/iron-list/iron-list.js';
import 'chrome://resources/polymer/v3_0/paper-ripple/paper-ripple.js';
import '../settings_shared.css.js';
import '../os_languages_page/shared_style.css.js';

import {CrButtonElement} from 'chrome://resources/ash/common/cr_elements/cr_button/cr_button.js';
import {CrDialogElement} from 'chrome://resources/ash/common/cr_elements/cr_dialog/cr_dialog.js';
import {CrSearchFieldElement} from 'chrome://resources/ash/common/cr_elements/cr_search_field/cr_search_field.js';
import {I18nMixin} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js';
import {IronListElement} from 'chrome://resources/polymer/v3_0/iron-list/iron-list.js';
import {afterNextRender, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

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

/**
 * A locale option for Dictation, including the human-readable name, the
 * locale value (like en-US), whether it works offline, whether the language
 * pack for the locale is installed, and whether it should be highlighted as
 * recommended to the user.
 */
export interface DictationLocaleOption {
  name: string;
  value: string;
  worksOffline: boolean;
  installed: boolean;
  recommended: boolean;
}

export interface ChangeDictationLocaleDialog {
  $: {
    allLocalesList: IronListElement,
    changeDictationLocaleDialog: CrDialogElement,
    recommendedLocalesList: IronListElement,
    search: CrSearchFieldElement,
    cancel: CrButtonElement,
    update: CrButtonElement,
  };
}

const ChangeDictationLocaleDialogBase = I18nMixin(PolymerElement);

export class ChangeDictationLocaleDialog extends
    ChangeDictationLocaleDialogBase {
  static get is() {
    return 'os-settings-change-dictation-locale-dialog' as const;
  }

  static get template() {
    return getTemplate();
  }

  static get properties() {
    return {
      /**
       * Set by the manage OS a11y page, this is the full list of locales
       * available.
       */
      options: Array,

      /**
       * Preference associated with Dictation locales.
       */
      pref: Object,

      displayedLocales_: {
        type: Array,
        computed: `getAllDictationLocales_(options, lowercaseQueryString_)`,
      },

      recommendedLocales_: {
        type: Array,
        computed:
            `getRecommendedDictationLocales_(options, lowercaseQueryString_)`,
      },

      /**
       * Whether any locales are displayed.
       */
      displayedLocalesEmpty_: {
        type: Boolean,
        computed: 'isZero_(displayedLocales_.length)',
      },

      /**
       * Whether any locales are displayed.
       */
      recommendedLocalesEmpty_: {
        type: Boolean,
        computed: 'isZero_(recommendedLocales_.length)',
      },

      /**
       * Whether to enable the button to update the locale pref.
       */
      disableUpdateButton_: {
        type: Boolean,
        computed: 'shouldDisableActionButton_(selectedLocale_)',
      },

      lowercaseQueryString_: {
        type: String,
        value: '',
      },

      /**
       * The currently selected locale from the recommended locales list.
       */
      selectedRecommendedLocale_: {
        type: Object,
        value: null,
      },

      /**
       * The currently selected locale from the full locales list.
       */
      selectedLocale_: {
        type: Object,
        value: null,
      },
    };
  }

  options: DictationLocaleOption[];
  pref: chrome.settingsPrivate.PrefObject<string>;
  private disableUpdateButton_: boolean;
  private displayedLocales_: DictationLocaleOption[];
  private displayedLocalesEmpty_: boolean;
  private lowercaseQueryString_: string;
  private recommendedLocales_: DictationLocaleOption[];
  private recommendedLocalesEmpty_: boolean;
  private selectedLocale_: DictationLocaleOption|null;
  private selectedRecommendedLocale_: DictationLocaleOption|null;

  override ready(): void {
    super.ready();
    this.addEventListener('exit-pane', () => this.onPaneExit_());
  }

  override connectedCallback(): void {
    super.connectedCallback();

    // Sets offset in iron-list that uses the body as a scrollTarget.
    afterNextRender(this, () => {
      this.$.allLocalesList.scrollOffset = this.$.allLocalesList.offsetTop;
    });
  }

  /**
   * Gets the list of all recommended Dictation locales based on the current
   * search.
   */
  private getRecommendedDictationLocales_(): DictationLocaleOption[] {
    return this.getPossibleDictationLocales_(/*recommendedOnly=*/ true);
  }

  /**
   * Gets the list of all possible Dictation locales based on the current
   * search.
   */
  private getAllDictationLocales_(): DictationLocaleOption[] {
    return this.getPossibleDictationLocales_(/*recommendedOnly=*/ false);
  }

  private getPossibleDictationLocales_(recommendedOnly: boolean):
      DictationLocaleOption[] {
    return this.options
        .filter(option => {
          // Filter recommended options. The currently selected option is also
          // recommended.
          if (recommendedOnly &&
              !(this.pref.value === option.value || option.recommended)) {
            return false;
          }
          return !this.lowercaseQueryString_ ||
              option.name.toLowerCase().includes(this.lowercaseQueryString_) ||
              option.value.toLowerCase().includes(this.lowercaseQueryString_);
        })
        .sort((first, second) => {
          return first.name.localeCompare(second.name);
        });
  }

  /**
   * |selectedRecommendedLocale_| is not changed by the time this is called. The
   * value that |selectedRecommendedLocale_| will be assigned to is stored in
   * |this.$.recommendedLocalesList.selectedItem|.
   */
  private selectedRecommendedLocaleChanged_(): void {
    const allLocalesSelected =
        this.$.allLocalesList.selectedItem as DictationLocaleOption | null;
    const recommendedLocalesSelected =
        this.$.recommendedLocalesList.selectedItem as DictationLocaleOption |
        null;

    // Check for equality before updating to avoid an infinite loop with
    // selectedLocaleChanged_().
    if (allLocalesSelected === recommendedLocalesSelected) {
      return;
    }
    if (recommendedLocalesSelected) {
      this.$.allLocalesList.selectItem(recommendedLocalesSelected);
    } else {
      this.$.allLocalesList.deselectItem(allLocalesSelected);
    }
  }

  /**
   * |selectedLocale_| is not changed by the time this is called. The value that
   * |selectedLocale_| will be assigned to is stored in
   * |this.$.allLocalesList.selectedItem|.
   */
  private selectedLocaleChanged_(): void {
    const allLocalesSelected =
        this.$.allLocalesList.selectedItem as DictationLocaleOption | null;
    const recommendedLocalesSelected =
        this.$.recommendedLocalesList.selectedItem as DictationLocaleOption |
        null;

    if (allLocalesSelected === recommendedLocalesSelected) {
      return;
    }
    // Check if the locale is also in the recommended list.
    if (allLocalesSelected?.recommended) {
      this.$.recommendedLocalesList.selectItem(allLocalesSelected);
    } else if (recommendedLocalesSelected) {
      this.$.recommendedLocalesList.deselectItem(recommendedLocalesSelected);
    }
  }

  private isZero_(num: number): boolean {
    return num === 0;
  }

  /**
   * Disable the action button unless a new locale has been selected.
   * @return Whether the "update" action button should be disabled.
   */
  private shouldDisableActionButton_(): boolean {
    return this.selectedLocale_ === null ||
        this.selectedLocale_.value === this.pref.value;
  }

  /**
   * Gets the ARIA label for an item given the online/offline state and selected
   * state, which are also portrayed via icons in the HTML.
   */
  private getAriaLabelForItem_(item: DictationLocaleOption, selected: boolean):
      string {
    const longName = item.worksOffline ?
        this.i18n(
            'dictationChangeLanguageDialogOfflineDescription', item.name) :
        item.name;
    const description = selected ?
        'dictationChangeLanguageDialogSelectedDescription' :
        'dictationChangeLanguageDialogNotSelectedDescription';
    return this.i18n(description, longName);
  }

  private getItemClass_(selected: boolean): string {
    return selected ? 'selected' : '';
  }

  private getIconClass_(item: DictationLocaleOption, selected: boolean):
      string {
    if (this.pref.value === item.value) {
      return 'previous';
    }
    return selected ? 'active' : 'hidden';
  }

  private onSearchChanged_(e: CustomEvent<string>): void {
    this.lowercaseQueryString_ = e.detail.toLowerCase();
  }

  private onKeydown_(e: KeyboardEvent): void {
    // Close dialog if 'esc' is pressed and the search box is already empty.
    if (e.key === 'Escape' && !this.$.search.getValue().trim()) {
      this.$.changeDictationLocaleDialog.close();
    } else if (e.key !== 'PageDown' && e.key !== 'PageUp') {
      this.$.search.scrollIntoViewIfNeeded();
    }
  }

  private onPaneExit_(): void {
    this.$.changeDictationLocaleDialog.close();
  }

  private onCancelClick_(): void {
    this.$.changeDictationLocaleDialog.close();
  }

  private onUpdateClick_(): void {
    if (this.selectedLocale_) {
      this.set('pref.value', this.selectedLocale_.value);
    }
    this.$.changeDictationLocaleDialog.close();
  }
}

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

customElements.define(
    ChangeDictationLocaleDialog.is, ChangeDictationLocaleDialog);