diff --git a/src/dev-app/button-toggle/button-toggle-demo.html b/src/dev-app/button-toggle/button-toggle-demo.html index b2cfc8186f3e..f0619ad7895a 100644 --- a/src/dev-app/button-toggle/button-toggle-demo.html +++ b/src/dev-app/button-toggle/button-toggle-demo.html @@ -6,10 +6,18 @@ Disable Button Toggle Items

+

+ Hide Single Selection Indicator +

+ +

+ Hide Multiple Selection Indicator +

+

Exclusive Selection

- + format_align_left @@ -26,7 +34,7 @@

Exclusive Selection

- + format_align_left @@ -45,7 +53,7 @@

Exclusive Selection

Disabled Group

- + format_bold @@ -60,7 +68,7 @@

Disabled Group

Multiple Selection

- + Flour Eggs Sugar @@ -68,7 +76,7 @@

Multiple Selection

- + Flour Eggs Sugar @@ -82,7 +90,7 @@

Single Toggle

Dynamic Exclusive Selection

- + @for (pie of pieOptions; track pie) { {{pie}} } diff --git a/src/dev-app/button-toggle/button-toggle-demo.ts b/src/dev-app/button-toggle/button-toggle-demo.ts index add6a66823a7..e7cbc1f27ee5 100644 --- a/src/dev-app/button-toggle/button-toggle-demo.ts +++ b/src/dev-app/button-toggle/button-toggle-demo.ts @@ -23,6 +23,8 @@ import {MatIconModule} from '@angular/material/icon'; export class ButtonToggleDemo { isVertical = false; isDisabled = false; + hideSingleSelectionIndicator = false; + hideMultipleSelectionIndicator = false; favoritePie = 'Apple'; pieOptions = ['Apple', 'Cherry', 'Pecan', 'Lemon']; } diff --git a/src/material/button-toggle/button-toggle.html b/src/material/button-toggle/button-toggle.html index 9a2113709715..0a1b9a06eeaf 100644 --- a/src/material/button-toggle/button-toggle.html +++ b/src/material/button-toggle/button-toggle.html @@ -9,6 +9,24 @@ [attr.aria-labelledby]="ariaLabelledby" (click)="_onButtonClick()"> + + @if (buttonToggleGroup && checked && !buttonToggleGroup.multiple && !buttonToggleGroup.hideSingleSelectionIndicator) { + + } + + @if (buttonToggleGroup && checked && buttonToggleGroup.multiple && !buttonToggleGroup.hideMultipleSelectionIndicator) { + + } diff --git a/src/material/button-toggle/button-toggle.scss b/src/material/button-toggle/button-toggle.scss index 47dcfef3c468..3cc13c4811c9 100644 --- a/src/material/button-toggle/button-toggle.scss +++ b/src/material/button-toggle/button-toggle.scss @@ -9,6 +9,7 @@ $standard-padding: 0 12px !default; $legacy-padding: 0 16px !default; +$checkmark-padding: 12px !default; // TODO(crisbeto): these variables aren't used anymore and should be removed. $legacy-height: 36px !default; @@ -52,6 +53,12 @@ $_standard-tokens: ( @include token-utils.use-tokens($_standard-tokens...) { @include token-utils.create-token-slot(border-radius, shape); border: solid 1px var(#{token-utils.get-token-variable(divider-color)}); + + .mat-pseudo-checkbox { + --mat-minimal-pseudo-checkbox-selected-checkmark-color: var( + #{token-utils.get-token-variable(selected-state-text-color)} + ); + } } &:not([class*='mat-elevation-z']) { @@ -85,6 +92,10 @@ $_standard-tokens: ( @include token-utils.create-token-slot(font-weight, label-text-weight); @include token-utils.create-token-slot(letter-spacing, label-text-tracking); + --mat-minimal-pseudo-checkbox-selected-checkmark-color: var( + #{token-utils.get-token-variable(selected-state-text-color)} + ); + &.cdk-keyboard-focused .mat-button-toggle-focus-overlay { @include token-utils.create-token-slot(opacity, focus-state-layer-opacity); } @@ -94,6 +105,14 @@ $_standard-tokens: ( .mat-icon svg { vertical-align: top; } + + .mat-pseudo-checkbox { + margin-right: $checkmark-padding; + [dir='rtl'] & { + margin-right: 0; + margin-left: $checkmark-padding; + } + } } .mat-button-toggle-checked { @@ -107,6 +126,9 @@ $_standard-tokens: ( @include token-utils.use-tokens($_legacy-tokens...) { @include token-utils.create-token-slot(color, disabled-state-text-color); @include token-utils.create-token-slot(background-color, disabled-state-background-color); + --mat-minimal-pseudo-checkbox-disabled-selected-checkmark-color: var( + #{token-utils.get-token-variable(disabled-state-text-color)} + ); &.mat-button-toggle-checked { @include token-utils.create-token-slot(background-color, @@ -150,6 +172,12 @@ $_standard-tokens: ( @include token-utils.create-token-slot(color, disabled-state-text-color); @include token-utils.create-token-slot(background-color, disabled-state-background-color); + .mat-pseudo-checkbox { + --mat-minimal-pseudo-checkbox-disabled-selected-checkmark-color: var( + #{token-utils.get-token-variable(disabled-selected-state-text-color)} + ); + } + &.mat-button-toggle-checked { @include token-utils.create-token-slot(color, disabled-selected-state-text-color); @include token-utils.create-token-slot(background-color, diff --git a/src/material/button-toggle/button-toggle.spec.ts b/src/material/button-toggle/button-toggle.spec.ts index 7f33cc6668f9..20265dd178fa 100644 --- a/src/material/button-toggle/button-toggle.spec.ts +++ b/src/material/button-toggle/button-toggle.spec.ts @@ -5,6 +5,7 @@ import {ComponentFixture, fakeAsync, flush, TestBed, tick} from '@angular/core/t import {FormControl, FormsModule, NgModel, ReactiveFormsModule} from '@angular/forms'; import {By} from '@angular/platform-browser'; import { + MAT_BUTTON_TOGGLE_DEFAULT_OPTIONS, MatButtonToggle, MatButtonToggleChange, MatButtonToggleGroup, @@ -546,6 +547,13 @@ describe('MatButtonToggle without forms', () => { expect(groupInstance.value).toBeFalsy(); expect(groupInstance.selected).toBeFalsy(); })); + + it('should show checkmark indicator by default', () => { + buttonToggleLabelElements[0].click(); + fixture.detectChanges(); + + expect(document.querySelectorAll('.mat-pseudo-checkbox').length).toBe(1); + }); }); describe('with initial value and change event', () => { @@ -701,6 +709,14 @@ describe('MatButtonToggle without forms', () => { groupInstance.value = 'not-an-array'; }).toThrowError(/Value must be an array/); }); + + it('should show checkmark indicator by default', () => { + buttonToggleLabelElements[0].click(); + buttonToggleLabelElements[1].click(); + fixture.detectChanges(); + + expect(document.querySelectorAll('.mat-pseudo-checkbox').length).toBe(2); + }); }); describe('as standalone', () => { @@ -876,6 +892,52 @@ describe('MatButtonToggle without forms', () => { }); }); + describe('with tokens to hide checkmark selection indicators', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + MatButtonToggleModule, + ButtonTogglesInsideButtonToggleGroup, + ButtonTogglesInsideButtonToggleGroupMultiple, + ], + providers: [ + { + provide: MAT_BUTTON_TOGGLE_DEFAULT_OPTIONS, + useValue: { + hideSingleSelectionIndicator: true, + hideMultipleSelectionIndicator: true, + }, + }, + ], + }); + + TestBed.compileComponents(); + }); + + it('should hide checkmark indicator for single selection', () => { + const fixture = TestBed.createComponent(ButtonTogglesInsideButtonToggleGroup); + fixture.detectChanges(); + + fixture.debugElement.query(By.css('button')).nativeElement.click(); + fixture.detectChanges(); + + expect(document.querySelectorAll('.mat-pseudo-checkbox').length).toBe(0); + }); + + it('should hide checkmark indicator for multiple selection', () => { + const fixture = TestBed.createComponent(ButtonTogglesInsideButtonToggleGroupMultiple); + fixture.detectChanges(); + + // Check all button toggles in the group + fixture.debugElement + .queryAll(By.css('button')) + .forEach(toggleButton => toggleButton.nativeElement.click()); + fixture.detectChanges(); + + expect(document.querySelectorAll('.mat-pseudo-checkbox').length).toBe(0); + }); + }); + it('should not throw on init when toggles are repeated and there is an initial value', () => { const fixture = TestBed.createComponent(RepeatedButtonTogglesWithPreselectedValue); diff --git a/src/material/button-toggle/button-toggle.ts b/src/material/button-toggle/button-toggle.ts index 34c5477a6944..ab9c8a527928 100644 --- a/src/material/button-toggle/button-toggle.ts +++ b/src/material/button-toggle/button-toggle.ts @@ -33,7 +33,7 @@ import { booleanAttribute, } from '@angular/core'; import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms'; -import {MatRipple} from '@angular/material/core'; +import {MatRipple, MatPseudoCheckbox} from '@angular/material/core'; /** * @deprecated No longer used. @@ -54,6 +54,10 @@ export interface MatButtonToggleDefaultOptions { * setting an appearance on a button toggle or group. */ appearance?: MatButtonToggleAppearance; + /** Whetehr icon indicators should be hidden for single-selection button toggle groups. */ + hideSingleSelectionIndicator?: boolean; + /** Whether icon indicators should be hidden for multiple-selection button toggle groups. */ + hideMultipleSelectionIndicator?: boolean; } /** @@ -62,8 +66,19 @@ export interface MatButtonToggleDefaultOptions { */ export const MAT_BUTTON_TOGGLE_DEFAULT_OPTIONS = new InjectionToken( 'MAT_BUTTON_TOGGLE_DEFAULT_OPTIONS', + { + providedIn: 'root', + factory: MAT_BUTTON_TOGGLE_GROUP_DEFAULT_OPTIONS_FACTORY, + }, ); +export function MAT_BUTTON_TOGGLE_GROUP_DEFAULT_OPTIONS_FACTORY(): MatButtonToggleDefaultOptions { + return { + hideSingleSelectionIndicator: false, + hideMultipleSelectionIndicator: false, + }; +} + /** * Injection token that can be used to reference instances of `MatButtonToggleGroup`. * It serves as alternative token to the actual `MatButtonToggleGroup` class which @@ -215,6 +230,28 @@ export class MatButtonToggleGroup implements ControlValueAccessor, OnInit, After @Output() readonly change: EventEmitter = new EventEmitter(); + /** Whether checkmark indicator for single-selection button toggle groups is hidden. */ + @Input({transform: booleanAttribute}) + get hideSingleSelectionIndicator(): boolean { + return this._hideSingleSelectionIndicator; + } + set hideSingleSelectionIndicator(value: boolean) { + this._hideSingleSelectionIndicator = value; + this._markButtonsForCheck(); + } + private _hideSingleSelectionIndicator: boolean; + + /** Whether checkmark indicator for multiple-selection button toggle groups is hidden. */ + @Input({transform: booleanAttribute}) + get hideMultipleSelectionIndicator(): boolean { + return this._hideMultipleSelectionIndicator; + } + set hideMultipleSelectionIndicator(value: boolean) { + this._hideMultipleSelectionIndicator = value; + this._markButtonsForCheck(); + } + private _hideMultipleSelectionIndicator: boolean; + constructor( private _changeDetector: ChangeDetectorRef, @Optional() @@ -223,6 +260,8 @@ export class MatButtonToggleGroup implements ControlValueAccessor, OnInit, After ) { this.appearance = defaultOptions && defaultOptions.appearance ? defaultOptions.appearance : 'standard'; + this.hideSingleSelectionIndicator = defaultOptions?.hideSingleSelectionIndicator ?? false; + this.hideMultipleSelectionIndicator = defaultOptions?.hideMultipleSelectionIndicator ?? false; } ngOnInit() { @@ -401,7 +440,7 @@ export class MatButtonToggleGroup implements ControlValueAccessor, OnInit, After 'role': 'presentation', }, standalone: true, - imports: [MatRipple], + imports: [MatRipple, MatPseudoCheckbox], }) export class MatButtonToggle implements OnInit, AfterViewInit, OnDestroy { private _checked = false; diff --git a/tools/public_api_guard/material/button-toggle.md b/tools/public_api_guard/material/button-toggle.md index 2c3fa8aaa840..38d3810694fb 100644 --- a/tools/public_api_guard/material/button-toggle.md +++ b/tools/public_api_guard/material/button-toggle.md @@ -24,6 +24,9 @@ export const MAT_BUTTON_TOGGLE_DEFAULT_OPTIONS: InjectionToken; +// @public (undocumented) +export function MAT_BUTTON_TOGGLE_GROUP_DEFAULT_OPTIONS_FACTORY(): MatButtonToggleDefaultOptions; + // @public export const MAT_BUTTON_TOGGLE_GROUP_VALUE_ACCESSOR: any; @@ -84,6 +87,8 @@ export class MatButtonToggleChange { // @public export interface MatButtonToggleDefaultOptions { appearance?: MatButtonToggleAppearance; + hideMultipleSelectionIndicator?: boolean; + hideSingleSelectionIndicator?: boolean; } // @public @@ -96,6 +101,10 @@ export class MatButtonToggleGroup implements ControlValueAccessor, OnInit, After get disabled(): boolean; set disabled(value: boolean); _emitChangeEvent(toggle: MatButtonToggle): void; + get hideMultipleSelectionIndicator(): boolean; + set hideMultipleSelectionIndicator(value: boolean); + get hideSingleSelectionIndicator(): boolean; + set hideSingleSelectionIndicator(value: boolean); _isPrechecked(toggle: MatButtonToggle): boolean; _isSelected(toggle: MatButtonToggle): boolean; get multiple(): boolean; @@ -105,6 +114,10 @@ export class MatButtonToggleGroup implements ControlValueAccessor, OnInit, After // (undocumented) static ngAcceptInputType_disabled: unknown; // (undocumented) + static ngAcceptInputType_hideMultipleSelectionIndicator: unknown; + // (undocumented) + static ngAcceptInputType_hideSingleSelectionIndicator: unknown; + // (undocumented) static ngAcceptInputType_multiple: unknown; // (undocumented) static ngAcceptInputType_vertical: unknown; @@ -127,7 +140,7 @@ export class MatButtonToggleGroup implements ControlValueAccessor, OnInit, After vertical: boolean; writeValue(value: any): void; // (undocumented) - static ɵdir: i0.ɵɵDirectiveDeclaration; + static ɵdir: i0.ɵɵDirectiveDeclaration; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration; }