diff --git a/src/cdk/menu/menu-item.spec.ts b/src/cdk/menu/menu-item.spec.ts index 9d40c2cd9aa4..a07a127c6ffc 100644 --- a/src/cdk/menu/menu-item.spec.ts +++ b/src/cdk/menu/menu-item.spec.ts @@ -67,10 +67,10 @@ describe('MenuItem', () => { expect(menuItem.hasMenu).toBeFalse(); }); - it('should prevent the default selection key action', () => { + it('should not prevent the default selection key action', () => { const event = dispatchKeyboardEvent(nativeButton, 'keydown', ENTER); fixture.detectChanges(); - expect(event.defaultPrevented).toBe(true); + expect(event.defaultPrevented).toBe(false); }); }); diff --git a/src/cdk/menu/menu-item.ts b/src/cdk/menu/menu-item.ts index 5f04075d8e37..a23fb46cb893 100644 --- a/src/cdk/menu/menu-item.ts +++ b/src/cdk/menu/menu-item.ts @@ -17,7 +17,7 @@ import { Output, } from '@angular/core'; import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion'; -import {FocusableOption} from '@angular/cdk/a11y'; +import {FocusableOption, InputModalityDetector} from '@angular/cdk/a11y'; import {ENTER, hasModifierKey, LEFT_ARROW, RIGHT_ARROW, SPACE} from '@angular/cdk/keycodes'; import {Directionality} from '@angular/cdk/bidi'; import {fromEvent, Subject} from 'rxjs'; @@ -44,18 +44,14 @@ import {MENU_AIM, Toggler} from './menu-aim'; '[attr.aria-disabled]': 'disabled || null', '(blur)': '_resetTabIndex()', '(focus)': '_setTabIndex()', - '(click)': 'trigger()', + '(click)': '_handleClick()', '(keydown)': '_onKeydown($event)', }, }) export class CdkMenuItem implements FocusableOption, FocusableElement, Toggler, OnDestroy { - /** The directionality (text direction) of the current page. */ protected readonly _dir = inject(Directionality, {optional: true}); - - /** The menu's native DOM host element. */ + private readonly _inputModalityDetector = inject(InputModalityDetector); readonly _elementRef: ElementRef = inject(ElementRef); - - /** The Angular zone. */ protected _ngZone = inject(NgZone); /** The menu aim service used by this menu. */ @@ -200,7 +196,6 @@ export class CdkMenuItem implements FocusableOption, FocusableElement, Toggler, case SPACE: case ENTER: if (!hasModifierKey(event)) { - event.preventDefault(); this.trigger({keepOpen: event.keyCode === SPACE && !this.closeOnSpacebarTrigger}); } break; @@ -231,6 +226,15 @@ export class CdkMenuItem implements FocusableOption, FocusableElement, Toggler, } } + /** Handles clicks on the menu item. */ + _handleClick() { + // Don't handle clicks originating from the keyboard since we + // already do the same on `keydown` events for enter and space. + if (this._inputModalityDetector.mostRecentModality !== 'keyboard') { + this.trigger(); + } + } + /** Whether this menu item is standalone or within a menu or menu bar. */ private _isStandaloneItem() { return !this._parentMenu; diff --git a/src/cdk/menu/menu-trigger.spec.ts b/src/cdk/menu/menu-trigger.spec.ts index 66e923d3e8ad..e2959c3391b0 100644 --- a/src/cdk/menu/menu-trigger.spec.ts +++ b/src/cdk/menu/menu-trigger.spec.ts @@ -438,7 +438,7 @@ describe('MenuTrigger', () => { expect(nativeMenus.length).toBe(2); }); - it('should toggle the menu on trigger', () => { + it('should toggle the menu on clicks', () => { nativeTrigger.click(); detectChanges(); expect(nativeMenus.length).toBe(2); @@ -451,13 +451,13 @@ describe('MenuTrigger', () => { it('should toggle the menu on keyboard events', () => { const firstEvent = dispatchKeyboardEvent(nativeTrigger, 'keydown', ENTER); detectChanges(); - expect(firstEvent.defaultPrevented).toBe(true); + expect(firstEvent.defaultPrevented).toBe(false); expect(nativeMenus.length).toBe(2); const secondEvent = dispatchKeyboardEvent(nativeTrigger, 'keydown', ENTER); detectChanges(); expect(nativeMenus.length).toBe(1); - expect(secondEvent.defaultPrevented).toBe(true); + expect(secondEvent.defaultPrevented).toBe(false); }); it('should close the open menu on background click', () => { diff --git a/src/cdk/menu/menu-trigger.ts b/src/cdk/menu/menu-trigger.ts index f00272cbe0b1..c4fdade15994 100644 --- a/src/cdk/menu/menu-trigger.ts +++ b/src/cdk/menu/menu-trigger.ts @@ -26,6 +26,7 @@ import { UP_ARROW, } from '@angular/cdk/keycodes'; import {_getEventTarget} from '@angular/cdk/platform'; +import {InputModalityDetector} from '@angular/cdk/a11y'; import {fromEvent} from 'rxjs'; import {filter, takeUntil} from 'rxjs/operators'; import {CDK_MENU, Menu} from './menu-interface'; @@ -51,7 +52,7 @@ import {CdkMenuTriggerBase, MENU_TRIGGER} from './menu-trigger-base'; '(focusin)': '_setHasFocus(true)', '(focusout)': '_setHasFocus(false)', '(keydown)': '_toggleOnKeydown($event)', - '(click)': 'toggle()', + '(click)': '_handleClick()', }, inputs: [ 'menuTemplateRef: cdkMenuTriggerFor', @@ -65,14 +66,11 @@ import {CdkMenuTriggerBase, MENU_TRIGGER} from './menu-trigger-base'; ], }) export class CdkMenuTrigger extends CdkMenuTriggerBase implements OnDestroy { - /** The host element. */ private readonly _elementRef: ElementRef = inject(ElementRef); - - /** The CDK overlay service. */ private readonly _overlay = inject(Overlay); - - /** The Angular zone. */ private readonly _ngZone = inject(NgZone); + private readonly _directionality = inject(Directionality, {optional: true}); + private readonly _inputModalityDetector = inject(InputModalityDetector); /** The parent menu this trigger belongs to. */ private readonly _parentMenu = inject(CDK_MENU, {optional: true}); @@ -80,9 +78,6 @@ export class CdkMenuTrigger extends CdkMenuTriggerBase implements OnDestroy { /** The menu aim service used by this menu. */ private readonly _menuAim = inject(MENU_AIM, {optional: true}); - /** The directionality of the page. */ - private readonly _directionality = inject(Directionality, {optional: true}); - constructor() { super(); this._setRole(); @@ -136,7 +131,6 @@ export class CdkMenuTrigger extends CdkMenuTriggerBase implements OnDestroy { case SPACE: case ENTER: if (!hasModifierKey(event)) { - event.preventDefault(); this.toggle(); this.childMenu?.focusFirstItem('keyboard'); } @@ -177,6 +171,15 @@ export class CdkMenuTrigger extends CdkMenuTriggerBase implements OnDestroy { } } + /** Handles clicks on the menu trigger. */ + _handleClick() { + // Don't handle clicks originating from the keyboard since we + // already do the same on `keydown` events for enter and space. + if (this._inputModalityDetector.mostRecentModality !== 'keyboard') { + this.toggle(); + } + } + /** * Sets whether the trigger's menu stack has focus. * @param hasFocus Whether the menu stack has focus. diff --git a/tools/public_api_guard/cdk/menu.md b/tools/public_api_guard/cdk/menu.md index 6338441cb57a..7ae63398cfa8 100644 --- a/tools/public_api_guard/cdk/menu.md +++ b/tools/public_api_guard/cdk/menu.md @@ -119,18 +119,22 @@ export class CdkMenuItem implements FocusableOption, FocusableElement, Toggler, constructor(); protected closeOnSpacebarTrigger: boolean; protected readonly destroyed: Subject; + // (undocumented) protected readonly _dir: Directionality | null; get disabled(): boolean; set disabled(value: BooleanInput); + // (undocumented) readonly _elementRef: ElementRef; focus(): void; getLabel(): string; getMenu(): Menu | undefined; getMenuTrigger(): CdkMenuTrigger | null; + _handleClick(): void; get hasMenu(): boolean; isMenuOpen(): boolean; // (undocumented) ngOnDestroy(): void; + // (undocumented) protected _ngZone: NgZone; _onKeydown(event: KeyboardEvent): void; _resetTabIndex(): void; @@ -198,6 +202,7 @@ export class CdkMenuTrigger extends CdkMenuTriggerBase implements OnDestroy { constructor(); close(): void; getMenu(): Menu | undefined; + _handleClick(): void; open(): void; _setHasFocus(hasFocus: boolean): void; toggle(): void;