chromium/ui/webui/resources/cr_components/history_clusters/horizontal_carousel.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 '//resources/cr_elements/cr_icon_button/cr_icon_button.js';
import '//resources/cr_elements/icons_lit.html.js';

import {EventTracker} from '//resources/js/event_tracker.js';
import {CrLitElement} from '//resources/lit/v3_0/lit.rollup.js';

import {getCss} from './horizontal_carousel.css.js';
import {getHtml} from './horizontal_carousel.html.js';

/**
 * @fileoverview This file provides a custom element displaying a horizontal
 * carousel for the carousel elements.
 */

declare global {
  interface HTMLElementTagNameMap {
    'horizontal-carousel': HorizontalCarouselElement;
  }
}

export interface HorizontalCarouselElement {
  $: {
    backButton: HTMLElement,
    carouselContainer: HTMLElement,
    forwardButton: HTMLElement,
  };
}

export class HorizontalCarouselElement extends CrLitElement {
  static get is() {
    return 'horizontal-carousel';
  }

  static override get styles() {
    return getCss();
  }

  override render() {
    return getHtml.bind(this)();
  }

  static override get properties() {
    return {
      showForwardButton_: {
        type: Boolean,
        reflect: true,
      },

      showBackButton_: {
        type: Boolean,
        reflect: true,
      },
    };
  }

  //============================================================================
  // Properties
  //============================================================================

  private resizeObserver_: ResizeObserver|null = null;
  private eventTracker_: EventTracker = new EventTracker();
  protected showBackButton_: boolean = false;
  protected showForwardButton_: boolean = false;

  //============================================================================
  // Overridden methods
  //============================================================================

  override connectedCallback() {
    super.connectedCallback();
    this.resizeObserver_ = new ResizeObserver(() => {
      this.setShowCarouselButtons_();
    });

    this.resizeObserver_.observe(this.$.carouselContainer);
    this.eventTracker_.add(this, 'keyup', this.onTabFocus_.bind(this));
  }

  override disconnectedCallback() {
    super.disconnectedCallback();
    if (this.resizeObserver_) {
      this.resizeObserver_.unobserve(this.$.carouselContainer);
      this.resizeObserver_ = null;
    }
  }
  //============================================================================
  // Event handlers
  //============================================================================

  protected onCarouselBackClick_() {
    const targetPosition = this.calculateTargetPosition_(-1);
    this.$.carouselContainer.scrollTo(
        {left: targetPosition, behavior: 'smooth'});
    this.showBackButton_ = targetPosition > 0;
    this.showForwardButton_ = true;
  }

  protected onCarouselForwardClick_() {
    const targetPosition = this.calculateTargetPosition_(1);
    this.$.carouselContainer.scrollTo(
        {left: targetPosition, behavior: 'smooth'});
    this.showForwardButton_ =
        targetPosition + this.$.carouselContainer.clientWidth <
        this.$.carouselContainer.scrollWidth;
    this.showBackButton_ = true;
  }

  private onTabFocus_(event: KeyboardEvent) {
    const element = event.target as HTMLElement;
    if (event.code === 'Tab') {
      // -2px as offsetLeft includes padding
      this.$.carouselContainer.scrollTo(
          {left: element.offsetLeft - 2, behavior: 'smooth'});
    }
  }

  //============================================================================
  // Helper methods
  //============================================================================

  private setShowCarouselButtons_() {
    if (Math.round(this.$.carouselContainer.scrollLeft) +
            this.$.carouselContainer.clientWidth <
        this.$.carouselContainer.scrollWidth) {
      // On shrinking the window, the forward button should show up again.
      this.showForwardButton_ = true;
    } else {
      // On expanding the window, the back and forward buttons should disappear.
      this.showBackButton_ = this.$.carouselContainer.scrollLeft > 0;
      this.showForwardButton_ = false;
    }
  }

  private calculateTargetPosition_(direction: number) {
    const offset = this.$.carouselContainer.clientWidth / 2 * direction;
    const targetPosition =
        Math.floor(this.$.carouselContainer.scrollLeft + offset);
    return Math.max(
        0,
        Math.min(
            targetPosition,
            this.$.carouselContainer.scrollWidth -
                this.$.carouselContainer.clientWidth));
  }
}

customElements.define(HorizontalCarouselElement.is, HorizontalCarouselElement);