diff --git a/src/lib/core/theming/_palette.scss b/src/lib/core/theming/_palette.scss
index 4ac830a085bd..6785050550bd 100644
--- a/src/lib/core/theming/_palette.scss
+++ b/src/lib/core/theming/_palette.scss
@@ -660,6 +660,7 @@ $mat-light-theme-background: (
selected-disabled-button: map_get($mat-grey, 400),
disabled-button-toggle: map_get($mat-grey, 200),
unselected-chip: map_get($mat-grey, 300),
+ disabled-list-option: map_get($mat-grey, 200),
);
// Background palette for dark themes.
@@ -675,8 +676,9 @@ $mat-dark-theme-background: (
focused-button: $white-6-opacity,
selected-button: map_get($mat-grey, 900),
selected-disabled-button: map_get($mat-grey, 800),
- disabled-button-toggle: map_get($mat-grey, 1000),
+ disabled-button-toggle: black,
unselected-chip: map_get($mat-grey, 700),
+ disabled-list-option: black,
);
// Foreground palette for light themes.
diff --git a/src/lib/list/_list-theme.scss b/src/lib/list/_list-theme.scss
index 2355a23d5c80..9ff7714ddc03 100644
--- a/src/lib/list/_list-theme.scss
+++ b/src/lib/list/_list-theme.scss
@@ -8,16 +8,24 @@
$background: map-get($theme, background);
$foreground: map-get($theme, foreground);
- .mat-list, .mat-nav-list {
+ .mat-list, .mat-nav-list, .mat-selection-list {
.mat-list-item {
color: mat-color($foreground, text);
}
+ .mat-list-option {
+ color: mat-color($foreground, text);
+ }
+
.mat-subheader {
color: mat-color($foreground, secondary-text);
}
}
+ .mat-list-item-disabled {
+ background-color: mat-color($background, disabled-list-option);
+ }
+
.mat-divider {
border-top-color: mat-color($foreground, divider);
}
@@ -29,6 +37,14 @@
background: mat-color($background, 'hover');
}
}
+
+ .mat-list-option {
+ outline: none;
+
+ &:hover, &.mat-list-item-focus {
+ background: mat-color($background, 'hover');
+ }
+ }
}
@mixin mat-list-typography($config) {
@@ -38,13 +54,22 @@
font-family: $font-family;
}
+ .mat-list-option {
+ font-family: $font-family;
+ }
+
// Default list
- .mat-list, .mat-nav-list {
+ .mat-list, .mat-nav-list, .mat-selection-list {
.mat-list-item {
font-size: mat-font-size($config, subheading-2);
@include mat-line-base(mat-font-size($config, body-1));
}
+ .mat-list-option {
+ font-size: mat-font-size($config, subheading-2);
+ @include mat-line-base(mat-font-size($config, body-1));
+ }
+
.mat-subheader {
font-family: mat-font-family($config, body-2);
font-size: mat-font-size($config, body-2);
@@ -53,12 +78,17 @@
}
// Dense list
- .mat-list[dense], .mat-nav-list[dense] {
+ .mat-list[dense], .mat-nav-list[dense], .mat-selection-list[dense] {
.mat-list-item {
font-size: mat-font-size($config, caption);
@include mat-line-base(mat-font-size($config, caption));
}
+ .mat-list-option {
+ font-size: mat-font-size($config, caption);
+ @include mat-line-base(mat-font-size($config, caption));
+ }
+
.mat-subheader {
font-family: $font-family;
font-size: mat-font-size($config, caption);
diff --git a/src/lib/list/index.ts b/src/lib/list/index.ts
index 986b20a1f23a..d5960fe69a19 100644
--- a/src/lib/list/index.ts
+++ b/src/lib/list/index.ts
@@ -7,7 +7,8 @@
*/
import {NgModule} from '@angular/core';
-import {MdLineModule, MdRippleModule, MdCommonModule} from '../core';
+import {MdLineModule, MdRippleModule, MdCommonModule, MdSelectionModule} from '../core';
+import {CommonModule} from '@angular/common';
import {
MdList,
MdListItem,
@@ -17,12 +18,13 @@ import {
MdListCssMatStyler,
MdNavListCssMatStyler,
MdDividerCssMatStyler,
- MdListSubheaderCssMatStyler,
+ MdListSubheaderCssMatStyler
} from './list';
+import {MdSelectionList, MdListOption} from './selection-list';
@NgModule({
- imports: [MdLineModule, MdRippleModule, MdCommonModule],
+ imports: [MdLineModule, MdRippleModule, MdCommonModule, MdSelectionModule, CommonModule],
exports: [
MdList,
MdListItem,
@@ -35,6 +37,9 @@ import {
MdNavListCssMatStyler,
MdDividerCssMatStyler,
MdListSubheaderCssMatStyler,
+ MdSelectionModule,
+ MdSelectionList,
+ MdListOption
],
declarations: [
MdList,
@@ -46,9 +51,12 @@ import {
MdNavListCssMatStyler,
MdDividerCssMatStyler,
MdListSubheaderCssMatStyler,
+ MdSelectionList,
+ MdListOption
],
})
export class MdListModule {}
export * from './list';
+export * from './selection-list';
diff --git a/src/lib/list/list-option.html b/src/lib/list/list-option.html
new file mode 100644
index 000000000000..638a472d7562
--- /dev/null
+++ b/src/lib/list/list-option.html
@@ -0,0 +1,10 @@
+
diff --git a/src/lib/list/list.scss b/src/lib/list/list.scss
index 7f91e6596895..850275c6dff5 100644
--- a/src/lib/list/list.scss
+++ b/src/lib/list/list.scss
@@ -43,6 +43,14 @@ $mat-dense-list-icon-size: 20px;
position: relative;
}
+ .mat-list-item-content-reverse {
+ display: flex;
+ align-items: center;
+ padding: 0 $mat-list-side-padding;
+ flex-direction: row-reverse;
+ justify-content: space-around;
+ }
+
.mat-list-item-ripple {
position: absolute;
left: 0;
@@ -128,7 +136,7 @@ $mat-dense-list-icon-size: 20px;
}
}
-.mat-list, .mat-nav-list {
+.mat-list, .mat-nav-list, .mat-selection-list {
padding-top: $mat-list-top-padding;
display: block;
@@ -145,10 +153,21 @@ $mat-dense-list-icon-size: 20px;
$mat-list-icon-size
);
}
+
+ .mat-list-option {
+ @include mat-list-item-base(
+ $mat-list-base-height,
+ $mat-list-avatar-height,
+ $mat-list-two-line-height,
+ $mat-list-three-line-height,
+ $mat-list-icon-size
+ );
+ }
}
-.mat-list[dense], .mat-nav-list[dense] {
+.mat-list[dense], .mat-nav-list[dense], .mat-selection-list[dense] {
+
padding-top: $mat-dense-top-padding;
display: block;
@@ -165,6 +184,16 @@ $mat-dense-list-icon-size: 20px;
$mat-dense-list-icon-size
);
}
+
+ .mat-list-option {
+ @include mat-list-item-base(
+ $mat-dense-base-height,
+ $mat-dense-avatar-height,
+ $mat-dense-two-line-height,
+ $mat-dense-three-line-height,
+ $mat-dense-list-icon-size
+ );
+ }
}
.mat-divider {
diff --git a/src/lib/list/list.ts b/src/lib/list/list.ts
index 95ddd64788a8..a78156d71e34 100644
--- a/src/lib/list/list.ts
+++ b/src/lib/list/list.ts
@@ -32,6 +32,7 @@ export const _MdListMixinBase = mixinDisableRipple(MdListBase);
export class MdListItemBase {}
export const _MdListItemMixinBase = mixinDisableRipple(MdListItemBase);
+
@Directive({
selector: 'md-divider, mat-divider',
host: {
diff --git a/src/lib/list/selection-list.spec.ts b/src/lib/list/selection-list.spec.ts
new file mode 100644
index 000000000000..926913c6e3c7
--- /dev/null
+++ b/src/lib/list/selection-list.spec.ts
@@ -0,0 +1,373 @@
+import {async, TestBed, ComponentFixture, inject} from '@angular/core/testing';
+import {Component, DebugElement} from '@angular/core';
+import {By} from '@angular/platform-browser';
+import {MdSelectionList, MdListOption, MdListModule} from './index';
+import {createKeyboardEvent} from '@angular/cdk/testing';
+import {UP_ARROW, DOWN_ARROW, SPACE} from '../core/keyboard/keycodes';
+import {Platform} from '../core/platform/index';
+
+
+describe('MdSelectionList', () => {
+ describe('with list option', () => {
+ let fixture: ComponentFixture;
+ let listOption: DebugElement[];
+ let listItemEl: DebugElement;
+ let selectionList: DebugElement;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [MdListModule],
+ declarations: [
+ SelectionListWithListOptions,
+ SelectionListWithCheckboxPositionAfter,
+ SelectionListWithListDisabled,
+ SelectionListWithOnlyOneOption
+ ],
+ });
+
+ TestBed.compileComponents();
+ }));
+
+ beforeEach(async(() => {
+ fixture = TestBed.createComponent(SelectionListWithListOptions);
+ listOption = fixture.debugElement.queryAll(By.directive(MdListOption));
+ listItemEl = fixture.debugElement.query(By.css('.mat-list-item'));
+ selectionList = fixture.debugElement.query(By.directive(MdSelectionList));
+ fixture.detectChanges();
+ }));
+
+ it('should add and remove focus class on focus/blur', () => {
+ expect(listItemEl.nativeElement.classList).not.toContain('mat-list-item-focus');
+
+ listOption[0].componentInstance._handleFocus();
+ fixture.detectChanges();
+ expect(listItemEl.nativeElement.className).toContain('mat-list-item-focus');
+
+ listOption[0].componentInstance._handleBlur();
+ fixture.detectChanges();
+ expect(listItemEl.nativeElement.className).not.toContain('mat-list-item-focus');
+ });
+
+ it('should be able to dispatch one selected item', () => {
+ let testListItem = listOption[2].injector.get(MdListOption);
+ let selectList = selectionList.injector.get(MdSelectionList).selectedOptions;
+
+ expect(selectList.selected.length).toBe(0);
+ expect(listOption[2].nativeElement.getAttribute('aria-selected')).toBe('false');
+
+ testListItem.toggle();
+ fixture.detectChanges();
+
+ expect(listOption[2].nativeElement.getAttribute('aria-selected')).toBe('true');
+ expect(listOption[2].nativeElement.getAttribute('aria-disabled')).toBe('false');
+ expect(selectList.selected.length).toBe(1);
+ });
+
+ it('should be able to dispatch multiple selected items', () => {
+ let testListItem = listOption[2].injector.get(MdListOption);
+ let testListItem2 = listOption[1].injector.get(MdListOption);
+ let selectList = selectionList.injector.get(MdSelectionList).selectedOptions;
+
+ expect(selectList.selected.length).toBe(0);
+ expect(listOption[2].nativeElement.getAttribute('aria-selected')).toBe('false');
+ expect(listOption[1].nativeElement.getAttribute('aria-selected')).toBe('false');
+
+ testListItem.toggle();
+ fixture.detectChanges();
+
+ testListItem2.toggle();
+ fixture.detectChanges();
+
+ expect(selectList.selected.length).toBe(2);
+ expect(listOption[2].nativeElement.getAttribute('aria-selected')).toBe('true');
+ expect(listOption[1].nativeElement.getAttribute('aria-selected')).toBe('true');
+ expect(listOption[1].nativeElement.getAttribute('aria-disabled')).toBe('false');
+ expect(listOption[2].nativeElement.getAttribute('aria-disabled')).toBe('false');
+ });
+
+ it('should be able to deselect an option', () => {
+ let testListItem = listOption[2].injector.get(MdListOption);
+ let selectList = selectionList.injector.get(MdSelectionList).selectedOptions;
+
+ expect(selectList.selected.length).toBe(0);
+
+ testListItem.toggle();
+ fixture.detectChanges();
+
+ expect(selectList.selected.length).toBe(1);
+
+ testListItem.toggle();
+ fixture.detectChanges();
+
+ expect(selectList.selected.length).toBe(0);
+ });
+
+ it('should not allow selection of disabled items', () => {
+ let testListItem = listOption[0].injector.get(MdListOption);
+ let selectList = selectionList.injector.get(MdSelectionList).selectedOptions;
+
+ expect(selectList.selected.length).toBe(0);
+ expect(listOption[0].nativeElement.getAttribute('aria-disabled')).toBe('true');
+
+ testListItem._handleClick();
+ fixture.detectChanges();
+
+ expect(selectList.selected.length).toBe(0);
+ });
+
+ it('should be able to un-disable disabled items', () => {
+ let testListItem = listOption[0].injector.get(MdListOption);
+
+ expect(listOption[0].nativeElement.getAttribute('aria-disabled')).toBe('true');
+
+ testListItem.disabled = false;
+ fixture.detectChanges();
+
+ expect(listOption[0].nativeElement.getAttribute('aria-disabled')).toBe('false');
+ });
+
+ it('should be able to use keyboard select with SPACE', () => {
+ let testListItem = listOption[1].nativeElement as HTMLElement;
+ let SPACE_EVENT: KeyboardEvent =
+ createKeyboardEvent('keydown', SPACE, testListItem);
+ let selectList = selectionList.injector.get(MdSelectionList).selectedOptions;
+ let options = selectionList.componentInstance.options;
+ let array = options.toArray();
+ let focusItem = array[1];
+ expect(selectList.selected.length).toBe(0);
+
+ focusItem.focus();
+ selectionList.componentInstance._keydown(SPACE_EVENT);
+
+ fixture.detectChanges();
+
+ expect(selectList.selected.length).toBe(1);
+ });
+
+ it('should focus previous item when press UP ARROW', () => {
+ let testListItem = listOption[2].nativeElement as HTMLElement;
+ let UP_EVENT: KeyboardEvent =
+ createKeyboardEvent('keydown', UP_ARROW, testListItem);
+ let options = selectionList.componentInstance.options;
+ let array = options.toArray();
+ let focusItem = array[2];
+ let manager = selectionList.componentInstance._keyManager;
+
+ focusItem.focus();
+ expect(manager.activeItemIndex).toEqual(2);
+
+ selectionList.componentInstance._keydown(UP_EVENT);
+
+ fixture.detectChanges();
+
+ expect(manager.activeItemIndex).toEqual(1);
+ });
+
+ it('should focus next item when press DOWN ARROW', () => {
+ let testListItem = listOption[2].nativeElement as HTMLElement;
+ let DOWN_EVENT: KeyboardEvent =
+ createKeyboardEvent('keydown', DOWN_ARROW, testListItem);
+ let options = selectionList.componentInstance.options;
+ let array = options.toArray();
+ let focusItem = array[2];
+ let manager = selectionList.componentInstance._keyManager;
+
+ focusItem.focus();
+ expect(manager.activeItemIndex).toEqual(2);
+
+ selectionList.componentInstance._keydown(DOWN_EVENT);
+
+ fixture.detectChanges();
+
+ expect(manager.activeItemIndex).toEqual(3);
+ });
+ });
+
+ describe('with single option', () => {
+ let fixture: ComponentFixture;
+ let listOption: DebugElement;
+ let listItemEl: DebugElement;
+ let selectionList: DebugElement;
+ let platform: Platform;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [MdListModule],
+ declarations: [
+ SelectionListWithListOptions,
+ SelectionListWithCheckboxPositionAfter,
+ SelectionListWithListDisabled,
+ SelectionListWithOnlyOneOption
+ ],
+ });
+
+ TestBed.compileComponents();
+ }));
+
+ beforeEach(async(() => {
+ fixture = TestBed.createComponent(SelectionListWithOnlyOneOption);
+ listOption = fixture.debugElement.query(By.directive(MdListOption));
+ listItemEl = fixture.debugElement.query(By.css('.mat-list-item'));
+ selectionList = fixture.debugElement.query(By.directive(MdSelectionList));
+ fixture.detectChanges();
+ }));
+
+ beforeEach(inject([Platform], (p: Platform) => {
+ platform = p;
+ }));
+
+ it('should be focused when focus on nativeElements', () => {
+ listOption.nativeElement.focus();
+ fixture.detectChanges();
+
+ expect(listItemEl.nativeElement).toBe(document.activeElement);
+ if (platform.SAFARI || platform.FIREFOX) {
+ expect(listItemEl.nativeElement.className).toContain('mat-list-item-focus');
+ }
+
+ listOption.nativeElement.blur();
+ fixture.detectChanges();
+
+ expect(listItemEl.nativeElement.className).not.toContain('mat-list-item-focus');
+ });
+ });
+
+ describe('with list disabled', () => {
+ let fixture: ComponentFixture;
+ let listOption: DebugElement[];
+ let listItemEl: DebugElement;
+ let selectionList: DebugElement;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [MdListModule],
+ declarations: [
+ SelectionListWithListOptions,
+ SelectionListWithCheckboxPositionAfter,
+ SelectionListWithListDisabled,
+ SelectionListWithOnlyOneOption
+ ],
+ });
+
+ TestBed.compileComponents();
+ }));
+
+ beforeEach(async(() => {
+ fixture = TestBed.createComponent(SelectionListWithListDisabled);
+ listOption = fixture.debugElement.queryAll(By.directive(MdListOption));
+ listItemEl = fixture.debugElement.query(By.css('.mat-list-item'));
+ selectionList = fixture.debugElement.query(By.directive(MdSelectionList));
+ fixture.detectChanges();
+ }));
+
+ it('should not allow selection on disabled selection-list', () => {
+ let testListItem = listOption[2].injector.get(MdListOption);
+ let selectList = selectionList.injector.get(MdSelectionList).selectedOptions;
+
+ expect(selectList.selected.length).toBe(0);
+
+ testListItem._handleClick();
+ fixture.detectChanges();
+
+ expect(selectList.selected.length).toBe(0);
+ });
+ });
+
+ describe('with checkbox position after', () => {
+ let fixture: ComponentFixture;
+ let listOption: DebugElement[];
+ let listItemEl: DebugElement;
+ let selectionList: DebugElement;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [MdListModule],
+ declarations: [
+ SelectionListWithListOptions,
+ SelectionListWithCheckboxPositionAfter,
+ SelectionListWithListDisabled,
+ SelectionListWithOnlyOneOption
+ ],
+ });
+
+ TestBed.compileComponents();
+ }));
+
+ beforeEach(async(() => {
+ fixture = TestBed.createComponent(SelectionListWithCheckboxPositionAfter);
+ listOption = fixture.debugElement.queryAll(By.directive(MdListOption));
+ listItemEl = fixture.debugElement.query(By.css('.mat-list-item'));
+ selectionList = fixture.debugElement.query(By.directive(MdSelectionList));
+ fixture.detectChanges();
+ }));
+
+ it('should be able to customize checkbox position', () => {
+ let listItemContent = fixture.debugElement.query(By.css('.mat-list-item-content'));
+ expect(listItemContent.nativeElement.classList).toContain('mat-list-item-content-reverse');
+ });
+ });
+});
+
+
+@Component({template: `
+
+
+ Inbox (disabled selection-option)
+
+
+ Starred
+
+
+ Sent Mail
+
+
+ Drafts
+
+ `})
+class SelectionListWithListOptions {
+}
+
+@Component({template: `
+
+
+ Inbox (disabled selection-option)
+
+
+ Starred
+
+
+ Sent Mail
+
+
+ Drafts
+
+ `})
+class SelectionListWithCheckboxPositionAfter {
+}
+
+@Component({template: `
+
+
+ Inbox (disabled selection-option)
+
+
+ Starred
+
+
+ Sent Mail
+
+
+ Drafts
+
+ `})
+class SelectionListWithListDisabled {
+}
+
+@Component({template: `
+
+
+ Inbox
+
+ `})
+class SelectionListWithOnlyOneOption {
+}
diff --git a/src/lib/list/selection-list.ts b/src/lib/list/selection-list.ts
new file mode 100644
index 000000000000..1eda2a70c32c
--- /dev/null
+++ b/src/lib/list/selection-list.ts
@@ -0,0 +1,321 @@
+/**
+ * @license
+ * Copyright Google Inc. All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import {
+ AfterContentInit,
+ Component,
+ ContentChildren,
+ ElementRef,
+ Input,
+ QueryList,
+ ViewEncapsulation,
+ Optional,
+ Renderer2,
+ EventEmitter,
+ Output,
+ ChangeDetectionStrategy,
+ ChangeDetectorRef,
+ OnDestroy,
+ forwardRef,
+ Inject,
+} from '@angular/core';
+import {coerceBooleanProperty, SelectionModel, MdLine, MdLineSetter} from '../core';
+import {FocusKeyManager} from '../core/a11y/focus-key-manager';
+import {Subscription} from 'rxjs/Subscription';
+import {SPACE} from '../core/keyboard/keycodes';
+import {FocusableOption} from '../core/a11y/focus-key-manager';
+import {CanDisable, mixinDisabled} from '../core/common-behaviors/disabled';
+import {RxChain, switchMap, startWith} from '../core/rxjs/index';
+import {merge} from 'rxjs/observable/merge';
+
+export class MdSelectionListBase {}
+export const _MdSelectionListMixinBase = mixinDisabled(MdSelectionListBase);
+
+
+export interface MdSelectionListOptionEvent {
+ option: MdListOption;
+}
+
+const FOCUSED_STYLE: string = 'mat-list-item-focus';
+
+/**
+ * Component for list-options of selection-list. Each list-option can automatically
+ * generate a checkbox and can put current item into the selectionModel of selection-list
+ * if the current item is checked.
+ */
+@Component({
+ moduleId: module.id,
+ selector: 'md-list-option, mat-list-option',
+ host: {
+ 'role': 'option',
+ 'class': 'mat-list-item mat-list-option',
+ '(focus)': '_handleFocus()',
+ '(blur)': '_handleBlur()',
+ '(click)': '_handleClick()',
+ 'tabindex': '-1',
+ '[attr.aria-selected]': 'selected.toString()',
+ '[attr.aria-disabled]': 'disabled.toString()',
+ },
+ templateUrl: 'list-option.html',
+ encapsulation: ViewEncapsulation.None,
+ changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class MdListOption implements AfterContentInit, OnDestroy, FocusableOption {
+ private _lineSetter: MdLineSetter;
+ private _disableRipple: boolean = false;
+ private _selected: boolean = false;
+ /** Whether the checkbox is disabled. */
+ private _disabled: boolean = false;
+ private _value: any;
+
+ /** Whether the option has focus. */
+ _hasFocus: boolean = false;
+
+ /**
+ * Whether the ripple effect on click should be disabled. This applies only to list items that are
+ * part of a selection list. The value of `disableRipple` on the `md-selection-list` overrides
+ * this flag
+ */
+ @Input()
+ get disableRipple() { return this._disableRipple; }
+ set disableRipple(value: boolean) { this._disableRipple = coerceBooleanProperty(value); }
+
+ @ContentChildren(MdLine) _lines: QueryList;
+
+ /** Whether the label should appear before or after the checkbox. Defaults to 'after' */
+ @Input() checkboxPosition: 'before' | 'after' = 'after';
+
+ /** Whether the option is disabled. */
+ @Input()
+ get disabled() { return (this.selectionList && this.selectionList.disabled) || this._disabled; }
+ set disabled(value: any) { this._disabled = coerceBooleanProperty(value); }
+
+ @Input()
+ get value() { return this._value; }
+ set value( val: any) { this._value = coerceBooleanProperty(val); }
+
+ @Input()
+ get selected() { return this._selected; }
+ set selected( val: boolean) { this._selected = coerceBooleanProperty(val); }
+
+ /** Emitted when the option is focused. */
+ onFocus = new EventEmitter();
+
+ /** Emitted when the option is selected. */
+ @Output() selectChange = new EventEmitter();
+
+ /** Emitted when the option is deselected. */
+ @Output() deselected = new EventEmitter();
+
+ /** Emitted when the option is destroyed. */
+ @Output() destroyed = new EventEmitter();
+
+ constructor(private _renderer: Renderer2,
+ private _element: ElementRef,
+ private _changeDetector: ChangeDetectorRef,
+ @Optional() @Inject(forwardRef(() => MdSelectionList)) public selectionList: MdSelectionList) { }
+
+
+ ngAfterContentInit() {
+ this._lineSetter = new MdLineSetter(this._lines, this._renderer, this._element);
+
+ if (this.selectionList.disabled) {
+ this.disabled = true;
+ }
+ }
+
+ ngOnDestroy(): void {
+ this.destroyed.emit({option: this});
+ }
+
+ toggle(): void {
+ this.selected = !this.selected;
+ this.selectionList.selectedOptions.toggle(this);
+ this._changeDetector.markForCheck();
+ }
+
+ /** Allows for programmatic focusing of the option. */
+ focus(): void {
+ this._element.nativeElement.focus();
+ this.onFocus.emit({option: this});
+ }
+
+ /** Whether this list item should show a ripple effect when clicked. */
+ isRippleEnabled() {
+ return !this.disableRipple && !this.selectionList.disableRipple;
+ }
+
+ _handleClick() {
+ if (!this.disabled) {
+ this.toggle();
+ }
+ }
+
+ _handleFocus() {
+ this._hasFocus = true;
+ this._renderer.addClass(this._element.nativeElement, FOCUSED_STYLE);
+ }
+
+ _handleBlur() {
+ this._renderer.removeClass(this._element.nativeElement, FOCUSED_STYLE);
+ }
+
+ /** Retrieves the DOM element of the component host. */
+ _getHostElement(): HTMLElement {
+ return this._element.nativeElement;
+ }
+}
+
+
+@Component({
+ moduleId: module.id,
+ selector: 'md-selection-list, mat-selection-list',
+ inputs: ['disabled'],
+ host: {
+ 'role': 'listbox',
+ '[attr.tabindex]': '_tabIndex',
+ 'class': 'mat-selection-list',
+ '(focus)': 'focus()',
+ '(keydown)': '_keydown($event)',
+ '[attr.aria-disabled]': 'disabled.toString()'},
+ template: '',
+ styleUrls: ['list.css'],
+ encapsulation: ViewEncapsulation.None,
+ changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class MdSelectionList extends _MdSelectionListMixinBase
+ implements FocusableOption, CanDisable, AfterContentInit, OnDestroy {
+ private _disableRipple: boolean = false;
+
+ /** Tab index for the selection-list. */
+ _tabIndex = 0;
+
+ /** Subscription to all list options' onFocus events */
+ private _optionFocusSubscription: Subscription;
+
+ /** Subscription to all list options' destroy events */
+ private _optionDestroyStream: Subscription;
+
+ /** The FocusKeyManager which handles focus. */
+ _keyManager: FocusKeyManager;
+
+ /** The option components contained within this selection-list. */
+ @ContentChildren(MdListOption) options;
+
+ /** options which are selected. */
+ selectedOptions: SelectionModel = new SelectionModel(true);
+
+ /**
+ * Whether the ripple effect should be disabled on the list-items or not.
+ * This flag only has an effect for `mat-selection-list` components.
+ */
+ @Input()
+ get disableRipple() { return this._disableRipple; }
+ set disableRipple(value: boolean) { this._disableRipple = coerceBooleanProperty(value); }
+
+ constructor(private _element: ElementRef) {
+ super();
+ }
+
+ ngAfterContentInit(): void {
+ this._keyManager = new FocusKeyManager(this.options).withWrap();
+
+ if (this.disabled) {
+ this._tabIndex = -1;
+ }
+
+ this._optionFocusSubscription = this._onFocusSubscription();
+ this._optionDestroyStream = this._onDestroySubscription();
+ }
+
+ ngOnDestroy(): void {
+ if (this._optionDestroyStream) {
+ this._optionDestroyStream.unsubscribe();
+ }
+
+ if (this._optionFocusSubscription) {
+ this._optionFocusSubscription.unsubscribe();
+ }
+ }
+
+ focus() {
+ this._element.nativeElement.focus();
+ }
+
+ /**
+ * Map all the options' destroy event subscriptions and merge them into one stream.
+ */
+ private _onDestroySubscription(): Subscription {
+ return RxChain.from(this.options.changes)
+ .call(startWith, this.options)
+ .call(switchMap, (options: MdListOption[]) => {
+ return merge(...options.map(option => option.destroyed));
+ }).subscribe((e: MdSelectionListOptionEvent) => {
+ let optionIndex: number = this.options.toArray().indexOf(e.option);
+ if (e.option._hasFocus) {
+ // Check whether the option is the last item
+ if (optionIndex < this.options.length - 1) {
+ this._keyManager.setActiveItem(optionIndex);
+ } else if (optionIndex - 1 >= 0) {
+ this._keyManager.setActiveItem(optionIndex - 1);
+ }
+ }
+ e.option.destroyed.unsubscribe();
+ });
+ }
+
+ /**
+ * Map all the options' onFocus event subscriptions and merge them into one stream.
+ */
+ private _onFocusSubscription(): Subscription {
+ return RxChain.from(this.options.changes)
+ .call(startWith, this.options)
+ .call(switchMap, (options: MdListOption[]) => {
+ return merge(...options.map(option => option.onFocus));
+ }).subscribe((e: MdSelectionListOptionEvent) => {
+ let optionIndex: number = this.options.toArray().indexOf(e.option);
+ this._keyManager.updateActiveItemIndex(optionIndex);
+ });
+ }
+
+ /** Passes relevant key presses to our key manager. */
+ _keydown(event: KeyboardEvent) {
+ switch (event.keyCode) {
+ case SPACE:
+ this._toggleSelectOnFocusedOption();
+ // Always prevent space from scrolling the page since the list has focus
+ event.preventDefault();
+ break;
+ default:
+ this._keyManager.onKeydown(event);
+ }
+ }
+
+ /** Toggles the selected state of the currently focused option. */
+ private _toggleSelectOnFocusedOption(): void {
+ let focusedIndex = this._keyManager.activeItemIndex;
+
+ if (focusedIndex != null && this._isValidIndex(focusedIndex)) {
+ let focusedOption: MdListOption = this.options.toArray()[focusedIndex];
+
+ if (focusedOption) {
+ focusedOption.toggle();
+ }
+ }
+ }
+
+ /**
+ * Utility to ensure all indexes are valid.
+ *
+ * @param index The index to be checked.
+ * @returns True if the index is valid for our list of options.
+ */
+ private _isValidIndex(index: number): boolean {
+ return index >= 0 && index < this.options.length;
+ }
+}