Skip to content

Commit

Permalink
fix(material/button-toggle): use radio pattern for single select Mat …
Browse files Browse the repository at this point in the history
…toggle button group
  • Loading branch information
clamli committed Feb 1, 2024
1 parent 2455a42 commit 5c845a3
Show file tree
Hide file tree
Showing 6 changed files with 71 additions and 37 deletions.
4 changes: 2 additions & 2 deletions src/dev-app/button-toggle/button-toggle-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<h1>Exclusive Selection</h1>

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

<section>
<mat-button-toggle-group appearance="legacy" name="alignment" [vertical]="isVertical">
<mat-button-toggle-group appearance="legacy" name="legacy alignment" [vertical]="isVertical">
<mat-button-toggle value="left" [disabled]="isDisabled">
<mat-icon>format_align_left</mat-icon>
</mat-button-toggle>
Expand Down
30 changes: 15 additions & 15 deletions src/material/button-toggle/button-toggle.html
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
<button #button class="mat-button-toggle-button mat-focus-indicator"
type="button"
[id]="buttonId"
[attr.tabindex]="disabled ? -1 : tabIndex"
[attr.aria-pressed]="checked"
[disabled]="disabled || null"
[attr.name]="_getButtonName()"
[attr.aria-label]="ariaLabel"
[attr.aria-labelledby]="ariaLabelledby"
(click)="_onButtonClick()">
<span class="mat-button-toggle-label-content">
<ng-content></ng-content>
</span>
</button>
<input #input class="mat-button-toggle-button mat-focus-indicator"
[type]="type"
[id]="buttonId"
[attr.tabindex]="disabled ? -1 : tabIndex"
[attr.aria-pressed]="_getAriaPressed()"
[disabled]="disabled || null"
[attr.name]="_getButtonName()"
[attr.aria-label]="ariaLabel"
[attr.aria-labelledby]="ariaLabelledby"
(click)="_onButtonClick()"
(change)="_onInteractionEvent($event)">
<label class="mat-button-toggle-label-content" [for]="buttonId">
<ng-content></ng-content>
</label>

<span class="mat-button-toggle-focus-overlay"></span>
<span class="mat-button-toggle-ripple" matRipple
[matRippleTrigger]="button"
[matRippleTrigger]="input"
[matRippleDisabled]="this.disableRipple || this.disabled">
</span>
8 changes: 8 additions & 0 deletions src/material/button-toggle/button-toggle.scss
Original file line number Diff line number Diff line change
Expand Up @@ -272,4 +272,12 @@ $_standard-tokens: (
&::-moz-focus-inner {
border: 0;
}

opacity: 0.01;
z-index: 100;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
32 changes: 16 additions & 16 deletions src/material/button-toggle/button-toggle.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ describe('MatButtonToggle with forms', () => {
buttonToggleDebugElements = fixture.debugElement.queryAll(By.directive(MatButtonToggle));
buttonToggleInstances = buttonToggleDebugElements.map(debugEl => debugEl.componentInstance);
innerButtons = buttonToggleDebugElements.map(
debugEl => debugEl.query(By.css('button'))!.nativeElement,
debugEl => debugEl.query(By.css('input'))!.nativeElement,
);

fixture.detectChanges();
Expand Down Expand Up @@ -256,7 +256,7 @@ describe('MatButtonToggle with forms', () => {
const fixture = TestBed.createComponent(ButtonToggleGroupWithIndirectDescendantToggles);
fixture.detectChanges();

const button = fixture.nativeElement.querySelector('.mat-button-toggle button');
const button = fixture.nativeElement.querySelector('.mat-button-toggle input');
const groupDebugElement = fixture.debugElement.query(By.directive(MatButtonToggleGroup))!;
const groupInstance =
groupDebugElement.injector.get<MatButtonToggleGroup>(MatButtonToggleGroup);
Expand Down Expand Up @@ -359,7 +359,7 @@ describe('MatButtonToggle without forms', () => {
buttonToggleNativeElements = buttonToggleDebugElements.map(debugEl => debugEl.nativeElement);

buttonToggleLabelElements = fixture.debugElement
.queryAll(By.css('button'))
.queryAll(By.css('input'))
.map(debugEl => debugEl.nativeElement);

buttonToggleInstances = buttonToggleDebugElements.map(debugEl => debugEl.componentInstance);
Expand Down Expand Up @@ -401,7 +401,7 @@ describe('MatButtonToggle without forms', () => {
});

it('should disable the underlying button when the group is disabled', () => {
const buttons = buttonToggleNativeElements.map(toggle => toggle.querySelector('button')!);
const buttons = buttonToggleNativeElements.map(toggle => toggle.querySelector('input')!);

expect(buttons.every(input => input.disabled)).toBe(false);

Expand Down Expand Up @@ -595,7 +595,7 @@ describe('MatButtonToggle without forms', () => {
buttonToggleDebugElements = fixture.debugElement.queryAll(By.directive(MatButtonToggle));
buttonToggleNativeElements = buttonToggleDebugElements.map(debugEl => debugEl.nativeElement);
buttonToggleLabelElements = fixture.debugElement
.queryAll(By.css('button'))
.queryAll(By.css('input'))
.map(debugEl => debugEl.nativeElement);
buttonToggleInstances = buttonToggleDebugElements.map(debugEl => debugEl.componentInstance);
}));
Expand All @@ -612,7 +612,7 @@ describe('MatButtonToggle without forms', () => {
expect(buttonToggleInstances.every(buttonToggle => !buttonToggle.checked)).toBe(true);

const nativeCheckboxLabel = buttonToggleDebugElements[0].query(
By.css('button'),
By.css('input'),
)!.nativeElement;

nativeCheckboxLabel.click();
Expand All @@ -638,7 +638,7 @@ describe('MatButtonToggle without forms', () => {

it('should check a button toggle upon interaction with underlying native checkbox', () => {
const nativeCheckboxButton = buttonToggleDebugElements[0].query(
By.css('button'),
By.css('input'),
)!.nativeElement;

nativeCheckboxButton.click();
Expand Down Expand Up @@ -722,7 +722,7 @@ describe('MatButtonToggle without forms', () => {
)!.nativeElement;
buttonToggleInstance = buttonToggleDebugElement.componentInstance;
buttonToggleButtonElement = buttonToggleNativeElement.querySelector(
'button',
'input',
)! as HTMLButtonElement;
}));

Expand Down Expand Up @@ -761,7 +761,7 @@ describe('MatButtonToggle without forms', () => {
}));

it('should focus on underlying input element when focus() is called', () => {
const nativeButton = buttonToggleDebugElement.query(By.css('button'))!.nativeElement;
const nativeButton = buttonToggleDebugElement.query(By.css('input'))!.nativeElement;
expect(document.activeElement).not.toBe(nativeButton);

buttonToggleInstance.focus();
Expand Down Expand Up @@ -790,7 +790,7 @@ describe('MatButtonToggle without forms', () => {
const fixture = TestBed.createComponent(StandaloneButtonToggle);
const checkboxDebugElement = fixture.debugElement.query(By.directive(MatButtonToggle))!;
const checkboxNativeElement = checkboxDebugElement.nativeElement;
const buttonElement = checkboxNativeElement.querySelector('button') as HTMLButtonElement;
const buttonElement = checkboxNativeElement.querySelector('input') as HTMLButtonElement;

fixture.detectChanges();
expect(buttonElement.hasAttribute('aria-label')).toBe(false);
Expand All @@ -800,7 +800,7 @@ describe('MatButtonToggle without forms', () => {
const fixture = TestBed.createComponent(ButtonToggleWithAriaLabel);
const checkboxDebugElement = fixture.debugElement.query(By.directive(MatButtonToggle))!;
const checkboxNativeElement = checkboxDebugElement.nativeElement;
const buttonElement = checkboxNativeElement.querySelector('button') as HTMLButtonElement;
const buttonElement = checkboxNativeElement.querySelector('input') as HTMLButtonElement;

fixture.detectChanges();
expect(buttonElement.getAttribute('aria-label')).toBe('Super effective');
Expand All @@ -825,7 +825,7 @@ describe('MatButtonToggle without forms', () => {
const fixture = TestBed.createComponent(ButtonToggleWithAriaLabelledby);
checkboxDebugElement = fixture.debugElement.query(By.directive(MatButtonToggle))!;
checkboxNativeElement = checkboxDebugElement.nativeElement;
buttonElement = checkboxNativeElement.querySelector('button') as HTMLButtonElement;
buttonElement = checkboxNativeElement.querySelector('input') as HTMLButtonElement;

fixture.detectChanges();
expect(buttonElement.getAttribute('aria-labelledby')).toBe('some-id');
Expand All @@ -835,7 +835,7 @@ describe('MatButtonToggle without forms', () => {
const fixture = TestBed.createComponent(StandaloneButtonToggle);
checkboxDebugElement = fixture.debugElement.query(By.directive(MatButtonToggle))!;
checkboxNativeElement = checkboxDebugElement.nativeElement;
buttonElement = checkboxNativeElement.querySelector('button') as HTMLButtonElement;
buttonElement = checkboxNativeElement.querySelector('input') as HTMLButtonElement;

fixture.detectChanges();
expect(buttonElement.getAttribute('aria-labelledby')).toBe(null);
Expand All @@ -847,7 +847,7 @@ describe('MatButtonToggle without forms', () => {
const fixture = TestBed.createComponent(ButtonToggleWithTabindex);
fixture.detectChanges();

const button = fixture.nativeElement.querySelector('.mat-button-toggle button');
const button = fixture.nativeElement.querySelector('.mat-button-toggle input');

expect(button.getAttribute('tabindex')).toBe('3');
});
Expand All @@ -866,7 +866,7 @@ describe('MatButtonToggle without forms', () => {
fixture.detectChanges();

const host = fixture.nativeElement.querySelector('.mat-button-toggle');
const button = host.querySelector('button');
const button = host.querySelector('input');

expect(document.activeElement).not.toBe(button);

Expand All @@ -891,7 +891,7 @@ describe('MatButtonToggle without forms', () => {
const hostNode: HTMLElement = fixture.nativeElement.querySelector('.mat-button-toggle');

expect(hostNode.hasAttribute('name')).toBe(false);
expect(hostNode.querySelector('button')!.getAttribute('name')).toBe('custom-name');
expect(hostNode.querySelector('input')!.getAttribute('name')).toBe('custom-name');
});

it(
Expand Down
29 changes: 26 additions & 3 deletions src/material/button-toggle/button-toggle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,8 @@ export class MatButtonToggleChange {
{provide: MAT_BUTTON_TOGGLE_GROUP, useExisting: MatButtonToggleGroup},
],
host: {
'role': 'group',
'class': 'mat-button-toggle-group',
'[role]': "multiple ? 'group' : 'radiogroup'",
'[attr.aria-disabled]': 'disabled',
'[class.mat-button-toggle-vertical]': 'vertical',
'[class.mat-button-toggle-group-appearance-standard]': 'appearance === "standard"',
Expand Down Expand Up @@ -417,8 +417,13 @@ export class MatButtonToggle implements OnInit, AfterViewInit, OnDestroy {
*/
@Input('aria-labelledby') ariaLabelledby: string | null = null;

/** Type of the button toggle. Either 'radio' or 'button'. */
get type(): string {
return this._isSingleSelector() ? 'radio' : 'button';
}

/** Underlying native `button` element. */
@ViewChild('button') _buttonElement: ElementRef<HTMLButtonElement>;
@ViewChild('input') _inputElement: ElementRef<HTMLInputElement>;

/** The parent button toggle group (exclusive selection). Optional. */
buttonToggleGroup: MatButtonToggleGroup;
Expand Down Expand Up @@ -536,7 +541,7 @@ export class MatButtonToggle implements OnInit, AfterViewInit, OnDestroy {

/** Focuses the button. */
focus(options?: FocusOptions): void {
this._buttonElement.nativeElement.focus(options);
this._inputElement.nativeElement.focus(options);
}

/** Checks the button toggle due to an interaction with the underlying native button. */
Expand All @@ -554,6 +559,15 @@ export class MatButtonToggle implements OnInit, AfterViewInit, OnDestroy {
this.change.emit(new MatButtonToggleChange(this, this.value));
}

/**
* Stop propagation on the change event.
* Otherwise the change event, from the input element, will bubble up and
* emit its event object to the `change` output.
*/
_onInteractionEvent(event: Event) {
event.stopPropagation();
}

/**
* Marks the button toggle as needing checking for change detection.
* This method is exposed because the parent button toggle group will directly
Expand All @@ -573,6 +587,15 @@ export class MatButtonToggle implements OnInit, AfterViewInit, OnDestroy {
return this.name || null;
}

/** Get the aria-pressed attribute value. */
_getAriaPressed(): boolean | null {
// When the toggle stands alone, or in multiple selection mode, use aria-pressed attribute.
if (!this._isSingleSelector()) {
return this.checked;
}
return null;
}

/** Whether the toggle is in single selection mode. */
private _isSingleSelector(): boolean {
return this.buttonToggleGroup && !this.buttonToggleGroup.multiple;
Expand Down
5 changes: 4 additions & 1 deletion tools/public_api_guard/material/button-toggle.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ export class MatButtonToggle implements OnInit, AfterViewInit, OnDestroy {
set appearance(value: MatButtonToggleAppearance);
ariaLabel: string;
ariaLabelledby: string | null;
_buttonElement: ElementRef<HTMLButtonElement>;
get buttonId(): string;
buttonToggleGroup: MatButtonToggleGroup;
readonly change: EventEmitter<MatButtonToggleChange>;
Expand All @@ -44,8 +43,10 @@ export class MatButtonToggle implements OnInit, AfterViewInit, OnDestroy {
set disabled(value: boolean);
disableRipple: boolean;
focus(options?: FocusOptions): void;
_getAriaPressed(): boolean | null;
_getButtonName(): string | null;
id: string;
_inputElement: ElementRef<HTMLInputElement>;
_markForCheck(): void;
name: string;
// (undocumented)
Expand All @@ -61,7 +62,9 @@ export class MatButtonToggle implements OnInit, AfterViewInit, OnDestroy {
// (undocumented)
ngOnInit(): void;
_onButtonClick(): void;
_onInteractionEvent(event: Event): void;
tabIndex: number | null;
get type(): string;
value: any;
// (undocumented)
static ɵcmp: i0.ɵɵComponentDeclaration<MatButtonToggle, "mat-button-toggle", ["matButtonToggle"], { "ariaLabel": { "alias": "aria-label"; "required": false; }; "ariaLabelledby": { "alias": "aria-labelledby"; "required": false; }; "id": { "alias": "id"; "required": false; }; "name": { "alias": "name"; "required": false; }; "value": { "alias": "value"; "required": false; }; "tabIndex": { "alias": "tabIndex"; "required": false; }; "disableRipple": { "alias": "disableRipple"; "required": false; }; "appearance": { "alias": "appearance"; "required": false; }; "checked": { "alias": "checked"; "required": false; }; "disabled": { "alias": "disabled"; "required": false; }; }, { "change": "change"; }, never, ["*"], true, never>;
Expand Down

0 comments on commit 5c845a3

Please sign in to comment.