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): incorrect navigation order when steps are added later on #23541

Merged
merged 1 commit into from Sep 10, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
27 changes: 26 additions & 1 deletion src/cdk/stepper/stepper.ts
Expand Up @@ -272,6 +272,9 @@ export class CdkStepper implements AfterContentInit, AfterViewInit, OnDestroy {
/** The list of step headers of the steps in the stepper. */
@ContentChildren(CdkStepHeader, {descendants: true}) _stepHeader: QueryList<CdkStepHeader>;

/** List of step headers sorted based on their DOM order. */
private _sortedHeaders = new QueryList<CdkStepHeader>();

/** Whether the validity of previous steps should be checked or not. */
@Input()
get linear(): boolean {
Expand Down Expand Up @@ -362,10 +365,31 @@ export class CdkStepper implements AfterContentInit, AfterViewInit, OnDestroy {
}

ngAfterViewInit() {
// If the step headers are defined outside of the `ngFor` that renders the steps, like in the
// Material stepper, they won't appear in the `QueryList` in the same order as they're
// rendered in the DOM which will lead to incorrect keyboard navigation. We need to sort
// them manually to ensure that they're correct. Alternatively, we can change the Material
// template to inline the headers in the `ngFor`, but that'll result in a lot of
// code duplciation. See #23539.
this._stepHeader.changes
.pipe(startWith(this._stepHeader), takeUntil(this._destroyed))
.subscribe((headers: QueryList<CdkStepHeader>) => {
this._sortedHeaders.reset(headers.toArray().sort((a, b) => {
const documentPosition =
a._elementRef.nativeElement.compareDocumentPosition(b._elementRef.nativeElement);

// `compareDocumentPosition` returns a bitmask so we have to use a bitwise operator.
// https://developer.mozilla.org/en-US/docs/Web/API/Node/compareDocumentPosition
// tslint:disable-next-line:no-bitwise
return documentPosition & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1;
}));
this._sortedHeaders.notifyOnChanges();
});

// Note that while the step headers are content children by default, any components that
// extend this one might have them as view children. We initialize the keyboard handling in
// AfterViewInit so we're guaranteed for both view and content children to be defined.
this._keyManager = new FocusKeyManager<FocusableOption>(this._stepHeader)
this._keyManager = new FocusKeyManager<FocusableOption>(this._sortedHeaders)
.withWrap()
.withHomeAndEnd()
.withVerticalOrientation(this._orientation === 'vertical');
Expand Down Expand Up @@ -393,6 +417,7 @@ export class CdkStepper implements AfterContentInit, AfterViewInit, OnDestroy {

ngOnDestroy() {
this.steps.destroy();
this._sortedHeaders.destroy();
this._destroyed.next();
this._destroyed.complete();
}
Expand Down
23 changes: 23 additions & 0 deletions src/material/stepper/stepper.spec.ts
Expand Up @@ -1019,6 +1019,16 @@ describe('MatStepper', () => {
assertArrowKeyInteractionInRtl(fixture, stepHeaders);
});

it('should maintain the correct navigation order when a step is added later on', () => {
const fixture = createComponent(HorizontalStepperWithDelayedStep);
fixture.detectChanges();
fixture.componentInstance.renderSecondStep = true;
fixture.detectChanges();

const stepHeaders = fixture.debugElement.queryAll(By.css('.mat-horizontal-stepper-header'));
assertCorrectKeyboardInteraction(fixture, stepHeaders, 'horizontal');
});

it('should reverse arrow key focus when switching into RTL after init', () => {
const fixture = createComponent(SimpleMatHorizontalStepperApp);
fixture.detectChanges();
Expand Down Expand Up @@ -2032,3 +2042,16 @@ class StepperWithStaticOutOfBoundsIndex {
class StepperWithLazyContent {
selectedIndex = 0;
}

@Component({
template: `
<mat-stepper>
<mat-step label="Step 1">Content 1</mat-step>
<mat-step label="Step 2" *ngIf="renderSecondStep">Content 2</mat-step>
<mat-step label="Step 3">Content 3</mat-step>
</mat-stepper>
`
})
class HorizontalStepperWithDelayedStep {
renderSecondStep = false;
}