Skip to content

Commit fbed180

Browse files
committed
fix(autocomplete): scroll options below fold into view (#2728)
1 parent 0fe8959 commit fbed180

File tree

4 files changed

+60
-2
lines changed

4 files changed

+60
-2
lines changed

src/lib/autocomplete/autocomplete-trigger.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,25 @@ import {ConnectedPositionStrategy} from '../core/overlay/position/connected-posi
99
import {Observable} from 'rxjs/Observable';
1010
import {MdOptionSelectEvent, MdOption} from '../core/option/option';
1111
import {ActiveDescendantKeyManager} from '../core/a11y/activedescendant-key-manager';
12-
import {ENTER} from '../core/keyboard/keycodes';
12+
import {ENTER, UP_ARROW, DOWN_ARROW} from '../core/keyboard/keycodes';
1313
import {Subscription} from 'rxjs/Subscription';
1414
import 'rxjs/add/observable/merge';
1515
import {Dir} from '../core/rtl/dir';
1616
import 'rxjs/add/operator/startWith';
1717
import 'rxjs/add/operator/switchMap';
1818

19+
/**
20+
* The following style constants are necessary to save here in order
21+
* to properly calculate the scrollTop of the panel. Because we are not
22+
* actually focusing the active item, scroll must be handled manually.
23+
*/
24+
25+
/** The height of each autocomplete option. */
26+
export const AUTOCOMPLETE_OPTION_HEIGHT = 48;
27+
28+
/** The total height of the autocomplete panel. */
29+
export const AUTOCOMPLETE_PANEL_HEIGHT = 256;
30+
1931
@Directive({
2032
selector: 'input[mdAutocomplete], input[matAutocomplete]',
2133
host: {
@@ -117,9 +129,25 @@ export class MdAutocompleteTrigger implements AfterContentInit, OnDestroy {
117129
} else {
118130
this.openPanel();
119131
this._keyManager.onKeydown(event);
132+
if (event.keyCode === UP_ARROW || event.keyCode === DOWN_ARROW) {
133+
this._scrollToOption();
134+
}
120135
}
121136
}
122137

138+
/**
139+
* Given that we are not actually focusing active options, we must manually adjust scroll
140+
* to reveal options below the fold. First, we find the offset of the option from the top
141+
* of the panel. The new scrollTop will be that offset - the panel height + the option
142+
* height, so the active option will be just visible at the bottom of the panel.
143+
*/
144+
private _scrollToOption(): void {
145+
const optionOffset = this._keyManager.activeItemIndex * AUTOCOMPLETE_OPTION_HEIGHT;
146+
const newScrollTop =
147+
Math.max(0, optionOffset - AUTOCOMPLETE_PANEL_HEIGHT + AUTOCOMPLETE_OPTION_HEIGHT);
148+
this.autocomplete._setScrollTop(newScrollTop);
149+
}
150+
123151
/**
124152
* This method listens to a stream of panel closing actions and resets the
125153
* stream every time the option list changes.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<template>
2-
<div class="md-autocomplete-panel" role="listbox" [id]="id" [ngClass]="_getPositionClass()">
2+
<div class="md-autocomplete-panel" role="listbox" [id]="id" [ngClass]="_getPositionClass()" #panel>
33
<ng-content></ng-content>
44
</div>
55
</template>

src/lib/autocomplete/autocomplete.spec.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,26 @@ describe('MdAutocomplete', () => {
391391
});
392392
}));
393393

394+
it('should scroll to active options below the fold', () => {
395+
fixture.componentInstance.trigger.openPanel();
396+
fixture.detectChanges();
397+
398+
const scrollContainer = document.querySelector('.cdk-overlay-pane .md-autocomplete-panel');
399+
400+
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
401+
fixture.detectChanges();
402+
expect(scrollContainer.scrollTop).toEqual(0, `Expected panel not to scroll.`);
403+
404+
// These down arrows will set the 6th option active, below the fold.
405+
[1, 2, 3, 4, 5].forEach(() => {
406+
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
407+
});
408+
fixture.detectChanges();
409+
410+
// Expect option bottom minus the panel height (288 - 256 = 32)
411+
expect(scrollContainer.scrollTop).toEqual(32, `Expected panel to reveal the sixth option.`);
412+
});
413+
394414
});
395415

396416
describe('aria', () => {

src/lib/autocomplete/autocomplete.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
Component,
33
ContentChildren,
4+
ElementRef,
45
QueryList,
56
TemplateRef,
67
ViewChild,
@@ -29,11 +30,20 @@ export class MdAutocomplete {
2930
positionY: MenuPositionY = 'below';
3031

3132
@ViewChild(TemplateRef) template: TemplateRef<any>;
33+
@ViewChild('panel') panel: ElementRef;
3234
@ContentChildren(MdOption) options: QueryList<MdOption>;
3335

3436
/** Unique ID to be used by autocomplete trigger's "aria-owns" property. */
3537
id: string = `md-autocomplete-${_uniqueAutocompleteIdCounter++}`;
3638

39+
/**
40+
* Sets the panel scrollTop. This allows us to manually scroll to display
41+
* options below the fold, as they are not actually being focused when active.
42+
*/
43+
_setScrollTop(scrollTop: number): void {
44+
this.panel.nativeElement.scrollTop = scrollTop;
45+
}
46+
3747
/** Sets a class on the panel based on its position (used to set y-offset). */
3848
_getPositionClass() {
3949
return {

0 commit comments

Comments
 (0)