Skip to content

Commit 781cd1f

Browse files
committed
refactor(material-experimental/mdc-snack-bar): remove MDC adapter usage (#24997)
Reworks the MDC snack bar so that it reuses the logic from our existing implementation instead of going through the MDC adapter. (cherry picked from commit 35d230b)
1 parent 59e1868 commit 781cd1f

File tree

9 files changed

+166
-352
lines changed

9 files changed

+166
-352
lines changed

src/material-experimental/mdc-snack-bar/BUILD.bazel

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ ng_module(
2525
"//src/material-experimental/mdc-core",
2626
"//src/material/snack-bar",
2727
"@npm//@angular/core",
28-
"@npm//@material/snackbar",
2928
],
3029
)
3130

src/material-experimental/mdc-snack-bar/snack-bar-container.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<div class="mdc-snackbar__surface" #surface>
1+
<div class="mdc-snackbar__surface">
22
<!--
33
This outer label wrapper will have the class `mdc-snackbar__label` applied if
44
the attached template/component does not contain it.

src/material-experimental/mdc-snack-bar/snack-bar-container.scss

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
@use '../mdc-helpers/mdc-helpers';
55

66
@include mdc-helpers.disable-fallback-declarations {
7-
@include mdc-snackbar.core-styles($query: mdc-helpers.$mat-base-styles-query);
7+
// Include the styles without the animations since we
8+
// reuse the same animation as the non-MDC version.
9+
@include mdc-snackbar.core-styles($query: mdc-helpers.$mat-base-styles-without-animation-query);
810
}
911

1012
// MDC sets the position as fixed and sets the container on the bottom center of the page (or
@@ -16,10 +18,6 @@
1618
@include cdk.high-contrast(active, off) {
1719
border: solid 1px;
1820
}
19-
20-
&._mat-animation-noopable .mdc-snackbar__surface {
21-
transition: none;
22-
}
2321
}
2422

2523
// These elements need to have full width using flex layout.

src/material-experimental/mdc-snack-bar/snack-bar-container.ts

Lines changed: 11 additions & 246 deletions
Original file line numberDiff line numberDiff line change
@@ -6,37 +6,14 @@
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';
169
import {
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

Comments
 (0)