libs/ui/expansion-panel/src/lib/panel/expansion-panel.component.ts
An expansion panel component to show/hide content
CdkAccordionItem
AfterContentInit
OnChanges
OnDestroy
<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>
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 |
Properties |
|
Inputs |
Outputs |
Accessors |
constructor(_changeDetectorRef: ChangeDetectorRef, _uniqueSelectionDispatcher: UniqueSelectionDispatcher, _viewContainerRef: ViewContainerRef, documentService: TsDocumentService, accordion: TsAccordionBase, animationMode?: string, defaultOptions?: TsExpansionPanelDefaultOptions)
|
||||||||||||||||||||||||
Parameters :
|
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 |
isExpanded | |
Type : boolean
|
|
Define if the panel should be open NOTE: CdkAccordionItem defines an input called |
transparentMode | |
Type : boolean
|
|
Support for transparent mode. Default set to false |
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 |
Public accordion |
Type : TsAccordionBase
|
Optionally defined accordion the expansion panel belongs to NOTE: This should be |
Public Optional animationMode |
Type : string
|
Decorators :
@Optional()
|
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 |
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. |
currentExpandedState |
getcurrentExpandedState()
|
Get the current expanded state
Returns :
TsExpansionPanelState
|
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 :
Returns :
void
|
isDisabled | ||||||
getisDisabled()
|
||||||
setisDisabled(value: boolean)
|
||||||
Define if the panel should be disabled NOTE: CdkAccordionItem defines an input called
Parameters :
Returns :
void
|
isExpanded | ||||||
getisExpanded()
|
||||||
setisExpanded(value: boolean)
|
||||||
Define if the panel should be open NOTE: CdkAccordionItem defines an input called
Parameters :
Returns :
void
|
transparentMode | ||||||
gettransparentMode()
|
||||||
settransparentMode(value: boolean)
|
||||||
Support for transparent mode. Default set to false
Parameters :
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();
}
}