From d8d40f05fd0c0fad920e54b634a559176e8d906d Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Tue, 25 Mar 2025 09:54:11 +0100 Subject: [PATCH] fix(material/sidenav): ignore escape events while overlay is open The sidenav isn't an overlay so it doesn't participate in the common event handling. This means that if there's an overlay in it that closes by pressing escape, the sidenav will close instead of the overlay. These changes add a check that will skip escape key presses while there are open overlays. Fixes #30507. --- src/material/sidenav/BUILD.bazel | 2 ++ src/material/sidenav/drawer.spec.ts | 40 +++++++++++++++++++++++++++++ src/material/sidenav/drawer.ts | 31 ++++++++++++---------- 3 files changed, 60 insertions(+), 13 deletions(-) diff --git a/src/material/sidenav/BUILD.bazel b/src/material/sidenav/BUILD.bazel index 52b318181a63..29327a427d67 100644 --- a/src/material/sidenav/BUILD.bazel +++ b/src/material/sidenav/BUILD.bazel @@ -24,6 +24,7 @@ ng_module( "//src/cdk/bidi", "//src/cdk/coercion", "//src/cdk/keycodes", + "//src/cdk/overlay", "//src/cdk/scrolling", "//src/material/core", "@npm//@angular/core", @@ -57,6 +58,7 @@ ng_test_library( "//src/cdk/a11y", "//src/cdk/bidi", "//src/cdk/keycodes", + "//src/cdk/overlay", "//src/cdk/platform", "//src/cdk/scrolling", "//src/cdk/testing", diff --git a/src/material/sidenav/drawer.spec.ts b/src/material/sidenav/drawer.spec.ts index 7e21171f5b97..7fd58b067661 100644 --- a/src/material/sidenav/drawer.spec.ts +++ b/src/material/sidenav/drawer.spec.ts @@ -2,6 +2,7 @@ import {A11yModule} from '@angular/cdk/a11y'; import {Direction} from '@angular/cdk/bidi'; import {ESCAPE} from '@angular/cdk/keycodes'; import {CdkScrollable} from '@angular/cdk/scrolling'; +import {OverlayKeyboardDispatcher, OverlayRef} from '@angular/cdk/overlay'; import { createKeyboardEvent, dispatchEvent, @@ -22,7 +23,13 @@ import {NoopAnimationsModule} from '@angular/platform-browser/animations'; import {MatDrawer, MatDrawerContainer, MatSidenavModule} from './index'; describe('MatDrawer', () => { + let fakeKeyboardDispatcher: OverlayKeyboardDispatcher; + beforeEach(waitForAsync(() => { + fakeKeyboardDispatcher = { + _attachedOverlays: [] as OverlayRef[], + } as OverlayKeyboardDispatcher; + TestBed.configureTestingModule({ imports: [ MatSidenavModule, @@ -39,6 +46,12 @@ describe('MatDrawer', () => { IndirectDescendantDrawer, NestedDrawerContainers, ], + providers: [ + { + provide: OverlayKeyboardDispatcher, + useValue: fakeKeyboardDispatcher, + }, + ], }); })); @@ -237,6 +250,33 @@ describe('MatDrawer', () => { expect(event.defaultPrevented).toBe(false); })); + it('should not close when pressing escape while an overlay is open', fakeAsync(() => { + const fixture = TestBed.createComponent(BasicTestApp); + fixture.detectChanges(); + + const testComponent: BasicTestApp = fixture.debugElement.componentInstance; + const drawer = fixture.debugElement.query(By.directive(MatDrawer))!; + + drawer.componentInstance.open(); + fixture.detectChanges(); + tick(); + + expect(testComponent.closeCount).withContext('Expected no close events.').toBe(0); + expect(testComponent.closeStartCount).withContext('Expected no close start events.').toBe(0); + + fakeKeyboardDispatcher._attachedOverlays.push(null!); + const event = createKeyboardEvent('keydown', ESCAPE); + dispatchEvent(drawer.nativeElement, event); + fixture.detectChanges(); + flush(); + + expect(testComponent.closeCount).withContext('Expected still no close events.').toBe(0); + expect(testComponent.closeStartCount) + .withContext('Expected still no close start events.') + .toBe(0); + expect(event.defaultPrevented).toBe(false); + })); + it('should fire the open event when open on init', fakeAsync(() => { const fixture = TestBed.createComponent(DrawerSetToOpenedTrue); diff --git a/src/material/sidenav/drawer.ts b/src/material/sidenav/drawer.ts index efbb55507e75..1e51ce3c00dd 100644 --- a/src/material/sidenav/drawer.ts +++ b/src/material/sidenav/drawer.ts @@ -16,6 +16,7 @@ import {Directionality} from '@angular/cdk/bidi'; import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion'; import {ESCAPE, hasModifierKey} from '@angular/cdk/keycodes'; import {Platform} from '@angular/cdk/platform'; +import {OverlayKeyboardDispatcher} from '@angular/cdk/overlay'; import {CdkScrollable, ScrollDispatcher, ViewportRuler} from '@angular/cdk/scrolling'; import {DOCUMENT} from '@angular/common'; import { @@ -179,6 +180,7 @@ export class MatDrawer implements AfterViewInit, OnDestroy { private _renderer = inject(Renderer2); private readonly _interactivityChecker = inject(InteractivityChecker); private _doc = inject(DOCUMENT, {optional: true})!; + private _keyboardDispatcher = inject(OverlayKeyboardDispatcher, {optional: true}); _container? = inject(MAT_DRAWER_CONTAINER, {optional: true}); private _focusTrap: FocusTrap | null = null; @@ -360,19 +362,22 @@ export class MatDrawer implements AfterViewInit, OnDestroy { this._ngZone.runOutsideAngular(() => { const element = this._elementRef.nativeElement; (fromEvent(element, 'keydown') as Observable) - .pipe( - filter(event => { - return event.keyCode === ESCAPE && !this.disableClose && !hasModifierKey(event); - }), - takeUntil(this._destroyed), - ) - .subscribe(event => - this._ngZone.run(() => { - this.close(); - event.stopPropagation(); - event.preventDefault(); - }), - ); + .pipe(takeUntil(this._destroyed)) + .subscribe(event => { + // Skip keyboard events if there are open overlays since they may be + // placed inside the sidenav and cause it to close unexpectedly. + if (this._keyboardDispatcher && this._keyboardDispatcher._attachedOverlays.length > 0) { + return; + } + + if (event.keyCode === ESCAPE && !this.disableClose && !hasModifierKey(event)) { + this._ngZone.run(() => { + this.close(); + event.stopPropagation(); + event.preventDefault(); + }); + } + }); this._eventCleanups = [ this._renderer.listen(element, 'transitionrun', this._handleTransitionEvent),