Skip to content

Commit 09111d0

Browse files
authored
fix(material/button-toggle): Add checkmark indicators with hideSingleSelectionIndicator and hideMultipleSelectionIndicator input and config options (#28553)
* fix(material/button-toggle): Add checkmark indicators with hideSingleSelectionIndicator and hideMultipleSelectionIndicator input and config options * Make checkmark color match text color
1 parent 8abb33d commit 09111d0

File tree

7 files changed

+179
-9
lines changed

7 files changed

+179
-9
lines changed

src/dev-app/button-toggle/button-toggle-demo.html

+14-6
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,18 @@
66
<mat-checkbox (change)="isDisabled = $event.checked">Disable Button Toggle Items</mat-checkbox>
77
</p>
88

9+
<p>
10+
<mat-checkbox (change)="hideSingleSelectionIndicator = $event.checked">Hide Single Selection Indicator</mat-checkbox>
11+
</p>
12+
13+
<p>
14+
<mat-checkbox (change)="hideMultipleSelectionIndicator = $event.checked">Hide Multiple Selection Indicator</mat-checkbox>
15+
</p>
16+
917
<h1>Exclusive Selection</h1>
1018

1119
<section>
12-
<mat-button-toggle-group name="alignment" [vertical]="isVertical">
20+
<mat-button-toggle-group name="alignment" [vertical]="isVertical" [hideSingleSelectionIndicator]="hideSingleSelectionIndicator">
1321
<mat-button-toggle value="left" [disabled]="isDisabled">
1422
<mat-icon>format_align_left</mat-icon>
1523
</mat-button-toggle>
@@ -26,7 +34,7 @@ <h1>Exclusive Selection</h1>
2634
</section>
2735

2836
<section>
29-
<mat-button-toggle-group appearance="legacy" name="alignment" [vertical]="isVertical">
37+
<mat-button-toggle-group appearance="legacy" name="alignment" [vertical]="isVertical" [hideSingleSelectionIndicator]="hideSingleSelectionIndicator">
3038
<mat-button-toggle value="left" [disabled]="isDisabled">
3139
<mat-icon>format_align_left</mat-icon>
3240
</mat-button-toggle>
@@ -45,7 +53,7 @@ <h1>Exclusive Selection</h1>
4553
<h1>Disabled Group</h1>
4654

4755
<section>
48-
<mat-button-toggle-group name="checkbox" [vertical]="isVertical" [disabled]="isDisabled">
56+
<mat-button-toggle-group name="checkbox" [vertical]="isVertical" [disabled]="isDisabled" [hideSingleSelectionIndicator]="hideSingleSelectionIndicator">
4957
<mat-button-toggle value="bold">
5058
<mat-icon>format_bold</mat-icon>
5159
</mat-button-toggle>
@@ -60,15 +68,15 @@ <h1>Disabled Group</h1>
6068

6169
<h1>Multiple Selection</h1>
6270
<section>
63-
<mat-button-toggle-group multiple [vertical]="isVertical">
71+
<mat-button-toggle-group multiple [vertical]="isVertical" [hideMultipleSelectionIndicator]="hideMultipleSelectionIndicator">
6472
<mat-button-toggle>Flour</mat-button-toggle>
6573
<mat-button-toggle>Eggs</mat-button-toggle>
6674
<mat-button-toggle>Sugar</mat-button-toggle>
6775
<mat-button-toggle [disabled]="isDisabled">Milk</mat-button-toggle>
6876
</mat-button-toggle-group>
6977
</section>
7078
<section>
71-
<mat-button-toggle-group appearance="legacy" multiple [vertical]="isVertical">
79+
<mat-button-toggle-group appearance="legacy" multiple [vertical]="isVertical" [hideMultipleSelectionIndicator]="hideMultipleSelectionIndicator">
7280
<mat-button-toggle>Flour</mat-button-toggle>
7381
<mat-button-toggle>Eggs</mat-button-toggle>
7482
<mat-button-toggle>Sugar</mat-button-toggle>
@@ -82,7 +90,7 @@ <h1>Single Toggle</h1>
8290

8391
<h1>Dynamic Exclusive Selection</h1>
8492
<section>
85-
<mat-button-toggle-group name="pies" [(ngModel)]="favoritePie" [vertical]="isVertical">
93+
<mat-button-toggle-group name="pies" [(ngModel)]="favoritePie" [vertical]="isVertical" [hideSingleSelectionIndicator]="hideSingleSelectionIndicator">
8694
@for (pie of pieOptions; track pie) {
8795
<mat-button-toggle [value]="pie">{{pie}}</mat-button-toggle>
8896
}

src/dev-app/button-toggle/button-toggle-demo.ts

+2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import {MatIconModule} from '@angular/material/icon';
2323
export class ButtonToggleDemo {
2424
isVertical = false;
2525
isDisabled = false;
26+
hideSingleSelectionIndicator = false;
27+
hideMultipleSelectionIndicator = false;
2628
favoritePie = 'Apple';
2729
pieOptions = ['Apple', 'Cherry', 'Pecan', 'Lemon'];
2830
}

src/material/button-toggle/button-toggle.html

+18
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,24 @@
99
[attr.aria-labelledby]="ariaLabelledby"
1010
(click)="_onButtonClick()">
1111
<span class="mat-button-toggle-label-content">
12+
<!-- Render checkmark at the beginning for single-selection. -->
13+
@if (buttonToggleGroup && checked && !buttonToggleGroup.multiple && !buttonToggleGroup.hideSingleSelectionIndicator) {
14+
<mat-pseudo-checkbox
15+
class="mat-mdc-option-pseudo-checkbox"
16+
[disabled]="disabled"
17+
state="checked"
18+
aria-hidden="true"
19+
appearance="minimal"></mat-pseudo-checkbox>
20+
}
21+
<!-- Render checkmark at the beginning for multiple-selection. -->
22+
@if (buttonToggleGroup && checked && buttonToggleGroup.multiple && !buttonToggleGroup.hideMultipleSelectionIndicator) {
23+
<mat-pseudo-checkbox
24+
class="mat-mdc-option-pseudo-checkbox"
25+
[disabled]="disabled"
26+
state="checked"
27+
aria-hidden="true"
28+
appearance="minimal"></mat-pseudo-checkbox>
29+
}
1230
<ng-content></ng-content>
1331
</span>
1432
</button>

src/material/button-toggle/button-toggle.scss

+28
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
$standard-padding: 0 12px !default;
1111
$legacy-padding: 0 16px !default;
12+
$checkmark-padding: 12px !default;
1213

1314
// TODO(crisbeto): these variables aren't used anymore and should be removed.
1415
$legacy-height: 36px !default;
@@ -52,6 +53,12 @@ $_standard-tokens: (
5253
@include token-utils.use-tokens($_standard-tokens...) {
5354
@include token-utils.create-token-slot(border-radius, shape);
5455
border: solid 1px var(#{token-utils.get-token-variable(divider-color)});
56+
57+
.mat-pseudo-checkbox {
58+
--mat-minimal-pseudo-checkbox-selected-checkmark-color: var(
59+
#{token-utils.get-token-variable(selected-state-text-color)}
60+
);
61+
}
5562
}
5663

5764
&:not([class*='mat-elevation-z']) {
@@ -85,6 +92,10 @@ $_standard-tokens: (
8592
@include token-utils.create-token-slot(font-weight, label-text-weight);
8693
@include token-utils.create-token-slot(letter-spacing, label-text-tracking);
8794

95+
--mat-minimal-pseudo-checkbox-selected-checkmark-color: var(
96+
#{token-utils.get-token-variable(selected-state-text-color)}
97+
);
98+
8899
&.cdk-keyboard-focused .mat-button-toggle-focus-overlay {
89100
@include token-utils.create-token-slot(opacity, focus-state-layer-opacity);
90101
}
@@ -94,6 +105,14 @@ $_standard-tokens: (
94105
.mat-icon svg {
95106
vertical-align: top;
96107
}
108+
109+
.mat-pseudo-checkbox {
110+
margin-right: $checkmark-padding;
111+
[dir='rtl'] & {
112+
margin-right: 0;
113+
margin-left: $checkmark-padding;
114+
}
115+
}
97116
}
98117

99118
.mat-button-toggle-checked {
@@ -107,6 +126,9 @@ $_standard-tokens: (
107126
@include token-utils.use-tokens($_legacy-tokens...) {
108127
@include token-utils.create-token-slot(color, disabled-state-text-color);
109128
@include token-utils.create-token-slot(background-color, disabled-state-background-color);
129+
--mat-minimal-pseudo-checkbox-disabled-selected-checkmark-color: var(
130+
#{token-utils.get-token-variable(disabled-state-text-color)}
131+
);
110132

111133
&.mat-button-toggle-checked {
112134
@include token-utils.create-token-slot(background-color,
@@ -150,6 +172,12 @@ $_standard-tokens: (
150172
@include token-utils.create-token-slot(color, disabled-state-text-color);
151173
@include token-utils.create-token-slot(background-color, disabled-state-background-color);
152174

175+
.mat-pseudo-checkbox {
176+
--mat-minimal-pseudo-checkbox-disabled-selected-checkmark-color: var(
177+
#{token-utils.get-token-variable(disabled-selected-state-text-color)}
178+
);
179+
}
180+
153181
&.mat-button-toggle-checked {
154182
@include token-utils.create-token-slot(color, disabled-selected-state-text-color);
155183
@include token-utils.create-token-slot(background-color,

src/material/button-toggle/button-toggle.spec.ts

+62
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {ComponentFixture, fakeAsync, flush, TestBed, tick} from '@angular/core/t
55
import {FormControl, FormsModule, NgModel, ReactiveFormsModule} from '@angular/forms';
66
import {By} from '@angular/platform-browser';
77
import {
8+
MAT_BUTTON_TOGGLE_DEFAULT_OPTIONS,
89
MatButtonToggle,
910
MatButtonToggleChange,
1011
MatButtonToggleGroup,
@@ -546,6 +547,13 @@ describe('MatButtonToggle without forms', () => {
546547
expect(groupInstance.value).toBeFalsy();
547548
expect(groupInstance.selected).toBeFalsy();
548549
}));
550+
551+
it('should show checkmark indicator by default', () => {
552+
buttonToggleLabelElements[0].click();
553+
fixture.detectChanges();
554+
555+
expect(document.querySelectorAll('.mat-pseudo-checkbox').length).toBe(1);
556+
});
549557
});
550558

551559
describe('with initial value and change event', () => {
@@ -701,6 +709,14 @@ describe('MatButtonToggle without forms', () => {
701709
groupInstance.value = 'not-an-array';
702710
}).toThrowError(/Value must be an array/);
703711
});
712+
713+
it('should show checkmark indicator by default', () => {
714+
buttonToggleLabelElements[0].click();
715+
buttonToggleLabelElements[1].click();
716+
fixture.detectChanges();
717+
718+
expect(document.querySelectorAll('.mat-pseudo-checkbox').length).toBe(2);
719+
});
704720
});
705721

706722
describe('as standalone', () => {
@@ -876,6 +892,52 @@ describe('MatButtonToggle without forms', () => {
876892
});
877893
});
878894

895+
describe('with tokens to hide checkmark selection indicators', () => {
896+
beforeEach(() => {
897+
TestBed.configureTestingModule({
898+
imports: [
899+
MatButtonToggleModule,
900+
ButtonTogglesInsideButtonToggleGroup,
901+
ButtonTogglesInsideButtonToggleGroupMultiple,
902+
],
903+
providers: [
904+
{
905+
provide: MAT_BUTTON_TOGGLE_DEFAULT_OPTIONS,
906+
useValue: {
907+
hideSingleSelectionIndicator: true,
908+
hideMultipleSelectionIndicator: true,
909+
},
910+
},
911+
],
912+
});
913+
914+
TestBed.compileComponents();
915+
});
916+
917+
it('should hide checkmark indicator for single selection', () => {
918+
const fixture = TestBed.createComponent(ButtonTogglesInsideButtonToggleGroup);
919+
fixture.detectChanges();
920+
921+
fixture.debugElement.query(By.css('button')).nativeElement.click();
922+
fixture.detectChanges();
923+
924+
expect(document.querySelectorAll('.mat-pseudo-checkbox').length).toBe(0);
925+
});
926+
927+
it('should hide checkmark indicator for multiple selection', () => {
928+
const fixture = TestBed.createComponent(ButtonTogglesInsideButtonToggleGroupMultiple);
929+
fixture.detectChanges();
930+
931+
// Check all button toggles in the group
932+
fixture.debugElement
933+
.queryAll(By.css('button'))
934+
.forEach(toggleButton => toggleButton.nativeElement.click());
935+
fixture.detectChanges();
936+
937+
expect(document.querySelectorAll('.mat-pseudo-checkbox').length).toBe(0);
938+
});
939+
});
940+
879941
it('should not throw on init when toggles are repeated and there is an initial value', () => {
880942
const fixture = TestBed.createComponent(RepeatedButtonTogglesWithPreselectedValue);
881943

src/material/button-toggle/button-toggle.ts

+41-2
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import {
3333
booleanAttribute,
3434
} from '@angular/core';
3535
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
36-
import {MatRipple} from '@angular/material/core';
36+
import {MatRipple, MatPseudoCheckbox} from '@angular/material/core';
3737

3838
/**
3939
* @deprecated No longer used.
@@ -54,6 +54,10 @@ export interface MatButtonToggleDefaultOptions {
5454
* setting an appearance on a button toggle or group.
5555
*/
5656
appearance?: MatButtonToggleAppearance;
57+
/** Whetehr icon indicators should be hidden for single-selection button toggle groups. */
58+
hideSingleSelectionIndicator?: boolean;
59+
/** Whether icon indicators should be hidden for multiple-selection button toggle groups. */
60+
hideMultipleSelectionIndicator?: boolean;
5761
}
5862

5963
/**
@@ -62,8 +66,19 @@ export interface MatButtonToggleDefaultOptions {
6266
*/
6367
export const MAT_BUTTON_TOGGLE_DEFAULT_OPTIONS = new InjectionToken<MatButtonToggleDefaultOptions>(
6468
'MAT_BUTTON_TOGGLE_DEFAULT_OPTIONS',
69+
{
70+
providedIn: 'root',
71+
factory: MAT_BUTTON_TOGGLE_GROUP_DEFAULT_OPTIONS_FACTORY,
72+
},
6573
);
6674

75+
export function MAT_BUTTON_TOGGLE_GROUP_DEFAULT_OPTIONS_FACTORY(): MatButtonToggleDefaultOptions {
76+
return {
77+
hideSingleSelectionIndicator: false,
78+
hideMultipleSelectionIndicator: false,
79+
};
80+
}
81+
6782
/**
6883
* Injection token that can be used to reference instances of `MatButtonToggleGroup`.
6984
* It serves as alternative token to the actual `MatButtonToggleGroup` class which
@@ -215,6 +230,28 @@ export class MatButtonToggleGroup implements ControlValueAccessor, OnInit, After
215230
@Output() readonly change: EventEmitter<MatButtonToggleChange> =
216231
new EventEmitter<MatButtonToggleChange>();
217232

233+
/** Whether checkmark indicator for single-selection button toggle groups is hidden. */
234+
@Input({transform: booleanAttribute})
235+
get hideSingleSelectionIndicator(): boolean {
236+
return this._hideSingleSelectionIndicator;
237+
}
238+
set hideSingleSelectionIndicator(value: boolean) {
239+
this._hideSingleSelectionIndicator = value;
240+
this._markButtonsForCheck();
241+
}
242+
private _hideSingleSelectionIndicator: boolean;
243+
244+
/** Whether checkmark indicator for multiple-selection button toggle groups is hidden. */
245+
@Input({transform: booleanAttribute})
246+
get hideMultipleSelectionIndicator(): boolean {
247+
return this._hideMultipleSelectionIndicator;
248+
}
249+
set hideMultipleSelectionIndicator(value: boolean) {
250+
this._hideMultipleSelectionIndicator = value;
251+
this._markButtonsForCheck();
252+
}
253+
private _hideMultipleSelectionIndicator: boolean;
254+
218255
constructor(
219256
private _changeDetector: ChangeDetectorRef,
220257
@Optional()
@@ -223,6 +260,8 @@ export class MatButtonToggleGroup implements ControlValueAccessor, OnInit, After
223260
) {
224261
this.appearance =
225262
defaultOptions && defaultOptions.appearance ? defaultOptions.appearance : 'standard';
263+
this.hideSingleSelectionIndicator = defaultOptions?.hideSingleSelectionIndicator ?? false;
264+
this.hideMultipleSelectionIndicator = defaultOptions?.hideMultipleSelectionIndicator ?? false;
226265
}
227266

228267
ngOnInit() {
@@ -401,7 +440,7 @@ export class MatButtonToggleGroup implements ControlValueAccessor, OnInit, After
401440
'role': 'presentation',
402441
},
403442
standalone: true,
404-
imports: [MatRipple],
443+
imports: [MatRipple, MatPseudoCheckbox],
405444
})
406445
export class MatButtonToggle implements OnInit, AfterViewInit, OnDestroy {
407446
private _checked = false;

tools/public_api_guard/material/button-toggle.md

+14-1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ export const MAT_BUTTON_TOGGLE_DEFAULT_OPTIONS: InjectionToken<MatButtonToggleDe
2424
// @public
2525
export const MAT_BUTTON_TOGGLE_GROUP: InjectionToken<MatButtonToggleGroup>;
2626

27+
// @public (undocumented)
28+
export function MAT_BUTTON_TOGGLE_GROUP_DEFAULT_OPTIONS_FACTORY(): MatButtonToggleDefaultOptions;
29+
2730
// @public
2831
export const MAT_BUTTON_TOGGLE_GROUP_VALUE_ACCESSOR: any;
2932

@@ -84,6 +87,8 @@ export class MatButtonToggleChange {
8487
// @public
8588
export interface MatButtonToggleDefaultOptions {
8689
appearance?: MatButtonToggleAppearance;
90+
hideMultipleSelectionIndicator?: boolean;
91+
hideSingleSelectionIndicator?: boolean;
8792
}
8893

8994
// @public
@@ -96,6 +101,10 @@ export class MatButtonToggleGroup implements ControlValueAccessor, OnInit, After
96101
get disabled(): boolean;
97102
set disabled(value: boolean);
98103
_emitChangeEvent(toggle: MatButtonToggle): void;
104+
get hideMultipleSelectionIndicator(): boolean;
105+
set hideMultipleSelectionIndicator(value: boolean);
106+
get hideSingleSelectionIndicator(): boolean;
107+
set hideSingleSelectionIndicator(value: boolean);
99108
_isPrechecked(toggle: MatButtonToggle): boolean;
100109
_isSelected(toggle: MatButtonToggle): boolean;
101110
get multiple(): boolean;
@@ -105,6 +114,10 @@ export class MatButtonToggleGroup implements ControlValueAccessor, OnInit, After
105114
// (undocumented)
106115
static ngAcceptInputType_disabled: unknown;
107116
// (undocumented)
117+
static ngAcceptInputType_hideMultipleSelectionIndicator: unknown;
118+
// (undocumented)
119+
static ngAcceptInputType_hideSingleSelectionIndicator: unknown;
120+
// (undocumented)
108121
static ngAcceptInputType_multiple: unknown;
109122
// (undocumented)
110123
static ngAcceptInputType_vertical: unknown;
@@ -127,7 +140,7 @@ export class MatButtonToggleGroup implements ControlValueAccessor, OnInit, After
127140
vertical: boolean;
128141
writeValue(value: any): void;
129142
// (undocumented)
130-
static ɵdir: i0.ɵɵDirectiveDeclaration<MatButtonToggleGroup, "mat-button-toggle-group", ["matButtonToggleGroup"], { "appearance": { "alias": "appearance"; "required": false; }; "name": { "alias": "name"; "required": false; }; "vertical": { "alias": "vertical"; "required": false; }; "value": { "alias": "value"; "required": false; }; "multiple": { "alias": "multiple"; "required": false; }; "disabled": { "alias": "disabled"; "required": false; }; }, { "valueChange": "valueChange"; "change": "change"; }, ["_buttonToggles"], never, true, never>;
143+
static ɵdir: i0.ɵɵDirectiveDeclaration<MatButtonToggleGroup, "mat-button-toggle-group", ["matButtonToggleGroup"], { "appearance": { "alias": "appearance"; "required": false; }; "name": { "alias": "name"; "required": false; }; "vertical": { "alias": "vertical"; "required": false; }; "value": { "alias": "value"; "required": false; }; "multiple": { "alias": "multiple"; "required": false; }; "disabled": { "alias": "disabled"; "required": false; }; "hideSingleSelectionIndicator": { "alias": "hideSingleSelectionIndicator"; "required": false; }; "hideMultipleSelectionIndicator": { "alias": "hideMultipleSelectionIndicator"; "required": false; }; }, { "valueChange": "valueChange"; "change": "change"; }, ["_buttonToggles"], never, true, never>;
131144
// (undocumented)
132145
static ɵfac: i0.ɵɵFactoryDeclaration<MatButtonToggleGroup, [null, { optional: true; }]>;
133146
}

0 commit comments

Comments
 (0)