Skip to content

Commit

Permalink
fix(autocomplete): scroll options below fold into view
Browse files Browse the repository at this point in the history
  • Loading branch information
kara committed Jan 25, 2017
1 parent e2e3163 commit fbc1088
Show file tree
Hide file tree
Showing 4 changed files with 60 additions and 2 deletions.
30 changes: 29 additions & 1 deletion src/lib/autocomplete/autocomplete-trigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,25 @@ import {ConnectedPositionStrategy} from '../core/overlay/position/connected-posi
import {Observable} from 'rxjs/Observable';
import {MdOptionSelectEvent, MdOption} from '../core/option/option';
import {ActiveDescendantKeyManager} from '../core/a11y/activedescendant-key-manager';
import {ENTER} from '../core/keyboard/keycodes';
import {ENTER, UP_ARROW, DOWN_ARROW} from '../core/keyboard/keycodes';
import {Subscription} from 'rxjs/Subscription';
import 'rxjs/add/observable/merge';
import {Dir} from '../core/rtl/dir';
import 'rxjs/add/operator/startWith';
import 'rxjs/add/operator/switchMap';

/**
* The following style constants are necessary to save here in order
* to properly calculate the scrollTop of the panel. Because we are not
* actually focusing the active item, scroll must be handled manually.
*/

/** The height of each autocomplete option. */
export const AUTOCOMPLETE_OPTION_HEIGHT = 48;

/** The total height of the autocomplete panel. */
export const MD_AUTOCOMPLETE_PANEL_HEIGHT = 256;

@Directive({
selector: 'input[mdAutocomplete], input[matAutocomplete]',
host: {
Expand Down Expand Up @@ -117,9 +129,25 @@ export class MdAutocompleteTrigger implements AfterContentInit, OnDestroy {
} else {
this.openPanel();
this._keyManager.onKeydown(event);
if (event.keyCode === UP_ARROW || event.keyCode === DOWN_ARROW) {
this._scrollToOption();
}
}
}

/**
* Given that we are not actually focusing active options, we must manually adjust scroll
* to reveal options below the fold. First, we find the offset of the option from the top
* of the panel. The new scrollTop will be that offset - the panel height + the option
* height, so the active option will be just visible at the bottom of the panel.
*/
private _scrollToOption(): void {
const optionOffset = this._keyManager.activeItemIndex * AUTOCOMPLETE_OPTION_HEIGHT;
const newScrollTop =
Math.max(0, optionOffset - MD_AUTOCOMPLETE_PANEL_HEIGHT + AUTOCOMPLETE_OPTION_HEIGHT);
this.autocomplete._setScrollTop(newScrollTop);
}

/**
* This method listens to a stream of panel closing actions and resets the
* stream every time the option list changes.
Expand Down
2 changes: 1 addition & 1 deletion src/lib/autocomplete/autocomplete.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<div class="md-autocomplete-panel" role="listbox" [id]="id" [ngClass]="_getPositionClass()">
<div class="md-autocomplete-panel" role="listbox" [id]="id" [ngClass]="_getPositionClass()" #panel>
<ng-content></ng-content>
</div>
</template>
20 changes: 20 additions & 0 deletions src/lib/autocomplete/autocomplete.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,26 @@ describe('MdAutocomplete', () => {
});
}));

it('should scroll to active options below the fold', () => {
fixture.componentInstance.trigger.openPanel();
fixture.detectChanges();

const scrollContainer = document.querySelector('.cdk-overlay-pane .md-autocomplete-panel');

fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
fixture.detectChanges();
expect(scrollContainer.scrollTop).toEqual(0, `Expected panel not to scroll.`);

// These down arrows will set the 6th option active, below the fold.
[1, 2, 3, 4, 5].forEach(() => {
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
});
fixture.detectChanges();

// Expect option bottom minus the panel height (288 - 256 = 32)
expect(scrollContainer.scrollTop).toEqual(32, `Expected panel to reveal the sixth option.`);
});

});

describe('aria', () => {
Expand Down
10 changes: 10 additions & 0 deletions src/lib/autocomplete/autocomplete.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
Component,
ContentChildren,
ElementRef,
QueryList,
TemplateRef,
ViewChild,
Expand Down Expand Up @@ -29,11 +30,20 @@ export class MdAutocomplete {
positionY: MenuPositionY = 'below';

@ViewChild(TemplateRef) template: TemplateRef<any>;
@ViewChild('panel') panel: ElementRef;
@ContentChildren(MdOption) options: QueryList<MdOption>;

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

/**
* Sets the panel scrollTop. This allows us to manually scroll to display
* options below the fold, as they are not actually being focused when active.
*/
_setScrollTop(scrollTop: number): void {
this.panel.nativeElement.scrollTop = scrollTop;
}

/** Sets a class on the panel based on its position (used to set y-offset). */
_getPositionClass() {
return {
Expand Down

0 comments on commit fbc1088

Please sign in to comment.