Skip to content

Commit

Permalink
fix(material/button-toggle): Add checkmark indicators with hideSingle…
Browse files Browse the repository at this point in the history
…SelectionIndicator 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
  • Loading branch information
amysorto authored Feb 27, 2024
1 parent 8abb33d commit 09111d0
Show file tree
Hide file tree
Showing 7 changed files with 179 additions and 9 deletions.
20 changes: 14 additions & 6 deletions src/dev-app/button-toggle/button-toggle-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,18 @@
<mat-checkbox (change)="isDisabled = $event.checked">Disable Button Toggle Items</mat-checkbox>
</p>

<p>
<mat-checkbox (change)="hideSingleSelectionIndicator = $event.checked">Hide Single Selection Indicator</mat-checkbox>
</p>

<p>
<mat-checkbox (change)="hideMultipleSelectionIndicator = $event.checked">Hide Multiple Selection Indicator</mat-checkbox>
</p>

<h1>Exclusive Selection</h1>

<section>
<mat-button-toggle-group name="alignment" [vertical]="isVertical">
<mat-button-toggle-group name="alignment" [vertical]="isVertical" [hideSingleSelectionIndicator]="hideSingleSelectionIndicator">
<mat-button-toggle value="left" [disabled]="isDisabled">
<mat-icon>format_align_left</mat-icon>
</mat-button-toggle>
Expand All @@ -26,7 +34,7 @@ <h1>Exclusive Selection</h1>
</section>

<section>
<mat-button-toggle-group appearance="legacy" name="alignment" [vertical]="isVertical">
<mat-button-toggle-group appearance="legacy" name="alignment" [vertical]="isVertical" [hideSingleSelectionIndicator]="hideSingleSelectionIndicator">
<mat-button-toggle value="left" [disabled]="isDisabled">
<mat-icon>format_align_left</mat-icon>
</mat-button-toggle>
Expand All @@ -45,7 +53,7 @@ <h1>Exclusive Selection</h1>
<h1>Disabled Group</h1>

<section>
<mat-button-toggle-group name="checkbox" [vertical]="isVertical" [disabled]="isDisabled">
<mat-button-toggle-group name="checkbox" [vertical]="isVertical" [disabled]="isDisabled" [hideSingleSelectionIndicator]="hideSingleSelectionIndicator">
<mat-button-toggle value="bold">
<mat-icon>format_bold</mat-icon>
</mat-button-toggle>
Expand All @@ -60,15 +68,15 @@ <h1>Disabled Group</h1>

<h1>Multiple Selection</h1>
<section>
<mat-button-toggle-group multiple [vertical]="isVertical">
<mat-button-toggle-group multiple [vertical]="isVertical" [hideMultipleSelectionIndicator]="hideMultipleSelectionIndicator">
<mat-button-toggle>Flour</mat-button-toggle>
<mat-button-toggle>Eggs</mat-button-toggle>
<mat-button-toggle>Sugar</mat-button-toggle>
<mat-button-toggle [disabled]="isDisabled">Milk</mat-button-toggle>
</mat-button-toggle-group>
</section>
<section>
<mat-button-toggle-group appearance="legacy" multiple [vertical]="isVertical">
<mat-button-toggle-group appearance="legacy" multiple [vertical]="isVertical" [hideMultipleSelectionIndicator]="hideMultipleSelectionIndicator">
<mat-button-toggle>Flour</mat-button-toggle>
<mat-button-toggle>Eggs</mat-button-toggle>
<mat-button-toggle>Sugar</mat-button-toggle>
Expand All @@ -82,7 +90,7 @@ <h1>Single Toggle</h1>

<h1>Dynamic Exclusive Selection</h1>
<section>
<mat-button-toggle-group name="pies" [(ngModel)]="favoritePie" [vertical]="isVertical">
<mat-button-toggle-group name="pies" [(ngModel)]="favoritePie" [vertical]="isVertical" [hideSingleSelectionIndicator]="hideSingleSelectionIndicator">
@for (pie of pieOptions; track pie) {
<mat-button-toggle [value]="pie">{{pie}}</mat-button-toggle>
}
Expand Down
2 changes: 2 additions & 0 deletions src/dev-app/button-toggle/button-toggle-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
}
18 changes: 18 additions & 0 deletions src/material/button-toggle/button-toggle.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,24 @@
[attr.aria-labelledby]="ariaLabelledby"
(click)="_onButtonClick()">
<span class="mat-button-toggle-label-content">
<!-- Render checkmark at the beginning for single-selection. -->
@if (buttonToggleGroup && checked && !buttonToggleGroup.multiple && !buttonToggleGroup.hideSingleSelectionIndicator) {
<mat-pseudo-checkbox
class="mat-mdc-option-pseudo-checkbox"
[disabled]="disabled"
state="checked"
aria-hidden="true"
appearance="minimal"></mat-pseudo-checkbox>
}
<!-- Render checkmark at the beginning for multiple-selection. -->
@if (buttonToggleGroup && checked && buttonToggleGroup.multiple && !buttonToggleGroup.hideMultipleSelectionIndicator) {
<mat-pseudo-checkbox
class="mat-mdc-option-pseudo-checkbox"
[disabled]="disabled"
state="checked"
aria-hidden="true"
appearance="minimal"></mat-pseudo-checkbox>
}
<ng-content></ng-content>
</span>
</button>
Expand Down
28 changes: 28 additions & 0 deletions src/material/button-toggle/button-toggle.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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']) {
Expand Down Expand Up @@ -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);
}
Expand All @@ -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 {
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
62 changes: 62 additions & 0 deletions src/material/button-toggle/button-toggle.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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);

Expand Down
43 changes: 41 additions & 2 deletions src/material/button-toggle/button-toggle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;
}

/**
Expand All @@ -62,8 +66,19 @@ export interface MatButtonToggleDefaultOptions {
*/
export const MAT_BUTTON_TOGGLE_DEFAULT_OPTIONS = new InjectionToken<MatButtonToggleDefaultOptions>(
'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
Expand Down Expand Up @@ -215,6 +230,28 @@ export class MatButtonToggleGroup implements ControlValueAccessor, OnInit, After
@Output() readonly change: EventEmitter<MatButtonToggleChange> =
new EventEmitter<MatButtonToggleChange>();

/** 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()
Expand All @@ -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() {
Expand Down Expand Up @@ -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;
Expand Down
15 changes: 14 additions & 1 deletion tools/public_api_guard/material/button-toggle.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ export const MAT_BUTTON_TOGGLE_DEFAULT_OPTIONS: InjectionToken<MatButtonToggleDe
// @public
export const MAT_BUTTON_TOGGLE_GROUP: InjectionToken<MatButtonToggleGroup>;

// @public (undocumented)
export function MAT_BUTTON_TOGGLE_GROUP_DEFAULT_OPTIONS_FACTORY(): MatButtonToggleDefaultOptions;

// @public
export const MAT_BUTTON_TOGGLE_GROUP_VALUE_ACCESSOR: any;

Expand Down Expand Up @@ -84,6 +87,8 @@ export class MatButtonToggleChange {
// @public
export interface MatButtonToggleDefaultOptions {
appearance?: MatButtonToggleAppearance;
hideMultipleSelectionIndicator?: boolean;
hideSingleSelectionIndicator?: boolean;
}

// @public
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -127,7 +140,7 @@ export class MatButtonToggleGroup implements ControlValueAccessor, OnInit, After
vertical: boolean;
writeValue(value: any): void;
// (undocumented)
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>;
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>;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration<MatButtonToggleGroup, [null, { optional: true; }]>;
}
Expand Down

0 comments on commit 09111d0

Please sign in to comment.