Skip to content

fix(select): don't prevent enter and space keys if a modifier is pressed #14090

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
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
1 change: 1 addition & 0 deletions src/lib/core/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ ng_test_library(
"@angular//packages/platform-browser/animations",
"//src/cdk/platform",
"//src/cdk/testing",
"//src/cdk/keycodes",
"//src/lib/core",
]
)
Expand Down
67 changes: 66 additions & 1 deletion src/lib/core/option/option.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {Component, DebugElement} from '@angular/core';
import {By} from '@angular/platform-browser';
import {dispatchFakeEvent} from '@angular/cdk/testing';
import {
dispatchFakeEvent,
dispatchKeyboardEvent,
createKeyboardEvent,
dispatchEvent,
} from '@angular/cdk/testing';
import {SPACE, ENTER} from '@angular/cdk/keycodes';
import {MatOption, MatOptionModule} from './index';

describe('MatOption component', () => {
Expand Down Expand Up @@ -82,6 +88,65 @@ describe('MatOption component', () => {
expect(optionInstance.id).toBe('custom-option');
});

it('should select the option when pressing space', () => {
const fixture = TestBed.createComponent(BasicOption);
fixture.detectChanges();

const optionDebugElement = fixture.debugElement.query(By.directive(MatOption));
const optionNativeElement: HTMLElement = optionDebugElement.nativeElement;
const optionInstance: MatOption = optionDebugElement.componentInstance;
const spy = jasmine.createSpy('selection change spy');
const subscription = optionInstance.onSelectionChange.subscribe(spy);

const event = dispatchKeyboardEvent(optionNativeElement, 'keydown', SPACE);
fixture.detectChanges();

expect(spy).toHaveBeenCalled();
expect(event.defaultPrevented).toBe(true);
subscription.unsubscribe();
});

it('should select the option when pressing enter', () => {
const fixture = TestBed.createComponent(BasicOption);
fixture.detectChanges();

const optionDebugElement = fixture.debugElement.query(By.directive(MatOption));
const optionNativeElement: HTMLElement = optionDebugElement.nativeElement;
const optionInstance: MatOption = optionDebugElement.componentInstance;
const spy = jasmine.createSpy('selection change spy');
const subscription = optionInstance.onSelectionChange.subscribe(spy);

const event = dispatchKeyboardEvent(optionNativeElement, 'keydown', ENTER);
fixture.detectChanges();

expect(spy).toHaveBeenCalled();
expect(event.defaultPrevented).toBe(true);
subscription.unsubscribe();
});

it('should not do anything when pressing the selection keys with a modifier', () => {
const fixture = TestBed.createComponent(BasicOption);
fixture.detectChanges();

const optionDebugElement = fixture.debugElement.query(By.directive(MatOption));
const optionNativeElement: HTMLElement = optionDebugElement.nativeElement;
const optionInstance: MatOption = optionDebugElement.componentInstance;
const spy = jasmine.createSpy('selection change spy');
const subscription = optionInstance.onSelectionChange.subscribe(spy);

[ENTER, SPACE].forEach(key => {
const event = createKeyboardEvent('keydown', key);
Object.defineProperty(event, 'shiftKey', {get: () => true});
dispatchEvent(optionNativeElement, event);
fixture.detectChanges();

expect(event.defaultPrevented).toBe(false);
});

expect(spy).not.toHaveBeenCalled();
subscription.unsubscribe();
});

describe('ripples', () => {
let fixture: ComponentFixture<BasicOption>;
let optionDebugElement: DebugElement;
Expand Down
4 changes: 2 additions & 2 deletions src/lib/core/option/option.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

import {coerceBooleanProperty} from '@angular/cdk/coercion';
import {ENTER, SPACE} from '@angular/cdk/keycodes';
import {ENTER, SPACE, hasModifierKey} from '@angular/cdk/keycodes';
import {
AfterViewChecked,
ChangeDetectionStrategy,
Expand Down Expand Up @@ -200,7 +200,7 @@ export class MatOption implements AfterViewChecked, OnDestroy {

/** Ensures the option is selected when activated from the keyboard. */
_handleKeydown(event: KeyboardEvent): void {
if (event.keyCode === ENTER || event.keyCode === SPACE) {
if ((event.keyCode === ENTER || event.keyCode === SPACE) && !hasModifierKey(event)) {
this._selectViaInteraction();

// Prevent the page from scrolling down and form submits.
Expand Down
35 changes: 15 additions & 20 deletions src/lib/select/select.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -669,6 +669,21 @@ describe('MatSelect', () => {
expect(event.defaultPrevented).toBe(true);
}));

it('should prevent the default action when pressing enter', fakeAsync(() => {
const event = dispatchKeyboardEvent(select, 'keydown', ENTER);
expect(event.defaultPrevented).toBe(true);
}));

it('should not prevent the default actions on selection keys when pressing a modifier',
fakeAsync(() => {
[ENTER, SPACE].forEach(key => {
const event = createKeyboardEvent('keydown', key);
Object.defineProperty(event, 'shiftKey', {get: () => true});
expect(event.defaultPrevented).toBe(false);
});

}));

it('should consider the selection a result of a user action when closed', fakeAsync(() => {
const option = fixture.componentInstance.options.first;
const spy = jasmine.createSpy('option selection spy');
Expand Down Expand Up @@ -1067,26 +1082,6 @@ describe('MatSelect', () => {
expect(panel.classList).toContain('custom-two');
}));

it('should prevent the default action when pressing SPACE on an option', fakeAsync(() => {
trigger.click();
fixture.detectChanges();

const option = overlayContainerElement.querySelector('mat-option')!;
const event = dispatchKeyboardEvent(option, 'keydown', SPACE);

expect(event.defaultPrevented).toBe(true);
}));

it('should prevent the default action when pressing ENTER on an option', fakeAsync(() => {
trigger.click();
fixture.detectChanges();

const option = overlayContainerElement.querySelector('mat-option')!;
const event = dispatchKeyboardEvent(option, 'keydown', ENTER);

expect(event.defaultPrevented).toBe(true);
}));

it('should update disableRipple properly on each option', fakeAsync(() => {
const options = fixture.componentInstance.options.toArray();

Expand Down
6 changes: 4 additions & 2 deletions src/lib/select/select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
RIGHT_ARROW,
SPACE,
UP_ARROW,
hasModifierKey,
} from '@angular/cdk/keycodes';
import {CdkConnectedOverlay, Overlay, ScrollStrategy} from '@angular/cdk/overlay';
import {ViewportRuler} from '@angular/cdk/scrolling';
Expand Down Expand Up @@ -695,7 +696,7 @@ export class MatSelect extends _MatSelectMixinBase implements AfterContentInit,
const manager = this._keyManager;

// Open the select on ALT + arrow key to match the native <select>
if (isOpenKey || ((this.multiple || event.altKey) && isArrowKey)) {
if ((isOpenKey && !hasModifierKey(event)) || ((this.multiple || event.altKey) && isArrowKey)) {
event.preventDefault(); // prevents the page from scrolling down when pressing space
this.open();
} else if (!this.multiple) {
Expand All @@ -721,7 +722,8 @@ export class MatSelect extends _MatSelectMixinBase implements AfterContentInit,
// Close the select on ALT + arrow key to match the native <select>
event.preventDefault();
this.close();
} else if ((keyCode === ENTER || keyCode === SPACE) && manager.activeItem) {
} else if ((keyCode === ENTER || keyCode === SPACE) && manager.activeItem &&
!hasModifierKey(event)) {
event.preventDefault();
manager.activeItem._selectViaInteraction();
} else if (this._multiple && keyCode === A && event.ctrlKey) {
Expand Down