diff --git a/src/components-examples/material/list/index.ts b/src/components-examples/material/list/index.ts
index eea8dd9e66de..a4bac254fe6d 100644
--- a/src/components-examples/material/list/index.ts
+++ b/src/components-examples/material/list/index.ts
@@ -5,17 +5,20 @@ import {MatListModule} from '@angular/material/list';
import {ListOverviewExample} from './list-overview/list-overview-example';
import {ListSectionsExample} from './list-sections/list-sections-example';
import {ListSelectionExample} from './list-selection/list-selection-example';
+import {ListSingleSelectionExample} from './list-single-selection/list-single-selection-example';
export {
ListOverviewExample,
ListSectionsExample,
ListSelectionExample,
+ ListSingleSelectionExample,
};
const EXAMPLES = [
ListOverviewExample,
ListSectionsExample,
ListSelectionExample,
+ ListSingleSelectionExample,
];
@NgModule({
diff --git a/src/components-examples/material/list/list-single-selection/list-single-selection-example.css b/src/components-examples/material/list/list-single-selection/list-single-selection-example.css
new file mode 100644
index 000000000000..7949471cec95
--- /dev/null
+++ b/src/components-examples/material/list/list-single-selection/list-single-selection-example.css
@@ -0,0 +1 @@
+/** No styles for this example. */
diff --git a/src/components-examples/material/list/list-single-selection/list-single-selection-example.html b/src/components-examples/material/list/list-single-selection/list-single-selection-example.html
new file mode 100644
index 000000000000..29cd74bbe71d
--- /dev/null
+++ b/src/components-examples/material/list/list-single-selection/list-single-selection-example.html
@@ -0,0 +1,9 @@
+
+
+ {{shoe}}
+
+
+
+
+ Option selected: {{shoes.selectedOptions.selected}}
+
diff --git a/src/components-examples/material/list/list-single-selection/list-single-selection-example.ts b/src/components-examples/material/list/list-single-selection/list-single-selection-example.ts
new file mode 100644
index 000000000000..8e1ee0de2ead
--- /dev/null
+++ b/src/components-examples/material/list/list-single-selection/list-single-selection-example.ts
@@ -0,0 +1,13 @@
+import {Component} from '@angular/core';
+
+/**
+ * @title List with single selection
+ */
+@Component({
+ selector: 'list-single-selection-example',
+ styleUrls: ['list-single-selection-example.css'],
+ templateUrl: 'list-single-selection-example.html',
+})
+export class ListSingleSelectionExample {
+ typesOfShoes: string[] = ['Boots', 'Clogs', 'Loafers', 'Moccasins', 'Sneakers'];
+}
diff --git a/src/dev-app/list/list-demo.html b/src/dev-app/list/list-demo.html
index 5df3da6e3b16..3325bce2db43 100644
--- a/src/dev-app/list/list-demo.html
+++ b/src/dev-app/list/list-demo.html
@@ -162,4 +162,22 @@ Dogs
+
+
+
Single Selection list
+
+
+ Favorite Grocery
+
+ Bananas
+ Oranges
+ Apples
+ Strawberries
+
+
+
Selected: {{favoriteOptions | json}}
+
diff --git a/src/dev-app/list/list-demo.ts b/src/dev-app/list/list-demo.ts
index 84a6eaf0a37b..9c1ec2908019 100644
--- a/src/dev-app/list/list-demo.ts
+++ b/src/dev-app/list/list-demo.ts
@@ -70,6 +70,8 @@ export class ListDemo {
this.modelChangeEventCount++;
}
+ favoriteOptions: string[] = [];
+
alertItem(msg: string) {
alert(msg);
}
diff --git a/src/material/list/_list-theme.scss b/src/material/list/_list-theme.scss
index 71fe8e938724..9095273ca551 100644
--- a/src/material/list/_list-theme.scss
+++ b/src/material/list/_list-theme.scss
@@ -33,6 +33,12 @@
background: mat-color($background, 'hover');
}
}
+
+ .mat-list-single-selected-option {
+ &, &:hover, &:focus {
+ background: mat-color($background, hover, 0.12);
+ }
+ }
}
@mixin mat-list-typography($config) {
diff --git a/src/material/list/list-option.html b/src/material/list/list-option.html
index b785eca5a6e6..d8a34a308cbf 100644
--- a/src/material/list/list-option.html
+++ b/src/material/list/list-option.html
@@ -7,6 +7,7 @@
[matRippleDisabled]="_isRippleDisabled()">
diff --git a/src/material/list/selection-list.spec.ts b/src/material/list/selection-list.spec.ts
index ce45f1aa4483..ed9644a37374 100644
--- a/src/material/list/selection-list.spec.ts
+++ b/src/material/list/selection-list.spec.ts
@@ -188,6 +188,16 @@ describe('MatSelectionList without forms', () => {
expect(selectList.selected.length).toBe(0);
});
+ it('should not add the mat-list-single-selected-option class (in multiple mode)', () => {
+ let testListItem = listOptions[2].injector.get(MatListOption);
+
+ testListItem._handleClick();
+ fixture.detectChanges();
+
+ expect(listOptions[2].nativeElement.classList.contains('mat-list-single-selected-option'))
+ .toBe(false);
+ });
+
it('should not allow selection of disabled items', () => {
let testListItem = listOptions[0].injector.get(MatListOption);
let selectList =
@@ -882,6 +892,80 @@ describe('MatSelectionList without forms', () => {
expect(listOption.classList).toContain('mat-list-item-with-avatar');
});
});
+
+ describe('with single selection', () => {
+ let fixture: ComponentFixture;
+ let listOption: DebugElement[];
+ let selectionList: DebugElement;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [MatListModule],
+ declarations: [
+ SelectionListWithListOptions,
+ ],
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(SelectionListWithListOptions);
+ fixture.componentInstance.multiple = false;
+ listOption = fixture.debugElement.queryAll(By.directive(MatListOption));
+ selectionList = fixture.debugElement.query(By.directive(MatSelectionList))!;
+ fixture.detectChanges();
+ }));
+
+ it('should select one option at a time', () => {
+ const testListItem1 = listOption[1].injector.get(MatListOption);
+ const testListItem2 = listOption[2].injector.get(MatListOption);
+ const selectList =
+ selectionList.injector.get(MatSelectionList).selectedOptions;
+
+ expect(selectList.selected.length).toBe(0);
+
+ dispatchFakeEvent(testListItem1._getHostElement(), 'click');
+ fixture.detectChanges();
+
+ expect(selectList.selected).toEqual([testListItem1]);
+ expect(listOption[1].nativeElement.classList.contains('mat-list-single-selected-option'))
+ .toBe(true);
+
+ dispatchFakeEvent(testListItem2._getHostElement(), 'click');
+ fixture.detectChanges();
+
+ expect(selectList.selected).toEqual([testListItem2]);
+ expect(listOption[1].nativeElement.classList.contains('mat-list-single-selected-option'))
+ .toBe(false);
+ expect(listOption[2].nativeElement.classList.contains('mat-list-single-selected-option'))
+ .toBe(true);
+ });
+
+ it('should not show check boxes', () => {
+ expect(fixture.nativeElement.querySelector('mat-pseudo-checkbox')).toBeFalsy();
+ });
+
+ it('should not deselect the target option on click', () => {
+ const testListItem1 = listOption[1].injector.get(MatListOption);
+ const selectList =
+ selectionList.injector.get(MatSelectionList).selectedOptions;
+
+ expect(selectList.selected.length).toBe(0);
+
+ dispatchFakeEvent(testListItem1._getHostElement(), 'click');
+ fixture.detectChanges();
+
+ expect(selectList.selected).toEqual([testListItem1]);
+
+ dispatchFakeEvent(testListItem1._getHostElement(), 'click');
+ fixture.detectChanges();
+
+ expect(selectList.selected).toEqual([testListItem1]);
+ });
+
+ it('throws an exception when toggling single/multiple mode after bootstrap', () => {
+ fixture.componentInstance.multiple = true;
+ expect(() => fixture.detectChanges()).toThrow(new Error(
+ 'Cannot change `multiple` mode of mat-selection-list after initialization.'));
+ });
+ });
});
describe('MatSelectionList with forms', () => {
@@ -1255,7 +1339,8 @@ describe('MatSelectionList with forms', () => {
id="selection-list-1"
(selectionChange)="onValueChange($event)"
[disableRipple]="listRippleDisabled"
- [color]="selectionListColor">
+ [color]="selectionListColor"
+ [multiple]="multiple">
Inbox (disabled selection-option)
@@ -1274,6 +1359,7 @@ describe('MatSelectionList with forms', () => {
class SelectionListWithListOptions {
showLastOption: boolean = true;
listRippleDisabled = false;
+ multiple = true;
selectionListColor: ThemePalette;
firstOptionColor: ThemePalette;
diff --git a/src/material/list/selection-list.ts b/src/material/list/selection-list.ts
index 559ec05863a1..21d04e81c275 100644
--- a/src/material/list/selection-list.ts
+++ b/src/material/list/selection-list.ts
@@ -40,6 +40,7 @@ import {
SimpleChanges,
ViewChild,
ViewEncapsulation,
+ isDevMode,
} from '@angular/core';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
import {
@@ -50,6 +51,7 @@ import {
setLines,
ThemePalette,
} from '@angular/material/core';
+
import {Subject} from 'rxjs';
import {takeUntil} from 'rxjs/operators';
@@ -108,6 +110,7 @@ export class MatSelectionListChange {
// be placed inside a parent that has one of the other colors with a higher specificity.
'[class.mat-accent]': 'color !== "primary" && color !== "warn"',
'[class.mat-warn]': 'color === "warn"',
+ '[class.mat-list-single-selected-option]': 'selected && !selectionList.multiple',
'[attr.aria-selected]': 'selected',
'[attr.aria-disabled]': 'disabled',
},
@@ -255,7 +258,7 @@ export class MatListOption extends _MatListOptionMixinBase implements AfterConte
}
_handleClick() {
- if (!this.disabled) {
+ if (!this.disabled && (this.selectionList.multiple || !this.selected)) {
this.toggle();
// Emit a change event if the selected state of the option changed through user interaction.
@@ -324,7 +327,7 @@ export class MatListOption extends _MatListOptionMixinBase implements AfterConte
'class': 'mat-selection-list mat-list-base',
'(blur)': '_onTouched()',
'(keydown)': '_keydown($event)',
- 'aria-multiselectable': 'true',
+ '[attr.aria-multiselectable]': 'multiple',
'[attr.aria-disabled]': 'disabled.toString()',
},
template: '',
@@ -335,6 +338,8 @@ export class MatListOption extends _MatListOptionMixinBase implements AfterConte
})
export class MatSelectionList extends _MatSelectionListMixinBase implements CanDisableRipple,
AfterContentInit, ControlValueAccessor, OnDestroy, OnChanges {
+ private _multiple = true;
+ private _contentInitialized = false;
/** The FocusKeyManager which handles focus. */
_keyManager: FocusKeyManager;
@@ -373,8 +378,25 @@ export class MatSelectionList extends _MatSelectionListMixinBase implements CanD
}
private _disabled: boolean = false;
+ /** Whether selection is limited to one or multiple items (default multiple). */
+ @Input()
+ get multiple(): boolean { return this._multiple; }
+ set multiple(value: boolean) {
+ const newValue = coerceBooleanProperty(value);
+
+ if (newValue !== this._multiple) {
+ if (isDevMode() && this._contentInitialized) {
+ throw new Error(
+ 'Cannot change `multiple` mode of mat-selection-list after initialization.');
+ }
+
+ this._multiple = newValue;
+ this.selectedOptions = new SelectionModel(this._multiple, this.selectedOptions.selected);
+ }
+ }
+
/** The currently selected options. */
- selectedOptions: SelectionModel = new SelectionModel(true);
+ selectedOptions = new SelectionModel(this._multiple);
/** View to model callback that should be called whenever the selected options change. */
private _onChange: (value: any) => void = (_: any) => {};
@@ -397,6 +419,8 @@ export class MatSelectionList extends _MatSelectionListMixinBase implements CanD
}
ngAfterContentInit(): void {
+ this._contentInitialized = true;
+
this._keyManager = new FocusKeyManager(this.options)
.withWrap()
.withTypeAhead()
@@ -589,7 +613,7 @@ export class MatSelectionList extends _MatSelectionListMixinBase implements CanD
if (focusedIndex != null && this._isValidIndex(focusedIndex)) {
let focusedOption: MatListOption = this.options.toArray()[focusedIndex];
- if (focusedOption && !focusedOption.disabled) {
+ if (focusedOption && !focusedOption.disabled && (this._multiple || !focusedOption.selected)) {
focusedOption.toggle();
// Emit a change event because the focused option changed its state through user
@@ -642,4 +666,5 @@ export class MatSelectionList extends _MatSelectionListMixinBase implements CanD
static ngAcceptInputType_disabled: BooleanInput;
static ngAcceptInputType_disableRipple: BooleanInput;
+ static ngAcceptInputType_multiple: BooleanInput;
}
diff --git a/tools/public_api_guard/material/list.d.ts b/tools/public_api_guard/material/list.d.ts
index 8b7ad109681d..57c2d107e6ec 100644
--- a/tools/public_api_guard/material/list.d.ts
+++ b/tools/public_api_guard/material/list.d.ts
@@ -94,6 +94,7 @@ export declare class MatSelectionList extends _MatSelectionListMixinBase impleme
color: ThemePalette;
compareWith: (o1: any, o2: any) => boolean;
disabled: boolean;
+ multiple: boolean;
options: QueryList;
selectedOptions: SelectionModel;
readonly selectionChange: EventEmitter;
@@ -116,7 +117,8 @@ export declare class MatSelectionList extends _MatSelectionListMixinBase impleme
writeValue(values: string[]): void;
static ngAcceptInputType_disableRipple: BooleanInput;
static ngAcceptInputType_disabled: BooleanInput;
- static ɵcmp: i0.ɵɵComponentDefWithMeta;
+ static ngAcceptInputType_multiple: BooleanInput;
+ static ɵcmp: i0.ɵɵComponentDefWithMeta;
static ɵfac: i0.ɵɵFactoryDef;
}