chromium/chrome/browser/resources/ash/settings/controls/v2/settings_slider_row.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-slider-row' is a component which wraps a basic 'settings-row' with
 * a slotted control component `settings-slider-v2`. This component can be used
 * with or without prefs.
 *
 * - Usage: without pref
 *   - `value` must be specified and `pref` must not be used.
 *
 *   <settings-slider-row
 *       label="Lorem ipsum"
 *       sublabel="Lorem ipsum dolor sit amet"
 *       icon="os-settings:display"
 *       value="[[sliderValue_]]"
 *       ticks="[[sliderTicks_]]"
 *       on-change="onSliderRowChange_"
 *       min-label="$i18n{low}"
 *       max-label="$i18n{high}">
 *   <settings-slider-v2>
 *
 * - Usage: with pref
 *   - `pref` must be specified and `value` must not be used.
 *
 *   <settings-slider-row
 *       label="Lorem ipsum"
 *       sublabel="Lorem ipsum dolor sit amet"
 *       icon="os-settings:display"
 *       pref="[[prefs.foo.bar]]"
 *       min="0"
 *       max="100"
 *       scale="100"
 *       on-change="onSliderRowChange_"
 *       min-label="$i18n{low}"
 *       max-label="$i18n{high}"
 *       update-value-instantly>
 *   <settings-slider-row>
 */

import './settings_slider_v2.js';
import './settings_row.js';

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

import {BaseRowMixin} from './base_row_mixin.js';
import {PrefControlMixinInternal} from './pref_control_mixin_internal.js';
import {SettingsRowElement} from './settings_row.js';
import {getTemplate} from './settings_slider_row.html.js';
import {SettingsSliderV2Element} from './settings_slider_v2.js';

export interface SettingsSliderRowElement {
  $: {
    slider: SettingsSliderV2Element,
    internalRow: SettingsRowElement,
  };
}

const SettingsSliderRowElementBase =
    PrefControlMixinInternal(BaseRowMixin(PolymerElement));

export class SettingsSliderRowElement extends SettingsSliderRowElementBase {
  static get is() {
    return 'settings-slider-row' as const;
  }

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

  static get properties() {
    return {
      /**
       * The current value of the slider. It shouldn't be used or updated if
       * `pref` is specified.
       */
      value: Number,

      /**
       * Values corresponding to each tick. It should be empty if `scale` is
       * specified.
       */
      ticks: {
        type: Array,
        value: () => [],
      },

      /**
       * A scale factor used to support fractional values. For example, if
       * min=0, max=10, scale=10, the value ranges for any decimal number
       * between [0, 1]. If min=0, max=10, scale=1, the value ranges for any
       * decimal number between [0, 10].
       * This is not compatible with `ticks`, i.e. if `scale` is not 1 then
       * `ticks` must not be set.
       */
      scale: {
        type: Number,
        value: 1,
      },

      /**
       * The slider minimum value. If `ticks` is not used, this must be
       * specified.
       */
      min: Number,

      /**
       * The slider maximum value. If `ticks` is not used, this must be
       * specified.
       */
      max: Number,

      /**
       * Label for the min value of the slider that shows below the slider. Also
       * used as the A11y label for the min value.
       */
      minLabel: String,

      /**
       * Label for the max value of the slider that shows below the slider. Also
       * used as the A11y label for the max value.
       */
      maxLabel: String,

      // A11y properties added since they are data-bound in HTML.
      ariaLabel: {
        type: String,
        reflectToAttribute: false,
        observer: 'onAriaLabelSet_',
      },

      ariaDescription: {
        type: String,
        reflectToAttribute: false,
        observer: 'onAriaDescriptionSet_',
      },

      /**
       * Whether or not to hide the min and max labels below the slider.
       * Defaults to false.
       */
      hideLabel: {
        type: Boolean,
        value: false,
      },

      /**
       * Whether or not to hide tick marks on the slider. Default to false.
       * Only compatible with `ticks`, and not compatible with `scale`.
       */
      hideMarkers: {
        type: Boolean,
        value: false,
      },

      updateValueInstantly: {
        type: Boolean,
        value: false,
      },
    };
  }

  ticks: SliderTick[]|number[];
  scale: number;
  min: number;
  max: number;
  hideLabel: boolean;
  minLabel: string;
  maxLabel: string;
  hideMarkers: boolean;
  updateValueInstantly: boolean;
  value: number;

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

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

  /**
   * Manually remove the aria-label attribute from the host node since it is
   * applied to the internal slider. `reflectToAttribute=false` does not resolve
   * this issue. This prevents the aria-label from being duplicated by
   * screen readers.
   */
  private onAriaLabelSet_(): void {
    const ariaLabel = this.getAttribute('aria-label');
    this.removeAttribute('aria-label');
    if (ariaLabel) {
      this.ariaLabel = ariaLabel;
    }
  }

  /**
   * Manually remove the aria-description attribute from the host node since it
   * is applied to the internal slider. `reflectToAttribute=false` does not
   * resolve this issue. This prevents the aria-description from being
   * duplicated by screen readers.
   */
  private onAriaDescriptionSet_(): void {
    const ariaDescription = this.getAttribute('aria-description');
    this.removeAttribute('aria-description');
    if (ariaDescription) {
      this.ariaDescription = ariaDescription;
    }
  }
}

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

customElements.define(SettingsSliderRowElement.is, SettingsSliderRowElement);