Skip to content

Commit

Permalink
fix(material/list): add opt-out for radio indicators
Browse files Browse the repository at this point in the history
Add an opt-out for radio indicators for single-selection. Adds both an
Input and DI token to specify if radio indicators are hidden. By
default, display radio indicators. If both DI token and Input are
specified, the Input wins.

PR #25933 added radio toggles for single-selection. Add an opt-out to
provide a way to have same appearance as before #25933.

API changes
 - add `@Input hideSingleSelectionIndicator` to specify if radio
   indicators are displayed
 - add MAT_LIST_CONFIG token to specify default value for
   `hideSingleSelectionIndicator`
  • Loading branch information
zarend committed Dec 16, 2022
1 parent 0d7f060 commit 11334df
Show file tree
Hide file tree
Showing 10 changed files with 171 additions and 4 deletions.
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 {
&::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

0 comments on commit 11334df

Please sign in to comment.