chromium/chrome/browser/resources/ash/settings/controls/v2/settings_toggle_v2.ts

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

/**
 * @fileoverview
 * `settings-toggle-v2` wraps the cr-toggle element. Works with or
 * without a pref object.
 *
 * - Usage: without pref
 *   - 'checked' is false by default
 *
 *   <settings-toggle-v2
 *       checked="[[checked]]"
 *       on-change="onToggleChange_">
 *   <settings-toggle-v2>
 *
 * - Usage: with pref
 *   - 'pref' must be provided
 *
 *   <settings-toggle-v2
 *       pref="[[prefs.foo.bar]]"
 *       on-change="onToggleChange_">
 *   <settings-toggle-v2>
 *
 * - Usage: no-set-pref
 *   - 'pref' must be provided
 *   - If no-set-pref is passed, the pref value will not change based on the
 *     toggle change. The property 'checked' changes with the user's click,
 *     even when no-set-pref is true.
 *
 *     Example: A use-case is when changing the toggle triggers a dialog to
 *     open where a user must confirm or cancel the toggle change.
 *     - Invoking 'resetToPrefValue()' will change the toggle value to the
 *       pref's value.
 *     - Invoking 'commitPrefChange()' will change the pref value to the
 *       toggle's 'checked' value.
 *
 *   <settings-toggle-v2
 *       pref="[[prefs.foo.bar]]"
 *       no-set-pref
 *       on-change="openToggleDialog_">
 *   <settings-toggle-v2>
 *
 * - Usage: inverted
 *   - 'pref' must be provided
 *   - The checked value will be the opposite of the pref's value
 *
 *     Example: Suppose we have multiple functionalities, such as fA and fB,
 *     but only one of them can be enabled at any given time. fA’s value is
 *     tied to the preference prefA. We want the toggle for fB to display the
 *     inverse value of prefA. In other words, when fA is enabled, the toggle
 *     for fB will show an unchecked value (OFF), and vice versa.
 *
 *   <settings-toggle-v2
 *       pref="[[prefs.foo.bar]]"
 *       on-change="onToggleChange_"
 *       inverted>
 *   <settings-toggle-v2>
 */

import 'chrome://resources/ash/common/cr_elements/cr_toggle/cr_toggle.js';
import 'chrome://resources/ash/common/cr_elements/cros_color_overrides.css.js';
import 'chrome://resources/ash/common/cr_elements/policy/cr_policy_pref_indicator.js';

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

import {PrefControlMixinInternal} from './pref_control_mixin_internal.js';
import {getTemplate} from './settings_toggle_v2.html.js';

export interface SettingsToggleV2Element {
  $: {
    control: CrToggleElement,
  };
}

const SettingsToggleV2ElementBase = PrefControlMixinInternal(PolymerElement);

export class SettingsToggleV2Element extends SettingsToggleV2ElementBase {
  static get is() {
    return 'settings-toggle-v2' as const;
  }

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

  static get properties() {
    return {
      /**
       * Used to manually set the toggle on or off.
       */
      checked: {
        type: Boolean,
        reflectToAttribute: true,
        value: false,
      },

      /** Whether the control should represent the inverted pref value. */
      inverted: {
        type: Boolean,
        value: false,
      },

      /**
       * If true, changing the control’s value will not update the pref
       * automatically. This allows the container to confirm the change first
       * then call either sendPrefChange or resetToPrefValue accordingly.
       */
      noSetPref: {
        type: Boolean,
        reflectToAttribute: true,
        value: false,
      },
    };
  }

  static get observers() {
    return [
      'setToPrefValue_(pref.*)',
    ];
  }

  checked: boolean;
  inverted: boolean;
  noSetPref: boolean;
  override validPrefTypes: chrome.settingsPrivate.PrefType[] = [
    chrome.settingsPrivate.PrefType.BOOLEAN,
  ];

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

  /**
   * Handle downward data binding from pref to update the toggle accordingly.
   */
  private setToPrefValue_(): void {
    const currentPrefValue = this.pref!.value;
    this.checked = this.inverted ? !currentPrefValue : currentPrefValue;
  }

  /**
   * Event handler for when toggle has been toggled by user action. Dispatches a
   * `change` event containing the checked value.
   */
  private onChange_(): void {
    if (this.disabled) {
      return;
    }

    this.checked = !this.checked;

    if (this.pref && !this.noSetPref) {
      this.commitPrefChange();
    }

    this.dispatchEvent(new CustomEvent('change', {
      bubbles: true,
      composed: false,  // Event should not pass the shadow DOM boundary.
      detail: this.checked,
    }));
  }

  /**
   * Updates the pref value to the `checked` property value.
   */
  commitPrefChange(): void {
    // updatePrefValueFromUserAction() will ensure that the pref is defined
    // before committing the change.
    this.updatePrefValueFromUserAction(
        this.inverted ? !this.checked : this.checked);
  }

  /**
   * Reset the control element’s value to match the pref’s current value.
   */
  resetToPrefValue(): void {
    assert(this.pref, 'resetToPrefValue() requires pref to be defined.');

    this.setToPrefValue_();
  }
}

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

customElements.define(SettingsToggleV2Element.is, SettingsToggleV2Element);