chromium/ash/webui/common/resources/keyboard_key.js

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

import 'chrome://resources/polymer/v3_0/iron-icon/iron-icon.js';
import './keyboard_icons.html.js';
import 'chrome://resources/ash/common/cr_elements/cr_shared_style.css.js';

import {I18nBehavior, I18nBehaviorInterface} from 'chrome://resources/ash/common/i18n_behavior.js';
import {html, mixinBehaviors, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import {getTemplate} from './keyboard_key.html.js';

// TODO(michaelcheco): Add unit test coverage for keyboard-key.

/**
 * @fileoverview
 * 'keyboard-key' provides a visual representation of a single key for the
 * 'keyboard-diagram' component. A single 'keyboard-key' can display one "main
 * glyph" and up to four "corner glyphs".
 *
 * The main glyph can be a text string (specified by 'main-glyph') or an icon
 * (specified by 'icon'). By default it is centered vertically and horizontally
 * on the key, and icons are scaled to fill the key while maintaining their
 * aspect ratio. The main glyph supports ellipsing text strings that don't fit
 * on the key, making it suitable for long labels such as "backspace" as well as
 * letter keys with single characters on them.
 *
 * Adding the 'left' or 'right' classes to the key will align the main glyph to
 * the left or right side respectively, and set icon widths to 24px.
 *
 * The four corner glyphs ('top-left-glyph', 'bottom-left-glyph', etc.) must be
 * text strings, generally single characters. If at least one of the right-side
 * glyphs is set, the corner glyphs will be arranged in the four quadrants of
 * the key like so:
 *
 * +---+
 * |a c|
 * |   |
 * |b d|
 * +---+
 *
 * If neither right-side glyph is set, the "left" glyphs will be centered
 * horizontally, like so:
 *
 * +---+
 * | a |
 * |   |
 * | b |
 * +---+
 *
 * Both a main glyph and one or more corner glyphs may be set, for example for
 * the 'e' key on a German keyboard, which has a centered main glyph but also a
 * Euro symbol in the bottom-right:
 *
 * +---+
 * |   |
 * | e |
 * |  €|
 * +---+
 */

/**
 * Enum of key states.
 * @enum {string}
 */
export const KeyboardKeyState = {
  /** The key has not been pressed during this test session. */
  NOT_PRESSED: 'not-pressed',
  /** The key is currently pressed. */
  PRESSED: 'pressed',
  /** The key is not currently pressed, but we've seen it pressed previously. */
  TESTED: 'tested',
};

/**
 * @constructor
 * @extends {PolymerElement}
 * @implements {I18nBehaviorInterface}
 */
const KeyboardKeyElementBase =
    mixinBehaviors([I18nBehavior], PolymerElement);

/** @polymer */
export class KeyboardKeyElement extends KeyboardKeyElementBase {
  static get is() {
    return 'keyboard-key';
  }

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

  static get properties() {
    return {
      /**
       * The text to show on the key, if any.
       * @type {?string}
       */
      mainGlyph: String,

      /**
       * The text to show in the top-left of the key (or top-center if no
       * right-side glyphs are set).
       * @type {?string}
       */
      topLeftGlyph: String,

      /**
       * The text to show in the top-right of the key.
       * @type {?string}
       */
      topRightGlyph: String,

      /**
       * The text to show in the bottom-left of the key (or bottom-center if no
       * right-side glyphs are set).
       * @type {?string}
       */
      bottomLeftGlyph: String,

      /**
       * The text to show in the bottom-right of the key.
       * @type {?string}
       */
      bottomRightGlyph: String,

      /** @protected {boolean} */
      showCornerGlyphs_: {
        type: Boolean,
        computed: 'computeShowCornerGlyphs_(' +
            'topLeftGlyph, topRightGlyph, bottomLeftGlyph, bottomRightGlyph)',
      },

      /** @protected {boolean} */
      showSecondColumn_: {
        type: Boolean,
        computed: 'computeShowSecondColumn_(topRightGlyph, bottomRightGlyph)',
      },

      /**
       * The name of the icon to use, if any. The name should be of the form:
       * `iconset_name:icon_name`.
       * @type {?string}
       */
      icon: String,

      /**
       * The state to display the key in.
       * @type {!KeyboardKeyState}
       */
      state: {
        type: String,
        value: KeyboardKeyState.NOT_PRESSED,
        reflectToAttribute: true,
        observer: KeyboardKeyElement.prototype.keyboardKeyStateChanged,
      },

      /**
       * The key name to report to assistive technologies. Defaults to mainGlyph
       * if not set.
       * @type {?string}
       */
      ariaName: String,

      /** @protected {string} */
      ariaLabel_: {
        type: String,
        computed: 'computeAriaLabel_(' +
            'ariaName, mainGlyph, bottomLeftGlyph, bottomRightGlyph, state)',
      },
    };
  }

  computeAriaLabel_(
      ariaName, mainGlyph, bottomLeftGlyph, bottomRightGlyph, state) {
    const name =
        ariaName || mainGlyph || bottomRightGlyph || bottomLeftGlyph || '';
    const stateStringIds = {
      [KeyboardKeyState.NOT_PRESSED]: 'keyboardDiagramAriaLabelNotPressed',
      [KeyboardKeyState.PRESSED]: 'keyboardDiagramAriaLabelPressed',
      [KeyboardKeyState.TESTED]: 'keyboardDiagramAriaLabelTested',
    };
    return this.i18n(stateStringIds[state], name);
  }

  /**
   * @param {?string} topLeftGlyph
   * @param {?string} topRightGlyph
   * @param {?string} bottomLeftGlyph
   * @param {?string} bottomRightGlyph
   * @return {boolean}
   * @private
   */
  computeShowCornerGlyphs_(
      topLeftGlyph, topRightGlyph, bottomLeftGlyph, bottomRightGlyph) {
    return !!(
        topLeftGlyph || topRightGlyph || bottomLeftGlyph || bottomRightGlyph);
  }

  /**
   * @param {?string} topRightGlyph
   * @param {?string} bottomRightGlyph
   * @return {boolean}
   * @private
   */
  computeShowSecondColumn_(topRightGlyph, bottomRightGlyph) {
    return !!(topRightGlyph || bottomRightGlyph);
  }

  /**
   * Triggers 'announce-text' event to be used in pair with cr-a11y-announcer.
   * Event provides A11Y "live" update to screen readers when a key is pressed.
   * @protected
   */
  keyboardKeyStateChanged() {
    if (this.state === KeyboardKeyState.PRESSED) {
      this.dispatchEvent(new CustomEvent('announce-text', {
        bubbles: true,
        composed: true,
        detail: {text: this.ariaLabel_},
      }));
    }
  }
}

customElements.define(KeyboardKeyElement.is, KeyboardKeyElement);