From c5ddfe8c8da93127ea61485cc4d1b7c92fc4045f Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Wed, 8 Apr 2020 07:24:23 +0200 Subject: [PATCH] =?UTF-8?q?fix(dialog):=20recapture=20focus=20when=20click?= =?UTF-8?q?ing=20on=20backdrop=20when=20cl=E2=80=A6=20(#18826)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `MatDialog` has an option to disable closing by clicking on the backdrop, but this can lead to focus being bumped back to the `body` and allowing users to tab past the dialog. These changes add some extra logic to recapture focus when the user clicks on the backdrop. Fixes #18799. --- src/material/dialog/dialog-container.ts | 41 +++++++++++++++------ src/material/dialog/dialog-ref.ts | 8 ++++ src/material/dialog/dialog.spec.ts | 23 ++++++++++++ src/material/dialog/dialog.ts | 9 ----- tools/public_api_guard/material/dialog.d.ts | 1 + 5 files changed, 61 insertions(+), 21 deletions(-) diff --git a/src/material/dialog/dialog-container.ts b/src/material/dialog/dialog-container.ts index 8ad2328a5544..a4af074ad51c 100644 --- a/src/material/dialog/dialog-container.ts +++ b/src/material/dialog/dialog-container.ts @@ -148,30 +148,31 @@ export class MatDialogContainer extends BasePortalOutlet { return this._portalOutlet.attachDomPortal(portal); } - /** Moves the focus inside the focus trap. */ - private _trapFocus() { - const element = this._elementRef.nativeElement; + /** Moves focus back into the dialog if it was moved out. */ + _recaptureFocus() { + if (!this._containsFocus()) { + const focusWasTrapped = this._getFocusTrap().focusInitialElement(); - if (!this._focusTrap) { - this._focusTrap = this._focusTrapFactory.create(element); + if (!focusWasTrapped) { + this._elementRef.nativeElement.focus(); + } } + } + /** Moves the focus inside the focus trap. */ + private _trapFocus() { // If we were to attempt to focus immediately, then the content of the dialog would not yet be // ready in instances where change detection has to run first. To deal with this, we simply // wait for the microtask queue to be empty. if (this._config.autoFocus) { - this._focusTrap.focusInitialElementWhenReady(); - } else { - const activeElement = this._document.activeElement; - + this._getFocusTrap().focusInitialElementWhenReady(); + } else if (!this._containsFocus()) { // Otherwise ensure that focus is on the dialog container. It's possible that a different // component tried to move focus while the open animation was running. See: // https://github.com/angular/components/issues/16215. Note that we only want to do this // if the focus isn't inside the dialog already, because it's possible that the consumer // turned off `autoFocus` in order to move focus themselves. - if (activeElement !== element && !element.contains(activeElement)) { - element.focus(); - } + this._elementRef.nativeElement.focus(); } } @@ -214,6 +215,22 @@ export class MatDialogContainer extends BasePortalOutlet { } } + /** Returns whether focus is inside the dialog. */ + private _containsFocus() { + const element = this._elementRef.nativeElement; + const activeElement = this._document.activeElement; + return element === activeElement || element.contains(activeElement); + } + + /** Gets the focus trap associated with the dialog. */ + private _getFocusTrap() { + if (!this._focusTrap) { + this._focusTrap = this._focusTrapFactory.create(this._elementRef.nativeElement); + } + + return this._focusTrap; + } + /** Callback, invoked whenever an animation on the host completes. */ _onAnimationDone(event: AnimationEvent) { if (event.toState === 'enter') { diff --git a/src/material/dialog/dialog-ref.ts b/src/material/dialog/dialog-ref.ts index b0bb0abc3f4e..246604dda820 100644 --- a/src/material/dialog/dialog-ref.ts +++ b/src/material/dialog/dialog-ref.ts @@ -94,6 +94,14 @@ export class MatDialogRef { event.preventDefault(); this.close(); }); + + _overlayRef.backdropClick().subscribe(() => { + if (this.disableClose) { + this._containerInstance._recaptureFocus(); + } else { + this.close(); + } + }); } /** diff --git a/src/material/dialog/dialog.spec.ts b/src/material/dialog/dialog.spec.ts index ead1e9bf3b68..7b153d995b38 100644 --- a/src/material/dialog/dialog.spec.ts +++ b/src/material/dialog/dialog.spec.ts @@ -973,6 +973,29 @@ describe('MatDialog', () => { expect(overlayContainerElement.querySelector('mat-dialog-container')).toBeFalsy(); })); + + it('should recapture focus when clicking on the backdrop', fakeAsync(() => { + dialog.open(PizzaMsg, { + disableClose: true, + viewContainerRef: testViewContainerRef + }); + + viewContainerFixture.detectChanges(); + flushMicrotasks(); + + let backdrop = overlayContainerElement.querySelector('.cdk-overlay-backdrop') as HTMLElement; + let input = overlayContainerElement.querySelector('input') as HTMLInputElement; + + expect(document.activeElement).toBe(input, 'Expected input to be focused on open'); + + input.blur(); // Programmatic clicks might not move focus so we simulate it. + backdrop.click(); + viewContainerFixture.detectChanges(); + flush(); + + expect(document.activeElement).toBe(input, 'Expected input to stay focused after click'); + })); + }); describe('hasBackdrop option', () => { diff --git a/src/material/dialog/dialog.ts b/src/material/dialog/dialog.ts index caee0e8b19c6..9c8ad48e852f 100644 --- a/src/material/dialog/dialog.ts +++ b/src/material/dialog/dialog.ts @@ -248,15 +248,6 @@ export class MatDialog implements OnDestroy { const dialogRef = new MatDialogRef(overlayRef, dialogContainer, config.id); - // When the dialog backdrop is clicked, we want to close it. - if (config.hasBackdrop) { - overlayRef.backdropClick().subscribe(() => { - if (!dialogRef.disableClose) { - dialogRef.close(); - } - }); - } - if (componentOrTemplateRef instanceof TemplateRef) { dialogContainer.attachTemplatePortal( new TemplatePortal(componentOrTemplateRef, null!, diff --git a/tools/public_api_guard/material/dialog.d.ts b/tools/public_api_guard/material/dialog.d.ts index 325de439d173..d41775d83773 100644 --- a/tools/public_api_guard/material/dialog.d.ts +++ b/tools/public_api_guard/material/dialog.d.ts @@ -99,6 +99,7 @@ export declare class MatDialogContainer extends BasePortalOutlet { _config: MatDialogConfig); _onAnimationDone(event: AnimationEvent): void; _onAnimationStart(event: AnimationEvent): void; + _recaptureFocus(): void; _startExitAnimation(): void; attachComponentPortal(portal: ComponentPortal): ComponentRef; attachTemplatePortal(portal: TemplatePortal): EmbeddedViewRef;