From eccc4a8bfb001fa9664da16cbf010d4f993fcbb4 Mon Sep 17 00:00:00 2001 From: Zach Arend Date: Tue, 1 Nov 2022 22:05:52 +0000 Subject: [PATCH] fix(material/list): add radio toggles 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 --- src/dev-app/list/list-demo.html | 26 ++++---- src/dev-app/list/list-demo.ts | 6 +- src/material/legacy-list/selection-list.ts | 2 +- src/material/list/BUILD.bazel | 1 + src/material/list/_list-option-theme.scss | 9 ++- src/material/list/list-item-sections.ts | 18 +++--- src/material/list/list-option-types.ts | 6 +- src/material/list/list-option.html | 22 ++++++- src/material/list/list-option.scss | 47 ++++++++++----- src/material/list/list-option.ts | 48 ++++++++++----- src/material/list/public-api.ts | 10 +++- src/material/list/selection-list.spec.ts | 48 +++++++-------- .../list/testing/list-harness.spec.ts | 2 +- .../list/testing/selection-list-harness.ts | 10 +++- src/material/radio/BUILD.bazel | 1 + src/material/radio/_radio-private.scss | 60 +++++++++++++++++++ src/material/radio/_radio-theme.scss | 52 +++------------- src/material/radio/radio.scss | 18 +----- .../public_api_guard/material/list-testing.md | 5 +- tools/public_api_guard/material/list.md | 16 +++-- 20 files changed, 250 insertions(+), 157 deletions(-) create mode 100644 src/material/radio/_radio-private.scss diff --git a/src/dev-app/list/list-demo.html b/src/dev-app/list/list-demo.html index 13ec4aec9b94..fbb560fdf191 100644 --- a/src/dev-app/list/list-demo.html +++ b/src/dev-app/list/list-demo.html @@ -126,21 +126,21 @@

Selection list

color="primary">
Groceries
- Bananas - Oranges - Apples - Strawberries + Bananas + Oranges + Apples + Strawberries
Dogs
- + Shiba Inu - + Other Shiba Inu @@ -177,9 +177,9 @@

Single Selection list

Favorite Grocery
Bananas - Oranges - Apples - Strawberries + Oranges + Apples + Strawberries

Selected: {{favoriteOptions | json}}

@@ -239,19 +239,19 @@

Line alignment

Icon alignment in selection list

- + info Bananas - + info Oranges - + info Cake - + info Fries diff --git a/src/dev-app/list/list-demo.ts b/src/dev-app/list/list-demo.ts index 9ddc9ed36c71..bad097b791fa 100644 --- a/src/dev-app/list/list-demo.ts +++ b/src/dev-app/list/list-demo.ts @@ -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'; @@ -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'}, @@ -75,7 +75,7 @@ export class ListDemo { } toggleCheckboxPosition() { - this.checkboxPosition = this.checkboxPosition === 'before' ? 'after' : 'before'; + this.togglePosition = this.togglePosition === 'before' ? 'after' : 'before'; } favoriteOptions: string[] = []; diff --git a/src/material/legacy-list/selection-list.ts b/src/material/legacy-list/selection-list.ts index 9afffc43ab88..b8736775da63 100644 --- a/src/material/legacy-list/selection-list.ts +++ b/src/material/legacy-list/selection-list.ts @@ -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'; diff --git a/src/material/list/BUILD.bazel b/src/material/list/BUILD.bazel index 8cca815c05fb..c0bf202baa63 100644 --- a/src/material/list/BUILD.bazel +++ b/src/material/list/BUILD.bazel @@ -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", ], ) diff --git a/src/material/list/_list-option-theme.scss b/src/material/list/_list-option-theme.scss index 021053ee259b..1569e9865dde 100644 --- a/src/material/list/_list-option-theme.scss +++ b/src/material/list/_list-option-theme.scss @@ -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); } } diff --git a/src/material/list/list-item-sections.ts b/src/material/list/list-item-sections.ts index 78101de89688..6a212923b06c 100644 --- a/src/material/list/list-item-sections.ts +++ b/src/material/list/list-item-sections.ts @@ -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()', }, @@ -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'; } } diff --git a/src/material/list/list-option-types.ts b/src/material/list/list-option-types.ts index fe6557ea6f1e..c65118ba3223 100644 --- a/src/material/list/list-option-types.ts +++ b/src/material/list/list-option-types.ts @@ -9,10 +9,10 @@ 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 @@ -20,7 +20,7 @@ export type MatListOptionCheckboxPosition = 'before' | 'after'; * @docs-private */ export interface ListOption { - _getCheckboxPosition(): MatListOptionCheckboxPosition; + _getTogglePosition(): MatListOptionTogglePosition; } /** diff --git a/src/material/list/list-option.html b/src/material/list/list-option.html index 3dd89050aa65..5d11eb2a5d0e 100644 --- a/src/material/list/list-option.html +++ b/src/material/list/list-option.html @@ -1,5 +1,5 @@ @@ -25,11 +25,27 @@ + +
+ +
+
+
+
+
+
+ + + + + @@ -49,6 +65,10 @@ + + + + diff --git a/src/material/list/list-option.scss b/src/material/list/list-option.scss index ea5e002a90af..8e06be1352ad 100644 --- a/src/material/list/list-option.scss +++ b/src/material/list/list-option.scss @@ -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'; @@ -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; } } diff --git a/src/material/list/list-option.ts b/src/material/list/list-option.ts index 87f21b4fdb69..c7795e0cf62a 100644 --- a/src/material/list/list-option.ts +++ b/src/material/list/list-option.ts @@ -30,7 +30,7 @@ import { } from '@angular/core'; import {MAT_RIPPLE_GLOBAL_OPTIONS, RippleGlobalOptions, ThemePalette} from '@angular/material/core'; import {MatListBase, MatListItemBase} from './list-base'; -import {LIST_OPTION, ListOption, MatListOptionCheckboxPosition} from './list-option-types'; +import {LIST_OPTION, ListOption, MatListOptionTogglePosition} from './list-option-types'; import {MatListItemLine, MatListItemTitle} from './list-item-sections'; import {Platform} from '@angular/cdk/platform'; @@ -67,16 +67,18 @@ export interface SelectionList extends MatListBase { // As per MDC, only list items in single selection mode should receive the `--selected` // class. For multi selection, the checkbox is used as indicator. '[class.mdc-list-item--selected]': 'selected && !_selectionList.multiple', - // Based on the checkbox position and whether there are icons or avatars, we apply MDC's + // 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")', '[class.mdc-list-item--with-leading-icon]': '_hasProjected("icons", "before")', '[class.mdc-list-item--with-trailing-icon]': '_hasProjected("icons", "after")', '[class.mat-mdc-list-option-with-trailing-avatar]': '_hasProjected("avatars", "after")', - // Based on the checkbox position, we apply the `--leading` or `--trailing` MDC classes - // which ensure that the checkbox is positioned correctly within the list item. + // Based on the checkbox/radio position, we apply the `--leading` or `--trailing` MDC classes + // which ensure that the checkbox/radio is positioned correctly within the list item. '[class.mdc-list-item--with-leading-checkbox]': '_hasCheckboxAt("before")', '[class.mdc-list-item--with-trailing-checkbox]': '_hasCheckboxAt("after")', + '[class.mdc-list-item--with-leading-radio]': '_hasRadioAt("before")', + '[class.mdc-list-item--with-trailing-radio]': '_hasRadioAt("after")', '[class.mat-accent]': 'color !== "primary" && color !== "warn"', '[class.mat-warn]': 'color === "warn"', '[class._mat-animation-noopable]': '_noopAnimations', @@ -105,10 +107,23 @@ export class MatListOption extends MatListItemBase implements ListOption, OnInit @Output() readonly selectedChange: EventEmitter = new EventEmitter(); - /** Whether the label should appear before or after the checkbox. Defaults to 'after' */ - @Input() checkboxPosition: MatListOptionCheckboxPosition = 'after'; + /** Whether the label should appear before or after the checkbox/radio. Defaults to 'after' */ + @Input() togglePosition: MatListOptionTogglePosition = 'after'; - /** Theme color of the list option. This sets the color of the checkbox. */ + /** + * Whether the label should appear before or after the checkbox/radio. Defaults to 'after' + * + * @deprecated Use `togglePosition` instead. + * @breaking-change 17.0.0 + */ + @Input() get checkboxPosition(): MatListOptionTogglePosition { + return this.togglePosition; + } + set checkboxPosition(value: MatListOptionTogglePosition) { + this.togglePosition = value; + } + + /** Theme color of the list option. This sets the color of the checkbox/radio. */ @Input() get color(): ThemePalette { return this._color || this._selectionList.color; @@ -225,8 +240,13 @@ export class MatListOption extends MatListItemBase implements ListOption, OnInit } /** Whether a checkbox is shown at the given position. */ - _hasCheckboxAt(position: MatListOptionCheckboxPosition): boolean { - return this._selectionList.multiple && this._getCheckboxPosition() === position; + _hasCheckboxAt(position: MatListOptionTogglePosition): boolean { + return this._selectionList.multiple && this._getTogglePosition() === position; + } + + /** Where a radio indicator is shown at the given position. */ + _hasRadioAt(position: MatListOptionTogglePosition): boolean { + return !this._selectionList.multiple && this._getTogglePosition() === position; } /** Whether icons or avatars are shown at the given position. */ @@ -236,10 +256,10 @@ export class MatListOption extends MatListItemBase implements ListOption, OnInit /** Gets whether the given type of element is projected at the specified position. */ _hasProjected(type: 'icons' | 'avatars', position: 'before' | 'after'): boolean { - // If the checkbox is shown at the specified position, neither icons or + // If the checkbox/radio is shown at the specified position, neither icons or // avatars can be shown at the position. return ( - this._getCheckboxPosition() !== position && + this._getTogglePosition() !== position && (type === 'avatars' ? this._avatars.length !== 0 : this._icons.length !== 0) ); } @@ -248,9 +268,9 @@ export class MatListOption extends MatListItemBase implements ListOption, OnInit this._selectionList._onTouched(); } - /** Gets the current position of the checkbox. */ - _getCheckboxPosition() { - return this.checkboxPosition || 'after'; + /** Gets the current position of the checkbox/radio. */ + _getTogglePosition() { + return this.togglePosition || 'after'; } /** diff --git a/src/material/list/public-api.ts b/src/material/list/public-api.ts index 7e8b6b3e6ebe..010eab276ae0 100644 --- a/src/material/list/public-api.ts +++ b/src/material/list/public-api.ts @@ -15,5 +15,13 @@ export * from './list-option'; export * from './subheader'; export * from './list-item-sections'; -export {MatListOptionCheckboxPosition} from './list-option-types'; export {MatListOption} from './list-option'; + +export { + MatListOptionTogglePosition, + /** + * @deprecated Use `MatListOptionTogglePosition` instead. + * @breaking-change 17.0.0 + */ + MatListOptionTogglePosition as MatListOptionCheckboxPosition, +} from './list-option-types'; diff --git a/src/material/list/selection-list.spec.ts b/src/material/list/selection-list.spec.ts index cc9665ac6807..1f94c69fba2e 100644 --- a/src/material/list/selection-list.spec.ts +++ b/src/material/list/selection-list.spec.ts @@ -27,7 +27,7 @@ import {By} from '@angular/platform-browser'; import { MatListModule, MatListOption, - MatListOptionCheckboxPosition, + MatListOptionTogglePosition, MatSelectionList, MatSelectionListChange, } from './index'; @@ -892,7 +892,7 @@ describe('MDC-based MatSelectionList without forms', () => { function expectCheckboxAtPosition( listItemElement: HTMLElement, - position: MatListOptionCheckboxPosition, + position: MatListOptionTogglePosition, ) { const containerSelector = position === 'before' ? '.mdc-list-item__start' : 'mdc-list-item__end'; @@ -974,12 +974,12 @@ describe('MDC-based MatSelectionList without forms', () => { expectCheckboxAtPosition(listOption, 'after'); expectIconAt(listOption, 'before'); - fixture.componentInstance.checkboxPosition = 'before'; + fixture.componentInstance.togglePosition = 'before'; fixture.detectChanges(); expectCheckboxAtPosition(listOption, 'before'); expectIconAt(listOption, 'after'); - fixture.componentInstance.checkboxPosition = 'after'; + fixture.componentInstance.togglePosition = 'after'; fixture.detectChanges(); expectCheckboxAtPosition(listOption, 'after'); expectIconAt(listOption, 'before'); @@ -995,12 +995,12 @@ describe('MDC-based MatSelectionList without forms', () => { expectCheckboxAtPosition(listOption, 'after'); expectAvatarAt(listOption, 'before'); - fixture.componentInstance.checkboxPosition = 'before'; + fixture.componentInstance.togglePosition = 'before'; fixture.detectChanges(); expectCheckboxAtPosition(listOption, 'before'); expectAvatarAt(listOption, 'after'); - fixture.componentInstance.checkboxPosition = 'after'; + fixture.componentInstance.togglePosition = 'after'; fixture.detectChanges(); expectCheckboxAtPosition(listOption, 'after'); expectAvatarAt(listOption, 'before'); @@ -1638,21 +1638,21 @@ describe('MDC-based MatSelectionList with forms', () => { [disableRipple]="listRippleDisabled" [color]="selectionListColor" [multiple]="multiple"> - Inbox (disabled selection-option) - Starred - + Sent Mail - + Archive - + Drafts
`, @@ -1670,16 +1670,16 @@ class SelectionListWithListOptions { @Component({ template: ` - + Inbox (disabled selection-option) - + Starred - + Sent Mail - + Drafts `, @@ -1689,16 +1689,16 @@ class SelectionListWithCheckboxPositionAfter {} @Component({ template: ` - + Inbox (disabled selection-option) - + Starred - + Sent Mail - + Drafts `, @@ -1742,7 +1742,7 @@ class SelectionListWithSelectedOptionAndValue { @Component({ template: ` - + Inbox `, @@ -1838,7 +1838,7 @@ class SelectionListWithCustomComparator { @Component({ template: ` - +
I
Inbox
@@ -1846,13 +1846,13 @@ class SelectionListWithCustomComparator { `, }) class SelectionListWithAvatar { - checkboxPosition: MatListOptionCheckboxPosition | undefined; + togglePosition: MatListOptionTogglePosition | undefined; } @Component({ template: ` - +
I
Inbox
@@ -1860,7 +1860,7 @@ class SelectionListWithAvatar { `, }) class SelectionListWithIcon { - checkboxPosition: MatListOptionCheckboxPosition | undefined; + togglePosition: MatListOptionTogglePosition | undefined; } @Component({ diff --git a/src/material/list/testing/list-harness.spec.ts b/src/material/list/testing/list-harness.spec.ts index e81782563037..4e76b420d6e9 100644 --- a/src/material/list/testing/list-harness.spec.ts +++ b/src/material/list/testing/list-harness.spec.ts @@ -639,7 +639,7 @@ class NavListHarnessTest { @Component({ template: ` - +
Item
1
icon
diff --git a/src/material/list/testing/selection-list-harness.ts b/src/material/list/testing/selection-list-harness.ts index c74f2e930394..f770e47514dd 100644 --- a/src/material/list/testing/selection-list-harness.ts +++ b/src/material/list/testing/selection-list-harness.ts @@ -7,7 +7,7 @@ */ import {ComponentHarnessConstructor, HarnessPredicate, parallel} from '@angular/cdk/testing'; -import {MatListOptionCheckboxPosition} from '@angular/material/list'; +import {MatListOptionTogglePosition} from '@angular/material/list'; import {MatListHarnessBase} from './list-harness-base'; import { ListItemHarnessFilters, @@ -98,12 +98,18 @@ export class MatListOptionHarness extends MatListItemHarnessBase { } private _beforeCheckbox = this.locatorForOptional('.mdc-list-item__start .mdc-checkbox'); + private _beforeRadio = this.locatorForOptional('.mdc-list-item__start .mdc-radio'); /** Gets the position of the checkbox relative to the list option content. */ - async getCheckboxPosition(): Promise { + async getCheckboxPosition(): Promise { return (await this._beforeCheckbox()) !== null ? 'before' : 'after'; } + /** Gets the position of the radio relative to the list option content. */ + async getRadioPosition(): Promise { + return (await this._beforeRadio()) !== null ? 'before' : 'after'; + } + /** Whether the list option is selected. */ async isSelected(): Promise { return (await (await this.host()).getAttribute('aria-selected')) === 'true'; diff --git a/src/material/radio/BUILD.bazel b/src/material/radio/BUILD.bazel index 2b899ce5476a..0ce7d6c63c1e 100644 --- a/src/material/radio/BUILD.bazel +++ b/src/material/radio/BUILD.bazel @@ -42,6 +42,7 @@ sass_binary( name = "radio_scss", src = "radio.scss", deps = [ + ":radio_scss_lib", "//:mdc_sass_lib", "//src/material/core:core_scss_lib", ], diff --git a/src/material/radio/_radio-private.scss b/src/material/radio/_radio-private.scss new file mode 100644 index 000000000000..e38dba1c4a3d --- /dev/null +++ b/src/material/radio/_radio-private.scss @@ -0,0 +1,60 @@ +@use '@material/radio/radio-theme' as mdc-radio-theme; +@use '@material/theme/theme-color' as mdc-theme-color; +@use '../core/theming/palette'; +@use '../core/theming/theming'; +@use 'sass:map'; + +$private-radio-theme-config: map.merge(mdc-radio-theme.$light-theme, ( + // Exclude the styles we don't need. + selected-focus-state-layer-color: null, + selected-focus-state-layer-opacity: null, + selected-hover-state-layer-color: null, + selected-hover-state-layer-opacity: null, + selected-pressed-state-layer-color: null, + selected-pressed-state-layer-opacity: null, + unselected-focus-icon-color: null, + unselected-focus-state-layer-color: null, + unselected-focus-state-layer-opacity: null, + unselected-hover-state-layer-color: null, + unselected-hover-state-layer-opacity: null, + unselected-pressed-state-layer-color: null, + unselected-pressed-state-layer-opacity: null, +)); + +@mixin private-radio-color($color-config, $color-palette) { + $foreground: map.get($color-config, foreground); + + $on-surface: rgba(mdc-theme-color.$on-surface, 0.54); + $is-dark: map.get($color-config, is-dark); + $active-border-color: if( + $is-dark, + theming.get-color-from-palette(palette.$gray-palette, 200), + theming.get-color-from-palette(palette.$gray-palette, 900) + ); + + & { + @include mdc-radio-theme.theme(( + // The disabled colors don't use the `rgba` version, because + // MDC applies a separate opacity to disabled buttons. + disabled-selected-icon-color: mdc-theme-color.$on-surface, + disabled-unselected-icon-color: mdc-theme-color.$on-surface, + unselected-focus-icon-color: $active-border-color, + unselected-hover-icon-color: $active-border-color, + unselected-icon-color: $on-surface, + unselected-pressed-icon-color: $on-surface, + selected-focus-icon-color: $color-palette, + selected-hover-icon-color: $color-palette, + selected-icon-color: $color-palette, + selected-pressed-icon-color: $color-palette, + )); + + --mat-mdc-radio-ripple-color: #{mdc-theme-color.prop-value(on-surface)}; + + // MDC should set the disabled color on the label, but doesn't, so we do it here instead. + .mdc-radio--disabled + label { + color: theming.get-color-from-palette($foreground, disabled-text); + } + + --mat-mdc-radio-checked-ripple-color: #{$color-palette}; + } +} diff --git a/src/material/radio/_radio-theme.scss b/src/material/radio/_radio-theme.scss index 42d26cb134fd..005a94cbcd5a 100644 --- a/src/material/radio/_radio-theme.scss +++ b/src/material/radio/_radio-theme.scss @@ -1,70 +1,32 @@ -@use 'sass:map'; -@use '@material/theme/theme-color' as mdc-theme-color; -@use '@material/radio/radio-theme' as mdc-radio-theme; @use '@material/radio/radio' as mdc-radio; +@use '@material/radio/radio-theme' as mdc-radio-theme; @use '@material/form-field' as mdc-form-field; -@use '../core/theming/theming'; @use '../core/mdc-helpers/mdc-helpers'; +@use '../core/theming/theming'; @use '../core/typography/typography'; -@use '../core/theming/palette'; - -@mixin _color-palette($color-palette) { - @include mdc-radio-theme.theme(( - selected-focus-icon-color: $color-palette, - selected-hover-icon-color: $color-palette, - selected-icon-color: $color-palette, - selected-pressed-icon-color: $color-palette, - )); - - --mat-mdc-radio-checked-ripple-color: #{$color-palette}; -} +@use './radio-private'; +@use 'sass:map'; @mixin color($config-or-theme) { $config: theming.get-color-config($config-or-theme); $primary: theming.get-color-from-palette(map.get($config, primary)); $accent: theming.get-color-from-palette(map.get($config, accent)); $warn: theming.get-color-from-palette(map.get($config, warn)); - $foreground: map.get($config, foreground); @include mdc-helpers.using-mdc-theme($config) { - $on-surface: rgba(mdc-theme-color.$on-surface, 0.54); - $is-dark: map.get($config, is-dark); - $active-border-color: if( - $is-dark, - theming.get-color-from-palette(palette.$gray-palette, 200), - theming.get-color-from-palette(palette.$gray-palette, 900) - ); - .mat-mdc-radio-button { @include mdc-form-field.core-styles($query: mdc-helpers.$mdc-theme-styles-query); - @include mdc-radio-theme.theme(( - // The disabled colors don't use the `rgba` version, because - // MDC applies a separate opacity to disabled buttons. - disabled-selected-icon-color: mdc-theme-color.$on-surface, - disabled-unselected-icon-color: mdc-theme-color.$on-surface, - unselected-focus-icon-color: $active-border-color, - unselected-hover-icon-color: $active-border-color, - unselected-icon-color: $on-surface, - unselected-pressed-icon-color: $on-surface, - )); - - --mat-mdc-radio-ripple-color: #{mdc-theme-color.prop-value(on-surface)}; - - // MDC should set the disabled color on the label, but doesn't, so we do it here instead. - .mdc-radio--disabled + label { - color: theming.get-color-from-palette($foreground, disabled-text); - } &.mat-primary { - @include _color-palette($primary); + @include radio-private.private-radio-color($config, $primary); } &.mat-accent { - @include _color-palette($accent); + @include radio-private.private-radio-color($config, $accent); } &.mat-warn { - @include _color-palette($warn); + @include radio-private.private-radio-color($config, $warn); } } } diff --git a/src/material/radio/radio.scss b/src/material/radio/radio.scss index e24b05f8a507..724c83d0cd95 100644 --- a/src/material/radio/radio.scss +++ b/src/material/radio/radio.scss @@ -6,6 +6,7 @@ @use '@material/ripple' as mdc-ripple; @use '../core/mdc-helpers/mdc-helpers'; @use '../core/style/layout-common'; +@use './radio-private'; @include mdc-helpers.disable-mdc-fallback-declarations { @@ -18,22 +19,7 @@ // 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-helpers.disable-mdc-fallback-declarations { - @include mdc-radio-theme.theme-styles(map.merge(mdc-radio-theme.$light-theme, ( - // Exclude the styles we don't need. - selected-focus-state-layer-color: null, - selected-focus-state-layer-opacity: null, - selected-hover-state-layer-color: null, - selected-hover-state-layer-opacity: null, - selected-pressed-state-layer-color: null, - selected-pressed-state-layer-opacity: null, - unselected-focus-icon-color: null, - unselected-focus-state-layer-color: null, - unselected-focus-state-layer-opacity: null, - unselected-hover-state-layer-color: null, - unselected-hover-state-layer-opacity: null, - unselected-pressed-state-layer-color: null, - unselected-pressed-state-layer-opacity: null, - ))); + @include mdc-radio-theme.theme-styles(radio-private.$private-radio-theme-config); // TODO(crisbeto): this should be included by MDC's `theme-styles`, but it isn't currently. @include mdc-radio-theme.focus-indicator-color( diff --git a/tools/public_api_guard/material/list-testing.md b/tools/public_api_guard/material/list-testing.md index 304935fd1001..8f85468dbc2f 100644 --- a/tools/public_api_guard/material/list-testing.md +++ b/tools/public_api_guard/material/list-testing.md @@ -11,7 +11,7 @@ import { ContentContainerComponentHarness } from '@angular/cdk/testing'; import { DividerHarnessFilters } from '@angular/material/divider/testing'; import { HarnessPredicate } from '@angular/cdk/testing'; import { MatDividerHarness } from '@angular/material/divider/testing'; -import { MatListOptionCheckboxPosition } from '@angular/material/list'; +import { MatListOptionTogglePosition } from '@angular/material/list'; // @public (undocumented) export interface ActionListHarnessFilters extends BaseHarnessFilters { @@ -102,7 +102,8 @@ export class MatListOptionHarness extends MatListItemHarnessBase { blur(): Promise; deselect(): Promise; focus(): Promise; - getCheckboxPosition(): Promise; + getCheckboxPosition(): Promise; + getRadioPosition(): Promise; static hostSelector: string; isFocused(): Promise; isSelected(): Promise; diff --git a/tools/public_api_guard/material/list.md b/tools/public_api_guard/material/list.md index da5ae144108e..d04df5b22b18 100644 --- a/tools/public_api_guard/material/list.md +++ b/tools/public_api_guard/material/list.md @@ -152,17 +152,20 @@ export class MatListModule { // @public (undocumented) export class MatListOption extends MatListItemBase implements ListOption, OnInit, OnDestroy { constructor(elementRef: ElementRef, ngZone: NgZone, _selectionList: SelectionList, platform: Platform, _changeDetectorRef: ChangeDetectorRef, globalRippleOptions?: RippleGlobalOptions, animationMode?: string); - checkboxPosition: MatListOptionCheckboxPosition; + // @deprecated + get checkboxPosition(): MatListOptionTogglePosition; + set checkboxPosition(value: MatListOptionTogglePosition); get color(): ThemePalette; set color(newValue: ThemePalette); focus(): void; - _getCheckboxPosition(): MatListOptionCheckboxPosition; getLabel(): string; + _getTogglePosition(): MatListOptionTogglePosition; // (undocumented) _handleBlur(): void; - _hasCheckboxAt(position: MatListOptionCheckboxPosition): boolean; + _hasCheckboxAt(position: MatListOptionTogglePosition): boolean; _hasIconsOrAvatarsAt(position: 'before' | 'after'): boolean; _hasProjected(type: 'icons' | 'avatars', position: 'before' | 'after'): boolean; + _hasRadioAt(position: MatListOptionTogglePosition): boolean; // (undocumented) _lines: QueryList; _markForCheck(): void; @@ -179,18 +182,21 @@ export class MatListOption extends MatListItemBase implements ListOption, OnInit _titles: QueryList; toggle(): void; _toggleOnInteraction(): void; + togglePosition: MatListOptionTogglePosition; // (undocumented) _unscopedContent: ElementRef; get value(): any; set value(newValue: any); // (undocumented) - static ɵcmp: i0.ɵɵComponentDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration; } // @public -export type MatListOptionCheckboxPosition = 'before' | 'after'; +type MatListOptionTogglePosition = 'before' | 'after'; +export { MatListOptionTogglePosition as MatListOptionCheckboxPosition } +export { MatListOptionTogglePosition } // @public export class MatListSubheaderCssMatStyler {