@progress/kendo-angular-layout
Version:
Kendo UI for Angular Layout Package - a collection of components to create professional application layoyts
505 lines (504 loc) • 19.8 kB
JavaScript
/**-----------------------------------------------------------------------------------------
* Copyright © 2025 Progress Software Corporation. All rights reserved.
* Licensed under commercial license. See LICENSE.md in the project root for more information
*-------------------------------------------------------------------------------------------*/
import { AnimationBuilder } from '@angular/animations';
import { isFocusable, hasClass } from './../common/dom-queries';
import { Component, ContentChild, EventEmitter, HostBinding, Input, Output, ElementRef, Renderer2, NgZone, ViewChild, isDevMode } from '@angular/core';
import { ExpansionPanelTitleDirective } from './expansionpanel-title.directive';
import { collapse, expand } from './animations';
import { isPresent } from '../common/util';
import { Subscription } from 'rxjs';
import { Keys } from '@progress/kendo-angular-common';
import { L10N_PREFIX, LocalizationService } from '@progress/kendo-angular-l10n';
import { validatePackage } from '@progress/kendo-licensing';
import { packageMetadata } from '../package-metadata';
import { ExpansionPanelActionEvent } from './events/action-event';
import { take } from 'rxjs/operators';
import { chevronDownIcon, chevronUpIcon } from '@progress/kendo-svg-icons';
import { IconWrapperComponent } from '@progress/kendo-angular-icons';
import { NgIf, NgTemplateOutlet } from '@angular/common';
import * as i0 from "@angular/core";
import * as i1 from "@progress/kendo-angular-l10n";
import * as i2 from "@angular/animations";
const DEFAULT_DURATION = 200;
const CONTENT_HIDDEN_CLASS = 'k-hidden';
/**
* Represents the [Kendo UI ExpansionPanel component for Angular]({% slug overview_expansionpanel %}).
*
* @example
* ```ts-preview
* _@Component({
* selector: 'my-app',
* template: `
* <kendo-expansionpanel title="Chile" subtitle="South America">
* There are various theories about the origin of the word Chile.
* </kendo-expansionpanel>
* `
* })
* class AppComponent {}
* ```
*/
export class ExpansionPanelComponent {
renderer;
hostElement;
ngZone;
localizationService;
builder;
/**
* Specifies the primary text in the header of the ExpansionPanel
* ([see example](slug:title_expansionpanel#toc-titles-and-subtitles)).
*/
title = '';
/**
* Specifies the secondary text in the header of the ExpansionPanel, which is rendered next to the collapse/expand icon
* ([see example](slug:title_expansionpanel#toc-titles-and-subtitles)).
*/
subtitle = '';
/**
* Specifies whether the ExpansionPanel is disabled. If disabled, the ExpansionPanel can be neither expanded nor collapsed
* ([see example]({% slug disabled_expansionpanel %})).
*
* @default false
*/
disabled = false;
/**
* Specifies whether the ExpansionPanel is expanded. The property supports two-way binding.
* ([see example]({% slug interaction_expansionpanel %}#toc-setting-the-initial-state)).
*
* @default false
*/
set expanded(value) {
if (value === this.expanded) {
return;
}
this._expanded = value;
if (this.expanded) {
this.removeContentHiddenClass();
}
else {
this.addContentHiddenClass();
}
}
get expanded() {
return this._expanded;
}
/**
* Defines an SVGIcon for the expanded state of the component.
* The input can take either an [existing Kendo SVG icon](slug:svgicon_list) or a custom one.
*/
set svgExpandIcon(icon) {
if (isDevMode() && icon && this.expandIcon) {
throw new Error('Setting both expandIcon/svgExpandIcon options at the same time is not supported.');
}
this._svgExpandIcon = icon;
}
get svgExpandIcon() {
return this._svgExpandIcon;
}
/**
* Defines an SVGIcon for the collapsed state of the component.
* The input can take either an [existing Kendo SVG icon](slug:svgicon_list) or a custom one.
*/
set svgCollapseIcon(icon) {
if (isDevMode() && icon && this.collapseIcon) {
throw new Error('Setting both collapseIcon/svgCollapseIcon options at the same time is not supported.');
}
this._svgCollapseIcon = icon;
}
get svgCollapseIcon() {
return this._svgCollapseIcon;
}
/**
* Sets a custom icon via css class(es), for the collapsed state of the component
* ([see example]({% slug icons_expansionpanel %}#toc-icons)).
*/
expandIcon;
/**
* Sets a custom icon via css class(es), for the expanded state of the component
* ([see example]({% slug icons_expansionpanel %}#toc-icons)).
*/
collapseIcon;
/**
* Specifies the animation settings of the ExpansionPanel
* ([see example]({% slug animations_expansionpanel %})).
*
* The possible values are:
* * Boolean
* * (Default) `true` Numeric values represent duration. Default duration is 200ms.
* * false
* * Number
*/
animation = true;
/**
* Fires when the `expanded` property of the component is updated.
* Used to provide a two-way binding for the `expanded` property
* ([see example](slug:events_expansionpanel)).
*/
expandedChange = new EventEmitter();
/**
* Fires when the expanded state of the ExpansionPanel is about to change. This event is preventable
* ([see example](slug:events_expansionpanel)).
*/
action = new EventEmitter();
/**
* Fires when the ExpansionPanel is expanded. If there is animation it will fire when the animation is complete
* ([see example](slug:events_expansionpanel)).
*/
expand = new EventEmitter();
/**
* Fires when the ExpansionPanel is collapsed. If there is animation it will fire when the animation is complete
* ([see example](slug:events_expansionpanel)).
*/
collapse = new EventEmitter();
/**
* @hidden
*/
titleTemplate;
content;
header;
hostClass = true;
get expandedClass() {
return this.expanded && !this.disabled;
}
direction;
/**
* @hidden
*/
focused = false;
animationEnd = new EventEmitter();
subscriptions = new Subscription();
_expanded = false;
_svgExpandIcon = chevronDownIcon;
_svgCollapseIcon = chevronUpIcon;
constructor(renderer, hostElement, ngZone, localizationService, builder) {
this.renderer = renderer;
this.hostElement = hostElement;
this.ngZone = ngZone;
this.localizationService = localizationService;
this.builder = builder;
validatePackage(packageMetadata);
this.direction = localizationService.rtl ? 'rtl' : 'ltr';
}
ngOnInit() {
this.renderer.removeAttribute(this.hostElement.nativeElement, 'title');
this.subscriptions = this.localizationService.changes.subscribe(({ rtl }) => { this.direction = rtl ? 'rtl' : 'ltr'; });
const elem = this.hostElement.nativeElement;
const header = this.header.nativeElement;
this.subscriptions.add(this.renderer.listen(header, 'focus', () => this.focusExpansionPanel(elem)));
this.subscriptions.add(this.renderer.listen(header, 'blur', () => this.blurExpansionPanel(elem)));
}
ngAfterViewInit() {
this.initDomEvents();
if (!this.expanded) {
this.renderer.addClass(this.content.nativeElement, CONTENT_HIDDEN_CLASS);
}
}
ngOnDestroy() {
this.subscriptions.unsubscribe();
}
/**
* @hidden
*/
initDomEvents() {
if (!this.hostElement) {
return;
}
this.ngZone.runOutsideAngular(() => {
const elem = this.hostElement.nativeElement;
this.subscriptions.add(this.renderer.listen(elem, 'keydown', this.keyDownHandler.bind(this)));
});
}
/**
* @hidden
*/
keyDownHandler(ev) {
const isEnterOrSpace = ev.keyCode === Keys.Enter || ev.keyCode === Keys.Space;
if (this.disabled || !isEnterOrSpace) {
return;
}
if (hasClass(ev.target, 'k-expander-header')) {
ev.preventDefault();
this.ngZone.run(() => {
this.onHeaderAction();
});
}
}
/**
* @hidden
*/
onHeaderClick(ev) {
const header = this.header.nativeElement;
if (!isFocusable(ev.target) || (ev.target === header) && !this.disabled) {
this.onHeaderAction();
}
}
/**
* @hidden
*/
onHeaderAction() {
const eventArgs = new ExpansionPanelActionEvent();
eventArgs.action = this.expanded ? 'collapse' : 'expand';
this.action.emit(eventArgs);
if (!eventArgs.isDefaultPrevented()) {
this.setExpanded(!this.expanded);
if (this.expanded) {
this.removeContentHiddenClass();
}
if (this.animation) {
this.animateContent();
return;
}
if (!this.expanded) {
this.addContentHiddenClass();
}
this.emitExpandCollapseEvent();
}
}
/**
* @hidden
*/
get expanderIndicatorClasses() {
if (this.expanded) {
return !this.collapseIcon ? `chevron-up` : '';
}
else {
return !this.expandIcon ? `chevron-down` : '';
}
}
/**
* @hidden
*/
get customExpanderIndicatorClasses() {
if (this.expanded) {
return this.collapseIcon ? this.collapseIcon : '';
}
else {
return this.expandIcon ? this.expandIcon : '';
}
}
/**
* @hidden
*/
get expanderSvgIcon() {
return this.expanded ? this.svgCollapseIcon : this.svgExpandIcon;
}
/**
* Toggles the visibility of the ExpansionPanel
* ([see example](slug:interaction_expansionpanel#toggling-between-states)).
*
* @param expanded? - Boolean. Specifies, whether the ExpansionPanel will be expanded or collapsed.
*/
toggle(expanded) {
const previous = this.expanded;
const current = isPresent(expanded) ? expanded : !previous;
if (current === previous) {
return;
}
this.setExpanded(current);
if (this.expanded) {
this.removeContentHiddenClass();
}
if (this.animation) {
this.animateContent();
return;
}
if (!this.expanded) {
this.addContentHiddenClass();
}
this.emitExpandCollapseEvent();
}
focusExpansionPanel(el) {
if (!this.focused) {
this.focused = true;
this.renderer.addClass(el, 'k-focus');
}
}
blurExpansionPanel(el) {
if (this.focused) {
this.focused = false;
this.renderer.removeClass(el, 'k-focus');
}
}
setExpanded(value) {
this._expanded = value;
this.expandedChange.emit(value);
}
animateContent() {
const duration = typeof this.animation === 'boolean' ? DEFAULT_DURATION : this.animation;
const contentHeight = getComputedStyle(this.content.nativeElement).height;
const animation = this.expanded ? expand(duration, contentHeight) : collapse(duration, contentHeight);
const player = this.createPlayer(animation, this.content.nativeElement);
this.animationEnd.pipe(take(1)).subscribe(() => {
if (!this.expanded) {
this.addContentHiddenClass();
}
this.emitExpandCollapseEvent();
});
player.play();
}
createPlayer(animation, animatedElement) {
const factory = this.builder.build(animation);
let player = factory.create(animatedElement);
player.onDone(() => {
if (player) {
this.animationEnd.emit();
player.destroy();
player = null;
}
});
return player;
}
emitExpandCollapseEvent() {
this[this.expanded ? 'expand' : 'collapse'].emit();
}
addContentHiddenClass() {
this.renderer.addClass(this.content.nativeElement, CONTENT_HIDDEN_CLASS);
}
removeContentHiddenClass() {
this.renderer.removeClass(this.content.nativeElement, CONTENT_HIDDEN_CLASS);
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ExpansionPanelComponent, deps: [{ token: i0.Renderer2 }, { token: i0.ElementRef }, { token: i0.NgZone }, { token: i1.LocalizationService }, { token: i2.AnimationBuilder }], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: ExpansionPanelComponent, isStandalone: true, selector: "kendo-expansionpanel", inputs: { title: "title", subtitle: "subtitle", disabled: "disabled", expanded: "expanded", svgExpandIcon: "svgExpandIcon", svgCollapseIcon: "svgCollapseIcon", expandIcon: "expandIcon", collapseIcon: "collapseIcon", animation: "animation" }, outputs: { expandedChange: "expandedChange", action: "action", expand: "expand", collapse: "collapse" }, host: { properties: { "class.k-expander": "this.hostClass", "class.k-expanded": "this.expandedClass", "attr.dir": "this.direction" } }, providers: [
LocalizationService,
{
provide: L10N_PREFIX,
useValue: 'kendo.expansionpanel'
}
], queries: [{ propertyName: "titleTemplate", first: true, predicate: ExpansionPanelTitleDirective, descendants: true }], viewQueries: [{ propertyName: "content", first: true, predicate: ["content"], descendants: true, static: true }, { propertyName: "header", first: true, predicate: ["header"], descendants: true, static: true }], exportAs: ["kendoExpansionPanel"], ngImport: i0, template: `
<div
#header
[class.k-expander-header]="true"
[class.k-disabled]="disabled"
[attr.aria-disabled]="disabled"
[attr.aria-expanded]="expanded && !disabled"
role="button"
tabindex="0"
[attr.aria-controls]="title"
(click)="onHeaderClick($event)"
>
<ng-container *ngIf="!titleTemplate">
<div *ngIf="title" class="k-expander-title">{{ title }}</div>
<span class="k-spacer"></span>
<div *ngIf="subtitle" class="k-expander-sub-title">
{{ subtitle }}
</div>
</ng-container>
<ng-template
*ngIf="titleTemplate"
[ngTemplateOutlet]="titleTemplate?.templateRef">
</ng-template>
<span class="k-expander-indicator">
<kendo-icon-wrapper
[name]="expanderIndicatorClasses"
[customFontClass]="customExpanderIndicatorClasses"
[svgIcon]="expanderSvgIcon"
>
</kendo-icon-wrapper>
</span>
</div>
<div #content [id]="title" class="k-expander-content-wrapper">
<div class="k-expander-content" [attr.aria-hidden]="!expanded">
<ng-content></ng-content>
</div>
</div>
`, isInline: true, dependencies: [{ kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: IconWrapperComponent, selector: "kendo-icon-wrapper", inputs: ["name", "svgIcon", "innerCssClass", "customFontClass", "size"], exportAs: ["kendoIconWrapper"] }] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ExpansionPanelComponent, decorators: [{
type: Component,
args: [{
exportAs: 'kendoExpansionPanel',
providers: [
LocalizationService,
{
provide: L10N_PREFIX,
useValue: 'kendo.expansionpanel'
}
],
selector: 'kendo-expansionpanel',
template: `
<div
#header
[class.k-expander-header]="true"
[class.k-disabled]="disabled"
[attr.aria-disabled]="disabled"
[attr.aria-expanded]="expanded && !disabled"
role="button"
tabindex="0"
[attr.aria-controls]="title"
(click)="onHeaderClick($event)"
>
<ng-container *ngIf="!titleTemplate">
<div *ngIf="title" class="k-expander-title">{{ title }}</div>
<span class="k-spacer"></span>
<div *ngIf="subtitle" class="k-expander-sub-title">
{{ subtitle }}
</div>
</ng-container>
<ng-template
*ngIf="titleTemplate"
[ngTemplateOutlet]="titleTemplate?.templateRef">
</ng-template>
<span class="k-expander-indicator">
<kendo-icon-wrapper
[name]="expanderIndicatorClasses"
[customFontClass]="customExpanderIndicatorClasses"
[svgIcon]="expanderSvgIcon"
>
</kendo-icon-wrapper>
</span>
</div>
<div #content [id]="title" class="k-expander-content-wrapper">
<div class="k-expander-content" [attr.aria-hidden]="!expanded">
<ng-content></ng-content>
</div>
</div>
`,
standalone: true,
imports: [NgIf, NgTemplateOutlet, IconWrapperComponent]
}]
}], ctorParameters: function () { return [{ type: i0.Renderer2 }, { type: i0.ElementRef }, { type: i0.NgZone }, { type: i1.LocalizationService }, { type: i2.AnimationBuilder }]; }, propDecorators: { title: [{
type: Input
}], subtitle: [{
type: Input
}], disabled: [{
type: Input
}], expanded: [{
type: Input
}], svgExpandIcon: [{
type: Input
}], svgCollapseIcon: [{
type: Input
}], expandIcon: [{
type: Input
}], collapseIcon: [{
type: Input
}], animation: [{
type: Input
}], expandedChange: [{
type: Output
}], action: [{
type: Output
}], expand: [{
type: Output
}], collapse: [{
type: Output
}], titleTemplate: [{
type: ContentChild,
args: [ExpansionPanelTitleDirective, { static: false }]
}], content: [{
type: ViewChild,
args: ['content', { static: true }]
}], header: [{
type: ViewChild,
args: ['header', { static: true }]
}], hostClass: [{
type: HostBinding,
args: ['class.k-expander']
}], expandedClass: [{
type: HostBinding,
args: ['class.k-expanded']
}], direction: [{
type: HostBinding,
args: ['attr.dir']
}] } });