From e8ee16841630f5d16ccdccdb2c0a21e1a6389835 Mon Sep 17 00:00:00 2001 From: Zach Arend Date: Mon, 7 Nov 2022 19:17:33 +0000 Subject: [PATCH] fix(material/core): display checkmark for single-selection Display checkmark for single-selection. 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. --- src/material/core/BUILD.bazel | 7 +++ src/material/core/option/index.ts | 10 +++- src/material/core/option/option.html | 3 + src/material/core/selection/index.ts | 2 + .../_pseudo-checkmark-common.scss | 18 ++++++ .../_pseudo-checkmark-theme.import.scss | 8 +++ .../_pseudo-checkmark-theme.scss | 35 ++++++++++++ .../pseudo-checkmark-module.ts | 18 ++++++ .../pseudo-checkmark/pseudo-checkmark.scss | 30 ++++++++++ .../pseudo-checkmark/pseudo-checkmark.ts | 55 +++++++++++++++++++ tools/public_api_guard/material/core.md | 28 +++++++++- 11 files changed, 211 insertions(+), 3 deletions(-) create mode 100644 src/material/core/selection/pseudo-checkmark/_pseudo-checkmark-common.scss create mode 100644 src/material/core/selection/pseudo-checkmark/_pseudo-checkmark-theme.import.scss create mode 100644 src/material/core/selection/pseudo-checkmark/_pseudo-checkmark-theme.scss create mode 100644 src/material/core/selection/pseudo-checkmark/pseudo-checkmark-module.ts create mode 100644 src/material/core/selection/pseudo-checkmark/pseudo-checkmark.scss create mode 100644 src/material/core/selection/pseudo-checkmark/pseudo-checkmark.ts diff --git a/src/material/core/BUILD.bazel b/src/material/core/BUILD.bazel index 62aa5d436820..36ffe5386c3e 100644 --- a/src/material/core/BUILD.bazel +++ b/src/material/core/BUILD.bazel @@ -21,6 +21,7 @@ ng_module( ), assets = [ ":selection/pseudo-checkbox/pseudo-checkbox.css", + ":selection/pseudo-checkmark/pseudo-checkmark.css", ":option/option.css", ":option/optgroup.css", ] + glob(["**/*.html"]), @@ -81,6 +82,12 @@ sass_binary( deps = [":core_scss_lib"], ) +sass_binary( + name = "pseudo_checkmark_scss", + src = "selection/pseudo-checkmark/pseudo-checkmark.scss", + deps = [":core_scss_lib"], +) + sass_binary( name = "option_scss", src = "option/option.scss", diff --git a/src/material/core/option/index.ts b/src/material/core/option/index.ts index 96da5f1c97fc..ad285e9551c5 100644 --- a/src/material/core/option/index.ts +++ b/src/material/core/option/index.ts @@ -9,13 +9,19 @@ import {NgModule} from '@angular/core'; import {CommonModule} from '@angular/common'; import {MatRippleModule} from '../ripple/index'; -import {MatPseudoCheckboxModule} from '../selection/index'; +import {MatPseudoCheckboxModule, MatPseudoCheckmarkModule} from '../selection/index'; import {MatCommonModule} from '../common-behaviors/common-module'; import {MatOption} from './option'; import {MatOptgroup} from './optgroup'; @NgModule({ - imports: [MatRippleModule, CommonModule, MatCommonModule, MatPseudoCheckboxModule], + imports: [ + MatRippleModule, + CommonModule, + MatCommonModule, + MatPseudoCheckboxModule, + MatPseudoCheckmarkModule, + ], exports: [MatOption, MatOptgroup], declarations: [MatOption, MatOptgroup], }) diff --git a/src/material/core/option/option.html b/src/material/core/option/option.html index cf0210e9cc04..757dcf5cb9a7 100644 --- a/src/material/core/option/option.html +++ b/src/material/core/option/option.html @@ -1,6 +1,9 @@ + + diff --git a/src/material/core/selection/index.ts b/src/material/core/selection/index.ts index 102eaefd2bab..7dab94b643bd 100644 --- a/src/material/core/selection/index.ts +++ b/src/material/core/selection/index.ts @@ -8,3 +8,5 @@ export * from './pseudo-checkbox/pseudo-checkbox'; export * from './pseudo-checkbox/pseudo-checkbox-module'; +export * from './pseudo-checkmark/pseudo-checkmark'; +export * from './pseudo-checkmark/pseudo-checkmark-module'; diff --git a/src/material/core/selection/pseudo-checkmark/_pseudo-checkmark-common.scss b/src/material/core/selection/pseudo-checkmark/_pseudo-checkmark-common.scss new file mode 100644 index 000000000000..4b526b2a4e53 --- /dev/null +++ b/src/material/core/selection/pseudo-checkmark/_pseudo-checkmark-common.scss @@ -0,0 +1,18 @@ +@use 'sass:math'; +@use '../../style/checkbox-common'; + +// Padding inside of a pseudo checkmark. +$padding: checkbox-common.$border-width * 2; + +/// Applies the styles that set the size of the pseudo checkmark +@mixin size($box-size) { + .mat-pseudo-checkmark { + width: $box-size; + height: $box-size; + } +} + +/// Applies the legacy size styles to the pseudo-checkmark +@mixin legacy-size() { + @include size(check-common.$legacy-size); +} diff --git a/src/material/core/selection/pseudo-checkmark/_pseudo-checkmark-theme.import.scss b/src/material/core/selection/pseudo-checkmark/_pseudo-checkmark-theme.import.scss new file mode 100644 index 000000000000..03f9a07e6b53 --- /dev/null +++ b/src/material/core/selection/pseudo-checkmark/_pseudo-checkmark-theme.import.scss @@ -0,0 +1,8 @@ +@forward '../../density/private/compatibility' as mat-*; +@forward '../../theming/palette'; // TODO: hide unused colors +@forward '../../theming/palette' as mat-*; // TODO: hide unused colors +@forward '../../theming/theming' as mat-*; +@import '../../theming/theming'; +@forward 'pseudo-checkmark-theme' hide color, theme, typography; +@forward 'pseudo-checkmark-theme' as mat-pseudo-checkmark-* hide mat-pseudo-checkmark-density; + diff --git a/src/material/core/selection/pseudo-checkmark/_pseudo-checkmark-theme.scss b/src/material/core/selection/pseudo-checkmark/_pseudo-checkmark-theme.scss new file mode 100644 index 000000000000..d58a8ea4a6ed --- /dev/null +++ b/src/material/core/selection/pseudo-checkmark/_pseudo-checkmark-theme.scss @@ -0,0 +1,35 @@ +@use 'sass:map'; +@use '../../theming/theming'; + +@mixin color($config-or-theme) { + $config: theming.get-color-config($config-or-theme); + $is-dark-theme: map.get($config, is-dark); + $primary: map.get($config, primary); + $accent: map.get($config, accent); + $warn: map.get($config, warn); + + // TODO: implement colors +} + +@mixin typography($config-or-theme) {} + +@mixin _density($config-or-theme) {} + +@mixin theme($theme-or-color-config) { + $theme: theming.private-legacy-get-theme($theme-or-color-config); + @include theming.private-check-duplicate-theme-styles($theme, 'mat-pseudo-checkmark') { + $color: theming.get-color-config($theme); + $density: theming.get-density-config($theme); + $typography: theming.get-typography-config($theme); + + @if $color != null { + @include color($color); + } + @if $density != null { + @include _density($density); + } + @if $typography != null { + @include typography($typography); + } + } +} \ No newline at end of file diff --git a/src/material/core/selection/pseudo-checkmark/pseudo-checkmark-module.ts b/src/material/core/selection/pseudo-checkmark/pseudo-checkmark-module.ts new file mode 100644 index 000000000000..e91461a7396b --- /dev/null +++ b/src/material/core/selection/pseudo-checkmark/pseudo-checkmark-module.ts @@ -0,0 +1,18 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {NgModule} from '@angular/core'; +import {MatPseudoCheckmark} from './pseudo-checkmark'; +import {MatCommonModule} from '../../common-behaviors/common-module'; + +@NgModule({ + imports: [MatCommonModule], + exports: [MatPseudoCheckmark], + declarations: [MatPseudoCheckmark], +}) +export class MatPseudoCheckmarkModule {} diff --git a/src/material/core/selection/pseudo-checkmark/pseudo-checkmark.scss b/src/material/core/selection/pseudo-checkmark/pseudo-checkmark.scss new file mode 100644 index 000000000000..923c152742de --- /dev/null +++ b/src/material/core/selection/pseudo-checkmark/pseudo-checkmark.scss @@ -0,0 +1,30 @@ +// TODO: implement styles +@use 'sass:math'; +@use '../../style/checkbox-common'; +@use '../../style/private'; +@use '../../style/variables'; +@use './pseudo-checkmark-common'; + +// TODO: implement a visual checkmark +.mat-pseudo-checkmark { + display: inline-block; + box-sizing: border-box; + position: relative; + flex-shrink: 0; + &::after { + position: absolute; + opacity: 0; + font-family: monospace; + content: '✔️'; // TODO: draw a checkmark with CSS. this is jsut a placeholder + } + + &.mat-pseudo-checkmark-checked::after { + opacity: 1; + } +} + +.mat-pseudo-checkmark-disabled { + cursor: default; +} + +@include pseudo-checkmark-common.size(checkbox-common.$size); \ No newline at end of file diff --git a/src/material/core/selection/pseudo-checkmark/pseudo-checkmark.ts b/src/material/core/selection/pseudo-checkmark/pseudo-checkmark.ts new file mode 100644 index 000000000000..33d0613bd3c6 --- /dev/null +++ b/src/material/core/selection/pseudo-checkmark/pseudo-checkmark.ts @@ -0,0 +1,55 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import { + Component, + ViewEncapsulation, + Input, + ChangeDetectionStrategy, + Inject, + Optional, +} from '@angular/core'; +import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations'; + +export type MatPseudoCheckmarkState = 'unchecked' | 'checked'; + +/** + * Component that shows a simplified checkmark without including any kind of "real" checkmark. + * Meant to be used when the checkmark is purely decorative and a large number of them will be + * included, such as for the options in a single-select. Uses no SVGs or complex animations. + * Note that theming is meant to be handled by the parent element, e.g. + * `mat-primary .mat-pseudo-checkmark`. + * + * Note that this component will be completely invisible to screen-reader users. This is *not* + * interchangeable with `` and should *not* be used if the user would directly + * interact with the checkmark. The pseudo-checkmark should only be used as an implementation detail + * of more complex components that appropriately handle selected / checked state. + * @docs-private + */ +@Component({ + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'mat-pseudo-checkmark', + styleUrls: ['pseudo-checkmark.css'], + template: '', + host: { + 'class': 'mat-pseudo-checkmark', + '[class.mat-pseudo-checkmark-checked]': 'state === "checked"', + '[class.mat-pseudo-checkmark-disabled]': 'disabled', + '[class._mat-animation-noopable]': '_animationMode === "NoopAnimations"', + }, +}) +export class MatPseudoCheckmark { + /** Display state of the checkbox. */ + @Input() state: MatPseudoCheckmarkState = 'unchecked'; + + /** Whether the checkbox is disabled. */ + @Input() disabled: boolean = false; + + constructor(@Optional() @Inject(ANIMATION_MODULE_TYPE) public _animationMode?: string) {} +} diff --git a/tools/public_api_guard/material/core.md b/tools/public_api_guard/material/core.md index 16d072137fa9..f3895c8a5b79 100644 --- a/tools/public_api_guard/material/core.md +++ b/tools/public_api_guard/material/core.md @@ -312,7 +312,7 @@ export class MatOptionModule { // (undocumented) static ɵinj: i0.ɵɵInjectorDeclaration; // (undocumented) - static ɵmod: i0.ɵɵNgModuleDeclaration; + static ɵmod: i0.ɵɵNgModuleDeclaration; } // @public @@ -360,6 +360,32 @@ export class MatPseudoCheckboxModule { // @public export type MatPseudoCheckboxState = 'unchecked' | 'checked' | 'indeterminate'; +// @public +export class MatPseudoCheckmark { + constructor(_animationMode?: string | undefined); + // (undocumented) + _animationMode?: string | undefined; + disabled: boolean; + state: MatPseudoCheckmarkState; + // (undocumented) + static ɵcmp: i0.ɵɵComponentDeclaration; + // (undocumented) + static ɵfac: i0.ɵɵFactoryDeclaration; +} + +// @public (undocumented) +export class MatPseudoCheckmarkModule { + // (undocumented) + static ɵfac: i0.ɵɵFactoryDeclaration; + // (undocumented) + static ɵinj: i0.ɵɵInjectorDeclaration; + // (undocumented) + static ɵmod: i0.ɵɵNgModuleDeclaration; +} + +// @public (undocumented) +export type MatPseudoCheckmarkState = 'unchecked' | 'checked'; + // @public (undocumented) export class MatRipple implements OnInit, OnDestroy, RippleTarget { constructor(_elementRef: ElementRef, ngZone: NgZone, platform: Platform, globalOptions?: RippleGlobalOptions, _animationMode?: string | undefined);