Skip to content

Commit 3284407

Browse files
committed
fix(cdk/menu): nested menus requiring two taps to open touch devices (#27586)
The CDK menu supports opening a nested menu by hover, but the problem is that touch devices dispatch fake `mouseenter` events which end up closing the menu immediately. These changes add some logic to ignore such events. Fixes #27508. (cherry picked from commit d190e87)
1 parent 9ec77aa commit 3284407

File tree

2 files changed

+20
-3
lines changed

2 files changed

+20
-3
lines changed

src/cdk/menu/menu-item.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
Output,
1818
} from '@angular/core';
1919
import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion';
20-
import {FocusableOption} from '@angular/cdk/a11y';
20+
import {FocusableOption, InputModalityDetector} from '@angular/cdk/a11y';
2121
import {ENTER, hasModifierKey, LEFT_ARROW, RIGHT_ARROW, SPACE} from '@angular/cdk/keycodes';
2222
import {Directionality} from '@angular/cdk/bidi';
2323
import {fromEvent, Subject} from 'rxjs';
@@ -53,6 +53,7 @@ export class CdkMenuItem implements FocusableOption, FocusableElement, Toggler,
5353
protected readonly _dir = inject(Directionality, {optional: true});
5454
readonly _elementRef: ElementRef<HTMLElement> = inject(ElementRef);
5555
protected _ngZone = inject(NgZone);
56+
private readonly _inputModalityDetector = inject(InputModalityDetector);
5657

5758
/** The menu aim service used by this menu. */
5859
private readonly _menuAim = inject(MENU_AIM, {optional: true});
@@ -276,7 +277,14 @@ export class CdkMenuItem implements FocusableOption, FocusableElement, Toggler,
276277
this._ngZone.runOutsideAngular(() =>
277278
fromEvent(this._elementRef.nativeElement, 'mouseenter')
278279
.pipe(
279-
filter(() => !this._menuStack.isEmpty() && !this.hasMenu),
280+
filter(() => {
281+
return (
282+
// Skip fake `mouseenter` events dispatched by touch devices.
283+
this._inputModalityDetector.mostRecentModality !== 'touch' &&
284+
!this._menuStack.isEmpty() &&
285+
!this.hasMenu
286+
);
287+
}),
280288
takeUntil(this.destroyed),
281289
)
282290
.subscribe(() => {

src/cdk/menu/menu-trigger.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88

99
import {Directive, ElementRef, inject, NgZone, OnDestroy} from '@angular/core';
10+
import {InputModalityDetector} from '@angular/cdk/a11y';
1011
import {Directionality} from '@angular/cdk/bidi';
1112
import {
1213
ConnectedPosition,
@@ -69,6 +70,7 @@ export class CdkMenuTrigger extends CdkMenuTriggerBase implements OnDestroy {
6970
private readonly _elementRef: ElementRef<HTMLElement> = inject(ElementRef);
7071
private readonly _overlay = inject(Overlay);
7172
private readonly _ngZone = inject(NgZone);
73+
private readonly _inputModalityDetector = inject(InputModalityDetector);
7274
private readonly _directionality = inject(Directionality, {optional: true});
7375

7476
/** The parent menu this trigger belongs to. */
@@ -195,7 +197,14 @@ export class CdkMenuTrigger extends CdkMenuTriggerBase implements OnDestroy {
195197
this._ngZone.runOutsideAngular(() => {
196198
fromEvent(this._elementRef.nativeElement, 'mouseenter')
197199
.pipe(
198-
filter(() => !this.menuStack.isEmpty() && !this.isOpen()),
200+
filter(() => {
201+
return (
202+
// Skip fake `mouseenter` events dispatched by touch devices.
203+
this._inputModalityDetector.mostRecentModality !== 'touch' &&
204+
!this.menuStack.isEmpty() &&
205+
!this.isOpen()
206+
);
207+
}),
199208
takeUntil(this.destroyed),
200209
)
201210
.subscribe(() => {

0 commit comments

Comments
 (0)