Skip to content

Commit fcd8a94

Browse files
committed
fix(material/button-toggle): use radio pattern for single select Mat toggle button group
1 parent 2455a42 commit fcd8a94

File tree

5 files changed

+54
-33
lines changed

5 files changed

+54
-33
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
<h1>Exclusive Selection</h1>
1010

1111
<section>
12-
<mat-button-toggle-group name="alignment" [vertical]="isVertical">
12+
<mat-button-toggle-group name="standard alignment" [vertical]="isVertical">
1313
<mat-button-toggle value="left" [disabled]="isDisabled">
1414
<mat-icon>format_align_left</mat-icon>
1515
</mat-button-toggle>
@@ -26,7 +26,7 @@ <h1>Exclusive Selection</h1>
2626
</section>
2727

2828
<section>
29-
<mat-button-toggle-group appearance="legacy" name="alignment" [vertical]="isVertical">
29+
<mat-button-toggle-group appearance="legacy" name="legacy alignment" [vertical]="isVertical">
3030
<mat-button-toggle value="left" [disabled]="isDisabled">
3131
<mat-icon>format_align_left</mat-icon>
3232
</mat-button-toggle>

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

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
1-
<button #button class="mat-button-toggle-button mat-focus-indicator"
2-
type="button"
3-
[id]="buttonId"
4-
[attr.tabindex]="disabled ? -1 : tabIndex"
5-
[attr.aria-pressed]="checked"
6-
[disabled]="disabled || null"
7-
[attr.name]="_getButtonName()"
8-
[attr.aria-label]="ariaLabel"
9-
[attr.aria-labelledby]="ariaLabelledby"
10-
(click)="_onButtonClick()">
11-
<span class="mat-button-toggle-label-content">
12-
<ng-content></ng-content>
13-
</span>
14-
</button>
1+
<input #button class="mat-button-toggle-button mat-focus-indicator"
2+
[type]="type"
3+
[id]="buttonId"
4+
[attr.tabindex]="disabled ? -1 : tabIndex"
5+
[attr.aria-pressed]="_getAriaPressed()"
6+
[disabled]="disabled || null"
7+
[attr.name]="_getButtonName()"
8+
[attr.aria-label]="ariaLabel"
9+
[attr.aria-labelledby]="ariaLabelledby"
10+
(click)="_onButtonClick()">
11+
<label class="mat-button-toggle-label-content" [for]="buttonId">
12+
<ng-content></ng-content>
13+
</label>
1514

1615
<span class="mat-button-toggle-focus-overlay"></span>
1716
<span class="mat-button-toggle-ripple" matRipple

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,4 +272,12 @@ $_standard-tokens: (
272272
&::-moz-focus-inner {
273273
border: 0;
274274
}
275+
276+
opacity: 0.01;
277+
z-index: 100;
278+
position: absolute;
279+
top: 0;
280+
left: 0;
281+
right: 0;
282+
bottom: 0;
275283
}

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

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ describe('MatButtonToggle with forms', () => {
9797
buttonToggleDebugElements = fixture.debugElement.queryAll(By.directive(MatButtonToggle));
9898
buttonToggleInstances = buttonToggleDebugElements.map(debugEl => debugEl.componentInstance);
9999
innerButtons = buttonToggleDebugElements.map(
100-
debugEl => debugEl.query(By.css('button'))!.nativeElement,
100+
debugEl => debugEl.query(By.css('input'))!.nativeElement,
101101
);
102102

103103
fixture.detectChanges();
@@ -256,7 +256,7 @@ describe('MatButtonToggle with forms', () => {
256256
const fixture = TestBed.createComponent(ButtonToggleGroupWithIndirectDescendantToggles);
257257
fixture.detectChanges();
258258

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

361361
buttonToggleLabelElements = fixture.debugElement
362-
.queryAll(By.css('button'))
362+
.queryAll(By.css('input'))
363363
.map(debugEl => debugEl.nativeElement);
364364

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

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

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

@@ -595,7 +595,7 @@ describe('MatButtonToggle without forms', () => {
595595
buttonToggleDebugElements = fixture.debugElement.queryAll(By.directive(MatButtonToggle));
596596
buttonToggleNativeElements = buttonToggleDebugElements.map(debugEl => debugEl.nativeElement);
597597
buttonToggleLabelElements = fixture.debugElement
598-
.queryAll(By.css('button'))
598+
.queryAll(By.css('input'))
599599
.map(debugEl => debugEl.nativeElement);
600600
buttonToggleInstances = buttonToggleDebugElements.map(debugEl => debugEl.componentInstance);
601601
}));
@@ -612,7 +612,7 @@ describe('MatButtonToggle without forms', () => {
612612
expect(buttonToggleInstances.every(buttonToggle => !buttonToggle.checked)).toBe(true);
613613

614614
const nativeCheckboxLabel = buttonToggleDebugElements[0].query(
615-
By.css('button'),
615+
By.css('input'),
616616
)!.nativeElement;
617617

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

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

644644
nativeCheckboxButton.click();
@@ -722,7 +722,7 @@ describe('MatButtonToggle without forms', () => {
722722
)!.nativeElement;
723723
buttonToggleInstance = buttonToggleDebugElement.componentInstance;
724724
buttonToggleButtonElement = buttonToggleNativeElement.querySelector(
725-
'button',
725+
'input',
726726
)! as HTMLButtonElement;
727727
}));
728728

@@ -761,7 +761,7 @@ describe('MatButtonToggle without forms', () => {
761761
}));
762762

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

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

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

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

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

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

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

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

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

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

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

893893
expect(hostNode.hasAttribute('name')).toBe(false);
894-
expect(hostNode.querySelector('button')!.getAttribute('name')).toBe('custom-name');
894+
expect(hostNode.querySelector('input')!.getAttribute('name')).toBe('custom-name');
895895
});
896896

897897
it(

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,8 @@ export class MatButtonToggleChange {
106106
{provide: MAT_BUTTON_TOGGLE_GROUP, useExisting: MatButtonToggleGroup},
107107
],
108108
host: {
109-
'role': 'group',
110109
'class': 'mat-button-toggle-group',
110+
'[role]': "multiple ? 'group' : 'radiogroup'",
111111
'[attr.aria-disabled]': 'disabled',
112112
'[class.mat-button-toggle-vertical]': 'vertical',
113113
'[class.mat-button-toggle-group-appearance-standard]': 'appearance === "standard"',
@@ -417,6 +417,11 @@ export class MatButtonToggle implements OnInit, AfterViewInit, OnDestroy {
417417
*/
418418
@Input('aria-labelledby') ariaLabelledby: string | null = null;
419419

420+
/** Type of the button toggle. Either 'radio' or 'button'. */
421+
get type(): string {
422+
return this._isSingleSelector() ? 'radio' : 'button';
423+
}
424+
420425
/** Underlying native `button` element. */
421426
@ViewChild('button') _buttonElement: ElementRef<HTMLButtonElement>;
422427

@@ -573,6 +578,15 @@ export class MatButtonToggle implements OnInit, AfterViewInit, OnDestroy {
573578
return this.name || null;
574579
}
575580

581+
/** Get the aria-pressed attribute value. */
582+
_getAriaPressed(): boolean | null {
583+
// When the toggle stands alone, or in multiple selection mode, use aria-pressed attribute.
584+
if (!this._isSingleSelector()) {
585+
return this.checked;
586+
}
587+
return null;
588+
}
589+
576590
/** Whether the toggle is in single selection mode. */
577591
private _isSingleSelector(): boolean {
578592
return this.buttonToggleGroup && !this.buttonToggleGroup.multiple;

0 commit comments

Comments
 (0)