Skip to content

fix(stepper): unable to internationalize labels #7122

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

Merged
merged 1 commit into from
Oct 3, 2017
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/lib/stepper/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ export * from './step-label';
export * from './stepper';
export * from './stepper-button';
export * from './step-header';

export * from './stepper-intl';
2 changes: 1 addition & 1 deletion src/lib/stepper/step-header.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@
<!-- It there is no label template, fall back to the text label. -->
<div class="mat-step-text-label" *ngIf="_stringLabel()">{{label}}</div>

<div class="mat-step-optional" *ngIf="optional">Optional</div>
<div class="mat-step-optional" *ngIf="optional">{{_intl.optionalLabel}}</div>
</div>

20 changes: 18 additions & 2 deletions src/lib/stepper/step-header.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,18 @@

import {FocusMonitor} from '@angular/cdk/a11y';
import {coerceBooleanProperty, coerceNumberProperty} from '@angular/cdk/coercion';
import {Component, Input, ViewEncapsulation, ElementRef, OnDestroy, Renderer2} from '@angular/core';
import {
Component,
Input,
ViewEncapsulation,
ChangeDetectorRef,
OnDestroy,
ElementRef,
Renderer2,
} from '@angular/core';
import {MatStepLabel} from './step-label';
import {MatStepperIntl} from './stepper-intl';
import {Subscription} from 'rxjs/Subscription';


@Component({
Expand All @@ -25,6 +35,8 @@ import {MatStepLabel} from './step-label';
preserveWhitespaces: false,
})
export class MatStepHeader implements OnDestroy {
private _intlSubscription: Subscription;

/** Icon for the given step. */
@Input() icon: string;

Expand Down Expand Up @@ -64,13 +76,17 @@ export class MatStepHeader implements OnDestroy {
private _optional: boolean;

constructor(
public _intl: MatStepperIntl,
private _focusMonitor: FocusMonitor,
private _element: ElementRef,
renderer: Renderer2) {
renderer: Renderer2,
changeDetectorRef: ChangeDetectorRef) {
_focusMonitor.monitor(_element.nativeElement, renderer, true);
this._intlSubscription = _intl.changes.subscribe(() => changeDetectorRef.markForCheck());
}

ngOnDestroy() {
this._intlSubscription.unsubscribe();
this._focusMonitor.stopMonitoring(this._element.nativeElement);
}

Expand Down
24 changes: 24 additions & 0 deletions src/lib/stepper/stepper-intl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {Injectable} from '@angular/core';
import {Subject} from 'rxjs/Subject';


/** Stepper data that is required for internationalization. */
@Injectable()
export class MatStepperIntl {
/**
* Stream that emits whenever the labels here are changed. Use this to notify
* components if the labels have changed after initialization.
*/
changes: Subject<void> = new Subject<void>();

/** Label that is rendered below optional steps. */
optionalLabel = 'Optional';
}
2 changes: 2 additions & 0 deletions src/lib/stepper/stepper-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {MatStepHeader} from './step-header';
import {MatStepLabel} from './step-label';
import {MatHorizontalStepper, MatStep, MatStepper, MatVerticalStepper} from './stepper';
import {MatStepperNext, MatStepperPrevious} from './stepper-button';
import {MatStepperIntl} from './stepper-intl';


@NgModule({
Expand All @@ -44,5 +45,6 @@ import {MatStepperNext, MatStepperPrevious} from './stepper-button';
],
declarations: [MatHorizontalStepper, MatVerticalStepper, MatStep, MatStepLabel, MatStepper,
MatStepperNext, MatStepperPrevious, MatStepHeader],
providers: [MatStepperIntl],
})
export class MatStepperModule {}
34 changes: 24 additions & 10 deletions src/lib/stepper/stepper.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ that drives a stepped workflow. Material stepper extends the CDK stepper and has
styling.

### Stepper variants
There are two stepper components: `mat-horizontal-stepper` and `mat-vertical-stepper`. They
can be used the same way. The only difference is the orientation of stepper.
There are two stepper components: `mat-horizontal-stepper` and `mat-vertical-stepper`. They
can be used the same way. The only difference is the orientation of stepper.
`mat-horizontal-stepper` selector can be used to create a horizontal stepper, and
`mat-vertical-stepper` can be used to create a vertical stepper. `mat-step` components need to be
placed inside either one of the two stepper components.
Expand All @@ -26,7 +26,7 @@ If a step's label is only text, then the `label` attribute can be used.
</mat-vertical-stepper>
```

For more complex labels, add a template with the `matStepLabel` directive inside the
For more complex labels, add a template with the `matStepLabel` directive inside the
`mat-step`.
```html
<mat-vertical-stepper>
Expand All @@ -49,22 +49,22 @@ There are two button directives to support navigation between different steps:
<button mat-button matStepperNext>Next</button>
</div>
</mat-step>
</mat-horizontal-stepper>
</mat-horizontal-stepper>
```

### Linear stepper
The `linear` attribute can be set on `mat-horizontal-stepper` and `mat-vertical-stepper` to create
a linear stepper that requires the user to complete previous steps before proceeding
to following steps. For each `mat-step`, the `stepControl` attribute can be set to the top level
`AbstractControl` that is used to check the validity of the step.
`AbstractControl` that is used to check the validity of the step.

There are two possible approaches. One is using a single form for stepper, and the other is
using a different form for each step.

#### Using a single form
When using a single form for the stepper, `matStepperPrevious` and `matStepperNext` have to be
set to `type="button"` in order to prevent submission of the form before all steps
are completed.
are completed.

```html
<form [formGroup]="formGroup">
Expand All @@ -83,7 +83,7 @@ are completed.
</div>
</mat-step>
...
</mat-horizontal-stepper>
</mat-horizontal-stepper>
</form>
```

Expand All @@ -106,11 +106,11 @@ are completed.

#### Optional step
If completion of a step in linear stepper is not required, then the `optional` attribute can be set
on `mat-step`.
on `mat-step`.

#### Editable step
By default, steps are editable, which means users can return to previously completed steps and
edit their responses. `editable="true"` can be set on `mat-step` to change the default.
edit their responses. `editable="true"` can be set on `mat-step` to change the default.

#### Completed step
By default, the `completed` attribute of a step returns `true` if the step is valid (in case of
Expand All @@ -124,11 +124,25 @@ this default `completed` behavior by setting the `completed` attribute as needed
- <kbd>TAB</kbd>: Focuses the next tabbable element
- <kbd>TAB</kbd>+<kbd>SHIFT</kbd>: Focuses the previous tabbable element

### Localizing labels
Labels used by the stepper are provided through `MatStepperIntl`. Localization of these messages
can be done by providing a subclass with translated values in your application root module.

```ts
@NgModule({
imports: [MatStepperModule],
providers: [
{provide: MatStepperIntl, useClass: MyIntl},
],
})
export class MyApp {}
```

### 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 and `aria-expanded` attribute of
step content 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`.
The stepper and each step should be given a meaningful label via `aria-label` or `aria-labelledby`.
20 changes: 18 additions & 2 deletions src/lib/stepper/stepper.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import {Directionality} from '@angular/cdk/bidi';
import {ENTER, LEFT_ARROW, RIGHT_ARROW, SPACE} from '@angular/cdk/keycodes';
import {dispatchKeyboardEvent} from '@angular/cdk/testing';
import {Component, DebugElement} from '@angular/core';
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {async, ComponentFixture, TestBed, inject} from '@angular/core/testing';
import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';
import {By} from '@angular/platform-browser';
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
import {MatStepperModule} from './index';
import {MatHorizontalStepper, MatStep, MatStepper, MatVerticalStepper} from './stepper';
import {MatStepperNext, MatStepperPrevious} from './stepper-button';
import {MatStepperIntl} from './stepper-intl';

const VALID_REGEX = /valid/;

Expand Down Expand Up @@ -95,6 +96,21 @@ describe('MatHorizontalStepper', () => {
it('should set done icon if step is not editable and is completed', () => {
assertCorrectStepIcon(fixture, false, 'done');
});

it('should re-render when the i18n labels change',
inject([MatStepperIntl], (intl: MatStepperIntl) => {
const header = fixture.debugElement.queryAll(By.css('mat-step-header'))[2].nativeElement;
const optionalLabel = header.querySelector('.mat-step-optional');

expect(optionalLabel).toBeTruthy();
expect(optionalLabel.textContent).toBe('Optional');

intl.optionalLabel = 'Valgfri';
intl.changes.next();
fixture.detectChanges();

expect(optionalLabel.textContent).toBe('Valgfri');
}));
});

describe('RTL', () => {
Expand Down Expand Up @@ -686,7 +702,7 @@ function assertCorrectStepIcon(fixture: ComponentFixture<any>,
<button mat-button matStepperNext>Next</button>
</div>
</mat-step>
<mat-step [label]="inputLabel">
<mat-step [label]="inputLabel" optional>
Content 3
<div>
<button mat-button matStepperPrevious>Back</button>
Expand Down