File

libs/ui/expansion-panel/src/lib/panel/expansion-panel.component.ts

Description

An expansion panel component to show/hide content

Extends

CdkAccordionItem

Implements

AfterContentInit OnChanges OnDestroy

Example

<ts-expansion-panel
   [hideToggle]="true"
   [isExpanded]="true"
   [isDisabled]="true"
   [transparentMode]="false"
   (opened)="panelOpened()"
   (closed)="panelClosed()"
   (expandedChange)="panelStateChanged($event)"
   (destroyed)="componentDestroyed()"
   (afterCollapse)="collapseAnimationDone"
   (afterExpand)="expandAnimationDone()"
>
   <ts-expansion-panel-trigger>
     Panel trigger
   </ts-expansion-panel-trigger>

   Panel content
</ts-expansion-panel>

<example-url>https://getterminus.github.io/ui-demos-release/components/expansion-panel</example-url>

Metadata

changeDetection ChangeDetectionStrategy.OnPush
encapsulation ViewEncapsulation.None
exportAs tsExpansionPanel
host {
}
providers { provide: TS_ACCORDION, useValue: undefined, }
selector ts-expansion-panel
styleUrls ./expansion-panel.component.scss
templateUrl ./expansion-panel.component.html

Index

Properties
Inputs
Outputs
Accessors

Constructor

constructor(_changeDetectorRef: ChangeDetectorRef, _uniqueSelectionDispatcher: UniqueSelectionDispatcher, _viewContainerRef: ViewContainerRef, documentService: TsDocumentService, accordion: TsAccordionBase, animationMode?: string, defaultOptions?: TsExpansionPanelDefaultOptions)
Parameters :
Name Type Optional
_changeDetectorRef ChangeDetectorRef No
_uniqueSelectionDispatcher UniqueSelectionDispatcher No
_viewContainerRef ViewContainerRef No
documentService TsDocumentService No
accordion TsAccordionBase No
animationMode string Yes
defaultOptions TsExpansionPanelDefaultOptions Yes

Inputs

hideToggle
Type : boolean

Determine if the toggle indicator should be hidden

isDisabled
Type : boolean

Define if the panel should be disabled

NOTE: CdkAccordionItem defines an input called disabled. This alias is to conform to our existing naming convention.

isExpanded
Type : boolean

Define if the panel should be open

NOTE: CdkAccordionItem defines an input called expanded. This alias is to conform to our existing naming convention.

transparentMode
Type : boolean

Support for transparent mode. Default set to false

Outputs

afterCollapse
Type : EventEmitter<void>

The event emitted after the panel body's collapse animation finishes

afterExpand
Type : EventEmitter<void>

The event emitted after the panel body's expansion animation finishes

Properties

Public accordion
Type : TsAccordionBase

Optionally defined accordion the expansion panel belongs to

NOTE: This should be TsAccordionBase | undefined but the underlying class doesn't define it as possibly undefined so we cannot do so here.

Public Optional animationMode
Type : string
Decorators :
@Optional()
@Inject(ANIMATION_MODULE_TYPE)
Public bodyAnimationDone
Default value : new Subject<AnimationEvent>()

Stream of body animation done events

Public Readonly inputChanges
Default value : new Subject<SimpleChanges>()

Stream that emits for changes in @Input properties

Public lazyContent
Type : TsExpansionPanelContentDirective
Decorators :
@ContentChild(TsExpansionPanelContentDirective)

Reference to a passed in template (for lazy loading)

Public panelBody
Type : ElementRef<HTMLElement>
Decorators :
@ViewChild('panelBody', {static: true})

The element containing the panel's user-provided content

Public portal
Type : TemplatePortal | undefined

Portal holding the user's content

Public triggerId
Default value : `ts-expansion-panel-trigger-${nextUniqueId++}`

The ID for the associated trigger element. Used for a11y labelling.

Accessors

currentExpandedState
getcurrentExpandedState()

Get the current expanded state

contentContainsFocus
getcontentContainsFocus()

Determine whether the expansion panel's content contains the currently-focused element

Returns : boolean
hideToggle
gethideToggle()
sethideToggle(value: boolean)

Determine if the toggle indicator should be hidden

Parameters :
Name Type Optional
value boolean No
Returns : void
isDisabled
getisDisabled()
setisDisabled(value: boolean)

Define if the panel should be disabled

NOTE: CdkAccordionItem defines an input called disabled. This alias is to conform to our existing naming convention.

Parameters :
Name Type Optional
value boolean No
Returns : void
isExpanded
getisExpanded()
setisExpanded(value: boolean)

Define if the panel should be open

NOTE: CdkAccordionItem defines an input called expanded. This alias is to conform to our existing naming convention.

Parameters :
Name Type Optional
value boolean No
Returns : void
transparentMode
gettransparentMode()
settransparentMode(value: boolean)

Support for transparent mode. Default set to false

Parameters :
Name Type Optional
value boolean No
Returns : void
import { AnimationEvent } from '@angular/animations';
import { CdkAccordionItem } from '@angular/cdk/accordion';
import { UniqueSelectionDispatcher } from '@angular/cdk/collections';
import { TemplatePortal } from '@angular/cdk/portal';
import {
  AfterContentInit,
  ChangeDetectionStrategy,
  ChangeDetectorRef,
  Component,
  ContentChild,
  ElementRef,
  EventEmitter,
  Inject,
  InjectionToken,
  Input,
  OnChanges,
  OnDestroy,
  Optional,
  Output,
  SimpleChanges,
  SkipSelf,
  ViewChild,
  ViewContainerRef,
  ViewEncapsulation,
} from '@angular/core';
import { ANIMATION_MODULE_TYPE } from '@angular/platform-browser/animations';
import { Subject } from 'rxjs';
import {
  distinctUntilChanged,
  filter,
  startWith,
  take,
} from 'rxjs/operators';

import {
  TsDocumentService,
  untilComponentDestroyed,
} from '@terminus/fe-utilities';


import {
  TS_ACCORDION,
  TsAccordionBase,
} from '../accordion/accordion-base';
import { tsExpansionPanelAnimations } from './expansion-animations';
import { TsExpansionPanelContentDirective } from './expansion-panel-content.directive';


/**
 * The possible states for a {@link TsExpansionPanelComponent}
 */
export type TsExpansionPanelState = 'expanded' | 'collapsed';


/**
 * Object that can be used to override the default options for all of the expansion panels in a module.
 */
export interface TsExpansionPanelDefaultOptions {
  /**
   * Height of the trigger while the panel is expanded
   */
  expandedHeight: string;
  /**
   * Height of the trigger while the panel is collapsed
   */
  collapsedHeight: string;
  /**
   * Whether the toggle indicator should be hidden
   */
  hideToggle: boolean;
}


/**
 * Injection token that can be used to configure the defalt options for the expansion panel component.
 */
export const TS_EXPANSION_PANEL_DEFAULT_OPTIONS = new InjectionToken<TsExpansionPanelDefaultOptions>('TS_EXPANSION_PANEL_DEFAULT_OPTIONS');

/**
 * Unique ID for each panel trigger ID
 */
let nextUniqueId = 0;


/**
 * An expansion panel component to show/hide content
 *
 * @example
 * <ts-expansion-panel
 *               [hideToggle]="true"
 *               [isExpanded]="true"
 *               [isDisabled]="true"
 *               [transparentMode]="false"
 *               (opened)="panelOpened()"
 *               (closed)="panelClosed()"
 *               (expandedChange)="panelStateChanged($event)"
 *               (destroyed)="componentDestroyed()"
 *               (afterCollapse)="collapseAnimationDone"
 *               (afterExpand)="expandAnimationDone()"
 * >
 *               <ts-expansion-panel-trigger>
 *                 Panel trigger
 *               </ts-expansion-panel-trigger>
 *
 *               Panel content
 * </ts-expansion-panel>
 *
 * <example-url>https://getterminus.github.io/ui-demos-release/components/expansion-panel</example-url>
 */
@Component({
  selector: 'ts-expansion-panel',
  templateUrl: './expansion-panel.component.html',
  styleUrls: ['./expansion-panel.component.scss'],
  // NOTE: @Outputs are defined here rather than using decorators since we are extending the @Outputs of the base class
  // eslint-disable-next-line @angular-eslint/no-outputs-metadata-property
  outputs: [
    'opened',
    'closed',
    'expandedChange',
    'destroyed',
  ],
  animations: [tsExpansionPanelAnimations.bodyExpansion],
  host: {
    'class': 'ts-expansion-panel',
    '[class.ts-expansion-panel--shadow]': '!transparentMode',
    '[class.ts-expansion-panel--expanded]': 'expanded',
    '[class.ts-expansion-panel--animation-noopable]': 'animationMode === "NoopAnimations"',
  },
  providers: [
    // Provide TsAccordionComponent as undefined to prevent nested expansion panels from registering to the same accordion.
    {
      provide: TS_ACCORDION,
      useValue: undefined,
    },
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  encapsulation: ViewEncapsulation.None,
  exportAs: 'tsExpansionPanel',
})
export class TsExpansionPanelComponent extends CdkAccordionItem implements AfterContentInit, OnChanges, OnDestroy {
  /**
   * Stream of body animation done events
   */
  public bodyAnimationDone = new Subject<AnimationEvent>();

  /**
   * The ID for the associated trigger element. Used for a11y labelling.
   */
  public triggerId = `ts-expansion-panel-trigger-${nextUniqueId++}`;

  /**
   * Portal holding the user's content
   */
  public portal: TemplatePortal | undefined;

  /**
   * Stream that emits for changes in `@Input` properties
   */
  public readonly inputChanges = new Subject<SimpleChanges>();

  /**
   * Optionally defined accordion the expansion panel belongs to
   *
   * NOTE: This should be `TsAccordionBase | undefined` but the underlying class doesn't define it as possibly undefined so we cannot
   * do so here.
   */
  public accordion: TsAccordionBase;

  /**
   * Get the current expanded state
   */
  public get currentExpandedState(): TsExpansionPanelState {
    return this.expanded ? 'expanded' : 'collapsed';
  }

  /**
   * Determine whether the expansion panel's content contains the currently-focused element
   */
  public get contentContainsFocus(): boolean {
    if (this.panelBody && this.documentService.document) {
      const focusedElement = this.documentService.document.activeElement;
      const bodyElement = this.panelBody.nativeElement;
      return focusedElement === bodyElement || bodyElement.contains(focusedElement);
    }

    return false;
  }

  /**
   * Reference to a passed in template (for lazy loading)
   */
  @ContentChild(TsExpansionPanelContentDirective)
  public lazyContent!: TsExpansionPanelContentDirective;

  /**
   * The element containing the panel's user-provided content
   */
  @ViewChild('panelBody', { static: true })
  public panelBody!: ElementRef<HTMLElement>;

  /**
   * Determine if the toggle indicator should be hidden
   *
   * @param value
   */
  @Input()
  public set hideToggle(value: boolean) {
    this._hideToggle = value;
  }
  public get hideToggle(): boolean {
    return this._hideToggle || (this.accordion && this.accordion.hideToggle);
  }
  private _hideToggle = false;

  /**
   * Define if the panel should be disabled
   *
   * NOTE: CdkAccordionItem defines an input called `disabled`.
   * This alias is to conform to our existing naming convention.
   *
   * @param value
   */
  @Input()
  public set isDisabled(value: boolean) {
    this.disabled = value;
  }
  public get isDisabled(): boolean {
    return this.disabled;
  }

  /**
   * Define if the panel should be open
   *
   * NOTE: CdkAccordionItem defines an input called `expanded`.
   * This alias is to conform to our existing naming convention.
   *
   * @param value
   */
  @Input()
  public set isExpanded(value: boolean) {
    this.expanded = value;
  }
  public get isExpanded(): boolean {
    return this.expanded;
  }

  /**
   * Support for transparent mode. Default set to false
   *
   * @param value
   */
  @Input()
  public set transparentMode(value: boolean) {
    this._transparentMode = value;
  }
  public get transparentMode(): boolean {
    return this._transparentMode;
  }
  private _transparentMode = false;

  /**
   * The event emitted after the panel body's expansion animation finishes
   */
  @Output()
  public readonly afterExpand: EventEmitter<void> = new EventEmitter();

  /**
   * The event emitted after the panel body's collapse animation finishes
   */
  @Output()
  public readonly afterCollapse: EventEmitter<void> = new EventEmitter();


  constructor(
    _changeDetectorRef: ChangeDetectorRef,
    protected _uniqueSelectionDispatcher: UniqueSelectionDispatcher,
    private _viewContainerRef: ViewContainerRef,
    private documentService: TsDocumentService,
    @Optional() @SkipSelf() @Inject(TS_ACCORDION) accordion: TsAccordionBase,
    @Optional() @Inject(ANIMATION_MODULE_TYPE) public animationMode?: string,
    @Optional() @Inject(TS_EXPANSION_PANEL_DEFAULT_OPTIONS) defaultOptions?: TsExpansionPanelDefaultOptions,
  ) {
    super(accordion, _changeDetectorRef, _uniqueSelectionDispatcher);

    this.accordion = accordion;

    // We need a Subject with distinctUntilChanged, because the `done` event fires twice on some browsers.
    // See https://github.com/angular/angular/issues/24084
    this.bodyAnimationDone.pipe(
      untilComponentDestroyed(this),
      distinctUntilChanged((x, y) => x.fromState === y.fromState && x.toState === y.toState),
    ).subscribe(event => {
      // istanbul ignore else
      if (event.fromState !== 'void') {
        if (event.toState === 'expanded') {
          this.afterExpand.emit();
        } else if (event.toState === 'collapsed') {
          this.afterCollapse.emit();
        }
      }
    });

    if (defaultOptions) {
      this.hideToggle = defaultOptions.hideToggle;
    }
  }


  /**
   * If a lazy-loaded template exists, inject it after the panel is opened
   */
  public ngAfterContentInit(): void {
    // istanbul ignore else
    if (this.lazyContent) {
      // Render the content as soon as the panel becomes open.
      this.opened.pipe(
        // eslint-disable-next-line deprecation/deprecation
        startWith<void, null>(null),
        filter(() => this.expanded && !this.portal),
        take(1),
      ).subscribe(() => {
        this.portal = new TemplatePortal(this.lazyContent.template, this._viewContainerRef);
      });
    }
  }

  /**
   * Send any input changes through the Subject stream
   *
   * @param changes
   */
  public ngOnChanges(changes: SimpleChanges): void {
    this.inputChanges.next(changes);
  }

  /**
   * Destroy the parent and finalize any subscriptions
   */
  public ngOnDestroy(): void {
    super.ngOnDestroy();
    this.inputChanges.complete();
  }
}

result-matching ""

    No results matching ""