Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(material/list): add opt-out for radio indicators #26283

Merged
merged 1 commit into from Dec 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/dev-app/list/list-demo.html
Expand Up @@ -123,6 +123,7 @@ <h2>Selection list</h2>
(selectionChange)="changeEventCount = changeEventCount + 1"
[disabled]="selectionListDisabled"
[disableRipple]="selectionListRippleDisabled"
[hideSingleSelectionIndicator]="selectionListSingleSelectionIndicatorHidden"
color="primary">
<div mat-subheader>Groceries</div>

Expand Down Expand Up @@ -161,6 +162,12 @@ <h2>Selection list</h2>
<input type="checkbox" [(ngModel)]="selectionListRippleDisabled">
</label>
</p>
<p>
<label>
Hide Single-Selection indicators
<input type="checkbox" [(ngModel)]="selectionListSingleSelectionIndicatorHidden">
</label>
</p>
<p>
<button mat-raised-button (click)="groceries.selectAll()">Select all</button>
<button mat-raised-button (click)="groceries.deselectAll()">Deselect all</button>
Expand All @@ -173,6 +180,7 @@ <h2>Single Selection list</h2>
<mat-selection-list #favorite
[(ngModel)]="favoriteOptions"
[multiple]="false"
[hideSingleSelectionIndicator]="selectionListSingleSelectionIndicatorHidden"
color="primary">
<div mat-subheader>Favorite Grocery</div>

Expand Down
1 change: 1 addition & 0 deletions src/dev-app/list/list-demo.ts
Expand Up @@ -64,6 +64,7 @@ export class ListDemo {
infoClicked = false;
selectionListDisabled = false;
selectionListRippleDisabled = false;
selectionListSingleSelectionIndicatorHidden = false;

selectedOptions: string[] = ['apples'];
changeEventCount = 0;
Expand Down
17 changes: 17 additions & 0 deletions src/material/list/_interactive-list-theme.scss
@@ -1,11 +1,13 @@
@use 'sass:map';
@use '@material/ripple' as mdc-ripple;
@use '../core/theming/theming';

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

.mat-mdc-list-item-interactive {
&::before {
Expand All @@ -19,5 +21,20 @@
&:focus::before {
opacity: mdc-ripple.states-opacity($active-base-color, focus);
}

&.mdc-list-item--selected {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these new styles all still related to us not using MDC's ripple?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not as familiar with ripple, but that's what it looks like to me. Someone more familiar with ripple might know more.

These styles where here all along. After originally adding the radio indicators in #25933, #26074 removed these styles because they were not used anymore. This PR adds them back.

&::before {
background: $selected-color;
opacity: mdc-ripple.states-opacity($selected-color, selected);
}

&:not(:focus):not(.mdc-list-item--disabled):hover::before {
// The hover and selected opacities need to be combined to match with what the MDC
// ripple state would render. More details here:
// https://github.com/material-components/material-components-web/blob/348665978ce73694ad4518626dd70cdf5b984113/packages/mdc-ripple/_ripple-theme.scss#L450.
opacity: mdc-ripple.states-opacity($selected-color, hover) +
mdc-ripple.states-opacity($selected-color, selected);
}
}
}
}
4 changes: 4 additions & 0 deletions src/material/list/list-base.ts
Expand Up @@ -13,6 +13,7 @@ import {
ContentChildren,
Directive,
ElementRef,
inject,
Inject,
Input,
NgZone,
Expand All @@ -35,6 +36,7 @@ import {
MatListItemIcon,
MatListItemAvatar,
} from './list-item-sections';
import {MAT_LIST_CONFIG} from './tokens';

@Directive({
host: {
Expand Down Expand Up @@ -67,6 +69,8 @@ export abstract class MatListBase {
this._disabled = coerceBooleanProperty(value);
}
private _disabled = false;

protected _defaultOptions = inject(MAT_LIST_CONFIG, {optional: true});
}

@Directive({
Expand Down
11 changes: 10 additions & 1 deletion src/material/list/list-option.ts
Expand Up @@ -50,6 +50,7 @@ export interface SelectionList extends MatListBase {
multiple: boolean;
color: ThemePalette;
selectedOptions: SelectionModel<MatListOption>;
hideSingleSelectionIndicator: boolean;
compareWith: (o1: any, o2: any) => boolean;
_value: string[] | null;
_reportValueChange(): void;
Expand All @@ -64,6 +65,10 @@ export interface SelectionList extends MatListBase {
host: {
'class': 'mat-mdc-list-item mat-mdc-list-option mdc-list-item',
'role': 'option',
// As per MDC, only list items without checkbox or radio indicator should receive the
// `--selected` class.
'[class.mdc-list-item--selected]':
'selected && !_selectionList.multiple && _selectionList.hideSingleSelectionIndicator',
// 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")',
Expand Down Expand Up @@ -243,7 +248,11 @@ export class MatListOption extends MatListItemBase implements ListOption, OnInit

/** Where a radio indicator is shown at the given position. */
_hasRadioAt(position: MatListOptionTogglePosition): boolean {
return !this._selectionList.multiple && this._getTogglePosition() === position;
return (
!this._selectionList.multiple &&
this._getTogglePosition() === position &&
!this._selectionList.hideSingleSelectionIndicator
);
}

/** Whether icons or avatars are shown at the given position. */
Expand Down
1 change: 1 addition & 0 deletions src/material/list/public-api.ts
Expand Up @@ -14,6 +14,7 @@ export * from './selection-list';
export * from './list-option';
export * from './subheader';
export * from './list-item-sections';
export * from './tokens';

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

Expand Down
86 changes: 85 additions & 1 deletion src/material/list/selection-list.spec.ts
Expand Up @@ -30,6 +30,8 @@ import {
MatListOptionTogglePosition,
MatSelectionList,
MatSelectionListChange,
MatListConfig,
MAT_LIST_CONFIG,
} from './index';

describe('MDC-based MatSelectionList without forms', () => {
Expand Down Expand Up @@ -663,7 +665,7 @@ describe('MDC-based MatSelectionList without forms', () => {
});
});

describe('with list option selected', () => {
describe('multiple-selection with list option selected', () => {
let fixture: ComponentFixture<SelectionListWithSelectedOption>;
let listOptionElements: DebugElement[];
let selectionList: DebugElement;
Expand Down Expand Up @@ -703,6 +705,79 @@ describe('MDC-based MatSelectionList without forms', () => {
}));
});

describe('single-selection with list option selected', () => {
let fixture: ComponentFixture<SingleSelectionListWithSelectedOption>;
let listOptionElements: DebugElement[];

beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [MatListModule],
declarations: [SingleSelectionListWithSelectedOption],
});

TestBed.compileComponents();
}));

beforeEach(waitForAsync(() => {
fixture = TestBed.createComponent(SingleSelectionListWithSelectedOption);
listOptionElements = fixture.debugElement.queryAll(By.directive(MatListOption))!;
fixture.detectChanges();
}));

it('displays radio indicators by default', () => {
expect(
listOptionElements[0].nativeElement.querySelector('input[type="radio"]'),
).not.toBeNull();
expect(
listOptionElements[1].nativeElement.querySelector('input[type="radio"]'),
).not.toBeNull();

expect(listOptionElements[0].nativeElement.classList).not.toContain(
'mdc-list-item--selected',
);
expect(listOptionElements[1].nativeElement.classList).not.toContain(
'mdc-list-item--selected',
);
});
});

describe('with token to hide radio indicators', () => {
let fixture: ComponentFixture<SingleSelectionListWithSelectedOption>;
let listOptionElements: DebugElement[];

beforeEach(waitForAsync(() => {
const matListConfig: MatListConfig = {hideSingleSelectionIndicator: true};

TestBed.configureTestingModule({
imports: [MatListModule],
declarations: [SingleSelectionListWithSelectedOption],
providers: [{provide: MAT_LIST_CONFIG, useValue: matListConfig}],
});

TestBed.compileComponents();
}));

beforeEach(waitForAsync(() => {
fixture = TestBed.createComponent(SingleSelectionListWithSelectedOption);
listOptionElements = fixture.debugElement.queryAll(By.directive(MatListOption))!;
fixture.detectChanges();
}));

it('does not display radio indicators', () => {
expect(listOptionElements[0].nativeElement.querySelector('input[type="radio"]')).toBeNull();
expect(listOptionElements[1].nativeElement.querySelector('input[type="radio"]')).toBeNull();

expect(listOptionElements[0].nativeElement.classList).not.toContain(
'mdc-list-item--selected',
);

expect(listOptionElements[1].nativeElement.getAttribute('aria-selected'))
.withContext('Expected second option to be selected')
.toBe('true');
expect(listOptionElements[1].nativeElement.classList).toContain('mdc-list-item--selected');
});
});

describe('with option disabled', () => {
let fixture: ComponentFixture<SelectionListWithDisabledOption>;
let listOptionEl: HTMLElement;
Expand Down Expand Up @@ -1727,6 +1802,15 @@ class SelectionListWithDisabledOption {
})
class SelectionListWithSelectedOption {}

@Component({
template: `
<mat-selection-list [multiple]="false">
<mat-list-option>Not selected - Item #1</mat-list-option>
<mat-list-option [selected]="true">Pre-selected - Item #2</mat-list-option>
</mat-selection-list>`,
})
class SingleSelectionListWithSelectedOption {}

@Component({
template: `
<mat-selection-list>
Expand Down
15 changes: 14 additions & 1 deletion src/material/list/selection-list.ts
Expand Up @@ -123,6 +123,17 @@ export class MatSelectionList
}
private _multiple = true;

/** Whether radio indicator for all list items is hidden. */
@Input()
get hideSingleSelectionIndicator(): boolean {
return this._hideSingleSelectionIndicator;
}
set hideSingleSelectionIndicator(value: BooleanInput) {
this._hideSingleSelectionIndicator = coerceBooleanProperty(value);
}
private _hideSingleSelectionIndicator: boolean =
this._defaultOptions?.hideSingleSelectionIndicator ?? false;

/** The currently selected options. */
selectedOptions = new SelectionModel<MatListOption>(this._multiple);

Expand Down Expand Up @@ -160,10 +171,12 @@ export class MatSelectionList
ngOnChanges(changes: SimpleChanges) {
const disabledChanges = changes['disabled'];
const disableRippleChanges = changes['disableRipple'];
const hideSingleSelectionIndicatorChanges = changes['hideSingleSelectionIndicator'];

if (
(disableRippleChanges && !disableRippleChanges.firstChange) ||
(disabledChanges && !disabledChanges.firstChange)
(disabledChanges && !disabledChanges.firstChange) ||
(hideSingleSelectionIndicatorChanges && !hideSingleSelectionIndicatorChanges.firstChange)
) {
this._markOptionsForCheck();
}
Expand Down
18 changes: 18 additions & 0 deletions src/material/list/tokens.ts
@@ -0,0 +1,18 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {InjectionToken} from '@angular/core';

/** Object that can be used to configure the default options for the list module. */
export interface MatListConfig {
/** Wheter icon indicators should be hidden for single-selection. */
hideSingleSelectionIndicator?: boolean;
}

/** Injection token that can be used to provide the default options the list module. */
export const MAT_LIST_CONFIG = new InjectionToken<MatListConfig>('MAT_LIST_CONFIG');
14 changes: 13 additions & 1 deletion tools/public_api_guard/material/list.md
Expand Up @@ -32,6 +32,9 @@ import { ThemePalette } from '@angular/material/core';
// @public
export const MAT_LIST: InjectionToken<MatList>;

// @public
export const MAT_LIST_CONFIG: InjectionToken<MatListConfig>;

// @public
export const MAT_NAV_LIST: InjectionToken<MatNavList>;

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

// @public
export interface MatListConfig {
hideSingleSelectionIndicator?: boolean;
}

// @public (undocumented)
export class MatListItem extends MatListItemBase {
constructor(element: ElementRef, ngZone: NgZone, listBase: MatListBase | null, platform: Platform, globalRippleOptions?: RippleGlobalOptions, animationMode?: string);
Expand Down Expand Up @@ -229,6 +237,8 @@ export class MatSelectionList extends MatListBase implements SelectionList, Cont
_emitChangeEvent(options: MatListOption[]): void;
focus(options?: FocusOptions): void;
_handleKeydown(event: KeyboardEvent): void;
get hideSingleSelectionIndicator(): boolean;
set hideSingleSelectionIndicator(value: BooleanInput);
// (undocumented)
_items: QueryList<MatListOption>;
get multiple(): boolean;
Expand All @@ -251,7 +261,7 @@ export class MatSelectionList extends MatListBase implements SelectionList, Cont
_value: string[] | null;
writeValue(values: string[]): void;
// (undocumented)
static ɵcmp: i0.ɵɵComponentDeclaration<MatSelectionList, "mat-selection-list", ["matSelectionList"], { "color": "color"; "compareWith": "compareWith"; "multiple": "multiple"; "disabled": "disabled"; }, { "selectionChange": "selectionChange"; }, ["_items"], ["*"], false, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<MatSelectionList, "mat-selection-list", ["matSelectionList"], { "color": "color"; "compareWith": "compareWith"; "multiple": "multiple"; "hideSingleSelectionIndicator": "hideSingleSelectionIndicator"; "disabled": "disabled"; }, { "selectionChange": "selectionChange"; }, ["_items"], ["*"], false, never>;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration<MatSelectionList, never>;
}
Expand All @@ -277,6 +287,8 @@ export interface SelectionList extends MatListBase {
// (undocumented)
_emitChangeEvent(options: MatListOption[]): void;
// (undocumented)
hideSingleSelectionIndicator: boolean;
// (undocumented)
multiple: boolean;
// (undocumented)
_onTouched(): void;
Expand Down