diff --git a/package-lock.json b/package-lock.json
index 304ff5aebfff..5c090d588a6a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -531,6 +531,26 @@
"through2": "2.0.3"
}
},
+ "@gulp-sourcemaps/identity-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@gulp-sourcemaps/identity-map/-/identity-map-1.0.1.tgz",
+ "integrity": "sha1-z6I7xYQPkQTOMqZedNt+epdLvuE=",
+ "dev": true,
+ "dependencies": {
+ "acorn": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.0.3.tgz",
+ "integrity": "sha1-xGDfCEkUY/AozLguqzcwvwEIez0=",
+ "dev": true
+ }
+ }
+ },
+ "@gulp-sourcemaps/map-sources": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@gulp-sourcemaps/map-sources/-/map-sources-1.0.0.tgz",
+ "integrity": "sha1-iQrnxdjId/bThIYCFazp1+yUW9o=",
+ "dev": true
+ },
"@types/chalk": {
"version": "0.4.31",
"resolved": "https://registry.npmjs.org/@types/chalk/-/chalk-0.4.31.tgz",
@@ -747,6 +767,11 @@
"integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=",
"dev": true
},
+ "angular": {
+ "version": "1.6.4",
+ "resolved": "https://registry.npmjs.org/angular/-/angular-1.6.4.tgz",
+ "integrity": "sha1-A7exXAGggC1+LPWTJA5gQFTcd/s="
+ },
"ansi-align": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-1.1.0.tgz",
@@ -1096,6 +1121,12 @@
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
"dev": true
},
+ "atob": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/atob/-/atob-1.1.3.tgz",
+ "integrity": "sha1-lfE2KbEsOlGl0hWr3OKqnzL4B3M=",
+ "dev": true
+ },
"autoprefixer": {
"version": "6.7.7",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-6.7.7.tgz",
@@ -2251,6 +2282,12 @@
"trim-off-newlines": "1.0.1"
}
},
+ "convert-source-map": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.0.tgz",
+ "integrity": "sha1-ms1whRxtXf3ZPZKC5e35SgP/RrU=",
+ "dev": true
+ },
"cookie": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
@@ -2437,6 +2474,20 @@
"uid-safe": "2.1.4"
}
},
+ "css": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/css/-/css-2.2.1.tgz",
+ "integrity": "sha1-c6TIHehdtmTU7mdPfUcIXjstVdw=",
+ "dev": true,
+ "dependencies": {
+ "source-map": {
+ "version": "0.1.43",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz",
+ "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=",
+ "dev": true
+ }
+ }
+ },
"css-color-names": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.3.tgz",
@@ -2711,6 +2762,20 @@
"ms": "2.0.0"
}
},
+ "debug-fabulous": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/debug-fabulous/-/debug-fabulous-0.1.0.tgz",
+ "integrity": "sha1-rQ6gel1RkyT7VYQqjzTuWcf4/2w=",
+ "dev": true,
+ "dependencies": {
+ "object-assign": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.0.tgz",
+ "integrity": "sha1-ejs9DpgGPUP0wD8uiubNUahog6A=",
+ "dev": true
+ }
+ }
+ },
"decamelize": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
@@ -2858,6 +2923,12 @@
"fs-exists-sync": "0.1.0"
}
},
+ "detect-newline": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz",
+ "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=",
+ "dev": true
+ },
"detective-amd": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/detective-amd/-/detective-amd-2.4.0.tgz",
@@ -8910,6 +8981,20 @@
"vinyl-sourcemaps-apply": "0.2.1"
}
},
+ "gulp-sourcemaps": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/gulp-sourcemaps/-/gulp-sourcemaps-2.6.0.tgz",
+ "integrity": "sha1-fMzomaijv8oVk6M0jQ+/Qd0/UeU=",
+ "dev": true,
+ "dependencies": {
+ "acorn": {
+ "version": "4.0.13",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz",
+ "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=",
+ "dev": true
+ }
+ }
+ },
"gulp-transform": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/gulp-transform/-/gulp-transform-2.0.0.tgz",
@@ -9603,12 +9688,6 @@
"lower-case": "1.1.4"
}
},
- "is-module": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
- "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=",
- "dev": true
- },
"is-my-json-valid": {
"version": "2.16.0",
"resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.16.0.tgz",
@@ -13327,6 +13406,18 @@
"uuid": "3.1.0"
}
},
+ "request-promise": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.1.tgz",
+ "integrity": "sha1-fuxWyJMXqCLL/qmbA5zlQ8LhX2c=",
+ "dev": true
+ },
+ "request-promise-core": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.1.tgz",
+ "integrity": "sha1-Pu4AssWqgyOc+wTFcA2jb4HNCLY=",
+ "dev": true
+ },
"require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@@ -13452,6 +13543,12 @@
"integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=",
"dev": true
},
+ "resolve-url": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz",
+ "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=",
+ "dev": true
+ },
"response-time": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/response-time/-/response-time-2.3.2.tgz",
@@ -14359,6 +14456,12 @@
"integrity": "sha1-dc449SvwczxafwwRjYEzSiu19BI=",
"dev": true
},
+ "source-map-resolve": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.3.1.tgz",
+ "integrity": "sha1-YQ9hIqRFuN1RU1oqcbeD38Ekh2E=",
+ "dev": true
+ },
"source-map-support": {
"version": "0.4.15",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.15.tgz",
@@ -14368,6 +14471,12 @@
"source-map": "0.5.6"
}
},
+ "source-map-url": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.3.0.tgz",
+ "integrity": "sha1-fsrxO1e80J2opAxdJp2zN5nUqvk=",
+ "dev": true
+ },
"sourcemap-codec": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.3.1.tgz",
@@ -14495,6 +14604,12 @@
"readable-stream": "2.3.3"
}
},
+ "stealthy-require": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz",
+ "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=",
+ "dev": true
+ },
"stream-combiner": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz",
@@ -14638,6 +14753,12 @@
"is-utf8": "0.2.1"
}
},
+ "strip-bom-string": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz",
+ "integrity": "sha1-5SEekiQ2n7uB1jOi8ABE3IztrZI=",
+ "dev": true
+ },
"strip-eof": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
@@ -15484,6 +15605,12 @@
"integrity": "sha1-YdvC1Ttp/2CRoSoWj9fUMxB+QPE=",
"dev": true
},
+ "travis-after-modes": {
+ "version": "0.0.7",
+ "resolved": "https://registry.npmjs.org/travis-after-modes/-/travis-after-modes-0.0.7.tgz",
+ "integrity": "sha1-o/PAWNPoaqp79Buwh8dduRRacYw=",
+ "dev": true
+ },
"trim-newlines": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz",
@@ -15607,7 +15734,8 @@
"tslib": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.7.1.tgz",
- "integrity": "sha1-vIAEFkaRkjp5/oN4u+s9ogF1OOw="
+ "integrity": "sha1-vIAEFkaRkjp5/oN4u+s9ogF1OOw=",
+ "dev": true
},
"tslint": {
"version": "5.5.0",
@@ -16027,6 +16155,12 @@
"upper-case": "1.1.3"
}
},
+ "urix": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz",
+ "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=",
+ "dev": true
+ },
"url-join": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/url-join/-/url-join-0.0.1.tgz",
diff --git a/package.json b/package.json
index 77a9c2e020d1..cebdc5e813ff 100644
--- a/package.json
+++ b/package.json
@@ -86,7 +86,7 @@
"highlight.js": "^9.11.0",
"http-rewrite-middleware": "^0.1.6",
"image-diff": "^1.6.3",
- "jasmine-core": "^2.6.2",
+ "jasmine-core": "^2.6.3",
"jsonwebtoken": "^7.4.1",
"karma": "^1.7.0",
"karma-browserstack-launcher": "^1.2.0",
diff --git a/src/lib/list/_list-theme.scss b/src/lib/list/_list-theme.scss
index 2355a23d5c80..e582e82eb8ba 100644
--- a/src/lib/list/_list-theme.scss
+++ b/src/lib/list/_list-theme.scss
@@ -8,11 +8,15 @@
$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);
}
@@ -29,6 +33,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 +50,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 +74,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..6ac9c1fee208 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,14 @@ import {
MdListCssMatStyler,
MdNavListCssMatStyler,
MdDividerCssMatStyler,
- MdListSubheaderCssMatStyler,
+ MdListSubheaderCssMatStyler
} from './list';
+import {MdSelectionList} from './selection-list';
+import {MdListOption} from './list-option';
@NgModule({
- imports: [MdLineModule, MdRippleModule, MdCommonModule],
+ imports: [MdLineModule, MdRippleModule, MdCommonModule, MdSelectionModule, CommonModule],
exports: [
MdList,
MdListItem,
@@ -35,6 +38,9 @@ import {
MdNavListCssMatStyler,
MdDividerCssMatStyler,
MdListSubheaderCssMatStyler,
+ MdSelectionModule,
+ MdSelectionList,
+ MdListOption
],
declarations: [
MdList,
@@ -46,6 +52,8 @@ import {
MdNavListCssMatStyler,
MdDividerCssMatStyler,
MdListSubheaderCssMatStyler,
+ MdSelectionList,
+ MdListOption
],
})
export class MdListModule {}
diff --git a/src/lib/list/list-option.html b/src/lib/list/list-option.html
new file mode 100644
index 000000000000..0489fa056d4b
--- /dev/null
+++ b/src/lib/list/list-option.html
@@ -0,0 +1,10 @@
+
diff --git a/src/lib/list/list-option.ts b/src/lib/list/list-option.ts
new file mode 100644
index 000000000000..6ee0d4259632
--- /dev/null
+++ b/src/lib/list/list-option.ts
@@ -0,0 +1,154 @@
+/**
+ * @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,
+ ContentChild,
+ ContentChildren,
+ Directive,
+ ElementRef,
+ Input,
+ Optional,
+ QueryList,
+ Renderer2,
+ ViewEncapsulation,
+ ChangeDetectionStrategy,
+ OnDestroy,
+ EventEmitter,
+ Output,
+ ChangeDetectorRef
+} from '@angular/core';
+import {coerceBooleanProperty, MdLine, MdLineSetter} from '../core';
+import {Focusable} from '../core/a11y/focus-key-manager';
+import {MdSelectionList} from './selection-list';
+
+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)': 'toggle()',
+ '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, Focusable {
+ 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 nav list. The value of `disableRipple` on the `md-nav-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 after or before 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() select = new EventEmitter();
+
+ /** Emitted when the option is deselected. */
+ @Output() deselect = new EventEmitter();
+
+ /** Emitted when the option is destroyed. */
+ @Output() destroy = new EventEmitter();
+
+ constructor(private _renderer: Renderer2,
+ private _element: ElementRef,
+ private _changeDetector: ChangeDetectorRef,
+ @Optional() public selectionList: MdSelectionList,) { }
+
+
+ ngAfterContentInit() {
+ this._lineSetter = new MdLineSetter(this._lines, this._renderer, this._element);
+ }
+
+ ngOnDestroy(): void {
+ this.destroy.emit({option: this});
+ }
+
+ toggle(): void {
+ if(this._disabled == false) {
+ 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;
+ }
+
+ _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;
+ }
+}
diff --git a/src/lib/list/list.scss b/src/lib/list/list.scss
index 7f91e6596895..3136db6e4f30 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,20 @@ $mat-dense-list-icon-size: 20px;
$mat-list-icon-size
);
}
+
+ .mat-list-option {
+ @include mat-line-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 +183,17 @@ $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 {
@@ -188,3 +217,5 @@ $mat-dense-list-icon-size: 20px;
}
}
}
+
+
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.ts b/src/lib/list/selection-list.ts
new file mode 100644
index 000000000000..b0ec663c03cd
--- /dev/null
+++ b/src/lib/list/selection-list.ts
@@ -0,0 +1,229 @@
+/**
+ * @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,
+ ContentChild,
+ ContentChildren,
+ Directive,
+ ElementRef,
+ Input,
+ Optional,
+ QueryList,
+ Renderer2,
+ ViewEncapsulation,
+ ChangeDetectionStrategy,
+ OnDestroy,
+ EventEmitter,
+ Output,
+ ChangeDetectorRef
+} from '@angular/core';
+import {coerceBooleanProperty, MdLine, MdLineSetter, SelectionModel} from '../core';
+import {FocusKeyManager} from '../core/a11y/focus-key-manager';
+import {Subscription} from 'rxjs/Subscription';
+import {SPACE} from '../core/keyboard/keycodes';
+import {Focusable} from '../core/a11y/focus-key-manager';
+import {MdListOption} from './list-option';
+
+@Component({
+ moduleId: module.id,
+ selector: 'md-selection-list, mat-selection-list',
+ host: {
+ 'role': 'listbox',
+ '[attr.tabindex]': '_tabIndex',
+ 'class': 'mat-selection-list',
+ '(focus)': 'focus()',
+ '(keydown)': 'keydown($event)'},
+ template: '',
+ styleUrls: ['list.css'],
+ encapsulation: ViewEncapsulation.None,
+ changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class MdSelectionList implements AfterContentInit, OnDestroy {
+ private _disableRipple: boolean = false;
+
+ private _disabled: boolean = false;
+
+ /** Tab index for the selection-list. */
+ _tabIndex = 0;
+
+ /** Track which options we're listening to for focus/destruction. */
+ private _subscribed: WeakMap = new WeakMap();
+
+ /** Subscription to tabbing out from the selection-list. */
+ private _tabOutSubscription: Subscription;
+
+ /** Subscription to option changes from the selection-list. */
+ private _optionSubscription: Subscription;
+
+ /** Whether or not the option is selectable. */
+ protected _selectable: boolean = true;
+
+ /** 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); }
+
+ /** Whether the selection-list is disabled */
+ @Input()
+ get disabled() { return this._disabled; }
+ set disabled(value: any) { this._disabled = coerceBooleanProperty(value); }
+
+ constructor(private _element: ElementRef) { }
+
+ ngAfterContentInit(): void {
+ this._keyManager = new FocusKeyManager(this.options).withWrap();
+
+ // Prevents the selection-list from capturing focus and redirecting
+ // it back to the first option when the user tabs out.
+ this._tabOutSubscription = this._keyManager.tabOut.subscribe(() => {
+ this._tabIndex = -1;
+ setTimeout(() => this._tabIndex = 0);
+ });
+
+ // Go ahead and subscribe all of the initial options
+ this._subscribeOptions(this.options);
+
+ // When the list changes, re-subscribe
+ this._optionSubscription =
+ this.options.changes.subscribe((options: QueryList) => {
+ this._subscribeOptions(options);
+ });
+ }
+
+ ngOnDestroy(): void {
+ if (this._tabOutSubscription) {
+ this._tabOutSubscription.unsubscribe();
+ }
+
+ if (this._optionSubscription) {
+ this._optionSubscription.unsubscribe();
+ }
+ }
+
+ /**
+ * Whether or not this option is selectable. When a option is not selectable,
+ * it's selected state is always ignored.
+ */
+ @Input()
+ get selectable(): boolean { return this._selectable; }
+ set selectable(value: boolean) {
+ this._selectable = coerceBooleanProperty(value);
+ }
+
+ focus() {
+ this._element.nativeElement.focus();
+ }
+
+ /** 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. */
+ protected _toggleSelectOnFocusedOption(): void {
+ if (!this.selectable) {
+ return;
+ }
+
+ let focusedIndex = this._keyManager.activeItemIndex;
+
+ if (typeof focusedIndex === 'number' && this._isValidIndex(focusedIndex)) {
+ let focusedOption: MdListOption = this.options.toArray()[focusedIndex];
+
+ if (focusedOption) {
+ focusedOption.toggle();
+ }
+ }
+ }
+
+
+ /**
+ * Iterate through the list of options and add them to our list of
+ * subscribed options.
+ *
+ * @param options The list of options to be subscribed.
+ */
+ protected _subscribeOptions(options: QueryList): void {
+ options.forEach(option => this._addOption(option));
+ }
+
+ /**
+ * Add a specific option to our subscribed list. If the option has
+ * already been subscribed, this ensures it is only subscribed
+ * once.
+ *
+ * @param option The option to be subscribed (or checked for existing
+ * subscription).
+ */
+ protected _addOption(option: MdListOption) {
+ // If we've already been subscribed to a parent, do nothing
+ if (this._subscribed.has(option)) {
+ return;
+ }
+
+ // Watch for focus events outside of the keyboard navigation
+ option.onFocus.subscribe(() => {
+ let optionIndex: number = this.options.toArray().indexOf(option);
+
+ if (this._isValidIndex(optionIndex)) {
+ this._keyManager.updateActiveItemIndex(optionIndex);
+ }
+ });
+
+ // On destroy, remove the item from our list, and check focus
+ option.destroy.subscribe(() => {
+ let optionIndex: number = this.options.toArray().indexOf(option);
+
+ if (this._isValidIndex(optionIndex) && 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);
+ }
+ }
+
+ this._subscribed.delete(option);
+ option.destroy.unsubscribe();
+ });
+
+ this._subscribed.set(option, true);
+ }
+
+ /**
+ * 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;
+ }
+}