Skip to content

Commit dbab436

Browse files
committed
fix(material/list): add opt-out for radio indicators
Add an opt-out for radio indicators for single-selection. Adds both an Input and DI token to specify if radio indicators are hidden. By default, display radio indicators. If both DI token and Input are specified, the Input wins. PR #25933 added radio toggles for single-selection. Add an opt-out to provide a way to have same appearance as before #25933. API changes - add `@Input hideSingleSelectionIndicator` to specify if radio indicators are displayed - add MAT_LIST_CONFIG token to specify default value for `hideSingleSelectionIndicator`
1 parent 0d7f060 commit dbab436

File tree

10 files changed

+167
-4
lines changed

10 files changed

+167
-4
lines changed

src/dev-app/list/list-demo.html

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ <h2>Selection list</h2>
123123
(selectionChange)="changeEventCount = changeEventCount + 1"
124124
[disabled]="selectionListDisabled"
125125
[disableRipple]="selectionListRippleDisabled"
126+
[hideSingleSelectionIndicator]="selectionListSingleSelectionIndicatorHidden"
126127
color="primary">
127128
<div mat-subheader>Groceries</div>
128129

@@ -161,6 +162,12 @@ <h2>Selection list</h2>
161162
<input type="checkbox" [(ngModel)]="selectionListRippleDisabled">
162163
</label>
163164
</p>
165+
<p>
166+
<label>
167+
Hide Single-Selection indicators
168+
<input type="checkbox" [(ngModel)]="selectionListSingleSelectionIndicatorHidden">
169+
</label>
170+
</p>
164171
<p>
165172
<button mat-raised-button (click)="groceries.selectAll()">Select all</button>
166173
<button mat-raised-button (click)="groceries.deselectAll()">Deselect all</button>
@@ -173,6 +180,7 @@ <h2>Single Selection list</h2>
173180
<mat-selection-list #favorite
174181
[(ngModel)]="favoriteOptions"
175182
[multiple]="false"
183+
[hideSingleSelectionIndicator]="selectionListSingleSelectionIndicatorHidden"
176184
color="primary">
177185
<div mat-subheader>Favorite Grocery</div>
178186

src/dev-app/list/list-demo.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ export class ListDemo {
6464
infoClicked = false;
6565
selectionListDisabled = false;
6666
selectionListRippleDisabled = false;
67+
selectionListSingleSelectionIndicatorHidden = false;
6768

6869
selectedOptions: string[] = ['apples'];
6970
changeEventCount = 0;

src/material/list/_interactive-list-theme.scss

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
@use 'sass:map';
22
@use '@material/ripple' as mdc-ripple;
3+
@use '../core/theming/theming';
34

45
// Mixin that provides colors for the various states of an interactive list-item. MDC
56
// has integrated styles for these states but relies on their complex ripples for it.
67
@mixin private-interactive-list-item-state-colors($config) {
78
$is-dark-theme: map.get($config, is-dark);
89
$active-base-color: if($is-dark-theme, white, black);
10+
$selected-color: theming.get-color-from-palette(map.get($config, primary));
911

1012
.mat-mdc-list-item-interactive {
1113
&::before {
@@ -19,5 +21,20 @@
1921
&:focus::before {
2022
opacity: mdc-ripple.states-opacity($active-base-color, focus);
2123
}
24+
25+
&.mdc-list-item--selected {
26+
&::before {
27+
background: $selected-color;
28+
opacity: mdc-ripple.states-opacity($selected-color, selected);
29+
}
30+
31+
&:not(:focus):not(.mdc-list-item--disabled):hover::before {
32+
// The hover and selected opacities need to be combined to match with what the MDC
33+
// ripple state would render. More details here:
34+
// https://github.com/material-components/material-components-web/blob/348665978ce73694ad4518626dd70cdf5b984113/packages/mdc-ripple/_ripple-theme.scss#L450.
35+
opacity: mdc-ripple.states-opacity($selected-color, hover) +
36+
mdc-ripple.states-opacity($selected-color, selected);
37+
}
38+
}
2239
}
2340
}

src/material/list/list-base.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
ContentChildren,
1414
Directive,
1515
ElementRef,
16+
inject,
1617
Inject,
1718
Input,
1819
NgZone,
@@ -35,6 +36,7 @@ import {
3536
MatListItemIcon,
3637
MatListItemAvatar,
3738
} from './list-item-sections';
39+
import {MAT_LIST_CONFIG} from './tokens';
3840

3941
@Directive({
4042
host: {
@@ -67,6 +69,8 @@ export abstract class MatListBase {
6769
this._disabled = coerceBooleanProperty(value);
6870
}
6971
private _disabled = false;
72+
73+
protected _defaultOptions = inject(MAT_LIST_CONFIG, {optional: true});
7074
}
7175

7276
@Directive({

src/material/list/list-option.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export interface SelectionList extends MatListBase {
5050
multiple: boolean;
5151
color: ThemePalette;
5252
selectedOptions: SelectionModel<MatListOption>;
53+
hideSingleSelectionIndicator: boolean;
5354
compareWith: (o1: any, o2: any) => boolean;
5455
_value: string[] | null;
5556
_reportValueChange(): void;
@@ -64,6 +65,10 @@ export interface SelectionList extends MatListBase {
6465
host: {
6566
'class': 'mat-mdc-list-item mat-mdc-list-option mdc-list-item',
6667
'role': 'option',
68+
// As per MDC, only list items without checkbox or radio indicator should receive the
69+
// `--selected` class.
70+
'[class.mdc-list-item--selected]':
71+
'selected && !_selectionList.multiple && _selectionList.hideSingleSelectionIndicator',
6772
// Based on the checkbox/radio position and whether there are icons or avatars, we apply MDC's
6873
// list-item `--leading` and `--trailing` classes.
6974
'[class.mdc-list-item--with-leading-avatar]': '_hasProjected("avatars", "before")',
@@ -243,7 +248,11 @@ export class MatListOption extends MatListItemBase implements ListOption, OnInit
243248

244249
/** Where a radio indicator is shown at the given position. */
245250
_hasRadioAt(position: MatListOptionTogglePosition): boolean {
246-
return !this._selectionList.multiple && this._getTogglePosition() === position;
251+
return (
252+
!this._selectionList.multiple &&
253+
this._getTogglePosition() === position &&
254+
!this._selectionList.hideSingleSelectionIndicator
255+
);
247256
}
248257

249258
/** Whether icons or avatars are shown at the given position. */

src/material/list/public-api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export * from './selection-list';
1414
export * from './list-option';
1515
export * from './subheader';
1616
export * from './list-item-sections';
17+
export * from './tokens';
1718

1819
export {MatListOption} from './list-option';
1920

src/material/list/selection-list.spec.ts

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ import {
3030
MatListOptionTogglePosition,
3131
MatSelectionList,
3232
MatSelectionListChange,
33+
MatListConfig,
34+
MAT_LIST_CONFIG,
3335
} from './index';
3436

3537
describe('MDC-based MatSelectionList without forms', () => {
@@ -663,7 +665,7 @@ describe('MDC-based MatSelectionList without forms', () => {
663665
});
664666
});
665667

666-
describe('with list option selected', () => {
668+
describe('multiple-selection with list option selected', () => {
667669
let fixture: ComponentFixture<SelectionListWithSelectedOption>;
668670
let listOptionElements: DebugElement[];
669671
let selectionList: DebugElement;
@@ -703,6 +705,83 @@ describe('MDC-based MatSelectionList without forms', () => {
703705
}));
704706
});
705707

708+
describe('single-selection with list option selected', () => {
709+
let fixture: ComponentFixture<SingleSelectionListWithSelectedOption>;
710+
let listOptionElements: DebugElement[];
711+
let selectionList: DebugElement;
712+
713+
beforeEach(waitForAsync(() => {
714+
TestBed.configureTestingModule({
715+
imports: [MatListModule],
716+
declarations: [SingleSelectionListWithSelectedOption],
717+
});
718+
719+
TestBed.compileComponents();
720+
}));
721+
722+
beforeEach(waitForAsync(() => {
723+
fixture = TestBed.createComponent(SingleSelectionListWithSelectedOption);
724+
listOptionElements = fixture.debugElement.queryAll(By.directive(MatListOption))!;
725+
selectionList = fixture.debugElement.query(By.directive(MatSelectionList))!;
726+
fixture.detectChanges();
727+
}));
728+
729+
it('displays radio indicators by default', () => {
730+
expect(
731+
listOptionElements[0].nativeElement.querySelector('input[type="radio"]'),
732+
).not.toBeNull();
733+
expect(
734+
listOptionElements[1].nativeElement.querySelector('input[type="radio"]'),
735+
).not.toBeNull();
736+
737+
expect(listOptionElements[0].nativeElement.classList).not.toContain(
738+
'mdc-list-item--selected',
739+
);
740+
expect(listOptionElements[1].nativeElement.classList).not.toContain(
741+
'mdc-list-item--selected',
742+
);
743+
});
744+
});
745+
746+
describe('with token to hide radio indicators', () => {
747+
let fixture: ComponentFixture<SingleSelectionListWithSelectedOption>;
748+
let listOptionElements: DebugElement[];
749+
let selectionList: DebugElement;
750+
751+
beforeEach(waitForAsync(() => {
752+
const matListConfig: MatListConfig = {hideSingleSelectionIndicator: true};
753+
754+
TestBed.configureTestingModule({
755+
imports: [MatListModule],
756+
declarations: [SingleSelectionListWithSelectedOption],
757+
providers: [{provide: MAT_LIST_CONFIG, useValue: matListConfig}],
758+
});
759+
760+
TestBed.compileComponents();
761+
}));
762+
763+
beforeEach(waitForAsync(() => {
764+
fixture = TestBed.createComponent(SingleSelectionListWithSelectedOption);
765+
listOptionElements = fixture.debugElement.queryAll(By.directive(MatListOption))!;
766+
selectionList = fixture.debugElement.query(By.directive(MatSelectionList))!;
767+
fixture.detectChanges();
768+
}));
769+
770+
it('does not display radio indicators', () => {
771+
expect(listOptionElements[0].nativeElement.querySelector('input[type="radio"]')).toBeNull();
772+
expect(listOptionElements[1].nativeElement.querySelector('input[type="radio"]')).toBeNull();
773+
774+
expect(listOptionElements[0].nativeElement.classList).not.toContain(
775+
'mdc-list-item--selected',
776+
);
777+
778+
expect(listOptionElements[1].nativeElement.getAttribute('aria-selected'))
779+
.withContext('Expected second option to be selected')
780+
.toBe('true');
781+
expect(listOptionElements[1].nativeElement.classList).toContain('mdc-list-item--selected');
782+
});
783+
});
784+
706785
describe('with option disabled', () => {
707786
let fixture: ComponentFixture<SelectionListWithDisabledOption>;
708787
let listOptionEl: HTMLElement;
@@ -1727,6 +1806,15 @@ class SelectionListWithDisabledOption {
17271806
})
17281807
class SelectionListWithSelectedOption {}
17291808

1809+
@Component({
1810+
template: `
1811+
<mat-selection-list [multiple]="false">
1812+
<mat-list-option>Not selected - Item #1</mat-list-option>
1813+
<mat-list-option [selected]="true">Pre-selected - Item #2</mat-list-option>
1814+
</mat-selection-list>`,
1815+
})
1816+
class SingleSelectionListWithSelectedOption {}
1817+
17301818
@Component({
17311819
template: `
17321820
<mat-selection-list>

src/material/list/selection-list.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,17 @@ export class MatSelectionList
123123
}
124124
private _multiple = true;
125125

126+
/** Whether radio indicator for all list items is hidden. */
127+
@Input()
128+
get hideSingleSelectionIndicator(): boolean {
129+
return this._hideSingleSelectionIndicator;
130+
}
131+
set hideSingleSelectionIndicator(value: BooleanInput) {
132+
this._hideSingleSelectionIndicator = coerceBooleanProperty(value);
133+
}
134+
private _hideSingleSelectionIndicator: boolean =
135+
this._defaultOptions?.hideSingleSelectionIndicator ?? false;
136+
126137
/** The currently selected options. */
127138
selectedOptions = new SelectionModel<MatListOption>(this._multiple);
128139

@@ -160,10 +171,12 @@ export class MatSelectionList
160171
ngOnChanges(changes: SimpleChanges) {
161172
const disabledChanges = changes['disabled'];
162173
const disableRippleChanges = changes['disableRipple'];
174+
const hideSingleSelectionIndicatorChanges = changes['hideSingleSelectionIndicator'];
163175

164176
if (
165177
(disableRippleChanges && !disableRippleChanges.firstChange) ||
166-
(disabledChanges && !disabledChanges.firstChange)
178+
(disabledChanges && !disabledChanges.firstChange) ||
179+
(hideSingleSelectionIndicatorChanges && !hideSingleSelectionIndicatorChanges.firstChange)
167180
) {
168181
this._markOptionsForCheck();
169182
}

src/material/list/tokens.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import {InjectionToken} from '@angular/core';
2+
3+
/** Object that can be used to configure the default options for the list module. */
4+
export interface MatListConfig {
5+
/** Wheter icon indicators should be hidden for single-selection. */
6+
hideSingleSelectionIndicator?: boolean;
7+
}
8+
9+
/** Injection token that can be used to provide the default options the list module. */
10+
export const MAT_LIST_CONFIG = new InjectionToken<MatListConfig>('MAT_LIST_CONFIG');

tools/public_api_guard/material/list.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ import { ThemePalette } from '@angular/material/core';
3232
// @public
3333
export const MAT_LIST: InjectionToken<MatList>;
3434

35+
// @public
36+
export const MAT_LIST_CONFIG: InjectionToken<MatListConfig>;
37+
3538
// @public
3639
export const MAT_NAV_LIST: InjectionToken<MatNavList>;
3740

@@ -56,6 +59,11 @@ export class MatList extends MatListBase {
5659
static ɵfac: i0.ɵɵFactoryDeclaration<MatList, never>;
5760
}
5861

62+
// @public
63+
export interface MatListConfig {
64+
hideSingleSelectionIndicator?: boolean;
65+
}
66+
5967
// @public (undocumented)
6068
export class MatListItem extends MatListItemBase {
6169
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
229237
_emitChangeEvent(options: MatListOption[]): void;
230238
focus(options?: FocusOptions): void;
231239
_handleKeydown(event: KeyboardEvent): void;
240+
get hideSingleSelectionIndicator(): boolean;
241+
set hideSingleSelectionIndicator(value: BooleanInput);
232242
// (undocumented)
233243
_items: QueryList<MatListOption>;
234244
get multiple(): boolean;
@@ -251,7 +261,7 @@ export class MatSelectionList extends MatListBase implements SelectionList, Cont
251261
_value: string[] | null;
252262
writeValue(values: string[]): void;
253263
// (undocumented)
254-
static ɵcmp: i0.ɵɵComponentDeclaration<MatSelectionList, "mat-selection-list", ["matSelectionList"], { "color": "color"; "compareWith": "compareWith"; "multiple": "multiple"; "disabled": "disabled"; }, { "selectionChange": "selectionChange"; }, ["_items"], ["*"], false, never>;
264+
static ɵcmp: i0.ɵɵComponentDeclaration<MatSelectionList, "mat-selection-list", ["matSelectionList"], { "color": "color"; "compareWith": "compareWith"; "multiple": "multiple"; "hideSingleSelectionIndicator": "hideSingleSelectionIndicator"; "disabled": "disabled"; }, { "selectionChange": "selectionChange"; }, ["_items"], ["*"], false, never>;
255265
// (undocumented)
256266
static ɵfac: i0.ɵɵFactoryDeclaration<MatSelectionList, never>;
257267
}
@@ -277,6 +287,8 @@ export interface SelectionList extends MatListBase {
277287
// (undocumented)
278288
_emitChangeEvent(options: MatListOption[]): void;
279289
// (undocumented)
290+
hideSingleSelectionIndicator: boolean;
291+
// (undocumented)
280292
multiple: boolean;
281293
// (undocumented)
282294
_onTouched(): void;

0 commit comments

Comments
 (0)