Skip to content

Commit

Permalink
fix(autocomplete): don't scroll panel if option is visible
Browse files Browse the repository at this point in the history
  • Loading branch information
willshowell committed Jun 22, 2017
1 parent fe31210 commit b693409
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 8 deletions.
23 changes: 19 additions & 4 deletions src/lib/autocomplete/autocomplete-trigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,15 +315,30 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
/**
* 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.
* of the panel. If that offset if below the fold, the new scrollTop will be the offset -
* the panel height + the option height, so the active option will be just visible at the
* bottom of the panel. If that offset is above the top of the panel, the new scrollTop
* will become the offset. If that offset is visible within the panel, the scrollTop is not
* adjusted.
*/
private _scrollToOption(): void {
const optionOffset = this.autocomplete._keyManager.activeItemIndex ?
this.autocomplete._keyManager.activeItemIndex * AUTOCOMPLETE_OPTION_HEIGHT : 0;
const newScrollTop =
const panelTop = this.autocomplete._getScrollTop();

// Scroll up to reveal selected option above the panel
if (optionOffset < panelTop) {
this.autocomplete._setScrollTop(optionOffset);
return;
}

// Scroll down to reveal selection option below the panel
if (optionOffset + AUTOCOMPLETE_OPTION_HEIGHT > panelTop + AUTOCOMPLETE_PANEL_HEIGHT) {
const newScrollTop =
Math.max(0, optionOffset - AUTOCOMPLETE_PANEL_HEIGHT + AUTOCOMPLETE_OPTION_HEIGHT);
this.autocomplete._setScrollTop(newScrollTop);
this.autocomplete._setScrollTop(newScrollTop);
return;
}
}

/**
Expand Down
62 changes: 60 additions & 2 deletions src/lib/autocomplete/autocomplete.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,7 @@ describe('MdAutocomplete', () => {
let fixture: ComponentFixture<SimpleAutocomplete>;
let input: HTMLInputElement;
let DOWN_ARROW_EVENT: KeyboardEvent;
let UP_ARROW_EVENT: KeyboardEvent;
let ENTER_EVENT: KeyboardEvent;

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

input = fixture.debugElement.query(By.css('input')).nativeElement;
DOWN_ARROW_EVENT = createKeyboardEvent('keydown', DOWN_ARROW);
UP_ARROW_EVENT = createKeyboardEvent('keydown', UP_ARROW);
ENTER_EVENT = createKeyboardEvent('keydown', ENTER);

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

const UP_ARROW_EVENT = createKeyboardEvent('keydown', UP_ARROW);
fixture.componentInstance.trigger._handleKeydown(UP_ARROW_EVENT);
tick();
fixture.detectChanges();
Expand Down Expand Up @@ -754,7 +755,6 @@ describe('MdAutocomplete', () => {
const scrollContainer =
document.querySelector('.cdk-overlay-pane .mat-autocomplete-panel')!;

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

it('should not scroll to active options that are fully in the panel', fakeAsync(() => {
tick();
const scrollContainer =
document.querySelector('.cdk-overlay-pane .mat-autocomplete-panel')!;

fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
tick();
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);
tick();
});

// Expect option bottom minus the panel height (288 - 256 = 32)
expect(scrollContainer.scrollTop)
.toEqual(32, `Expected panel to reveal the sixth option.`);

// These up arrows will set the 2nd option active
[4, 3, 2, 1].forEach(() => {
fixture.componentInstance.trigger._handleKeydown(UP_ARROW_EVENT);
tick();
});

// Expect no scrolling to have occurred. Still showing bottom of 6th option.
expect(scrollContainer.scrollTop)
.toEqual(32, `Expected panel to not scroll back.`);
}));

it('should scroll to active options that are above the panel', fakeAsync(() => {
tick();
const scrollContainer =
document.querySelector('.cdk-overlay-pane .mat-autocomplete-panel')!;

fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
tick();
fixture.detectChanges();
expect(scrollContainer.scrollTop).toEqual(0, `Expected panel not to scroll.`);

// These down arrows will set the 7th option active, below the fold.
[1, 2, 3, 4, 5, 6].forEach(() => {
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
tick();
});

// These up arrows will set the 2nd option active
[5, 4, 3, 2, 1].forEach(() => {
fixture.componentInstance.trigger._handleKeydown(UP_ARROW_EVENT);
tick();
});

// Expect to show the top of the 2nd option at the top of the panel
expect(scrollContainer.scrollTop)
.toEqual(48, `Expected panel to scroll up when option is above panel.`);
}));

it('should close the panel when pressing escape', async(() => {
const trigger = fixture.componentInstance.trigger;
const escapeEvent = createKeyboardEvent('keydown', ESCAPE);
Expand Down
9 changes: 7 additions & 2 deletions src/lib/autocomplete/autocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,15 +73,20 @@ export class MdAutocomplete implements AfterContentInit {
}

/**
* 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.
* Sets the panel scrollTop. This allows us to manually scroll to display options
* above or below the fold, as they are not actually being focused when active.
*/
_setScrollTop(scrollTop: number): void {
if (this.panel) {
this.panel.nativeElement.scrollTop = scrollTop;
}
}

/** Returns the panel's scrollTop. */
_getScrollTop(): number {
return this.panel ? this.panel.nativeElement.scrollTop : 0;
}

/** Panel should hide itself when the option list is empty. */
_setVisibility() {
Promise.resolve().then(() => {
Expand Down

0 comments on commit b693409

Please sign in to comment.