6
6
* found in the LICENSE file at https://angular.io/license
7
7
*/
8
8
9
- import { AriaLivePoliteness } from '@angular/cdk/a11y' ;
10
- import {
11
- BasePortalOutlet ,
12
- CdkPortalOutlet ,
13
- ComponentPortal ,
14
- TemplatePortal ,
15
- } from '@angular/cdk/portal' ;
16
9
import {
17
10
ChangeDetectionStrategy ,
18
11
Component ,
19
- ComponentRef ,
20
12
ElementRef ,
21
- EmbeddedViewRef ,
22
- Inject ,
23
- NgZone ,
24
- OnDestroy ,
25
- Optional ,
26
13
ViewChild ,
27
14
ViewEncapsulation ,
28
15
} 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' ;
40
17
41
18
/**
42
19
* Internal component that wraps user-provided snack bar content.
@@ -52,242 +29,30 @@ const MDC_SNACKBAR_LABEL_CLASS = 'mdc-snackbar__label';
52
29
// tslint:disable-next-line:validate-decorators
53
30
changeDetection : ChangeDetectionStrategy . Default ,
54
31
encapsulation : ViewEncapsulation . None ,
32
+ animations : [ matSnackBarAnimations . snackBarState ] ,
55
33
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)' ,
63
37
} ,
64
38
} )
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 {
114
40
/**
115
41
* Element that will have the `mdc-snackbar__label` class applied if the attached component
116
42
* or template does not have it. This ensures that the appropriate structure, typography, and
117
43
* color is applied to the attached view.
118
44
*/
119
45
@ViewChild ( 'label' , { static : true } ) _label : ElementRef ;
120
46
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
-
280
47
/** Applies the correct CSS class to the label based on its content. */
281
- private _applyLabelClass ( ) {
48
+ protected override _afterPortalAttached ( ) {
49
+ super . _afterPortalAttached ( ) ;
50
+
282
51
// Check to see if the attached component or template uses the MDC template structure,
283
52
// specifically the MDC label. If not, the container should apply the MDC label class to this
284
53
// component's label container, which will apply MDC's label styles to the attached view.
285
54
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 } ` ) ) ;
292
57
}
293
58
}
0 commit comments