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' ;
1111import { _getFocusedElementPierceShadowDom } from '@angular/cdk/platform' ;
1212import {
1313 BasePortalOutlet ,
@@ -26,6 +26,7 @@ import {
2626 EmbeddedViewRef ,
2727 HostBinding ,
2828 Inject ,
29+ NgZone ,
2930 OnDestroy ,
3031 Optional ,
3132 ViewChild ,
@@ -123,6 +124,8 @@ export class CdkDialogContainer extends BasePortalOutlet implements OnDestroy {
123124 private _elementRef : ElementRef < HTMLElement > ,
124125 private _focusTrapFactory : FocusTrapFactory ,
125126 private _changeDetectorRef : ChangeDetectorRef ,
127+ private readonly _interactivityChecker : InteractivityChecker ,
128+ private readonly _ngZone : NgZone ,
126129 @Optional ( ) @Inject ( DOCUMENT ) _document : any ,
127130 /** The dialog configuration. */
128131 public _config : DialogConfig ) {
@@ -138,7 +141,7 @@ export class CdkDialogContainer extends BasePortalOutlet implements OnDestroy {
138141 } ) ) . subscribe ( event => {
139142 // Emit lifecycle events based on animation `done` callback.
140143 if ( event . toState === 'enter' ) {
141- this . _autoFocusFirstTabbableElement ( ) ;
144+ this . _autoFocus ( ) ;
142145 this . _afterEnter . next ( ) ;
143146 this . _afterEnter . complete ( ) ;
144147 }
@@ -242,34 +245,74 @@ export class CdkDialogContainer extends BasePortalOutlet implements OnDestroy {
242245 }
243246
244247 /**
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.
247251 */
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 ( ) {
249281 const element = this . _elementRef . nativeElement ;
250282
251283 // If were to attempt to focus immediately, then the content of the dialog would not yet be
252284 // 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 ) ) {
259298 element . focus ( ) ;
260299 }
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 ;
273316 }
274317 }
275318
@@ -278,7 +321,7 @@ export class CdkDialogContainer extends BasePortalOutlet implements OnDestroy {
278321 const toFocus = this . _elementFocusedBeforeDialogWasOpened ;
279322 // We need the extra check, because IE can set the `activeElement` to null in some cases.
280323 if ( toFocus && typeof toFocus . focus === 'function' ) {
281- const activeElement = this . _document . activeElement ;
324+ const activeElement = _getFocusedElementPierceShadowDom ( ) ;
282325 const element = this . _elementRef . nativeElement ;
283326
284327 // Make sure that focus is still inside the dialog or is on the body (usually because a
0 commit comments