From 652df124ad907d114979f871ce13bed0f42e2280 Mon Sep 17 00:00:00 2001 From: Kara Erickson Date: Tue, 24 Jan 2017 17:36:42 -0800 Subject: [PATCH] fix(autocomplete): scroll options below fold into view --- src/lib/autocomplete/autocomplete-trigger.ts | 30 +++++++++++++++++++- src/lib/autocomplete/autocomplete.html | 2 +- src/lib/autocomplete/autocomplete.spec.ts | 20 +++++++++++++ src/lib/autocomplete/autocomplete.ts | 10 +++++++ 4 files changed, 60 insertions(+), 2 deletions(-) diff --git a/src/lib/autocomplete/autocomplete-trigger.ts b/src/lib/autocomplete/autocomplete-trigger.ts index 1b3fca3aa07c..3e0453c51ced 100644 --- a/src/lib/autocomplete/autocomplete-trigger.ts +++ b/src/lib/autocomplete/autocomplete-trigger.ts @@ -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: { @@ -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. diff --git a/src/lib/autocomplete/autocomplete.html b/src/lib/autocomplete/autocomplete.html index c1d9523c69de..2d5e9661c483 100644 --- a/src/lib/autocomplete/autocomplete.html +++ b/src/lib/autocomplete/autocomplete.html @@ -1,5 +1,5 @@ \ No newline at end of file diff --git a/src/lib/autocomplete/autocomplete.spec.ts b/src/lib/autocomplete/autocomplete.spec.ts index 37411c60aaf8..246f109f7761 100644 --- a/src/lib/autocomplete/autocomplete.spec.ts +++ b/src/lib/autocomplete/autocomplete.spec.ts @@ -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', () => { diff --git a/src/lib/autocomplete/autocomplete.ts b/src/lib/autocomplete/autocomplete.ts index e6608457f36a..792478fbbbe3 100644 --- a/src/lib/autocomplete/autocomplete.ts +++ b/src/lib/autocomplete/autocomplete.ts @@ -1,6 +1,7 @@ import { Component, ContentChildren, + ElementRef, QueryList, TemplateRef, ViewChild, @@ -29,11 +30,20 @@ export class MdAutocomplete { positionY: MenuPositionY = 'below'; @ViewChild(TemplateRef) template: TemplateRef; + @ViewChild('panel') panel: ElementRef; @ContentChildren(MdOption) options: QueryList; /** 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 {