Skip to content

Commit d3af57d

Browse files
willshowelltinayuangao
authored andcommitted
fix(autocomplete): don't scroll panel when option is visible (#4905)
* fix(autocomplete): don't scroll panel if option is visible * Clean up comments & indent by 4 on line continuations
1 parent b328d36 commit d3af57d

File tree

3 files changed

+83
-9
lines changed

3 files changed

+83
-9
lines changed

src/lib/autocomplete/autocomplete-trigger.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -321,15 +321,26 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
321321
/**
322322
* Given that we are not actually focusing active options, we must manually adjust scroll
323323
* to reveal options below the fold. First, we find the offset of the option from the top
324-
* of the panel. The new scrollTop will be that offset - the panel height + the option
325-
* height, so the active option will be just visible at the bottom of the panel.
324+
* of the panel. If that offset is below the fold, the new scrollTop will be the offset -
325+
* the panel height + the option height, so the active option will be just visible at the
326+
* bottom of the panel. If that offset is above the top of the visible panel, the new scrollTop
327+
* will become the offset. If that offset is visible within the panel already, the scrollTop is
328+
* not adjusted.
326329
*/
327330
private _scrollToOption(): void {
328331
const optionOffset = this.autocomplete._keyManager.activeItemIndex ?
329332
this.autocomplete._keyManager.activeItemIndex * AUTOCOMPLETE_OPTION_HEIGHT : 0;
330-
const newScrollTop =
331-
Math.max(0, optionOffset - AUTOCOMPLETE_PANEL_HEIGHT + AUTOCOMPLETE_OPTION_HEIGHT);
332-
this.autocomplete._setScrollTop(newScrollTop);
333+
const panelTop = this.autocomplete._getScrollTop();
334+
335+
if (optionOffset < panelTop) {
336+
// Scroll up to reveal selected option scrolled above the panel top
337+
this.autocomplete._setScrollTop(optionOffset);
338+
} else if (optionOffset + AUTOCOMPLETE_OPTION_HEIGHT > panelTop + AUTOCOMPLETE_PANEL_HEIGHT) {
339+
// Scroll down to reveal selected option scrolled below the panel bottom
340+
const newScrollTop =
341+
Math.max(0, optionOffset - AUTOCOMPLETE_PANEL_HEIGHT + AUTOCOMPLETE_OPTION_HEIGHT);
342+
this.autocomplete._setScrollTop(newScrollTop);
343+
}
333344
}
334345

335346
/**

src/lib/autocomplete/autocomplete.spec.ts

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -548,6 +548,7 @@ describe('MdAutocomplete', () => {
548548
let fixture: ComponentFixture<SimpleAutocomplete>;
549549
let input: HTMLInputElement;
550550
let DOWN_ARROW_EVENT: KeyboardEvent;
551+
let UP_ARROW_EVENT: KeyboardEvent;
551552
let ENTER_EVENT: KeyboardEvent;
552553

553554
beforeEach(() => {
@@ -556,6 +557,7 @@ describe('MdAutocomplete', () => {
556557

557558
input = fixture.debugElement.query(By.css('input')).nativeElement;
558559
DOWN_ARROW_EVENT = createKeyboardEvent('keydown', DOWN_ARROW);
560+
UP_ARROW_EVENT = createKeyboardEvent('keydown', UP_ARROW);
559561
ENTER_EVENT = createKeyboardEvent('keydown', ENTER);
560562

561563
fixture.componentInstance.trigger.openPanel();
@@ -614,7 +616,6 @@ describe('MdAutocomplete', () => {
614616
const optionEls =
615617
overlayContainerElement.querySelectorAll('md-option') as NodeListOf<HTMLElement>;
616618

617-
const UP_ARROW_EVENT = createKeyboardEvent('keydown', UP_ARROW);
618619
fixture.componentInstance.trigger._handleKeydown(UP_ARROW_EVENT);
619620
tick();
620621
fixture.detectChanges();
@@ -768,7 +769,6 @@ describe('MdAutocomplete', () => {
768769
const scrollContainer =
769770
document.querySelector('.cdk-overlay-pane .mat-autocomplete-panel')!;
770771

771-
const UP_ARROW_EVENT = createKeyboardEvent('keydown', UP_ARROW);
772772
fixture.componentInstance.trigger._handleKeydown(UP_ARROW_EVENT);
773773
tick();
774774
fixture.detectChanges();
@@ -777,6 +777,64 @@ describe('MdAutocomplete', () => {
777777
expect(scrollContainer.scrollTop).toEqual(272, `Expected panel to reveal last option.`);
778778
}));
779779

780+
it('should not scroll to active options that are fully in the panel', fakeAsync(() => {
781+
tick();
782+
const scrollContainer =
783+
document.querySelector('.cdk-overlay-pane .mat-autocomplete-panel')!;
784+
785+
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
786+
tick();
787+
fixture.detectChanges();
788+
expect(scrollContainer.scrollTop).toEqual(0, `Expected panel not to scroll.`);
789+
790+
// These down arrows will set the 6th option active, below the fold.
791+
[1, 2, 3, 4, 5].forEach(() => {
792+
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
793+
tick();
794+
});
795+
796+
// Expect option bottom minus the panel height (288 - 256 = 32)
797+
expect(scrollContainer.scrollTop)
798+
.toEqual(32, `Expected panel to reveal the sixth option.`);
799+
800+
// These up arrows will set the 2nd option active
801+
[4, 3, 2, 1].forEach(() => {
802+
fixture.componentInstance.trigger._handleKeydown(UP_ARROW_EVENT);
803+
tick();
804+
});
805+
806+
// Expect no scrolling to have occurred. Still showing bottom of 6th option.
807+
expect(scrollContainer.scrollTop)
808+
.toEqual(32, `Expected panel not to scroll up since sixth option still fully visible.`);
809+
}));
810+
811+
it('should scroll to active options that are above the panel', fakeAsync(() => {
812+
tick();
813+
const scrollContainer =
814+
document.querySelector('.cdk-overlay-pane .mat-autocomplete-panel')!;
815+
816+
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
817+
tick();
818+
fixture.detectChanges();
819+
expect(scrollContainer.scrollTop).toEqual(0, `Expected panel not to scroll.`);
820+
821+
// These down arrows will set the 7th option active, below the fold.
822+
[1, 2, 3, 4, 5, 6].forEach(() => {
823+
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
824+
tick();
825+
});
826+
827+
// These up arrows will set the 2nd option active
828+
[5, 4, 3, 2, 1].forEach(() => {
829+
fixture.componentInstance.trigger._handleKeydown(UP_ARROW_EVENT);
830+
tick();
831+
});
832+
833+
// Expect to show the top of the 2nd option at the top of the panel
834+
expect(scrollContainer.scrollTop)
835+
.toEqual(48, `Expected panel to scroll up when option is above panel.`);
836+
}));
837+
780838
it('should close the panel when pressing escape', async(() => {
781839
const trigger = fixture.componentInstance.trigger;
782840
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)