Skip to content

Commit 66e65c4

Browse files
crisbetokara
authored andcommitted
feat(select): add ability to cycle through options with arrow keys when closed (#3313)
Fixes #2990.
1 parent 5e7af26 commit 66e65c4

File tree

3 files changed

+101
-2
lines changed

3 files changed

+101
-2
lines changed

src/lib/core/a11y/focus-key-manager.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ export class FocusKeyManager extends ListKeyManager<Focusable> {
2323
*/
2424
setActiveItem(index: number): void {
2525
super.setActiveItem(index);
26-
this.activeItem.focus();
26+
27+
if (this.activeItem) {
28+
this.activeItem.focus();
29+
}
2730
}
2831

2932
}

src/lib/select/select.spec.ts

+76
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {MdSelect, MdSelectFloatPlaceholderType} from './select';
1616
import {MdSelectDynamicMultipleError, MdSelectNonArrayValueError} from './select-errors';
1717
import {MdOption} from '../core/option/option';
1818
import {Dir} from '../core/rtl/dir';
19+
import {DOWN_ARROW, UP_ARROW} from '../core/keyboard/keycodes';
1920
import {
2021
ControlValueAccessor, FormControl, FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModule
2122
} from '@angular/forms';
@@ -1342,6 +1343,81 @@ describe('MdSelect', () => {
13421343
expect(select.getAttribute('tabindex')).toEqual('0');
13431344
});
13441345

1346+
it('should be able to select options via the arrow keys on a closed select', () => {
1347+
const formControl = fixture.componentInstance.control;
1348+
const options = fixture.componentInstance.options.toArray();
1349+
1350+
expect(formControl.value).toBeFalsy('Expected no initial value.');
1351+
1352+
dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW);
1353+
1354+
expect(options[0].selected).toBe(true, 'Expected first option to be selected.');
1355+
expect(formControl.value).toBe(options[0].value,
1356+
'Expected value from first option to have been set on the model.');
1357+
1358+
dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW);
1359+
dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW);
1360+
1361+
// Note that the third option is skipped, because it is disabled.
1362+
expect(options[3].selected).toBe(true, 'Expected fourth option to be selected.');
1363+
expect(formControl.value).toBe(options[3].value,
1364+
'Expected value from fourth option to have been set on the model.');
1365+
1366+
dispatchKeyboardEvent(select, 'keydown', UP_ARROW);
1367+
1368+
expect(options[1].selected).toBe(true, 'Expected second option to be selected.');
1369+
expect(formControl.value).toBe(options[1].value,
1370+
'Expected value from second option to have been set on the model.');
1371+
});
1372+
1373+
it('should do nothing if the key manager did not change the active item', () => {
1374+
const formControl = fixture.componentInstance.control;
1375+
1376+
expect(formControl.value).toBeNull('Expected form control value to be empty.');
1377+
expect(formControl.pristine).toBe(true, 'Expected form control to be clean.');
1378+
1379+
dispatchKeyboardEvent(select, 'keydown', 16); // Press a random key.
1380+
1381+
expect(formControl.value).toBeNull('Expected form control value to stay empty.');
1382+
expect(formControl.pristine).toBe(true, 'Expected form control to stay clean.');
1383+
});
1384+
1385+
it('should continue from the selected option when the value is set programmatically', () => {
1386+
const formControl = fixture.componentInstance.control;
1387+
1388+
formControl.setValue('eggs-5');
1389+
fixture.detectChanges();
1390+
1391+
dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW);
1392+
1393+
expect(formControl.value).toBe('pasta-6');
1394+
expect(fixture.componentInstance.options.toArray()[6].selected).toBe(true);
1395+
});
1396+
1397+
it('should not cycle through the options if the control is disabled', () => {
1398+
const formControl = fixture.componentInstance.control;
1399+
1400+
formControl.setValue('eggs-5');
1401+
formControl.disable();
1402+
dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW);
1403+
1404+
expect(formControl.value).toBe('eggs-5', 'Expected value to remain unchaged.');
1405+
});
1406+
1407+
it('should not wrap selection around after reaching the end of the options', () => {
1408+
const lastOption = fixture.componentInstance.options.last;
1409+
1410+
fixture.componentInstance.options.forEach(() => {
1411+
dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW);
1412+
});
1413+
1414+
expect(lastOption.selected).toBe(true, 'Expected last option to be selected.');
1415+
1416+
dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW);
1417+
1418+
expect(lastOption.selected).toBe(true, 'Expected last option to stay selected.');
1419+
});
1420+
13451421
});
13461422

13471423
describe('for options', () => {

src/lib/select/select.ts

+21-1
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,24 @@ export class MdSelect implements AfterContentInit, OnDestroy, OnInit, ControlVal
484484
_handleKeydown(event: KeyboardEvent): void {
485485
if (event.keyCode === ENTER || event.keyCode === SPACE) {
486486
this.open();
487+
} else if (!this.disabled) {
488+
let prevActiveItem = this._keyManager.activeItem;
489+
490+
// Cycle though the select options even when the select is closed,
491+
// matching the behavior of the native select element.
492+
// TODO(crisbeto): native selects also cycle through the options with left/right arrows,
493+
// however the key manager only supports up/down at the moment.
494+
this._keyManager.onKeydown(event);
495+
496+
let currentActiveItem = this._keyManager.activeItem as MdOption;
497+
498+
if (this._multiple) {
499+
this.open();
500+
} else if (currentActiveItem !== prevActiveItem) {
501+
this._clearSelection();
502+
this._setSelectionByValue(currentActiveItem.value);
503+
this._propagateChanges();
504+
}
487505
}
488506
}
489507

@@ -572,11 +590,13 @@ export class MdSelect implements AfterContentInit, OnDestroy, OnInit, ControlVal
572590
* @returns Option that has the corresponding value.
573591
*/
574592
private _selectValue(value: any): MdOption {
575-
let correspondingOption = this.options.find(option => option.value === value);
593+
let optionsArray = this.options.toArray();
594+
let correspondingOption = optionsArray.find(option => option.value === value);
576595

577596
if (correspondingOption) {
578597
correspondingOption.select();
579598
this._selectionModel.select(correspondingOption);
599+
this._keyManager.setActiveItem(optionsArray.indexOf(correspondingOption));
580600
}
581601

582602
return correspondingOption;

0 commit comments

Comments
 (0)