import { AnimationEvent } from '@angular/animations' ; import { FocusMonitor , FocusOrigin , FocusTrap , FocusTrapFactory } from '@angular/cdk/a11y' ; import { _getFocusedElementPierceShadowDom } from '@angular/cdk/platform' ; import { BasePortalOutlet , CdkPortalOutlet , ComponentPortal , DomPortal , TemplatePortal , } from '@angular/cdk/portal' ; import { DOCUMENT } from '@angular/common' ; import { ChangeDetectionStrategy , ChangeDetectorRef , Component , ComponentRef , Directive , ElementRef , EmbeddedViewRef , EventEmitter , HostListener , Inject , Optional , ViewChild , ViewEncapsulation , } from '@angular/core' ; import { sbbDialogAnimations } from './dialog-animations' ; import { SbbDialogConfig } from './dialog-config' ; /** Event that captures the state of dialog container animations. */ interface DialogAnimationEvent { state : 'opened' | 'opening' | 'closing' | 'closed' ; totalTime : number ; } docs-private export function throwSbbDialogContentAlreadyAttachedError ( ) { throw Error ( 'Attempting to attach dialog content after content is already attached' ) ; } /**
 * Base class for the `SbbDialogContainer`. The base class does not implement
 * animations as these are left to implementers of the dialog container.
 */ @ Directive ( ) export abstract class _SbbDialogContainerBase extends BasePortalOutlet { protected _document : Document ; /** The portal outlet inside of this container into which the dialog content will be loaded. */ @ ViewChild ( CdkPortalOutlet , { static : true } ) _portalOutlet : CdkPortalOutlet ; /** The class that traps and manages focus within the dialog. */ private _focusTrap : FocusTrap ; /** Emits when an animation state changes. */ _animationStateChanged : EventEmitter < DialogAnimationEvent > = new EventEmitter < DialogAnimationEvent > ( ) ; /** Element that was focused before the dialog was opened. Save this to restore upon close. */ private _elementFocusedBeforeDialogWasOpened : HTMLElement | null = null ; /**
     * Type of interaction that led to the dialog being closed. This is used to determine
     * whether the focus style will be applied when returning focus to its original location
     * after the dialog is closed.
     */ _closeInteractionType : FocusOrigin | null = null ; /** ID of the element that should be considered as the dialog's label. */ _ariaLabelledBy : string | null ; /** ID for the container DOM element. */ _id : string ; constructor ( protected _elementRef , protected _focusTrapFactory , protected _changeDetectorRef , @ Optional ( ) @ Inject ( DOCUMENT ) document , /** The dialog configuration. */ public _config , private _focusMonitor ? ) { super ( ) ; this . _ariaLabelledBy = _config . ariaLabelledBy || null ; this . _document = document ; } /** Starts the dialog exit animation. */ abstract _startExitAnimation ( ) ; /** Initializes the dialog container with the attached content. */ _initializeWithAttachedContent ( ) { this . _setupFocusTrap ( ) ; this . _capturePreviouslyFocusedElement ( ) ; this . _focusDialogContainer ( ) ; } param portal attachComponentPortal < T > ( portal ) { if ( this . _portalOutlet . hasAttached ( ) && ( typeof ngDevMode === 'undefined' || ngDevMode ) ) { throwSbbDialogContentAlreadyAttachedError ( ) ; } return this . _portalOutlet . attachComponentPortal ( portal ) ; } param portal attachTemplatePortal < C > ( portal ) { if ( this . _portalOutlet . hasAttached ( ) && ( typeof ngDevMode === 'undefined' || ngDevMode ) ) { throwSbbDialogContentAlreadyAttachedError ( ) ; } return this . _portalOutlet . attachTemplatePortal ( portal ) ; } param portal override attachDomPortal = ( portal ) => { if ( this . _portalOutlet . hasAttached ( ) && ( typeof ngDevMode === 'undefined' || ngDevMode ) ) { throwSbbDialogContentAlreadyAttachedError ( ) ; } return this . _portalOutlet . attachDomPortal ( portal ) ; } ; /** Moves focus back into the dialog if it was moved out. */ _recaptureFocus ( ) { if ( ! this . _containsFocus ( ) ) { const focusContainer = ! this . _config . autoFocus || ! this . _focusTrap . focusInitialElement ( ) ; if ( focusContainer ) { this . _elementRef . nativeElement . focus ( ) ; } } } /** Moves the focus inside the focus trap. */ protected _trapFocus ( ) { if ( this . _config . autoFocus ) { this . _focusTrap . focusInitialElementWhenReady ( ) ; } else if ( ! this . _containsFocus ( ) ) { this . _elementRef . nativeElement . focus ( ) ; } } /** Restores focus to the element that was focused before the dialog opened. */ protected _restoreFocus ( ) { const previousElement = this . _elementFocusedBeforeDialogWasOpened ; if ( this . _config . restoreFocus && previousElement && typeof previousElement . focus === 'function' ) { const activeElement = _getFocusedElementPierceShadowDom ( ) ; const element = this . _elementRef . nativeElement ; if ( ! activeElement || activeElement === this . _document . body || activeElement === element || element . contains ( activeElement ) ) { if ( this . _focusMonitor ) { this . _focusMonitor . focusVia ( previousElement , this . _closeInteractionType ) ; this . _closeInteractionType = null ; } else { previousElement . focus ( ) ; } } } if ( this . _focusTrap ) { this . _focusTrap . destroy ( ) ; } } /** Sets up the focus trap. */ private _setupFocusTrap ( ) { this . _focusTrap = this . _focusTrapFactory . create ( this . _elementRef . nativeElement ) ; } /** Captures the element that was focused before the dialog was opened. */ private _capturePreviouslyFocusedElement ( ) { if ( this . _document ) { this . _elementFocusedBeforeDialogWasOpened = _getFocusedElementPierceShadowDom ( ) ; } } /** Focuses the dialog container. */ private _focusDialogContainer ( ) { if ( this . _elementRef . nativeElement . focus ) { this . _elementRef . nativeElement . focus ( ) ; } } /** Returns whether focus is inside the dialog. */ private _containsFocus ( ) { const element = this . _elementRef . nativeElement ; const activeElement = _getFocusedElementPierceShadowDom ( ) ; return element === activeElement || element . contains ( activeElement ) ; } } docs-private @ Component ( { selector : 'sbb-dialog-container' , templateUrl : 'dialog-container.html' , styleUrls : [ 'dialog.css' ] , encapsulation : ViewEncapsulation . None , changeDetection : ChangeDetectionStrategy . Default , animations : [ sbbDialogAnimations . dialogContainer ] , host : { class : 'sbb-dialog-container' , tabindex : '-1' , 'aria-modal' : 'true' , '[id]' : '_id' , '[attr.role]' : '_config.role' , '[attr.aria-labelledby]' : '_config.ariaLabel ? null : _ariaLabelledBy' , '[attr.aria-label]' : '_config.ariaLabel' , '[attr.aria-describedby]' : '_config.ariaDescribedBy || null' , '[@dialogContainer]' : '_state' , } , } ) export class SbbDialogContainer extends _SbbDialogContainerBase { /** State of the dialog animation. */ _state : 'void' | 'enter' | 'exit' = 'enter' ; /** Callback, invoked whenever an animation on the host completes. */ @ HostListener ( '@dialogContainer.done' , [ '$event' ] ) _onAnimationDone ( { toState , totalTime } ) { if ( toState === 'enter' ) { this . _trapFocus ( ) ; this . _animationStateChanged . next ( { state : 'opened' , totalTime } ) ; } else if ( toState === 'exit' ) { this . _restoreFocus ( ) ; this . _animationStateChanged . next ( { state : 'closed' , totalTime } ) ; } } /** Callback, invoked when an animation on the host starts. */ @ HostListener ( '@dialogContainer.start' , [ '$event' ] ) _onAnimationStart ( { toState , totalTime } ) { if ( toState === 'enter' ) { this . _animationStateChanged . next ( { state : 'opening' , totalTime } ) ; } else if ( toState === 'exit' || toState === 'void' ) { this . _animationStateChanged . next ( { state : 'closing' , totalTime } ) ; } } /** Starts the dialog exit animation. */ _startExitAnimation ( ) { this . _state = 'exit' ; this . _changeDetectorRef . markForCheck ( ) ; } }