Skip to content

Commit 329f19a

Browse files
crisbetojelbourn
authored andcommitted
fix(stepper): use up/down arrows for navigating vertical stepper (#8920)
Currently both vertical and horizontal steppers use the left/right arrows to move focus steps. Based on the a11y guidelines (https://www.w3.org/TR/wai-aria-practices-1.1/#tabpanel) the vertical stepper should use the up/down arrows instead.
1 parent f4202bd commit 329f19a

File tree

3 files changed

+64
-43
lines changed

3 files changed

+64
-43
lines changed

src/cdk/stepper/stepper.ts

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import {
2727
OnChanges,
2828
OnDestroy
2929
} from '@angular/core';
30-
import {LEFT_ARROW, RIGHT_ARROW, ENTER, SPACE} from '@angular/cdk/keycodes';
30+
import {LEFT_ARROW, RIGHT_ARROW, DOWN_ARROW, UP_ARROW, ENTER, SPACE} from '@angular/cdk/keycodes';
3131
import {CdkStepLabel} from './step-label';
3232
import {coerceBooleanProperty} from '@angular/cdk/coercion';
3333
import {AbstractControl} from '@angular/forms';
@@ -43,6 +43,9 @@ let nextId = 0;
4343
*/
4444
export type StepContentPositionState = 'previous' | 'current' | 'next';
4545

46+
/** Possible orientation of a stepper. */
47+
export type StepperOrientation = 'horizontal' | 'vertical';
48+
4649
/** Change event emitted on selection changes. */
4750
export class StepperSelectionEvent {
4851
/** Index of the step now selected. */
@@ -182,6 +185,8 @@ export class CdkStepper implements OnDestroy {
182185
/** Used to track unique ID for each stepper component. */
183186
_groupId: number;
184187

188+
protected _orientation: StepperOrientation = 'horizontal';
189+
185190
constructor(
186191
@Optional() private _dir: Directionality,
187192
private _changeDetectorRef: ChangeDetectorRef) {
@@ -252,30 +257,30 @@ export class CdkStepper implements OnDestroy {
252257
}
253258

254259
_onKeydown(event: KeyboardEvent) {
255-
switch (event.keyCode) {
256-
case RIGHT_ARROW:
257-
if (this._layoutDirection() === 'rtl') {
258-
this._focusPreviousStep();
259-
} else {
260-
this._focusNextStep();
261-
}
262-
break;
263-
case LEFT_ARROW:
264-
if (this._layoutDirection() === 'rtl') {
265-
this._focusNextStep();
266-
} else {
267-
this._focusPreviousStep();
268-
}
269-
break;
270-
case SPACE:
271-
case ENTER:
272-
this.selectedIndex = this._focusIndex;
273-
break;
274-
default:
275-
// Return to avoid calling preventDefault on keys that are not explicitly handled.
276-
return;
260+
const keyCode = event.keyCode;
261+
262+
// Note that the left/right arrows work both in vertical and horizontal mode.
263+
if (keyCode === RIGHT_ARROW) {
264+
this._layoutDirection() === 'rtl' ? this._focusPreviousStep() : this._focusNextStep();
265+
event.preventDefault();
266+
}
267+
268+
if (keyCode === LEFT_ARROW) {
269+
this._layoutDirection() === 'rtl' ? this._focusNextStep() : this._focusPreviousStep();
270+
event.preventDefault();
271+
}
272+
273+
// Note that the up/down arrows only work in vertical mode.
274+
// See: https://www.w3.org/TR/wai-aria-practices-1.1/#tabpanel
275+
if (this._orientation === 'vertical' && (keyCode === UP_ARROW || keyCode === DOWN_ARROW)) {
276+
keyCode === UP_ARROW ? this._focusPreviousStep() : this._focusNextStep();
277+
event.preventDefault();
278+
}
279+
280+
if (keyCode === SPACE || keyCode === ENTER) {
281+
this.selectedIndex = this._focusIndex;
282+
event.preventDefault();
277283
}
278-
event.preventDefault();
279284
}
280285

281286
private _focusNextStep() {

src/lib/stepper/stepper.spec.ts

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import {Directionality} from '@angular/cdk/bidi';
2-
import {ENTER, LEFT_ARROW, RIGHT_ARROW, SPACE} from '@angular/cdk/keycodes';
2+
import {ENTER, LEFT_ARROW, RIGHT_ARROW, UP_ARROW, DOWN_ARROW, SPACE} from '@angular/cdk/keycodes';
33
import {dispatchKeyboardEvent} from '@angular/cdk/testing';
44
import {Component, DebugElement} from '@angular/core';
55
import {async, ComponentFixture, TestBed, inject} from '@angular/core/testing';
66
import {AbstractControl, AsyncValidatorFn, FormControl, FormGroup, ReactiveFormsModule,
77
ValidationErrors, Validators} from '@angular/forms';
88
import {By} from '@angular/platform-browser';
99
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
10+
import {StepperOrientation} from '@angular/cdk/stepper';
1011
import {map} from 'rxjs/operators/map';
1112
import {take} from 'rxjs/operators/take';
1213
import {Observable} from 'rxjs/Observable';
@@ -89,9 +90,9 @@ describe('MatHorizontalStepper', () => {
8990
assertCorrectStepAnimationDirection(fixture);
9091
});
9192

92-
it('should support keyboard events to move and select focus', () => {
93+
it('should support using the left/right arrows to move focus', () => {
9394
let stepHeaders = fixture.debugElement.queryAll(By.css('.mat-horizontal-stepper-header'));
94-
assertCorrectKeyboardInteraction(fixture, stepHeaders);
95+
assertCorrectKeyboardInteraction(fixture, stepHeaders, 'horizontal');
9596
});
9697

9798
it('should not set focus on header of selected step if header is not clicked', () => {
@@ -321,9 +322,14 @@ describe('MatVerticalStepper', () => {
321322
assertCorrectStepAnimationDirection(fixture);
322323
});
323324

324-
it('should support keyboard events to move and select focus', () => {
325+
it('should support using the left/right arrows to move focus', () => {
325326
let stepHeaders = fixture.debugElement.queryAll(By.css('.mat-vertical-stepper-header'));
326-
assertCorrectKeyboardInteraction(fixture, stepHeaders);
327+
assertCorrectKeyboardInteraction(fixture, stepHeaders, 'horizontal');
328+
});
329+
330+
it('should support using the up/down arrows to move focus', () => {
331+
let stepHeaders = fixture.debugElement.queryAll(By.css('.mat-vertical-stepper-header'));
332+
assertCorrectKeyboardInteraction(fixture, stepHeaders, 'vertical');
327333
});
328334

329335
it('should not set focus on header of selected step if header is not clicked', () => {
@@ -566,20 +572,23 @@ function assertCorrectStepAnimationDirection(fixture: ComponentFixture<any>, rtl
566572

567573
/** Asserts that keyboard interaction works correctly. */
568574
function assertCorrectKeyboardInteraction(fixture: ComponentFixture<any>,
569-
stepHeaders: DebugElement[]) {
575+
stepHeaders: DebugElement[],
576+
orientation: StepperOrientation) {
570577
let stepperComponent = fixture.debugElement.query(By.directive(MatStepper)).componentInstance;
578+
let nextKey = orientation === 'vertical' ? DOWN_ARROW : RIGHT_ARROW;
579+
let prevKey = orientation === 'vertical' ? UP_ARROW : LEFT_ARROW;
571580

572581
expect(stepperComponent._focusIndex).toBe(0);
573582
expect(stepperComponent.selectedIndex).toBe(0);
574583

575584
let stepHeaderEl = stepHeaders[0].nativeElement;
576-
dispatchKeyboardEvent(stepHeaderEl, 'keydown', RIGHT_ARROW);
585+
dispatchKeyboardEvent(stepHeaderEl, 'keydown', nextKey);
577586
fixture.detectChanges();
578587

579588
expect(stepperComponent._focusIndex)
580-
.toBe(1, 'Expected index of focused step to increase by 1 after RIGHT_ARROW event.');
589+
.toBe(1, 'Expected index of focused step to increase by 1 after pressing the next key.');
581590
expect(stepperComponent.selectedIndex)
582-
.toBe(0, 'Expected index of selected step to remain unchanged after RIGHT_ARROW event.');
591+
.toBe(0, 'Expected index of selected step to remain unchanged after pressing the next key.');
583592

584593
stepHeaderEl = stepHeaders[1].nativeElement;
585594
dispatchKeyboardEvent(stepHeaderEl, 'keydown', ENTER);
@@ -592,26 +601,25 @@ function assertCorrectKeyboardInteraction(fixture: ComponentFixture<any>,
592601
'Expected index of selected step to change to index of focused step after ENTER event.');
593602

594603
stepHeaderEl = stepHeaders[1].nativeElement;
595-
dispatchKeyboardEvent(stepHeaderEl, 'keydown', LEFT_ARROW);
604+
dispatchKeyboardEvent(stepHeaderEl, 'keydown', prevKey);
596605
fixture.detectChanges();
597606

598607
expect(stepperComponent._focusIndex)
599-
.toBe(0, 'Expected index of focused step to decrease by 1 after LEFT_ARROW event.');
600-
expect(stepperComponent.selectedIndex)
601-
.toBe(1, 'Expected index of selected step to remain unchanged after LEFT_ARROW event.');
608+
.toBe(0, 'Expected index of focused step to decrease by 1 after pressing the previous key.');
609+
expect(stepperComponent.selectedIndex).toBe(1,
610+
'Expected index of selected step to remain unchanged after pressing the previous key.');
602611

603612
// When the focus is on the last step and right arrow key is pressed, the focus should cycle
604613
// through to the first step.
605614
stepperComponent._focusIndex = 2;
606615
stepHeaderEl = stepHeaders[2].nativeElement;
607-
dispatchKeyboardEvent(stepHeaderEl, 'keydown', RIGHT_ARROW);
616+
dispatchKeyboardEvent(stepHeaderEl, 'keydown', nextKey);
608617
fixture.detectChanges();
609618

610-
expect(stepperComponent._focusIndex)
611-
.toBe(0,
612-
'Expected index of focused step to cycle through to index 0 after RIGHT_ARROW event.');
619+
expect(stepperComponent._focusIndex).toBe(0,
620+
'Expected index of focused step to cycle through to index 0 after pressing the next key.');
613621
expect(stepperComponent.selectedIndex)
614-
.toBe(1, 'Expected index of selected step to remain unchanged after RIGHT_ARROW event.');
622+
.toBe(1, 'Expected index of selected step to remain unchanged after pressing the next key.');
615623

616624
stepHeaderEl = stepHeaders[0].nativeElement;
617625
dispatchKeyboardEvent(stepHeaderEl, 'keydown', SPACE);

src/lib/stepper/stepper.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import {animate, state, style, transition, trigger} from '@angular/animations';
1010
import {CdkStep, CdkStepper} from '@angular/cdk/stepper';
11+
import {Directionality} from '@angular/cdk/bidi';
1112
import {
1213
AfterContentInit,
1314
Component,
@@ -21,7 +22,9 @@ import {
2122
SkipSelf,
2223
ViewChildren,
2324
ViewEncapsulation,
25+
ChangeDetectorRef,
2426
ChangeDetectionStrategy,
27+
Optional,
2528
} from '@angular/core';
2629
import {FormControl, FormGroupDirective, NgForm} from '@angular/forms';
2730
import {ErrorStateMatcher} from '@angular/material/core';
@@ -129,4 +132,9 @@ export class MatHorizontalStepper extends MatStepper { }
129132
preserveWhitespaces: false,
130133
changeDetection: ChangeDetectionStrategy.OnPush,
131134
})
132-
export class MatVerticalStepper extends MatStepper { }
135+
export class MatVerticalStepper extends MatStepper {
136+
constructor(@Optional() dir: Directionality, changeDetectorRef: ChangeDetectorRef) {
137+
super(dir, changeDetectorRef);
138+
this._orientation = 'vertical';
139+
}
140+
}

0 commit comments

Comments
 (0)