77 */
88
99import { 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 ' ;
1212import { BreakpointObserver , Breakpoints } from '@angular/cdk/layout' ;
13+ import { OverlayRef } from '@angular/cdk/overlay' ;
1314import { _getFocusedElementPierceShadowDom } from '@angular/cdk/platform' ;
14- import {
15- BasePortalOutlet ,
16- CdkPortalOutlet ,
17- ComponentPortal ,
18- DomPortal ,
19- TemplatePortal ,
20- } from '@angular/cdk/portal' ;
2115import { DOCUMENT } from '@angular/common' ;
2216import {
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' ;
3728import { Subscription } from 'rxjs' ;
3829import { 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}
0 commit comments