Skip to content

Commit e8ee168

Browse files
committed
fix(material/core): display checkmark for single-selection
Display checkmark for single-selection. <mat-option> displays a checkmark using the pseudo-checkmark. Fix a11y issue where selected state for single-selection is visually communicated with color alone. Display selected state with both color and checkmark graphic.
1 parent 7e3a9df commit e8ee168

File tree

11 files changed

+211
-3
lines changed

11 files changed

+211
-3
lines changed

src/material/core/BUILD.bazel

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ ng_module(
2121
),
2222
assets = [
2323
":selection/pseudo-checkbox/pseudo-checkbox.css",
24+
":selection/pseudo-checkmark/pseudo-checkmark.css",
2425
":option/option.css",
2526
":option/optgroup.css",
2627
] + glob(["**/*.html"]),
@@ -81,6 +82,12 @@ sass_binary(
8182
deps = [":core_scss_lib"],
8283
)
8384

85+
sass_binary(
86+
name = "pseudo_checkmark_scss",
87+
src = "selection/pseudo-checkmark/pseudo-checkmark.scss",
88+
deps = [":core_scss_lib"],
89+
)
90+
8491
sass_binary(
8592
name = "option_scss",
8693
src = "option/option.scss",

src/material/core/option/index.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,19 @@
99
import {NgModule} from '@angular/core';
1010
import {CommonModule} from '@angular/common';
1111
import {MatRippleModule} from '../ripple/index';
12-
import {MatPseudoCheckboxModule} from '../selection/index';
12+
import {MatPseudoCheckboxModule, MatPseudoCheckmarkModule} from '../selection/index';
1313
import {MatCommonModule} from '../common-behaviors/common-module';
1414
import {MatOption} from './option';
1515
import {MatOptgroup} from './optgroup';
1616

1717
@NgModule({
18-
imports: [MatRippleModule, CommonModule, MatCommonModule, MatPseudoCheckboxModule],
18+
imports: [
19+
MatRippleModule,
20+
CommonModule,
21+
MatCommonModule,
22+
MatPseudoCheckboxModule,
23+
MatPseudoCheckmarkModule,
24+
],
1925
exports: [MatOption, MatOptgroup],
2026
declarations: [MatOption, MatOptgroup],
2127
})

src/material/core/option/option.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
<mat-pseudo-checkbox *ngIf="multiple" class="mat-mdc-option-pseudo-checkbox"
22
[state]="selected ? 'checked' : 'unchecked'" [disabled]="disabled"></mat-pseudo-checkbox>
33

4+
<mat-pseudo-checkmark *ngIf="!multiple" class="mat-mdc-option-pseudo-checkmark"
5+
[state]="selected ? 'checked' : 'unchecked'" [disabled]="disabled"></mat-pseudo-checkmark>
6+
47
<span class="mdc-list-item__primary-text"><ng-content></ng-content></span>
58

69
<!-- See a11y notes inside optgroup.ts for context behind this element. -->

src/material/core/selection/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,5 @@
88

99
export * from './pseudo-checkbox/pseudo-checkbox';
1010
export * from './pseudo-checkbox/pseudo-checkbox-module';
11+
export * from './pseudo-checkmark/pseudo-checkmark';
12+
export * from './pseudo-checkmark/pseudo-checkmark-module';
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
@use 'sass:math';
2+
@use '../../style/checkbox-common';
3+
4+
// Padding inside of a pseudo checkmark.
5+
$padding: checkbox-common.$border-width * 2;
6+
7+
/// Applies the styles that set the size of the pseudo checkmark
8+
@mixin size($box-size) {
9+
.mat-pseudo-checkmark {
10+
width: $box-size;
11+
height: $box-size;
12+
}
13+
}
14+
15+
/// Applies the legacy size styles to the pseudo-checkmark
16+
@mixin legacy-size() {
17+
@include size(check-common.$legacy-size);
18+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
@forward '../../density/private/compatibility' as mat-*;
2+
@forward '../../theming/palette'; // TODO: hide unused colors
3+
@forward '../../theming/palette' as mat-*; // TODO: hide unused colors
4+
@forward '../../theming/theming' as mat-*;
5+
@import '../../theming/theming';
6+
@forward 'pseudo-checkmark-theme' hide color, theme, typography;
7+
@forward 'pseudo-checkmark-theme' as mat-pseudo-checkmark-* hide mat-pseudo-checkmark-density;
8+
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
@use 'sass:map';
2+
@use '../../theming/theming';
3+
4+
@mixin color($config-or-theme) {
5+
$config: theming.get-color-config($config-or-theme);
6+
$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+
11+
// TODO: implement colors
12+
}
13+
14+
@mixin typography($config-or-theme) {}
15+
16+
@mixin _density($config-or-theme) {}
17+
18+
@mixin theme($theme-or-color-config) {
19+
$theme: theming.private-legacy-get-theme($theme-or-color-config);
20+
@include theming.private-check-duplicate-theme-styles($theme, 'mat-pseudo-checkmark') {
21+
$color: theming.get-color-config($theme);
22+
$density: theming.get-density-config($theme);
23+
$typography: theming.get-typography-config($theme);
24+
25+
@if $color != null {
26+
@include color($color);
27+
}
28+
@if $density != null {
29+
@include _density($density);
30+
}
31+
@if $typography != null {
32+
@include typography($typography);
33+
}
34+
}
35+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {NgModule} from '@angular/core';
10+
import {MatPseudoCheckmark} from './pseudo-checkmark';
11+
import {MatCommonModule} from '../../common-behaviors/common-module';
12+
13+
@NgModule({
14+
imports: [MatCommonModule],
15+
exports: [MatPseudoCheckmark],
16+
declarations: [MatPseudoCheckmark],
17+
})
18+
export class MatPseudoCheckmarkModule {}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// TODO: implement styles
2+
@use 'sass:math';
3+
@use '../../style/checkbox-common';
4+
@use '../../style/private';
5+
@use '../../style/variables';
6+
@use './pseudo-checkmark-common';
7+
8+
// TODO: implement a visual checkmark
9+
.mat-pseudo-checkmark {
10+
display: inline-block;
11+
box-sizing: border-box;
12+
position: relative;
13+
flex-shrink: 0;
14+
&::after {
15+
position: absolute;
16+
opacity: 0;
17+
font-family: monospace;
18+
content: '✔️'; // TODO: draw a checkmark with CSS. this is jsut a placeholder
19+
}
20+
21+
&.mat-pseudo-checkmark-checked::after {
22+
opacity: 1;
23+
}
24+
}
25+
26+
.mat-pseudo-checkmark-disabled {
27+
cursor: default;
28+
}
29+
30+
@include pseudo-checkmark-common.size(checkbox-common.$size);
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {
10+
Component,
11+
ViewEncapsulation,
12+
Input,
13+
ChangeDetectionStrategy,
14+
Inject,
15+
Optional,
16+
} from '@angular/core';
17+
import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations';
18+
19+
export type MatPseudoCheckmarkState = 'unchecked' | 'checked';
20+
21+
/**
22+
* Component that shows a simplified checkmark without including any kind of "real" checkmark.
23+
* Meant to be used when the checkmark is purely decorative and a large number of them will be
24+
* included, such as for the options in a single-select. Uses no SVGs or complex animations.
25+
* Note that theming is meant to be handled by the parent element, e.g.
26+
* `mat-primary .mat-pseudo-checkmark`.
27+
*
28+
* Note that this component will be completely invisible to screen-reader users. This is *not*
29+
* interchangeable with `<mat-radio>` and should *not* be used if the user would directly
30+
* interact with the checkmark. The pseudo-checkmark should only be used as an implementation detail
31+
* of more complex components that appropriately handle selected / checked state.
32+
* @docs-private
33+
*/
34+
@Component({
35+
encapsulation: ViewEncapsulation.None,
36+
changeDetection: ChangeDetectionStrategy.OnPush,
37+
selector: 'mat-pseudo-checkmark',
38+
styleUrls: ['pseudo-checkmark.css'],
39+
template: '',
40+
host: {
41+
'class': 'mat-pseudo-checkmark',
42+
'[class.mat-pseudo-checkmark-checked]': 'state === "checked"',
43+
'[class.mat-pseudo-checkmark-disabled]': 'disabled',
44+
'[class._mat-animation-noopable]': '_animationMode === "NoopAnimations"',
45+
},
46+
})
47+
export class MatPseudoCheckmark {
48+
/** Display state of the checkbox. */
49+
@Input() state: MatPseudoCheckmarkState = 'unchecked';
50+
51+
/** Whether the checkbox is disabled. */
52+
@Input() disabled: boolean = false;
53+
54+
constructor(@Optional() @Inject(ANIMATION_MODULE_TYPE) public _animationMode?: string) {}
55+
}

tools/public_api_guard/material/core.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ export class MatOptionModule {
312312
// (undocumented)
313313
static ɵinj: i0.ɵɵInjectorDeclaration<MatOptionModule>;
314314
// (undocumented)
315-
static ɵmod: i0.ɵɵNgModuleDeclaration<MatOptionModule, [typeof i1_3.MatOption, typeof i2.MatOptgroup], [typeof i3.MatRippleModule, typeof i4.CommonModule, typeof i1_2.MatCommonModule, typeof i6.MatPseudoCheckboxModule], [typeof i1_3.MatOption, typeof i2.MatOptgroup]>;
315+
static ɵmod: i0.ɵɵNgModuleDeclaration<MatOptionModule, [typeof i1_3.MatOption, typeof i2.MatOptgroup], [typeof i3.MatRippleModule, typeof i4.CommonModule, typeof i1_2.MatCommonModule, typeof i6.MatPseudoCheckboxModule, typeof i7.MatPseudoCheckmarkModule], [typeof i1_3.MatOption, typeof i2.MatOptgroup]>;
316316
}
317317

318318
// @public
@@ -360,6 +360,32 @@ export class MatPseudoCheckboxModule {
360360
// @public
361361
export type MatPseudoCheckboxState = 'unchecked' | 'checked' | 'indeterminate';
362362

363+
// @public
364+
export class MatPseudoCheckmark {
365+
constructor(_animationMode?: string | undefined);
366+
// (undocumented)
367+
_animationMode?: string | undefined;
368+
disabled: boolean;
369+
state: MatPseudoCheckmarkState;
370+
// (undocumented)
371+
static ɵcmp: i0.ɵɵComponentDeclaration<MatPseudoCheckmark, "mat-pseudo-checkmark", never, { "state": "state"; "disabled": "disabled"; }, {}, never, never, false, never>;
372+
// (undocumented)
373+
static ɵfac: i0.ɵɵFactoryDeclaration<MatPseudoCheckmark, [{ optional: true; }]>;
374+
}
375+
376+
// @public (undocumented)
377+
export class MatPseudoCheckmarkModule {
378+
// (undocumented)
379+
static ɵfac: i0.ɵɵFactoryDeclaration<MatPseudoCheckmarkModule, never>;
380+
// (undocumented)
381+
static ɵinj: i0.ɵɵInjectorDeclaration<MatPseudoCheckmarkModule>;
382+
// (undocumented)
383+
static ɵmod: i0.ɵɵNgModuleDeclaration<MatPseudoCheckmarkModule, [typeof i1_6.MatPseudoCheckmark], [typeof i1_2.MatCommonModule], [typeof i1_6.MatPseudoCheckmark]>;
384+
}
385+
386+
// @public (undocumented)
387+
export type MatPseudoCheckmarkState = 'unchecked' | 'checked';
388+
363389
// @public (undocumented)
364390
export class MatRipple implements OnInit, OnDestroy, RippleTarget {
365391
constructor(_elementRef: ElementRef<HTMLElement>, ngZone: NgZone, platform: Platform, globalOptions?: RippleGlobalOptions, _animationMode?: string | undefined);

0 commit comments

Comments
 (0)