Option groups (currentGroupedState): {{ currentGroupedState }}
+
+
+
+
+
+
+
+
+ {{ state.name }}
+ ({{state.code}})
+
+
+
+
+
+ {{ state.name }}
+
+
+
+
+
+ {{ state.name }}
+
+
diff --git a/src/demo-app/autocomplete/autocomplete-demo.scss b/src/demo-app/autocomplete/autocomplete-demo.scss
index f2018cc19e80..a74ecd15128f 100644
--- a/src/demo-app/autocomplete/autocomplete-demo.scss
+++ b/src/demo-app/autocomplete/autocomplete-demo.scss
@@ -9,6 +9,8 @@
.mat-form-field {
margin-top: 16px;
+ min-width: 200px;
+ max-width: 100%;
}
}
diff --git a/src/demo-app/autocomplete/autocomplete-demo.ts b/src/demo-app/autocomplete/autocomplete-demo.ts
index 96c25f70f866..a138a007df82 100644
--- a/src/demo-app/autocomplete/autocomplete-demo.ts
+++ b/src/demo-app/autocomplete/autocomplete-demo.ts
@@ -3,6 +3,16 @@ import {FormControl, NgModel} from '@angular/forms';
import 'rxjs/add/operator/startWith';
import 'rxjs/add/operator/map';
+export interface State {
+ code: string;
+ name: string;
+}
+
+export interface StateGroup {
+ letter: string;
+ states: State[];
+}
+
@Component({
moduleId: module.id,
selector: 'autocomplete-demo',
@@ -13,6 +23,7 @@ import 'rxjs/add/operator/map';
export class AutocompleteDemo {
stateCtrl: FormControl;
currentState = '';
+ currentGroupedState = '';
topHeightCtrl = new FormControl(0);
reactiveStates: any;
@@ -22,7 +33,9 @@ export class AutocompleteDemo {
@ViewChild(NgModel) modelDir: NgModel;
- states = [
+ groupedStates: StateGroup[];
+ filteredGroupedStates: StateGroup[];
+ states: State[] = [
{code: 'AL', name: 'Alabama'},
{code: 'AK', name: 'Alaska'},
{code: 'AZ', name: 'Arizona'},
@@ -82,6 +95,19 @@ export class AutocompleteDemo {
.startWith(this.stateCtrl.value)
.map(val => this.displayFn(val))
.map(name => this.filterStates(name));
+
+ this.filteredGroupedStates = this.groupedStates = this.states.reduce((groups, state) => {
+ let group = groups.find(g => g.letter === state.name[0]);
+
+ if (!group) {
+ group = { letter: state.name[0], states: [] };
+ groups.push(group);
+ }
+
+ group.states.push({ code: state.code, name: state.name });
+
+ return groups;
+ }, [] as StateGroup[]);
}
displayFn(value: any): string {
@@ -89,11 +115,21 @@ export class AutocompleteDemo {
}
filterStates(val: string) {
+ return val ? this._filter(this.states, val) : this.states;
+ }
+
+ filterStateGroups(val: string) {
if (val) {
- const filterValue = val.toLowerCase();
- return this.states.filter(state => state.name.toLowerCase().startsWith(filterValue));
+ return this.groupedStates
+ .map(group => ({ letter: group.letter, states: this._filter(group.states, val) }))
+ .filter(group => group.states.length > 0);
}
- return this.states;
+ return this.groupedStates;
+ }
+
+ private _filter(states: State[], val: string) {
+ const filterValue = val.toLowerCase();
+ return states.filter(state => state.name.toLowerCase().startsWith(filterValue));
}
}
diff --git a/src/lib/autocomplete/autocomplete-trigger.ts b/src/lib/autocomplete/autocomplete-trigger.ts
index 41b393d23553..8ac337756db6 100644
--- a/src/lib/autocomplete/autocomplete-trigger.ts
+++ b/src/lib/autocomplete/autocomplete-trigger.ts
@@ -357,8 +357,10 @@ 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 activeOptionIndex = this.autocomplete._keyManager.activeItemIndex || 0;
+ const labelCount = MdOption.countGroupLabelsBeforeOption(activeOptionIndex,
+ this.autocomplete.options, this.autocomplete.optionGroups);
+ const optionOffset = (activeOptionIndex + labelCount) * AUTOCOMPLETE_OPTION_HEIGHT;
const panelTop = this.autocomplete._getScrollTop();
if (optionOffset < panelTop) {
diff --git a/src/lib/autocomplete/autocomplete.md b/src/lib/autocomplete/autocomplete.md
index cb5a4f0c0d3b..b3e7e4ece279 100644
--- a/src/lib/autocomplete/autocomplete.md
+++ b/src/lib/autocomplete/autocomplete.md
@@ -1,11 +1,10 @@
-
-The autocomplete is a normal text input enhanced by a panel of suggested options. You can read more about
-autocompletes in the [Material Design spec](https://material.io/guidelines/components/text-fields.html#text-fields-auto-complete-text-field).
+The autocomplete is a normal text input enhanced by a panel of suggested options.
+You can read more about autocompletes in the [Material Design spec](https://material.io/guidelines/components/text-fields.html#text-fields-auto-complete-text-field).
### Simple autocomplete
-Start by adding a regular `mdInput` to the page. Let's assume you're using the `formControl` directive from the
-`@angular/forms` module to track the value of the input.
+Start by adding a regular `mdInput` to the page. Let's assume you're using the `formControl`
+directive from the `@angular/forms` module to track the value of the input.
*my-comp.html*
```html
@@ -14,10 +13,10 @@ Start by adding a regular `mdInput` to the page. Let's assume you're using the `
```
-Next, create the autocomplete panel and the options displayed inside it. Each option should be defined by an
-`md-option` tag. Set each option's value property to whatever you'd like the value of the text input to be
-upon that option's selection.
-
+Next, create the autocomplete panel and the options displayed inside it. Each option should be
+defined by an `md-option` tag. Set each option's value property to whatever you'd like the value
+of the text input to be upon that option's selection.
+
*my-comp.html*
```html
@@ -27,8 +26,9 @@ upon that option's selection.
```
-Now we'll need to link the text input to its panel. We can do this by exporting the autocomplete panel instance into a
-local template variable (here we called it "auto"), and binding that variable to the input's `mdAutocomplete` property.
+Now we'll need to link the text input to its panel. We can do this by exporting the autocomplete
+panel instance into a local template variable (here we called it "auto"), and binding that variable
+to the input's `mdAutocomplete` property.
*my-comp.html*
```html
@@ -47,38 +47,51 @@ local template variable (here we called it "auto"), and binding that variable to
### Adding a custom filter
-At this point, the autocomplete panel should be toggleable on focus and options should be selectable. But if we want
-our options to filter when we type, we need to add a custom filter.
+At this point, the autocomplete panel should be toggleable on focus and options should be
+selectable. But if we want our options to filter when we type, we need to add a custom filter.
-You can filter the options in any way you like based on the text input*. Here we will perform a simple string test on
-the option value to see if it matches the input value, starting from the option's first letter. We already have access
-to the built-in `valueChanges` observable on the `FormControl`, so we can simply map the text input's values to the
-suggested options by passing them through this filter. The resulting observable (`filteredOptions`) can be added to the
+You can filter the options in any way you like based on the text input*. Here we will perform a
+simple string test on the option value to see if it matches the input value, starting from the
+option's first letter. We already have access to the built-in `valueChanges` observable on the
+`FormControl`, so we can simply map the text input's values to the suggested options by passing
+them through this filter. The resulting observable (`filteredOptions`) can be added to the
template in place of the `options` property using the `async` pipe.
-Below we are also priming our value change stream with `null` so that the options are filtered by that value on init
-(before there are any value changes).
+Below we are also priming our value change stream with `null` so that the options are filtered by
+that value on init (before there are any value changes).
-*For optimal accessibility, you may want to consider adding text guidance on the page to explain filter criteria.
-This is especially helpful for screenreader users if you're using a non-standard filter that doesn't limit matches
-to the beginning of the string.
+*For optimal accessibility, you may want to consider adding text guidance on the page to explain
+filter criteria. This is especially helpful for screenreader users if you're using a non-standard
+filter that doesn't limit matches to the beginning of the string.
### Setting separate control and display values
-If you want the option's control value (what is saved in the form) to be different than the option's display value
-(what is displayed in the actual text field), you'll need to set the `displayWith` property on your autocomplete
-element. A common use case for this might be if you want to save your data as an object, but display just one of
-the option's string properties.
+If you want the option's control value (what is saved in the form) to be different than the option's
+display value (what is displayed in the actual text field), you'll need to set the `displayWith`
+property on your autocomplete element. A common use case for this might be if you want to save your
+data as an object, but display just one of the option's string properties.
-To make this work, create a function on your component class that maps the control value to the desired display value.
-Then bind it to the autocomplete's `displayWith` property.
+To make this work, create a function on your component class that maps the control value to the
+desired display value. Then bind it to the autocomplete's `displayWith` property.
-
### Keyboard interaction
- DOWN_ARROW: Next option becomes active.
- UP_ARROW: Previous option becomes active.
- ENTER: Select currently active item.
+
+#### Option groups
+`md-option` can be collected into groups using the `md-optgroup` element:
+
+```html
+
+
+
+ {{ option.name }}
+
+
+
+```
diff --git a/src/lib/autocomplete/autocomplete.spec.ts b/src/lib/autocomplete/autocomplete.spec.ts
index a20d0ab3d9ec..f9bf7961fb22 100644
--- a/src/lib/autocomplete/autocomplete.spec.ts
+++ b/src/lib/autocomplete/autocomplete.spec.ts
@@ -55,7 +55,8 @@ describe('MdAutocomplete', () => {
AutocompleteWithOnPushDelay,
AutocompleteWithNativeInput,
AutocompleteWithoutPanel,
- AutocompleteWithFormsAndNonfloatingPlaceholder
+ AutocompleteWithFormsAndNonfloatingPlaceholder,
+ AutocompleteWithGroups
],
providers: [
{provide: OverlayContainer, useFactory: () => {
@@ -986,6 +987,79 @@ describe('MdAutocomplete', () => {
});
+ describe('option groups', () => {
+ let fixture: ComponentFixture;
+ 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();
+ fixture.detectChanges();
+ 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();
+ });
+
+ //