/** * @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 { BasePortalOutlet, CdkPortalOutlet, ComponentPortal, TemplatePortal, } from '@angular/cdk/portal'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ComponentRef, ElementRef, EmbeddedViewRef, NgZone, OnDestroy, ViewChild, ViewEncapsulation, } from '@angular/core'; import {Observable, Subject} from 'rxjs'; import {take} from 'rxjs/operators'; import {matSnackBarAnimations} from './snack-bar-animations'; import {MatSnackBarConfig} from './snack-bar-config'; /** * Internal component that wraps user-provided snack bar content. * @docs-private */ @Component({ moduleId: module.id, selector: 'snack-bar-container', templateUrl: 'snack-bar-container.html', styleUrls: ['snack-bar-container.css'], // In Ivy embedded views will be change detected from their declaration place, rather than // where they were stamped out. This means that we can't have the snack bar container be OnPush, // because it might cause snack bars that were opened from a template not to be out of date. // tslint:disable-next-line:validate-decorators changeDetection: ChangeDetectionStrategy.Default, encapsulation: ViewEncapsulation.None, animations: [matSnackBarAnimations.snackBarState], host: { '[attr.role]': '_role', 'class': 'mat-snack-bar-container', '[@state]': '_animationState', '(@state.done)': 'onAnimationEnd($event)' }, }) export class MatSnackBarContainer extends BasePortalOutlet implements OnDestroy { /** Whether the component has been destroyed. */ private _destroyed = false; /** The portal outlet inside of this container into which the snack bar content will be loaded. */ @ViewChild(CdkPortalOutlet, {static: true}) _portalOutlet: CdkPortalOutlet; /** Subject for notifying that the snack bar has exited from view. */ readonly _onExit: Subject = new Subject(); /** Subject for notifying that the snack bar has finished entering the view. */ readonly _onEnter: Subject = new Subject(); /** The state of the snack bar animations. */ _animationState = 'void'; /** ARIA role for the snack bar container. */ _role: 'alert' | 'status' | null; constructor( private _ngZone: NgZone, private _elementRef: ElementRef, private _changeDetectorRef: ChangeDetectorRef, /** The snack bar configuration. */ public snackBarConfig: MatSnackBarConfig) { super(); // Based on the ARIA spec, `alert` and `status` roles have an // implicit `assertive` and `polite` politeness respectively. if (snackBarConfig.politeness === 'assertive' && !snackBarConfig.announcementMessage) { this._role = 'alert'; } else if (snackBarConfig.politeness === 'off') { this._role = null; } else { this._role = 'status'; } } /** Attach a component portal as content to this snack bar container. */ attachComponentPortal(portal: ComponentPortal): ComponentRef { this._assertNotAttached(); this._applySnackBarClasses(); return this._portalOutlet.attachComponentPortal(portal); } /** Attach a template portal as content to this snack bar container. */ attachTemplatePortal(portal: TemplatePortal): EmbeddedViewRef { this._assertNotAttached(); this._applySnackBarClasses(); return this._portalOutlet.attachTemplatePortal(portal); } /** Handle end of animations, updating the state of the snackbar. */ onAnimationEnd(event: AnimationEvent) { const {fromState, toState} = event; if ((toState === 'void' && fromState !== 'void') || toState === 'hidden') { this._completeExit(); } if (toState === 'visible') { // Note: we shouldn't use `this` inside the zone callback, // because it can cause a memory leak. const onEnter = this._onEnter; this._ngZone.run(() => { onEnter.next(); onEnter.complete(); }); } } /** Begin animation of snack bar entrance into view. */ enter(): void { if (!this._destroyed) { this._animationState = 'visible'; this._changeDetectorRef.detectChanges(); } } /** Begin animation of the snack bar exiting from view. */ exit(): Observable { // Note: this one transitions to `hidden`, rather than `void`, in order to handle the case // where multiple snack bars are opened in quick succession (e.g. two consecutive calls to // `MatSnackBar.open`). this._animationState = 'hidden'; return this._onExit; } /** Makes sure the exit callbacks have been invoked when the element is destroyed. */ ngOnDestroy() { this._destroyed = true; this._completeExit(); } /** * Waits for the zone to settle before removing the element. Helps prevent * errors where we end up removing an element which is in the middle of an animation. */ private _completeExit() { this._ngZone.onMicrotaskEmpty.asObservable().pipe(take(1)).subscribe(() => { this._onExit.next(); this._onExit.complete(); }); } /** Applies the various positioning and user-configured CSS classes to the snack bar. */ private _applySnackBarClasses() { const element: HTMLElement = this._elementRef.nativeElement; const panelClasses = this.snackBarConfig.panelClass; if (panelClasses) { if (Array.isArray(panelClasses)) { // Note that we can't use a spread here, because IE doesn't support multiple arguments. panelClasses.forEach(cssClass => element.classList.add(cssClass)); } else { element.classList.add(panelClasses); } } if (this.snackBarConfig.horizontalPosition === 'center') { element.classList.add('mat-snack-bar-center'); } if (this.snackBarConfig.verticalPosition === 'top') { element.classList.add('mat-snack-bar-top'); } } /** Asserts that no content is already attached to the container. */ private _assertNotAttached() { if (this._portalOutlet.hasAttached()) { throw Error('Attempting to attach snack bar content after content is already attached'); } } }