chromium/ash/webui/diagnostics_ui/resources/input_card.ts

// 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/ash/common/cr_elements/cr_button/cr_button.js';
import 'chrome://resources/ash/common/cr_elements/policy/cr_tooltip_icon.js';
import 'chrome://resources/polymer/v3_0/iron-icon/iron-icon.js';
import './diagnostics_card_frame.js';
import './icons.html.js';

import {loadTimeData} from 'chrome://resources/ash/common/load_time_data.m.js';
import {CrButtonElement} from 'chrome://resources/ash/common/cr_elements/cr_button/cr_button.js';
import {I18nMixin} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js';
import {assert} from 'chrome://resources/js/assert.js';
import {PolymerElementProperties} from 'chrome://resources/polymer/v3_0/polymer/interfaces.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import {ConnectionType, KeyboardInfo} from './input.mojom-webui.js';
import {getTemplate} from './input_card.html.js';
import {InputDataProviderInterface, TouchDeviceInfo} from './input_data_provider.mojom-webui.js';
import {HostDeviceStatus} from './input_list.js';
import {getInputDataProvider} from './mojo_interface_provider.js';

declare global {
  interface HTMLElementEventMap {
    'test-button-click': CustomEvent<{evdevId: number}>;
  }
}

/**
 * @fileoverview
 * 'input-card' is responsible for displaying a list of input devices with links
 * to their testers.
 */

/**
 * Enum of device types supported by input-card elements.
 */
export enum InputCardType {
  KEYBOARD = 'keyboard',
  TOUCHPAD = 'touchpad',
  TOUCHSCREEN = 'touchscreen',
}

const InputCardElementBase = I18nMixin(PolymerElement);

export class InputCardElement extends InputCardElementBase {
  static get is(): string {
    return 'input-card';
  }

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

  static get properties(): PolymerElementProperties {
    return {
      /**
       * The type of input device to be displayed. Valid values are 'keyboard',
       * 'touchpad', and 'touchscreen'.
       */
      deviceType: String,

      devices: {
        type: Array,
        value: () => [],
      },

      deviceIcon: {
        type: String,
        computed: 'computeDeviceIcon(deviceType)',
      },

      hostDeviceStatus: {
        type: Object,
      },
    };
  }

  deviceType: InputCardType;
  devices: KeyboardInfo[]|TouchDeviceInfo[];
  hostDeviceStatus: HostDeviceStatus;

  private deviceIcon: string;
  private inputDataProvider: InputDataProviderInterface =
      getInputDataProvider();

  private computeDeviceIcon(deviceType: InputCardType): string {
    return {
      [InputCardType.KEYBOARD]: 'diagnostics:keyboard',
      [InputCardType.TOUCHPAD]: 'diagnostics:touchpad',
      [InputCardType.TOUCHSCREEN]: 'diagnostics:touchscreen',
    }[deviceType];
  }

  /**
   * Fetches the description string for a device based on its connection type
   * (e.g. "Bluetooth keyboard", "Internal touchpad").
   */
  private getDeviceDescription(device: KeyboardInfo|TouchDeviceInfo): string {
    if (device.connectionType === ConnectionType.kUnknown) {
      return '';
    }
    const connectionTypeString = {
      [ConnectionType.kInternal]: 'Internal',
      [ConnectionType.kUsb]: 'Usb',
      [ConnectionType.kBluetooth]: 'Bluetooth',
    }[device.connectionType];
    const deviceTypeString = {
      [InputCardType.KEYBOARD]: 'Keyboard',
      [InputCardType.TOUCHPAD]: 'Touchpad',
      [InputCardType.TOUCHSCREEN]: 'Touchscreen',
    }[this.deviceType];
    return loadTimeData.getString(
        'inputDescription' + connectionTypeString + deviceTypeString);
  }

  private isInternalKeyboard(device: KeyboardInfo|TouchDeviceInfo): boolean {
    return this.deviceType == InputCardType.KEYBOARD &&
        device.connectionType == ConnectionType.kInternal;
  }

  private isInternalKeyboardTestable(): boolean {
    return !this.hostDeviceStatus.isTabletMode &&
        this.hostDeviceStatus.isLidOpen;
  }

  /**
   * Grey out the test button if the test device is untestable. e.g. if the
   * laptop's lid is closed, the internal touchscreen is untestable.
   */
  private getDeviceTestability(device: KeyboardInfo|TouchDeviceInfo): boolean {
    // If the device has the key 'testable', we check its testable state.
    if ('testable' in device) {
      return (device as TouchDeviceInfo).testable;
    }

    if (this.isInternalKeyboard(device)) {
      return this.isInternalKeyboardTestable();
    }

    return true;
  }

  private getDeviceTestabilityErrorMessage(device: KeyboardInfo|
                                           TouchDeviceInfo): string {
    // If it is not an internal keyboard, return the generic untestable string.
    if (!this.isInternalKeyboard(device)) {
      return loadTimeData.getString('inputDeviceUntestableNote');
    }

    // Otherwise, differentiate the string based on the reason it is untestable.
    if (this.hostDeviceStatus.isTabletMode) {
      return loadTimeData.getString('inputKeyboardUntestableTabletModeNote');
    }

    if (!this.hostDeviceStatus.isLidOpen) {
      return loadTimeData.getString('inputKeyboardUntestableLidClosedNote');
    }

    // Otherwise, there is no error.
    return '';
  }

  private handleTestButtonClick(e: PointerEvent): void {
    const inputDeviceButton = e.target as CrButtonElement;
    assert(inputDeviceButton);
    const closestDevice: HTMLDivElement|null =
        inputDeviceButton.closest('.device');
    assert(closestDevice);
    const dataEvdevId = closestDevice.getAttribute('data-evdev-id');
    assert(typeof dataEvdevId === 'string');
    const evdevId = parseInt(dataEvdevId, 10);
    this.dispatchEvent(new CustomEvent(
        'test-button-click', {composed: true, detail: {evdevId: evdevId}}));
  }
}

declare global {
  interface HTMLElementTagNameMap {
    'input-card': InputCardElement;
  }
}

customElements.define(InputCardElement.is, InputCardElement);