Skip to content

Commit 18ea893

Browse files
committed
fix(material/list): add radio toggles
Add radio toggles for single selection. Fix a11y issue where selected state is visually communicated with color alone. Add `togglePosition` Input, which configures the position of both the radio and checkbox. Deprecates `checkboxPosition`, which will configure the position of both the radio and checkbox. Summary of API and behavior changes - MDC List displays radio indicators for single-selection - add `togglePosition` Input to to configure position of both radio and checkbox - deprecate `checkboxPosition`, which is a shim for `togglePosition` Closes #7157, Fixes #25900
1 parent 7e3a9df commit 18ea893

16 files changed

+201
-108
lines changed

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

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -126,21 +126,21 @@ <h2>Selection list</h2>
126126
color="primary">
127127
<div mat-subheader>Groceries</div>
128128

129-
<mat-list-option value="bananas" checkboxPosition="before">Bananas</mat-list-option>
130-
<mat-list-option selected value="oranges">Oranges</mat-list-option>
131-
<mat-list-option value="apples" color="accent">Apples</mat-list-option>
132-
<mat-list-option value="strawberries" color="warn">Strawberries</mat-list-option>
129+
<mat-list-option value="bananas" togglePosition="before">Bananas</mat-list-option>
130+
<mat-list-option selected value="oranges" color="accent">Oranges</mat-list-option>
131+
<mat-list-option value="apples" color="warn">Apples</mat-list-option>
132+
<mat-list-option value="strawberries" disabled>Strawberries</mat-list-option>
133133
</mat-selection-list>
134134

135135
<mat-selection-list [disableRipple]="selectionListRippleDisabled">
136136
<div mat-subheader>Dogs</div>
137137

138-
<mat-list-option checkboxPosition="before">
138+
<mat-list-option togglePosition="before">
139139
<img matListItemAvatar src="https://material.angular.io/assets/img/examples/shiba1.jpg">
140140
<span matListItemTitle>Shiba Inu</span>
141141
</mat-list-option>
142142

143-
<mat-list-option checkboxPosition="after">
143+
<mat-list-option togglePosition="after">
144144
<img matListItemAvatar src="https://material.angular.io/assets/img/examples/shiba2.jpg">
145145
<span matListItemTitle>Other Shiba Inu</span>
146146
</mat-list-option>
@@ -177,9 +177,9 @@ <h2>Single Selection list</h2>
177177
<div mat-subheader>Favorite Grocery</div>
178178

179179
<mat-list-option value="bananas">Bananas</mat-list-option>
180-
<mat-list-option selected value="oranges">Oranges</mat-list-option>
181-
<mat-list-option value="apples">Apples</mat-list-option>
182-
<mat-list-option value="strawberries" color="warn">Strawberries</mat-list-option>
180+
<mat-list-option selected value="oranges" color="accent">Oranges</mat-list-option>
181+
<mat-list-option value="apples" color="warn">Apples</mat-list-option>
182+
<mat-list-option value="strawberries" disabled>Strawberries</mat-list-option>
183183
</mat-selection-list>
184184

185185
<p>Selected: {{favoriteOptions | json}}</p>
@@ -239,19 +239,19 @@ <h2>Line alignment</h2>
239239
<h2>Icon alignment in selection list</h2>
240240

241241
<mat-selection-list>
242-
<mat-list-option value="bananas" [checkboxPosition]="checkboxPosition">
242+
<mat-list-option value="bananas" [togglePosition]="togglePosition">
243243
<mat-icon matListItemIcon>info</mat-icon>
244244
Bananas
245245
</mat-list-option>
246-
<mat-list-option value="oranges" [checkboxPosition]="checkboxPosition">
246+
<mat-list-option value="oranges" [togglePosition]="togglePosition">
247247
<mat-icon matListItemIcon #ok>info</mat-icon>
248248
Oranges
249249
</mat-list-option>
250-
<mat-list-option value="cake" [checkboxPosition]="checkboxPosition">
250+
<mat-list-option value="cake" [togglePosition]="togglePosition">
251251
<mat-icon matListItemIcon>info</mat-icon>
252252
Cake
253253
</mat-list-option>
254-
<mat-list-option value="fries" [checkboxPosition]="checkboxPosition">
254+
<mat-list-option value="fries" [togglePosition]="togglePosition">
255255
<mat-icon matListItemIcon>info</mat-icon>
256256
Fries
257257
</mat-list-option>

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import {Component} from '@angular/core';
1010
import {FormsModule} from '@angular/forms';
1111
import {MatButtonModule} from '@angular/material/button';
12-
import {MatListModule, MatListOptionCheckboxPosition} from '@angular/material/list';
12+
import {MatListModule, MatListOptionTogglePosition} from '@angular/material/list';
1313
import {MatIconModule} from '@angular/material/icon';
1414
import {CommonModule} from '@angular/common';
1515

@@ -23,7 +23,7 @@ import {CommonModule} from '@angular/common';
2323
export class ListDemo {
2424
items: string[] = ['Pepper', 'Salt', 'Paprika'];
2525

26-
checkboxPosition: MatListOptionCheckboxPosition = 'before';
26+
togglePosition: MatListOptionTogglePosition = 'before';
2727

2828
contacts: {name: string; headline: string}[] = [
2929
{name: 'Nancy', headline: 'Software engineer'},
@@ -75,7 +75,7 @@ export class ListDemo {
7575
}
7676

7777
toggleCheckboxPosition() {
78-
this.checkboxPosition = this.checkboxPosition === 'before' ? 'after' : 'before';
78+
this.togglePosition = this.togglePosition === 'before' ? 'after' : 'before';
7979
}
8080

8181
favoriteOptions: string[] = [];

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ export class MatLegacySelectionListChange {
7474
/**
7575
* Type describing possible positions of a checkbox in a list option
7676
* with respect to the list item's text.
77-
* @deprecated Use `MatListOptionCheckboxPosition` from `@angular/material/list` instead. See https://material.angular.io/guide/mdc-migration for information about migrating.
77+
* @deprecated Use `MatListOptionTogglePosition` from `@angular/material/list` instead. See https://material.angular.io/guide/mdc-migration for information about migrating.
7878
* @breaking-change 17.0.0
7979
*/
8080
export type MatLegacyListOptionCheckboxPosition = 'before' | 'after';

src/material/list/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ sass_library(
5252
"//:mdc_sass_lib",
5353
"//src/material/checkbox:checkbox_scss_lib",
5454
"//src/material/core:core_scss_lib",
55+
"//src/material/radio:radio_scss_lib",
5556
],
5657
)
5758

src/material/list/_list-option-theme.scss

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22
@use '../core/mdc-helpers/mdc-helpers';
33
@use '../checkbox/checkbox-private';
44
@use './list-option-trailing-avatar-compat';
5+
@use '../radio/radio-private';
56

67
// Mixin that overrides the selected item and checkbox colors for list options. By
78
// default, the MDC list uses the `primary` color for list items. The MDC checkbox
89
// inside list options by default uses the `primary` color too.
910
@mixin private-list-option-color-override($color-config, $color, $mdc-color) {
1011
& .mdc-list-item__start, & .mdc-list-item__end {
1112
@include checkbox-private.private-checkbox-styles-with-color($color-config, $color, $mdc-color);
13+
@include radio-private.private-radio-color($color-config, $color);
1214
}
1315
}
1416

src/material/list/list-item-sections.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@ export class _MatListItemGraphicBase {
7272

7373
_isAlignedAtStart() {
7474
// By default, in all list items the graphic is aligned at start. In list options,
75-
// the graphic is only aligned at start if the checkbox is at the end.
76-
return !this._listOption || this._listOption?._getCheckboxPosition() === 'after';
75+
// the graphic is only aligned at start if the checkbox/radio is at the end.
76+
return !this._listOption || this._listOption?._getTogglePosition() === 'after';
7777
}
7878
}
7979

src/material/list/list-option-types.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,27 @@
99
import {InjectionToken} from '@angular/core';
1010

1111
/**
12-
* Type describing possible positions of a checkbox in a list option
12+
* Type describing possible positions of a checkbox or radio in a list option
1313
* with respect to the list item's text.
14+
*
15+
* @deprecated Use `MatListOptionTogglePosition` instead.
16+
* @breaking-change 17.0.0
1417
*/
1518
export type MatListOptionCheckboxPosition = 'before' | 'after';
1619

20+
/**
21+
* Type describing possible positions of a checkbox or radio in a list option
22+
* with respect to the list item's text.
23+
*/
24+
export type MatListOptionTogglePosition = 'before' | 'after';
25+
1726
/**
1827
* Interface describing a list option. This is used to avoid circular
1928
* dependencies between the list-option and the styler directives.
2029
* @docs-private
2130
*/
2231
export interface ListOption {
23-
_getCheckboxPosition(): MatListOptionCheckboxPosition;
32+
_getTogglePosition(): MatListOptionTogglePosition;
2433
}
2534

2635
/**

src/material/list/list-option.html

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,27 @@
2525
</div>
2626
</ng-template>
2727

28+
<ng-template #radio>
29+
<div class="mdc-radio" [class.mdc-radio--disabled]="disabled">
30+
<input type="radio" class="mdc-radio__native-control"
31+
[checked]="selected" [disabled]="disabled"/>
32+
<div class="mdc-radio__background">
33+
<div class="mdc-radio__outer-circle"></div>
34+
<div class="mdc-radio__inner-circle"></div>
35+
</div>
36+
</div>
37+
</ng-template>
38+
2839
<!-- Container for the checkbox at start. -->
2940
<span class="mdc-list-item__start mat-mdc-list-option-checkbox-before"
3041
*ngIf="_hasCheckboxAt('before')">
3142
<ng-template [ngTemplateOutlet]="checkbox"></ng-template>
3243
</span>
44+
<!-- Container for the radio at the start. -->
45+
<span class="mdc-list-item__start mat-mdc-list-option-radio-before"
46+
*ngIf="_hasRadioAt('before')">
47+
<ng-template [ngTemplateOutlet]="radio"></ng-template>
48+
</span>
3349
<!-- Conditionally renders icons/avatars before the list item text. -->
3450
<ng-template [ngIf]="_hasIconsOrAvatarsAt('before')">
3551
<ng-template [ngTemplateOutlet]="icons"></ng-template>
@@ -49,6 +65,10 @@
4965
<span class="mdc-list-item__end" *ngIf="_hasCheckboxAt('after')">
5066
<ng-template [ngTemplateOutlet]="checkbox"></ng-template>
5167
</span>
68+
<!-- Container for the radio at the end. -->
69+
<span class="mdc-list-item__end" *ngIf="_hasRadioAt('after')">
70+
<ng-template [ngTemplateOutlet]="radio"></ng-template>
71+
</span>
5272
<!-- Conditionally renders icons/avatars after the list item text. -->
5373
<ng-template [ngIf]="_hasIconsOrAvatarsAt('after')">
5474
<ng-template [ngTemplateOutlet]="icons"></ng-template>

src/material/list/list-option.scss

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
@use 'sass:map';
22
@use '@material/checkbox/checkbox' as mdc-checkbox;
33
@use '@material/checkbox/checkbox-theme' as mdc-checkbox-theme;
4+
@use '@material/radio/radio' as mdc-radio;
5+
@use '@material/radio/radio-theme' as mdc-radio-theme;
46

57
@use '../core/mdc-helpers/mdc-helpers';
68
@use '../checkbox/checkbox-private';
9+
@use '../radio/radio-private';
710
@use './list-option-trailing-avatar-compat';
811
@use './list-item-hcm-indicator';
912

@@ -17,30 +20,44 @@
1720
@include mdc-helpers.disable-mdc-fallback-declarations {
1821
@include mdc-checkbox.static-styles(
1922
$query: mdc-helpers.$mdc-base-styles-without-animation-query);
23+
@include mdc-radio.static-styles(
24+
$query: mdc-helpers.$mdc-base-styles-without-animation-query);
2025

2126
&:not(._mat-animation-noopable) {
2227
@include mdc-checkbox.static-styles($query: animation);
28+
@include mdc-radio.static-styles($query: animation);
2329
}
2430
}
2531

26-
// We can't use the MDC checkbox here directly, because this checkbox is purely
27-
// decorative and including the MDC one will bring in unnecessary JS.
28-
.mdc-checkbox {
29-
$config: map.merge(checkbox-private.$private-checkbox-theme-config, (
30-
// Since this checkbox isn't interactive, we can exclude the focus/hover/press styles.
32+
$without-ripple-config: (
33+
// Since this checkbox/radio isn't interactive, we can exclude the focus/hover/press styles.
3134
selected-focus-icon-color: null,
3235
selected-hover-icon-color: null,
3336
selected-pressed-icon-color: null,
3437
unselected-focus-icon-color: null,
3538
unselected-hover-icon-color: null,
3639
unselected-pressed-icon-color: null,
37-
));
40+
);
41+
42+
// We can't use the MDC checkbox here directly, because this checkbox is purely
43+
// decorative and including the MDC one will bring in unnecessary JS.
44+
.mdc-checkbox {
45+
$config: map.merge(checkbox-private.$private-checkbox-theme-config, $without-ripple-config);
3846

3947
// MDC theme styles also include structural styles so we have to include the theme at least
4048
// once here. The values will be overwritten by our own theme file afterwards.
4149
@include mdc-checkbox-theme.theme-styles($config);
4250
}
4351

52+
.mdc-radio {
53+
$config: map.merge(radio-private.$private-radio-theme-config, $without-ripple-config);
54+
55+
// MDC theme styles also include structural styles so we have to include the theme at least
56+
// once here. The values will be overwritten by our own theme file afterwards.
57+
@include mdc-radio-theme.theme-styles($config);
58+
}
59+
60+
4461
// The internal checkbox is purely decorative, but because it's an `input`, the user can still
4562
// focus it by tabbing or clicking. Furthermore, `mat-list-option` has the `option` role which
4663
// doesn't allow a nested `input`. We use `display: none` both to remove it from the tab order
@@ -50,6 +67,9 @@
5067
.mdc-checkbox__native-control {
5168
display: none;
5269
}
70+
.mdc-radio__native-control {
71+
display: none;
72+
}
5373
}
5474

5575
.mat-mdc-list-option.mdc-list-item--selected {

src/material/list/list-option.ts

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,12 @@ import {
3030
} from '@angular/core';
3131
import {MAT_RIPPLE_GLOBAL_OPTIONS, RippleGlobalOptions, ThemePalette} from '@angular/material/core';
3232
import {MatListBase, MatListItemBase} from './list-base';
33-
import {LIST_OPTION, ListOption, MatListOptionCheckboxPosition} from './list-option-types';
33+
import {
34+
LIST_OPTION,
35+
ListOption,
36+
MatListOptionTogglePosition,
37+
MatListOptionCheckboxPosition,
38+
} from './list-option-types';
3439
import {MatListItemLine, MatListItemTitle} from './list-item-sections';
3540
import {Platform} from '@angular/cdk/platform';
3641

@@ -77,6 +82,8 @@ export interface SelectionList extends MatListBase {
7782
// which ensure that the checkbox is positioned correctly within the list item.
7883
'[class.mdc-list-item--with-leading-checkbox]': '_hasCheckboxAt("before")',
7984
'[class.mdc-list-item--with-trailing-checkbox]': '_hasCheckboxAt("after")',
85+
'[class.mdc-list-item--with-leading-radio]': '_hasRadioAt("before")',
86+
'[class.mdc-list-item--with-trailing-radio]': '_hasRadioAt("after")',
8087
'[class.mat-accent]': 'color !== "primary" && color !== "warn"',
8188
'[class.mat-warn]': 'color === "warn"',
8289
'[class._mat-animation-noopable]': '_noopAnimations',
@@ -105,8 +112,21 @@ export class MatListOption extends MatListItemBase implements ListOption, OnInit
105112
@Output()
106113
readonly selectedChange: EventEmitter<boolean> = new EventEmitter<boolean>();
107114

108-
/** Whether the label should appear before or after the checkbox. Defaults to 'after' */
109-
@Input() checkboxPosition: MatListOptionCheckboxPosition = 'after';
115+
/** Whether the label should appear before or after the checkbox/radio. Defaults to 'after' */
116+
@Input() togglePosition: MatListOptionTogglePosition = 'after';
117+
118+
/**
119+
* Whether the label should appear before or after the checkbox/radio. Defaults to 'after'
120+
*
121+
* @deprecated Use `togglePosition` instead.
122+
* @breaking-change 17.0.0
123+
*/
124+
@Input() get checkboxPosition(): MatListOptionCheckboxPosition {
125+
return this.togglePosition;
126+
}
127+
set checkboxPosition(value: MatListOptionCheckboxPosition) {
128+
this.togglePosition = value;
129+
}
110130

111131
/** Theme color of the list option. This sets the color of the checkbox. */
112132
@Input()
@@ -225,8 +245,13 @@ export class MatListOption extends MatListItemBase implements ListOption, OnInit
225245
}
226246

227247
/** Whether a checkbox is shown at the given position. */
228-
_hasCheckboxAt(position: MatListOptionCheckboxPosition): boolean {
229-
return this._selectionList.multiple && this._getCheckboxPosition() === position;
248+
_hasCheckboxAt(position: MatListOptionTogglePosition): boolean {
249+
return this._selectionList.multiple && this._getTogglePosition() === position;
250+
}
251+
252+
/** Where a radio indicator is shown at the given position. */
253+
_hasRadioAt(position: MatListOptionTogglePosition): boolean {
254+
return !this._selectionList.multiple && this._getTogglePosition() === position;
230255
}
231256

232257
/** Whether icons or avatars are shown at the given position. */
@@ -239,7 +264,7 @@ export class MatListOption extends MatListItemBase implements ListOption, OnInit
239264
// If the checkbox is shown at the specified position, neither icons or
240265
// avatars can be shown at the position.
241266
return (
242-
this._getCheckboxPosition() !== position &&
267+
this._getTogglePosition() !== position &&
243268
(type === 'avatars' ? this._avatars.length !== 0 : this._icons.length !== 0)
244269
);
245270
}
@@ -248,9 +273,9 @@ export class MatListOption extends MatListItemBase implements ListOption, OnInit
248273
this._selectionList._onTouched();
249274
}
250275

251-
/** Gets the current position of the checkbox. */
252-
_getCheckboxPosition() {
253-
return this.checkboxPosition || 'after';
276+
/** Gets the current position of the checkbox/radio. */
277+
_getTogglePosition() {
278+
return this.togglePosition || 'after';
254279
}
255280

256281
/**

src/material/list/public-api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,5 @@ export * from './list-option';
1515
export * from './subheader';
1616
export * from './list-item-sections';
1717

18-
export {MatListOptionCheckboxPosition} from './list-option-types';
18+
export {MatListOptionTogglePosition, MatListOptionCheckboxPosition} from './list-option-types';
1919
export {MatListOption} from './list-option';

src/material/radio/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ sass_binary(
4242
name = "radio_scss",
4343
src = "radio.scss",
4444
deps = [
45+
":radio_scss_lib",
4546
"//:mdc_sass_lib",
4647
"//src/material/core:core_scss_lib",
4748
],

0 commit comments

Comments
 (0)