7
7
*/
8
8
9
9
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 ' ;
12
12
import { BreakpointObserver , Breakpoints } from '@angular/cdk/layout' ;
13
+ import { OverlayRef } from '@angular/cdk/overlay' ;
13
14
import { _getFocusedElementPierceShadowDom } from '@angular/cdk/platform' ;
14
- import {
15
- BasePortalOutlet ,
16
- CdkPortalOutlet ,
17
- ComponentPortal ,
18
- DomPortal ,
19
- TemplatePortal ,
20
- } from '@angular/cdk/portal' ;
21
15
import { DOCUMENT } from '@angular/common' ;
22
16
import {
23
17
ChangeDetectionStrategy ,
24
18
ChangeDetectorRef ,
25
19
Component ,
26
- ComponentRef ,
27
20
ElementRef ,
28
- EmbeddedViewRef ,
29
21
EventEmitter ,
30
22
Inject ,
31
23
NgZone ,
32
24
OnDestroy ,
33
25
Optional ,
34
- ViewChild ,
35
26
ViewEncapsulation ,
36
27
} from '@angular/core' ;
37
28
import { Subscription } from 'rxjs' ;
38
29
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
42
30
43
31
/**
44
32
* Internal component that wraps user-provided bottom sheet content.
@@ -58,52 +46,49 @@ import {MatBottomSheetConfig} from './bottom-sheet-config';
58
46
host : {
59
47
'class' : 'mat-bottom-sheet-container' ,
60
48
'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' ,
64
52
'[@state]' : '_animationState' ,
65
53
'(@state.start)' : '_onAnimationStart($event)' ,
66
54
'(@state.done)' : '_onAnimationDone($event)' ,
67
55
} ,
68
56
} )
69
- export class MatBottomSheetContainer extends BasePortalOutlet implements OnDestroy {
57
+ export class MatBottomSheetContainer extends CdkDialogContainer implements OnDestroy {
70
58
private _breakpointSubscription : Subscription ;
71
59
72
- /** The portal outlet inside of this container into which the content will be loaded. */
73
- @ViewChild ( CdkPortalOutlet , { static : true } ) _portalOutlet : CdkPortalOutlet ;
74
-
75
60
/** The state of the bottom sheet animations. */
76
61
_animationState : 'void' | 'visible' | 'hidden' = 'void' ;
77
62
78
63
/** Emits whenever the state of the animation changes. */
79
64
_animationStateChanged = new EventEmitter < AnimationEvent > ( ) ;
80
65
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
-
90
66
/** Whether the component has been destroyed. */
91
67
private _destroyed : boolean ;
92
68
93
69
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 ,
100
72
@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 ,
103
80
) {
104
- super ( ) ;
81
+ super (
82
+ elementRef ,
83
+ focusTrapFactory ,
84
+ document ,
85
+ config ,
86
+ checker ,
87
+ ngZone ,
88
+ overlayRef ,
89
+ focusMonitor ,
90
+ ) ;
105
91
106
- this . _document = document ;
107
92
this . _breakpointSubscription = breakpointObserver
108
93
. observe ( [ Breakpoints . Medium , Breakpoints . Large , Breakpoints . XLarge ] )
109
94
. subscribe ( ( ) => {
@@ -122,34 +107,6 @@ export class MatBottomSheetContainer extends BasePortalOutlet implements OnDestr
122
107
} ) ;
123
108
}
124
109
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
-
153
110
/** Begin animation of bottom sheet entrance into view. */
154
111
enter ( ) : void {
155
112
if ( ! this . _destroyed ) {
@@ -166,15 +123,14 @@ export class MatBottomSheetContainer extends BasePortalOutlet implements OnDestr
166
123
}
167
124
}
168
125
169
- ngOnDestroy ( ) {
126
+ override ngOnDestroy ( ) {
127
+ super . ngOnDestroy ( ) ;
170
128
this . _breakpointSubscription . unsubscribe ( ) ;
171
129
this . _destroyed = true ;
172
130
}
173
131
174
132
_onAnimationDone ( event : AnimationEvent ) {
175
- if ( event . toState === 'hidden' ) {
176
- this . _restoreFocus ( ) ;
177
- } else if ( event . toState === 'visible' ) {
133
+ if ( event . toState === 'visible' ) {
178
134
this . _trapFocus ( ) ;
179
135
}
180
136
@@ -185,136 +141,9 @@ export class MatBottomSheetContainer extends BasePortalOutlet implements OnDestr
185
141
this . _animationStateChanged . emit ( event ) ;
186
142
}
187
143
144
+ protected override _captureInitialFocus ( ) : void { }
145
+
188
146
private _toggleClass ( cssClass : string , add : boolean ) {
189
147
this . _elementRef . nativeElement . classList . toggle ( cssClass , add ) ;
190
148
}
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
- }
320
149
}
0 commit comments