Skip to content

Commit

Permalink
feat(autocomplete): support for md-optgroup
Browse files Browse the repository at this point in the history
Adds support for `md-optgroup` in `md-autocomplete` by:
* Changing the `@ViewChild` query to pick up descendant options.
* Tweaking the keyboard scrolling to handle having group labels before options.

Fixes #5581.
  • Loading branch information
crisbeto committed Jul 8, 2017
1 parent 8817b4d commit 8ad659f
Show file tree
Hide file tree
Showing 3 changed files with 137 additions and 5 deletions.
26 changes: 24 additions & 2 deletions src/lib/autocomplete/autocomplete-trigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,8 +328,7 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
* not adjusted.
*/
private _scrollToOption(): void {
const optionOffset = this.autocomplete._keyManager.activeItemIndex ?
this.autocomplete._keyManager.activeItemIndex * AUTOCOMPLETE_OPTION_HEIGHT : 0;
const optionOffset = this._getActiveOptionIndex() * AUTOCOMPLETE_OPTION_HEIGHT;
const panelTop = this.autocomplete._getScrollTop();

if (optionOffset < panelTop) {
Expand All @@ -343,6 +342,29 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
}
}

/** Determines the index of the active option. */
private _getActiveOptionIndex(): number {
let optionIndex = this.autocomplete._keyManager.activeItemIndex || 0;

// If there are any option groups, we need to offset
// the index by the amount of groups that come before the option.
if (this.autocomplete.optionGroups.length) {
const options = this.autocomplete.options.toArray();
const groups = this.autocomplete.optionGroups.toArray();
let groupCounter = 0;

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

optionIndex += groupCounter;
}

return optionIndex;
}

/**
* This method listens to a stream of panel closing actions and resets the
* stream every time the option list changes.
Expand Down
109 changes: 108 additions & 1 deletion src/lib/autocomplete/autocomplete.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ describe('MdAutocomplete', () => {
AutocompleteWithNumbers,
AutocompleteWithOnPushDelay,
AutocompleteWithNativeInput,
AutocompleteWithoutPanel
AutocompleteWithoutPanel,
AutocompleteWithGroups
],
providers: [
{provide: OverlayContainer, useFactory: () => {
Expand Down Expand Up @@ -853,6 +854,78 @@ describe('MdAutocomplete', () => {

});

describe('option groups', () => {
let fixture: ComponentFixture<AutocompleteWithGroups>;
let DOWN_ARROW_EVENT: KeyboardEvent;
let UP_ARROW_EVENT: KeyboardEvent;
let container: HTMLElement;

beforeEach(fakeAsync(() => {
fixture = TestBed.createComponent(AutocompleteWithGroups);
fixture.detectChanges();

DOWN_ARROW_EVENT = createKeyboardEvent('keydown', DOWN_ARROW);
UP_ARROW_EVENT = createKeyboardEvent('keydown', UP_ARROW);

fixture.componentInstance.trigger.openPanel();
fixture.detectChanges();
tick();
container = document.querySelector('.mat-autocomplete-panel') as HTMLElement;
}));

it('should scroll to active options below the fold', fakeAsync(() => {
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
tick();
fixture.detectChanges();
expect(container.scrollTop).toBe(0, 'Expected the panel not to scroll.');

// Press the down arrow five times.
[1, 2, 3, 4, 5].forEach(() => {
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
tick();
});

// <option bottom> - <panel height> + <2x group labels> = 128
// 288 - 256 + 96 = 128
expect(container.scrollTop)
.toBe(128, 'Expected panel to reveal the sixth option.');
}));

it('should scroll to active options on UP arrow', fakeAsync(() => {
fixture.componentInstance.trigger._handleKeydown(UP_ARROW_EVENT);
tick();
fixture.detectChanges();

// <option bottom> - <panel height> + <3x group label> = 464
// 576 - 256 + 144 = 464
expect(container.scrollTop).toBe(464, 'Expected panel to reveal last option.');
}));

it('should scroll to active options that are above the panel', fakeAsync(() => {
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
tick();
fixture.detectChanges();
expect(container.scrollTop).toBe(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.
// It is offset by 48, because there's a group label above it.
expect(container.scrollTop)
.toBe(96, 'Expected panel to scroll up when option is above panel.');
}));
});

describe('aria', () => {
let fixture: ComponentFixture<SimpleAutocomplete>;
let input: HTMLInputElement;
Expand Down Expand Up @@ -1551,3 +1624,37 @@ class AutocompleteWithNativeInput {
class AutocompleteWithoutPanel {
@ViewChild(MdAutocompleteTrigger) trigger: MdAutocompleteTrigger;
}

@Component({
template: `
<md-input-container>
<input mdInput placeholder="State" [mdAutocomplete]="auto" [(ngModel)]="selectedState">
</md-input-container>
<md-autocomplete #auto="mdAutocomplete">
<md-optgroup *ngFor="let group of stateGroups" [label]="group.label">
<md-option *ngFor="let state of group.states" [value]="state">
<span>{{ state }}</span>
</md-option>
</md-optgroup>
</md-autocomplete>
`
})
class AutocompleteWithGroups {
@ViewChild(MdAutocompleteTrigger) trigger: MdAutocompleteTrigger;
selectedState: string;
stateGroups = [
{
title: 'One',
states: ['Alabama', 'California', 'Florida', 'Oregon']
},
{
title: 'Two',
states: ['Kansas', 'Massachusetts', 'New York', 'Pennsylvania']
},
{
title: 'Three',
states: ['Tennessee', 'Virginia', 'Wyoming', 'Alaska']
}
];
}
7 changes: 5 additions & 2 deletions src/lib/autocomplete/autocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
ViewEncapsulation,
ChangeDetectorRef,
} from '@angular/core';
import {MdOption} from '../core';
import {MdOption, MdOptgroup} from '../core';
import {ActiveDescendantKeyManager} from '../core/a11y/activedescendant-key-manager';

/**
Expand Down Expand Up @@ -58,7 +58,10 @@ export class MdAutocomplete implements AfterContentInit {
@ViewChild('panel') panel: ElementRef;

/** @docs-private */
@ContentChildren(MdOption) options: QueryList<MdOption>;
@ContentChildren(MdOption, { descendants: true }) options: QueryList<MdOption>;

/** @docs-private */
@ContentChildren(MdOptgroup) optionGroups: QueryList<MdOptgroup>;

/** Function that maps an option's control value to its display value in the trigger. */
@Input() displayWith: ((value: any) => string) | null = null;
Expand Down

0 comments on commit 8ad659f

Please sign in to comment.