/** * @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.io/license */ import {AnimationEvent} from '@angular/animations'; import {CdkAccordionItem} from '@angular/cdk/accordion'; import {coerceBooleanProperty} from '@angular/cdk/coercion'; import {UniqueSelectionDispatcher} from '@angular/cdk/collections'; import {TemplatePortal} from '@angular/cdk/portal'; import { AfterContentInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, Directive, EventEmitter, ElementRef, Input, Inject, OnChanges, OnDestroy, Optional, Output, SimpleChanges, SkipSelf, ViewContainerRef, ViewEncapsulation, ViewChild, InjectionToken, } from '@angular/core'; import {DOCUMENT} from '@angular/common'; import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations'; import {Subject} from 'rxjs'; import {filter, startWith, take, distinctUntilChanged} from 'rxjs/operators'; import {matExpansionAnimations} from './expansion-animations'; import {MatExpansionPanelContent} from './expansion-panel-content'; import {MAT_ACCORDION, MatAccordionBase, MatAccordionTogglePosition} from './accordion-base'; /** MatExpansionPanel's states. */ export type MatExpansionPanelState = 'expanded' | 'collapsed'; /** Counter for generating unique element ids. */ let uniqueId = 0; /** * 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 defalt * options for the expansion panel component. */ export const MAT_EXPANSION_PANEL_DEFAULT_OPTIONS = new InjectionToken('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({ moduleId: module.id, styleUrls: ['./expansion-panel.css'], selector: 'mat-expansion-panel', exportAs: 'matExpansionPanel', templateUrl: './expansion-panel.html', encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, inputs: ['disabled', 'expanded'], outputs: ['opened', 'closed', 'expandedChange'], animations: [matExpansionAnimations.bodyExpansion], providers: [ // Provide MatAccordion as undefined to prevent nested expansion panels from registering // to the same accordion. {provide: MAT_ACCORDION, useValue: undefined}, ], host: { 'class': 'mat-expansion-panel', '[class.mat-expanded]': 'expanded', '[class._mat-animation-noopable]': '_animationMode === "NoopAnimations"', '[class.mat-expansion-panel-spacing]': '_hasSpacing()', } }) export class MatExpansionPanel extends CdkAccordionItem implements AfterContentInit, OnChanges, OnDestroy { private _document: Document; private _hideToggle = false; private _togglePosition: MatAccordionTogglePosition; /** Whether the toggle indicator should be hidden. */ @Input() get hideToggle(): boolean { return this._hideToggle || (this.accordion && this.accordion.hideToggle); } set hideToggle(value: boolean) { this._hideToggle = coerceBooleanProperty(value); } /** 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; } /** An event emitted after the body's expansion animation happens. */ @Output() afterExpand = new EventEmitter(); /** An event emitted after the body's collapse animation happens. */ @Output() afterCollapse = new EventEmitter(); /** Stream that emits for changes in `@Input` properties. */ readonly _inputChanges = new Subject(); /** Optionally defined accordion the expansion panel belongs to. */ accordion: MatAccordionBase; /** Content that will be rendered lazily. */ @ContentChild(MatExpansionPanelContent, {static: false}) _lazyContent: MatExpansionPanelContent; /** Element containing the panel's user-provided content. */ @ViewChild('body', {static: false}) _body: ElementRef; /** Portal holding the user's content. */ _portal: TemplatePortal; /** ID for the associated header element. Used for a11y labelling. */ _headerId = `mat-expansion-panel-header-${uniqueId++}`; /** Stream of body animation done events. */ _bodyAnimationDone = new Subject(); constructor(@Optional() @SkipSelf() @Inject(MAT_ACCORDION) accordion: MatAccordionBase, _changeDetectorRef: ChangeDetectorRef, _uniqueSelectionDispatcher: UniqueSelectionDispatcher, private _viewContainerRef: ViewContainerRef, @Inject(DOCUMENT) _document: any, @Optional() @Inject(ANIMATION_MODULE_TYPE) public _animationMode: string, @Inject(MAT_EXPANSION_PANEL_DEFAULT_OPTIONS) @Optional() defaultOptions?: MatExpansionPanelDefaultOptions) { super(accordion, _changeDetectorRef, _uniqueSelectionDispatcher); this.accordion = accordion; this._document = _document; // 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(distinctUntilChanged((x, y) => { return x.fromState === y.fromState && x.toState === y.toState; })).subscribe(event => { 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; } } /** Determines whether the expansion panel should have spacing between it and its siblings. */ _hasSpacing(): boolean { if (this.accordion) { // We don't need to subscribe to the `stateChanges` of the parent accordion because each time // the [displayMode] input changes, the change detection will also cover the host bindings // of this expansion panel. return (this.expanded ? this.accordion.displayMode : this._getExpandedState()) === 'default'; } return false; } /** Gets the expanded state string. */ _getExpandedState(): MatExpansionPanelState { return this.expanded ? 'expanded' : 'collapsed'; } ngAfterContentInit() { if (this._lazyContent) { // 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); }); } } ngOnChanges(changes: SimpleChanges) { this._inputChanges.next(changes); } ngOnDestroy() { super.ngOnDestroy(); this._bodyAnimationDone.complete(); 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; } } @Directive({ selector: 'mat-action-row', host: { class: 'mat-action-row' } }) export class MatExpansionPanelActionRow {}