Skip to content

Commit 18283be

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 c08e3f1 commit 18283be

File tree

11 files changed

+221
-60
lines changed

11 files changed

+221
-60
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: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33

44
<span class="mdc-list-item__primary-text"><ng-content></ng-content></span>
55

6+
<!-- Render checkmark at the end to avoid an awkward whitespace when noting is selected. -->
7+
<mat-pseudo-checkbox *ngIf="!multiple" appearance="minimal" class="mat-mdc-option-pseudo-checkbox"
8+
[state]="selected ? 'checked' : 'unchecked'" [disabled]="disabled"></mat-pseudo-checkbox>
9+
610
<!-- See a11y notes inside optgroup.ts for context behind this element. -->
711
<span class="cdk-visually-hidden" *ngIf="group && group._inert">({{ group.label }})</span>
812

src/material/core/option/option.scss

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@
8181
font-family: inherit;
8282
text-decoration: inherit;
8383
text-transform: inherit;
84+
85+
flex-grow: 1;
8486
}
8587

8688
@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

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

Lines changed: 42 additions & 27 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) {
5+
.mat-pseudo-checkbox-checked,
6+
.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,43 +37,38 @@
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 {
65+
.mat-pseudo-checkbox-disabled.mat-pseudo-checkbox-checked,
66+
.mat-pseudo-checkbox-disabled.mat-pseudo-checkbox-indeterminate {
67+
&.mat-pseudo-checkbox-minimal::after {
68+
color: $disabled-color;
69+
}
70+
71+
&.mat-pseudo-checkbox-full {
5772
background: $disabled-color;
5873
}
5974
}

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)