chromium/ash/webui/personalization_app/resources/js/keyboard_backlight/color_selector_element.ts

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

/**
 * @fileoverview
 * The color selector provides the selection of wallpaper or preset colors in
 * keyboard backlight and zone customization section.
 */

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/cr_elements/cr_lazy_render/cr_lazy_render.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 'chrome://resources/polymer/v3_0/paper-ripple/paper-ripple.js';
import './color_icon_element.js';

import {assert} from 'chrome://resources/js/assert.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.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 {BacklightColor, CurrentBacklightState} from '../../personalization_app.mojom-webui.js';
import {isMultiZoneRgbKeyboardSupported} from '../load_time_booleans.js';
import {WithPersonalizationStore} from '../personalization_store.js';
import {ColorInfo, getPresetColors, isSelectionEvent, RAINBOW, WALLPAPER, WHITE} from '../utils.js';

import {getTemplate} from './color_selector_element.html.js';
import {getShouldShowNudge, handleNudgeShown} from './keyboard_backlight_controller.js';
import {getKeyboardBacklightProvider} from './keyboard_backlight_interface_provider.js';

export type PresetColorSelectedEvent = CustomEvent<{colorId: string}>;
export type RainbowColorSelectedEvent = CustomEvent<null>;
export type WallpaperColorSelectedEvent = CustomEvent<null>;

const presetColorSelectedEventName = 'preset-color-selected';
const rainbowColorSelectedEventName = 'rainbow-color-selected';
const wallpaperColorSelectedEventName = 'wallpaper-color-selected';

declare global {
  interface HTMLElementEventMap {
    [presetColorSelectedEventName]: PresetColorSelectedEvent;
    [rainbowColorSelectedEventName]: RainbowColorSelectedEvent;
    [wallpaperColorSelectedEventName]: WallpaperColorSelectedEvent;
  }
}

export interface ColorSelectorElement {
  $: {
    keys: IronA11yKeysElement,
    selector: IronSelectorElement,
  };
}

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

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

  static get properties() {
    return {
      isCustomizedDialog: {
        type: Boolean,
        value: false,
      },

      ironSelectedColor_: Object,

      isMultiZoneRgbKeyboardSupported_: {
        type: Boolean,
        value() {
          return isMultiZoneRgbKeyboardSupported();
        },
      },

      presetColors_: {
        type: Object,
        value() {
          return getPresetColors();
        },
      },

      presetColorIds_: {
        type: Array,
        computed: 'computePresetColorIds_(presetColors_)',
      },

      rainbowColorId_: {
        type: String,
        value: RAINBOW,
      },

      wallpaperColorId_: {
        type: String,
        value: WALLPAPER,
      },

      currentBacklightState_: Object,

      selectedColor: BacklightColor,

      /** The current wallpaper extracted color. */
      wallpaperColor_: Object,

      shouldShowNudge_: {
        type: Boolean,
        value: false,
        observer: 'onShouldShowNudgeChanged_',
      },
    };
  }

  private isCustomizedDialog: string;
  private ironSelectedColor_: HTMLElement;
  private presetColors_: Record<string, ColorInfo>;
  private presetColorIds_: string[];
  private rainbowColorId_: string;
  private wallpaperColorId_: string;
  private currentBacklightState_: CurrentBacklightState|null;
  private selectedColor: BacklightColor;
  private wallpaperColor_: SkColor|null;
  private shouldShowNudge_: boolean;

  override ready() {
    super.ready();
    this.$.keys.target = this.$.selector;
  }

  override connectedCallback() {
    super.connectedCallback();
    this.watch<ColorSelectorElement['currentBacklightState_']>(
        'currentBacklightState_',
        state => state.keyboardBacklight.currentBacklightState);
    this.watch<ColorSelectorElement['shouldShowNudge_']>(
        'shouldShowNudge_', state => state.keyboardBacklight.shouldShowNudge);
    this.watch<ColorSelectorElement['wallpaperColor_']>(
        'wallpaperColor_', state => state.keyboardBacklight.wallpaperColor);
    this.updateFromStore();

    getShouldShowNudge(getKeyboardBacklightProvider(), this.getStore());
  }

  /** Handle keyboard navigation. */
  private onKeysPress_(
      e: CustomEvent<{key: string, keyboardEvent: KeyboardEvent}>) {
    const selector = this.$.selector;
    const prevButton = this.ironSelectedColor_;
    switch (e.detail.key) {
      case 'left':
        selector.selectPrevious();
        break;
      case 'right':
        selector.selectNext();
        break;
      case 'enter':
        switch (this.ironSelectedColor_.id) {
          case this.rainbowColorId_:
            this.dispatchEvent(new CustomEvent(
                rainbowColorSelectedEventName,
                {bubbles: true, composed: true, detail: null}));
            break;
          case this.wallpaperColorId_:
            this.dispatchEvent(new CustomEvent(
                wallpaperColorSelectedEventName,
                {bubbles: true, composed: true, detail: null}));
            break;
          default:
            this.dispatchEvent(new CustomEvent(presetColorSelectedEventName, {
              bubbles: true,
              composed: true,
              detail: {colorId: this.ironSelectedColor_.id},
            }));
            break;
        }
        break;
      default:
        return;
    }
    // Remove focus state of color icon in previous button.
    if (prevButton) {
      const colorIconElem = this.getColorIconElement_(prevButton);
      if (colorIconElem) {
        colorIconElem.removeAttribute('tabindex');
      }
    }
    // Add focus state for the color icon in new button.
    if (this.ironSelectedColor_) {
      const colorIconElem = this.getColorIconElement_(this.ironSelectedColor_);
      if (colorIconElem) {
        colorIconElem.setAttribute('tabindex', '0');
        colorIconElem.focus();
      }
    }
    e.detail.keyboardEvent.preventDefault();
  }

  private shouldShowRainbowColorItem_(): boolean {
    return !this.isCustomizedDialog;
  }

  private computePresetColorIds_(presetColors: Record<string, string>):
      string[] {
    // ES2020 maintains ordering of Object.keys.
    return Object.keys(presetColors);
  }

  /** Invoked when the wallpaper color is selected. */
  private onWallpaperColorSelected_(e: Event) {
    if (!isSelectionEvent(e)) {
      return;
    }
    const eventTarget = e.target as HTMLElement;
    if (eventTarget.id === 'wallpaperColorIcon') {
      // Only dispatch the event if the icon is clicked.
      this.dispatchEvent(new CustomEvent(
          wallpaperColorSelectedEventName,
          {bubbles: true, composed: true, detail: null}));
    }
  }

  /** Invoked when a preset color is selected. */
  private onPresetColorSelected_(e: Event) {
    if (!isSelectionEvent(e)) {
      return;
    }
    const htmlElement = e.currentTarget as HTMLElement;
    const colorId = htmlElement.id;
    assert(colorId !== undefined, 'colorId not found');
    this.dispatchEvent(new CustomEvent(
        presetColorSelectedEventName,
        {bubbles: true, composed: true, detail: {colorId: colorId}}));
  }

  /** Invoked when the rainbow color is selected. */
  private onRainbowColorSelected_(e: Event) {
    if (!isSelectionEvent(e)) {
      return;
    }
    this.dispatchEvent(new CustomEvent(
        rainbowColorSelectedEventName,
        {bubbles: true, composed: true, detail: null}));
  }

  private onShouldShowNudgeChanged_(shouldShowNudge: boolean) {
    if (shouldShowNudge) {
      setTimeout(() => {
        handleNudgeShown(getKeyboardBacklightProvider(), this.getStore());
      }, 3000);
    }
  }

  private getColorIconElement_(button: HTMLElement): HTMLElement {
    return this.shadowRoot!.getElementById(button.id)!.querySelector(
        'color-icon')!;
  }

  private getColorSelectorAriaLabel_(): string {
    return loadTimeData.getString('keyboardBacklightTitle');
  }

  private getPresetColorTabIndex_(
      isMultiZoneRgbKeyboardSupported: boolean, presetColorId: string): string {
    return isMultiZoneRgbKeyboardSupported && presetColorId === WHITE ? '0' :
                                                                        '-1';
  }

  private getPresetColorAriaLabel_(presetColorId: string): string {
    return this.i18n(presetColorId);
  }

  private getWallpaperColorContainerClass_(selectedColor: BacklightColor):
      string {
    return this.getColorContainerClass_(
        this.getWallpaperColorAriaChecked_(selectedColor));
  }

  private getPresetColorContainerClass_(
      colorId: string, colors: Record<string, ColorInfo>,
      selectedColor: BacklightColor) {
    return this.getColorContainerClass_(
        this.getPresetColorAriaChecked_(colorId, colors, selectedColor));
  }

  private getRainbowColorContainerClass_(selectedColor: BacklightColor) {
    return this.getColorContainerClass_(
        this.getRainbowColorAriaChecked_(selectedColor));
  }

  private getColorContainerClass_(isSelected: string) {
    const defaultClassName = 'selectable';
    return isSelected === 'true' ? `${defaultClassName} tast-selected-color` :
                                   defaultClassName;
  }

  private getWallpaperColorAriaChecked_(selectedColor: BacklightColor) {
    return (selectedColor === BacklightColor.kWallpaper).toString();
  }

  private getPresetColorAriaChecked_(
      colorId: string, colors: Record<string, ColorInfo>,
      selectedColor: BacklightColor) {
    if (!colorId || !colors[colorId]) {
      return 'false';
    }
    return (colors[colorId].enumVal === selectedColor).toString();
  }

  private getRainbowColorAriaChecked_(selectedColor: BacklightColor) {
    return (selectedColor === BacklightColor.kRainbow).toString();
  }

  private getWallpaperColorTitle_() {
    return this.i18n('wallpaperColorTooltipText');
  }
}

customElements.define(ColorSelectorElement.is, ColorSelectorElement);