feat(select): basic selection and animations (#1647)

This commit is contained in:
Kara
2016-11-01 13:53:48 -07:00
committed by Hans
parent 3ee1b599de
commit b14bb72f8e
15 changed files with 871 additions and 20 deletions
+5 -1
View File
@@ -1 +1,5 @@
<md-select></md-select>
<div class="demo-select">
<md-select placeholder="Food">
<md-option *ngFor="let food of foods"> {{ food.viewValue }} </md-option>
</md-select>
</div>
+2
View File
@@ -0,0 +1,2 @@
.demo-select {
}
+5 -1
View File
@@ -8,5 +8,9 @@ import {Component} from '@angular/core';
styleUrls: ['select-demo.css'],
})
export class SelectDemo {
foods = [
{value: 'steak', viewValue: 'Steak'},
{value: 'pizza', viewValue: 'Pizza'},
{value: 'tacos', viewValue: 'Tacos'}
];
}
+4
View File
@@ -30,6 +30,10 @@ export class ListKeyManager {
return this._tabOut.asObservable();
}
get focusedItemIndex(): number {
return this._focusedItemIndex;
}
set focusedItemIndex(value: number) {
this._focusedItemIndex = value;
}
@@ -26,6 +26,7 @@ export const ELEMENTS_SELECTOR = `
md-list-item,
md-menu,
md-nav-list,
md-option,
md-placeholder,
md-progress-bar,
md-progress-circle,
+3
View File
@@ -0,0 +1,3 @@
## Work in progress!
The select is still a work in progress, so most features have not been implemented. Not ready for use!
+45
View File
@@ -2,5 +2,50 @@
@import '../core/theming/theming';
@mixin md-select-theme($theme) {
$foreground: map-get($theme, foreground);
$background: map-get($theme, background);
$primary: map-get($theme, primary);
.md-select-trigger {
color: md-color($foreground, hint-text);
border-bottom: 1px solid md-color($foreground, divider);
md-select:focus & {
border-bottom: 1px solid md-color($primary);
}
}
.md-select-placeholder {
md-select:focus & {
color: md-color($primary);
}
}
.md-select-arrow {
color: md-color($foreground, hint-text);
md-select:focus & {
color: md-color($primary);
}
}
.md-select-content {
background: md-color($background, card);
}
.md-select-value {
color: md-color($foreground, text);
}
md-option {
&:hover, &:focus {
background: md-color($background, hover);
}
&.md-selected {
background: md-color($background, hover);
color: md-color($primary);
}
}
}
+6 -2
View File
@@ -1,10 +1,14 @@
import {NgModule, ModuleWithProviders} from '@angular/core';
import {CommonModule} from '@angular/common';
import {MdSelect} from './select';
import {MdOption} from './option';
import {OverlayModule} from '../core/overlay/overlay-directives';
import {MdRippleModule} from '../core/ripple/ripple';
import {OVERLAY_PROVIDERS} from '../core/overlay/overlay';
export * from './select';
@NgModule({
imports: [],
imports: [CommonModule, OverlayModule, MdRippleModule],
exports: [MdSelect, MdOption],
declarations: [MdSelect, MdOption],
})
@@ -12,7 +16,7 @@ export class MdSelectModule {
static forRoot(): ModuleWithProviders {
return {
ngModule: MdSelectModule,
providers: []
providers: [OVERLAY_PROVIDERS]
};
}
}
+3
View File
@@ -0,0 +1,3 @@
<ng-content></ng-content>
<div class="md-option-ripple" md-ripple md-ripple-background-color="rgba(0,0,0,0)"
[md-ripple-trigger]="_getHostElement()"></div>
+68 -3
View File
@@ -1,10 +1,75 @@
import {Component, ViewEncapsulation} from '@angular/core';
import {
Component,
ElementRef,
EventEmitter,
Output,
Renderer,
ViewEncapsulation
} from '@angular/core';
import {ENTER, SPACE} from '../core/keyboard/keycodes';
@Component({
moduleId: module.id,
selector: 'md-option',
template: ``,
host: {
'role': 'option',
'tabindex': '0',
'[class.md-selected]': 'selected',
'[attr.aria-selected]': 'selected.toString()',
'(click)': 'select()',
'(keydown)': '_handleKeydown($event)'
},
templateUrl: 'option.html',
styleUrls: ['select.css'],
encapsulation: ViewEncapsulation.None
})
export class MdOption {}
export class MdOption {
private _selected = false;
/** Event emitted when the option is selected. */
@Output() onSelect = new EventEmitter();
constructor(private _element: ElementRef, private _renderer: Renderer) {}
/** Whether or not the option is currently selected. */
get selected(): boolean {
return this._selected;
}
/**
* The displayed value of the option. It is necessary to show the selected option in the
* select's trigger.
* TODO(kara): Add input property alternative for node envs.
*/
get viewValue(): string {
return this._getHostElement().textContent.trim();
}
/** Selects the option. */
select(): void {
this._selected = true;
this.onSelect.emit();
}
/** Deselects the option. */
deselect(): void {
this._selected = false;
}
/** Sets focus onto this option. */
focus(): void {
this._renderer.invokeElementMethod(this._getHostElement(), 'focus');
}
/** Ensures the option is selected when activated from the keyboard. */
_handleKeydown(event: KeyboardEvent): void {
if (event.keyCode === ENTER || event.keyCode === SPACE) {
this.select();
}
}
_getHostElement(): HTMLElement {
return this._element.nativeElement;
}
}
+79
View File
@@ -0,0 +1,79 @@
import {
animate,
AnimationEntryMetadata,
state,
style,
transition,
trigger,
} from '@angular/core';
/**
* The following are all the animations for the md-select component, with each
* const containing the metadata for one animation.
*
* The values below match the implementation of the Material 1 md-select animation.
*/
/**
* This animation shrinks the placeholder text to 75% of its normal size and translates
* it to either the top left corner (ltr) or top right corner (rtl) of the trigger,
* depending on the text direction of the application.
*/
export const transformPlaceholder: AnimationEntryMetadata = trigger('transformPlaceholder', [
state('normal', style({
transform: `translate3d(0, 0, 0) scale(1)`
})),
state('floating-ltr', style({
transform: `translate3d(-2px, -22px, 0) scale(0.75)`
})),
state('floating-rtl', style({
transform: `translate3d(2px, -22px, 0) scale(0.75)`
})),
transition('* => *', animate(`400ms cubic-bezier(0.25, 0.8, 0.25, 1)`))
]);
/**
* This animation transforms the select's overlay panel on and off the page.
*
* When the panel is attached to the DOM, it expands its width 32px, scales it up to
* 100% on the Y axis, fades in its border, and translates slightly up and to the
* side to ensure the option text correctly overlaps the trigger text.
*
* When the panel is removed from the DOM, it simply fades out linearly.
*/
export const transformPanel: AnimationEntryMetadata = trigger('transformPanel', [
state('showing-ltr', style({
opacity: 1,
width: 'calc(100% + 32px)',
transform: `translate3d(-16px, -9px, 0) scaleY(1)`
})),
state('showing-rtl', style({
opacity: 1,
width: 'calc(100% + 32px)',
transform: `translate3d(16px, -9px, 0) scaleY(1)`
})),
transition('void => *', [
style({
opacity: 0,
width: '100%',
transform: `translate3d(0, 0, 0) scaleY(0)`
}),
animate(`150ms cubic-bezier(0.55, 0, 0.55, 0.2)`)
]),
transition('* => void', [
animate('250ms linear', style({opacity: 0}))
])
]);
/**
* This animation fades in the background color and text content of the
* select's options. It is time delayed to occur 100ms after the overlay
* panel has transformed in.
*/
export const fadeInContent: AnimationEntryMetadata = trigger('fadeInContent', [
state('showing', style({opacity: 1})),
transition('void => showing', [
style({opacity: 0}),
animate(`150ms 100ms cubic-bezier(0.55, 0, 0.55, 0.2)`)
])
]);
+15 -1
View File
@@ -1 +1,15 @@
I'm a select!
<div class="md-select-trigger" overlay-origin (click)="toggle()" #origin="overlayOrigin" #trigger>
<span class="md-select-placeholder" [@transformPlaceholder]="_getPlaceholderState()"> {{ placeholder }} </span>
<span class="md-select-value" *ngIf="selected"> {{ selected?.viewValue }} </span>
<span class="md-select-arrow"></span>
</div>
<template connected-overlay [origin]="origin" [open]="panelOpen" hasBackdrop (backdropClick)="close()"
backdropClass="md-overlay-transparent-backdrop" [positions]="_positions" [width]="_getWidth()">
<div class="md-select-panel" [@transformPanel]="_getPanelState()" (@transformPanel.done)="_onPanelDone()"
(keydown)="_keyManager.onKeydown($event)">
<div class="md-select-content" [@fadeInContent]="'showing'">
<ng-content></ng-content>
</div>
</div>
</template>
+62
View File
@@ -0,0 +1,62 @@
@import '../core/style/menu-common';
$md-select-trigger-height: 30px !default;
$md-select-trigger-min-width: 112px !default;
$md-select-arrow-size: 5px !default;
md-select {
display: inline-block;
outline: none;
}
.md-select-trigger {
display: flex;
justify-content: space-between;
align-items: center;
height: $md-select-trigger-height;
min-width: $md-select-trigger-min-width;
cursor: pointer;
}
.md-select-placeholder {
padding: 0 2px;
transform-origin: left top;
[dir='rtl'] & {
transform-origin: right top;
}
}
.md-select-value {
position: absolute;
}
.md-select-arrow {
width: 0;
height: 0;
border-left: $md-select-arrow-size solid transparent;
border-right: $md-select-arrow-size solid transparent;
border-top: $md-select-arrow-size solid;
}
.md-select-panel {
@include md-menu-base();
padding-top: 0;
padding-bottom: 0;
transform-origin: top;
}
md-option {
@include md-menu-item-base();
position: relative;
cursor: pointer;
outline: none;
}
.md-option-ripple {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
+351 -9
View File
@@ -1,23 +1,365 @@
import {TestBed, async} from '@angular/core/testing';
import {Component} from '@angular/core';
import {TestBed, async, ComponentFixture} from '@angular/core/testing';
import {By} from '@angular/platform-browser';
import {Component, DebugElement, QueryList, ViewChild, ViewChildren} from '@angular/core';
import {MdSelectModule} from './index';
import {OverlayContainer} from '../core/overlay/overlay-container';
import {MdSelect} from './select';
import {MdOption} from './option';
import {Dir} from '../core/rtl/dir';
describe('MdSelect', () => {
let overlayContainerElement: HTMLElement;
let dir: {value: string};
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [MdSelectModule.forRoot()],
declarations: [TestSelect],
declarations: [BasicSelect],
providers: [
{provide: OverlayContainer, useFactory: () => {
overlayContainerElement = document.createElement('div');
return {getContainerElement: () => overlayContainerElement};
}},
{provide: Dir, useFactory: () => {
return dir = { value: 'ltr' };
}}
]
});
TestBed.compileComponents();
}));
it('should test', () => {
// let fixture = TestBed.createComponent(TestSelect);
expect(true).toBeTruthy();
describe('overlay panel', () => {
let fixture: ComponentFixture<BasicSelect>;
let trigger: HTMLElement;
beforeEach(() => {
fixture = TestBed.createComponent(BasicSelect);
fixture.detectChanges();
trigger = fixture.debugElement.query(By.css('.md-select-trigger')).nativeElement;
});
it('should open the panel when trigger is clicked', () => {
trigger.click();
fixture.detectChanges();
expect(fixture.componentInstance.select.panelOpen).toBe(true);
expect(overlayContainerElement.textContent).toContain('Steak');
expect(overlayContainerElement.textContent).toContain('Pizza');
expect(overlayContainerElement.textContent).toContain('Tacos');
});
it('should close the panel when an item is clicked', async(() => {
trigger.click();
fixture.detectChanges();
const option = overlayContainerElement.querySelector('md-option') as HTMLElement;
option.click();
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(overlayContainerElement.textContent).toEqual('');
expect(fixture.componentInstance.select.panelOpen).toBe(false);
});
}));
it('should close the panel when a click occurs outside the panel', async(() => {
trigger.click();
fixture.detectChanges();
const backdrop = overlayContainerElement.querySelector('.md-overlay-backdrop') as HTMLElement;
backdrop.click();
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(overlayContainerElement.textContent).toEqual('');
expect(fixture.componentInstance.select.panelOpen).toBe(false);
});
}));
it('should set the width of the overlay based on the trigger', () => {
trigger.style.width = '200px';
trigger.click();
fixture.detectChanges();
const pane = overlayContainerElement.children[0] as HTMLElement;
expect(pane.style.width).toBe('200px');
});
});
describe('selection logic', () => {
let fixture: ComponentFixture<BasicSelect>;
let trigger: HTMLElement;
beforeEach(() => {
fixture = TestBed.createComponent(BasicSelect);
fixture.detectChanges();
trigger = fixture.debugElement.query(By.css('.md-select-trigger')).nativeElement;
});
it('should display placeholder if no option is selected', () => {
expect(trigger.textContent.trim()).toEqual('Food');
});
it('should focus the first option if no option is selected', async(() => {
trigger.click();
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(fixture.componentInstance.select._keyManager.focusedItemIndex).toEqual(0);
});
}));
it('should select an option when it is clicked', () => {
trigger.click();
fixture.detectChanges();
let option = overlayContainerElement.querySelector('md-option') as HTMLElement;
option.click();
fixture.detectChanges();
trigger.click();
fixture.detectChanges();
option = overlayContainerElement.querySelector('md-option') as HTMLElement;
expect(option.classList).toContain('md-selected');
expect(fixture.componentInstance.options.first.selected).toBe(true);
expect(fixture.componentInstance.select.selected)
.toBe(fixture.componentInstance.options.first);
});
it('should deselect other options when one is selected', () => {
trigger.click();
fixture.detectChanges();
let options =
overlayContainerElement.querySelectorAll('md-option') as NodeListOf<HTMLElement>;
options[0].click();
fixture.detectChanges();
trigger.click();
fixture.detectChanges();
options =
overlayContainerElement.querySelectorAll('md-option') as NodeListOf<HTMLElement>;
expect(options[1].classList).not.toContain('md-selected');
expect(options[2].classList).not.toContain('md-selected');
const optionInstances = fixture.componentInstance.options.toArray();
expect(optionInstances[1].selected).toBe(false);
expect(optionInstances[2].selected).toBe(false);
});
it('should display the selected option in the trigger', () => {
trigger.click();
fixture.detectChanges();
const option = overlayContainerElement.querySelector('md-option') as HTMLElement;
option.click();
fixture.detectChanges();
const value = fixture.debugElement.query(By.css('.md-select-value')).nativeElement;
const placeholder =
fixture.debugElement.query(By.css('.md-select-placeholder')).nativeElement;
expect(placeholder.textContent).toContain('Food');
expect(value.textContent).toContain('Steak');
expect(trigger.textContent).toContain('Food');
expect(trigger.textContent).toContain('Steak');
});
it('should focus the selected option if an option is selected', async(() => {
trigger.click();
fixture.detectChanges();
const options =
overlayContainerElement.querySelectorAll('md-option') as NodeListOf<HTMLElement>;
options[1].click();
fixture.detectChanges();
trigger.click();
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(fixture.componentInstance.select._keyManager.focusedItemIndex).toEqual(1);
});
}));
it('should select an option that was added after initialization', () => {
fixture.componentInstance.foods.push({viewValue: 'Pasta'});
trigger.click();
fixture.detectChanges();
const options =
overlayContainerElement.querySelectorAll('md-option') as NodeListOf<HTMLElement>;
options[3].click();
fixture.detectChanges();
expect(trigger.textContent).toContain('Pasta');
expect(fixture.componentInstance.select.selected)
.toBe(fixture.componentInstance.options.last);
});
});
describe('animations', () => {
let fixture: ComponentFixture<BasicSelect>;
let trigger: HTMLElement;
beforeEach(() => {
fixture = TestBed.createComponent(BasicSelect);
fixture.detectChanges();
trigger = fixture.debugElement.query(By.css('.md-select-trigger')).nativeElement;
});
it('should float the placeholder when the panel is open', () => {
expect(fixture.componentInstance.select._getPlaceholderState()).toEqual('normal');
trigger.click();
fixture.detectChanges();
expect(fixture.componentInstance.select._getPlaceholderState()).toEqual('floating-ltr');
const backdrop =
overlayContainerElement.querySelector('.md-overlay-backdrop') as HTMLElement;
backdrop.click();
fixture.detectChanges();
expect(fixture.componentInstance.select._getPlaceholderState()).toEqual('normal');
});
it('should float the placeholder when there is a selection', () => {
trigger.click();
fixture.detectChanges();
const option = overlayContainerElement.querySelector('md-option') as HTMLElement;
option.click();
fixture.detectChanges();
expect(fixture.componentInstance.select._getPlaceholderState()).toEqual('floating-ltr');
});
it('should use the floating-rtl state when the dir is rtl', () => {
dir.value = 'rtl';
trigger.click();
fixture.detectChanges();
expect(fixture.componentInstance.select._getPlaceholderState()).toEqual('floating-rtl');
});
it('should use the ltr panel state when the dir is ltr', () => {
dir.value = 'ltr';
trigger.click();
fixture.detectChanges();
expect(fixture.componentInstance.select._getPanelState()).toEqual('showing-ltr');
});
it('should use the rtl panel state when the dir is rtl', () => {
dir.value = 'rtl';
trigger.click();
fixture.detectChanges();
expect(fixture.componentInstance.select._getPanelState()).toEqual('showing-rtl');
});
});
describe('accessibility', () => {
let fixture: ComponentFixture<BasicSelect>;
beforeEach(() => {
fixture = TestBed.createComponent(BasicSelect);
fixture.detectChanges();
});
describe('for select', () => {
let select: DebugElement;
beforeEach(() => {
select = fixture.debugElement.query(By.css('md-select'));
});
it('should set the role of the select to listbox', () => {
expect(select.nativeElement.getAttribute('role')).toEqual('listbox');
});
it('should set the aria label of the select to the placeholder', () => {
expect(select.nativeElement.getAttribute('aria-label')).toEqual('Food');
});
it('should set the tabindex of the select to 0', () => {
expect(select.nativeElement.getAttribute('tabindex')).toEqual('0');
});
});
describe('for options', () => {
let trigger: HTMLElement;
beforeEach(() => {
trigger = fixture.debugElement.query(By.css('.md-select-trigger')).nativeElement;
trigger.click();
fixture.detectChanges();
});
it('should set the role of md-option to option', () => {
const option = overlayContainerElement.querySelector('md-option') as HTMLElement;
expect(option.getAttribute('role')).toEqual('option');
});
it('should set aria-selected on each option', () => {
const options =
overlayContainerElement.querySelectorAll('md-option') as NodeListOf<HTMLElement>;
expect(options[0].getAttribute('aria-selected')).toEqual('false');
expect(options[1].getAttribute('aria-selected')).toEqual('false');
expect(options[2].getAttribute('aria-selected')).toEqual('false');
options[1].click();
fixture.detectChanges();
trigger.click();
fixture.detectChanges();
expect(options[0].getAttribute('aria-selected')).toEqual('false');
expect(options[1].getAttribute('aria-selected')).toEqual('true');
expect(options[2].getAttribute('aria-selected')).toEqual('false');
});
it('should set the tabindex of each option to 0', () => {
const options =
overlayContainerElement.querySelectorAll('md-option') as NodeListOf<HTMLElement>;
expect(options[0].getAttribute('tabindex')).toEqual('0');
expect(options[1].getAttribute('tabindex')).toEqual('0');
expect(options[2].getAttribute('tabindex')).toEqual('0');
});
});
});
});
@Component({selector: 'test-select', template: ``})
class TestSelect {}
@Component({
selector: 'basic-select',
template: `
<md-select placeholder="Food">
<md-option *ngFor="let food of foods">{{ food.viewValue }}</md-option>
</md-select>
`
})
class BasicSelect {
foods = [
{ viewValue: 'Steak' },
{ viewValue: 'Pizza' },
{ viewValue: 'Tacos' },
];
@ViewChild(MdSelect) select: MdSelect;
@ViewChildren(MdOption) options: QueryList<MdOption>;
}
+222 -3
View File
@@ -1,10 +1,229 @@
import {Component, ViewEncapsulation} from '@angular/core';
import {
AfterContentInit,
Component,
ContentChildren,
ElementRef,
EventEmitter,
Input,
OnDestroy,
Optional,
Output,
QueryList,
Renderer,
ViewEncapsulation,
ViewChild
} from '@angular/core';
import {MdOption} from './option';
import {ENTER, SPACE} from '../core/keyboard/keycodes';
import {ListKeyManager} from '../core/a11y/list-key-manager';
import {Dir} from '../core/rtl/dir';
import {Subscription} from 'rxjs/Subscription';
import {transformPlaceholder, transformPanel, fadeInContent} from './select-animations';
@Component({
moduleId: module.id,
selector: 'md-select',
templateUrl: 'select.html',
styleUrls: ['select.css'],
encapsulation: ViewEncapsulation.None
encapsulation: ViewEncapsulation.None,
host: {
'role': 'listbox',
'tabindex': '0',
'[attr.aria-label]': 'placeholder',
'(keydown)': '_openOnActivate($event)'
},
animations: [
transformPlaceholder,
transformPanel,
fadeInContent
],
exportAs: 'mdSelect'
})
export class MdSelect {}
export class MdSelect implements AfterContentInit, OnDestroy {
/** Whether or not the overlay panel is open. */
private _panelOpen = false;
/** The currently selected option. */
private _selected: MdOption;
/** Subscriptions to option events. */
private _subscriptions: Subscription[] = [];
/** Subscription to changes in the option list. */
private _changeSubscription: Subscription;
/** Subscription to tab events while overlay is focused. */
private _tabSubscription: Subscription;
/** Manages keyboard events for options in the panel. */
_keyManager: ListKeyManager;
/** This position config ensures that the top left corner of the overlay
* is aligned with with the top left of the origin (overlapping the trigger
* completely). In RTL mode, the top right corners are aligned instead.
*/
_positions = [{
originX: 'start',
originY: 'top',
overlayX: 'start',
overlayY: 'top'
}];
@ViewChild('trigger') trigger: ElementRef;
@ContentChildren(MdOption) options: QueryList<MdOption>;
@Input() placeholder: string;
@Output() onOpen = new EventEmitter();
@Output() onClose = new EventEmitter();
constructor(private _element: ElementRef, private _renderer: Renderer,
@Optional() private _dir: Dir) {}
ngAfterContentInit() {
this._initKeyManager();
this._listenToOptions();
this._changeSubscription = this.options.changes.subscribe(() => {
this._dropSubscriptions();
this._listenToOptions();
});
}
ngOnDestroy() {
this._dropSubscriptions();
this._changeSubscription.unsubscribe();
this._tabSubscription.unsubscribe();
}
/** Toggles the overlay panel open or closed. */
toggle(): void {
this.panelOpen ? this.close() : this.open();
}
/** Opens the overlay panel. */
open(): void {
this._panelOpen = true;
}
/** Closes the overlay panel. */
close(): void {
this._panelOpen = false;
}
/** Whether or not the overlay panel is open. */
get panelOpen(): boolean {
return this._panelOpen;
}
/** The currently selected option. */
get selected(): MdOption {
return this._selected;
}
_isRtl(): boolean {
return this._dir ? this._dir.value === 'rtl' : false;
}
/** The width of the trigger element. This is necessary to match
* the overlay width to the trigger width.
*/
_getWidth(): number {
return this.trigger.nativeElement.getBoundingClientRect().width;
}
/** The animation state of the placeholder. */
_getPlaceholderState(): string {
if (this.panelOpen || this.selected) {
return this._isRtl() ? 'floating-rtl' : 'floating-ltr';
} else {
return 'normal';
}
}
/** The animation state of the overlay panel. */
_getPanelState(): string {
return this._isRtl() ? 'showing-rtl' : 'showing-ltr';
}
/** Ensures the panel opens if activated by the keyboard. */
_openOnActivate(event: KeyboardEvent): void {
if (event.keyCode === ENTER || event.keyCode === SPACE) {
this.open();
}
}
/** Emits an event and sets focus when overlay panel is finished animating. */
_onPanelDone(): void {
if (this.panelOpen) {
this._focusCorrectOption();
this.onOpen.emit();
} else {
this._focusHost();
this.onClose.emit();
}
}
/** Sets up a key manager to listen to keyboard events on the overlay panel. */
private _initKeyManager() {
this._keyManager = new ListKeyManager(this.options);
this._tabSubscription = this._keyManager.tabOut.subscribe(() => {
this.close();
});
}
/** Listens to selection events on each option. */
private _listenToOptions(): void {
this.options.forEach((option: MdOption) => {
const sub = option.onSelect.subscribe(() => this._onSelect(option));
this._subscriptions.push(sub);
});
}
/** Unsubscribes from all option subscriptions. */
private _dropSubscriptions(): void {
this._subscriptions.forEach((sub: Subscription) => sub.unsubscribe());
this._subscriptions = [];
}
/** When a new option is selected, deselects the others and closes the panel. */
private _onSelect(option: MdOption): void {
this._selected = option;
this._updateOptions();
this.close();
}
/** Deselect each option that doesn't match the current selection. */
private _updateOptions(): void {
this.options.forEach((option: MdOption) => {
if (option !== this.selected) {
option.deselect();
}
});
}
/** Focuses the selected item. If no option is selected, it will focus
* the first item instead.
*/
private _focusCorrectOption(): void {
if (this.selected) {
this._keyManager.focusedItemIndex = this._getOptionIndex(this.selected);
this.selected.focus();
} else {
this._keyManager.focusedItemIndex = 0;
this.options.first.focus();
}
}
/** Focuses the host element when the panel closes. */
private _focusHost(): void {
this._renderer.invokeElementMethod(this._element.nativeElement, 'focus');
}
/** Gets the index of the provided option in the option list. */
private _getOptionIndex(option: MdOption): number {
return this.options.reduce((result: number, current: MdOption, index: number) => {
return result === undefined ? (option === current ? index : undefined) : result;
}, undefined);
}
}