Skip to content

Commit 72ae1c8

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. Summary of API and behavior changes: - Add an `@Input appearance` to pseudo-checkbox with options for "full" and "minimal". - mat-option renders "minimal" appearance for single-select, which looks like a checkmark. - Select and autocomplete components display checkmark on selected option. What's not changing: - does not affect multiple selection - mat-option renders "full" apperance by default, which is same as existing behavior Fixes: #25961
1 parent 1d2d7e1 commit 72ae1c8

File tree

12 files changed

+297
-61
lines changed

12 files changed

+297
-61
lines changed

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/select-demo.html

Lines changed: 37 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,35 @@ <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 class="demo-narrow-sandwich">
365+
<mat-form-field>
366+
<mat-label>Bread</mat-label>
367+
<mat-select [(ngModel)]="sandwichBread">
368+
<mat-option *ngFor="let bread of breads" [value]="bread.value">
369+
{{ bread.viewValue }}
370+
</mat-option>
371+
</mat-select>
372+
</mat-form-field>
373+
<mat-form-field>
374+
<mat-label>Meat</mat-label>
375+
<mat-select [(ngModel)]="sandwichMeat">
376+
<mat-option *ngFor="let meat of meats" [value]="meat.value">
377+
{{ meat.viewValue }}
378+
</mat-option>
379+
</mat-select>
380+
</mat-form-field>
381+
<mat-form-field>
382+
<mat-label>Cheese</mat-label>
383+
<mat-select [(ngModel)]="sandwichCheese">
384+
<mat-option *ngFor="let cheese of cheeses" [value]="cheese.value">
385+
{{ cheese.viewValue }}
386+
</mat-option>
387+
</mat-select>
388+
</mat-form-field>
389+
390+
</mat-card-content>
391+
392+
</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: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ export class SelectDemo {
4848
drinkObjectRequired = false;
4949
pokemonRequired = false;
5050
drinksDisabled = false;
51+
drinksOptionsDisabled = false;
5152
pokemonDisabled = false;
53+
pokemonOptionsDisabled = false;
5254
showSelect = false;
5355
currentDrink: string;
5456
currentDrinkObject: {} | undefined = {value: 'tea-5', viewValue: 'Tea'};
@@ -66,6 +68,10 @@ export class SelectDemo {
6668
compareByValue = true;
6769
selectFormControl = new FormControl('', Validators.required);
6870

71+
sandwichBread = '';
72+
sandwichMeat = '';
73+
sandwichCheese = '';
74+
6975
foods = [
7076
{value: null, viewValue: 'None'},
7177
{value: 'steak-0', viewValue: 'Steak'},
@@ -74,19 +80,19 @@ export class SelectDemo {
7480
];
7581

7682
drinks = [
77-
{value: 'coke-0', viewValue: 'Coke', disabled: false},
83+
{value: 'coke-0', viewValue: 'Coke'},
7884
{
7985
value: 'long-name-1',
8086
viewValue: 'Decaf Chocolate Brownie Vanilla Gingerbread Frappuccino',
8187
disabled: false,
8288
},
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},
89+
{value: 'water-2', viewValue: 'Water'},
90+
{value: 'pepper-3', viewValue: 'Dr. Pepper'},
91+
{value: 'coffee-4', viewValue: 'Coffee'},
92+
{value: 'tea-5', viewValue: 'Tea'},
93+
{value: 'juice-6', viewValue: 'Orange juice'},
94+
{value: 'wine-7', viewValue: 'Wine'},
95+
{value: 'milk-8', viewValue: 'Milk'},
9096
];
9197

9298
pokemon = [
@@ -149,6 +155,26 @@ export class SelectDemo {
149155
{value: 'indramon-5', viewValue: 'Indramon'},
150156
];
151157

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

src/material/core/option/option.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55

66
<span class="mdc-list-item__primary-text" #text><ng-content></ng-content></span>
77

8+
<!-- Render checkmark at the end for single-selection. -->
9+
<mat-pseudo-checkbox *ngIf="!multiple && selected" class="mat-mdc-option-pseudo-checkbox"
10+
state="checked" [disabled]="disabled" appearance="minimal"></mat-pseudo-checkbox>
11+
812
<!-- See a11y notes inside optgroup.ts for context behind this element. -->
913
<span class="cdk-visually-hidden" *ngIf="group && group._inert">({{ group.label }})</span>
1014

src/material/core/option/option.scss

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
}
4949

5050
.mat-icon,
51-
.mat-pseudo-checkbox {
51+
.mat-pseudo-checkbox-full {
5252
margin-right: mdc-list-variables.$side-padding;
5353
flex-shrink: 0;
5454

@@ -58,6 +58,16 @@
5858
}
5959
}
6060

61+
.mat-pseudo-checkbox-minimal {
62+
margin-left: mdc-list-variables.$side-padding;
63+
flex-shrink: 0;
64+
65+
[dir='rtl'] & {
66+
margin-right: mdc-list-variables.$side-padding;
67+
margin-left: 0;
68+
}
69+
}
70+
6171
// Increase specificity because ripple styles are part of the `mat-core` mixin and can
6272
// potentially overwrite the absolute position of the container.
6373
.mat-mdc-option-ripple {
@@ -84,6 +94,8 @@
8494
font-family: inherit;
8595
text-decoration: inherit;
8696
text-transform: inherit;
97+
98+
flex-grow: 1;
8799
}
88100

89101
@include cdk.high-contrast(active, off) {

src/material/core/selection/pseudo-checkbox/_pseudo-checkbox-common.scss

Lines changed: 63 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,79 @@
44
// Padding inside of a pseudo checkbox.
55
$padding: checkbox-common.$border-width * 2;
66

7+
// Center a checkmark indicator inside the checkbox.
8+
//
9+
// `$box-size`: size of the checkbox
10+
// `$mark-size`: size of the checkmark indicator
11+
@mixin _checkbox-checked-styles-with-size($box-size, $mark-size) {
12+
// Center a checkmark. `$checkbox-cmmon.$border-width` is the width of the line of the checkmark.
13+
$width: $mark-size;
14+
$height: math.div($mark-size - checkbox-common.$border-width, 2);
15+
16+
// The rendered length of the short-side of the checkmark graphic when rendered. Add length of
17+
// the border-width since this element is content-box.
18+
$short-side: $height + checkbox-common.$border-width;
19+
20+
width: $width;
21+
height: $height;
22+
23+
// Rotate on the center of the element. This makes it easier to center the checkmark graphic.
24+
transform-origin: center;
25+
26+
// Take negative one times the distance from the top corner of the checkmark graphic to the top
27+
// of the element in its rotated position. This accounts for the top corner of the elemant being
28+
// blank since we only use the left and bottom borders to draw the checkmark graphic.
29+
top: -1 * math.div($short-side - checkbox-common.$border-width, math.sqrt(2));
30+
31+
left: 0;
32+
bottom: 0;
33+
right: 0;
34+
35+
// center the checkmark graphic with margin auto
36+
margin: auto;
37+
}
38+
39+
// Center a horizontal line placed in the vertical and horizontal center of the checkbox. It does
40+
// not touch the border of the checkbox.
41+
//
42+
// `$box-size`: size of the checkbox.
43+
// `$border-size`: size of the checkbox's border.
44+
@mixin _checkbox-indeterminate-styles-with-size($box-size, $border-size) {
45+
// Center the line in the the checkbox. `$checkbox-common.$border-width` is the width of the line.
46+
top: math.div($box-size - checkbox-common.$border-width, 2) - $border-size;
47+
width: $box-size - checkbox-common.$border-width - (2 * $border-size);
48+
}
49+
750
/// Applies the styles that set the size of the pseudo checkbox
851
@mixin size($box-size) {
9-
$mark-size: $box-size - (2 * $padding);
1052

1153
.mat-pseudo-checkbox {
1254
width: $box-size;
1355
height: $box-size;
1456
}
1557

16-
.mat-pseudo-checkbox-indeterminate::after {
17-
top: math.div($box-size - checkbox-common.$border-width, 2) -
18-
checkbox-common.$border-width;
19-
width: $box-size - 6px;
58+
.mat-pseudo-checkbox-minimal {
59+
$mark-size: $box-size - $padding;
60+
$border-size: 0; // Minimal appearance does not have a border.
61+
62+
&.mat-pseudo-checkbox-checked::after {
63+
@include _checkbox-checked-styles-with-size($box-size, $mark-size);
64+
}
65+
&.mat-pseudo-checkbox-indeterminate::after {
66+
@include _checkbox-indeterminate-styles-with-size($box-size, $border-size);
67+
}
2068
}
2169

22-
.mat-pseudo-checkbox-checked::after {
23-
top: math.div($box-size, 2) - math.div($mark-size, 4) -
24-
math.div($box-size, 10) - checkbox-common.$border-width;
25-
width: $mark-size;
26-
height: math.div($mark-size - checkbox-common.$border-width, 2);
70+
.mat-pseudo-checkbox-full {
71+
$mark-size: $box-size - (2 * $padding); // Apply a smaller mark to account for the border.
72+
$border-size: checkbox-common.$border-width;
73+
74+
&.mat-pseudo-checkbox-checked::after {
75+
@include _checkbox-checked-styles-with-size($box-size, $mark-size);
76+
}
77+
&.mat-pseudo-checkbox-indeterminate::after {
78+
@include _checkbox-indeterminate-styles-with-size($box-size, $border-size);
79+
}
2780
}
2881
}
2982

0 commit comments

Comments
 (0)