Skip to content

Commit 7d4f46e

Browse files
committed
fix(autocomplete): don't scroll panel if option is visible
1 parent 945aa43 commit 7d4f46e

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
@@ -287,15 +287,30 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
287287
/**
288288
* Given that we are not actually focusing active options, we must manually adjust scroll
289289
* to reveal options below the fold. First, we find the offset of the option from the top
290-
* of the panel. The new scrollTop will be that offset - the panel height + the option
291-
* height, so the active option will be just visible at the bottom of the panel.
290+
* of the panel. If that offset if below the fold, the new scrollTop will be the offset -
291+
* the panel height + the option height, so the active option will be just visible at the
292+
* bottom of the panel. If that offset is above the top of the panel, the new scrollTop
293+
* will become the offset. If that offset is visible within the panel, the scrollTop is not
294+
* adjusted.
292295
*/
293296
private _scrollToOption(): void {
294297
const optionOffset =
295298
this.autocomplete._keyManager.activeItemIndex * AUTOCOMPLETE_OPTION_HEIGHT;
296-
const newScrollTop =
299+
const panelTop = this.autocomplete._getScrollTop();
300+
301+
// Scroll up to reveal selected option above the panel
302+
if (optionOffset < panelTop) {
303+
this.autocomplete._setScrollTop(optionOffset);
304+
return;
305+
}
306+
307+
// Scroll down to reveal selection option below the panel
308+
if (optionOffset + AUTOCOMPLETE_OPTION_HEIGHT > panelTop + AUTOCOMPLETE_PANEL_HEIGHT) {
309+
const newScrollTop =
297310
Math.max(0, optionOffset - AUTOCOMPLETE_PANEL_HEIGHT + AUTOCOMPLETE_OPTION_HEIGHT);
298-
this.autocomplete._setScrollTop(newScrollTop);
311+
this.autocomplete._setScrollTop(newScrollTop);
312+
return;
313+
}
299314
}
300315

301316
/**

src/lib/autocomplete/autocomplete.spec.ts

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,7 @@ describe('MdAutocomplete', () => {
529529
let fixture: ComponentFixture<SimpleAutocomplete>;
530530
let input: HTMLInputElement;
531531
let DOWN_ARROW_EVENT: KeyboardEvent;
532+
let UP_ARROW_EVENT: KeyboardEvent;
532533
let ENTER_EVENT: KeyboardEvent;
533534

534535
beforeEach(() => {
@@ -537,6 +538,7 @@ describe('MdAutocomplete', () => {
537538

538539
input = fixture.debugElement.query(By.css('input')).nativeElement;
539540
DOWN_ARROW_EVENT = createKeyboardEvent('keydown', DOWN_ARROW);
541+
UP_ARROW_EVENT = createKeyboardEvent('keydown', UP_ARROW);
540542
ENTER_EVENT = createKeyboardEvent('keydown', ENTER);
541543

542544
fixture.componentInstance.trigger.openPanel();
@@ -595,7 +597,6 @@ describe('MdAutocomplete', () => {
595597
const optionEls =
596598
overlayContainerElement.querySelectorAll('md-option') as NodeListOf<HTMLElement>;
597599

598-
const UP_ARROW_EVENT = createKeyboardEvent('keydown', UP_ARROW);
599600
fixture.componentInstance.trigger._handleKeydown(UP_ARROW_EVENT);
600601
tick();
601602
fixture.detectChanges();
@@ -749,7 +750,6 @@ describe('MdAutocomplete', () => {
749750
const scrollContainer =
750751
document.querySelector('.cdk-overlay-pane .mat-autocomplete-panel');
751752

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

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

src/lib/autocomplete/autocomplete.ts

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

6262
/**
63-
* Sets the panel scrollTop. This allows us to manually scroll to display
64-
* options below the fold, as they are not actually being focused when active.
63+
* Sets the panel scrollTop. This allows us to manually scroll to display options
64+
* above or below the fold, as they are not actually being focused when active.
6565
*/
6666
_setScrollTop(scrollTop: number): void {
6767
if (this.panel) {
6868
this.panel.nativeElement.scrollTop = scrollTop;
6969
}
7070
}
7171

72+
/** Returns the panel's scrollTop. */
73+
_getScrollTop(): number {
74+
return this.panel ? this.panel.nativeElement.scrollTop : 0;
75+
}
76+
7277
/** Panel should hide itself when the option list is empty. */
7378
_setVisibility() {
7479
Promise.resolve().then(() => {

0 commit comments

Comments
 (0)