7
7
*/
8
8
9
9
import { animate , AnimationEvent , state , style , transition , trigger } from '@angular/animations' ;
10
- import { FocusTrapFactory } from '@angular/cdk/a11y' ;
10
+ import { FocusTrapFactory , InteractivityChecker } from '@angular/cdk/a11y' ;
11
+ import { _getFocusedElementPierceShadowDom } from '@angular/cdk/platform' ;
11
12
import {
12
13
BasePortalOutlet ,
13
14
CdkPortalOutlet ,
@@ -25,6 +26,7 @@ import {
25
26
EmbeddedViewRef ,
26
27
HostBinding ,
27
28
Inject ,
29
+ NgZone ,
28
30
OnDestroy ,
29
31
Optional ,
30
32
ViewChild ,
@@ -122,6 +124,8 @@ export class CdkDialogContainer extends BasePortalOutlet implements OnDestroy {
122
124
private _elementRef : ElementRef < HTMLElement > ,
123
125
private _focusTrapFactory : FocusTrapFactory ,
124
126
private _changeDetectorRef : ChangeDetectorRef ,
127
+ private readonly _interactivityChecker : InteractivityChecker ,
128
+ private readonly _ngZone : NgZone ,
125
129
@Optional ( ) @Inject ( DOCUMENT ) _document : any ,
126
130
/** The dialog configuration. */
127
131
public _config : DialogConfig ) {
@@ -137,7 +141,7 @@ export class CdkDialogContainer extends BasePortalOutlet implements OnDestroy {
137
141
} ) ) . subscribe ( event => {
138
142
// Emit lifecycle events based on animation `done` callback.
139
143
if ( event . toState === 'enter' ) {
140
- this . _autoFocusFirstTabbableElement ( ) ;
144
+ this . _autoFocus ( ) ;
141
145
this . _afterEnter . next ( ) ;
142
146
this . _afterEnter . complete ( ) ;
143
147
}
@@ -228,7 +232,7 @@ export class CdkDialogContainer extends BasePortalOutlet implements OnDestroy {
228
232
/** Saves a reference to the element that was focused before the dialog was opened. */
229
233
private _savePreviouslyFocusedElement ( ) {
230
234
if ( this . _document ) {
231
- this . _elementFocusedBeforeDialogWasOpened = this . _document . activeElement as HTMLElement ;
235
+ this . _elementFocusedBeforeDialogWasOpened = _getFocusedElementPierceShadowDom ( ) ;
232
236
}
233
237
}
234
238
@@ -241,34 +245,72 @@ export class CdkDialogContainer extends BasePortalOutlet implements OnDestroy {
241
245
}
242
246
243
247
/**
244
- * Autofocus the first tabbable element inside of the dialog, if there is not a tabbable element,
245
- * focus the dialog instead.
248
+ * Focuses the provided element. If the element is not focusable, it will add a tabIndex
249
+ * attribute to forcefully focus it. The attribute is removed after focus is moved.
250
+ * @param element The element to focus.
246
251
*/
247
- private _autoFocusFirstTabbableElement ( ) {
252
+ private _forceFocus ( element : HTMLElement , options ?: FocusOptions ) {
253
+ if ( ! this . _interactivityChecker . isFocusable ( element ) ) {
254
+ element . tabIndex = - 1 ;
255
+ // The tabindex attribute should be removed to avoid navigating to that element again
256
+ this . _ngZone . runOutsideAngular ( ( ) =>
257
+ element . addEventListener ( 'blur' , ( ) => element . removeAttribute ( 'tabindex' ) ) ) ;
258
+ }
259
+ element . focus ( options ) ;
260
+ }
261
+
262
+ /**
263
+ * Focuses the first element that matches the given selector within the focus trap.
264
+ * @param selector The CSS selector for the element to set focus to.
265
+ */
266
+ private _focusByCssSelector ( selector : string , options ?: FocusOptions ) {
267
+ let elementToFocus =
268
+ this . _elementRef . nativeElement . querySelector ( selector ) as HTMLElement | null ;
269
+ if ( elementToFocus ) {
270
+ this . _forceFocus ( elementToFocus , options ) ;
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Autofocus the element specified by the autoFocus field. When autoFocus is not 'dialog', if
276
+ * for some reason the element cannot be focused, the dialog container will be focused.
277
+ */
278
+ private _autoFocus ( ) {
248
279
const element = this . _elementRef . nativeElement ;
249
280
250
281
// If were to attempt to focus immediately, then the content of the dialog would not yet be
251
282
// ready in instances where change detection has to run first. To deal with this, we simply
252
- // wait for the microtask queue to be empty.
253
- if ( this . _config . autoFocus ) {
254
- this . _focusTrap . focusInitialElementWhenReady ( ) . then ( hasMovedFocus => {
255
- // If we didn't find any focusable elements inside the dialog, focus the
256
- // container so the user can't tab into other elements behind it.
257
- if ( ! hasMovedFocus ) {
283
+ // wait for the microtask queue to be empty when setting focus when autoFocus isn't set to
284
+ // dialog. If the element inside the dialog can't be focused, then the container is focused
285
+ // so the user can't tab into other elements behind it.
286
+ switch ( this . _config . autoFocus ) {
287
+ case false :
288
+ case 'dialog' :
289
+ const activeElement = _getFocusedElementPierceShadowDom ( ) ;
290
+ // Ensure that focus is on the dialog container. It's possible that a different
291
+ // component tried to move focus while the open animation was running. See:
292
+ // https://github.com/angular/components/issues/16215. Note that we only want to do this
293
+ // if the focus isn't inside the dialog already, because it's possible that the consumer
294
+ // turned off `autoFocus` in order to move focus themselves.
295
+ if ( activeElement !== element && ! element . contains ( activeElement ) ) {
258
296
element . focus ( ) ;
259
297
}
260
- } ) ;
261
- } else {
262
- const activeElement = this . _document . activeElement ;
263
-
264
- // Otherwise ensure that focus is on the dialog container. It's possible that a different
265
- // component tried to move focus while the open animation was running. See:
266
- // https://github.com/angular/components/issues/16215. Note that we only want to do this
267
- // if the focus isn't inside the dialog already, because it's possible that the consumer
268
- // turned off `autoFocus` in order to move focus themselves.
269
- if ( activeElement !== element && ! element . contains ( activeElement ) ) {
270
- element . focus ( ) ;
271
- }
298
+ break ;
299
+ case true :
300
+ case 'first-tabbable' :
301
+ this . _focusTrap . focusInitialElementWhenReady ( )
302
+ . then ( hasMovedFocus => {
303
+ if ( ! hasMovedFocus ) {
304
+ element . focus ( ) ;
305
+ }
306
+ } ) ;
307
+ break ;
308
+ case 'first-heading' :
309
+ this . _focusByCssSelector ( 'h1, h2, h3, h4, h5, h6' ) ;
310
+ break ;
311
+ default :
312
+ this . _focusByCssSelector ( this . _config . autoFocus ! ) ;
313
+ break ;
272
314
}
273
315
}
274
316
@@ -277,7 +319,7 @@ export class CdkDialogContainer extends BasePortalOutlet implements OnDestroy {
277
319
const toFocus = this . _elementFocusedBeforeDialogWasOpened ;
278
320
// We need the extra check, because IE can set the `activeElement` to null in some cases.
279
321
if ( toFocus && typeof toFocus . focus === 'function' ) {
280
- const activeElement = this . _document . activeElement ;
322
+ const activeElement = _getFocusedElementPierceShadowDom ( ) ;
281
323
const element = this . _elementRef . nativeElement ;
282
324
283
325
// Make sure that focus is still inside the dialog or is on the body (usually because a
0 commit comments