Skip to content

Commit

Permalink
fix(stepper): unable to internationalize labels
Browse files Browse the repository at this point in the history
Adds a provider that allows for labels inside the stepper to be internationalized.
  • Loading branch information
crisbeto committed Sep 29, 2017
1 parent 53c42a4 commit 4728389
Show file tree
Hide file tree
Showing 7 changed files with 91 additions and 17 deletions.
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 @@ -13,6 +13,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>

23 changes: 21 additions & 2 deletions src/lib/stepper/step-header.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,17 @@
*/

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


@Component({
Expand All @@ -23,7 +32,9 @@ import {MatStepLabel} from './step-label';
encapsulation: ViewEncapsulation.None,
preserveWhitespaces: false,
})
export class MatStepHeader {
export class MatStepHeader implements OnDestroy {
private _intlSubscription: Subscription;

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

Expand Down Expand Up @@ -62,6 +73,14 @@ export class MatStepHeader {
}
private _optional: boolean;

constructor(public _intl: MatStepperIntl, changeDetectorRef: ChangeDetectorRef) {
this._intlSubscription = _intl.changes.subscribe(() => changeDetectorRef.markForCheck());
}

ngOnDestroy() {
this._intlSubscription.unsubscribe();
}

/** Returns string label of given step if it is a text label. */
_stringLabel(): string | null {
return this.label instanceof MatStepLabel ? null : this.label;
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 @@ -17,6 +17,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 @@ -41,5 +42,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`.
21 changes: 18 additions & 3 deletions src/lib/stepper/stepper.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +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, MatStepper, MatVerticalStepper} from './stepper';
import {MatStepperNext, MatStepperPrevious} from './stepper-button';

import {MatStepperIntl} from './stepper-intl';

const VALID_REGEX = /valid/;

Expand Down Expand Up @@ -96,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 @@ -684,7 +699,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

0 comments on commit 4728389

Please sign in to comment.