Skip to content

Commit 57676e4

Browse files
authored
fix(material/list): add radio toggles (#25933)
Add radio toggles for single selection. Fix a11y issue where selected state is visually communicated with color alone. Rename `checkboxPosition` Input to `togglePosition` and deprecate `checkboxPosition`. `togglePosition` configures the position of both the radio and checkbox indicators. `checkboxPosition` also configures the position of both. Summary of API and behavior changes: - MDC List displays radio indicators for single-selection - rename `checkboxPosition` Input to `togglePosition` - rename `type MatListOptionCheckboxPosition` to `type MatListOptionTogglePosition` DEPRECTED: * `checkboxPosition` is deprecated because `togglePosition` replaces it * `MatListOptionCheckboxPosition` is deprecated because `MatListOptionTogglePosition` replaces it Closes #7157, Fixes #25900
1 parent 46d18a0 commit 57676e4

20 files changed

+250
-157
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: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@
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

6-
// Mixin that overrides the selected item and checkbox colors for list options. By
7-
// default, the MDC list uses the `primary` color for list items. The MDC checkbox
8-
// inside list options by default uses the `primary` color too.
7+
// Mixin that overrides the selected item and toggle indicator colors for list
8+
// options. By default, the MDC list uses the `primary` color for list items.
9+
// The MDC radio/checkbox inside list options by default uses the `primary`
10+
// color too.
911
@mixin private-list-option-color-override($color-config, $color, $mdc-color) {
1012
& .mdc-list-item__start, & .mdc-list-item__end {
1113
@include checkbox-private.private-checkbox-styles-with-color($color-config, $color, $mdc-color);
14+
@include radio-private.private-radio-color($color-config, $color);
1215
}
1316
}
1417

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

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -52,17 +52,17 @@ export class MatListItemMeta {}
5252
/**
5353
* @docs-private
5454
*
55-
* MDC uses the very intuitively named classes `.mdc-list-item__start` and `.mat-list-item__end`
56-
* to position content such as icons or checkboxes that comes either before or after the text
57-
* content respectively. This directive detects the placement of the checkbox and applies the
55+
* MDC uses the very intuitively named classes `.mdc-list-item__start` and `.mat-list-item__end` to
56+
* position content such as icons or checkboxes/radios that comes either before or after the text
57+
* content respectively. This directive detects the placement of the checkbox/radio and applies the
5858
* correct MDC class to position the icon/avatar on the opposite side.
5959
*/
6060
@Directive({
6161
host: {
62-
// MDC uses intuitively named classes `.mdc-list-item__start` and `.mat-list-item__end`
63-
// to position content such as icons or checkboxes that comes either before or after the text
64-
// content respectively. This directive detects the placement of the checkbox and applies the
65-
// correct MDC class to position the icon/avatar on the opposite side.
62+
// MDC uses intuitively named classes `.mdc-list-item__start` and `.mat-list-item__end` to
63+
// position content such as icons or checkboxes/radios that comes either before or after the
64+
// text content respectively. This directive detects the placement of the checkbox/radio and
65+
// applies the correct MDC class to position the icon/avatar on the opposite side.
6666
'[class.mdc-list-item__start]': '_isAlignedAtStart()',
6767
'[class.mdc-list-item__end]': '!_isAlignedAtStart()',
6868
},
@@ -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: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,18 @@
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.
1414
*/
15-
export type MatListOptionCheckboxPosition = 'before' | 'after';
15+
export type MatListOptionTogglePosition = 'before' | 'after';
1616

1717
/**
1818
* Interface describing a list option. This is used to avoid circular
1919
* dependencies between the list-option and the styler directives.
2020
* @docs-private
2121
*/
2222
export interface ListOption {
23-
_getCheckboxPosition(): MatListOptionCheckboxPosition;
23+
_getTogglePosition(): MatListOptionTogglePosition;
2424
}
2525

2626
/**

src/material/list/list-option.html

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<!--
2-
Save icons and the pseudo checkbox so that they can be re-used in the template without
2+
Save icons and the pseudo checkbox/radio so that they can be re-used in the template without
33
duplication. Also content can only be injected once so we need to extract icons/avatars
44
into a template since we use it in multiple places.
55
-->
@@ -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: 33 additions & 14 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

@@ -12,42 +15,58 @@
1215
@include list-option-trailing-avatar-compat.core-styles($query: mdc-helpers.$mdc-base-styles-query);
1316

1417
.mat-mdc-list-option {
15-
// The MDC-based list-option uses the MDC checkbox for the selection indicators.
16-
// We need to ensure that the checkbox styles are not included for the list-option.
18+
// The MDC-based list-option uses the MDC checkbox/radio for the selection indicators.
19+
// We need to ensure that the checkbox and radio styles are not included for the list-option.
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

44-
// The internal checkbox is purely decorative, but because it's an `input`, the user can still
45-
// focus it by tabbing or clicking. Furthermore, `mat-list-option` has the `option` role which
46-
// doesn't allow a nested `input`. We use `display: none` both to remove it from the tab order
47-
// and to prevent focus from reaching it through the screen reader's forms mode. Ideally we'd
48-
// remove the `input` completely, but we can't because MDC uses a `:checked` selector to
52+
// We can't use the MDC radio here directly, because this radio is purely
53+
// decorative and including the MDC one will bring in unnecessary JS.
54+
.mdc-radio {
55+
$config: map.merge(radio-private.$private-radio-theme-config, $without-ripple-config);
56+
57+
// MDC theme styles also include structural styles so we have to include the theme at least
58+
// once here. The values will be overwritten by our own theme file afterwards.
59+
@include mdc-radio-theme.theme-styles($config);
60+
}
61+
62+
63+
// The internal checkbox/radio is purely decorative, but because it's an `input`, the user can
64+
// still focus it by tabbing or clicking. Furthermore, `mat-list-option` has the `option` role
65+
// which doesn't allow a nested `input`. We use `display: none` both to remove it from the tab
66+
// order and to prevent focus from reaching it through the screen reader's forms mode. Ideally
67+
// we'd remove the `input` completely, but we can't because MDC uses a `:checked` selector to
4968
// toggle the selected styles.
50-
.mdc-checkbox__native-control {
69+
.mdc-checkbox__native-control, .mdc-radio__native-control {
5170
display: none;
5271
}
5372
}

0 commit comments

Comments
 (0)