Skip to content

Commit a94d135

Browse files
committed
fix(autocomplete): up arrow should set last item active
1 parent fbed180 commit a94d135

File tree

4 files changed

+72
-25
lines changed

4 files changed

+72
-25
lines changed

src/lib/autocomplete/autocomplete-trigger.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export class MdAutocompleteTrigger implements AfterContentInit, OnDestroy {
4848
private _panelOpen: boolean = false;
4949

5050
/** The subscription to positioning changes in the autocomplete panel. */
51-
private _panelPositionSub: Subscription;
51+
private _panelPositionSubscription: Subscription;
5252

5353
/** Manages active item in option list based on key events. */
5454
private _keyManager: ActiveDescendantKeyManager;
@@ -62,12 +62,12 @@ export class MdAutocompleteTrigger implements AfterContentInit, OnDestroy {
6262
@Optional() private _controlDir: NgControl, @Optional() private _dir: Dir) {}
6363

6464
ngAfterContentInit() {
65-
this._keyManager = new ActiveDescendantKeyManager(this.autocomplete.options);
65+
this._keyManager = new ActiveDescendantKeyManager(this.autocomplete.options).withWrap();
6666
}
6767

6868
ngOnDestroy() {
69-
if (this._panelPositionSub) {
70-
this._panelPositionSub.unsubscribe();
69+
if (this._panelPositionSubscription) {
70+
this._panelPositionSubscription.unsubscribe();
7171
}
7272

7373
this._destroyPanel();
@@ -225,7 +225,7 @@ export class MdAutocompleteTrigger implements AfterContentInit, OnDestroy {
225225
* y-offset can be adjusted to match the new position.
226226
*/
227227
private _subscribeToPositionChanges(strategy: ConnectedPositionStrategy) {
228-
this._panelPositionSub = strategy.onPositionChange.subscribe(change => {
228+
this._panelPositionSubscription = strategy.onPositionChange.subscribe(change => {
229229
this.autocomplete.positionY = change.connectionPair.originY === 'top' ? 'above' : 'below';
230230
});
231231
}
@@ -235,9 +235,9 @@ export class MdAutocompleteTrigger implements AfterContentInit, OnDestroy {
235235
return this._element.nativeElement.getBoundingClientRect().width;
236236
}
237237

238-
/** Reset active item to -1 so DOWN_ARROW event will activate the first option.*/
238+
/** Reset active item to null so arrow events will activate the correct options.*/
239239
private _resetActiveItem(): void {
240-
this._keyManager.setActiveItem(-1);
240+
this._keyManager.setActiveItem(null);
241241
}
242242

243243
/**

src/lib/autocomplete/autocomplete.spec.ts

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ import {MdInputModule} from '../input/index';
77
import {Dir, LayoutDirection} from '../core/rtl/dir';
88
import {FormControl, ReactiveFormsModule} from '@angular/forms';
99
import {Subscription} from 'rxjs/Subscription';
10-
import {ENTER, DOWN_ARROW, SPACE} from '../core/keyboard/keycodes';
10+
import {ENTER, DOWN_ARROW, SPACE, UP_ARROW} from '../core/keyboard/keycodes';
1111
import {MdOption} from '../core/option/option';
1212
import {ViewportRuler} from '../core/overlay/position/viewport-ruler';
13+
import {FakeViewportRuler} from '../core/overlay/position/fake-viewport-ruler';
14+
1315

1416
describe('MdAutocomplete', () => {
1517
let overlayContainerElement: HTMLElement;
@@ -294,6 +296,36 @@ describe('MdAutocomplete', () => {
294296
});
295297
}));
296298

299+
it('should set the active item to the last option when UP key is pressed', async(() => {
300+
fixture.componentInstance.trigger.openPanel();
301+
fixture.detectChanges();
302+
303+
const optionEls =
304+
overlayContainerElement.querySelectorAll('md-option') as NodeListOf<HTMLElement>;
305+
306+
const UP_ARROW_EVENT = new FakeKeyboardEvent(UP_ARROW) as KeyboardEvent;
307+
fixture.componentInstance.trigger._handleKeydown(UP_ARROW_EVENT);
308+
309+
fixture.whenStable().then(() => {
310+
fixture.detectChanges();
311+
expect(fixture.componentInstance.trigger.activeOption)
312+
.toBe(fixture.componentInstance.options.last, 'Expected last option to be active.');
313+
expect(optionEls[10].classList).toContain('md-active');
314+
expect(optionEls[0].classList).not.toContain('md-active');
315+
316+
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
317+
318+
fixture.whenStable().then(() => {
319+
fixture.detectChanges();
320+
expect(fixture.componentInstance.trigger.activeOption)
321+
.toBe(fixture.componentInstance.options.first,
322+
'Expected first option to be active.');
323+
expect(optionEls[0].classList).toContain('md-active');
324+
expect(optionEls[10].classList).not.toContain('md-active');
325+
});
326+
});
327+
}));
328+
297329
it('should set the active item properly after filtering', async(() => {
298330
fixture.componentInstance.trigger.openPanel();
299331
fixture.detectChanges();
@@ -532,7 +564,7 @@ describe('MdAutocomplete', () => {
532564

533565
it('should fall back to above position if panel cannot fit below', () => {
534566
// Push the autocomplete trigger down so it won't have room to open "below"
535-
input.style.top = '400px';
567+
input.style.top = '600px';
536568
input.style.position = 'relative';
537569

538570
fixture.componentInstance.trigger.openPanel();
@@ -551,7 +583,7 @@ describe('MdAutocomplete', () => {
551583

552584
it('should align panel properly when filtering in "above" position', () => {
553585
// Push the autocomplete trigger down so it won't have room to open "below"
554-
input.style.top = '400px';
586+
input.style.top = '600px';
555587
input.style.position = 'relative';
556588

557589
fixture.componentInstance.trigger.openPanel();
@@ -645,16 +677,3 @@ class FakeKeyboardEvent {
645677
constructor(public keyCode: number) {}
646678
preventDefault() {}
647679
}
648-
649-
class FakeViewportRuler {
650-
getViewportRect() {
651-
return {
652-
left: 0, top: 0, width: 500, height: 500, bottom: 500, right: 500
653-
};
654-
}
655-
656-
getViewportScrollPosition() {
657-
return {top: 0, left: 0};
658-
}
659-
}
660-

src/lib/core/a11y/list-key-manager.spec.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,17 @@ describe('Key managers', () => {
8787
expect(keyManager.setActiveItem).not.toHaveBeenCalledWith(0);
8888
});
8989

90+
it('should set first item active when down arrow pressed if no active item', () => {
91+
keyManager.setActiveItem(null);
92+
keyManager.onKeydown(DOWN_ARROW_EVENT);
93+
94+
expect(keyManager.activeItemIndex)
95+
.toBe(0, 'Expected active item to be 0 after down key if active item was null.');
96+
expect(keyManager.setActiveItem).toHaveBeenCalledWith(0);
97+
expect(keyManager.setActiveItem).not.toHaveBeenCalledWith(1);
98+
expect(keyManager.setActiveItem).not.toHaveBeenCalledWith(2);
99+
});
100+
90101
it('should set previous items as active when up arrow is pressed', () => {
91102
keyManager.onKeydown(DOWN_ARROW_EVENT);
92103

@@ -347,6 +358,22 @@ describe('Key managers', () => {
347358
expect(keyManager.activeItemIndex).toBe(2, 'Expected active item to wrap to end.');
348359
});
349360

361+
it('should set last item active when up arrow is pressed if no active item', () => {
362+
keyManager.withWrap();
363+
keyManager.setActiveItem(null);
364+
keyManager.onKeydown(UP_ARROW_EVENT);
365+
366+
expect(keyManager.activeItemIndex)
367+
.toBe(2, 'Expected last item to be active on up arrow if no active item.');
368+
expect(keyManager.setActiveItem).not.toHaveBeenCalledWith(0);
369+
expect(keyManager.setActiveItem).toHaveBeenCalledWith(2);
370+
371+
keyManager.onKeydown(DOWN_ARROW_EVENT);
372+
expect(keyManager.activeItemIndex)
373+
.toBe(0, 'Expected active item to be 0 after wrapping back to beginning.');
374+
expect(keyManager.setActiveItem).toHaveBeenCalledWith(0);
375+
});
376+
350377
});
351378

352379
});

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,12 +96,13 @@ export class ListKeyManager<T extends CanDisable> {
9696

9797
/** Sets the active item to the next enabled item in the list. */
9898
setNextItemActive(): void {
99-
this._setActiveItemByDelta(1);
99+
this._activeItemIndex === null ? this.setFirstItemActive() : this._setActiveItemByDelta(1);
100100
}
101101

102102
/** Sets the active item to a previous enabled item in the list. */
103103
setPreviousItemActive(): void {
104-
this._setActiveItemByDelta(-1);
104+
this._activeItemIndex === null && this._wrap ? this.setLastItemActive()
105+
: this._setActiveItemByDelta(-1);
105106
}
106107

107108
/**

0 commit comments

Comments
 (0)