Skip to content

Commit b693409

Browse files
committed
fix(autocomplete): don't scroll panel if option is visible
1 parent fe31210 commit b693409

File tree

3 files changed

+86
-8
lines changed

3 files changed

+86
-8
lines changed

src/lib/autocomplete/autocomplete-trigger.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -315,15 +315,30 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
315315
/**
316316
* Given that we are not actually focusing active options, we must manually adjust scroll
317317
* to reveal options below the fold. First, we find the offset of the option from the top
318-
* of the panel. The new scrollTop will be that offset - the panel height + the option
319-
* height, so the active option will be just visible at the bottom of the panel.
318+
* of the panel. If that offset if below the fold, the new scrollTop will be the offset -
319+
* the panel height + the option height, so the active option will be just visible at the
320+
* bottom of the panel. If that offset is above the top of the panel, the new scrollTop
321+
* will become the offset. If that offset is visible within the panel, the scrollTop is not
322+
* adjusted.
320323
*/
321324
private _scrollToOption(): void {
322325
const optionOffset = this.autocomplete._keyManager.activeItemIndex ?
323326
this.autocomplete._keyManager.activeItemIndex * AUTOCOMPLETE_OPTION_HEIGHT : 0;
324-
const newScrollTop =
327+
const panelTop = this.autocomplete._getScrollTop();
328+
329+
// Scroll up to reveal selected option above the panel
330+
if (optionOffset < panelTop) {
331+
this.autocomplete._setScrollTop(optionOffset);
332+
return;
333+
}
334+
335+
// Scroll down to reveal selection option below the panel
336+
if (optionOffset + AUTOCOMPLETE_OPTION_HEIGHT > panelTop + AUTOCOMPLETE_PANEL_HEIGHT) {
337+
const newScrollTop =
325338
Math.max(0, optionOffset - AUTOCOMPLETE_PANEL_HEIGHT + AUTOCOMPLETE_OPTION_HEIGHT);
326-
this.autocomplete._setScrollTop(newScrollTop);
339+
this.autocomplete._setScrollTop(newScrollTop);
340+
return;
341+
}
327342
}
328343

329344
/**

src/lib/autocomplete/autocomplete.spec.ts

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -534,6 +534,7 @@ describe('MdAutocomplete', () => {
534534
let fixture: ComponentFixture<SimpleAutocomplete>;
535535
let input: HTMLInputElement;
536536
let DOWN_ARROW_EVENT: KeyboardEvent;
537+
let UP_ARROW_EVENT: KeyboardEvent;
537538
let ENTER_EVENT: KeyboardEvent;
538539

539540
beforeEach(() => {
@@ -542,6 +543,7 @@ describe('MdAutocomplete', () => {
542543

543544
input = fixture.debugElement.query(By.css('input')).nativeElement;
544545
DOWN_ARROW_EVENT = createKeyboardEvent('keydown', DOWN_ARROW);
546+
UP_ARROW_EVENT = createKeyboardEvent('keydown', UP_ARROW);
545547
ENTER_EVENT = createKeyboardEvent('keydown', ENTER);
546548

547549
fixture.componentInstance.trigger.openPanel();
@@ -600,7 +602,6 @@ describe('MdAutocomplete', () => {
600602
const optionEls =
601603
overlayContainerElement.querySelectorAll('md-option') as NodeListOf<HTMLElement>;
602604

603-
const UP_ARROW_EVENT = createKeyboardEvent('keydown', UP_ARROW);
604605
fixture.componentInstance.trigger._handleKeydown(UP_ARROW_EVENT);
605606
tick();
606607
fixture.detectChanges();
@@ -754,7 +755,6 @@ describe('MdAutocomplete', () => {
754755
const scrollContainer =
755756
document.querySelector('.cdk-overlay-pane .mat-autocomplete-panel')!;
756757

757-
const UP_ARROW_EVENT = createKeyboardEvent('keydown', UP_ARROW);
758758
fixture.componentInstance.trigger._handleKeydown(UP_ARROW_EVENT);
759759
tick();
760760
fixture.detectChanges();
@@ -763,6 +763,64 @@ describe('MdAutocomplete', () => {
763763
expect(scrollContainer.scrollTop).toEqual(272, `Expected panel to reveal last option.`);
764764
}));
765765

766+
it('should not scroll to active options that are fully in the panel', fakeAsync(() => {
767+
tick();
768+
const scrollContainer =
769+
document.querySelector('.cdk-overlay-pane .mat-autocomplete-panel')!;
770+
771+
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
772+
tick();
773+
fixture.detectChanges();
774+
expect(scrollContainer.scrollTop).toEqual(0, `Expected panel not to scroll.`);
775+
776+
// These down arrows will set the 6th option active, below the fold.
777+
[1, 2, 3, 4, 5].forEach(() => {
778+
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
779+
tick();
780+
});
781+
782+
// Expect option bottom minus the panel height (288 - 256 = 32)
783+
expect(scrollContainer.scrollTop)
784+
.toEqual(32, `Expected panel to reveal the sixth option.`);
785+
786+
// These up arrows will set the 2nd option active
787+
[4, 3, 2, 1].forEach(() => {
788+
fixture.componentInstance.trigger._handleKeydown(UP_ARROW_EVENT);
789+
tick();
790+
});
791+
792+
// Expect no scrolling to have occurred. Still showing bottom of 6th option.
793+
expect(scrollContainer.scrollTop)
794+
.toEqual(32, `Expected panel to not scroll back.`);
795+
}));
796+
797+
it('should scroll to active options that are above the panel', fakeAsync(() => {
798+
tick();
799+
const scrollContainer =
800+
document.querySelector('.cdk-overlay-pane .mat-autocomplete-panel')!;
801+
802+
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
803+
tick();
804+
fixture.detectChanges();
805+
expect(scrollContainer.scrollTop).toEqual(0, `Expected panel not to scroll.`);
806+
807+
// These down arrows will set the 7th option active, below the fold.
808+
[1, 2, 3, 4, 5, 6].forEach(() => {
809+
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
810+
tick();
811+
});
812+
813+
// These up arrows will set the 2nd option active
814+
[5, 4, 3, 2, 1].forEach(() => {
815+
fixture.componentInstance.trigger._handleKeydown(UP_ARROW_EVENT);
816+
tick();
817+
});
818+
819+
// Expect to show the top of the 2nd option at the top of the panel
820+
expect(scrollContainer.scrollTop)
821+
.toEqual(48, `Expected panel to scroll up when option is above panel.`);
822+
}));
823+
766824
it('should close the panel when pressing escape', async(() => {
767825
const trigger = fixture.componentInstance.trigger;
768826
const escapeEvent = createKeyboardEvent('keydown', ESCAPE);

src/lib/autocomplete/autocomplete.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,15 +73,20 @@ export class MdAutocomplete implements AfterContentInit {
7373
}
7474

7575
/**
76-
* Sets the panel scrollTop. This allows us to manually scroll to display
77-
* options below the fold, as they are not actually being focused when active.
76+
* Sets the panel scrollTop. This allows us to manually scroll to display options
77+
* above or below the fold, as they are not actually being focused when active.
7878
*/
7979
_setScrollTop(scrollTop: number): void {
8080
if (this.panel) {
8181
this.panel.nativeElement.scrollTop = scrollTop;
8282
}
8383
}
8484

85+
/** Returns the panel's scrollTop. */
86+
_getScrollTop(): number {
87+
return this.panel ? this.panel.nativeElement.scrollTop : 0;
88+
}
89+
8590
/** Panel should hide itself when the option list is empty. */
8691
_setVisibility() {
8792
Promise.resolve().then(() => {

0 commit comments

Comments
 (0)