Skip to content

Commit

Permalink
fix(material/list): add radio toggles (#25933)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
zarend committed Nov 22, 2022
1 parent 46d18a0 commit 57676e4
Show file tree
Hide file tree
Showing 20 changed files with 250 additions and 157 deletions.
26 changes: 13 additions & 13 deletions src/dev-app/list/list-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -126,21 +126,21 @@ <h2>Selection list</h2>
color="primary">
<div mat-subheader>Groceries</div>

<mat-list-option value="bananas" checkboxPosition="before">Bananas</mat-list-option>
<mat-list-option selected value="oranges">Oranges</mat-list-option>
<mat-list-option value="apples" color="accent">Apples</mat-list-option>
<mat-list-option value="strawberries" color="warn">Strawberries</mat-list-option>
<mat-list-option value="bananas" togglePosition="before">Bananas</mat-list-option>
<mat-list-option selected value="oranges" color="accent">Oranges</mat-list-option>
<mat-list-option value="apples" color="warn">Apples</mat-list-option>
<mat-list-option value="strawberries" disabled>Strawberries</mat-list-option>
</mat-selection-list>

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

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

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

<mat-list-option value="bananas">Bananas</mat-list-option>
<mat-list-option selected value="oranges">Oranges</mat-list-option>
<mat-list-option value="apples">Apples</mat-list-option>
<mat-list-option value="strawberries" color="warn">Strawberries</mat-list-option>
<mat-list-option selected value="oranges" color="accent">Oranges</mat-list-option>
<mat-list-option value="apples" color="warn">Apples</mat-list-option>
<mat-list-option value="strawberries" disabled>Strawberries</mat-list-option>
</mat-selection-list>

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

<mat-selection-list>
<mat-list-option value="bananas" [checkboxPosition]="checkboxPosition">
<mat-list-option value="bananas" [togglePosition]="togglePosition">
<mat-icon matListItemIcon>info</mat-icon>
Bananas
</mat-list-option>
<mat-list-option value="oranges" [checkboxPosition]="checkboxPosition">
<mat-list-option value="oranges" [togglePosition]="togglePosition">
<mat-icon matListItemIcon #ok>info</mat-icon>
Oranges
</mat-list-option>
<mat-list-option value="cake" [checkboxPosition]="checkboxPosition">
<mat-list-option value="cake" [togglePosition]="togglePosition">
<mat-icon matListItemIcon>info</mat-icon>
Cake
</mat-list-option>
<mat-list-option value="fries" [checkboxPosition]="checkboxPosition">
<mat-list-option value="fries" [togglePosition]="togglePosition">
<mat-icon matListItemIcon>info</mat-icon>
Fries
</mat-list-option>
Expand Down
6 changes: 3 additions & 3 deletions src/dev-app/list/list-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import {Component} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {MatButtonModule} from '@angular/material/button';
import {MatListModule, MatListOptionCheckboxPosition} from '@angular/material/list';
import {MatListModule, MatListOptionTogglePosition} from '@angular/material/list';
import {MatIconModule} from '@angular/material/icon';
import {CommonModule} from '@angular/common';

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

checkboxPosition: MatListOptionCheckboxPosition = 'before';
togglePosition: MatListOptionTogglePosition = 'before';

contacts: {name: string; headline: string}[] = [
{name: 'Nancy', headline: 'Software engineer'},
Expand Down Expand Up @@ -75,7 +75,7 @@ export class ListDemo {
}

toggleCheckboxPosition() {
this.checkboxPosition = this.checkboxPosition === 'before' ? 'after' : 'before';
this.togglePosition = this.togglePosition === 'before' ? 'after' : 'before';
}

favoriteOptions: string[] = [];
Expand Down
2 changes: 1 addition & 1 deletion src/material/legacy-list/selection-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export class MatLegacySelectionListChange {
/**
* Type describing possible positions of a checkbox in a list option
* with respect to the list item's text.
* @deprecated Use `MatListOptionCheckboxPosition` from `@angular/material/list` instead. See https://material.angular.io/guide/mdc-migration for information about migrating.
* @deprecated Use `MatListOptionTogglePosition` from `@angular/material/list` instead. See https://material.angular.io/guide/mdc-migration for information about migrating.
* @breaking-change 17.0.0
*/
export type MatLegacyListOptionCheckboxPosition = 'before' | 'after';
Expand Down
1 change: 1 addition & 0 deletions src/material/list/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ sass_library(
"//:mdc_sass_lib",
"//src/material/checkbox:checkbox_scss_lib",
"//src/material/core:core_scss_lib",
"//src/material/radio:radio_scss_lib",
],
)

Expand Down
9 changes: 6 additions & 3 deletions src/material/list/_list-option-theme.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@
@use '../core/mdc-helpers/mdc-helpers';
@use '../checkbox/checkbox-private';
@use './list-option-trailing-avatar-compat';
@use '../radio/radio-private';

// Mixin that overrides the selected item and checkbox colors for list options. By
// default, the MDC list uses the `primary` color for list items. The MDC checkbox
// inside list options by default uses the `primary` color too.
// Mixin that overrides the selected item and toggle indicator colors for list
// options. By default, the MDC list uses the `primary` color for list items.
// The MDC radio/checkbox inside list options by default uses the `primary`
// color too.
@mixin private-list-option-color-override($color-config, $color, $mdc-color) {
& .mdc-list-item__start, & .mdc-list-item__end {
@include checkbox-private.private-checkbox-styles-with-color($color-config, $color, $mdc-color);
@include radio-private.private-radio-color($color-config, $color);
}
}

Expand Down
18 changes: 9 additions & 9 deletions src/material/list/list-item-sections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,17 +52,17 @@ export class MatListItemMeta {}
/**
* @docs-private
*
* MDC uses the very intuitively named classes `.mdc-list-item__start` and `.mat-list-item__end`
* to position content such as icons or checkboxes that comes either before or after the text
* content respectively. This directive detects the placement of the checkbox and applies the
* MDC uses the very intuitively named classes `.mdc-list-item__start` and `.mat-list-item__end` to
* position content such as icons or checkboxes/radios that comes either before or after the text
* content respectively. This directive detects the placement of the checkbox/radio and applies the
* correct MDC class to position the icon/avatar on the opposite side.
*/
@Directive({
host: {
// MDC uses intuitively named classes `.mdc-list-item__start` and `.mat-list-item__end`
// to position content such as icons or checkboxes that comes either before or after the text
// content respectively. This directive detects the placement of the checkbox and applies the
// correct MDC class to position the icon/avatar on the opposite side.
// MDC uses intuitively named classes `.mdc-list-item__start` and `.mat-list-item__end` to
// position content such as icons or checkboxes/radios that comes either before or after the
// text content respectively. This directive detects the placement of the checkbox/radio and
// applies the correct MDC class to position the icon/avatar on the opposite side.
'[class.mdc-list-item__start]': '_isAlignedAtStart()',
'[class.mdc-list-item__end]': '!_isAlignedAtStart()',
},
Expand All @@ -72,8 +72,8 @@ export class _MatListItemGraphicBase {

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

Expand Down
6 changes: 3 additions & 3 deletions src/material/list/list-option-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,18 @@
import {InjectionToken} from '@angular/core';

/**
* Type describing possible positions of a checkbox in a list option
* Type describing possible positions of a checkbox or radio in a list option
* with respect to the list item's text.
*/
export type MatListOptionCheckboxPosition = 'before' | 'after';
export type MatListOptionTogglePosition = 'before' | 'after';

/**
* Interface describing a list option. This is used to avoid circular
* dependencies between the list-option and the styler directives.
* @docs-private
*/
export interface ListOption {
_getCheckboxPosition(): MatListOptionCheckboxPosition;
_getTogglePosition(): MatListOptionTogglePosition;
}

/**
Expand Down
22 changes: 21 additions & 1 deletion src/material/list/list-option.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!--
Save icons and the pseudo checkbox so that they can be re-used in the template without
Save icons and the pseudo checkbox/radio so that they can be re-used in the template without
duplication. Also content can only be injected once so we need to extract icons/avatars
into a template since we use it in multiple places.
-->
Expand All @@ -25,11 +25,27 @@
</div>
</ng-template>

<ng-template #radio>
<div class="mdc-radio" [class.mdc-radio--disabled]="disabled">
<input type="radio" class="mdc-radio__native-control"
[checked]="selected" [disabled]="disabled"/>
<div class="mdc-radio__background">
<div class="mdc-radio__outer-circle"></div>
<div class="mdc-radio__inner-circle"></div>
</div>
</div>
</ng-template>

<!-- Container for the checkbox at start. -->
<span class="mdc-list-item__start mat-mdc-list-option-checkbox-before"
*ngIf="_hasCheckboxAt('before')">
<ng-template [ngTemplateOutlet]="checkbox"></ng-template>
</span>
<!-- Container for the radio at the start. -->
<span class="mdc-list-item__start mat-mdc-list-option-radio-before"
*ngIf="_hasRadioAt('before')">
<ng-template [ngTemplateOutlet]="radio"></ng-template>
</span>
<!-- Conditionally renders icons/avatars before the list item text. -->
<ng-template [ngIf]="_hasIconsOrAvatarsAt('before')">
<ng-template [ngTemplateOutlet]="icons"></ng-template>
Expand All @@ -49,6 +65,10 @@
<span class="mdc-list-item__end" *ngIf="_hasCheckboxAt('after')">
<ng-template [ngTemplateOutlet]="checkbox"></ng-template>
</span>
<!-- Container for the radio at the end. -->
<span class="mdc-list-item__end" *ngIf="_hasRadioAt('after')">
<ng-template [ngTemplateOutlet]="radio"></ng-template>
</span>
<!-- Conditionally renders icons/avatars after the list item text. -->
<ng-template [ngIf]="_hasIconsOrAvatarsAt('after')">
<ng-template [ngTemplateOutlet]="icons"></ng-template>
Expand Down
47 changes: 33 additions & 14 deletions src/material/list/list-option.scss
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
@use 'sass:map';
@use '@material/checkbox/checkbox' as mdc-checkbox;
@use '@material/checkbox/checkbox-theme' as mdc-checkbox-theme;
@use '@material/radio/radio' as mdc-radio;
@use '@material/radio/radio-theme' as mdc-radio-theme;

@use '../core/mdc-helpers/mdc-helpers';
@use '../checkbox/checkbox-private';
@use '../radio/radio-private';
@use './list-option-trailing-avatar-compat';
@use './list-item-hcm-indicator';

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

.mat-mdc-list-option {
// The MDC-based list-option uses the MDC checkbox for the selection indicators.
// We need to ensure that the checkbox styles are not included for the list-option.
// The MDC-based list-option uses the MDC checkbox/radio for the selection indicators.
// We need to ensure that the checkbox and radio styles are not included for the list-option.
@include mdc-helpers.disable-mdc-fallback-declarations {
@include mdc-checkbox.static-styles(
$query: mdc-helpers.$mdc-base-styles-without-animation-query);
@include mdc-radio.static-styles(
$query: mdc-helpers.$mdc-base-styles-without-animation-query);

&:not(._mat-animation-noopable) {
@include mdc-checkbox.static-styles($query: animation);
@include mdc-radio.static-styles($query: animation);
}
}

// We can't use the MDC checkbox here directly, because this checkbox is purely
// decorative and including the MDC one will bring in unnecessary JS.
.mdc-checkbox {
$config: map.merge(checkbox-private.$private-checkbox-theme-config, (
// Since this checkbox isn't interactive, we can exclude the focus/hover/press styles.
$without-ripple-config: (
// Since this checkbox/radio isn't interactive, we can exclude the focus/hover/press styles.
selected-focus-icon-color: null,
selected-hover-icon-color: null,
selected-pressed-icon-color: null,
unselected-focus-icon-color: null,
unselected-hover-icon-color: null,
unselected-pressed-icon-color: null,
));
);

// We can't use the MDC checkbox here directly, because this checkbox is purely
// decorative and including the MDC one will bring in unnecessary JS.
.mdc-checkbox {
$config: map.merge(checkbox-private.$private-checkbox-theme-config, $without-ripple-config);

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

// The internal checkbox is purely decorative, but because it's an `input`, the user can still
// focus it by tabbing or clicking. Furthermore, `mat-list-option` has the `option` role which
// doesn't allow a nested `input`. We use `display: none` both to remove it from the tab order
// and to prevent focus from reaching it through the screen reader's forms mode. Ideally we'd
// remove the `input` completely, but we can't because MDC uses a `:checked` selector to
// We can't use the MDC radio here directly, because this radio is purely
// decorative and including the MDC one will bring in unnecessary JS.
.mdc-radio {
$config: map.merge(radio-private.$private-radio-theme-config, $without-ripple-config);

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


// The internal checkbox/radio is purely decorative, but because it's an `input`, the user can
// still focus it by tabbing or clicking. Furthermore, `mat-list-option` has the `option` role
// which doesn't allow a nested `input`. We use `display: none` both to remove it from the tab
// order and to prevent focus from reaching it through the screen reader's forms mode. Ideally
// we'd remove the `input` completely, but we can't because MDC uses a `:checked` selector to
// toggle the selected styles.
.mdc-checkbox__native-control {
.mdc-checkbox__native-control, .mdc-radio__native-control {
display: none;
}
}
Expand Down

0 comments on commit 57676e4

Please sign in to comment.