chromium/chrome/browser/resources/tab_strip/tab_group.ts

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

import {CustomElement} from 'chrome://resources/js/custom_element.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';

import {getTemplate} from './tab_group.html.js';
import type {TabGroupVisualData} from './tab_strip.mojom-webui.js';
import type {TabsApiProxy} from './tabs_api_proxy.js';
import {TabsApiProxyImpl} from './tabs_api_proxy.js';

export class TabGroupElement extends CustomElement {
  static override get template() {
    return getTemplate();
  }

  private tabsApi_: TabsApiProxy;
  private chip_: HTMLElement;
  private isValidDragOverTarget_: boolean;

  constructor() {
    super();

    this.tabsApi_ = TabsApiProxyImpl.getInstance();

    this.chip_ = this.getRequiredElement('#chip');
    this.chip_.addEventListener('click', () => this.onClickChip_());
    this.chip_.addEventListener(
        'keydown', e => this.onKeydownChip_(/** @type {!KeyboardEvent} */ (e)));

    /**
     * Flag indicating if this element can accept dragover events. This flag
     * is updated by TabListElement while animating.
     */
    this.isValidDragOverTarget_ = true;
  }

  get isValidDragOverTarget(): boolean {
    return !this.hasAttribute('dragging_') && this.isValidDragOverTarget_;
  }

  set isValidDragOverTarget(isValid: boolean) {
    this.isValidDragOverTarget_ = isValid;
  }

  getDragImage(): HTMLElement {
    return this.getRequiredElement('#dragImage');
  }

  getDragImageCenter(): HTMLElement {
    // Since the drag handle is #dragHandle, the drag image should be
    // centered relatively to it.
    return this.getRequiredElement('#dragHandle');
  }

  private onClickChip_() {
    if (!this.dataset['groupId']) {
      return;
    }

    const boundingBox =
        this.getRequiredElement('#chip').getBoundingClientRect();
    this.tabsApi_.showEditDialogForGroup(
        this.dataset['groupId'], boundingBox.left, boundingBox.top,
        boundingBox.width, boundingBox.height);
  }

  private onKeydownChip_(event: KeyboardEvent) {
    if (event.key === 'Enter' || event.key === ' ') {
      this.onClickChip_();
    }
  }

  setDragging(enabled: boolean) {
    // Since the draggable target is the #chip, if the #chip moves and is no
    // longer under the pointer while the dragstart event is happening, the drag
    // will get canceled. This is unfortunately the behavior of the native drag
    // and drop API. The workaround is to have two different attributes: one
    // to get the drag image and start the drag event while keeping #chip in
    // place, and another to update the placeholder to take the place of where
    // the #chip would be.
    this.toggleAttribute('getting-drag-image_', enabled);
    requestAnimationFrame(() => {
      this.toggleAttribute('dragging', enabled);
    });
  }

  setDraggedOut(isDraggedOut: boolean) {
    this.toggleAttribute('dragged-out_', isDraggedOut);
  }

  isDraggedOut(): boolean {
    return this.hasAttribute('dragged-out_');
  }

  setTouchPressed(isTouchPressed: boolean) {
    this.toggleAttribute('touch_pressed_', isTouchPressed);
  }

  updateVisuals(visualData: TabGroupVisualData) {
    this.getRequiredElement('#title').innerText = visualData.title;
    this.style.setProperty('--tabstrip-tab-group-color-rgb', visualData.color);
    this.style.setProperty(
        '--tabstrip-tab-group-text-color-rgb', visualData.textColor);

    // Content strings are empty for the label and are instead replaced by
    // the aria-describedby attribute on the chip.
    if (visualData.title) {
      this.chip_.setAttribute(
          'aria-label',
          loadTimeData.getStringF('namedGroupLabel', visualData.title, ''));
    } else {
      this.chip_.setAttribute(
          'aria-label', loadTimeData.getStringF('unnamedGroupLabel', ''));
    }
  }
}

declare global {
  interface HTMLElementTagNameMap {
    'tabstrip-tab-group': TabGroupElement;
  }
}

customElements.define('tabstrip-tab-group', TabGroupElement);

export function isTabGroupElement(element: Element): boolean {
  return element.tagName === 'TABSTRIP-TAB-GROUP';
}

export function isDragHandle(element: Element): boolean {
  return element.id === 'dragHandle';
}