66 * found in the LICENSE file at https://angular.io/license
77 */
88
9- import { AriaLivePoliteness } from '@angular/cdk/a11y' ;
10- import {
11- BasePortalOutlet ,
12- CdkPortalOutlet ,
13- ComponentPortal ,
14- TemplatePortal ,
15- } from '@angular/cdk/portal' ;
169import {
1710 ChangeDetectionStrategy ,
1811 Component ,
19- ComponentRef ,
2012 ElementRef ,
21- EmbeddedViewRef ,
22- Inject ,
23- NgZone ,
24- OnDestroy ,
25- Optional ,
2613 ViewChild ,
2714 ViewEncapsulation ,
2815} from '@angular/core' ;
29- import { MatSnackBarConfig , _SnackBarContainer } from '@angular/material/snack-bar' ;
30- import { ANIMATION_MODULE_TYPE } from '@angular/platform-browser/animations' ;
31- import { MDCSnackbarAdapter , MDCSnackbarFoundation , cssClasses } from '@material/snackbar' ;
32- import { Platform } from '@angular/cdk/platform' ;
33- import { Observable , Subject } from 'rxjs' ;
34-
35- /**
36- * The MDC label class that should wrap the label content of the snack bar.
37- * @docs -private
38- */
39- const MDC_SNACKBAR_LABEL_CLASS = 'mdc-snackbar__label' ;
16+ import { matSnackBarAnimations , _MatSnackBarContainerBase } from '@angular/material/snack-bar' ;
4017
4118/**
4219 * Internal component that wraps user-provided snack bar content.
@@ -52,242 +29,30 @@ const MDC_SNACKBAR_LABEL_CLASS = 'mdc-snackbar__label';
5229 // tslint:disable-next-line:validate-decorators
5330 changeDetection : ChangeDetectionStrategy . Default ,
5431 encapsulation : ViewEncapsulation . None ,
32+ animations : [ matSnackBarAnimations . snackBarState ] ,
5533 host : {
56- 'class' : 'mdc-snackbar mat-mdc-snack-bar-container' ,
57- '[class.mat-snack-bar-container]' : 'false' ,
58- // Mark this element with a 'mat-exit' attribute to indicate that the snackbar has
59- // been dismissed and will soon be removed from the DOM. This is used by the snackbar
60- // test harness.
61- '[attr.mat-exit]' : `_exiting ? '' : null` ,
62- '[class._mat-animation-noopable]' : `_animationMode === 'NoopAnimations'` ,
34+ 'class' : 'mdc-snackbar mat-mdc-snack-bar-container mdc-snackbar--open' ,
35+ '[@state]' : '_animationState' ,
36+ '(@state.done)' : 'onAnimationEnd($event)' ,
6337 } ,
6438} )
65- export class MatSnackBarContainer
66- extends BasePortalOutlet
67- implements _SnackBarContainer , OnDestroy
68- {
69- /** The number of milliseconds to wait before announcing the snack bar's content. */
70- private readonly _announceDelay : number = 150 ;
71-
72- /** The timeout for announcing the snack bar's content. */
73- private _announceTimeoutId : number ;
74-
75- /** Subject for notifying that the snack bar has announced to screen readers. */
76- readonly _onAnnounce : Subject < void > = new Subject ( ) ;
77-
78- /** Subject for notifying that the snack bar has exited from view. */
79- readonly _onExit : Subject < void > = new Subject ( ) ;
80-
81- /** Subject for notifying that the snack bar has finished entering the view. */
82- readonly _onEnter : Subject < void > = new Subject ( ) ;
83-
84- /** aria-live value for the live region. */
85- _live : AriaLivePoliteness ;
86-
87- /** Whether the snack bar is currently exiting. */
88- _exiting = false ;
89-
90- /**
91- * Role of the live region. This is only for Firefox as there is a known issue where Firefox +
92- * JAWS does not read out aria-live message.
93- */
94- _role ?: 'status' | 'alert' ;
95-
96- private _mdcAdapter : MDCSnackbarAdapter = {
97- addClass : ( className : string ) => this . _setClass ( className , true ) ,
98- removeClass : ( className : string ) => this . _setClass ( className , false ) ,
99- announce : ( ) => { } ,
100- notifyClosed : ( ) => this . _finishExit ( ) ,
101- notifyClosing : ( ) => { } ,
102- notifyOpened : ( ) => this . _onEnter . next ( ) ,
103- notifyOpening : ( ) => { } ,
104- } ;
105-
106- _mdcFoundation = new MDCSnackbarFoundation ( this . _mdcAdapter ) ;
107-
108- /** The portal outlet inside of this container into which the snack bar content will be loaded. */
109- @ViewChild ( CdkPortalOutlet , { static : true } ) _portalOutlet : CdkPortalOutlet ;
110-
111- /** Element that acts as the MDC surface container which should contain the label and actions. */
112- @ViewChild ( 'surface' , { static : true } ) _surface : ElementRef ;
113-
39+ export class MatSnackBarContainer extends _MatSnackBarContainerBase {
11440 /**
11541 * Element that will have the `mdc-snackbar__label` class applied if the attached component
11642 * or template does not have it. This ensures that the appropriate structure, typography, and
11743 * color is applied to the attached view.
11844 */
11945 @ViewChild ( 'label' , { static : true } ) _label : ElementRef ;
12046
121- constructor (
122- private _elementRef : ElementRef < HTMLElement > ,
123- public snackBarConfig : MatSnackBarConfig ,
124- private _platform : Platform ,
125- private _ngZone : NgZone ,
126- @Optional ( ) @Inject ( ANIMATION_MODULE_TYPE ) public _animationMode ?: string ,
127- ) {
128- super ( ) ;
129-
130- // Use aria-live rather than a live role like 'alert' or 'status'
131- // because NVDA and JAWS have show inconsistent behavior with live roles.
132- if ( snackBarConfig . politeness === 'assertive' && ! snackBarConfig . announcementMessage ) {
133- this . _live = 'assertive' ;
134- } else if ( snackBarConfig . politeness === 'off' ) {
135- this . _live = 'off' ;
136- } else {
137- this . _live = 'polite' ;
138- }
139-
140- // Only set role for Firefox. Set role based on aria-live because setting role="alert" implies
141- // aria-live="assertive" which may cause issues if aria-live is set to "polite" above.
142- if ( this . _platform . FIREFOX ) {
143- if ( this . _live === 'polite' ) {
144- this . _role = 'status' ;
145- }
146- if ( this . _live === 'assertive' ) {
147- this . _role = 'alert' ;
148- }
149- }
150-
151- // `MatSnackBar` will use the config's timeout to determine when the snack bar should be closed.
152- // Set this to `-1` to mark it as indefinitely open so that MDC does not close itself.
153- this . _mdcFoundation . setTimeoutMs ( - 1 ) ;
154- }
155-
156- /** Makes sure the exit callbacks have been invoked when the element is destroyed. */
157- ngOnDestroy ( ) {
158- this . _mdcFoundation . close ( ) ;
159- }
160-
161- enter ( ) {
162- // MDC uses some browser APIs that will throw during server-side rendering.
163- if ( this . _platform . isBrowser ) {
164- this . _ngZone . run ( ( ) => {
165- this . _mdcFoundation . open ( ) ;
166- this . _screenReaderAnnounce ( ) ;
167- } ) ;
168- }
169- }
170-
171- exit ( ) : Observable < void > {
172- const classList = this . _elementRef . nativeElement . classList ;
173-
174- // MDC won't complete the closing sequence if it starts while opening hasn't finished.
175- // If that's the case, destroy immediately to ensure that our stream emits as expected.
176- if ( classList . contains ( cssClasses . OPENING ) || ! classList . contains ( cssClasses . OPEN ) ) {
177- this . _finishExit ( ) ;
178- } else {
179- // It's common for snack bars to be opened by random outside calls like HTTP requests or
180- // errors. Run inside the NgZone to ensure that it functions correctly.
181- this . _ngZone . run ( ( ) => {
182- this . _exiting = true ;
183- this . _mdcFoundation . close ( ) ;
184- } ) ;
185- }
186-
187- // If the snack bar hasn't been announced by the time it exits it wouldn't have been open
188- // long enough to visually read it either, so clear the timeout for announcing.
189- clearTimeout ( this . _announceTimeoutId ) ;
190-
191- return this . _onExit ;
192- }
193-
194- /** Attach a component portal as content to this snack bar container. */
195- attachComponentPortal < T > ( portal : ComponentPortal < T > ) : ComponentRef < T > {
196- this . _assertNotAttached ( ) ;
197- this . _applySnackBarClasses ( ) ;
198- const componentRef = this . _portalOutlet . attachComponentPortal ( portal ) ;
199- this . _applyLabelClass ( ) ;
200- return componentRef ;
201- }
202-
203- /** Attach a template portal as content to this snack bar container. */
204- attachTemplatePortal < C > ( portal : TemplatePortal < C > ) : EmbeddedViewRef < C > {
205- this . _assertNotAttached ( ) ;
206- this . _applySnackBarClasses ( ) ;
207- const viewRef = this . _portalOutlet . attachTemplatePortal ( portal ) ;
208- this . _applyLabelClass ( ) ;
209- return viewRef ;
210- }
211-
212- private _setClass ( cssClass : string , active : boolean ) {
213- this . _elementRef . nativeElement . classList . toggle ( cssClass , active ) ;
214- }
215-
216- /** Applies the user-configured CSS classes to the snack bar. */
217- private _applySnackBarClasses ( ) {
218- const panelClasses = this . snackBarConfig . panelClass ;
219- if ( panelClasses ) {
220- if ( Array . isArray ( panelClasses ) ) {
221- // Note that we can't use a spread here, because IE doesn't support multiple arguments.
222- panelClasses . forEach ( cssClass => this . _setClass ( cssClass , true ) ) ;
223- } else {
224- this . _setClass ( panelClasses , true ) ;
225- }
226- }
227- }
228-
229- /** Asserts that no content is already attached to the container. */
230- private _assertNotAttached ( ) {
231- if ( this . _portalOutlet . hasAttached ( ) && ( typeof ngDevMode === 'undefined' || ngDevMode ) ) {
232- throw Error ( 'Attempting to attach snack bar content after content is already attached' ) ;
233- }
234- }
235-
236- /** Finishes the exit sequence of the container. */
237- private _finishExit ( ) {
238- this . _onExit . next ( ) ;
239- this . _onExit . complete ( ) ;
240-
241- if ( this . _platform . isBrowser ) {
242- this . _mdcFoundation . destroy ( ) ;
243- }
244- }
245-
246- /**
247- * Starts a timeout to move the snack bar content to the live region so screen readers will
248- * announce it.
249- */
250- private _screenReaderAnnounce ( ) {
251- if ( ! this . _announceTimeoutId ) {
252- this . _ngZone . runOutsideAngular ( ( ) => {
253- this . _announceTimeoutId = setTimeout ( ( ) => {
254- const inertElement = this . _elementRef . nativeElement . querySelector ( '[aria-hidden]' ) ;
255- const liveElement = this . _elementRef . nativeElement . querySelector ( '[aria-live]' ) ;
256-
257- if ( inertElement && liveElement ) {
258- // If an element in the snack bar content is focused before being moved
259- // track it and restore focus after moving to the live region.
260- let focusedElement : HTMLElement | null = null ;
261- if (
262- document . activeElement instanceof HTMLElement &&
263- inertElement . contains ( document . activeElement )
264- ) {
265- focusedElement = document . activeElement ;
266- }
267-
268- inertElement . removeAttribute ( 'aria-hidden' ) ;
269- liveElement . appendChild ( inertElement ) ;
270- focusedElement ?. focus ( ) ;
271-
272- this . _onAnnounce . next ( ) ;
273- this . _onAnnounce . complete ( ) ;
274- }
275- } , this . _announceDelay ) ;
276- } ) ;
277- }
278- }
279-
28047 /** Applies the correct CSS class to the label based on its content. */
281- private _applyLabelClass ( ) {
48+ protected override _afterPortalAttached ( ) {
49+ super . _afterPortalAttached ( ) ;
50+
28251 // Check to see if the attached component or template uses the MDC template structure,
28352 // specifically the MDC label. If not, the container should apply the MDC label class to this
28453 // component's label container, which will apply MDC's label styles to the attached view.
28554 const label = this . _label . nativeElement ;
286-
287- if ( ! label . querySelector ( `.${ MDC_SNACKBAR_LABEL_CLASS } ` ) ) {
288- label . classList . add ( MDC_SNACKBAR_LABEL_CLASS ) ;
289- } else {
290- label . classList . remove ( MDC_SNACKBAR_LABEL_CLASS ) ;
291- }
55+ const labelClass = 'mdc-snackbar__label' ;
56+ label . classList . toggle ( labelClass , ! label . querySelector ( `.${ labelClass } ` ) ) ;
29257 }
29358}
0 commit comments