Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions src/cdk/menu/menu-item-checkbox.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,21 @@ import {ComponentFixture, TestBed} from '@angular/core/testing';
import {By} from '@angular/platform-browser';
import {CdkMenuModule} from './menu-module';
import {CdkMenuItemCheckbox} from './menu-item-checkbox';
import {MENU_STACK, MenuStack} from './menu-stack';
import {ENTER, SPACE} from '../keycodes';

/**
* Simulates what the browser does when a native button is activated from the keyboard: a trusted
* keydown that the item lets through, followed by a click with a `detail` of 0.
*/
function pressKeyOnNativeButton(item: CdkMenuItemCheckbox, element: HTMLElement, keyCode: number) {
item._onKeydown({
keyCode,
isTrusted: true,
preventDefault: () => {},
} as unknown as KeyboardEvent);
element.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 0}));
}

describe('MenuItemCheckbox', () => {
let fixture: ComponentFixture<SingleCheckboxButton>;
Expand Down Expand Up @@ -72,6 +87,42 @@ describe('MenuItemCheckbox', () => {
expect(spy).toHaveBeenCalledTimes(1);
});

describe('keyboard activation of a native button', () => {
let menuStack: MenuStack;

beforeEach(() => {
menuStack = fixture.debugElement
.query(By.directive(CdkMenuItemCheckbox))
.injector.get(MENU_STACK);
spyOn(menuStack, 'closeAll');
});

it('should keep the menu open when toggled with space', () => {
pressKeyOnNativeButton(checkbox, checkboxElement, SPACE);

expect(checkbox.checked).toBeTrue();
expect(menuStack.closeAll).not.toHaveBeenCalled();
});

it('should close the menu when toggled with enter', () => {
pressKeyOnNativeButton(checkbox, checkboxElement, ENTER);

expect(checkbox.checked).toBeTrue();
expect(menuStack.closeAll).toHaveBeenCalled();
});

it('should close the menu on a mouse click after the space key was pressed', () => {
checkbox._onKeydown({
keyCode: SPACE,
isTrusted: true,
preventDefault: () => {},
} as unknown as KeyboardEvent);
checkboxElement.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1}));

expect(menuStack.closeAll).toHaveBeenCalled();
});
});

it('should not emit on clicked emitter when disabled', () => {
const spy = jasmine.createSpy('cdkMenuItemCheckbox clicked spy');
checkbox.triggered.subscribe(spy);
Expand Down
20 changes: 20 additions & 0 deletions src/cdk/menu/menu-item-radio.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import {ComponentFixture, TestBed} from '@angular/core/testing';
import {By} from '@angular/platform-browser';
import {CdkMenuModule} from './menu-module';
import {CdkMenuItemRadio} from './menu-item-radio';
import {MENU_STACK} from './menu-stack';
import {SPACE} from '../keycodes';

describe('MenuItemRadio', () => {
let fixture: ComponentFixture<SimpleRadioButton>;
Expand Down Expand Up @@ -62,6 +64,24 @@ describe('MenuItemRadio', () => {
expect(spy).toHaveBeenCalledTimes(1);
});

it('should keep the menu open when a native button is checked with space', () => {
const menuStack = fixture.debugElement
.query(By.directive(CdkMenuItemRadio))
.injector.get(MENU_STACK);
spyOn(menuStack, 'closeAll');

// The browser turns a trusted space keydown on a button into a click with a `detail` of 0.
radioButton._onKeydown({
keyCode: SPACE,
isTrusted: true,
preventDefault: () => {},
} as unknown as KeyboardEvent);
radioElement.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 0}));

expect(radioButton.checked).toBeTrue();
expect(menuStack.closeAll).not.toHaveBeenCalled();
});

it('should not emit on clicked emitter when disabled', () => {
const spy = jasmine.createSpy('cdkMenuItemRadio clicked spy');
radioButton.triggered.subscribe(spy);
Expand Down
26 changes: 22 additions & 4 deletions src/cdk/menu/menu-item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,12 @@ export class CdkMenuItem implements FocusableOption, FocusableElement, Toggler,
/** Whether the item should close the menu if triggered by the spacebar. */
protected closeOnSpacebarTrigger = true;

/**
* Whether the click that the browser dispatches for the last keydown should keep the menu open.
* Native buttons turn space and enter into a click, so the keydown handler can't trigger them.
*/
private _keepOpenOnKeyboardClick = false;

/** Emits when the menu item is destroyed. */
protected readonly destroyed = new Subject<void>();

Expand Down Expand Up @@ -188,8 +194,12 @@ export class CdkMenuItem implements FocusableOption, FocusableElement, Toggler,
event.preventDefault();
event.stopPropagation();
} else {
this.trigger();
// Clicks dispatched by the browser for a keydown have a `detail` of 0.
const keepOpen = this._keepOpenOnKeyboardClick && event.detail === 0;
this.trigger({keepOpen});
}

this._keepOpenOnKeyboardClick = false;
}

/**
Expand All @@ -202,8 +212,16 @@ export class CdkMenuItem implements FocusableOption, FocusableElement, Toggler,
switch (event.keyCode) {
case SPACE:
case ENTER:
// Skip events that will trigger clicks so the handler doesn't get triggered twice.
if (!hasModifierKey(event) && !eventDispatchesNativeClick(this._elementRef, event)) {
if (!hasModifierKey(event)) {
const keepOpen = event.keyCode === SPACE && !this.closeOnSpacebarTrigger;

// Skip events that will trigger clicks so the handler doesn't get triggered twice.
// The click handler needs to know whether to keep the menu open though.
if (eventDispatchesNativeClick(this._elementRef, event)) {
this._keepOpenOnKeyboardClick = keepOpen;
break;
}

const nodeName = this._elementRef.nativeElement.nodeName;

// Avoid repeat events on non-native elements (see #30250). Note that we don't do this
Expand All @@ -212,7 +230,7 @@ export class CdkMenuItem implements FocusableOption, FocusableElement, Toggler,
event.preventDefault();
}

this.trigger({keepOpen: event.keyCode === SPACE && !this.closeOnSpacebarTrigger});
this.trigger({keepOpen});
}
break;

Expand Down
Loading