Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(material/button-toggle): Add checkmark indicators with hideSingleSelectionIndicator and hideMultipleSelectionIndicator input and config options #28553

Merged
merged 2 commits into from
Feb 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading