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/stepper): updates vertical-stepper aria roles #30577

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
5102017
fix(material/stepper): updates vertical-stepper aria roles
essjay05 Mar 3, 2025
619d094
refactor(material/stepper): remove shared mat-step-header
essjay05 Mar 4, 2025
84729bd
fix(material/stepper): update vertical-stepper aria attributes
essjay05 Mar 5, 2025
decfe57
test(material/stepper): updates role value for stepper.spec.ts
essjay05 Mar 5, 2025
8178b96
test(material/stepper): update stepper tests
essjay05 Mar 5, 2025
ee4179f
test(material/stepper): fixes failing stepper tests
essjay05 Mar 6, 2025
0c18fd4
test(material/stepper): revert updated harness files
essjay05 Mar 7, 2025
4d1a565
refactor(material/stepper): remove aria-pressed from vertical stepper
essjay05 Mar 7, 2025
bc8d475
test(material/stepper): update stepper-harness tests
essjay05 Mar 7, 2025
42d8ffb
build(material/stepper): update stepper api golden tests
essjay05 Mar 11, 2025
93b6b25
refactor(material/stepper): update code to use ng-container
essjay05 Mar 14, 2025
cf968c5
refactor(material/stepper): updates stepper to use same aria-roles pa…
essjay05 Mar 17, 2025
e311270
docs(material/stepper): updates docs to use new aria roles
essjay05 Mar 17, 2025
5fdc4b2
test(material/stepper): updates tests affected by new stepper aria ro…
essjay05 Mar 17, 2025
be3fc76
build(material/stepper): update stepper api goldens
essjay05 Mar 17, 2025
9a96219
refactor(material/stepper): update aria-attributes for horizontal ste…
essjay05 Mar 17, 2025
1690902
test(material/stepper): update tests based on new stepper aria-attrib…
essjay05 Mar 17, 2025
27226d0
build(material/stepper): update api goldens
essjay05 Mar 17, 2025
5d9f3b5
test(material/stepper): reset tests to use isSelected
essjay05 Mar 24, 2025
d89fa45
fix(material/stepper): add default role to material/stepper/step-head…
essjay05 Mar 28, 2025
55f47e7
fix(material/stepper): add data-test-index attribute to improve unit …
essjay05 Apr 1, 2025
8270c46
refactor(material/stepper): update naming convention for new index
essjay05 Apr 1, 2025
1ce35e1
build(material/stepper): update material api goldens
essjay05 Apr 1, 2025
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
2 changes: 1 addition & 1 deletion src/cdk/stepper/step-header.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {FocusableOption} from '../a11y';
@Directive({
selector: '[cdkStepHeader]',
host: {
'role': 'tab',
'role': 'button',
},
})
export class CdkStepHeader implements FocusableOption {
Expand Down
10 changes: 5 additions & 5 deletions src/cdk/stepper/stepper.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,10 @@ resetting it will call `reset` on the underlying form control which clears the v

### Accessibility
Apart from the built-in keyboard support, the stepper doesn't apply any treatment. When implementing
your own component, it is recommended that the stepper is treated as a tabbed view for accessibility
purposes by giving it a `role="tablist"`. The header of step that can be clicked to select the step
should be given `role="tab"`, and the content that can be expanded upon selection should be given
`role="tabpanel"`. Furthermore, the step header should have an `aria-selected` attribute that
reflects its selected state.
your own component, it is recommended that the stepper is treated as a grouped view for accessibility
purposes by giving it a `role="group"`. The header of step that can be clicked to select the step
should be given `role="button"`, and the content that can be expanded upon selection should be given
`role="region"`. Furthermore, the step header should have an `aria-expanded` attribute that
reflects its expanded state.

You can refer to the [Angular Material stepper](https://github.com/angular/components/tree/main/src/material/stepper) as an example of an accessible implementation.
2 changes: 1 addition & 1 deletion src/material/stepper/step-header.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import {_CdkPrivateStyleLoader, _VisuallyHiddenLoader} from '@angular/cdk/privat
host: {
'class': 'mat-step-header',
'[class]': '"mat-" + (color || "primary")',
'role': 'tab',
'role': 'button',
},
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
Expand Down
54 changes: 29 additions & 25 deletions src/material/stepper/stepper.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

@switch (orientation) {
@case ('horizontal') {
<div class="mat-horizontal-stepper-wrapper">
<div class="mat-stepper-wrapper" role="group">
<div class="mat-horizontal-stepper-header-container">
@for (step of steps; track step) {
<ng-container
Expand All @@ -27,7 +27,7 @@
<div
#animatedContainer
class="mat-horizontal-stepper-content"
role="tabpanel"
role="region"
[id]="_getStepContentId($index)"
[attr.aria-labelledby]="_getStepLabelId($index)"
[class]="'mat-horizontal-stepper-content-' + _getAnimationDirection($index)"
Expand All @@ -40,28 +40,30 @@
}

@case ('vertical') {
@for (step of steps; track step) {
<div class="mat-step">
<ng-container
[ngTemplateOutlet]="stepTemplate"
[ngTemplateOutletContext]="{step, i: $index}"/>
<div
#animatedContainer
class="mat-vertical-content-container"
[class.mat-stepper-vertical-line]="!$last"
[class.mat-vertical-content-container-active]="selectedIndex === $index"
[attr.inert]="selectedIndex === $index ? null : ''">
<div class="mat-vertical-stepper-content"
role="tabpanel"
[id]="_getStepContentId($index)"
[attr.aria-labelledby]="_getStepLabelId($index)">
<div class="mat-vertical-content">
<ng-container [ngTemplateOutlet]="step.content"/>
<div class="mat-stepper-wrapper" role="group">
@for (step of steps; track step) {
<div class="mat-step">
<ng-container
[ngTemplateOutlet]="stepTemplate"
[ngTemplateOutletContext]="{step, i: $index}"/>
<div
#animatedContainer
class="mat-vertical-content-container"
[class.mat-stepper-vertical-line]="!$last"
[class.mat-vertical-content-container-active]="selectedIndex === $index"
[attr.inert]="selectedIndex === $index ? null : ''">
<div class="mat-vertical-stepper-content"
role="region"
[id]="_getStepContentId($index)"
[attr.aria-labelledby]="_getStepLabelId($index)">
<div class="mat-vertical-content">
<ng-container [ngTemplateOutlet]="step.content"/>
</div>
</div>
</div>
</div>
</div>
}
}
</div>
}
}

Expand All @@ -74,13 +76,15 @@
(keydown)="_onKeydown($event)"
[tabIndex]="_getFocusIndex() === i ? 0 : -1"
[id]="_getStepLabelId(i)"
[attr.aria-posinset]="i + 1"
[attr.aria-setsize]="steps.length"
[attr.aria-controls]="_getStepContentId(i)"
[attr.aria-selected]="selectedIndex == i"
[attr.aria-expanded]="orientation === 'vertical' ? selectedIndex == i : undefined"
[attr.aria-current]="selectedIndex == i ? 'step' : undefined"
[attr.aria-pressed]="selectedIndex == i"
[attr.aria-label]="step.ariaLabel || null"
[attr.aria-labelledby]="(!step.ariaLabel && step.ariaLabelledby) ? step.ariaLabelledby : null"
[attr.aria-disabled]="_stepIsNavigable(i, step) ? null : true"
[attr.aria-owns]="_getStepContentId(i)"
[attr.mat-step-index]="i"
[index]="i"
[state]="_getIndicatorType(i, step.state)"
[label]="step.stepLabel || step.label"
Expand All @@ -91,4 +95,4 @@
[iconOverrides]="_iconOverrides"
[disableRipple]="disableRipple || !_stepIsNavigable(i, step)"
[color]="step.color || color"/>
</ng-template>
</ng-template>
8 changes: 4 additions & 4 deletions src/material/stepper/stepper.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,10 +228,10 @@ export class MyApp {}
<!-- example(stepper-intl) -->

### Accessibility
The stepper is treated as a tabbed view for accessibility purposes, so it is given
`role="tablist"` by default. The header of step that can be clicked to select the step
is given `role="tab"`, and the content that can be expanded upon selection is given
`role="tabpanel"`. `aria-selected` attribute of step header is automatically set based on
The stepper is treated as a grouped view for general accessibility purposes, so it is given
`role="group"` by default. The header of step that can be clicked to select the step
is given `role="button"`, and the content that can be expanded upon selection is given
`role="region"`. `aria-expanded` attribute of step header is automatically set based on
step selection change.

The stepper and each step should be given a meaningful label via `aria-label` or `aria-labelledby`.
Expand Down
24 changes: 9 additions & 15 deletions src/material/stepper/stepper.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,28 +132,31 @@ describe('MatStepper', () => {
expect(stepperComponent.selected instanceof MatStep).toBe(true);
});

it('should set the "tablist" role on stepper', () => {
const stepperEl = fixture.debugElement.query(By.css('mat-stepper'))!.nativeElement;
expect(stepperEl.getAttribute('role')).toBe('tablist');
it('should set the "group" role on the stepper', () => {
const stepperWrapper = fixture.debugElement.query(By.css('.mat-stepper-wrapper'));
expect(stepperWrapper).toBeTruthy();

const stepperEl = stepperWrapper!.nativeElement;
expect(stepperEl.getAttribute('role')).toBe('group');
});

it('should display the correct label', () => {
const stepperComponent = fixture.debugElement.query(
By.directive(MatStepper),
)!.componentInstance;
let selectedLabel = fixture.nativeElement.querySelector('[aria-selected="true"]');
let selectedLabel = fixture.nativeElement.querySelector('[aria-expanded="true"]');
expect(selectedLabel.textContent).toMatch('Step 1');

stepperComponent.selectedIndex = 2;
fixture.detectChanges();

selectedLabel = fixture.nativeElement.querySelector('[aria-selected="true"]');
selectedLabel = fixture.nativeElement.querySelector('[aria-expanded="true"]');
expect(selectedLabel.textContent).toMatch('Step 3');

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

selectedLabel = fixture.nativeElement.querySelector('[aria-selected="true"]');
selectedLabel = fixture.nativeElement.querySelector('[aria-expanded="true"]');
expect(selectedLabel.textContent).toMatch('New Label');
});

Expand Down Expand Up @@ -381,15 +384,6 @@ describe('MatStepper', () => {
animationDoneSubscription.unsubscribe();
});

it('should set the correct aria-posinset and aria-setsize', () => {
const headers = Array.from<HTMLElement>(
fixture.nativeElement.querySelectorAll('.mat-step-header'),
);

expect(headers.map(header => header.getAttribute('aria-posinset'))).toEqual(['1', '2', '3']);
expect(headers.every(header => header.getAttribute('aria-setsize') === '3')).toBe(true);
});

it('should adjust the index when removing a step before the current one', () => {
const stepperComponent: MatStepper = fixture.debugElement.query(
By.css('mat-stepper'),
Expand Down
1 change: 0 additions & 1 deletion src/material/stepper/stepper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,6 @@ export class MatStep extends CdkStep implements ErrorStateMatcher, AfterContentI
'[class.mat-stepper-animating]': '_isAnimating()',
'[style.--mat-stepper-animation-duration]': '_getAnimationDuration()',
'[attr.aria-orientation]': 'orientation',
'role': 'tablist',
},
providers: [{provide: CdkStepper, useExisting: MatStepper}],
encapsulation: ViewEncapsulation.None,
Expand Down
2 changes: 1 addition & 1 deletion src/material/stepper/testing/step-harness-filters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export enum StepperOrientation {
export interface StepHarnessFilters extends BaseHarnessFilters {
/** Only find instances whose label matches the given value. */
label?: string | RegExp;
/** Only find steps with the given selected state. */
/** Only find steps with the given pressed/expanded for Stepper state. */
selected?: boolean;
/** Only find completed steps. */
completed?: boolean;
Expand Down
6 changes: 3 additions & 3 deletions src/material/stepper/testing/step-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,10 @@ export class MatStepHarness extends ContentContainerComponentHarness<string> {
return (await this.host()).getAttribute('aria-labelledby');
}

/** Whether the step is selected. */
/** Whether the step of Stepper is pressed/expanded. */
async isSelected(): Promise<boolean> {
const host = await this.host();
return (await host.getAttribute('aria-selected')) === 'true';
return (await host.getAttribute('aria-pressed')) === 'true';
}

/** Whether the step has been filled out. */
Expand Down Expand Up @@ -92,7 +92,7 @@ export class MatStepHarness extends ContentContainerComponentHarness<string> {
}

/**
* Selects the given step by clicking on the label. The step may not be selected
* Selects the given step by clicking on the label. The step may not be selected/expanded
* if the stepper doesn't allow it (e.g. if there are validation errors).
*/
async select(): Promise<void> {
Expand Down
Loading
Loading