diff --git a/src/dev-app/list/list-demo.html b/src/dev-app/list/list-demo.html
index fbb560fdf191..2be72e3bbba6 100644
--- a/src/dev-app/list/list-demo.html
+++ b/src/dev-app/list/list-demo.html
@@ -123,6 +123,7 @@
Selection list
(selectionChange)="changeEventCount = changeEventCount + 1"
[disabled]="selectionListDisabled"
[disableRipple]="selectionListRippleDisabled"
+ [hideSingleSelectionIndicator]="selectionListSingleSelectionIndicatorHidden"
color="primary">
Groceries
@@ -161,6 +162,12 @@ Selection list
+
+
+
@@ -173,6 +180,7 @@
Single Selection list
Favorite Grocery
diff --git a/src/dev-app/list/list-demo.ts b/src/dev-app/list/list-demo.ts
index bad097b791fa..30ff2459576f 100644
--- a/src/dev-app/list/list-demo.ts
+++ b/src/dev-app/list/list-demo.ts
@@ -64,6 +64,7 @@ export class ListDemo {
infoClicked = false;
selectionListDisabled = false;
selectionListRippleDisabled = false;
+ selectionListSingleSelectionIndicatorHidden = false;
selectedOptions: string[] = ['apples'];
changeEventCount = 0;
diff --git a/src/material/list/_interactive-list-theme.scss b/src/material/list/_interactive-list-theme.scss
index 8c40ba7585e8..b3c36d8f005d 100644
--- a/src/material/list/_interactive-list-theme.scss
+++ b/src/material/list/_interactive-list-theme.scss
@@ -1,11 +1,13 @@
@use 'sass:map';
@use '@material/ripple' as mdc-ripple;
+@use '../core/theming/theming';
// Mixin that provides colors for the various states of an interactive list-item. MDC
// has integrated styles for these states but relies on their complex ripples for it.
@mixin private-interactive-list-item-state-colors($config) {
$is-dark-theme: map.get($config, is-dark);
$active-base-color: if($is-dark-theme, white, black);
+ $selected-color: theming.get-color-from-palette(map.get($config, primary));
.mat-mdc-list-item-interactive {
&::before {
@@ -19,5 +21,20 @@
&:focus::before {
opacity: mdc-ripple.states-opacity($active-base-color, focus);
}
+
+ &.mdc-list-item--selected {
+ &::before {
+ background: $selected-color;
+ opacity: mdc-ripple.states-opacity($selected-color, selected);
+ }
+
+ &:not(:focus):not(.mdc-list-item--disabled):hover::before {
+ // The hover and selected opacities need to be combined to match with what the MDC
+ // ripple state would render. More details here:
+ // https://github.com/material-components/material-components-web/blob/348665978ce73694ad4518626dd70cdf5b984113/packages/mdc-ripple/_ripple-theme.scss#L450.
+ opacity: mdc-ripple.states-opacity($selected-color, hover) +
+ mdc-ripple.states-opacity($selected-color, selected);
+ }
+ }
}
}
diff --git a/src/material/list/list-base.ts b/src/material/list/list-base.ts
index a5341e05cc9e..327d2ec3698c 100644
--- a/src/material/list/list-base.ts
+++ b/src/material/list/list-base.ts
@@ -13,6 +13,7 @@ import {
ContentChildren,
Directive,
ElementRef,
+ inject,
Inject,
Input,
NgZone,
@@ -35,6 +36,7 @@ import {
MatListItemIcon,
MatListItemAvatar,
} from './list-item-sections';
+import {MAT_LIST_CONFIG} from './tokens';
@Directive({
host: {
@@ -67,6 +69,8 @@ export abstract class MatListBase {
this._disabled = coerceBooleanProperty(value);
}
private _disabled = false;
+
+ protected _defaultOptions = inject(MAT_LIST_CONFIG, {optional: true});
}
@Directive({
diff --git a/src/material/list/list-option.ts b/src/material/list/list-option.ts
index 1bcf18d1360c..25552ee5924c 100644
--- a/src/material/list/list-option.ts
+++ b/src/material/list/list-option.ts
@@ -50,6 +50,7 @@ export interface SelectionList extends MatListBase {
multiple: boolean;
color: ThemePalette;
selectedOptions: SelectionModel;
+ hideSingleSelectionIndicator: boolean;
compareWith: (o1: any, o2: any) => boolean;
_value: string[] | null;
_reportValueChange(): void;
@@ -64,6 +65,10 @@ export interface SelectionList extends MatListBase {
host: {
'class': 'mat-mdc-list-item mat-mdc-list-option mdc-list-item',
'role': 'option',
+ // As per MDC, only list items without checkbox or radio indicator should receive the
+ // `--selected` class.
+ '[class.mdc-list-item--selected]':
+ 'selected && !_selectionList.multiple && _selectionList.hideSingleSelectionIndicator',
// Based on the checkbox/radio position and whether there are icons or avatars, we apply MDC's
// list-item `--leading` and `--trailing` classes.
'[class.mdc-list-item--with-leading-avatar]': '_hasProjected("avatars", "before")',
@@ -243,7 +248,11 @@ export class MatListOption extends MatListItemBase implements ListOption, OnInit
/** Where a radio indicator is shown at the given position. */
_hasRadioAt(position: MatListOptionTogglePosition): boolean {
- return !this._selectionList.multiple && this._getTogglePosition() === position;
+ return (
+ !this._selectionList.multiple &&
+ this._getTogglePosition() === position &&
+ !this._selectionList.hideSingleSelectionIndicator
+ );
}
/** Whether icons or avatars are shown at the given position. */
diff --git a/src/material/list/public-api.ts b/src/material/list/public-api.ts
index 010eab276ae0..8f1c32f610fd 100644
--- a/src/material/list/public-api.ts
+++ b/src/material/list/public-api.ts
@@ -14,6 +14,7 @@ export * from './selection-list';
export * from './list-option';
export * from './subheader';
export * from './list-item-sections';
+export * from './tokens';
export {MatListOption} from './list-option';
diff --git a/src/material/list/selection-list.spec.ts b/src/material/list/selection-list.spec.ts
index 3aff5f76e789..c908b52a24f6 100644
--- a/src/material/list/selection-list.spec.ts
+++ b/src/material/list/selection-list.spec.ts
@@ -30,6 +30,8 @@ import {
MatListOptionTogglePosition,
MatSelectionList,
MatSelectionListChange,
+ MatListConfig,
+ MAT_LIST_CONFIG,
} from './index';
describe('MDC-based MatSelectionList without forms', () => {
@@ -663,7 +665,7 @@ describe('MDC-based MatSelectionList without forms', () => {
});
});
- describe('with list option selected', () => {
+ describe('multiple-selection with list option selected', () => {
let fixture: ComponentFixture;
let listOptionElements: DebugElement[];
let selectionList: DebugElement;
@@ -703,6 +705,79 @@ describe('MDC-based MatSelectionList without forms', () => {
}));
});
+ describe('single-selection with list option selected', () => {
+ let fixture: ComponentFixture;
+ let listOptionElements: DebugElement[];
+
+ beforeEach(waitForAsync(() => {
+ TestBed.configureTestingModule({
+ imports: [MatListModule],
+ declarations: [SingleSelectionListWithSelectedOption],
+ });
+
+ TestBed.compileComponents();
+ }));
+
+ beforeEach(waitForAsync(() => {
+ fixture = TestBed.createComponent(SingleSelectionListWithSelectedOption);
+ listOptionElements = fixture.debugElement.queryAll(By.directive(MatListOption))!;
+ fixture.detectChanges();
+ }));
+
+ it('displays radio indicators by default', () => {
+ expect(
+ listOptionElements[0].nativeElement.querySelector('input[type="radio"]'),
+ ).not.toBeNull();
+ expect(
+ listOptionElements[1].nativeElement.querySelector('input[type="radio"]'),
+ ).not.toBeNull();
+
+ expect(listOptionElements[0].nativeElement.classList).not.toContain(
+ 'mdc-list-item--selected',
+ );
+ expect(listOptionElements[1].nativeElement.classList).not.toContain(
+ 'mdc-list-item--selected',
+ );
+ });
+ });
+
+ describe('with token to hide radio indicators', () => {
+ let fixture: ComponentFixture;
+ let listOptionElements: DebugElement[];
+
+ beforeEach(waitForAsync(() => {
+ const matListConfig: MatListConfig = {hideSingleSelectionIndicator: true};
+
+ TestBed.configureTestingModule({
+ imports: [MatListModule],
+ declarations: [SingleSelectionListWithSelectedOption],
+ providers: [{provide: MAT_LIST_CONFIG, useValue: matListConfig}],
+ });
+
+ TestBed.compileComponents();
+ }));
+
+ beforeEach(waitForAsync(() => {
+ fixture = TestBed.createComponent(SingleSelectionListWithSelectedOption);
+ listOptionElements = fixture.debugElement.queryAll(By.directive(MatListOption))!;
+ fixture.detectChanges();
+ }));
+
+ it('does not display radio indicators', () => {
+ expect(listOptionElements[0].nativeElement.querySelector('input[type="radio"]')).toBeNull();
+ expect(listOptionElements[1].nativeElement.querySelector('input[type="radio"]')).toBeNull();
+
+ expect(listOptionElements[0].nativeElement.classList).not.toContain(
+ 'mdc-list-item--selected',
+ );
+
+ expect(listOptionElements[1].nativeElement.getAttribute('aria-selected'))
+ .withContext('Expected second option to be selected')
+ .toBe('true');
+ expect(listOptionElements[1].nativeElement.classList).toContain('mdc-list-item--selected');
+ });
+ });
+
describe('with option disabled', () => {
let fixture: ComponentFixture;
let listOptionEl: HTMLElement;
@@ -1727,6 +1802,15 @@ class SelectionListWithDisabledOption {
})
class SelectionListWithSelectedOption {}
+@Component({
+ template: `
+
+ Not selected - Item #1
+ Pre-selected - Item #2
+ `,
+})
+class SingleSelectionListWithSelectedOption {}
+
@Component({
template: `
diff --git a/src/material/list/selection-list.ts b/src/material/list/selection-list.ts
index 7f5f91696c20..0ec4ac7a9e57 100644
--- a/src/material/list/selection-list.ts
+++ b/src/material/list/selection-list.ts
@@ -123,6 +123,17 @@ export class MatSelectionList
}
private _multiple = true;
+ /** Whether radio indicator for all list items is hidden. */
+ @Input()
+ get hideSingleSelectionIndicator(): boolean {
+ return this._hideSingleSelectionIndicator;
+ }
+ set hideSingleSelectionIndicator(value: BooleanInput) {
+ this._hideSingleSelectionIndicator = coerceBooleanProperty(value);
+ }
+ private _hideSingleSelectionIndicator: boolean =
+ this._defaultOptions?.hideSingleSelectionIndicator ?? false;
+
/** The currently selected options. */
selectedOptions = new SelectionModel(this._multiple);
@@ -160,10 +171,12 @@ export class MatSelectionList
ngOnChanges(changes: SimpleChanges) {
const disabledChanges = changes['disabled'];
const disableRippleChanges = changes['disableRipple'];
+ const hideSingleSelectionIndicatorChanges = changes['hideSingleSelectionIndicator'];
if (
(disableRippleChanges && !disableRippleChanges.firstChange) ||
- (disabledChanges && !disabledChanges.firstChange)
+ (disabledChanges && !disabledChanges.firstChange) ||
+ (hideSingleSelectionIndicatorChanges && !hideSingleSelectionIndicatorChanges.firstChange)
) {
this._markOptionsForCheck();
}
diff --git a/src/material/list/tokens.ts b/src/material/list/tokens.ts
new file mode 100644
index 000000000000..9f0ccb30ff0a
--- /dev/null
+++ b/src/material/list/tokens.ts
@@ -0,0 +1,18 @@
+/**
+ * @license
+ * Copyright Google LLC 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 {InjectionToken} from '@angular/core';
+
+/** Object that can be used to configure the default options for the list module. */
+export interface MatListConfig {
+ /** Wheter icon indicators should be hidden for single-selection. */
+ hideSingleSelectionIndicator?: boolean;
+}
+
+/** Injection token that can be used to provide the default options the list module. */
+export const MAT_LIST_CONFIG = new InjectionToken('MAT_LIST_CONFIG');
diff --git a/tools/public_api_guard/material/list.md b/tools/public_api_guard/material/list.md
index e5c92cb29e89..5ccf15c94b80 100644
--- a/tools/public_api_guard/material/list.md
+++ b/tools/public_api_guard/material/list.md
@@ -32,6 +32,9 @@ import { ThemePalette } from '@angular/material/core';
// @public
export const MAT_LIST: InjectionToken;
+// @public
+export const MAT_LIST_CONFIG: InjectionToken;
+
// @public
export const MAT_NAV_LIST: InjectionToken;
@@ -56,6 +59,11 @@ export class MatList extends MatListBase {
static ɵfac: i0.ɵɵFactoryDeclaration;
}
+// @public
+export interface MatListConfig {
+ hideSingleSelectionIndicator?: boolean;
+}
+
// @public (undocumented)
export class MatListItem extends MatListItemBase {
constructor(element: ElementRef, ngZone: NgZone, listBase: MatListBase | null, platform: Platform, globalRippleOptions?: RippleGlobalOptions, animationMode?: string);
@@ -229,6 +237,8 @@ export class MatSelectionList extends MatListBase implements SelectionList, Cont
_emitChangeEvent(options: MatListOption[]): void;
focus(options?: FocusOptions): void;
_handleKeydown(event: KeyboardEvent): void;
+ get hideSingleSelectionIndicator(): boolean;
+ set hideSingleSelectionIndicator(value: BooleanInput);
// (undocumented)
_items: QueryList;
get multiple(): boolean;
@@ -251,7 +261,7 @@ export class MatSelectionList extends MatListBase implements SelectionList, Cont
_value: string[] | null;
writeValue(values: string[]): void;
// (undocumented)
- static ɵcmp: i0.ɵɵComponentDeclaration;
+ static ɵcmp: i0.ɵɵComponentDeclaration;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration;
}
@@ -277,6 +287,8 @@ export interface SelectionList extends MatListBase {
// (undocumented)
_emitChangeEvent(options: MatListOption[]): void;
// (undocumented)
+ hideSingleSelectionIndicator: boolean;
+ // (undocumented)
multiple: boolean;
// (undocumented)
_onTouched(): void;