igniteui-angular-sovn
Version:
Ignite UI for Angular is a dependency-free Angular toolkit for building modern web apps
410 lines (379 loc) • 14.1 kB
text/typescript
import { AfterContentInit, AfterViewInit, ChangeDetectorRef, Component, ContentChildren, EventEmitter,
HostBinding, Input, OnDestroy, Output, QueryList } from '@angular/core';
import { fromEvent, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { ACCORDION_NAVIGATION_KEYS } from '../core/utils';
import { IExpansionPanelCancelableEventArgs,
IExpansionPanelEventArgs, IgxExpansionPanelBase } from '../expansion-panel/expansion-panel.common';
import { IgxExpansionPanelComponent } from '../expansion-panel/expansion-panel.component';
import { ToggleAnimationSettings } from '../expansion-panel/toggle-animation-component';
export interface IAccordionEventArgs extends IExpansionPanelEventArgs {
owner: IgxAccordionComponent;
/** Provides a reference to the `IgxExpansionPanelComponent` which was expanded/collapsed. */
panel: IgxExpansionPanelBase;
}
export interface IAccordionCancelableEventArgs extends IExpansionPanelCancelableEventArgs {
owner: IgxAccordionComponent;
/** Provides a reference to the `IgxExpansionPanelComponent` which is currently expanding/collapsing. */
panel: IgxExpansionPanelBase;
}
let NEXT_ID = 0;
/**
* IgxAccordion is a container-based component that contains that can house multiple expansion panels.
*
* @igxModule IgxAccordionModule
*
* @igxKeywords accordion
*
* @igxGroup Layouts
*
* @remark
* The Ignite UI for Angular Accordion component enables the user to navigate among multiple collapsing panels
* displayed in a single container.
* The accordion offers keyboard navigation and API to control the underlying panels' expansion state.
*
* @example
* ```html
* <igx-accordion>
* <igx-expansion-panel *ngFor="let panel of panels">
* ...
* </igx-expansion-panel>
* </igx-accordion>
* ```
*/
export class IgxAccordionComponent implements AfterContentInit, AfterViewInit, OnDestroy {
/**
* Get/Set the `id` of the accordion component.
* Default value is `"igx-accordion-0"`;
* ```html
* <igx-accordion id="my-first-accordion"></igx-accordion>
* ```
* ```typescript
* const accordionId = this.accordion.id;
* ```
*/
public id = `igx-accordion-${NEXT_ID++}`;
/** @hidden @internal **/
public cssClass = 'igx-accordion';
/** @hidden @internal **/
public displayStyle = 'block';
/**
* Get/Set the animation settings that panels should use when expanding/collpasing.
*
* ```html
* <igx-accordion [animationSettings]="customAnimationSettings"></igx-accordion>
* ```
*
* ```typescript
* const customAnimationSettings: ToggleAnimationSettings = {
* openAnimation: growVerIn,
* closeAnimation: growVerOut
* };
*
* this.accordion.animationSettings = customAnimationSettings;
* ```
*/
public get animationSettings(): ToggleAnimationSettings {
return this._animationSettings;
}
public set animationSettings(value: ToggleAnimationSettings) {
this._animationSettings = value;
this.updatePanelsAnimation();
}
/**
* Get/Set how the accordion handles the expansion of the projected expansion panels.
* If set to `true`, only a single panel can be expanded at a time, collapsing all others
*
* ```html
* <igx-accordion [singleBranchExpand]="true">
* ...
* </igx-accordion>
* ```
*
* ```typescript
* this.accordion.singleBranchExpand = false;
* ```
*/
public get singleBranchExpand(): boolean {
return this._singleBranchExpand;
}
public set singleBranchExpand(val: boolean) {
this._singleBranchExpand = val;
if (val) {
this.collapseAllExceptLast();
}
}
/**
* Emitted before a panel is expanded.
*
* @remarks
* This event is cancelable.
*
* ```html
* <igx-accordion (panelExpanding)="handlePanelExpanding($event)">
* </igx-accordion>
* ```
*
*```typescript
* public handlePanelExpanding(event: IExpansionPanelCancelableEventArgs){
* const expandedPanel: IgxExpansionPanelComponent = event.panel;
* if (expandedPanel.disabled) {
* event.cancel = true;
* }
* }
*```
*/
public panelExpanding = new EventEmitter<IAccordionCancelableEventArgs>();
/**
* Emitted after a panel has been expanded.
*
* ```html
* <igx-accordion (panelExpanded)="handlePanelExpanded($event)">
* </igx-accordion>
* ```
*
*```typescript
* public handlePanelExpanded(event: IExpansionPanelCancelableEventArgs) {
* const expandedPanel: IgxExpansionPanelComponent = event.panel;
* console.log("Panel is expanded: ", expandedPanel.id);
* }
*```
*/
public panelExpanded = new EventEmitter<IAccordionEventArgs>();
/**
* Emitted before a panel is collapsed.
*
* @remarks
* This event is cancelable.
*
* ```html
* <igx-accordion (panelCollapsing)="handlePanelCollapsing($event)">
* </igx-accordion>
* ```
*/
public panelCollapsing = new EventEmitter<IAccordionCancelableEventArgs>();
/**
* Emitted after a panel has been collapsed.
*
* ```html
* <igx-accordion (panelCollapsed)="handlePanelCollapsed($event)">
* </igx-accordion>
* ```
*/
public panelCollapsed = new EventEmitter<IAccordionEventArgs>();
/**
* Get all panels.
*
* ```typescript
* const panels: IgxExpansionPanelComponent[] = this.accordion.panels;
* ```
*/
public get panels(): IgxExpansionPanelComponent[] {
return this._panels?.toArray();
}
private _panels!: QueryList<IgxExpansionPanelComponent>;
private _animationSettings!: ToggleAnimationSettings;
private _expandedPanels!: Set<IgxExpansionPanelComponent>;
private _expandingPanels!: Set<IgxExpansionPanelComponent>;
private _destroy$ = new Subject<void>();
private _unsubChildren$ = new Subject<void>();
private _enabledPanels!: IgxExpansionPanelComponent[];
private _singleBranchExpand = false;
constructor(private cdr: ChangeDetectorRef) { }
/** @hidden @internal **/
public ngAfterContentInit(): void {
this.updatePanelsAnimation();
if (this.singleBranchExpand) {
this.collapseAllExceptLast();
}
}
/** @hidden @internal **/
public ngAfterViewInit(): void {
this._expandedPanels = new Set<IgxExpansionPanelComponent>(this._panels.filter(panel => !panel.collapsed));
this._expandingPanels = new Set<IgxExpansionPanelComponent>();
this._panels.changes.pipe(takeUntil(this._destroy$)).subscribe(() => {
this.subToChanges();
});
this.subToChanges();
}
/** @hidden @internal */
public ngOnDestroy(): void {
this._unsubChildren$.next();
this._unsubChildren$.complete();
this._destroy$.next();
this._destroy$.complete();
}
/**
* Expands all collapsed expansion panels.
*
* ```typescript
* accordion.expandAll();
* ```
*/
public expandAll(): void {
if (this.singleBranchExpand) {
for(let i = 0; i < this.panels.length - 1; i++) {
this.panels[i].collapse();
}
this._panels.last.expand();
return;
}
this.panels.forEach(panel => panel.expand());
}
/**
* Collapses all expanded expansion panels.
*
* ```typescript
* accordion.collapseAll();
* ```
*/
public collapseAll(): void {
this.panels.forEach(panel => panel.collapse());
}
private collapseAllExceptLast(): void {
const lastExpanded = this.panels?.filter(p => !p.collapsed && !p.header.disabled).pop();
this.panels?.forEach((p: IgxExpansionPanelComponent) => {
if (p !== lastExpanded && !p.header.disabled) {
p.collapsed = true;
}
});
this.cdr.detectChanges();
}
private handleKeydown(event: KeyboardEvent, panel: IgxExpansionPanelComponent): void {
const key = event.key.toLowerCase();
if (!(ACCORDION_NAVIGATION_KEYS.has(key))) {
return;
}
// TO DO: if we ever want to improve the performance of the accordion,
// enabledPanels could be cached (by making a disabledChange emitter on the panel header)
this._enabledPanels = this._panels.filter(p => !p.header.disabled);
event.preventDefault();
this.handleNavigation(event, panel);
}
private handleNavigation(event: KeyboardEvent, panel: IgxExpansionPanelComponent): void {
switch (event.key.toLowerCase()) {
case 'home':
this._enabledPanels[0].header.innerElement.focus();
break;
case 'end':
this._enabledPanels[this._enabledPanels.length - 1].header.innerElement.focus();
break;
case 'arrowup':
case 'up':
this.handleUpDownArrow(true, event, panel);
break;
case 'arrowdown':
case 'down':
this.handleUpDownArrow(false, event, panel);
break;
}
}
private handleUpDownArrow(isUp: boolean, event: KeyboardEvent, panel: IgxExpansionPanelComponent): void {
if (!event.altKey) {
const focusedPanel = panel;
const next = this.getNextPanel(focusedPanel, isUp ? -1 : 1);
if (next === focusedPanel) {
return;
}
next.header.innerElement.focus();
}
if (event.altKey && event.shiftKey) {
if (isUp) {
this._enabledPanels.forEach(p => p.collapse());
} else {
if (this.singleBranchExpand) {
for(let i = 0; i < this._enabledPanels.length - 1; i++) {
this._enabledPanels[i].collapse();
}
this._enabledPanels[this._enabledPanels.length - 1].expand();
return;
}
this._enabledPanels.forEach(p => p.expand());
}
}
}
private getNextPanel(panel: IgxExpansionPanelComponent, dir: 1 | -1 = 1): IgxExpansionPanelComponent {
const panelIndex = this._enabledPanels.indexOf(panel);
return this._enabledPanels[panelIndex + dir] || panel;
}
private subToChanges(): void {
this._unsubChildren$.next();
this._panels.forEach(panel => {
panel.contentExpanded.pipe(takeUntil(this._unsubChildren$)).subscribe((args: IExpansionPanelEventArgs) => {
this._expandedPanels.add(args.owner);
this._expandingPanels.delete(args.owner);
const evArgs: IAccordionEventArgs = { ...args, owner: this, panel: args.owner };
this.panelExpanded.emit(evArgs);
});
panel.contentExpanding.pipe(takeUntil(this._unsubChildren$)).subscribe((args: IExpansionPanelCancelableEventArgs) => {
if (args.cancel) {
return;
}
const evArgs: IAccordionCancelableEventArgs = { ...args, owner: this, panel: args.owner };
this.panelExpanding.emit(evArgs);
if (evArgs.cancel) {
args.cancel = true;
return;
}
if (this.singleBranchExpand) {
this._expandedPanels.forEach(p => {
if (!p.header.disabled) {
p.collapse();
}
});
this._expandingPanels.forEach(p => {
if (!p.header.disabled) {
if (!p.animationSettings.closeAnimation) {
p.openAnimationPlayer?.reset();
}
if (!p.animationSettings.openAnimation) {
p.closeAnimationPlayer?.reset();
}
p.collapse();
}
});
this._expandingPanels.add(args.owner);
}
});
panel.contentCollapsed.pipe(takeUntil(this._unsubChildren$)).subscribe((args: IExpansionPanelEventArgs) => {
this._expandedPanels.delete(args.owner);
this._expandingPanels.delete(args.owner);
const evArgs: IAccordionEventArgs = { ...args, owner: this, panel: args.owner };
this.panelCollapsed.emit(evArgs);
});
panel.contentCollapsing.pipe(takeUntil(this._unsubChildren$)).subscribe((args: IExpansionPanelCancelableEventArgs) => {
const evArgs: IAccordionCancelableEventArgs = { ...args, owner: this, panel: args.owner };
this.panelCollapsing.emit(evArgs);
if (evArgs.cancel) {
args.cancel = true;
}
});
fromEvent(panel.header.innerElement, 'keydown')
.pipe(takeUntil(this._unsubChildren$))
.subscribe((e: KeyboardEvent) => {
this.handleKeydown(e, panel);
});
});
}
private updatePanelsAnimation(): void {
if (this.animationSettings !== undefined) {
this.panels?.forEach(panel => panel.animationSettings = this.animationSettings);
}
}
}