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 Jan 31, 2024
1 parent 2455a42 commit fcd8a94
Show file tree
Hide file tree
Showing 5 changed files with 54 additions and 33 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
27 changes: 13 additions & 14 deletions src/material/button-toggle/button-toggle.html
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
<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 #button 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()">
<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
Expand Down
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
16 changes: 15 additions & 1 deletion 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,6 +417,11 @@ 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>;

Expand Down Expand Up @@ -573,6 +578,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

0 comments on commit fcd8a94

Please sign in to comment.