276 lines
8.4 KiB
TypeScript
276 lines
8.4 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright Google LLC All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
* found in the LICENSE file at https://angular.dev/license
|
|
*/
|
|
|
|
import {CdkAccordionItem} from '@angular/cdk/accordion';
|
|
import {UniqueSelectionDispatcher} from '@angular/cdk/collections';
|
|
import {CdkPortalOutlet, TemplatePortal} from '@angular/cdk/portal';
|
|
import {DOCUMENT} from '@angular/common';
|
|
import {
|
|
AfterContentInit,
|
|
ChangeDetectionStrategy,
|
|
Component,
|
|
ContentChild,
|
|
Directive,
|
|
ElementRef,
|
|
EventEmitter,
|
|
InjectionToken,
|
|
Input,
|
|
OnChanges,
|
|
OnDestroy,
|
|
Output,
|
|
SimpleChanges,
|
|
ViewChild,
|
|
ViewContainerRef,
|
|
ViewEncapsulation,
|
|
booleanAttribute,
|
|
ANIMATION_MODULE_TYPE,
|
|
inject,
|
|
NgZone,
|
|
} from '@angular/core';
|
|
import {_IdGenerator} from '@angular/cdk/a11y';
|
|
import {Subject} from 'rxjs';
|
|
import {filter, startWith, take} from 'rxjs/operators';
|
|
import {MatAccordionBase, MatAccordionTogglePosition, MAT_ACCORDION} from './accordion-base';
|
|
import {MAT_EXPANSION_PANEL} from './expansion-panel-base';
|
|
import {MatExpansionPanelContent} from './expansion-panel-content';
|
|
|
|
/** MatExpansionPanel's states. */
|
|
export type MatExpansionPanelState = 'expanded' | 'collapsed';
|
|
|
|
/**
|
|
* Object that can be used to override the default options
|
|
* for all of the expansion panels in a module.
|
|
*/
|
|
export interface MatExpansionPanelDefaultOptions {
|
|
/** Height of the header while the panel is expanded. */
|
|
expandedHeight: string;
|
|
|
|
/** Height of the header 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 default
|
|
* options for the expansion panel component.
|
|
*/
|
|
export const MAT_EXPANSION_PANEL_DEFAULT_OPTIONS =
|
|
new InjectionToken<MatExpansionPanelDefaultOptions>('MAT_EXPANSION_PANEL_DEFAULT_OPTIONS');
|
|
|
|
/**
|
|
* This component can be used as a single element to show expandable content, or as one of
|
|
* multiple children of an element with the MatAccordion directive attached.
|
|
*/
|
|
@Component({
|
|
styleUrl: 'expansion-panel.css',
|
|
selector: 'mat-expansion-panel',
|
|
exportAs: 'matExpansionPanel',
|
|
templateUrl: 'expansion-panel.html',
|
|
encapsulation: ViewEncapsulation.None,
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
providers: [
|
|
// Provide MatAccordion as undefined to prevent nested expansion panels from registering
|
|
// to the same accordion.
|
|
{provide: MAT_ACCORDION, useValue: undefined},
|
|
{provide: MAT_EXPANSION_PANEL, useExisting: MatExpansionPanel},
|
|
],
|
|
host: {
|
|
'class': 'mat-expansion-panel',
|
|
'[class.mat-expanded]': 'expanded',
|
|
'[class.mat-expansion-panel-spacing]': '_hasSpacing()',
|
|
},
|
|
imports: [CdkPortalOutlet],
|
|
})
|
|
export class MatExpansionPanel
|
|
extends CdkAccordionItem
|
|
implements AfterContentInit, OnChanges, OnDestroy
|
|
{
|
|
private _viewContainerRef = inject(ViewContainerRef);
|
|
private readonly _animationsDisabled =
|
|
inject(ANIMATION_MODULE_TYPE, {optional: true}) === 'NoopAnimations';
|
|
private _document = inject(DOCUMENT);
|
|
private _ngZone = inject(NgZone);
|
|
private _elementRef = inject<ElementRef<HTMLElement>>(ElementRef);
|
|
|
|
/** Whether the toggle indicator should be hidden. */
|
|
@Input({transform: booleanAttribute})
|
|
get hideToggle(): boolean {
|
|
return this._hideToggle || (this.accordion && this.accordion.hideToggle);
|
|
}
|
|
set hideToggle(value: boolean) {
|
|
this._hideToggle = value;
|
|
}
|
|
private _hideToggle = false;
|
|
|
|
/** The position of the expansion indicator. */
|
|
@Input()
|
|
get togglePosition(): MatAccordionTogglePosition {
|
|
return this._togglePosition || (this.accordion && this.accordion.togglePosition);
|
|
}
|
|
set togglePosition(value: MatAccordionTogglePosition) {
|
|
this._togglePosition = value;
|
|
}
|
|
private _togglePosition: MatAccordionTogglePosition;
|
|
|
|
/** An event emitted after the body's expansion animation happens. */
|
|
@Output() readonly afterExpand = new EventEmitter<void>();
|
|
|
|
/** An event emitted after the body's collapse animation happens. */
|
|
@Output() readonly afterCollapse = new EventEmitter<void>();
|
|
|
|
/** Stream that emits for changes in `@Input` properties. */
|
|
readonly _inputChanges = new Subject<SimpleChanges>();
|
|
|
|
/** Optionally defined accordion the expansion panel belongs to. */
|
|
override accordion = inject<MatAccordionBase>(MAT_ACCORDION, {optional: true, skipSelf: true})!;
|
|
|
|
/** Content that will be rendered lazily. */
|
|
@ContentChild(MatExpansionPanelContent) _lazyContent: MatExpansionPanelContent;
|
|
|
|
/** Element containing the panel's user-provided content. */
|
|
@ViewChild('body') _body: ElementRef<HTMLElement>;
|
|
|
|
/** Element wrapping the panel body. */
|
|
@ViewChild('bodyWrapper')
|
|
protected _bodyWrapper: ElementRef<HTMLElement> | undefined;
|
|
|
|
/** Portal holding the user's content. */
|
|
_portal: TemplatePortal;
|
|
|
|
/** ID for the associated header element. Used for a11y labelling. */
|
|
_headerId: string = inject(_IdGenerator).getId('mat-expansion-panel-header-');
|
|
|
|
constructor(...args: unknown[]);
|
|
|
|
constructor() {
|
|
super();
|
|
|
|
const defaultOptions = inject<MatExpansionPanelDefaultOptions>(
|
|
MAT_EXPANSION_PANEL_DEFAULT_OPTIONS,
|
|
{optional: true},
|
|
);
|
|
|
|
this._expansionDispatcher = inject(UniqueSelectionDispatcher);
|
|
|
|
if (defaultOptions) {
|
|
this.hideToggle = defaultOptions.hideToggle;
|
|
}
|
|
}
|
|
|
|
/** Determines whether the expansion panel should have spacing between it and its siblings. */
|
|
_hasSpacing(): boolean {
|
|
if (this.accordion) {
|
|
return this.expanded && this.accordion.displayMode === 'default';
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/** Gets the expanded state string. */
|
|
_getExpandedState(): MatExpansionPanelState {
|
|
return this.expanded ? 'expanded' : 'collapsed';
|
|
}
|
|
|
|
/** Toggles the expanded state of the expansion panel. */
|
|
override toggle(): void {
|
|
this.expanded = !this.expanded;
|
|
}
|
|
|
|
/** Sets the expanded state of the expansion panel to false. */
|
|
override close(): void {
|
|
this.expanded = false;
|
|
}
|
|
|
|
/** Sets the expanded state of the expansion panel to true. */
|
|
override open(): void {
|
|
this.expanded = true;
|
|
}
|
|
|
|
ngAfterContentInit() {
|
|
if (this._lazyContent && this._lazyContent._expansionPanel === this) {
|
|
// Render the content as soon as the panel becomes open.
|
|
this.opened
|
|
.pipe(
|
|
startWith(null),
|
|
filter(() => this.expanded && !this._portal),
|
|
take(1),
|
|
)
|
|
.subscribe(() => {
|
|
this._portal = new TemplatePortal(this._lazyContent._template, this._viewContainerRef);
|
|
});
|
|
}
|
|
|
|
this._setupAnimationEvents();
|
|
}
|
|
|
|
ngOnChanges(changes: SimpleChanges) {
|
|
this._inputChanges.next(changes);
|
|
}
|
|
|
|
override ngOnDestroy() {
|
|
super.ngOnDestroy();
|
|
this._bodyWrapper?.nativeElement.removeEventListener(
|
|
'transitionend',
|
|
this._transitionEndListener,
|
|
);
|
|
this._inputChanges.complete();
|
|
}
|
|
|
|
/** Checks whether the expansion panel's content contains the currently-focused element. */
|
|
_containsFocus(): boolean {
|
|
if (this._body) {
|
|
const focusedElement = this._document.activeElement;
|
|
const bodyElement = this._body.nativeElement;
|
|
return focusedElement === bodyElement || bodyElement.contains(focusedElement);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private _transitionEndListener = ({target, propertyName}: TransitionEvent) => {
|
|
if (target === this._bodyWrapper?.nativeElement && propertyName === 'grid-template-rows') {
|
|
this._ngZone.run(() => {
|
|
if (this.expanded) {
|
|
this.afterExpand.emit();
|
|
} else {
|
|
this.afterCollapse.emit();
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
protected _setupAnimationEvents() {
|
|
// This method is defined separately, because we need to
|
|
// disable this logic in some internal components.
|
|
this._ngZone.runOutsideAngular(() => {
|
|
if (this._animationsDisabled) {
|
|
this.opened.subscribe(() => this._ngZone.run(() => this.afterExpand.emit()));
|
|
this.closed.subscribe(() => this._ngZone.run(() => this.afterCollapse.emit()));
|
|
} else {
|
|
setTimeout(() => {
|
|
const element = this._elementRef.nativeElement;
|
|
element.addEventListener('transitionend', this._transitionEndListener);
|
|
element.classList.add('mat-expansion-panel-animations-enabled');
|
|
}, 200);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Actions of a `<mat-expansion-panel>`.
|
|
*/
|
|
@Directive({
|
|
selector: 'mat-action-row',
|
|
host: {
|
|
class: 'mat-action-row',
|
|
},
|
|
})
|
|
export class MatExpansionPanelActionRow {}
|