Skip to content

Commit a71930e

Browse files
committed
fix(material/core): add checkmark for single-select
Add checkmark to mat-option for single-select. Fix a11y issues where selected state is visually communicated with color alone. Communicate selection with both color and a checkmark indicator. Affect components that use mat-option for single-selection, which include select and autocomplete. Add an `appearance` Input to mat-pseudo-checkbox. "full" appearance renders a checkbox, which is the current behavior. Render "full" appearance by default. "minimal" apperance renders only a checkmark. Add an opt-out to Selection and Autocomplete components for checkmark indicators for single-selection. Add both Input and DI token to specify if checkmark indicators are hidden for single-select. By default display checkmark indicators for single-selection. If both DI token and Input are specified, the Input wins. Does not affect multiple-selection. Does not affect legacy components. Summary of API and behavior changes: - Add an `@Input appearance` to pseudo-checkbox with options for "full" and "minimal". "full" appearance is same and current appearance, which renders a checkmark inside a box. "minimal" appearance renders the checkmark without a box. - By default, mat-option renders "minimal" appearance for single-select. - Add `hideSingleSelectionIndicator` property to `MatOptionParentComponent`. mat-option hides single-selection indicator when specified by its parent. - by default, Select and Autocomplete components display checkmark on selected option. - Both Autocomplete and Select add `@Input hideSingleSelectionIndicator` to specify if checkmark indicator is displayed for single-selection. - Add `hideSingleSelectionIndicator` property to `MatSelectConfig`, which specifies default value for `hideSingleSelectionIndicator`. - Add `hideSingleSelectionIndicator` property to `MatAutocompleteDefaultOptions`, which specifies default value for `hideSingleSelectionIndicator`. Fixes: #25961
1 parent 8c71026 commit a71930e

23 files changed

+492
-73
lines changed

src/dev-app/autocomplete/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ ng_module(
1313
"//src/material/autocomplete",
1414
"//src/material/button",
1515
"//src/material/card",
16+
"//src/material/checkbox",
1617
"//src/material/form-field",
1718
"//src/material/input",
1819
"@npm//@angular/forms",

src/dev-app/autocomplete/autocomplete-demo.html

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
<mat-label>State</mat-label>
1111
<input matInput [matAutocomplete]="reactiveAuto" [formControl]="stateCtrl">
1212
</mat-form-field>
13-
<mat-autocomplete #reactiveAuto="matAutocomplete" [displayWith]="displayFn">
13+
<mat-autocomplete #reactiveAuto="matAutocomplete" [displayWith]="displayFn"
14+
[hideSingleSelectionIndicator]="reactiveHideSingleSelectionIndicator">
1415
<mat-option *ngFor="let state of tempStates" [value]="state">
1516
<span>{{ state.name }}</span>
1617
<span class="demo-secondary-text"> ({{ state.code }}) </span>
@@ -23,11 +24,11 @@
2324
<button mat-button (click)="stateCtrl.enabled ? stateCtrl.disable() : stateCtrl.enable()">
2425
TOGGLE DISABLED
2526
</button>
26-
<select [(ngModel)]="reactiveStatesTheme">
27-
<option *ngFor="let theme of availableThemes" [value]="theme.value">
28-
{{theme.name}}
29-
</option>
30-
</select>
27+
</mat-card-actions>
28+
<mat-card-actions>
29+
<mat-checkbox [(ngModel)]="reactiveHideSingleSelectionIndicator">
30+
Hide Single-Selection Indicator
31+
</mat-checkbox>
3132
</mat-card-actions>
3233

3334
</mat-card>
@@ -42,7 +43,8 @@
4243
<mat-label>State</mat-label>
4344
<input matInput [matAutocomplete]="tdAuto" [(ngModel)]="currentState"
4445
(ngModelChange)="tdStates = filterStates(currentState)" [disabled]="tdDisabled">
45-
<mat-autocomplete #tdAuto="matAutocomplete">
46+
<mat-autocomplete #tdAuto="matAutocomplete"
47+
[hideSingleSelectionIndicator]="templateHideSingleSelectionIndicator">
4648
<mat-option *ngFor="let state of tdStates" [value]="state.name">
4749
<span>{{ state.name }}</span>
4850
</mat-option>
@@ -61,6 +63,11 @@
6163
</option>
6264
</select>
6365
</mat-card-actions>
66+
<mat-card-actions>
67+
<mat-checkbox [(ngModel)]="templateHideSingleSelectionIndicator">
68+
Hide Single-Selection Indicator
69+
</mat-checkbox>
70+
</mat-card-actions>
6471

6572
</mat-card>
6673

src/dev-app/autocomplete/autocomplete-demo.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {CommonModule} from '@angular/common';
1212
import {MatAutocompleteModule} from '@angular/material/autocomplete';
1313
import {MatButtonModule} from '@angular/material/button';
1414
import {MatCardModule} from '@angular/material/card';
15+
import {MatCheckboxModule} from '@angular/material/checkbox';
1516
import {MatInputModule} from '@angular/material/input';
1617
import {Observable} from 'rxjs';
1718
import {map, startWith} from 'rxjs/operators';
@@ -38,6 +39,7 @@ export interface StateGroup {
3839
MatAutocompleteModule,
3940
MatButtonModule,
4041
MatCardModule,
42+
MatCheckboxModule,
4143
MatInputModule,
4244
ReactiveFormsModule,
4345
],
@@ -52,6 +54,7 @@ export class AutocompleteDemo {
5254
tdStates: State[];
5355

5456
tdDisabled = false;
57+
hideSingleSelectionIndicators = false;
5558

5659
reactiveStatesTheme: ThemePalette = 'primary';
5760
templateStatesTheme: ThemePalette = 'primary';
@@ -62,6 +65,9 @@ export class AutocompleteDemo {
6265
{value: 'warn', name: 'Warn'},
6366
];
6467

68+
reactiveHideSingleSelectionIndicator = false;
69+
templateHideSingleSelectionIndicator = false;
70+
6571
@ViewChild(NgModel) modelDir: NgModel;
6672

6773
groupedStates: StateGroup[];

src/dev-app/checkbox/checkbox-demo.html

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,29 @@ <h1>mat-checkbox: Basic Example</h1>
6464
</div>
6565

6666
<h1>Pseudo checkboxes</h1>
67-
<mat-pseudo-checkbox></mat-pseudo-checkbox>
68-
<mat-pseudo-checkbox [disabled]="true"></mat-pseudo-checkbox>
67+
<div>
68+
<h2>Full appearance</h2>
69+
<mat-pseudo-checkbox></mat-pseudo-checkbox>
70+
<mat-pseudo-checkbox [disabled]="true"></mat-pseudo-checkbox>
71+
72+
<mat-pseudo-checkbox state="checked"></mat-pseudo-checkbox>
73+
<mat-pseudo-checkbox state="checked" [disabled]="true"></mat-pseudo-checkbox>
74+
75+
<mat-pseudo-checkbox state="indeterminate"></mat-pseudo-checkbox>
76+
<mat-pseudo-checkbox state="indeterminate" [disabled]="true"></mat-pseudo-checkbox>
77+
<div>
78+
<div>
79+
<h2>Minimal appearance</h2>
80+
<mat-pseudo-checkbox appearance="minimal"></mat-pseudo-checkbox>
81+
<mat-pseudo-checkbox appearance="minimal" [disabled]="true"></mat-pseudo-checkbox>
6982

70-
<mat-pseudo-checkbox state="checked"></mat-pseudo-checkbox>
71-
<mat-pseudo-checkbox state="checked" [disabled]="true"></mat-pseudo-checkbox>
83+
<mat-pseudo-checkbox appearance="minimal" state="checked"></mat-pseudo-checkbox>
84+
<mat-pseudo-checkbox appearance="minimal" state="checked" [disabled]="true"></mat-pseudo-checkbox>
7285

73-
<mat-pseudo-checkbox state="indeterminate"></mat-pseudo-checkbox>
74-
<mat-pseudo-checkbox state="indeterminate" [disabled]="true"></mat-pseudo-checkbox>
86+
<mat-pseudo-checkbox appearance="minimal" state="indeterminate"></mat-pseudo-checkbox>
87+
<mat-pseudo-checkbox appearance="minimal" state="indeterminate" [disabled]="true">
88+
</mat-pseudo-checkbox>
89+
<div>
7590

7691
<h1>Nested Checklist</h1>
7792
<mat-checkbox-demo-nested-checklist></mat-checkbox-demo-nested-checklist>

src/dev-app/select/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ ng_module(
1212
deps = [
1313
"//src/material/button",
1414
"//src/material/card",
15+
"//src/material/checkbox",
1516
"//src/material/form-field",
1617
"//src/material/icon",
1718
"//src/material/input",

src/dev-app/select/select-demo.html

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
<mat-label>Drink</mat-label>
1313
<mat-select [(ngModel)]="currentDrink" [required]="drinksRequired"
1414
[disabled]="drinksDisabled" #drinkControl="ngModel">
15-
<mat-option>None</mat-option>
16-
<mat-option *ngFor="let drink of drinks" [value]="drink.value" [disabled]="drink.disabled">
15+
<mat-option [disabled]="drinksOptionsDisabled">None</mat-option>
16+
<mat-option *ngFor="let drink of drinks" [value]="drink.value" [disabled]="drinksOptionsDisabled">
1717
{{ drink.viewValue }}
1818
</mat-option>
1919
</mat-select>
@@ -52,6 +52,7 @@
5252
<button mat-button (click)="currentDrink='water-2'">SET VALUE</button>
5353
<button mat-button (click)="drinksRequired=!drinksRequired">TOGGLE REQUIRED</button>
5454
<button mat-button (click)="drinksDisabled=!drinksDisabled">TOGGLE DISABLED</button>
55+
<button mat-button (click)="drinksOptionsDisabled=!drinksOptionsDisabled">TOGGLE DISABLED OPTIONS</button>
5556
<button mat-button (click)="drinkControl.reset()">RESET</button>
5657
</mat-card-content>
5758
</mat-card>
@@ -64,7 +65,7 @@
6465
<mat-label>Pokemon</mat-label>
6566
<mat-select multiple [(ngModel)]="currentPokemon"
6667
[required]="pokemonRequired" [disabled]="pokemonDisabled" #pokemonControl="ngModel">
67-
<mat-option *ngFor="let creature of pokemon" [value]="creature.value">
68+
<mat-option *ngFor="let creature of pokemon" [value]="creature.value" [disabled]="pokemonOptionsDisabled">
6869
{{ creature.viewValue }}
6970
</mat-option>
7071
</mat-select>
@@ -82,6 +83,7 @@
8283
<button mat-button (click)="setPokemonValue()">SET VALUE</button>
8384
<button mat-button (click)="pokemonRequired=!pokemonRequired">TOGGLE REQUIRED</button>
8485
<button mat-button (click)="pokemonDisabled=!pokemonDisabled">TOGGLE DISABLED</button>
86+
<button mat-button (click)="pokemonOptionsDisabled=!pokemonOptionsDisabled">TOGGLE DISABLED OPTIONS</button>
8587
<button mat-button (click)="pokemonControl.reset()">RESET</button>
8688
</mat-card-content>
8789
</mat-card>
@@ -356,3 +358,41 @@ <h4>Error message with errorStateMatcher</h4>
356358
</form>
357359
</mat-card-content>
358360
</mat-card>
361+
362+
<mat-card class="demo-card demo-narrow">
363+
<mat-card-subtitle>Narrow</mat-card-subtitle>
364+
<mat-card-content>
365+
<p class="demo-narrow-sandwich">
366+
<mat-form-field>
367+
<mat-label>Bread</mat-label>
368+
<mat-select [(ngModel)]="sandwichBread"
369+
[hideSingleSelectionIndicator]="sandwichHideSingleSelectionIndicator">
370+
<mat-option *ngFor="let bread of breads" [value]="bread.value">
371+
{{ bread.viewValue }}
372+
</mat-option>
373+
</mat-select>
374+
</mat-form-field>
375+
<mat-form-field>
376+
<mat-label>Meat</mat-label>
377+
<mat-select [(ngModel)]="sandwichMeat"
378+
[hideSingleSelectionIndicator]="sandwichHideSingleSelectionIndicator">
379+
<mat-option *ngFor="let meat of meats" [value]="meat.value">
380+
{{ meat.viewValue }}
381+
</mat-option>
382+
</mat-select>
383+
</mat-form-field>
384+
<mat-form-field>
385+
<mat-label>Cheese</mat-label>
386+
<mat-select [(ngModel)]="sandwichCheese"
387+
[hideSingleSelectionIndicator]="sandwichHideSingleSelectionIndicator">
388+
<mat-option *ngFor="let cheese of cheeses" [value]="cheese.value">
389+
{{ cheese.viewValue }}
390+
</mat-option>
391+
</mat-select>
392+
</mat-form-field>
393+
</p>
394+
<mat-checkbox [(ngModel)]="sandwichHideSingleSelectionIndicator">
395+
Hide Single-Selection Indicator
396+
</mat-checkbox>
397+
</mat-card-content>
398+
</mat-card>

src/dev-app/select/select-demo.scss

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,12 @@
2525
.demo-card {
2626
margin: 30px 0;
2727
}
28+
29+
.demo-narrow {
30+
max-width: 450px;
31+
32+
.demo-narrow-sandwich {
33+
display: flex;
34+
gap: 16px;
35+
}
36+
}

src/dev-app/select/select-demo.ts

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {MatCardModule} from '@angular/material/card';
1616
import {MatIconModule} from '@angular/material/icon';
1717
import {MatButtonModule} from '@angular/material/button';
1818
import {MatInputModule} from '@angular/material/input';
19+
import {MatCheckboxModule} from '@angular/material/checkbox';
1920

2021
/** Error any time control is invalid */
2122
export class MyErrorStateMatcher implements ErrorStateMatcher {
@@ -37,6 +38,7 @@ export class MyErrorStateMatcher implements ErrorStateMatcher {
3738
FormsModule,
3839
MatButtonModule,
3940
MatCardModule,
41+
MatCheckboxModule,
4042
MatIconModule,
4143
MatInputModule,
4244
MatSelectModule,
@@ -48,7 +50,9 @@ export class SelectDemo {
4850
drinkObjectRequired = false;
4951
pokemonRequired = false;
5052
drinksDisabled = false;
53+
drinksOptionsDisabled = false;
5154
pokemonDisabled = false;
55+
pokemonOptionsDisabled = false;
5256
showSelect = false;
5357
currentDrink: string;
5458
currentDrinkObject: {} | undefined = {value: 'tea-5', viewValue: 'Tea'};
@@ -66,6 +70,12 @@ export class SelectDemo {
6670
compareByValue = true;
6771
selectFormControl = new FormControl('', Validators.required);
6872

73+
sandwichBread = '';
74+
sandwichMeat = '';
75+
sandwichCheese = '';
76+
77+
sandwichHideSingleSelectionIndicator = false;
78+
6979
foods = [
7080
{value: null, viewValue: 'None'},
7181
{value: 'steak-0', viewValue: 'Steak'},
@@ -74,19 +84,19 @@ export class SelectDemo {
7484
];
7585

7686
drinks = [
77-
{value: 'coke-0', viewValue: 'Coke', disabled: false},
87+
{value: 'coke-0', viewValue: 'Coke'},
7888
{
7989
value: 'long-name-1',
8090
viewValue: 'Decaf Chocolate Brownie Vanilla Gingerbread Frappuccino',
8191
disabled: false,
8292
},
83-
{value: 'water-2', viewValue: 'Water', disabled: false},
84-
{value: 'pepper-3', viewValue: 'Dr. Pepper', disabled: false},
85-
{value: 'coffee-4', viewValue: 'Coffee', disabled: false},
86-
{value: 'tea-5', viewValue: 'Tea', disabled: false},
87-
{value: 'juice-6', viewValue: 'Orange juice', disabled: false},
88-
{value: 'wine-7', viewValue: 'Wine', disabled: false},
89-
{value: 'milk-8', viewValue: 'Milk', disabled: true},
93+
{value: 'water-2', viewValue: 'Water'},
94+
{value: 'pepper-3', viewValue: 'Dr. Pepper'},
95+
{value: 'coffee-4', viewValue: 'Coffee'},
96+
{value: 'tea-5', viewValue: 'Tea'},
97+
{value: 'juice-6', viewValue: 'Orange juice'},
98+
{value: 'wine-7', viewValue: 'Wine'},
99+
{value: 'milk-8', viewValue: 'Milk'},
90100
];
91101

92102
pokemon = [
@@ -149,6 +159,26 @@ export class SelectDemo {
149159
{value: 'indramon-5', viewValue: 'Indramon'},
150160
];
151161

162+
breads = [
163+
{value: 'white', viewValue: 'White'},
164+
{value: 'white', viewValue: 'Wheat'},
165+
{value: 'white', viewValue: 'Sourdough'},
166+
];
167+
168+
meats = [
169+
{value: 'turkey', viewValue: 'Turkey'},
170+
{value: 'bacon', viewValue: 'Bacon'},
171+
{value: 'veggiePatty', viewValue: 'Veggie Patty'},
172+
{value: 'tuna', viewValue: 'Tuna'},
173+
];
174+
175+
cheeses = [
176+
{value: 'none', viewValue: 'None'},
177+
{value: 'swiss', viewValue: 'Swiss'},
178+
{value: 'american', viewValue: 'American'},
179+
{value: 'cheddar', viewValue: 'Cheddar'},
180+
];
181+
152182
toggleDisabled() {
153183
this.foodControl.enabled ? this.foodControl.disable() : this.foodControl.enable();
154184
}

src/material/autocomplete/autocomplete.spec.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import {map, startWith} from 'rxjs/operators';
4646
import {
4747
getMatAutocompleteMissingPanelError,
4848
MatAutocomplete,
49+
MatAutocompleteDefaultOptions,
4950
MatAutocompleteModule,
5051
MatAutocompleteOrigin,
5152
MatAutocompleteSelectedEvent,
@@ -3412,6 +3413,50 @@ describe('MDC-based MatAutocomplete', () => {
34123413

34133414
subscription.unsubscribe();
34143415
}));
3416+
3417+
describe('a11y', () => {
3418+
it('should display checkmark for selection by default', () => {
3419+
const fixture = createComponent(AutocompleteWithNgModel);
3420+
fixture.componentInstance.selectedState = 'New York';
3421+
fixture.detectChanges();
3422+
3423+
fixture.componentInstance.trigger.openPanel();
3424+
fixture.detectChanges();
3425+
3426+
dispatchFakeEvent(document.querySelector('mat-option')!, 'click');
3427+
fixture.detectChanges();
3428+
3429+
const selectedOption = document.querySelector('mat-option[aria-selected="true"');
3430+
expect(selectedOption).withContext('Expected an option to be selected.').not.toBeNull();
3431+
expect(selectedOption?.querySelector('.mat-pseudo-checkbox.mat-pseudo-checkbox-minimal'))
3432+
.withContext(
3433+
'Expected selection option to have a pseudo-checkbox with "minimal" appearance.',
3434+
)
3435+
.toBeTruthy();
3436+
});
3437+
});
3438+
3439+
describe('with token to hide single selection indicator', () => {
3440+
it('should not display checkmark', () => {
3441+
const defaultOptions: MatAutocompleteDefaultOptions = {
3442+
hideSingleSelectionIndicator: true,
3443+
};
3444+
const fixture = createComponent(AutocompleteWithNgModel, [
3445+
{provide: MAT_AUTOCOMPLETE_DEFAULT_OPTIONS, useValue: defaultOptions},
3446+
]);
3447+
fixture.detectChanges();
3448+
3449+
fixture.componentInstance.trigger.openPanel();
3450+
fixture.detectChanges();
3451+
3452+
dispatchFakeEvent(document.querySelector('mat-option')!, 'click');
3453+
fixture.detectChanges();
3454+
3455+
const selectedOption = document.querySelector('mat-option[aria-selected="true"');
3456+
expect(selectedOption).withContext('Expected an option to be selected.').not.toBeNull();
3457+
expect(document.querySelectorAll('.mat-pseudo-checkbox').length).toBe(0);
3458+
});
3459+
});
34153460
});
34163461

34173462
const SIMPLE_AUTOCOMPLETE_TEMPLATE = `
@@ -3576,6 +3621,8 @@ class AutocompleteWithNgModel {
35763621
selectedState: string;
35773622
states = ['New York', 'Washington', 'Oregon'];
35783623

3624+
@ViewChild(MatAutocompleteTrigger, {static: true}) trigger: MatAutocompleteTrigger;
3625+
35793626
constructor() {
35803627
this.filteredStates = this.states.slice();
35813628
}

0 commit comments

Comments
 (0)