Skip to content

Commit

Permalink
fix(select): support changing the value using left/right arrow keys w…
Browse files Browse the repository at this point in the history
…hile closed (#9578)

Based on the native `<select>`, adds the ability for users to change the value on a closed select using the left/right arrow keys.
  • Loading branch information
crisbeto authored and jelbourn committed Jan 29, 2018
1 parent 8319a57 commit b11523a
Show file tree
Hide file tree
Showing 2 changed files with 114 additions and 20 deletions.
110 changes: 94 additions & 16 deletions src/lib/select/select.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
import {Directionality} from '@angular/cdk/bidi';
import {DOWN_ARROW, END, ENTER, HOME, SPACE, TAB, UP_ARROW} from '@angular/cdk/keycodes';
import {
DOWN_ARROW,
END,
ENTER,
HOME,
SPACE,
TAB,
UP_ARROW,
LEFT_ARROW,
RIGHT_ARROW,
} from '@angular/cdk/keycodes';
import {OverlayContainer} from '@angular/cdk/overlay';
import {Platform} from '@angular/cdk/platform';
import {ScrollDispatcher, ViewportRuler} from '@angular/cdk/scrolling';
Expand Down Expand Up @@ -219,7 +229,7 @@ describe('MatSelect', () => {
expect(select.getAttribute('tabindex')).toEqual('0');
}));

it('should select options via the arrow keys on a closed select', fakeAsync(() => {
it('should select options via the UP/DOWN arrow keys on a closed select', fakeAsync(() => {
const formControl = fixture.componentInstance.control;
const options = fixture.componentInstance.options.toArray();

Expand All @@ -246,6 +256,33 @@ describe('MatSelect', () => {
'Expected value from second option to have been set on the model.');
}));

it('should select options via LEFT/RIGHT arrow keys on a closed select', fakeAsync(() => {
const formControl = fixture.componentInstance.control;
const options = fixture.componentInstance.options.toArray();

expect(formControl.value).toBeFalsy('Expected no initial value.');

dispatchKeyboardEvent(select, 'keydown', RIGHT_ARROW);

expect(options[0].selected).toBe(true, 'Expected first option to be selected.');
expect(formControl.value).toBe(options[0].value,
'Expected value from first option to have been set on the model.');

dispatchKeyboardEvent(select, 'keydown', RIGHT_ARROW);
dispatchKeyboardEvent(select, 'keydown', RIGHT_ARROW);

// Note that the third option is skipped, because it is disabled.
expect(options[3].selected).toBe(true, 'Expected fourth option to be selected.');
expect(formControl.value).toBe(options[3].value,
'Expected value from fourth option to have been set on the model.');

dispatchKeyboardEvent(select, 'keydown', LEFT_ARROW);

expect(options[1].selected).toBe(true, 'Expected second option to be selected.');
expect(formControl.value).toBe(options[1].value,
'Expected value from second option to have been set on the model.');
}));

it('should open a single-selection select using ALT + DOWN_ARROW', fakeAsync(() => {
const {control: formControl, select: selectInstance} = fixture.componentInstance;

Expand Down Expand Up @@ -331,26 +368,47 @@ describe('MatSelect', () => {
'Expected value from sixth option to have been set on the model.');
}));

it('should open the panel when pressing the arrow keys on a closed multiple select',
fakeAsync(() => {
fixture.destroy();
it('should open the panel when pressing a vertical arrow key on a closed multiple select',
fakeAsync(() => {
fixture.destroy();

const multiFixture = TestBed.createComponent(MultiSelect);
const instance = multiFixture.componentInstance;
const multiFixture = TestBed.createComponent(MultiSelect);
const instance = multiFixture.componentInstance;

multiFixture.detectChanges();
select = multiFixture.debugElement.query(By.css('mat-select')).nativeElement;
multiFixture.detectChanges();
select = multiFixture.debugElement.query(By.css('mat-select')).nativeElement;

const initialValue = instance.control.value;
const initialValue = instance.control.value;

expect(instance.select.panelOpen).toBe(false, 'Expected panel to be closed.');
expect(instance.select.panelOpen).toBe(false, 'Expected panel to be closed.');

const event = dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW);
const event = dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW);

expect(instance.select.panelOpen).toBe(true, 'Expected panel to be open.');
expect(instance.control.value).toBe(initialValue, 'Expected value to stay the same.');
expect(event.defaultPrevented).toBe(true, 'Expected default to be prevented.');
}));
expect(instance.select.panelOpen).toBe(true, 'Expected panel to be open.');
expect(instance.control.value).toBe(initialValue, 'Expected value to stay the same.');
expect(event.defaultPrevented).toBe(true, 'Expected default to be prevented.');
}));

it('should open the panel when pressing a horizontal arrow key on closed multiple select',
fakeAsync(() => {
fixture.destroy();

const multiFixture = TestBed.createComponent(MultiSelect);
const instance = multiFixture.componentInstance;

multiFixture.detectChanges();
select = multiFixture.debugElement.query(By.css('mat-select')).nativeElement;

const initialValue = instance.control.value;

expect(instance.select.panelOpen).toBe(false, 'Expected panel to be closed.');

const event = dispatchKeyboardEvent(select, 'keydown', RIGHT_ARROW);

expect(instance.select.panelOpen).toBe(true, 'Expected panel to be open.');
expect(instance.control.value).toBe(initialValue, 'Expected value to stay the same.');
expect(event.defaultPrevented).toBe(true, 'Expected default to be prevented.');
}));

it('should do nothing when typing on a closed multi-select', fakeAsync(() => {
fixture.destroy();
Expand Down Expand Up @@ -623,6 +681,26 @@ describe('MatSelect', () => {
expect(host.getAttribute('aria-activedescendant')).toBe(options[3].id);
}));

it('should not change the aria-activedescendant using the horizontal arrow keys',
fakeAsync(() => {
const host = fixture.debugElement.query(By.css('mat-select')).nativeElement;

fixture.componentInstance.select.open();
fixture.detectChanges();
flush();

const options = overlayContainerElement.querySelectorAll('mat-option');

expect(host.getAttribute('aria-activedescendant')).toBe(options[0].id);

[1, 2, 3].forEach(() => {
dispatchKeyboardEvent(host, 'keydown', RIGHT_ARROW);
fixture.detectChanges();
});

expect(host.getAttribute('aria-activedescendant')).toBe(options[0].id);
}));

});

describe('for options', () => {
Expand Down
24 changes: 20 additions & 4 deletions src/lib/select/select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,16 @@ import {ActiveDescendantKeyManager} from '@angular/cdk/a11y';
import {Directionality} from '@angular/cdk/bidi';
import {coerceBooleanProperty} from '@angular/cdk/coercion';
import {SelectionModel} from '@angular/cdk/collections';
import {DOWN_ARROW, END, ENTER, HOME, SPACE, UP_ARROW} from '@angular/cdk/keycodes';
import {
DOWN_ARROW,
END,
ENTER,
HOME,
SPACE,
UP_ARROW,
LEFT_ARROW,
RIGHT_ARROW,
} from '@angular/cdk/keycodes';
import {
CdkConnectedOverlay,
Overlay,
Expand Down Expand Up @@ -531,6 +540,7 @@ export class MatSelect extends _MatSelectMixinBase implements AfterContentInit,
this._triggerFontSize = parseInt(getComputedStyle(this.trigger.nativeElement)['font-size']);

this._panelOpen = true;
this._keyManager.withHorizontalOrientation(null);
this._calculateOverlayPosition();
this._highlightCorrectOption();
this._changeDetectorRef.markForCheck();
Expand All @@ -548,6 +558,7 @@ export class MatSelect extends _MatSelectMixinBase implements AfterContentInit,
close(): void {
if (this._panelOpen) {
this._panelOpen = false;
this._keyManager.withHorizontalOrientation(this._isRtl() ? 'rtl' : 'ltr');
this._changeDetectorRef.markForCheck();
this._onTouched();
}
Expand Down Expand Up @@ -644,7 +655,8 @@ export class MatSelect extends _MatSelectMixinBase implements AfterContentInit,
/** Handles keyboard events while the select is closed. */
private _handleClosedKeydown(event: KeyboardEvent): void {
const keyCode = event.keyCode;
const isArrowKey = keyCode === DOWN_ARROW || keyCode === UP_ARROW;
const isArrowKey = keyCode === DOWN_ARROW || keyCode === UP_ARROW ||
keyCode === LEFT_ARROW || keyCode === RIGHT_ARROW;
const isOpenKey = keyCode === ENTER || keyCode === SPACE;

// Open the select on ALT + arrow key to match the native <select>
Expand Down Expand Up @@ -831,8 +843,12 @@ export class MatSelect extends _MatSelectMixinBase implements AfterContentInit,

/** Sets up a key manager to listen to keyboard events on the overlay panel. */
private _initKeyManager() {
this._keyManager = new ActiveDescendantKeyManager<MatOption>(this.options).withTypeAhead();
this._keyManager.tabOut.pipe(takeUntil(this._destroy)).subscribe(() => this.close());
this._keyManager = new ActiveDescendantKeyManager<MatOption>(this.options)
.withTypeAhead()
.withVerticalOrientation()
.withHorizontalOrientation(this._isRtl() ? 'rtl' : 'ltr');

this._keyManager.tabOut.pipe(takeUntil(this._destroy)).subscribe(() => this.close());
this._keyManager.change.pipe(takeUntil(this._destroy)).subscribe(() => {
if (this._panelOpen && this.panel) {
this._scrollActiveOptionIntoView();
Expand Down

0 comments on commit b11523a

Please sign in to comment.