From 6e3bbcb6ef90a37145204bc8775626679f655b91 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Tue, 3 Oct 2017 20:48:53 +0200 Subject: [PATCH] fix(stepper): unable to internationalize labels (#7122) --- src/lib/stepper/public-api.ts | 2 +- src/lib/stepper/step-header.html | 2 +- src/lib/stepper/step-header.ts | 20 ++++++++++++++++-- src/lib/stepper/stepper-intl.ts | 24 ++++++++++++++++++++++ src/lib/stepper/stepper-module.ts | 2 ++ src/lib/stepper/stepper.md | 34 ++++++++++++++++++++++--------- src/lib/stepper/stepper.spec.ts | 20 ++++++++++++++++-- 7 files changed, 88 insertions(+), 16 deletions(-) create mode 100644 src/lib/stepper/stepper-intl.ts diff --git a/src/lib/stepper/public-api.ts b/src/lib/stepper/public-api.ts index 3fa16f569ac9..302085124235 100644 --- a/src/lib/stepper/public-api.ts +++ b/src/lib/stepper/public-api.ts @@ -11,4 +11,4 @@ export * from './step-label'; export * from './stepper'; export * from './stepper-button'; export * from './step-header'; - +export * from './stepper-intl'; diff --git a/src/lib/stepper/step-header.html b/src/lib/stepper/step-header.html index 580a382e4b22..e0115b4a0f50 100644 --- a/src/lib/stepper/step-header.html +++ b/src/lib/stepper/step-header.html @@ -15,6 +15,6 @@
{{label}}
-
Optional
+
{{_intl.optionalLabel}}
diff --git a/src/lib/stepper/step-header.ts b/src/lib/stepper/step-header.ts index 90fdadcefb8d..42f11b755ee1 100644 --- a/src/lib/stepper/step-header.ts +++ b/src/lib/stepper/step-header.ts @@ -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({ @@ -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; @@ -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); } diff --git a/src/lib/stepper/stepper-intl.ts b/src/lib/stepper/stepper-intl.ts new file mode 100644 index 000000000000..2975cdb8006a --- /dev/null +++ b/src/lib/stepper/stepper-intl.ts @@ -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 = new Subject(); + + /** Label that is rendered below optional steps. */ + optionalLabel = 'Optional'; +} diff --git a/src/lib/stepper/stepper-module.ts b/src/lib/stepper/stepper-module.ts index 02f8b4f65867..970e93b45099 100644 --- a/src/lib/stepper/stepper-module.ts +++ b/src/lib/stepper/stepper-module.ts @@ -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({ @@ -44,5 +45,6 @@ import {MatStepperNext, MatStepperPrevious} from './stepper-button'; ], declarations: [MatHorizontalStepper, MatVerticalStepper, MatStep, MatStepLabel, MatStepper, MatStepperNext, MatStepperPrevious, MatStepHeader], + providers: [MatStepperIntl], }) export class MatStepperModule {} diff --git a/src/lib/stepper/stepper.md b/src/lib/stepper/stepper.md index e41d89b68c39..808a25b769c8 100644 --- a/src/lib/stepper/stepper.md +++ b/src/lib/stepper/stepper.md @@ -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. @@ -26,7 +26,7 @@ If a step's label is only text, then the `label` attribute can be used. ``` -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 @@ -49,14 +49,14 @@ There are two button directives to support navigation between different steps: - + ``` ### 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. @@ -64,7 +64,7 @@ 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
@@ -83,7 +83,7 @@ are completed. ... - +
``` @@ -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 @@ -124,6 +124,20 @@ this default `completed` behavior by setting the `completed` attribute as needed - TAB: Focuses the next tabbable element - TAB+SHIFT: 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 @@ -131,4 +145,4 @@ is given `role="tab"`, and the content that can be expanded upon selection is gi `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`. \ No newline at end of file +The stepper and each step should be given a meaningful label via `aria-label` or `aria-labelledby`. diff --git a/src/lib/stepper/stepper.spec.ts b/src/lib/stepper/stepper.spec.ts index 8f07d66f4ef6..7413e001946f 100644 --- a/src/lib/stepper/stepper.spec.ts +++ b/src/lib/stepper/stepper.spec.ts @@ -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/; @@ -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', () => { @@ -686,7 +702,7 @@ function assertCorrectStepIcon(fixture: ComponentFixture, - + Content 3