Nested Checklist
diff --git a/src/dev-app/select/BUILD.bazel b/src/dev-app/select/BUILD.bazel
index 29f86eda8d8d..ba99ea4e37ce 100644
--- a/src/dev-app/select/BUILD.bazel
+++ b/src/dev-app/select/BUILD.bazel
@@ -12,6 +12,7 @@ ng_module(
deps = [
"//src/material/button",
"//src/material/card",
+ "//src/material/checkbox",
"//src/material/form-field",
"//src/material/icon",
"//src/material/input",
diff --git a/src/dev-app/select/select-demo.html b/src/dev-app/select/select-demo.html
index d2b63249dcca..d5639d040139 100644
--- a/src/dev-app/select/select-demo.html
+++ b/src/dev-app/select/select-demo.html
@@ -12,8 +12,8 @@
Drink
- None
-
+ None
+
{{ drink.viewValue }}
@@ -52,6 +52,7 @@
+
@@ -64,7 +65,7 @@
Pokemon
-
+
{{ creature.viewValue }}
@@ -82,6 +83,7 @@
+
@@ -356,3 +358,41 @@
Error message with errorStateMatcher
+
+
+ Narrow
+
+
+
+ Bread
+
+
+ {{ bread.viewValue }}
+
+
+
+
+ Meat
+
+
+ {{ meat.viewValue }}
+
+
+
+
+ Cheese
+
+
+ {{ cheese.viewValue }}
+
+
+
+
+
+ Hide Single-Selection Indicator
+
+
+
\ No newline at end of file
diff --git a/src/dev-app/select/select-demo.scss b/src/dev-app/select/select-demo.scss
index 448dc495ffa9..4c0776362eb8 100644
--- a/src/dev-app/select/select-demo.scss
+++ b/src/dev-app/select/select-demo.scss
@@ -25,3 +25,12 @@
.demo-card {
margin: 30px 0;
}
+
+.demo-narrow {
+ max-width: 450px;
+
+ .demo-narrow-sandwich {
+ display: flex;
+ gap: 16px;
+ }
+}
diff --git a/src/dev-app/select/select-demo.ts b/src/dev-app/select/select-demo.ts
index 8fe6b1a7461e..0f8fb788672e 100644
--- a/src/dev-app/select/select-demo.ts
+++ b/src/dev-app/select/select-demo.ts
@@ -16,6 +16,7 @@ import {MatCardModule} from '@angular/material/card';
import {MatIconModule} from '@angular/material/icon';
import {MatButtonModule} from '@angular/material/button';
import {MatInputModule} from '@angular/material/input';
+import {MatCheckboxModule} from '@angular/material/checkbox';
/** Error any time control is invalid */
export class MyErrorStateMatcher implements ErrorStateMatcher {
@@ -37,6 +38,7 @@ export class MyErrorStateMatcher implements ErrorStateMatcher {
FormsModule,
MatButtonModule,
MatCardModule,
+ MatCheckboxModule,
MatIconModule,
MatInputModule,
MatSelectModule,
@@ -48,7 +50,9 @@ export class SelectDemo {
drinkObjectRequired = false;
pokemonRequired = false;
drinksDisabled = false;
+ drinksOptionsDisabled = false;
pokemonDisabled = false;
+ pokemonOptionsDisabled = false;
showSelect = false;
currentDrink: string;
currentDrinkObject: {} | undefined = {value: 'tea-5', viewValue: 'Tea'};
@@ -66,6 +70,12 @@ export class SelectDemo {
compareByValue = true;
selectFormControl = new FormControl('', Validators.required);
+ sandwichBread = '';
+ sandwichMeat = '';
+ sandwichCheese = '';
+
+ sandwichHideSingleSelectionIndicator = false;
+
foods = [
{value: null, viewValue: 'None'},
{value: 'steak-0', viewValue: 'Steak'},
@@ -74,19 +84,19 @@ export class SelectDemo {
];
drinks = [
- {value: 'coke-0', viewValue: 'Coke', disabled: false},
+ {value: 'coke-0', viewValue: 'Coke'},
{
value: 'long-name-1',
viewValue: 'Decaf Chocolate Brownie Vanilla Gingerbread Frappuccino',
disabled: false,
},
- {value: 'water-2', viewValue: 'Water', disabled: false},
- {value: 'pepper-3', viewValue: 'Dr. Pepper', disabled: false},
- {value: 'coffee-4', viewValue: 'Coffee', disabled: false},
- {value: 'tea-5', viewValue: 'Tea', disabled: false},
- {value: 'juice-6', viewValue: 'Orange juice', disabled: false},
- {value: 'wine-7', viewValue: 'Wine', disabled: false},
- {value: 'milk-8', viewValue: 'Milk', disabled: true},
+ {value: 'water-2', viewValue: 'Water'},
+ {value: 'pepper-3', viewValue: 'Dr. Pepper'},
+ {value: 'coffee-4', viewValue: 'Coffee'},
+ {value: 'tea-5', viewValue: 'Tea'},
+ {value: 'juice-6', viewValue: 'Orange juice'},
+ {value: 'wine-7', viewValue: 'Wine'},
+ {value: 'milk-8', viewValue: 'Milk'},
];
pokemon = [
@@ -149,6 +159,26 @@ export class SelectDemo {
{value: 'indramon-5', viewValue: 'Indramon'},
];
+ breads = [
+ {value: 'white', viewValue: 'White'},
+ {value: 'white', viewValue: 'Wheat'},
+ {value: 'white', viewValue: 'Sourdough'},
+ ];
+
+ meats = [
+ {value: 'turkey', viewValue: 'Turkey'},
+ {value: 'bacon', viewValue: 'Bacon'},
+ {value: 'veggiePatty', viewValue: 'Veggie Patty'},
+ {value: 'tuna', viewValue: 'Tuna'},
+ ];
+
+ cheeses = [
+ {value: 'none', viewValue: 'None'},
+ {value: 'swiss', viewValue: 'Swiss'},
+ {value: 'american', viewValue: 'American'},
+ {value: 'cheddar', viewValue: 'Cheddar'},
+ ];
+
toggleDisabled() {
this.foodControl.enabled ? this.foodControl.disable() : this.foodControl.enable();
}
diff --git a/src/material/autocomplete/autocomplete.spec.ts b/src/material/autocomplete/autocomplete.spec.ts
index caba31899329..86a97786e92f 100644
--- a/src/material/autocomplete/autocomplete.spec.ts
+++ b/src/material/autocomplete/autocomplete.spec.ts
@@ -46,6 +46,7 @@ import {map, startWith} from 'rxjs/operators';
import {
getMatAutocompleteMissingPanelError,
MatAutocomplete,
+ MatAutocompleteDefaultOptions,
MatAutocompleteModule,
MatAutocompleteOrigin,
MatAutocompleteSelectedEvent,
@@ -3412,6 +3413,50 @@ describe('MDC-based MatAutocomplete', () => {
subscription.unsubscribe();
}));
+
+ describe('a11y', () => {
+ it('should display checkmark for selection by default', () => {
+ const fixture = createComponent(AutocompleteWithNgModel);
+ fixture.componentInstance.selectedState = 'New York';
+ fixture.detectChanges();
+
+ fixture.componentInstance.trigger.openPanel();
+ fixture.detectChanges();
+
+ dispatchFakeEvent(document.querySelector('mat-option')!, 'click');
+ fixture.detectChanges();
+
+ const selectedOption = document.querySelector('mat-option[aria-selected="true"');
+ expect(selectedOption).withContext('Expected an option to be selected.').not.toBeNull();
+ expect(selectedOption?.querySelector('.mat-pseudo-checkbox.mat-pseudo-checkbox-minimal'))
+ .withContext(
+ 'Expected selection option to have a pseudo-checkbox with "minimal" appearance.',
+ )
+ .toBeTruthy();
+ });
+ });
+
+ describe('with token to hide single selection indicator', () => {
+ it('should not display checkmark', () => {
+ const defaultOptions: MatAutocompleteDefaultOptions = {
+ hideSingleSelectionIndicator: true,
+ };
+ const fixture = createComponent(AutocompleteWithNgModel, [
+ {provide: MAT_AUTOCOMPLETE_DEFAULT_OPTIONS, useValue: defaultOptions},
+ ]);
+ fixture.detectChanges();
+
+ fixture.componentInstance.trigger.openPanel();
+ fixture.detectChanges();
+
+ dispatchFakeEvent(document.querySelector('mat-option')!, 'click');
+ fixture.detectChanges();
+
+ const selectedOption = document.querySelector('mat-option[aria-selected="true"');
+ expect(selectedOption).withContext('Expected an option to be selected.').not.toBeNull();
+ expect(document.querySelectorAll('.mat-pseudo-checkbox').length).toBe(0);
+ });
+ });
});
const SIMPLE_AUTOCOMPLETE_TEMPLATE = `
@@ -3576,6 +3621,8 @@ class AutocompleteWithNgModel {
selectedState: string;
states = ['New York', 'Washington', 'Oregon'];
+ @ViewChild(MatAutocompleteTrigger, {static: true}) trigger: MatAutocompleteTrigger;
+
constructor() {
this.filteredStates = this.states.slice();
}
diff --git a/src/material/autocomplete/autocomplete.ts b/src/material/autocomplete/autocomplete.ts
index 01fe3576ab02..3e28603edcde 100644
--- a/src/material/autocomplete/autocomplete.ts
+++ b/src/material/autocomplete/autocomplete.ts
@@ -81,6 +81,9 @@ export interface MatAutocompleteDefaultOptions {
/** Class or list of classes to be applied to the autocomplete's overlay panel. */
overlayPanelClass?: string | string[];
+
+ /** Wheter icon indicators should be hidden for single-selection. */
+ hideSingleSelectionIndicator?: boolean;
}
/** Injection token to be used to override the default options for `mat-autocomplete`. */
@@ -94,7 +97,11 @@ export const MAT_AUTOCOMPLETE_DEFAULT_OPTIONS = new InjectionToken
,
- @Inject(MAT_AUTOCOMPLETE_DEFAULT_OPTIONS) defaults: MatAutocompleteDefaultOptions,
+ @Inject(MAT_AUTOCOMPLETE_DEFAULT_OPTIONS) protected _defaults: MatAutocompleteDefaultOptions,
platform?: Platform,
) {
super();
@@ -242,8 +249,6 @@ export abstract class _MatAutocompleteBase
// wasn't resolved in VoiceOver, and if it has, we can remove this and the `inertGroups`
// option altogether.
this.inertGroups = platform?.SAFARI || false;
- this._autoActiveFirstOption = !!defaults.autoActiveFirstOption;
- this._autoSelectActiveOption = !!defaults.autoSelectActiveOption;
}
ngAfterContentInit() {
@@ -336,4 +341,25 @@ export class MatAutocomplete extends _MatAutocompleteBase {
@ContentChildren(MatOption, {descendants: true}) options: QueryList;
protected _visibleClass = 'mat-mdc-autocomplete-visible';
protected _hiddenClass = 'mat-mdc-autocomplete-hidden';
+
+ /** Whether checkmark indicator for single-selection options is hidden. */
+ @Input()
+ get hideSingleSelectionIndicator(): boolean {
+ return this._hideSingleSelectionIndicator;
+ }
+ set hideSingleSelectionIndicator(value: BooleanInput) {
+ this._hideSingleSelectionIndicator = coerceBooleanProperty(value);
+ this._syncParentProperties();
+ }
+ private _hideSingleSelectionIndicator: boolean =
+ this._defaults.hideSingleSelectionIndicator ?? false;
+
+ /** Syncs the parent state with the individual options. */
+ _syncParentProperties(): void {
+ if (this.options) {
+ for (const option of this.options) {
+ option._changeDetectorRef.markForCheck();
+ }
+ }
+ }
}
diff --git a/src/material/core/option/option-parent.ts b/src/material/core/option/option-parent.ts
index 3cb515aedd97..bd617add9817 100644
--- a/src/material/core/option/option-parent.ts
+++ b/src/material/core/option/option-parent.ts
@@ -17,6 +17,7 @@ export interface MatOptionParentComponent {
disableRipple?: boolean;
multiple?: boolean;
inertGroups?: boolean;
+ hideSingleSelectionIndicator?: boolean;
}
/**
diff --git a/src/material/core/option/option.html b/src/material/core/option/option.html
index d29859b31e12..e6e94c70c81f 100644
--- a/src/material/core/option/option.html
+++ b/src/material/core/option/option.html
@@ -5,6 +5,11 @@
+
+
+
({{ group.label }})
diff --git a/src/material/core/option/option.scss b/src/material/core/option/option.scss
index 1657e73b312a..a5f4a7a3ed19 100644
--- a/src/material/core/option/option.scss
+++ b/src/material/core/option/option.scss
@@ -48,7 +48,7 @@
}
.mat-icon,
- .mat-pseudo-checkbox {
+ .mat-pseudo-checkbox-full {
margin-right: mdc-list-variables.$side-padding;
flex-shrink: 0;
@@ -58,6 +58,16 @@
}
}
+ .mat-pseudo-checkbox-minimal {
+ margin-left: mdc-list-variables.$side-padding;
+ flex-shrink: 0;
+
+ [dir='rtl'] & {
+ margin-right: mdc-list-variables.$side-padding;
+ margin-left: 0;
+ }
+ }
+
// Increase specificity because ripple styles are part of the `mat-core` mixin and can
// potentially overwrite the absolute position of the container.
.mat-mdc-option-ripple {
@@ -84,6 +94,13 @@
font-family: inherit;
text-decoration: inherit;
text-transform: inherit;
+
+ margin-right: auto;
+
+ [dir='rtl'] & {
+ margin-right: 0;
+ margin-left: auto;
+ }
}
@include cdk.high-contrast(active, off) {
diff --git a/src/material/core/option/option.ts b/src/material/core/option/option.ts
index 97e386d9599d..2f427ff9b256 100644
--- a/src/material/core/option/option.ts
+++ b/src/material/core/option/option.ts
@@ -83,6 +83,11 @@ export class _MatOptionBase implements FocusableOption, AfterViewChecke
return !!(this._parent && this._parent.disableRipple);
}
+ /** Whether to display checkmark for single-selection. */
+ get hideSingleSelectionIndicator(): boolean {
+ return !!(this._parent && this._parent.hideSingleSelectionIndicator);
+ }
+
/** Event emitted when the option is selected or deselected. */
// tslint:disable-next-line:no-output-on-prefix
@Output() readonly onSelectionChange = new EventEmitter>();
@@ -95,7 +100,7 @@ export class _MatOptionBase implements FocusableOption, AfterViewChecke
constructor(
private _element: ElementRef,
- private _changeDetectorRef: ChangeDetectorRef,
+ public _changeDetectorRef: ChangeDetectorRef,
private _parent: MatOptionParentComponent,
readonly group: _MatOptgroupBase,
) {}
diff --git a/src/material/core/selection/pseudo-checkbox/_pseudo-checkbox-common.scss b/src/material/core/selection/pseudo-checkbox/_pseudo-checkbox-common.scss
index dd93f9cfc93a..6532895a189c 100644
--- a/src/material/core/selection/pseudo-checkbox/_pseudo-checkbox-common.scss
+++ b/src/material/core/selection/pseudo-checkbox/_pseudo-checkbox-common.scss
@@ -4,26 +4,79 @@
// Padding inside of a pseudo checkbox.
$padding: checkbox-common.$border-width * 2;
+// Center a checkmark indicator inside the checkbox.
+//
+// `$box-size`: size of the checkbox
+// `$mark-size`: size of the checkmark indicator
+@mixin _checkbox-checked-styles-with-size($box-size, $mark-size) {
+ // Center a checkmark. `$checkbox-cmmon.$border-width` is the width of the line of the checkmark.
+ $width: $mark-size;
+ $height: math.div($mark-size - checkbox-common.$border-width, 2);
+
+ // The rendered length of the short-side of the checkmark graphic when rendered. Add length of
+ // the border-width since this element is content-box.
+ $short-side: $height + checkbox-common.$border-width;
+
+ width: $width;
+ height: $height;
+
+ // Rotate on the center of the element. This makes it easier to center the checkmark graphic.
+ transform-origin: center;
+
+ // Take negative one times the distance from the top corner of the checkmark graphic to the top
+ // of the element in its rotated position. This accounts for the top corner of the elemant being
+ // blank since we only use the left and bottom borders to draw the checkmark graphic.
+ top: -1 * math.div($short-side - checkbox-common.$border-width, math.sqrt(2));
+
+ left: 0;
+ bottom: 0;
+ right: 0;
+
+ // center the checkmark graphic with margin auto
+ margin: auto;
+}
+
+// Center a horizontal line placed in the vertical and horizontal center of the checkbox. It does
+// not touch the border of the checkbox.
+//
+// `$box-size`: size of the checkbox.
+// `$border-size`: size of the checkbox's border.
+@mixin _checkbox-indeterminate-styles-with-size($box-size, $border-size) {
+ // Center the line in the the checkbox. `$checkbox-common.$border-width` is the width of the line.
+ top: math.div($box-size - checkbox-common.$border-width, 2) - $border-size;
+ width: $box-size - checkbox-common.$border-width - (2 * $border-size);
+}
+
/// Applies the styles that set the size of the pseudo checkbox
@mixin size($box-size) {
- $mark-size: $box-size - (2 * $padding);
.mat-pseudo-checkbox {
width: $box-size;
height: $box-size;
}
- .mat-pseudo-checkbox-indeterminate::after {
- top: math.div($box-size - checkbox-common.$border-width, 2) -
- checkbox-common.$border-width;
- width: $box-size - 6px;
+ .mat-pseudo-checkbox-minimal {
+ $mark-size: $box-size - $padding;
+ $border-size: 0; // Minimal appearance does not have a border.
+
+ &.mat-pseudo-checkbox-checked::after {
+ @include _checkbox-checked-styles-with-size($box-size, $mark-size);
+ }
+ &.mat-pseudo-checkbox-indeterminate::after {
+ @include _checkbox-indeterminate-styles-with-size($box-size, $border-size);
+ }
}
- .mat-pseudo-checkbox-checked::after {
- top: math.div($box-size, 2) - math.div($mark-size, 4) -
- math.div($box-size, 10) - checkbox-common.$border-width;
- width: $mark-size;
- height: math.div($mark-size - checkbox-common.$border-width, 2);
+ .mat-pseudo-checkbox-full {
+ $mark-size: $box-size - (2 * $padding); // Apply a smaller mark to account for the border.
+ $border-size: checkbox-common.$border-width;
+
+ &.mat-pseudo-checkbox-checked::after {
+ @include _checkbox-checked-styles-with-size($box-size, $mark-size);
+ }
+ &.mat-pseudo-checkbox-indeterminate::after {
+ @include _checkbox-indeterminate-styles-with-size($box-size, $border-size);
+ }
}
}
diff --git a/src/material/core/selection/pseudo-checkbox/_pseudo-checkbox-theme.scss b/src/material/core/selection/pseudo-checkbox/_pseudo-checkbox-theme.scss
index a83fa1410b6d..e0e9d624b338 100644
--- a/src/material/core/selection/pseudo-checkbox/_pseudo-checkbox-theme.scss
+++ b/src/material/core/selection/pseudo-checkbox/_pseudo-checkbox-theme.scss
@@ -1,13 +1,33 @@
@use 'sass:map';
@use '../../theming/theming';
+@mixin _psuedo-checkbox-styles-with-color($text-color, $background) {
+ .mat-pseudo-checkbox-checked,
+ .mat-pseudo-checkbox-indeterminate {
+ &.mat-pseudo-checkbox-minimal::after {
+ color: $text-color;
+ }
+
+ // Full (checkbox) appearance inverts colors of text and background.
+ &.mat-pseudo-checkbox-full {
+ &::after {
+ color: $background;
+ }
+
+ background: $text-color;
+ }
+ }
+}
+
@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);
- $background: map.get($config, background);
+
+ $primary: theming.get-color-from-palette(map.get($config, primary));
+ $accent: theming.get-color-from-palette(map.get($config, accent));
+ $warn: theming.get-color-from-palette(map.get($config, warn));
+ $background: theming.get-color-from-palette(map.get($config, background), background);
+ $secondary-text: theming.get-color-from-palette(map.get($config, foreground), secondary-text);
// NOTE(traviskaufman): While the spec calls for translucent blacks/whites for disabled colors,
// this does not work well with elements layered on top of one another. To get around this we
@@ -17,21 +37,15 @@
$disabled-color: if($is-dark-theme, $white-30pct-opacity-on-dark, $black-26pct-opacity-on-light);
$colored-box-selector: '.mat-pseudo-checkbox-checked, .mat-pseudo-checkbox-indeterminate';
- .mat-pseudo-checkbox {
- color: theming.get-color-from-palette(map.get($config, foreground), secondary-text);
-
- &::after {
- color: theming.get-color-from-palette($background, background);
+ .mat-pseudo-checkbox-full {
+ color: $secondary-text;
+ &.mat-pseudo-checkbox-disabled {
+ color: $disabled-color;
}
}
- .mat-pseudo-checkbox-disabled {
- color: $disabled-color;
- }
-
- .mat-primary .mat-pseudo-checkbox-checked,
- .mat-primary .mat-pseudo-checkbox-indeterminate {
- background: theming.get-color-from-palette(map.get($config, primary));
+ .mat-primary {
+ @include _psuedo-checkbox-styles-with-color($primary, $background);
}
// Default to the accent color. Note that the pseudo checkboxes are meant to inherit the
@@ -39,21 +53,22 @@
// don't attach to the `mat-*` classes. Also note that this needs to be below `.mat-primary`
// in order to allow for the color to be overwritten if the checkbox is inside a parent that
// has `mat-accent` and is placed inside another parent that has `mat-primary`.
- .mat-pseudo-checkbox-checked,
- .mat-pseudo-checkbox-indeterminate,
- .mat-accent .mat-pseudo-checkbox-checked,
- .mat-accent .mat-pseudo-checkbox-indeterminate {
- background: theming.get-color-from-palette(map.get($config, accent));
+ @include _psuedo-checkbox-styles-with-color($accent, $background);
+ .mat-accent {
+ @include _psuedo-checkbox-styles-with-color($accent, $background);
}
- .mat-warn .mat-pseudo-checkbox-checked,
- .mat-warn .mat-pseudo-checkbox-indeterminate {
- background: theming.get-color-from-palette(map.get($config, warn));
+ .mat-warn {
+ @include _psuedo-checkbox-styles-with-color($warn, $background);
}
- .mat-pseudo-checkbox-checked,
- .mat-pseudo-checkbox-indeterminate {
- &.mat-pseudo-checkbox-disabled {
+ .mat-pseudo-checkbox-disabled.mat-pseudo-checkbox-checked,
+ .mat-pseudo-checkbox-disabled.mat-pseudo-checkbox-indeterminate {
+ &.mat-pseudo-checkbox-minimal::after {
+ color: $disabled-color;
+ }
+
+ &.mat-pseudo-checkbox-full {
background: $disabled-color;
}
}
diff --git a/src/material/core/selection/pseudo-checkbox/pseudo-checkbox.scss b/src/material/core/selection/pseudo-checkbox/pseudo-checkbox.scss
index dccd8931124c..a1544d57239b 100644
--- a/src/material/core/selection/pseudo-checkbox/pseudo-checkbox.scss
+++ b/src/material/core/selection/pseudo-checkbox/pseudo-checkbox.scss
@@ -5,7 +5,6 @@
@use './pseudo-checkbox-common';
.mat-pseudo-checkbox {
- border: checkbox-common.$border-width solid;
border-radius: 2px;
cursor: pointer;
display: inline-block;
@@ -27,10 +26,6 @@
variables.$linear-out-slow-in-timing-function;
}
- &.mat-pseudo-checkbox-checked, &.mat-pseudo-checkbox-indeterminate {
- border-color: transparent;
- }
-
@include private.private-animation-noop {
&::after {
transition: none;
@@ -56,4 +51,11 @@
box-sizing: content-box;
}
+.mat-pseudo-checkbox-full {
+ border: checkbox-common.$border-width solid;
+ &.mat-pseudo-checkbox-checked, &.mat-pseudo-checkbox-indeterminate {
+ border-color: transparent;
+ }
+}
+
@include pseudo-checkbox-common.size(checkbox-common.$size);
diff --git a/src/material/core/selection/pseudo-checkbox/pseudo-checkbox.ts b/src/material/core/selection/pseudo-checkbox/pseudo-checkbox.ts
index 1ce539c7ddd6..4c3252b71d94 100644
--- a/src/material/core/selection/pseudo-checkbox/pseudo-checkbox.ts
+++ b/src/material/core/selection/pseudo-checkbox/pseudo-checkbox.ts
@@ -46,6 +46,8 @@ export type MatPseudoCheckboxState = 'unchecked' | 'checked' | 'indeterminate';
'[class.mat-pseudo-checkbox-indeterminate]': 'state === "indeterminate"',
'[class.mat-pseudo-checkbox-checked]': 'state === "checked"',
'[class.mat-pseudo-checkbox-disabled]': 'disabled',
+ '[class.mat-pseudo-checkbox-minimal]': 'appearance === "minimal"',
+ '[class.mat-pseudo-checkbox-full]': 'appearance === "full"',
'[class._mat-animation-noopable]': '_animationMode === "NoopAnimations"',
},
})
@@ -56,5 +58,11 @@ export class MatPseudoCheckbox {
/** Whether the checkbox is disabled. */
@Input() disabled: boolean = false;
+ /**
+ * Appearance of the pseudo checkbox. Default appearance of 'full' renders a checkmark/mixedmark
+ * indicator inside a square box. 'minimal' appearance only renders the checkmark/mixedmark.
+ */
+ @Input() appearance: 'minimal' | 'full' = 'full';
+
constructor(@Optional() @Inject(ANIMATION_MODULE_TYPE) public _animationMode?: string) {}
}
diff --git a/src/material/select/select.spec.ts b/src/material/select/select.spec.ts
index 7f3886c56609..3a541e180ffa 100644
--- a/src/material/select/select.spec.ts
+++ b/src/material/select/select.spec.ts
@@ -1321,6 +1321,56 @@ describe('MDC-based MatSelect', () => {
)
.toEqual([options[7]]);
}));
+
+ it('should render a checkmark on selected option', fakeAsync(() => {
+ fixture.componentInstance.control.setValue(fixture.componentInstance.foods[2].value);
+ fixture.detectChanges();
+
+ trigger.click();
+ fixture.detectChanges();
+ flush();
+
+ const pseudoCheckboxes = options
+ .map(option => option.querySelector('.mat-pseudo-checkbox-minimal'))
+ .filter((x): x is HTMLElement => !!x);
+ const selectedOption = options[2];
+
+ expect(selectedOption.querySelector('.mat-pseudo-checkbox-minimal')).not.toBeNull();
+ expect(pseudoCheckboxes.length).toBe(1);
+ }));
+
+ it('should render checkboxes for multi-select', fakeAsync(() => {
+ fixture.destroy();
+
+ const multiFixture = TestBed.createComponent(MultiSelect);
+ multiFixture.detectChanges();
+
+ multiFixture.componentInstance.control.setValue([
+ multiFixture.componentInstance.foods[2].value,
+ ]);
+ multiFixture.detectChanges();
+
+ trigger = multiFixture.debugElement.query(
+ By.css('.mat-mdc-select-trigger'),
+ )!.nativeElement;
+
+ trigger.click();
+ multiFixture.detectChanges();
+ flush();
+
+ options = Array.from(overlayContainerElement.querySelectorAll('mat-option'));
+ const pseudoCheckboxes = options
+ .map(option => option.querySelector('.mat-pseudo-checkbox.mat-pseudo-checkbox-full'))
+ .filter((x): x is HTMLElement => !!x);
+ const selectedPseudoCheckbox = pseudoCheckboxes[2];
+
+ expect(pseudoCheckboxes.length)
+ .withContext('expecting each option to have a pseudo-checkbox with "full" appearance')
+ .toEqual(options.length);
+ expect(selectedPseudoCheckbox.classList)
+ .withContext('expecting selected pseudo-checkbox to be checked')
+ .toContain('mat-pseudo-checkbox-checked');
+ }));
});
describe('for option groups', () => {
@@ -4377,6 +4427,39 @@ describe('MDC-based MatSelect', () => {
expect(document.querySelector('.cdk-overlay-pane')?.classList).toContain('test-panel-class');
}));
+ it('should be able to hide checkmark icon through an injection token', () => {
+ const matSelectConfig: MatSelectConfig = {hideSingleSelectionIndicator: true};
+ configureMatSelectTestingModule(
+ [NgModelSelect],
+ [
+ {
+ provide: MAT_SELECT_CONFIG,
+ useValue: matSelectConfig,
+ },
+ ],
+ );
+ const fixture = TestBed.createComponent(NgModelSelect);
+ fixture.detectChanges();
+ const select = fixture.componentInstance.select;
+
+ fixture.componentInstance.select.value = fixture.componentInstance.foods[0].value;
+ select.open();
+ fixture.detectChanges();
+
+ // Select the first value to ensure selection state is displayed. That way this test ensures
+ // that the selection state hides the checkmark icon, rather than hiding the checkmark icon
+ // because nothing is selected.
+ expect(document.querySelector('mat-option[aria-selected="true"]'))
+ .withContext('expecting selection state to be displayed')
+ .not.toBeNull();
+
+ const pseudoCheckboxes = document.querySelectorAll('.mat-pseudo-checkbox');
+
+ expect(pseudoCheckboxes.length)
+ .withContext('expecting not to display a pseudo-checkbox')
+ .toBe(0);
+ });
+
it('should not not throw if the select is inside an ng-container with ngIf', fakeAsync(() => {
configureMatSelectTestingModule([SelectInNgContainer]);
const fixture = TestBed.createComponent(SelectInNgContainer);
diff --git a/src/material/select/select.ts b/src/material/select/select.ts
index 95c9a5159dcf..b3b77e64b6fd 100644
--- a/src/material/select/select.ts
+++ b/src/material/select/select.ts
@@ -130,6 +130,9 @@ export interface MatSelectConfig {
/** Class or list of classes to be applied to the menu's overlay panel. */
overlayPanelClass?: string | string[];
+
+ /** Wheter icon indicators should be hidden for single-selection. */
+ hideSingleSelectionIndicator?: boolean;
}
/** Injection token that can be used to provide the default options the select module. */
@@ -483,7 +486,7 @@ export abstract class _MatSelectBase
@Attribute('tabindex') tabIndex: string,
@Inject(MAT_SELECT_SCROLL_STRATEGY) scrollStrategyFactory: any,
private _liveAnnouncer: LiveAnnouncer,
- @Optional() @Inject(MAT_SELECT_CONFIG) private _defaultOptions?: MatSelectConfig,
+ @Optional() @Inject(MAT_SELECT_CONFIG) protected _defaultOptions?: MatSelectConfig,
) {
super(elementRef, _defaultErrorStateMatcher, _parentForm, _parentFormGroup, ngControl);
@@ -1291,4 +1294,25 @@ export class MatSelect extends _MatSelectBase implements OnInit
: this._preferredOverlayOrigin || this._elementRef;
return refToMeasure.nativeElement.getBoundingClientRect().width;
}
+
+ /** Whether checkmark indicator for single-selection options is hidden. */
+ @Input()
+ get hideSingleSelectionIndicator(): boolean {
+ return this._hideSingleSelectionIndicator;
+ }
+ set hideSingleSelectionIndicator(value: BooleanInput) {
+ this._hideSingleSelectionIndicator = coerceBooleanProperty(value);
+ this._syncParentProperties();
+ }
+ private _hideSingleSelectionIndicator: boolean =
+ this._defaultOptions?.hideSingleSelectionIndicator ?? false;
+
+ /** Syncs the parent state with the individual options. */
+ _syncParentProperties(): void {
+ if (this.options) {
+ for (const option of this.options) {
+ option._changeDetectorRef.markForCheck();
+ }
+ }
+ }
}
diff --git a/tools/public_api_guard/material/autocomplete.md b/tools/public_api_guard/material/autocomplete.md
index 8ac93204e103..615c21e501ba 100644
--- a/tools/public_api_guard/material/autocomplete.md
+++ b/tools/public_api_guard/material/autocomplete.md
@@ -71,12 +71,15 @@ export const MAT_AUTOCOMPLETE_VALUE_ACCESSOR: any;
export class MatAutocomplete extends _MatAutocompleteBase {
// (undocumented)
protected _hiddenClass: string;
+ get hideSingleSelectionIndicator(): boolean;
+ set hideSingleSelectionIndicator(value: BooleanInput);
optionGroups: QueryList;
options: QueryList;
+ _syncParentProperties(): void;
// (undocumented)
protected _visibleClass: string;
// (undocumented)
- static ɵcmp: i0.ɵɵComponentDeclaration;
+ static ɵcmp: i0.ɵɵComponentDeclaration;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration;
}
@@ -89,7 +92,7 @@ export interface MatAutocompleteActivatedEvent {
// @public
export abstract class _MatAutocompleteBase extends _MatAutocompleteMixinBase implements AfterContentInit, CanDisableRipple, OnDestroy {
- constructor(_changeDetectorRef: ChangeDetectorRef, _elementRef: ElementRef, defaults: MatAutocompleteDefaultOptions, platform?: Platform);
+ constructor(_changeDetectorRef: ChangeDetectorRef, _elementRef: ElementRef, _defaults: MatAutocompleteDefaultOptions, platform?: Platform);
ariaLabel: string;
ariaLabelledby: string;
get autoActiveFirstOption(): boolean;
@@ -102,6 +105,8 @@ export abstract class _MatAutocompleteBase extends _MatAutocompleteMixinBase imp
[key: string]: boolean;
};
readonly closed: EventEmitter;
+ // (undocumented)
+ protected _defaults: MatAutocompleteDefaultOptions;
displayWith: ((value: any) => string) | null;
_emitSelectEvent(option: _MatOptionBase): void;
_getPanelAriaLabelledby(labelId: string | null): string | null;
@@ -140,6 +145,7 @@ export abstract class _MatAutocompleteBase extends _MatAutocompleteMixinBase imp
export interface MatAutocompleteDefaultOptions {
autoActiveFirstOption?: boolean;
autoSelectActiveOption?: boolean;
+ hideSingleSelectionIndicator?: boolean;
overlayPanelClass?: string | string[];
}
diff --git a/tools/public_api_guard/material/core.md b/tools/public_api_guard/material/core.md
index 4c68e948abf6..d636b62761f5 100644
--- a/tools/public_api_guard/material/core.md
+++ b/tools/public_api_guard/material/core.md
@@ -272,6 +272,8 @@ export class MatOption extends _MatOptionBase {
export class _MatOptionBase implements FocusableOption, AfterViewChecked, OnDestroy {
constructor(_element: ElementRef, _changeDetectorRef: ChangeDetectorRef, _parent: MatOptionParentComponent, group: _MatOptgroupBase);
get active(): boolean;
+ // (undocumented)
+ _changeDetectorRef: ChangeDetectorRef;
deselect(): void;
get disabled(): boolean;
set disabled(value: BooleanInput);
@@ -284,6 +286,7 @@ export class _MatOptionBase implements FocusableOption, AfterViewChecke
// (undocumented)
readonly group: _MatOptgroupBase;
_handleKeydown(event: KeyboardEvent): void;
+ get hideSingleSelectionIndicator(): boolean;
id: string;
get multiple(): boolean | undefined;
// (undocumented)
@@ -321,6 +324,8 @@ export interface MatOptionParentComponent {
// (undocumented)
disableRipple?: boolean;
// (undocumented)
+ hideSingleSelectionIndicator?: boolean;
+ // (undocumented)
inertGroups?: boolean;
// (undocumented)
multiple?: boolean;
@@ -340,10 +345,11 @@ export class MatPseudoCheckbox {
constructor(_animationMode?: string | undefined);
// (undocumented)
_animationMode?: string | undefined;
+ appearance: 'minimal' | 'full';
disabled: boolean;
state: MatPseudoCheckboxState;
// (undocumented)
- static ɵcmp: i0.ɵɵComponentDeclaration;
+ static ɵcmp: i0.ɵɵComponentDeclaration;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration;
}
diff --git a/tools/public_api_guard/material/select.md b/tools/public_api_guard/material/select.md
index d34dfdf10020..3e1523a0e664 100644
--- a/tools/public_api_guard/material/select.md
+++ b/tools/public_api_guard/material/select.md
@@ -83,6 +83,8 @@ export class MatSelect extends _MatSelectBase implements OnInit
customTrigger: MatSelectTrigger;
// (undocumented)
protected _getChangeEvent(value: any): MatSelectChange;
+ get hideSingleSelectionIndicator(): boolean;
+ set hideSingleSelectionIndicator(value: BooleanInput);
// (undocumented)
ngAfterViewInit(): void;
// (undocumented)
@@ -102,8 +104,9 @@ export class MatSelect extends _MatSelectBase implements OnInit
protected _scrollOptionIntoView(index: number): void;
// (undocumented)
get shouldLabelFloat(): boolean;
+ _syncParentProperties(): void;
// (undocumented)
- static ɵcmp: i0.ɵɵComponentDeclaration;
+ static ɵcmp: i0.ɵɵComponentDeclaration;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration;
}
@@ -128,6 +131,8 @@ export abstract class _MatSelectBase extends _MatSelectMixinBase implements A
set compareWith(fn: (o1: any, o2: any) => boolean);
controlType: string;
abstract customTrigger: {};
+ // (undocumented)
+ protected _defaultOptions?: MatSelectConfig | undefined;
protected readonly _destroy: Subject;
get disableOptionCentering(): boolean;
set disableOptionCentering(value: BooleanInput);
@@ -231,6 +236,7 @@ export class MatSelectChange {
// @public
export interface MatSelectConfig {
disableOptionCentering?: boolean;
+ hideSingleSelectionIndicator?: boolean;
overlayPanelClass?: string | string[];
typeaheadDebounceInterval?: number;
}