Skip to content

Commit d3b277b

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 1e56524 commit d3b277b

File tree

10 files changed

+196
-65
lines changed

10 files changed

+196
-65
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: 5 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 value="" [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>

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

Lines changed: 10 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'};
@@ -74,19 +76,19 @@ export class SelectDemo {
7476
];
7577

7678
drinks = [
77-
{value: 'coke-0', viewValue: 'Coke', disabled: false},
79+
{value: 'coke-0', viewValue: 'Coke'},
7880
{
7981
value: 'long-name-1',
8082
viewValue: 'Decaf Chocolate Brownie Vanilla Gingerbread Frappuccino',
8183
disabled: false,
8284
},
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},
85+
{value: 'water-2', viewValue: 'Water'},
86+
{value: 'pepper-3', viewValue: 'Dr. Pepper'},
87+
{value: 'coffee-4', viewValue: 'Coffee'},
88+
{value: 'tea-5', viewValue: 'Tea'},
89+
{value: 'juice-6', viewValue: 'Orange juice'},
90+
{value: 'wine-7', viewValue: 'Wine'},
91+
{value: 'milk-8', viewValue: 'Milk'},
9092
];
9193

9294
pokemon = [

src/material/core/option/option.html

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
<mat-pseudo-checkbox *ngIf="multiple" class="mat-mdc-option-pseudo-checkbox"
2-
[state]="selected ? 'checked' : 'unchecked'" [disabled]="disabled"></mat-pseudo-checkbox>
1+
<mat-pseudo-checkbox [appearance]="multiple ? 'full' : 'minimal'"
2+
class="mat-mdc-option-pseudo-checkbox" [state]="selected ? 'checked' : 'unchecked'"
3+
[disabled]="disabled"></mat-pseudo-checkbox>
34

45
<span class="mdc-list-item__primary-text"><ng-content></ng-content></span>
56

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

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,59 @@
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+
top: math.div($box-size, 2) - math.div($mark-size, 4) -
14+
math.div($box-size, 10) - checkbox-common.$border-width;
15+
width: $mark-size;
16+
height: math.div($mark-size - checkbox-common.$border-width, 2);
17+
}
18+
19+
// Center a horizontal line placed in the vertical and horizontal center of the checkbox. It does
20+
// not touch the border of the checkbox.
21+
//
22+
// `$box-size`: size of the checkbox.
23+
// `$border-size`: size of the checkbox's border.
24+
@mixin _checkbox-indeterminate-styles-with-size($box-size, $border-size) {
25+
// Center the line in the the checkbox. `$checkbox-common.$border-width` is the width of the line.
26+
top: math.div($box-size - checkbox-common.$border-width, 2) - $border-size;
27+
width: $box-size - checkbox-common.$border-width - (2 * $border-size);
28+
}
29+
730
/// Applies the styles that set the size of the pseudo checkbox
831
@mixin size($box-size) {
9-
$mark-size: $box-size - (2 * $padding);
1032

1133
.mat-pseudo-checkbox {
1234
width: $box-size;
1335
height: $box-size;
1436
}
1537

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;
38+
.mat-pseudo-checkbox-minimal {
39+
$mark-size: $box-size - $padding;
40+
$border-size: 0; // Minimal appearance does not have a border.
41+
42+
&.mat-pseudo-checkbox-checked::after {
43+
@include _checkbox-checked-styles-with-size($box-size, $mark-size);
44+
}
45+
&.mat-pseudo-checkbox-indeterminate::after {
46+
@include _checkbox-indeterminate-styles-with-size($box-size, $border-size);
47+
}
2048
}
2149

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);
50+
.mat-pseudo-checkbox-full {
51+
$mark-size: $box-size - (2 * $padding); // Apply a smaller mark to account for the border.
52+
$border-size: checkbox-common.$border-width;
53+
54+
&.mat-pseudo-checkbox-checked::after {
55+
@include _checkbox-checked-styles-with-size($box-size, $mark-size);
56+
}
57+
&.mat-pseudo-checkbox-indeterminate::after {
58+
@include _checkbox-indeterminate-styles-with-size($box-size, $border-size);
59+
}
2760
}
2861
}
2962

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

Lines changed: 40 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,33 @@
11
@use 'sass:map';
22
@use '../../theming/theming';
33

4+
@mixin _psuedo-checkbox-styles-with-color($text-color, $background, $root-selector: '') {
5+
#{$root-selector}.mat-pseudo-checkbox-checked,
6+
#{$root-selector}.mat-pseudo-checkbox-indeterminate {
7+
&.mat-pseudo-checkbox-minimal::after {
8+
color: $text-color;
9+
}
10+
11+
// Full (checkbox) appearance inverts colors of text and background.
12+
&.mat-pseudo-checkbox-full {
13+
&::after {
14+
color: $background;
15+
}
16+
17+
background: $text-color;
18+
}
19+
}
20+
}
21+
422
@mixin color($config-or-theme) {
523
$config: theming.get-color-config($config-or-theme);
624
$is-dark-theme: map.get($config, is-dark);
7-
$primary: map.get($config, primary);
8-
$accent: map.get($config, accent);
9-
$warn: map.get($config, warn);
10-
$background: map.get($config, background);
25+
26+
$primary: theming.get-color-from-palette(map.get($config, primary));
27+
$accent: theming.get-color-from-palette(map.get($config, accent));
28+
$warn: theming.get-color-from-palette(map.get($config, warn));
29+
$background: theming.get-color-from-palette(map.get($config, background), background);
30+
$secondary-text: theming.get-color-from-palette(map.get($config, foreground), secondary-text);
1131

1232
// NOTE(traviskaufman): While the spec calls for translucent blacks/whites for disabled colors,
1333
// this does not work well with elements layered on top of one another. To get around this we
@@ -17,46 +37,36 @@
1737
$disabled-color: if($is-dark-theme, $white-30pct-opacity-on-dark, $black-26pct-opacity-on-light);
1838
$colored-box-selector: '.mat-pseudo-checkbox-checked, .mat-pseudo-checkbox-indeterminate';
1939

20-
.mat-pseudo-checkbox {
21-
color: theming.get-color-from-palette(map.get($config, foreground), secondary-text);
22-
23-
&::after {
24-
color: theming.get-color-from-palette($background, background);
40+
.mat-pseudo-checkbox-full {
41+
color: $secondary-text;
42+
&.mat-pseudo-checkbox-disabled {
43+
color: $disabled-color;
2544
}
2645
}
2746

28-
.mat-pseudo-checkbox-disabled {
29-
color: $disabled-color;
30-
}
31-
32-
.mat-primary .mat-pseudo-checkbox-checked,
33-
.mat-primary .mat-pseudo-checkbox-indeterminate {
34-
background: theming.get-color-from-palette(map.get($config, primary));
47+
.mat-primary {
48+
@include _psuedo-checkbox-styles-with-color($primary, $background);
3549
}
3650

3751
// Default to the accent color. Note that the pseudo checkboxes are meant to inherit the
3852
// theme from their parent, rather than implementing their own theming, which is why we
3953
// don't attach to the `mat-*` classes. Also note that this needs to be below `.mat-primary`
4054
// in order to allow for the color to be overwritten if the checkbox is inside a parent that
4155
// has `mat-accent` and is placed inside another parent that has `mat-primary`.
42-
.mat-pseudo-checkbox-checked,
43-
.mat-pseudo-checkbox-indeterminate,
44-
.mat-accent .mat-pseudo-checkbox-checked,
45-
.mat-accent .mat-pseudo-checkbox-indeterminate {
46-
background: theming.get-color-from-palette(map.get($config, accent));
56+
@include _psuedo-checkbox-styles-with-color($accent, $background);
57+
.mat-accent {
58+
@include _psuedo-checkbox-styles-with-color($accent, $background);
4759
}
4860

49-
.mat-warn .mat-pseudo-checkbox-checked,
50-
.mat-warn .mat-pseudo-checkbox-indeterminate {
51-
background: theming.get-color-from-palette(map.get($config, warn));
61+
.mat-warn {
62+
@include _psuedo-checkbox-styles-with-color($warn, $background);
5263
}
5364

54-
.mat-pseudo-checkbox-checked,
55-
.mat-pseudo-checkbox-indeterminate {
56-
&.mat-pseudo-checkbox-disabled {
57-
background: $disabled-color;
58-
}
59-
}
65+
@include _psuedo-checkbox-styles-with-color(
66+
$disabled-color,
67+
null,
68+
'.mat-pseudo-checkbox-disabled'
69+
);
6070
}
6171

6272
@mixin typography($config-or-theme) {}

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

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
@use './pseudo-checkbox-common';
66

77
.mat-pseudo-checkbox {
8-
border: checkbox-common.$border-width solid;
98
border-radius: 2px;
109
cursor: pointer;
1110
display: inline-block;
@@ -27,10 +26,6 @@
2726
variables.$linear-out-slow-in-timing-function;
2827
}
2928

30-
&.mat-pseudo-checkbox-checked, &.mat-pseudo-checkbox-indeterminate {
31-
border-color: transparent;
32-
}
33-
3429
@include private.private-animation-noop {
3530
&::after {
3631
transition: none;
@@ -56,4 +51,12 @@
5651
box-sizing: content-box;
5752
}
5853

54+
.mat-pseudo-checkbox-full {
55+
border: checkbox-common.$border-width solid;
56+
&.mat-pseudo-checkbox-checked, &.mat-pseudo-checkbox-indeterminate {
57+
border-color: transparent;
58+
}
59+
}
60+
61+
5962
@include pseudo-checkbox-common.size(checkbox-common.$size);

src/material/core/selection/pseudo-checkbox/pseudo-checkbox.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ export type MatPseudoCheckboxState = 'unchecked' | 'checked' | 'indeterminate';
4646
'[class.mat-pseudo-checkbox-indeterminate]': 'state === "indeterminate"',
4747
'[class.mat-pseudo-checkbox-checked]': 'state === "checked"',
4848
'[class.mat-pseudo-checkbox-disabled]': 'disabled',
49+
'[class.mat-pseudo-checkbox-minimal]': 'appearance === "minimal"',
50+
'[class.mat-pseudo-checkbox-full]': 'appearance === "full"',
4951
'[class._mat-animation-noopable]': '_animationMode === "NoopAnimations"',
5052
},
5153
})
@@ -56,5 +58,11 @@ export class MatPseudoCheckbox {
5658
/** Whether the checkbox is disabled. */
5759
@Input() disabled: boolean = false;
5860

61+
/**
62+
* Appearance of the pseudo checkbox. Default appearance of 'full' renders a checkmark/mixedmark
63+
* indicator inside a square box. 'minimal' appearance only renders the checkmark/mixedmark.
64+
*/
65+
@Input() appearance: 'minimal' | 'full' = 'full';
66+
5967
constructor(@Optional() @Inject(ANIMATION_MODULE_TYPE) public _animationMode?: string) {}
6068
}

0 commit comments

Comments
 (0)