Skip to content

Commit 1307aaf

Browse files
crisbetojosephperrott
authored andcommitted
fix(select): don't prevent enter and space keys if a modifier is pressed (#14090)
* Only handles the space and enter keys, and prevents their default actions, if none of the modifier keys are pressed. * Moves some tests that were testing `mat-option` logic into the `mat-option` specs.
1 parent f229f9e commit 1307aaf

File tree

5 files changed

+88
-25
lines changed

5 files changed

+88
-25
lines changed

src/lib/core/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ ng_test_library(
118118
"@angular//packages/platform-browser/animations",
119119
"//src/cdk/platform",
120120
"//src/cdk/testing",
121+
"//src/cdk/keycodes",
121122
"//src/lib/core",
122123
]
123124
)

src/lib/core/option/option.spec.ts

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
22
import {Component, DebugElement} from '@angular/core';
33
import {By} from '@angular/platform-browser';
4-
import {dispatchFakeEvent} from '@angular/cdk/testing';
4+
import {
5+
dispatchFakeEvent,
6+
dispatchKeyboardEvent,
7+
createKeyboardEvent,
8+
dispatchEvent,
9+
} from '@angular/cdk/testing';
10+
import {SPACE, ENTER} from '@angular/cdk/keycodes';
511
import {MatOption, MatOptionModule} from './index';
612

713
describe('MatOption component', () => {
@@ -82,6 +88,65 @@ describe('MatOption component', () => {
8288
expect(optionInstance.id).toBe('custom-option');
8389
});
8490

91+
it('should select the option when pressing space', () => {
92+
const fixture = TestBed.createComponent(BasicOption);
93+
fixture.detectChanges();
94+
95+
const optionDebugElement = fixture.debugElement.query(By.directive(MatOption));
96+
const optionNativeElement: HTMLElement = optionDebugElement.nativeElement;
97+
const optionInstance: MatOption = optionDebugElement.componentInstance;
98+
const spy = jasmine.createSpy('selection change spy');
99+
const subscription = optionInstance.onSelectionChange.subscribe(spy);
100+
101+
const event = dispatchKeyboardEvent(optionNativeElement, 'keydown', SPACE);
102+
fixture.detectChanges();
103+
104+
expect(spy).toHaveBeenCalled();
105+
expect(event.defaultPrevented).toBe(true);
106+
subscription.unsubscribe();
107+
});
108+
109+
it('should select the option when pressing enter', () => {
110+
const fixture = TestBed.createComponent(BasicOption);
111+
fixture.detectChanges();
112+
113+
const optionDebugElement = fixture.debugElement.query(By.directive(MatOption));
114+
const optionNativeElement: HTMLElement = optionDebugElement.nativeElement;
115+
const optionInstance: MatOption = optionDebugElement.componentInstance;
116+
const spy = jasmine.createSpy('selection change spy');
117+
const subscription = optionInstance.onSelectionChange.subscribe(spy);
118+
119+
const event = dispatchKeyboardEvent(optionNativeElement, 'keydown', ENTER);
120+
fixture.detectChanges();
121+
122+
expect(spy).toHaveBeenCalled();
123+
expect(event.defaultPrevented).toBe(true);
124+
subscription.unsubscribe();
125+
});
126+
127+
it('should not do anything when pressing the selection keys with a modifier', () => {
128+
const fixture = TestBed.createComponent(BasicOption);
129+
fixture.detectChanges();
130+
131+
const optionDebugElement = fixture.debugElement.query(By.directive(MatOption));
132+
const optionNativeElement: HTMLElement = optionDebugElement.nativeElement;
133+
const optionInstance: MatOption = optionDebugElement.componentInstance;
134+
const spy = jasmine.createSpy('selection change spy');
135+
const subscription = optionInstance.onSelectionChange.subscribe(spy);
136+
137+
[ENTER, SPACE].forEach(key => {
138+
const event = createKeyboardEvent('keydown', key);
139+
Object.defineProperty(event, 'shiftKey', {get: () => true});
140+
dispatchEvent(optionNativeElement, event);
141+
fixture.detectChanges();
142+
143+
expect(event.defaultPrevented).toBe(false);
144+
});
145+
146+
expect(spy).not.toHaveBeenCalled();
147+
subscription.unsubscribe();
148+
});
149+
85150
describe('ripples', () => {
86151
let fixture: ComponentFixture<BasicOption>;
87152
let optionDebugElement: DebugElement;

src/lib/core/option/option.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import {coerceBooleanProperty} from '@angular/cdk/coercion';
10-
import {ENTER, SPACE} from '@angular/cdk/keycodes';
10+
import {ENTER, SPACE, hasModifierKey} from '@angular/cdk/keycodes';
1111
import {
1212
AfterViewChecked,
1313
ChangeDetectionStrategy,
@@ -200,7 +200,7 @@ export class MatOption implements AfterViewChecked, OnDestroy {
200200

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

206206
// Prevent the page from scrolling down and form submits.

src/lib/select/select.spec.ts

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -669,6 +669,21 @@ describe('MatSelect', () => {
669669
expect(event.defaultPrevented).toBe(true);
670670
}));
671671

672+
it('should prevent the default action when pressing enter', fakeAsync(() => {
673+
const event = dispatchKeyboardEvent(select, 'keydown', ENTER);
674+
expect(event.defaultPrevented).toBe(true);
675+
}));
676+
677+
it('should not prevent the default actions on selection keys when pressing a modifier',
678+
fakeAsync(() => {
679+
[ENTER, SPACE].forEach(key => {
680+
const event = createKeyboardEvent('keydown', key);
681+
Object.defineProperty(event, 'shiftKey', {get: () => true});
682+
expect(event.defaultPrevented).toBe(false);
683+
});
684+
685+
}));
686+
672687
it('should consider the selection a result of a user action when closed', fakeAsync(() => {
673688
const option = fixture.componentInstance.options.first;
674689
const spy = jasmine.createSpy('option selection spy');
@@ -1067,26 +1082,6 @@ describe('MatSelect', () => {
10671082
expect(panel.classList).toContain('custom-two');
10681083
}));
10691084

1070-
it('should prevent the default action when pressing SPACE on an option', fakeAsync(() => {
1071-
trigger.click();
1072-
fixture.detectChanges();
1073-
1074-
const option = overlayContainerElement.querySelector('mat-option')!;
1075-
const event = dispatchKeyboardEvent(option, 'keydown', SPACE);
1076-
1077-
expect(event.defaultPrevented).toBe(true);
1078-
}));
1079-
1080-
it('should prevent the default action when pressing ENTER on an option', fakeAsync(() => {
1081-
trigger.click();
1082-
fixture.detectChanges();
1083-
1084-
const option = overlayContainerElement.querySelector('mat-option')!;
1085-
const event = dispatchKeyboardEvent(option, 'keydown', ENTER);
1086-
1087-
expect(event.defaultPrevented).toBe(true);
1088-
}));
1089-
10901085
it('should update disableRipple properly on each option', fakeAsync(() => {
10911086
const options = fixture.componentInstance.options.toArray();
10921087

src/lib/select/select.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
RIGHT_ARROW,
2121
SPACE,
2222
UP_ARROW,
23+
hasModifierKey,
2324
} from '@angular/cdk/keycodes';
2425
import {CdkConnectedOverlay, Overlay, ScrollStrategy} from '@angular/cdk/overlay';
2526
import {ViewportRuler} from '@angular/cdk/scrolling';
@@ -695,7 +696,7 @@ export class MatSelect extends _MatSelectMixinBase implements AfterContentInit,
695696
const manager = this._keyManager;
696697

697698
// Open the select on ALT + arrow key to match the native <select>
698-
if (isOpenKey || ((this.multiple || event.altKey) && isArrowKey)) {
699+
if ((isOpenKey && !hasModifierKey(event)) || ((this.multiple || event.altKey) && isArrowKey)) {
699700
event.preventDefault(); // prevents the page from scrolling down when pressing space
700701
this.open();
701702
} else if (!this.multiple) {
@@ -721,7 +722,8 @@ export class MatSelect extends _MatSelectMixinBase implements AfterContentInit,
721722
// Close the select on ALT + arrow key to match the native <select>
722723
event.preventDefault();
723724
this.close();
724-
} else if ((keyCode === ENTER || keyCode === SPACE) && manager.activeItem) {
725+
} else if ((keyCode === ENTER || keyCode === SPACE) && manager.activeItem &&
726+
!hasModifierKey(event)) {
725727
event.preventDefault();
726728
manager.activeItem._selectViaInteraction();
727729
} else if (this._multiple && keyCode === A && event.ctrlKey) {

0 commit comments

Comments
 (0)