chromium/ui/webui/resources/cr_elements/cr_expand_button/cr_expand_button.ts

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

/**
 * @fileoverview
 * 'cr-expand-button' is a chrome-specific wrapper around a button that toggles
 * between an opened (expanded) and closed state.
 */
import '../cr_icon_button/cr_icon_button.js';
import '../icons_lit.html.js';

import {focusWithoutInk} from '//resources/js/focus_without_ink.js';
import type {PropertyValues} from '//resources/lit/v3_0/lit.rollup.js';
import {CrLitElement} from '//resources/lit/v3_0/lit.rollup.js';

import type {CrIconButtonElement} from '../cr_icon_button/cr_icon_button.js';

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

export interface CrExpandButtonElement {
  $: {
    icon: CrIconButtonElement,
  };
}

export class CrExpandButtonElement extends CrLitElement {
  static get is() {
    return 'cr-expand-button';
  }

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

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

  static override get properties() {
    return {
      /**
       * If true, the button is in the expanded state and will show the icon
       * specified in the `collapseIcon` property. If false, the button shows
       * the icon specified in the `expandIcon` property.
       */
      expanded: {
        type: Boolean,
        notify: true,
      },

      /**
       * If true, the button will be disabled and grayed out.
       */
      disabled: {
        type: Boolean,
        reflect: true,
      },

      /** A11y text descriptor for this control. */
      ariaLabel: {type: String},

      tabIndex: {type: Number},
      expandIcon: {type: String},
      collapseIcon: {type: String},
      expandTitle: {type: String},
      collapseTitle: {type: String},
    };
  }

  expanded: boolean = false;
  disabled: boolean = false;
  expandIcon: string = 'cr:expand-more';
  collapseIcon: string = 'cr:expand-less';
  expandTitle?: string;
  collapseTitle?: string;
  override tabIndex: number = 0;

  override firstUpdated() {
    this.addEventListener('click', this.toggleExpand_);
  }

  override willUpdate(changedProperties: PropertyValues<this>) {
    super.willUpdate(changedProperties);

    if (changedProperties.has('expanded') ||
        changedProperties.has('collapseTitle') ||
        changedProperties.has('expandTitle')) {
      this.title =
          (this.expanded ? this.collapseTitle : this.expandTitle) || '';
    }
  }

  override updated(changedProperties: PropertyValues<this>) {
    super.updated(changedProperties);

    if (changedProperties.has('ariaLabel')) {
      this.onAriaLabelChange_();
    }
  }

  override focus() {
    this.$.icon.focus();
  }

  protected getIcon_(): string {
    return this.expanded ? this.collapseIcon : this.expandIcon;
  }

  protected getAriaExpanded_(): string {
    return this.expanded ? 'true' : 'false';
  }

  private onAriaLabelChange_() {
    if (this.ariaLabel) {
      this.$.icon.removeAttribute('aria-labelledby');
      this.$.icon.setAttribute('aria-label', this.ariaLabel);
    } else {
      this.$.icon.removeAttribute('aria-label');
      this.$.icon.setAttribute('aria-labelledby', 'label');
    }
  }

  private toggleExpand_(event: Event) {
    // Prevent |click| event from bubbling. It can cause parents of this
    // elements to erroneously re-toggle this control.
    event.stopPropagation();
    event.preventDefault();

    this.scrollIntoViewIfNeeded();
    this.expanded = !this.expanded;
    focusWithoutInk(this.$.icon);
  }
}

declare global {
  interface HTMLElementTagNameMap {
    'cr-expand-button': CrExpandButtonElement;
  }
}

customElements.define(CrExpandButtonElement.is, CrExpandButtonElement);