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