Skip to content

Commit ff10f04

Browse files
authored
fix(material/stepper): Adjust aria tab-related roles to fix violations (#31844)
1 parent a6fbdd9 commit ff10f04

File tree

5 files changed

+54
-53
lines changed

5 files changed

+54
-53
lines changed

src/material/stepper/step-header.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import {_CdkPrivateStyleLoader, _VisuallyHiddenLoader} from '@angular/cdk/privat
3636
'class': 'mat-step-header',
3737
'[class.mat-step-header-empty-label]': '_hasEmptyLabel()',
3838
'[class]': '"mat-" + (color || "primary")',
39-
'role': 'tab',
39+
'role': '', // ignore cdk role in favor of setting appropriately in html
4040
},
4141
encapsulation: ViewEncapsulation.None,
4242
changeDetection: ChangeDetectionStrategy.OnPush,

src/material/stepper/stepper.html

Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@
1111
@switch (orientation) {
1212
@case ('horizontal') {
1313
<div class="mat-horizontal-stepper-wrapper">
14-
<div class="mat-horizontal-stepper-header-container">
14+
<div
15+
aria-orientation="horizontal"
16+
class="mat-horizontal-stepper-header-container"
17+
role="tablist">
1518
@for (step of steps; track step) {
1619
<ng-container
1720
[ngTemplateOutlet]="stepTemplate"
@@ -40,28 +43,31 @@
4043
}
4144

4245
@case ('vertical') {
43-
@for (step of steps; track step) {
44-
<div class="mat-step">
45-
<ng-container
46-
[ngTemplateOutlet]="stepTemplate"
47-
[ngTemplateOutletContext]="{step}"/>
48-
<div
49-
#animatedContainer
50-
class="mat-vertical-content-container"
51-
[class.mat-stepper-vertical-line]="!$last"
52-
[class.mat-vertical-content-container-active]="selectedIndex === $index"
53-
[attr.inert]="selectedIndex === $index ? null : ''">
54-
<div class="mat-vertical-stepper-content"
55-
role="tabpanel"
56-
[id]="_getStepContentId($index)"
57-
[attr.aria-labelledby]="_getStepLabelId($index)">
58-
<div class="mat-vertical-content">
59-
<ng-container [ngTemplateOutlet]="step.content"/>
46+
<div class="mat-vertical-stepper-wrapper">
47+
@for (step of steps; track step) {
48+
<div class="mat-step">
49+
<ng-container
50+
[ngTemplateOutlet]="stepTemplate"
51+
[ngTemplateOutletContext]="{step}"/>
52+
<div
53+
#animatedContainer
54+
class="mat-vertical-content-container"
55+
[class.mat-stepper-vertical-line]="!$last"
56+
[class.mat-vertical-content-container-active]="selectedIndex === $index"
57+
[attr.inert]="selectedIndex === $index ? null : ''">
58+
<div
59+
class="mat-vertical-stepper-content"
60+
role="region"
61+
[id]="_getStepContentId($index)"
62+
[attr.aria-labelledby]="_getStepLabelId($index)">
63+
<div class="mat-vertical-content">
64+
<ng-container [ngTemplateOutlet]="step.content"/>
65+
</div>
6066
</div>
6167
</div>
6268
</div>
63-
</div>
64-
}
69+
}
70+
</div>
6571
}
6672
}
6773

@@ -74,10 +80,14 @@
7480
(keydown)="_onKeydown($event)"
7581
[tabIndex]="_getFocusIndex() === step.index() ? 0 : -1"
7682
[id]="_getStepLabelId(step.index())"
77-
[attr.aria-posinset]="step.index() + 1"
78-
[attr.aria-setsize]="steps.length"
83+
[attr.role]="orientation === 'horizontal' ? 'tab' : 'button'"
84+
[attr.aria-posinset]="orientation === 'horizontal' ? step.index() + 1 : null"
85+
[attr.aria-setsize]="orientation === 'horizontal' ? steps.length : null"
86+
[attr.aria-selected]="orientation === 'horizontal' ? step.isSelected() : null"
87+
[attr.aria-current]="orientation === 'vertical' && step.isSelected() ? 'step' : null"
88+
[attr.aria-disabled]="orientation === 'vertical' && step.isSelected() ? 'true' : null"
89+
[attr.aria-expanded]="orientation === 'vertical' ? step.isSelected() : null"
7990
[attr.aria-controls]="_getStepContentId(step.index())"
80-
[attr.aria-selected]="step.isSelected()"
8191
[attr.aria-label]="step.ariaLabel || null"
8292
[attr.aria-labelledby]="(!step.ariaLabel && step.ariaLabelledby) ? step.ariaLabelledby : null"
8393
[attr.aria-disabled]="step.isNavigable() ? null : true"

src/material/stepper/stepper.spec.ts

Lines changed: 16 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -114,25 +114,20 @@ describe('MatStepper', () => {
114114
expect(stepper.selected instanceof MatStep).toBe(true);
115115
});
116116

117-
it('should set the "tablist" role on stepper', () => {
118-
const stepperEl = fixture.debugElement.query(By.css('mat-stepper'))!.nativeElement;
119-
expect(stepperEl.getAttribute('role')).toBe('tablist');
120-
});
121-
122117
it('should display the correct label', () => {
123-
let selectedLabel = fixture.nativeElement.querySelector('[aria-selected="true"]');
118+
let selectedLabel = fixture.nativeElement.querySelector('[aria-current="step"]');
124119
expect(selectedLabel.textContent).toMatch('Step 1');
125120

126121
fixture.componentInstance.stepper.selectedIndex = 2;
127122
fixture.detectChanges();
128123

129-
selectedLabel = fixture.nativeElement.querySelector('[aria-selected="true"]');
124+
selectedLabel = fixture.nativeElement.querySelector('[aria-current="step"]');
130125
expect(selectedLabel.textContent).toMatch('Step 3');
131126

132127
fixture.componentInstance.inputLabel.set('New Label');
133128
fixture.detectChanges();
134129

135-
selectedLabel = fixture.nativeElement.querySelector('[aria-selected="true"]');
130+
selectedLabel = fixture.nativeElement.querySelector('[aria-current="step"]');
136131
expect(selectedLabel.textContent).toMatch('New Label');
137132
});
138133

@@ -342,15 +337,6 @@ describe('MatStepper', () => {
342337
animationDoneSubscription.unsubscribe();
343338
});
344339

345-
it('should set the correct aria-posinset and aria-setsize', () => {
346-
const headers = Array.from<HTMLElement>(
347-
fixture.nativeElement.querySelectorAll('.mat-step-header'),
348-
);
349-
350-
expect(headers.map(header => header.getAttribute('aria-posinset'))).toEqual(['1', '2', '3']);
351-
expect(headers.every(header => header.getAttribute('aria-setsize') === '3')).toBe(true);
352-
});
353-
354340
it('should adjust the index when removing a step before the current one', () => {
355341
const stepper = fixture.componentInstance.stepper;
356342

@@ -937,14 +923,6 @@ describe('MatStepper', () => {
937923
});
938924

939925
describe('vertical stepper', () => {
940-
it('should set the aria-orientation to "vertical"', () => {
941-
const fixture = createComponent(SimpleMatVerticalStepperApp);
942-
fixture.detectChanges();
943-
944-
const stepperEl = fixture.debugElement.query(By.css('mat-stepper'))!.nativeElement;
945-
expect(stepperEl.getAttribute('aria-orientation')).toBe('vertical');
946-
});
947-
948926
it('should support using the left/right arrows to move focus', () => {
949927
const fixture = createComponent(SimpleMatVerticalStepperApp);
950928
fixture.detectChanges();
@@ -1045,7 +1023,7 @@ describe('MatStepper', () => {
10451023
const fixture = createComponent(SimpleMatHorizontalStepperApp);
10461024
fixture.detectChanges();
10471025

1048-
const stepperEl = fixture.debugElement.query(By.css('mat-stepper'))!.nativeElement;
1026+
const stepperEl = fixture.debugElement.query(By.css('[role="tablist"]'))!.nativeElement;
10491027
expect(stepperEl.getAttribute('aria-orientation')).toBe('horizontal');
10501028
});
10511029

@@ -1066,6 +1044,18 @@ describe('MatStepper', () => {
10661044
assertArrowKeyInteractionInRtl(fixture, stepHeaders);
10671045
});
10681046

1047+
it('should set the correct aria-posinset and aria-setsize', () => {
1048+
const fixture = createComponent(SimpleMatHorizontalStepperApp);
1049+
fixture.detectChanges();
1050+
1051+
const headers = Array.from<HTMLElement>(
1052+
fixture.nativeElement.querySelectorAll('.mat-step-header'),
1053+
);
1054+
1055+
expect(headers.map(header => header.getAttribute('aria-posinset'))).toEqual(['1', '2', '3']);
1056+
expect(headers.every(header => header.getAttribute('aria-setsize') === '3')).toBe(true);
1057+
});
1058+
10691059
it('should maintain the correct navigation order when a step is added later on', () => {
10701060
const fixture = createComponent(HorizontalStepperWithDelayedStep);
10711061
fixture.detectChanges();

src/material/stepper/stepper.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,6 @@ export class MatStep extends CdkStep implements ErrorStateMatcher, AfterContentI
130130
'[class.mat-stepper-header-position-bottom]': 'headerPosition === "bottom"',
131131
'[class.mat-stepper-animating]': '_isAnimating()',
132132
'[style.--mat-stepper-animation-duration]': '_getAnimationDuration()',
133-
'[attr.aria-orientation]': 'orientation',
134-
'role': 'tablist',
135133
},
136134
providers: [{provide: CdkStepper, useExisting: MatStepper}],
137135
encapsulation: ViewEncapsulation.None,

src/material/stepper/testing/step-harness.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,10 @@ export class MatStepHarness extends ContentContainerComponentHarness<string> {
6464
/** Whether the step is selected. */
6565
async isSelected(): Promise<boolean> {
6666
const host = await this.host();
67-
return (await host.getAttribute('aria-selected')) === 'true';
67+
return (
68+
(await host.getAttribute('aria-selected')) === 'true' ||
69+
(await host.getAttribute('aria-current')) === 'step'
70+
);
6871
}
6972

7073
/** Whether the step has been filled out. */

0 commit comments

Comments
 (0)