chromium/chrome/browser/resources/chromeos/emoji_picker/emoji_image.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.

import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import {VISUAL_CONTENT_WIDTH} from './constants.js';
import {getTemplate} from './emoji_image.html.js';
import { createCustomEvent, EMOJI_CLEAR_RECENTS_CLICK } from './events.js';
import {CategoryEnum, EmojiVariants} from './types.js';

export class EmojiImageComponent extends PolymerElement {
  static get is() {
    return 'emoji-image' as const;
  }

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

  static get properties() {
    return {
      index: Number,
      item: Object,
      showTooltip: Object,
      emojiClick: Object,

      clearable: {type: Boolean, value: false},
      showClearButton: {type: Boolean, value: false},
    };
  }

  index: number;
  item: EmojiVariants;
  loading: boolean = true;

  showTooltip: (e: MouseEvent|FocusEvent) => void;
  emojiClick: (e: MouseEvent) => void;

  clearable: boolean;
  showClearButton: boolean;

  private handleMouseEnter(event: MouseEvent): void {
    this.showTooltip(event);
  }

  private handleFocus(event: FocusEvent): void {
    this.showTooltip(event);
  }

  private handleClick(event: MouseEvent): void {
    this.emojiClick(event);
  }

  private findSiblingEmojiImageByIndex(index: number):
      EmojiImageComponent|null {
    // The shadow root of emoji-group.
    const parentShadowRoot = this.shadowRoot!.host.getRootNode() as ShadowRoot;

    for (const emojiImage of parentShadowRoot.querySelectorAll('emoji-image')) {
      if (emojiImage.index === index) {
        return emojiImage;
      }
    }

    return null;
  }

  private handleKeydown(event: KeyboardEvent): void {
    // The img element where the keyboard event is triggered.
    const target = event.target as HTMLImageElement;

    // Triggers click event to insert the current GIF image.
    if (event.code === 'Enter') {
      event.stopPropagation();
      target.click();
      return;
    }

    // Moves focus to the correct sibling.
    if (event.code === 'Tab') {
      const siblingIndex = this.index + (event.shiftKey ? -1 : +1);
      const sibling = this.findSiblingEmojiImageByIndex(siblingIndex);

      if (sibling !== null) {
        event.preventDefault();
        event.stopPropagation();
        sibling.focus();
        return;
      }
    }
  }

  override focus() {
    this.shadowRoot?.querySelector('img')?.focus();
  }

  private handleLoad(): void {
    this.loading = false;
  }

  private handleContextMenu(evt: Event): void {
    if (this.clearable) {
      evt.preventDefault();
      evt.stopPropagation();
      this.showClearButton = true;
    }
  }

  private handleMouseLeave(): void {
    if (this.showClearButton) {
      this.showClearButton = false;
    }
  }

  private handleClear(evt: Event): void {
    evt.preventDefault();
    evt.stopPropagation();
    this.showClearButton = false;
    this.dispatchEvent(createCustomEvent(
      EMOJI_CLEAR_RECENTS_CLICK, {
        category: CategoryEnum.GIF,
        item: this.item,
      },
    ));
  }

  private getImageClassName(loading: boolean) {
    return loading ? 'emoji-image loading' : 'emoji-image';
  }

  /**
   * Returns visual content preview url.
   */
  private getUrl(item: EmojiVariants) {
    return item.base.visualContent?.url.preview.url;
  }

  private getStyles(item: EmojiVariants) {
    const {visualContent} = item.base;

    if (visualContent === undefined) {
      return;
    }

    const {height, width} = visualContent.previewSize;
    const visualContentHeight = height / width * VISUAL_CONTENT_WIDTH;

    return `--visual-content-height: ${visualContentHeight}px`;
  }
}

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

customElements.define(EmojiImageComponent.is, EmojiImageComponent);