chromium/chrome/browser/resources/ash/settings/multidevice_page/multidevice_feature_toggle.ts

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

/**
 * @fileoverview
 * A toggle button specially suited for the MultiDevice Settings UI use-case.
 * Instead of changing on clicks, it requests a pref change from the
 * MultiDevice service and/or triggers a password check to grab an auth token
 * for the user. It also receives real time updates on feature states and
 * reflects them in the toggle status.
 */

import 'chrome://resources/ash/common/cr_elements/cr_toggle/cr_toggle.js';

import {CrToggleElement} from 'chrome://resources/ash/common/cr_elements/cr_toggle/cr_toggle.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import {MultiDeviceFeature, MultiDeviceFeatureState} from './multidevice_constants.js';
import {MultiDeviceFeatureMixin} from './multidevice_feature_mixin.js';
import {getTemplate} from './multidevice_feature_toggle.html.js';

export interface SettingsMultideviceFeatureToggleElement {
  $: {
    toggle: CrToggleElement,
  };
}

const SettingsMultideviceFeatureToggleElementBase =
    MultiDeviceFeatureMixin(PolymerElement);

export class SettingsMultideviceFeatureToggleElement extends
    SettingsMultideviceFeatureToggleElementBase {
  static get is() {
    return 'settings-multidevice-feature-toggle' as const;
  }

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

  static get properties() {
    return {
      feature: Number,
      toggleAriaLabel: String,
      checked_: Boolean,
    };
  }

  static get observers() {
    return ['resetChecked_(feature, pageContentData)'];
  }

  feature: MultiDeviceFeature;
  toggleAriaLabel: string;
  private checked_: boolean;

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

    this.addEventListener('click', this.onDisabledInnerToggleClick_);
  }

  override focus(): void {
    this.$.toggle.focus();
  }

  /**
   * Callback for clicking on the toggle itself, or a row containing a toggle
   * without other links. It attempts to toggle the feature's status if the user
   * is allowed.
   */
  toggleFeature(): void {
    this.resetChecked_();

    // Pass the negation of |this.checked_|: this indicates that if the toggle
    // is checked, the intent is for it to be unchecked, and vice versa.
    const featureToggleClickedEvent =
        new CustomEvent('feature-toggle-clicked', {
          bubbles: true,
          composed: true,
          detail: {feature: this.feature, enabled: !this.checked_},
        });
    this.dispatchEvent(featureToggleClickedEvent);
  }

  /**
   * Because MultiDevice prefs are only meant to be controlled via the
   * MultiDevice mojo service, we need the cr-toggle to appear not to change
   * when pressed. This method resets it before a change is visible to the
   * user.
   */
  private resetChecked_(): void {
    // If Phone Hub notification access is prohibited, the toggle is always off.
    if (this.feature === MultiDeviceFeature.PHONE_HUB_NOTIFICATIONS &&
        this.isPhoneHubNotificationAccessProhibited()) {
      this.checked_ = false;
      return;
    }

    // If Phone Hub apps access is prohibited, the toggle is always off.
    if (this.feature === MultiDeviceFeature.ECHE &&
        this.isPhoneHubAppsAccessProhibited()) {
      this.checked_ = false;
      return;
    }

    this.checked_ = this.getFeatureState(this.feature) ===
        MultiDeviceFeatureState.ENABLED_BY_USER;
  }

  /**
   * This handles the edge case in which the inner toggle (i.e., the cr-toggle)
   * is disabled. For context, the cr-toggle element naturally stops clicks
   * from propagating as long as its disabled attribute is false. However, if
   * the cr-toggle's disabled attribute is set to true, its pointer-event CSS
   * property is set to 'none' automatically. Thus, if the cr-toggle is clicked
   * while it is disabled, the click event targets the parent element directly
   * instead of propagating through the cr-toggle. This handler prevents such a
   * click from unintentionally bubbling up the tree.
   */
  private onDisabledInnerToggleClick_(event: Event): void {
    event.stopPropagation();
  }

  /**
   * Returns the A11y label for the toggle.
   */
  private getToggleA11yLabel_(): string {
    return this.toggleAriaLabel || this.getFeatureName(this.feature);
  }
}

declare global {
  interface HTMLElementTagNameMap {
    [SettingsMultideviceFeatureToggleElement.is]:
        SettingsMultideviceFeatureToggleElement;
  }
  interface HTMLElementEventMap {
    'feature-toggle-clicked':
        CustomEvent<{feature: MultiDeviceFeature, enabled: boolean}>;
  }
}

customElements.define(
    SettingsMultideviceFeatureToggleElement.is,
    SettingsMultideviceFeatureToggleElement);