77 */
88
99import { 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' ;
1112import {
1213 BasePortalOutlet ,
1314 CdkPortalOutlet ,
@@ -25,6 +26,7 @@ import {
2526 EmbeddedViewRef ,
2627 HostBinding ,
2728 Inject ,
29+ NgZone ,
2830 OnDestroy ,
2931 Optional ,
3032 ViewChild ,
@@ -122,6 +124,8 @@ export class CdkDialogContainer extends BasePortalOutlet implements OnDestroy {
122124 private _elementRef : ElementRef < HTMLElement > ,
123125 private _focusTrapFactory : FocusTrapFactory ,
124126 private _changeDetectorRef : ChangeDetectorRef ,
127+ private readonly _checker : InteractivityChecker ,
128+ private readonly _ngZone : NgZone ,
125129 @Optional ( ) @Inject ( DOCUMENT ) _document : any ,
126130 /** The dialog configuration. */
127131 public _config : DialogConfig ) {
@@ -137,7 +141,7 @@ export class CdkDialogContainer extends BasePortalOutlet implements OnDestroy {
137141 } ) ) . subscribe ( event => {
138142 // Emit lifecycle events based on animation `done` callback.
139143 if ( event . toState === 'enter' ) {
140- this . _autoFocusFirstTabbableElement ( ) ;
144+ this . _autoFocus ( ) ;
141145 this . _afterEnter . next ( ) ;
142146 this . _afterEnter . complete ( ) ;
143147 }
@@ -228,7 +232,7 @@ export class CdkDialogContainer extends BasePortalOutlet implements OnDestroy {
228232 /** Saves a reference to the element that was focused before the dialog was opened. */
229233 private _savePreviouslyFocusedElement ( ) {
230234 if ( this . _document ) {
231- this . _elementFocusedBeforeDialogWasOpened = this . _document . activeElement as HTMLElement ;
235+ this . _elementFocusedBeforeDialogWasOpened = _getFocusedElementPierceShadowDom ( ) ;
232236 }
233237 }
234238
@@ -241,34 +245,71 @@ export class CdkDialogContainer extends BasePortalOutlet implements OnDestroy {
241245 }
242246
243247 /**
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.
246251 */
247- private _autoFocusFirstTabbableElement ( ) {
252+ private _forceFocus ( element : HTMLElement , options ?: FocusOptions ) {
253+ if ( ! this . _checker . 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 = this . _elementRef . nativeElement . querySelector ( selector ) as HTMLElement ;
268+ if ( elementToFocus ) {
269+ this . _forceFocus ( elementToFocus , options ) ;
270+ }
271+ }
272+
273+ /**
274+ * Autofocus the element specified by the autoFocus field. When autoFocus is not 'dialog', if
275+ * for some reason the element cannot be focused, the dialog container will be focused.
276+ */
277+ private _autoFocus ( ) {
248278 const element = this . _elementRef . nativeElement ;
249279
250280 // If were to attempt to focus immediately, then the content of the dialog would not yet be
251281 // 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 ) {
282+ // wait for the microtask queue to be empty when setting focus when autoFocus isn't set to
283+ // dialog. If the element inside the dialog can't be focused, then the container is focused
284+ // so the user can't tab into other elements behind it.
285+ switch ( this . _config . autoFocus ) {
286+ case false :
287+ case 'dialog' :
288+ const activeElement = _getFocusedElementPierceShadowDom ( ) ;
289+ // Ensure that focus is on the dialog container. It's possible that a different
290+ // component tried to move focus while the open animation was running. See:
291+ // https://github.com/angular/components/issues/16215. Note that we only want to do this
292+ // if the focus isn't inside the dialog already, because it's possible that the consumer
293+ // turned off `autoFocus` in order to move focus themselves.
294+ if ( activeElement !== element && ! element . contains ( activeElement ) ) {
258295 element . focus ( ) ;
259296 }
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- }
297+ break ;
298+ case true :
299+ case 'first-tabbable' :
300+ this . _focusTrap . focusInitialElementWhenReady ( )
301+ . then ( hasMovedFocus => {
302+ if ( ! hasMovedFocus ) {
303+ element . focus ( ) ;
304+ }
305+ } ) ;
306+ break ;
307+ case 'first-heading' :
308+ this . _focusByCssSelector ( 'h1, h2, h3, h4, h5, h6' ) ;
309+ break ;
310+ default :
311+ this . _focusByCssSelector ( this . _config . autoFocus ! ) ;
312+ break ;
272313 }
273314 }
274315
@@ -277,7 +318,7 @@ export class CdkDialogContainer extends BasePortalOutlet implements OnDestroy {
277318 const toFocus = this . _elementFocusedBeforeDialogWasOpened ;
278319 // We need the extra check, because IE can set the `activeElement` to null in some cases.
279320 if ( toFocus && typeof toFocus . focus === 'function' ) {
280- const activeElement = this . _document . activeElement ;
321+ const activeElement = _getFocusedElementPierceShadowDom ( ) ;
281322 const element = this . _elementRef . nativeElement ;
282323
283324 // Make sure that focus is still inside the dialog or is on the body (usually because a
0 commit comments