Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(autocomplete): scroll options below fold into view #2728

Merged
merged 1 commit into from
Jan 25, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 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 - 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