Skip to content

refactor(select,autocomplete): consolidate logic for scrolling options into view #9630

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 8, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 17 additions & 14 deletions src/lib/autocomplete/autocomplete-trigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,12 @@ import {
ViewContainerRef,
} from '@angular/core';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
import {MatOption, MatOptionSelectionChange} from '@angular/material/core';
import {
MatOption,
MatOptionSelectionChange,
_getOptionScrollPosition,
_countGroupLabelsBeforeOption,
} from '@angular/material/core';
import {MatFormField} from '@angular/material/form-field';
import {DOCUMENT} from '@angular/common';
import {Observable} from 'rxjs/Observable';
Expand Down Expand Up @@ -374,20 +379,18 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
* not adjusted.
*/
private _scrollToOption(): void {
const activeOptionIndex = this.autocomplete._keyManager.activeItemIndex || 0;
const labelCount = MatOption.countGroupLabelsBeforeOption(activeOptionIndex,
const index = this.autocomplete._keyManager.activeItemIndex || 0;
const labelCount = _countGroupLabelsBeforeOption(index,
this.autocomplete.options, this.autocomplete.optionGroups);
const optionOffset = (activeOptionIndex + labelCount) * AUTOCOMPLETE_OPTION_HEIGHT;
const panelTop = this.autocomplete._getScrollTop();

if (optionOffset < panelTop) {
// Scroll up to reveal selected option scrolled above the panel top
this.autocomplete._setScrollTop(optionOffset);
} else if (optionOffset + AUTOCOMPLETE_OPTION_HEIGHT > panelTop + AUTOCOMPLETE_PANEL_HEIGHT) {
// Scroll down to reveal selected option scrolled below the panel bottom
const newScrollTop = optionOffset - AUTOCOMPLETE_PANEL_HEIGHT + AUTOCOMPLETE_OPTION_HEIGHT;
this.autocomplete._setScrollTop(Math.max(0, newScrollTop));
}

const newScrollPosition = _getOptionScrollPosition(
index + labelCount,
AUTOCOMPLETE_OPTION_HEIGHT,
this.autocomplete._getScrollTop(),
AUTOCOMPLETE_PANEL_HEIGHT
);

this.autocomplete._setScrollTop(newScrollPosition);
}

/**
Expand Down
66 changes: 45 additions & 21 deletions src/lib/core/option/option.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,31 +246,55 @@ export class MatOption implements AfterViewChecked {
private _emitSelectionChangeEvent(isUserInput = false): void {
this.onSelectionChange.emit(new MatOptionSelectionChange(this, isUserInput));
}
}

/**
* Counts the amount of option group labels that precede the specified option.
* @param optionIndex Index of the option at which to start counting.
* @param options Flat list of all of the options.
* @param optionGroups Flat list of all of the option groups.
*/
static countGroupLabelsBeforeOption(optionIndex: number, options: QueryList<MatOption>,
optionGroups: QueryList<MatOptgroup>): number {

if (optionGroups.length) {
let optionsArray = options.toArray();
let groups = optionGroups.toArray();
let groupCounter = 0;

for (let i = 0; i < optionIndex + 1; i++) {
if (optionsArray[i].group && optionsArray[i].group === groups[groupCounter]) {
groupCounter++;
}
}
/**
* Counts the amount of option group labels that precede the specified option.
* @param optionIndex Index of the option at which to start counting.
* @param options Flat list of all of the options.
* @param optionGroups Flat list of all of the option groups.
* @docs-private
*/
export function _countGroupLabelsBeforeOption(optionIndex: number, options: QueryList<MatOption>,
optionGroups: QueryList<MatOptgroup>): number {

if (optionGroups.length) {
let optionsArray = options.toArray();
let groups = optionGroups.toArray();
let groupCounter = 0;

return groupCounter;
for (let i = 0; i < optionIndex + 1; i++) {
if (optionsArray[i].group && optionsArray[i].group === groups[groupCounter]) {
groupCounter++;
}
}

return 0;
return groupCounter;
}

return 0;
}

/**
* Determines the position to which to scroll a panel in order for an option to be into view.
* @param optionIndex Index of the option to be scrolled into the view.
* @param optionHeight Height of the options.
* @param currentScrollPosition Current scroll position of the panel.
* @param panelHeight Height of the panel.
* @docs-private
*/
export function _getOptionScrollPosition(optionIndex: number, optionHeight: number,
currentScrollPosition: number, panelHeight: number): number {
const optionOffset = optionIndex * optionHeight;

if (optionOffset < currentScrollPosition) {
return optionOffset;
}

if (optionOffset + optionHeight > currentScrollPosition + panelHeight) {
return Math.max(0, optionOffset - panelHeight + optionHeight);
}

return currentScrollPosition;
}

27 changes: 13 additions & 14 deletions src/lib/select/select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ import {
MAT_OPTION_PARENT_COMPONENT,
mixinDisableRipple,
CanDisableRipple,
_countGroupLabelsBeforeOption,
_getOptionScrollPosition,
} from '@angular/material/core';
import {MatFormField, MatFormFieldControl} from '@angular/material/form-field';
import {Observable} from 'rxjs/Observable';
Expand Down Expand Up @@ -971,19 +973,16 @@ export class MatSelect extends _MatSelectMixinBase implements AfterContentInit,

/** Scrolls the active option into view. */
private _scrollActiveOptionIntoView(): void {
const itemHeight = this._getItemHeight();
const activeOptionIndex = this._keyManager.activeItemIndex || 0;
const labelCount = MatOption.countGroupLabelsBeforeOption(activeOptionIndex,
this.options, this.optionGroups);
const scrollOffset = (activeOptionIndex + labelCount) * itemHeight;
const panelTop = this.panel.nativeElement.scrollTop;

if (scrollOffset < panelTop) {
this.panel.nativeElement.scrollTop = scrollOffset;
} else if (scrollOffset + itemHeight > panelTop + SELECT_PANEL_MAX_HEIGHT) {
this.panel.nativeElement.scrollTop =
Math.max(0, scrollOffset - SELECT_PANEL_MAX_HEIGHT + itemHeight);
}
const labelCount = _countGroupLabelsBeforeOption(activeOptionIndex, this.options,
this.optionGroups);

this.panel.nativeElement.scrollTop = _getOptionScrollPosition(
activeOptionIndex + labelCount,
this._getItemHeight(),
this.panel.nativeElement.scrollTop,
SELECT_PANEL_MAX_HEIGHT
);
}

/** Focuses the select element. */
Expand Down Expand Up @@ -1012,8 +1011,8 @@ export class MatSelect extends _MatSelectMixinBase implements AfterContentInit,
let selectedOptionOffset =
this.empty ? 0 : this._getOptionIndex(this._selectionModel.selected[0])!;

selectedOptionOffset += MatOption.countGroupLabelsBeforeOption(selectedOptionOffset,
this.options, this.optionGroups);
selectedOptionOffset += _countGroupLabelsBeforeOption(selectedOptionOffset, this.options,
this.optionGroups);

// We must maintain a scroll buffer so the selected option will be scrolled to the
// center of the overlay panel rather than the top.
Expand Down