diff --git a/src/cdk-experimental/dialog/dialog-ref.ts b/src/cdk-experimental/dialog/dialog-ref.ts index d8b26381311..428f345e52d 100644 --- a/src/cdk-experimental/dialog/dialog-ref.ts +++ b/src/cdk-experimental/dialog/dialog-ref.ts @@ -8,7 +8,7 @@ import {OverlayRef, GlobalPositionStrategy, OverlaySizeConfig} from '@angular/cdk/overlay'; -import {ESCAPE} from '@angular/cdk/keycodes'; +import {ESCAPE, hasModifierKey} from '@angular/cdk/keycodes'; import {Observable} from 'rxjs'; import {map, filter} from 'rxjs/operators'; import {DialogPosition} from './dialog-config'; @@ -56,8 +56,13 @@ export class DialogRef { // Close when escape keydown event occurs _overlayRef.keydownEvents() - .pipe(filter(event => event.keyCode === ESCAPE && !this.disableClose)) - .subscribe(() => this.close()); + .pipe(filter(event => { + return event.keyCode === ESCAPE && !this.disableClose && !hasModifierKey(event); + })) + .subscribe(event => { + event.preventDefault(); + this.close(); + }); } /** Gets an observable that emits when the overlay's backdrop has been clicked. */ diff --git a/src/cdk-experimental/dialog/dialog.spec.ts b/src/cdk-experimental/dialog/dialog.spec.ts index d95ad46cfec..3ebca125f07 100644 --- a/src/cdk-experimental/dialog/dialog.spec.ts +++ b/src/cdk-experimental/dialog/dialog.spec.ts @@ -26,7 +26,7 @@ import {Directionality} from '@angular/cdk/bidi'; import {CdkDialogContainer} from './dialog-container'; import {OverlayContainer} from '@angular/cdk/overlay'; import {A, ESCAPE} from '@angular/cdk/keycodes'; -import {dispatchKeyboardEvent} from '@angular/cdk/testing'; +import {dispatchKeyboardEvent, createKeyboardEvent, dispatchEvent} from '@angular/cdk/testing'; import {DIALOG_DATA, Dialog, DialogModule, DialogRef} from './index'; describe('Dialog', () => { @@ -216,11 +216,26 @@ describe('Dialog', () => { dialog.openFromComponent(PizzaMsg, {viewContainerRef: testViewContainerRef}); viewContainerFixture.detectChanges(); - dispatchKeyboardEvent(document.body, 'keydown', ESCAPE); + const event = dispatchKeyboardEvent(document.body, 'keydown', ESCAPE); viewContainerFixture.detectChanges(); flush(); expect(overlayContainerElement.querySelector('cdk-dialog-container')).toBeNull(); + expect(event.defaultPrevented).toBe(true); + })); + + it('should not close a dialog via the escape key if a modifier is pressed', fakeAsync(() => { + dialog.openFromComponent(PizzaMsg, {viewContainerRef: testViewContainerRef}); + + viewContainerFixture.detectChanges(); + const event = createKeyboardEvent('keydown', ESCAPE); + Object.defineProperty(event, 'altKey', {get: () => true}); + dispatchEvent(document.body, event); + viewContainerFixture.detectChanges(); + flush(); + + expect(overlayContainerElement.querySelector('cdk-dialog-container')).toBeTruthy(); + expect(event.defaultPrevented).toBe(false); })); it('should close from a ViewContainerRef with OnPush change detection', fakeAsync(() => { diff --git a/src/cdk/overlay/overlay-directives.spec.ts b/src/cdk/overlay/overlay-directives.spec.ts index ff8d346fe47..bc0a91b4e44 100644 --- a/src/cdk/overlay/overlay-directives.spec.ts +++ b/src/cdk/overlay/overlay-directives.spec.ts @@ -2,7 +2,7 @@ import {Component, ViewChild} from '@angular/core'; import {By} from '@angular/platform-browser'; import {ComponentFixture, TestBed, async, inject, fakeAsync, tick} from '@angular/core/testing'; import {Directionality} from '@angular/cdk/bidi'; -import {dispatchKeyboardEvent} from '@angular/cdk/testing'; +import {dispatchKeyboardEvent, createKeyboardEvent, dispatchEvent} from '@angular/cdk/testing'; import {ESCAPE, A} from '@angular/cdk/keycodes'; import {CdkConnectedOverlay, OverlayModule, CdkOverlayOrigin} from './index'; import {OverlayContainer} from './overlay-container'; @@ -109,11 +109,25 @@ describe('Overlay directives', () => { fixture.componentInstance.isOpen = true; fixture.detectChanges(); - dispatchKeyboardEvent(document.body, 'keydown', ESCAPE); + const event = dispatchKeyboardEvent(document.body, 'keydown', ESCAPE); fixture.detectChanges(); expect(overlayContainerElement.textContent!.trim()).toBe('', 'Expected overlay to have been detached.'); + expect(event.defaultPrevented).toBe(true); + }); + + it('should not close when pressing escape with a modifier', () => { + fixture.componentInstance.isOpen = true; + fixture.detectChanges(); + + const event = createKeyboardEvent('keydown', ESCAPE); + Object.defineProperty(event, 'altKey', {get: () => true}); + dispatchEvent(document.body, event); + fixture.detectChanges(); + + expect(overlayContainerElement.textContent!.trim()).toBeTruthy(); + expect(event.defaultPrevented).toBe(false); }); it('should not depend on the order in which the `origin` and `open` are set', async(() => { diff --git a/src/cdk/overlay/overlay-directives.ts b/src/cdk/overlay/overlay-directives.ts index c360c50bea7..97eb07d5eb2 100644 --- a/src/cdk/overlay/overlay-directives.ts +++ b/src/cdk/overlay/overlay-directives.ts @@ -8,7 +8,7 @@ import {Direction, Directionality} from '@angular/cdk/bidi'; import {coerceBooleanProperty} from '@angular/cdk/coercion'; -import {ESCAPE} from '@angular/cdk/keycodes'; +import {ESCAPE, hasModifierKey} from '@angular/cdk/keycodes'; import {TemplatePortal} from '@angular/cdk/portal'; import { Directive, @@ -275,7 +275,8 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges { this._overlayRef.keydownEvents().subscribe((event: KeyboardEvent) => { this.overlayKeydown.next(event); - if (event.keyCode === ESCAPE) { + if (event.keyCode === ESCAPE && !hasModifierKey(event)) { + event.preventDefault(); this._detachOverlay(); } }); diff --git a/src/material-experimental/mdc-menu/menu.spec.ts b/src/material-experimental/mdc-menu/menu.spec.ts index 4e6ed3a5c12..7c9a6c6a019 100644 --- a/src/material-experimental/mdc-menu/menu.spec.ts +++ b/src/material-experimental/mdc-menu/menu.spec.ts @@ -346,6 +346,24 @@ describe('MatMenu', () => { tick(500); expect(overlayContainerElement.textContent).toBe(''); + expect(event.defaultPrevented).toBe(true); + })); + + it('should not close the menu when pressing ESCAPE with a modifier', fakeAsync(() => { + const fixture = createComponent(SimpleMenu, [], [FakeIcon]); + fixture.detectChanges(); + fixture.componentInstance.trigger.openMenu(); + + const panel = overlayContainerElement.querySelector('.mat-mdc-menu-panel')!; + const event = createKeyboardEvent('keydown', ESCAPE); + + Object.defineProperty(event, 'altKey', {get: () => true}); + dispatchEvent(panel, event); + fixture.detectChanges(); + tick(500); + + expect(overlayContainerElement.textContent).toBeTruthy(); + expect(event.defaultPrevented).toBe(false); })); it('should open a custom menu', () => { diff --git a/src/material/bottom-sheet/bottom-sheet-ref.ts b/src/material/bottom-sheet/bottom-sheet-ref.ts index 258de1fcd33..d80544ef95b 100644 --- a/src/material/bottom-sheet/bottom-sheet-ref.ts +++ b/src/material/bottom-sheet/bottom-sheet-ref.ts @@ -7,7 +7,7 @@ */ import {Location} from '@angular/common'; -import {ESCAPE} from '@angular/cdk/keycodes'; +import {ESCAPE, hasModifierKey} from '@angular/cdk/keycodes'; import {OverlayRef} from '@angular/cdk/overlay'; import {merge, Observable, Subject} from 'rxjs'; import {filter, take} from 'rxjs/operators'; @@ -72,8 +72,10 @@ export class MatBottomSheetRef { merge( _overlayRef.backdropClick(), _overlayRef.keydownEvents().pipe(filter(event => event.keyCode === ESCAPE)) - ).subscribe(() => { - if (!this.disableClose) { + ).subscribe(event => { + if (!this.disableClose && + (event.type !== 'keydown' || !hasModifierKey(event as KeyboardEvent))) { + event.preventDefault(); this.dismiss(); } }); diff --git a/src/material/bottom-sheet/bottom-sheet.spec.ts b/src/material/bottom-sheet/bottom-sheet.spec.ts index d51b7c38dbc..d8ec9bde934 100644 --- a/src/material/bottom-sheet/bottom-sheet.spec.ts +++ b/src/material/bottom-sheet/bottom-sheet.spec.ts @@ -2,7 +2,7 @@ import {Directionality} from '@angular/cdk/bidi'; import {A, ESCAPE} from '@angular/cdk/keycodes'; import {OverlayContainer, ScrollStrategy} from '@angular/cdk/overlay'; import {ViewportRuler} from '@angular/cdk/scrolling'; -import {dispatchKeyboardEvent} from '@angular/cdk/testing'; +import {dispatchKeyboardEvent, createKeyboardEvent, dispatchEvent} from '@angular/cdk/testing'; import {Location} from '@angular/common'; import {SpyLocation} from '@angular/common/testing'; import { @@ -155,11 +155,25 @@ describe('MatBottomSheet', () => { it('should close a bottom sheet via the escape key', fakeAsync(() => { bottomSheet.open(PizzaMsg, {viewContainerRef: testViewContainerRef}); - dispatchKeyboardEvent(document.body, 'keydown', ESCAPE); + const event = dispatchKeyboardEvent(document.body, 'keydown', ESCAPE); viewContainerFixture.detectChanges(); flush(); expect(overlayContainerElement.querySelector('mat-bottom-sheet-container')).toBeNull(); + expect(event.defaultPrevented).toBe(true); + })); + + it('should not close a bottom sheet via the escape key with a modifier', fakeAsync(() => { + bottomSheet.open(PizzaMsg, {viewContainerRef: testViewContainerRef}); + + const event = createKeyboardEvent('keydown', ESCAPE); + Object.defineProperty(event, 'altKey', {get: () => true}); + dispatchEvent(document.body, event); + viewContainerFixture.detectChanges(); + flush(); + + expect(overlayContainerElement.querySelector('mat-bottom-sheet-container')).toBeTruthy(); + expect(event.defaultPrevented).toBe(false); })); it('should close when clicking on the overlay backdrop', fakeAsync(() => { diff --git a/src/material/datepicker/datepicker.spec.ts b/src/material/datepicker/datepicker.spec.ts index ab5bcedc18c..ff7a0195697 100644 --- a/src/material/datepicker/datepicker.spec.ts +++ b/src/material/datepicker/datepicker.spec.ts @@ -193,11 +193,12 @@ describe('MatDatepicker', () => { expect(testComponent.datepicker.opened).toBe(true, 'Expected datepicker to be open.'); - dispatchKeyboardEvent(document.body, 'keydown', ESCAPE); + const event = dispatchKeyboardEvent(document.body, 'keydown', ESCAPE); fixture.detectChanges(); flush(); expect(testComponent.datepicker.opened).toBe(false, 'Expected datepicker to be closed.'); + expect(event.defaultPrevented).toBe(true); })); it('should set the proper role on the popup', fakeAsync(() => { diff --git a/src/material/datepicker/datepicker.ts b/src/material/datepicker/datepicker.ts index 8a5dc4faf70..25ee51eb6f2 100644 --- a/src/material/datepicker/datepicker.ts +++ b/src/material/datepicker/datepicker.ts @@ -459,7 +459,13 @@ export class MatDatepicker implements OnDestroy, CanColor { return event.keyCode === ESCAPE || (this._datepickerInput && event.altKey && event.keyCode === UP_ARROW); })) - ).subscribe(() => this.close()); + ).subscribe(event => { + if (event) { + event.preventDefault(); + } + + this.close(); + }); } /** Create the popup PositionStrategy. */ diff --git a/src/material/dialog/dialog-ref.ts b/src/material/dialog/dialog-ref.ts index 490e5699480..02b1ec017a2 100644 --- a/src/material/dialog/dialog-ref.ts +++ b/src/material/dialog/dialog-ref.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {ESCAPE} from '@angular/cdk/keycodes'; +import {ESCAPE, hasModifierKey} from '@angular/cdk/keycodes'; import {GlobalPositionStrategy, OverlayRef} from '@angular/cdk/overlay'; import {Location} from '@angular/common'; import {Observable, Subject} from 'rxjs'; @@ -78,8 +78,13 @@ export class MatDialogRef { }); _overlayRef.keydownEvents() - .pipe(filter(event => event.keyCode === ESCAPE && !this.disableClose)) - .subscribe(() => this.close()); + .pipe(filter(event => { + return event.keyCode === ESCAPE && !this.disableClose && !hasModifierKey(event); + })) + .subscribe(event => { + event.preventDefault(); + this.close(); + }); } /** diff --git a/src/material/dialog/dialog.spec.ts b/src/material/dialog/dialog.spec.ts index 2ae1e539c02..38ba018d3f8 100644 --- a/src/material/dialog/dialog.spec.ts +++ b/src/material/dialog/dialog.spec.ts @@ -27,7 +27,7 @@ import {MatDialogContainer} from './dialog-container'; import {OverlayContainer, ScrollStrategy, Overlay} from '@angular/cdk/overlay'; import {ScrollDispatcher} from '@angular/cdk/scrolling'; import {A, ESCAPE} from '@angular/cdk/keycodes'; -import {dispatchKeyboardEvent} from '@angular/cdk/testing'; +import {dispatchKeyboardEvent, createKeyboardEvent} from '@angular/cdk/testing'; import { MAT_DIALOG_DATA, MatDialog, @@ -240,11 +240,26 @@ describe('MatDialog', () => { viewContainerRef: testViewContainerRef }); - dispatchKeyboardEvent(document.body, 'keydown', ESCAPE); + const event = dispatchKeyboardEvent(document.body, 'keydown', ESCAPE); viewContainerFixture.detectChanges(); flush(); expect(overlayContainerElement.querySelector('mat-dialog-container')).toBeNull(); + expect(event.defaultPrevented).toBe(true); + })); + + it('should not close a dialog via the escape key with a modifier', fakeAsync(() => { + dialog.open(PizzaMsg, { + viewContainerRef: testViewContainerRef + }); + + const event = createKeyboardEvent('keydown', ESCAPE); + Object.defineProperty(event, 'altKey', {get: () => true}); + viewContainerFixture.detectChanges(); + flush(); + + expect(overlayContainerElement.querySelector('mat-dialog-container')).toBeTruthy(); + expect(event.defaultPrevented).toBe(false); })); it('should close from a ViewContainerRef with OnPush change detection', fakeAsync(() => { diff --git a/src/material/menu/menu.spec.ts b/src/material/menu/menu.spec.ts index dc6d7f78b8c..afcf7cbfee8 100644 --- a/src/material/menu/menu.spec.ts +++ b/src/material/menu/menu.spec.ts @@ -347,6 +347,24 @@ describe('MatMenu', () => { tick(500); expect(overlayContainerElement.textContent).toBe(''); + expect(event.defaultPrevented).toBe(true); + })); + + it('should not close the menu when pressing ESCAPE with a modifier', fakeAsync(() => { + const fixture = createComponent(SimpleMenu, [], [FakeIcon]); + fixture.detectChanges(); + fixture.componentInstance.trigger.openMenu(); + + const panel = overlayContainerElement.querySelector('.mat-menu-panel')!; + const event = createKeyboardEvent('keydown', ESCAPE); + + Object.defineProperty(event, 'altKey', {get: () => true}); + dispatchEvent(panel, event); + fixture.detectChanges(); + tick(500); + + expect(overlayContainerElement.textContent).toBeTruthy(); + expect(event.defaultPrevented).toBe(false); })); it('should open a custom menu', () => { diff --git a/src/material/menu/menu.ts b/src/material/menu/menu.ts index 1710c866379..fb4e65d03a8 100644 --- a/src/material/menu/menu.ts +++ b/src/material/menu/menu.ts @@ -266,7 +266,10 @@ export class _MatMenuBase implements AfterContentInit, MatMenuPanel switch (keyCode) { case ESCAPE: - this.closed.emit('keydown'); + if (!hasModifierKey(event)) { + event.preventDefault(); + this.closed.emit('keydown'); + } break; case LEFT_ARROW: if (this.parentMenu && this.direction === 'ltr') { diff --git a/src/material/sidenav/drawer.spec.ts b/src/material/sidenav/drawer.spec.ts index 3f85bb0ee3b..1ddb8e0bb50 100644 --- a/src/material/sidenav/drawer.spec.ts +++ b/src/material/sidenav/drawer.spec.ts @@ -15,7 +15,7 @@ import {Direction} from '@angular/cdk/bidi'; import {A11yModule} from '@angular/cdk/a11y'; import {PlatformModule} from '@angular/cdk/platform'; import {ESCAPE} from '@angular/cdk/keycodes'; -import {dispatchKeyboardEvent} from '@angular/cdk/testing'; +import {dispatchKeyboardEvent, createKeyboardEvent, dispatchEvent} from '@angular/cdk/testing'; import {CdkScrollable} from '@angular/cdk/scrolling'; @@ -202,12 +202,39 @@ describe('MatDrawer', () => { expect(testComponent.closeCount).toBe(0, 'Expected no close events.'); expect(testComponent.closeStartCount).toBe(0, 'Expected no close start events.'); - dispatchKeyboardEvent(drawer.nativeElement, 'keydown', ESCAPE); + const event = dispatchKeyboardEvent(drawer.nativeElement, 'keydown', ESCAPE); fixture.detectChanges(); flush(); expect(testComponent.closeCount).toBe(1, 'Expected one close event.'); expect(testComponent.closeStartCount).toBe(1, 'Expected one close start event.'); + expect(event.defaultPrevented).toBe(true); + })); + + it('should not close when pressing escape with a modifier', fakeAsync(() => { + let fixture = TestBed.createComponent(BasicTestApp); + + fixture.detectChanges(); + + let testComponent: BasicTestApp = fixture.debugElement.componentInstance; + let drawer = fixture.debugElement.query(By.directive(MatDrawer)); + + drawer.componentInstance.open(); + fixture.detectChanges(); + tick(); + + expect(testComponent.closeCount).toBe(0, 'Expected no close events.'); + expect(testComponent.closeStartCount).toBe(0, 'Expected no close start events.'); + + const event = createKeyboardEvent('keydown', ESCAPE); + Object.defineProperty(event, 'altKey', {get: () => true}); + dispatchEvent(drawer.nativeElement, event); + fixture.detectChanges(); + flush(); + + expect(testComponent.closeCount).toBe(0, 'Expected still no close events.'); + expect(testComponent.closeStartCount).toBe(0, 'Expected still no close start events.'); + expect(event.defaultPrevented).toBe(false); })); it('should fire the open event when open on init', fakeAsync(() => { diff --git a/src/material/sidenav/drawer.ts b/src/material/sidenav/drawer.ts index d79de71389c..a3d28c8d620 100644 --- a/src/material/sidenav/drawer.ts +++ b/src/material/sidenav/drawer.ts @@ -9,7 +9,7 @@ import {AnimationEvent} from '@angular/animations'; import {FocusMonitor, FocusOrigin, FocusTrap, FocusTrapFactory} from '@angular/cdk/a11y'; import {Directionality} from '@angular/cdk/bidi'; import {coerceBooleanProperty} from '@angular/cdk/coercion'; -import {ESCAPE} from '@angular/cdk/keycodes'; +import {ESCAPE, hasModifierKey} from '@angular/cdk/keycodes'; import {Platform} from '@angular/cdk/platform'; import {CdkScrollable, ScrollDispatcher, ViewportRuler} from '@angular/cdk/scrolling'; import {DOCUMENT} from '@angular/common'; @@ -267,11 +267,14 @@ export class MatDrawer implements AfterContentInit, AfterContentChecked, OnDestr */ this._ngZone.runOutsideAngular(() => { (fromEvent(this._elementRef.nativeElement, 'keydown') as Observable).pipe( - filter(event => event.keyCode === ESCAPE && !this.disableClose), + filter(event => { + return event.keyCode === ESCAPE && !this.disableClose && !hasModifierKey(event); + }), takeUntil(this._destroyed) ).subscribe(event => this._ngZone.run(() => { this.close(); event.stopPropagation(); + event.preventDefault(); })); }); diff --git a/src/material/tooltip/tooltip.spec.ts b/src/material/tooltip/tooltip.spec.ts index e906b317022..06f887ef99c 100644 --- a/src/material/tooltip/tooltip.spec.ts +++ b/src/material/tooltip/tooltip.spec.ts @@ -27,6 +27,8 @@ import { dispatchKeyboardEvent, patchElementFocus, dispatchMouseEvent, + createKeyboardEvent, + dispatchEvent, } from '@angular/cdk/testing'; import {ESCAPE} from '@angular/cdk/keycodes'; import {FocusMonitor} from '@angular/cdk/a11y'; @@ -635,6 +637,32 @@ describe('MatTooltip', () => { flush(); })); + it('should preventDefault when pressing ESCAPE', fakeAsync(() => { + tooltipDirective.show(); + tick(0); + fixture.detectChanges(); + + const event = dispatchKeyboardEvent(buttonElement, 'keydown', ESCAPE); + fixture.detectChanges(); + flush(); + + expect(event.defaultPrevented).toBe(true); + })); + + it('should not preventDefault when pressing ESCAPE with a modifier', fakeAsync(() => { + tooltipDirective.show(); + tick(0); + fixture.detectChanges(); + + const event = createKeyboardEvent('keydown', ESCAPE); + Object.defineProperty(event, 'altKey', {get: () => true}); + dispatchEvent(buttonElement, event); + fixture.detectChanges(); + flush(); + + expect(event.defaultPrevented).toBe(false); + })); + it('should not show the tooltip on progammatic focus', fakeAsync(() => { patchElementFocus(buttonElement); assertTooltipInstance(tooltipDirective, false); diff --git a/src/material/tooltip/tooltip.ts b/src/material/tooltip/tooltip.ts index 4f8179ce0a6..040cdaf2d23 100644 --- a/src/material/tooltip/tooltip.ts +++ b/src/material/tooltip/tooltip.ts @@ -9,7 +9,7 @@ import {AnimationEvent} from '@angular/animations'; import {AriaDescriber, FocusMonitor} from '@angular/cdk/a11y'; import {Directionality} from '@angular/cdk/bidi'; import {coerceBooleanProperty} from '@angular/cdk/coercion'; -import {ESCAPE} from '@angular/cdk/keycodes'; +import {ESCAPE, hasModifierKey} from '@angular/cdk/keycodes'; import {BreakpointObserver, Breakpoints, BreakpointState} from '@angular/cdk/layout'; import { FlexibleConnectedPositionStrategy, @@ -331,7 +331,8 @@ export class MatTooltip implements OnDestroy, OnInit { /** Handles the keydown events on the host element. */ _handleKeydown(e: KeyboardEvent) { - if (this._isTooltipVisible() && e.keyCode === ESCAPE) { + if (this._isTooltipVisible() && e.keyCode === ESCAPE && !hasModifierKey(e)) { + e.preventDefault(); e.stopPropagation(); this.hide(0); }