Skip to content

Commit 64b6506

Browse files
authored
refactor(material/bottom-sheet): use CDK dialog internally (#24951)
Switches the internals of the bottom sheet to use the CDK dialog.
1 parent c67a299 commit 64b6506

File tree

6 files changed

+127
-393
lines changed

6 files changed

+127
-393
lines changed

src/material/bottom-sheet/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ ng_module(
2424
"//src/cdk/a11y",
2525
"//src/cdk/bidi",
2626
"//src/cdk/coercion",
27+
"//src/cdk/dialog",
2728
"//src/cdk/keycodes",
2829
"//src/cdk/layout",
2930
"//src/cdk/overlay",

src/material/bottom-sheet/bottom-sheet-container.ts

Lines changed: 31 additions & 202 deletions
Original file line numberDiff line numberDiff line change
@@ -7,38 +7,26 @@
77
*/
88

99
import {AnimationEvent} from '@angular/animations';
10-
import {FocusTrap, FocusTrapFactory, InteractivityChecker} from '@angular/cdk/a11y';
11-
import {coerceArray} from '@angular/cdk/coercion';
10+
import {CdkDialogContainer, DialogConfig} from '@angular/cdk/dialog';
11+
import {FocusMonitor, FocusTrapFactory, InteractivityChecker} from '@angular/cdk/a11y';
1212
import {BreakpointObserver, Breakpoints} from '@angular/cdk/layout';
13+
import {OverlayRef} from '@angular/cdk/overlay';
1314
import {_getFocusedElementPierceShadowDom} from '@angular/cdk/platform';
14-
import {
15-
BasePortalOutlet,
16-
CdkPortalOutlet,
17-
ComponentPortal,
18-
DomPortal,
19-
TemplatePortal,
20-
} from '@angular/cdk/portal';
2115
import {DOCUMENT} from '@angular/common';
2216
import {
2317
ChangeDetectionStrategy,
2418
ChangeDetectorRef,
2519
Component,
26-
ComponentRef,
2720
ElementRef,
28-
EmbeddedViewRef,
2921
EventEmitter,
3022
Inject,
3123
NgZone,
3224
OnDestroy,
3325
Optional,
34-
ViewChild,
3526
ViewEncapsulation,
3627
} from '@angular/core';
3728
import {Subscription} from 'rxjs';
3829
import {matBottomSheetAnimations} from './bottom-sheet-animations';
39-
import {MatBottomSheetConfig} from './bottom-sheet-config';
40-
41-
// TODO(crisbeto): consolidate some logic between this, MatDialog and MatSnackBar
4230

4331
/**
4432
* Internal component that wraps user-provided bottom sheet content.
@@ -58,52 +46,49 @@ import {MatBottomSheetConfig} from './bottom-sheet-config';
5846
host: {
5947
'class': 'mat-bottom-sheet-container',
6048
'tabindex': '-1',
61-
'role': 'dialog',
62-
'aria-modal': 'true',
63-
'[attr.aria-label]': 'bottomSheetConfig?.ariaLabel',
49+
'[attr.role]': '_config.role',
50+
'[attr.aria-modal]': '_config.isModal',
51+
'[attr.aria-label]': '_config.ariaLabel',
6452
'[@state]': '_animationState',
6553
'(@state.start)': '_onAnimationStart($event)',
6654
'(@state.done)': '_onAnimationDone($event)',
6755
},
6856
})
69-
export class MatBottomSheetContainer extends BasePortalOutlet implements OnDestroy {
57+
export class MatBottomSheetContainer extends CdkDialogContainer implements OnDestroy {
7058
private _breakpointSubscription: Subscription;
7159

72-
/** The portal outlet inside of this container into which the content will be loaded. */
73-
@ViewChild(CdkPortalOutlet, {static: true}) _portalOutlet: CdkPortalOutlet;
74-
7560
/** The state of the bottom sheet animations. */
7661
_animationState: 'void' | 'visible' | 'hidden' = 'void';
7762

7863
/** Emits whenever the state of the animation changes. */
7964
_animationStateChanged = new EventEmitter<AnimationEvent>();
8065

81-
/** The class that traps and manages focus within the bottom sheet. */
82-
private _focusTrap: FocusTrap;
83-
84-
/** Element that was focused before the bottom sheet was opened. */
85-
private _elementFocusedBeforeOpened: HTMLElement | null = null;
86-
87-
/** Server-side rendering-compatible reference to the global document object. */
88-
private _document: Document;
89-
9066
/** Whether the component has been destroyed. */
9167
private _destroyed: boolean;
9268

9369
constructor(
94-
private _elementRef: ElementRef<HTMLElement>,
95-
private _changeDetectorRef: ChangeDetectorRef,
96-
private _focusTrapFactory: FocusTrapFactory,
97-
private readonly _interactivityChecker: InteractivityChecker,
98-
private readonly _ngZone: NgZone,
99-
breakpointObserver: BreakpointObserver,
70+
elementRef: ElementRef,
71+
focusTrapFactory: FocusTrapFactory,
10072
@Optional() @Inject(DOCUMENT) document: any,
101-
/** The bottom sheet configuration. */
102-
public bottomSheetConfig: MatBottomSheetConfig,
73+
config: DialogConfig,
74+
checker: InteractivityChecker,
75+
ngZone: NgZone,
76+
overlayRef: OverlayRef,
77+
breakpointObserver: BreakpointObserver,
78+
private _changeDetectorRef: ChangeDetectorRef,
79+
focusMonitor?: FocusMonitor,
10380
) {
104-
super();
81+
super(
82+
elementRef,
83+
focusTrapFactory,
84+
document,
85+
config,
86+
checker,
87+
ngZone,
88+
overlayRef,
89+
focusMonitor,
90+
);
10591

106-
this._document = document;
10792
this._breakpointSubscription = breakpointObserver
10893
.observe([Breakpoints.Medium, Breakpoints.Large, Breakpoints.XLarge])
10994
.subscribe(() => {
@@ -122,34 +107,6 @@ export class MatBottomSheetContainer extends BasePortalOutlet implements OnDestr
122107
});
123108
}
124109

125-
/** Attach a component portal as content to this bottom sheet container. */
126-
attachComponentPortal<T>(portal: ComponentPortal<T>): ComponentRef<T> {
127-
this._validatePortalAttached();
128-
this._setPanelClass();
129-
this._savePreviouslyFocusedElement();
130-
return this._portalOutlet.attachComponentPortal(portal);
131-
}
132-
133-
/** Attach a template portal as content to this bottom sheet container. */
134-
attachTemplatePortal<C>(portal: TemplatePortal<C>): EmbeddedViewRef<C> {
135-
this._validatePortalAttached();
136-
this._setPanelClass();
137-
this._savePreviouslyFocusedElement();
138-
return this._portalOutlet.attachTemplatePortal(portal);
139-
}
140-
141-
/**
142-
* Attaches a DOM portal to the bottom sheet container.
143-
* @deprecated To be turned into a method.
144-
* @breaking-change 10.0.0
145-
*/
146-
override attachDomPortal = (portal: DomPortal) => {
147-
this._validatePortalAttached();
148-
this._setPanelClass();
149-
this._savePreviouslyFocusedElement();
150-
return this._portalOutlet.attachDomPortal(portal);
151-
};
152-
153110
/** Begin animation of bottom sheet entrance into view. */
154111
enter(): void {
155112
if (!this._destroyed) {
@@ -166,15 +123,14 @@ export class MatBottomSheetContainer extends BasePortalOutlet implements OnDestr
166123
}
167124
}
168125

169-
ngOnDestroy() {
126+
override ngOnDestroy() {
127+
super.ngOnDestroy();
170128
this._breakpointSubscription.unsubscribe();
171129
this._destroyed = true;
172130
}
173131

174132
_onAnimationDone(event: AnimationEvent) {
175-
if (event.toState === 'hidden') {
176-
this._restoreFocus();
177-
} else if (event.toState === 'visible') {
133+
if (event.toState === 'visible') {
178134
this._trapFocus();
179135
}
180136

@@ -185,136 +141,9 @@ export class MatBottomSheetContainer extends BasePortalOutlet implements OnDestr
185141
this._animationStateChanged.emit(event);
186142
}
187143

144+
protected override _captureInitialFocus(): void {}
145+
188146
private _toggleClass(cssClass: string, add: boolean) {
189147
this._elementRef.nativeElement.classList.toggle(cssClass, add);
190148
}
191-
192-
private _validatePortalAttached() {
193-
if (this._portalOutlet.hasAttached() && (typeof ngDevMode === 'undefined' || ngDevMode)) {
194-
throw Error('Attempting to attach bottom sheet content after content is already attached');
195-
}
196-
}
197-
198-
private _setPanelClass() {
199-
const element: HTMLElement = this._elementRef.nativeElement;
200-
element.classList.add(...coerceArray(this.bottomSheetConfig.panelClass || []));
201-
}
202-
203-
/**
204-
* Focuses the provided element. If the element is not focusable, it will add a tabIndex
205-
* attribute to forcefully focus it. The attribute is removed after focus is moved.
206-
* @param element The element to focus.
207-
*/
208-
private _forceFocus(element: HTMLElement, options?: FocusOptions) {
209-
if (!this._interactivityChecker.isFocusable(element)) {
210-
element.tabIndex = -1;
211-
// The tabindex attribute should be removed to avoid navigating to that element again
212-
this._ngZone.runOutsideAngular(() => {
213-
const callback = () => {
214-
element.removeEventListener('blur', callback);
215-
element.removeEventListener('mousedown', callback);
216-
element.removeAttribute('tabindex');
217-
};
218-
219-
element.addEventListener('blur', callback);
220-
element.addEventListener('mousedown', callback);
221-
});
222-
}
223-
element.focus(options);
224-
}
225-
226-
/**
227-
* Focuses the first element that matches the given selector within the focus trap.
228-
* @param selector The CSS selector for the element to set focus to.
229-
*/
230-
private _focusByCssSelector(selector: string, options?: FocusOptions) {
231-
let elementToFocus = this._elementRef.nativeElement.querySelector(
232-
selector,
233-
) as HTMLElement | null;
234-
if (elementToFocus) {
235-
this._forceFocus(elementToFocus, options);
236-
}
237-
}
238-
239-
/**
240-
* Moves the focus inside the focus trap. When autoFocus is not set to 'bottom-sheet',
241-
* if focus cannot be moved then focus will go to the bottom sheet container.
242-
*/
243-
private _trapFocus() {
244-
const element = this._elementRef.nativeElement;
245-
246-
if (!this._focusTrap) {
247-
this._focusTrap = this._focusTrapFactory.create(element);
248-
}
249-
250-
// If were to attempt to focus immediately, then the content of the bottom sheet would not
251-
// yet be ready in instances where change detection has to run first. To deal with this,
252-
// we simply wait for the microtask queue to be empty when setting focus when autoFocus
253-
// isn't set to bottom sheet. If the element inside the bottom sheet can't be focused,
254-
// then the container is focused so the user can't tab into other elements behind it.
255-
switch (this.bottomSheetConfig.autoFocus) {
256-
case false:
257-
case 'dialog':
258-
const activeElement = _getFocusedElementPierceShadowDom();
259-
// Ensure that focus is on the bottom sheet container. It's possible that a different
260-
// component tried to move focus while the open animation was running. See:
261-
// https://github.com/angular/components/issues/16215. Note that we only want to do this
262-
// if the focus isn't inside the bottom sheet already, because it's possible that the
263-
// consumer specified `autoFocus` in order to move focus themselves.
264-
if (activeElement !== element && !element.contains(activeElement)) {
265-
element.focus();
266-
}
267-
break;
268-
case true:
269-
case 'first-tabbable':
270-
this._focusTrap.focusInitialElementWhenReady();
271-
break;
272-
case 'first-heading':
273-
this._focusByCssSelector('h1, h2, h3, h4, h5, h6, [role="heading"]');
274-
break;
275-
default:
276-
this._focusByCssSelector(this.bottomSheetConfig.autoFocus!);
277-
break;
278-
}
279-
}
280-
281-
/** Restores focus to the element that was focused before the bottom sheet was opened. */
282-
private _restoreFocus() {
283-
const toFocus = this._elementFocusedBeforeOpened;
284-
285-
// We need the extra check, because IE can set the `activeElement` to null in some cases.
286-
if (this.bottomSheetConfig.restoreFocus && toFocus && typeof toFocus.focus === 'function') {
287-
const activeElement = _getFocusedElementPierceShadowDom();
288-
const element = this._elementRef.nativeElement;
289-
290-
// Make sure that focus is still inside the bottom sheet or is on the body (usually because a
291-
// non-focusable element like the backdrop was clicked) before moving it. It's possible that
292-
// the consumer moved it themselves before the animation was done, in which case we shouldn't
293-
// do anything.
294-
if (
295-
!activeElement ||
296-
activeElement === this._document.body ||
297-
activeElement === element ||
298-
element.contains(activeElement)
299-
) {
300-
toFocus.focus();
301-
}
302-
}
303-
304-
if (this._focusTrap) {
305-
this._focusTrap.destroy();
306-
}
307-
}
308-
309-
/** Saves a reference to the element that was focused before the bottom sheet was opened. */
310-
private _savePreviouslyFocusedElement() {
311-
this._elementFocusedBeforeOpened = _getFocusedElementPierceShadowDom();
312-
313-
// The `focus` method isn't available during server-side rendering.
314-
if (this._elementRef.nativeElement.focus) {
315-
this._ngZone.runOutsideAngular(() => {
316-
Promise.resolve().then(() => this._elementRef.nativeElement.focus());
317-
});
318-
}
319-
}
320149
}

src/material/bottom-sheet/bottom-sheet-module.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {OverlayModule} from '@angular/cdk/overlay';
9+
import {DialogModule} from '@angular/cdk/dialog';
1010
import {PortalModule} from '@angular/cdk/portal';
1111
import {NgModule} from '@angular/core';
1212
import {MatCommonModule} from '@angular/material/core';
1313
import {MatBottomSheetContainer} from './bottom-sheet-container';
1414

1515
@NgModule({
16-
imports: [OverlayModule, MatCommonModule, PortalModule],
16+
imports: [DialogModule, MatCommonModule, PortalModule],
1717
exports: [MatBottomSheetContainer, MatCommonModule],
1818
declarations: [MatBottomSheetContainer],
1919
})

0 commit comments

Comments
 (0)