chromium/ash/webui/personalization_app/resources/js/theme/dynamic_color_element.ts

// Copyright 2022 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 component displays the dynamic color options.
 */

import 'chrome://resources/ash/common/personalization/common.css.js';
import 'chrome://resources/ash/common/personalization/cros_button_style.css.js';
import 'chrome://resources/ash/common/personalization/personalization_shared_icons.html.js';
import 'chrome://resources/ash/common/cr_elements/cr_button/cr_button.js';
import 'chrome://resources/ash/common/cr_elements/cr_toggle/cr_toggle.js';
import 'chrome://resources/polymer/v3_0/iron-a11y-keys/iron-a11y-keys.js';
import 'chrome://resources/polymer/v3_0/iron-selector/iron-selector.js';

import {CrButtonElement} from 'chrome://resources/ash/common/cr_elements/cr_button/cr_button.js';
import {hexColorToSkColor} from 'chrome://resources/js/color_utils.js';
import {SkColor} from 'chrome://resources/mojo/skia/public/mojom/skcolor.mojom-webui.js';
import {IronA11yKeysElement} from 'chrome://resources/polymer/v3_0/iron-a11y-keys/iron-a11y-keys.js';
import {IronSelectorElement} from 'chrome://resources/polymer/v3_0/iron-selector/iron-selector.js';

import {ColorScheme} from '../../color_scheme.mojom-webui.js';
import {SampleColorScheme, STATIC_COLOR_DARK_GREEN, STATIC_COLOR_GOOGLE_BLUE, STATIC_COLOR_LIGHT_PINK, STATIC_COLOR_LIGHT_PURPLE, StaticColor} from '../../personalization_app.mojom-webui.js';
import {logDynamicColorColorSchemeButtonClick, logDynamicColorStaticColorButtonClick, logDynamicColorToggleButtonClick} from '../personalization_metrics_logger.js';
import {WithPersonalizationStore} from '../personalization_store.js';
import {convertToRgbHexStr} from '../utils.js';

import {getTemplate} from './dynamic_color_element.html.js';
import {initializeDynamicColorData, setColorSchemePref, setStaticColorPref} from './theme_controller.js';
import {getThemeProvider} from './theme_interface_provider.js';
import {ThemeObserver} from './theme_observer.js';
import {DEFAULT_COLOR_SCHEME, DEFAULT_STATIC_COLOR, isAutomaticSeedColorEnabled} from './utils.js';

export interface DynamicColorElement {
  $: {
    staticColorKeys: IronA11yKeysElement,
    colorSchemeKeys: IronA11yKeysElement,
    colorSchemeSelector: IronSelectorElement,
    staticColorSelector: IronSelectorElement,
  };
}

/**
 * enumVal: the StaticColor enum value.
 * fillVal: the color displayed on the button.
 * seedVal: the color stored on the backend, used for calculating color
 * palettes.
 */
export interface StaticColorInfo {
  enumVal: StaticColor;
  fillVal: string;
  seedVal: string;
}

interface OnStaticColorSelectedEvent {
  model: {staticColor: StaticColorInfo};
}

export class DynamicColorElement extends WithPersonalizationStore {
  static get is() {
    return 'dynamic-color';
  }

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

  static get properties() {
    return {
      // Whether or not to use the wallpaper to calculate the seed color.
      automaticSeedColorEnabled: {
        type: Boolean,
        computed: 'isAutomaticSeedColorEnabled_(colorSchemeSelected_)',
      },
      // The static color stored in the backend.
      staticColorSelected_: Object,
      // The color scheme stored in the backend.
      colorSchemeSelected_: Object,
      staticColors_: {
        type: Object,
        computed: 'computePresetStaticColors_()',
      },
      sampleColorSchemes_: {
        type: Array,
        notify: true,
      },
      // The color scheme button currently highlighted by keyboard navigation.
      colorSchemeHighlightedButton_: {
        type: Object,
        notify: true,
      },
      // The static color button currently highlighted by keyboard navigation.
      staticColorHighlightedButton_: {
        type: Object,
        notify: true,
      },
    };
  }

  automaticSeedColorEnabled: boolean;
  private previousStaticColorSelected_: SkColor|null;
  private previousColorSchemeSelected_: ColorScheme|null;
  private staticColorSelected_: SkColor|null;
  private colorSchemeSelected_: ColorScheme|null;
  private staticColors_: StaticColorInfo[];
  private sampleColorSchemes_: SampleColorScheme[];
  private colorSchemeHighlightedButton_: CrButtonElement;
  private staticColorHighlightedButton_: CrButtonElement;

  override ready() {
    super.ready();
    this.$.staticColorKeys.target = this.$.staticColorSelector;
    this.$.colorSchemeKeys.target = this.$.colorSchemeSelector;
  }

  override connectedCallback() {
    super.connectedCallback();
    ThemeObserver.initThemeObserverIfNeeded();
    this.watch<DynamicColorElement['staticColorSelected_']>(
        'staticColorSelected_', state => state.theme.staticColorSelected);
    this.watch<DynamicColorElement['colorSchemeSelected_']>(
        'colorSchemeSelected_', state => state.theme.colorSchemeSelected);
    this.watch<DynamicColorElement['sampleColorSchemes_']>(
        'sampleColorSchemes_', state => state.theme.sampleColorSchemes);
    this.updateFromStore();
    initializeDynamicColorData(getThemeProvider(), this.getStore());
  }

  private computePresetStaticColors_() {
    const lightPink = convertToRgbHexStr(STATIC_COLOR_LIGHT_PINK);
    const darkGreen = convertToRgbHexStr(STATIC_COLOR_DARK_GREEN);
    const lightPurple = convertToRgbHexStr(STATIC_COLOR_LIGHT_PURPLE);
    return [
      {
        enumVal: StaticColor.kGoogleBlue,
        fillVal: '#4d72b4',
        seedVal: convertToRgbHexStr(STATIC_COLOR_GOOGLE_BLUE),
      },
      {
        enumVal: StaticColor.kLightPink,
        fillVal: lightPink,
        seedVal: lightPink,
      },
      {
        enumVal: StaticColor.kDarkGreen,
        fillVal: darkGreen,
        seedVal: darkGreen,
      },
      {
        enumVal: StaticColor.kLightPurple,
        fillVal: lightPurple,
        seedVal: lightPurple,
      },
    ];
  }

  private onClickColorSchemeButton_(event: Event) {
    const eventTarget = event.currentTarget as HTMLElement;
    const colorScheme = Number(eventTarget.dataset['colorSchemeId']);
    logDynamicColorColorSchemeButtonClick(colorScheme);
    setColorSchemePref(colorScheme, getThemeProvider(), this.getStore());
  }

  private onClickStaticColorButton_(event: OnStaticColorSelectedEvent) {
    const staticColorInfo = event.model.staticColor;
    logDynamicColorStaticColorButtonClick(staticColorInfo.enumVal);
    setStaticColorPref(
        hexColorToSkColor(staticColorInfo.seedVal), getThemeProvider(),
        this.getStore());
  }

  private onToggleChanged_() {
    // automaticSeedColorEnabled represents the state before the toggle button
    // was clicked. We flip the state of automaticSeedColorEnabled to show the
    // result of clicking the toggle.
    logDynamicColorToggleButtonClick(!this.automaticSeedColorEnabled);
    if (this.automaticSeedColorEnabled) {
      this.previousColorSchemeSelected_ = this.colorSchemeSelected_;
      const staticColor =
          this.previousStaticColorSelected_ || DEFAULT_STATIC_COLOR;
      setStaticColorPref(staticColor, getThemeProvider(), this.getStore());
    } else {
      this.previousStaticColorSelected_ = this.staticColorSelected_;
      const colorScheme =
          this.previousColorSchemeSelected_ || DEFAULT_COLOR_SCHEME;
      setColorSchemePref(colorScheme, getThemeProvider(), this.getStore());
    }
  }

  private isAutomaticSeedColorEnabled_(colorScheme: ColorScheme|null) {
    return isAutomaticSeedColorEnabled(colorScheme);
  }

  private getColorSchemeAriaChecked_(
      colorScheme: number, colorSchemeSelected: number|null): 'true'|'false' {
    const checkedColorScheme = colorSchemeSelected || DEFAULT_COLOR_SCHEME;
    return checkedColorScheme === colorScheme ? 'true' : 'false';
  }

  private getColorSchemeAriaDescription_(colorScheme: ColorScheme): string {
    switch (colorScheme) {
      case ColorScheme.kTonalSpot:
        return this.i18n('colorSchemeTonalSpot');
      case ColorScheme.kExpressive:
        return this.i18n('colorSchemeExpressive');
      case ColorScheme.kNeutral:
        return this.i18n('colorSchemeNeutral');
      case ColorScheme.kVibrant:
        return this.i18n('colorSchemeVibrant');
      default:
        console.warn('Invalid color scheme value.');
        return '';
    }
  }

  private getStaticColorAriaChecked_(
      staticColor: string, staticColorSelected: SkColor|null): 'true'|'false' {
    const checkedStaticColor = staticColorSelected || DEFAULT_STATIC_COLOR;
    return staticColor === convertToRgbHexStr(checkedStaticColor.value) ?
        'true' :
        'false';
  }

  private getStaticColorAriaDescription_(staticColor: StaticColor): string {
    switch (staticColor) {
      case StaticColor.kGoogleBlue:
        return this.i18n('staticColorGoogleBlue');
      case StaticColor.kLightPink:
        return this.i18n('staticColorLightPink');
      case StaticColor.kDarkGreen:
        return this.i18n('staticColorDarkGreen');
      case StaticColor.kLightPurple:
        return this.i18n('staticColorLightPurple');
      default:
        console.warn('Invalid static color value.');
        return '';
    }
  }


  private onStaticColorKeysPress_(
      e: CustomEvent<{key: string, keyboardEvent: KeyboardEvent}>) {
    this.onKeysPress_(
        e, this.$.staticColorSelector, this.staticColorHighlightedButton_);
  }

  private onColorSchemeKeysPress_(
      e: CustomEvent<{key: string, keyboardEvent: KeyboardEvent}>) {
    this.onKeysPress_(
        e, this.$.colorSchemeSelector, this.colorSchemeHighlightedButton_);
  }

  /** Handle keyboard navigation. */
  private onKeysPress_(
      e: CustomEvent<{key: string, keyboardEvent: KeyboardEvent}>,
      selector: IronSelectorElement, prevButton: CrButtonElement) {
    switch (e.detail.key) {
      case 'left':
        selector.selectPrevious();
        break;
      case 'right':
        selector.selectNext();
        break;
      default:
        return;
    }
    // Remove focus state of previous button.
    if (prevButton) {
      prevButton.removeAttribute('tabindex');
    }

    // Add focus state for new button.
    const highlightedButton = this.automaticSeedColorEnabled ?
        this.colorSchemeHighlightedButton_ :
        this.staticColorHighlightedButton_;
    highlightedButton.setAttribute('tabindex', '0');
    highlightedButton.focus();

    e.detail.keyboardEvent.preventDefault();
  }

  /**
   * Returns the tab index for the color scheme buttons.
   */
  private getColorSchemeTabIndex_(id: number): string {
    return id === DEFAULT_COLOR_SCHEME ? '0' : '-1';
  }

  /**
   * Returns the tab index for the static color buttons.
   */
  private getStaticColorTabIndex_(id: string): string {
    return hexColorToSkColor(id).value === DEFAULT_STATIC_COLOR.value ? '0' :
                                                                        '-1';
  }
}

customElements.define(DynamicColorElement.is, DynamicColorElement);