chromium/chrome/browser/resources/ash/settings/controls/v2/pref_control_mixin_internal.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
 * PrefControlMixinInternal is a mixin that enhances the client element with
 * pref read/write capabilities (i.e. ability to control a pref). The
 * subscribing element is not required to provide a pref object and should be
 * able to work with or without a pref object.
 *
 * This mixin should only be used by settings components, hence the "internal"
 * suffix.
 */

import {CrSettingsPrefs} from '/shared/settings/prefs/prefs_types.js';
import {assert} from 'chrome://resources/js/assert.js';
import {dedupingMixin, type PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import type {Constructor, UserActionSettingPrefChangeEvent} from '../../common/types.js';

type PrefObject = chrome.settingsPrivate.PrefObject;

export interface PrefControlMixinInternalInterface {
  disabled: boolean;
  readonly isPrefEnforced: boolean;
  pref?: PrefObject;
  validPrefTypes: chrome.settingsPrivate.PrefType[];
  updatePrefValueFromUserAction(value: any): void;
  validatePref(): void;
}

export const PrefControlMixinInternal = dedupingMixin(
    <T extends Constructor<PolymerElement>>(superClass: T): T&
    Constructor<PrefControlMixinInternalInterface> => {
      class PrefControlMixinInternalImpl extends superClass {
        static get properties() {
          return {
            /**
             * The PrefObject being controlled by this element. This should be
             * treated as immutable data.
             */
            pref: {
              type: Object,
              notify: false,  // Two-way binding is intentionally unsupported.
              value: undefined,
            },

            /**
             * Represents if `pref` is enforced by a policy, in which case
             * user-initiated updates are prevented.
             */
            isPrefEnforced: {
              type: Boolean,
              computed: 'computeIsPrefEnforced_(pref.*)',
              readOnly: true,
              value: false,
            },

            /**
             * Represents if this element should be disabled or not. If
             * `isPrefEnforced` is true, then `disabled` is always true.
             */
            disabled: {
              type: Boolean,
              reflectToAttribute: true,
              value: false,
            },
          };
        }

        static get observers() {
          return ['syncPrefEnforcementToDisabled_(disabled, isPrefEnforced)'];
        }

        disabled: boolean;
        readonly isPrefEnforced: boolean;
        pref?: PrefObject;

        /**
         * Array of valid types for `pref`. An empty array means all pref types
         * are supported. A non-empty array means that only those specified
         * types are supported.
         */
        validPrefTypes: chrome.settingsPrivate.PrefType[] = [];

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

          CrSettingsPrefs.initialized.then(() => {
            this.validatePref();
          });
        }

        /**
         * Logs an error once prefs are initialized if the tracked pref is not
         * found. Subscribing elements can override and/or extend this method to
         * handle additional validation.
         */
        validatePref(): void {
          // Pref is not a required property.
          if (this.pref === undefined) {
            return;
          }

          assert(this.pref, this.makeErrorMessage_('Pref not found.'));

          // Assignment for `pref` happens via Polymer data-binding in HTML
          // which has no typechecking. This check catches data-binding errors
          // like `pref="prefs.foo.bar"` during runtime.
          assert(
              typeof this.pref !== 'string',
              this.makeErrorMessage_('Invalid string literal.'));

          if (this.validPrefTypes.length > 0) {
            assert(
                this.validPrefTypes.includes(this.pref.type),
                this.makeErrorMessage_(`Invalid pref type ${this.pref.type}.`));
          }
        }

        /**
         * This method treats `pref` as immutable data and does not update its
         * value directly. Instead, it dispatches a
         * `user-action-setting-pref-change` event to sync the pref update to
         * the prefs state. Raises an error if called when `pref` is not
         * defined.
         * @param value the new value of the pref.
         */
        updatePrefValueFromUserAction(value: any): void {
          assert(
              this.pref,
              'updatePrefValueFromUserAction() requires pref to be defined.');

          // Polymer treats the contents of objects as always being available
          // for two-way binding. That is, when updating a subproperty (e.g.
          // `this.pref.value = newValue`), upward data flow events are fired,
          // even if the property is not marked as notifying. Reference:
          // https://polymer-library.polymer-project.org/3.0/docs/devguide/data-system#data-flow-objects-arrays
          // Since these components should not support two-way binding, the
          // `user-action-setting-pref-change` event will update the pref
          // instead.
          const event: UserActionSettingPrefChangeEvent =
              new CustomEvent('user-action-setting-pref-change', {
                bubbles: true,
                composed: true,
                detail: {prefKey: this.pref.key, value},
              });
          this.dispatchEvent(event);
        }

        /**
         * PrefObjects are marked as enforced per `PrefsUtil::GetPref()` in
         * `chrome/browser/extensions/api/settings_private/prefs_util.cc`.
         * @returns true if `pref` exists and is enforced. Else returns false.
         */
        private computeIsPrefEnforced_(): boolean {
          return this.pref?.enforcement ===
              chrome.settingsPrivate.Enforcement.ENFORCED;
        }

        /**
         * Observes changes to the `isPrefEnforced` property and updates the
         * `disabled` property accordingly.
         */
        private syncPrefEnforcementToDisabled_(): void {
          this.disabled = this.disabled || this.isPrefEnforced;
        }

        /**
         * Generates an error message, including additional information about
         * the element causing the error.
         */
        private makeErrorMessage_(message: string): string {
          let error = this.tagName;
          if (this.id) {
            error += `#${this.id}`;
          }
          error += ` error: ${message}`;
          return error;
        }
      }

      return PrefControlMixinInternalImpl;
    });