ipsos-components
Version:
Material Design components for Angular
204 lines (175 loc) • 7.22 kB
text/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.io/license
*/
import {
Component,
ComponentRef,
ElementRef,
EmbeddedViewRef,
EventEmitter,
Inject,
Optional,
ChangeDetectorRef,
ViewChild,
ViewEncapsulation,
ChangeDetectionStrategy,
} from '@angular/core';
import {animate, AnimationEvent, state, style, transition, trigger} from '@angular/animations';
import {DOCUMENT} from '@angular/common';
import {
BasePortalOutlet,
ComponentPortal,
CdkPortalOutlet,
TemplatePortal
} from '@angular/cdk/portal';
import {FocusTrap, FocusTrapFactory} from '@angular/cdk/a11y';
import {MatDialogConfig} from './dialog-config';
/**
* Throws an exception for the case when a ComponentPortal is
* attached to a DomPortalOutlet without an origin.
* @docs-private
*/
export function throwMatDialogContentAlreadyAttachedError() {
throw Error('Attempting to attach dialog content after content is already attached');
}
/**
* Internal component that wraps user-provided dialog content.
* Animation is based on https://material.io/guidelines/motion/choreography.html.
* @docs-private
*/
({
moduleId: module.id,
selector: 'mat-dialog-container',
templateUrl: 'dialog-container.html',
styleUrls: ['dialog.css'],
encapsulation: ViewEncapsulation.None,
preserveWhitespaces: false,
// Using OnPush for dialogs caused some G3 sync issues. Disabled until we can track them down.
// tslint:disable-next-line:validate-decorators
changeDetection: ChangeDetectionStrategy.Default,
animations: [
trigger('slideDialog', [
// Note: The `enter` animation doesn't transition to something like `translate3d(0, 0, 0)
// scale(1)`, because for some reason specifying the transform explicitly, causes IE both
// to blur the dialog content and decimate the animation performance. Leaving it as `none`
// solves both issues.
state('enter', style({ transform: 'none', opacity: 1 })),
state('void', style({ transform: 'translate3d(0, 25%, 0) scale(0.9)', opacity: 0 })),
state('exit', style({ transform: 'translate3d(0, 25%, 0)', opacity: 0 })),
transition('* => *', animate('400ms cubic-bezier(0.25, 0.8, 0.25, 1)')),
])
],
host: {
'class': 'mat-dialog-container',
'tabindex': '-1',
'[attr.role]': '_config?.role',
'[attr.aria-labelledby]': '_config?.ariaLabel ? null : _ariaLabelledBy',
'[attr.aria-label]': '_config?.ariaLabel',
'[attr.aria-describedby]': '_config?.ariaDescribedBy || null',
'[@slideDialog]': '_state',
'(@slideDialog.start)': '_onAnimationStart($event)',
'(@slideDialog.done)': '_onAnimationDone($event)',
},
})
export class MatDialogContainer extends BasePortalOutlet {
/** The portal outlet inside of this container into which the dialog content will be loaded. */
(CdkPortalOutlet) _portalOutlet: CdkPortalOutlet;
/** The class that traps and manages focus within the dialog. */
private _focusTrap: FocusTrap;
/** Element that was focused before the dialog was opened. Save this to restore upon close. */
private _elementFocusedBeforeDialogWasOpened: HTMLElement | null = null;
/** The dialog configuration. */
_config: MatDialogConfig;
/** State of the dialog animation. */
_state: 'void' | 'enter' | 'exit' = 'enter';
/** Emits when an animation state changes. */
_animationStateChanged = new EventEmitter<AnimationEvent>();
/** ID of the element that should be considered as the dialog's label. */
_ariaLabelledBy: string | null = null;
constructor(
private _elementRef: ElementRef,
private _focusTrapFactory: FocusTrapFactory,
private _changeDetectorRef: ChangeDetectorRef,
() (DOCUMENT) private _document: any) {
super();
}
/**
* Attach a ComponentPortal as content to this dialog container.
* @param portal Portal to be attached as the dialog content.
*/
attachComponentPortal<T>(portal: ComponentPortal<T>): ComponentRef<T> {
if (this._portalOutlet.hasAttached()) {
throwMatDialogContentAlreadyAttachedError();
}
this._savePreviouslyFocusedElement();
return this._portalOutlet.attachComponentPortal(portal);
}
/**
* Attach a TemplatePortal as content to this dialog container.
* @param portal Portal to be attached as the dialog content.
*/
attachTemplatePortal<C>(portal: TemplatePortal<C>): EmbeddedViewRef<C> {
if (this._portalOutlet.hasAttached()) {
throwMatDialogContentAlreadyAttachedError();
}
this._savePreviouslyFocusedElement();
return this._portalOutlet.attachTemplatePortal(portal);
}
/** Moves the focus inside the focus trap. */
private _trapFocus() {
if (!this._focusTrap) {
this._focusTrap = this._focusTrapFactory.create(this._elementRef.nativeElement);
}
// If were to attempt to focus immediately, then the content of the dialog would not yet be
// ready in instances where change detection has to run first. To deal with this, we simply
// wait for the microtask queue to be empty.
if (this._config.autoFocus) {
this._focusTrap.focusInitialElementWhenReady();
}
}
/** Restores focus to the element that was focused before the dialog opened. */
private _restoreFocus() {
const toFocus = this._elementFocusedBeforeDialogWasOpened;
// We need the extra check, because IE can set the `activeElement` to null in some cases.
if (toFocus && typeof toFocus.focus === 'function') {
toFocus.focus();
}
if (this._focusTrap) {
this._focusTrap.destroy();
}
}
/** Saves a reference to the element that was focused before the dialog was opened. */
private _savePreviouslyFocusedElement() {
if (this._document) {
this._elementFocusedBeforeDialogWasOpened = this._document.activeElement as HTMLElement;
// Move focus onto the dialog immediately in order to prevent the user from accidentally
// opening multiple dialogs at the same time. Needs to be async, because the element
// may not be focusable immediately.
Promise.resolve().then(() => this._elementRef.nativeElement.focus());
}
}
/** Callback, invoked whenever an animation on the host completes. */
_onAnimationDone(event: AnimationEvent) {
if (event.toState === 'enter') {
this._trapFocus();
} else if (event.toState === 'exit') {
this._restoreFocus();
}
this._animationStateChanged.emit(event);
}
/** Callback, invoked when an animation on the host starts. */
_onAnimationStart(event: AnimationEvent) {
this._animationStateChanged.emit(event);
}
/** Starts the dialog exit animation. */
_startExitAnimation(): void {
this._state = 'exit';
// Mark the container for check so it can react if the
// view container is using OnPush change detection.
this._changeDetectorRef.markForCheck();
}
}