mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
committed by
Kara
parent
7f0e58e432
commit
e41d0f3e9b
@@ -51,4 +51,37 @@ Space above cards: <input type="number" [formControl]="topHeightCtrl">
|
||||
</md-card-actions>
|
||||
|
||||
</md-card>
|
||||
|
||||
<md-card>
|
||||
<div>Option groups (currentGroupedState): {{ currentGroupedState }}</div>
|
||||
|
||||
<md-input-container>
|
||||
<input
|
||||
mdInput
|
||||
placeholder="State"
|
||||
[mdAutocomplete]="groupedAuto"
|
||||
[(ngModel)]="currentGroupedState"
|
||||
(ngModelChange)="filteredGroupedStates = filterStateGroups(currentGroupedState)">
|
||||
</md-input-container>
|
||||
</md-card>
|
||||
</div>
|
||||
|
||||
<md-autocomplete #reactiveAuto="mdAutocomplete" [displayWith]="displayFn">
|
||||
<md-option *ngFor="let state of reactiveStates | async" [value]="state">
|
||||
<span>{{ state.name }}</span>
|
||||
<span class="demo-secondary-text"> ({{state.code}}) </span>
|
||||
</md-option>
|
||||
</md-autocomplete>
|
||||
|
||||
<md-autocomplete #tdAuto="mdAutocomplete">
|
||||
<md-option *ngFor="let state of tdStates" [value]="state.name">
|
||||
<span>{{ state.name }}</span>
|
||||
</md-option>
|
||||
</md-autocomplete>
|
||||
|
||||
<md-autocomplete #groupedAuto="mdAutocomplete">
|
||||
<md-optgroup *ngFor="let group of filteredGroupedStates"
|
||||
[label]="'States starting with ' + group.letter">
|
||||
<md-option *ngFor="let state of group.states" [value]="state.name">{{ state.name }}</md-option>
|
||||
</md-optgroup>
|
||||
</md-autocomplete>
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
|
||||
.mat-form-field {
|
||||
margin-top: 16px;
|
||||
min-width: 200px;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 `
|
||||
</md-form-field>
|
||||
```
|
||||
|
||||
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
|
||||
<md-autocomplete>
|
||||
@@ -27,8 +26,9 @@ upon that option's selection.
|
||||
</md-autocomplete>
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
<!-- example(autocomplete-filter) -->
|
||||
|
||||
### 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.
|
||||
|
||||
<!-- example(autocomplete-display) -->
|
||||
|
||||
|
||||
### Keyboard interaction
|
||||
- <kbd>DOWN_ARROW</kbd>: Next option becomes active.
|
||||
- <kbd>UP_ARROW</kbd>: Previous option becomes active.
|
||||
- <kbd>ENTER</kbd>: Select currently active item.
|
||||
|
||||
#### Option groups
|
||||
`md-option` can be collected into groups using the `md-optgroup` element:
|
||||
|
||||
```html
|
||||
<md-autocomplete #auto="mdAutocomplete">
|
||||
<md-optgroup *ngFor="let group of filteredGroups | async" [label]="group.name">
|
||||
<md-option *ngFor="let option of group.options" [value]="option">
|
||||
{{ option.name }}
|
||||
</md-option>
|
||||
</md-optgroup>
|
||||
</md-autocomplete>
|
||||
```
|
||||
|
||||
@@ -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<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();
|
||||
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();
|
||||
});
|
||||
|
||||
// <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;
|
||||
@@ -1706,3 +1780,38 @@ class AutocompleteWithoutPanel {
|
||||
class AutocompleteWithFormsAndNonfloatingPlaceholder {
|
||||
formControl = new FormControl('California');
|
||||
}
|
||||
|
||||
|
||||
@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']
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
ChangeDetectorRef,
|
||||
ChangeDetectionStrategy,
|
||||
} from '@angular/core';
|
||||
import {MdOption} from '../core';
|
||||
import {MdOption, MdOptgroup} from '../core';
|
||||
import {ActiveDescendantKeyManager} from '@angular/cdk/a11y';
|
||||
|
||||
/**
|
||||
@@ -55,7 +55,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;
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
Optional,
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
QueryList,
|
||||
} from '@angular/core';
|
||||
import {ENTER, SPACE} from '../keyboard/keycodes';
|
||||
import {coerceBooleanProperty} from '@angular/cdk/coercion';
|
||||
@@ -216,4 +217,30 @@ export class MdOption {
|
||||
this.onSelectionChange.emit(new MdOptionSelectionChange(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<MdOption>,
|
||||
optionGroups: QueryList<MdOptgroup>): 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++;
|
||||
}
|
||||
}
|
||||
|
||||
return groupCounter;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -939,7 +939,8 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On
|
||||
if (this._hasValue()) {
|
||||
let selectedOptionOffset = this._getOptionIndex(this._selectionModel.selected[0])!;
|
||||
|
||||
selectedOptionOffset += this._getLabelCountBeforeOption(selectedOptionOffset);
|
||||
selectedOptionOffset += MdOption.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.
|
||||
@@ -951,8 +952,10 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On
|
||||
// we must only adjust for the height difference between the option element
|
||||
// and the trigger element, then multiply it by -1 to ensure the panel moves
|
||||
// in the correct direction up the page.
|
||||
let groupLabels = MdOption.countGroupLabelsBeforeOption(0, this.options, this.optionGroups);
|
||||
|
||||
this._offsetY = (SELECT_ITEM_HEIGHT - SELECT_TRIGGER_HEIGHT) / 2 * -1 -
|
||||
(this._getLabelCountBeforeOption(0) * SELECT_ITEM_HEIGHT);
|
||||
(groupLabels * SELECT_ITEM_HEIGHT);
|
||||
}
|
||||
|
||||
this._checkOverlayWithinViewport(maxScroll);
|
||||
@@ -1197,30 +1200,6 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On
|
||||
private _getItemCount(): number {
|
||||
return this.options.length + this.optionGroups.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the amount of option group labels that precede the specified option.
|
||||
* Useful when positioning the panel, because the labels will offset the index of the
|
||||
* currently-selected option.
|
||||
*/
|
||||
private _getLabelCountBeforeOption(optionIndex: number): number {
|
||||
if (this.optionGroups.length) {
|
||||
let options = this.options.toArray();
|
||||
let groups = this.optionGroups.toArray();
|
||||
let groupCounter = 0;
|
||||
|
||||
for (let i = 0; i < optionIndex + 1; i++) {
|
||||
if (options[i].group && options[i].group === groups[groupCounter]) {
|
||||
groupCounter++;
|
||||
}
|
||||
}
|
||||
|
||||
return groupCounter;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/** Clamps a value n between min and max values. */
|
||||
|
||||
Reference in New Issue
Block a user