From add81b016630b044be3d2c19b6175b77f5cec6ca Mon Sep 17 00:00:00 2001 From: Ji Won Shin Date: Mon, 21 Aug 2017 14:57:38 -0700 Subject: [PATCH] Refactor md-step-header and md-step-content + optional step change --- src/cdk/stepper/public_api.ts | 16 +-- src/cdk/stepper/step-content.ts | 61 ++++++++++ src/cdk/stepper/step-header.ts | 71 +++++++++++ src/cdk/stepper/step-icon.ts | 41 ------- src/cdk/stepper/step-label-container.ts | 28 ----- src/cdk/stepper/stepper.ts | 4 +- src/demo-app/stepper/stepper-demo.html | 14 +-- src/demo-app/stepper/stepper-demo.ts | 12 +- src/lib/stepper/_stepper-theme.scss | 20 ++-- src/lib/stepper/index.ts | 12 +- src/lib/stepper/step-content.html | 3 + src/lib/stepper/step-content.scss | 6 + src/lib/stepper/step-content.ts | 30 +++++ src/lib/stepper/step-header.html | 20 ++++ src/lib/stepper/step-header.scss | 66 +++++++++++ src/lib/stepper/step-header.ts | 29 +++++ src/lib/stepper/step-icon.html | 5 - src/lib/stepper/step-icon.spec.ts | 83 ------------- src/lib/stepper/step-icon.ts | 20 ---- src/lib/stepper/step-label-container.html | 7 -- src/lib/stepper/step-label-container.ts | 21 ---- src/lib/stepper/stepper-horizontal.html | 34 +++--- src/lib/stepper/stepper-vertical.html | 34 +++--- src/lib/stepper/stepper.scss | 58 +-------- src/lib/stepper/stepper.spec.ts | 137 ++++++++++++++++++---- src/lib/stepper/stepper.ts | 4 +- 26 files changed, 474 insertions(+), 362 deletions(-) create mode 100644 src/cdk/stepper/step-content.ts create mode 100644 src/cdk/stepper/step-header.ts delete mode 100644 src/cdk/stepper/step-icon.ts delete mode 100644 src/cdk/stepper/step-label-container.ts create mode 100644 src/lib/stepper/step-content.html create mode 100644 src/lib/stepper/step-content.scss create mode 100644 src/lib/stepper/step-content.ts create mode 100644 src/lib/stepper/step-header.html create mode 100644 src/lib/stepper/step-header.scss create mode 100644 src/lib/stepper/step-header.ts delete mode 100644 src/lib/stepper/step-icon.html delete mode 100644 src/lib/stepper/step-icon.spec.ts delete mode 100644 src/lib/stepper/step-icon.ts delete mode 100644 src/lib/stepper/step-label-container.html delete mode 100644 src/lib/stepper/step-label-container.ts diff --git a/src/cdk/stepper/public_api.ts b/src/cdk/stepper/public_api.ts index 8989f3ee7df4..341d90ce7c89 100644 --- a/src/cdk/stepper/public_api.ts +++ b/src/cdk/stepper/public_api.ts @@ -11,20 +11,20 @@ import {CdkStepper, CdkStep} from './stepper'; import {CommonModule} from '@angular/common'; import {CdkStepLabel} from './step-label'; import {CdkStepperNext, CdkStepperPrevious} from './stepper-button'; -import {CdkStepIcon} from './step-icon'; -import {CdkStepLabelContainer} from './step-label-container'; +import {CdkStepHeader} from './step-header'; +import {CdkStepContent} from './step-content'; @NgModule({ imports: [CommonModule], - exports: [CdkStep, CdkStepper, CdkStepLabel, CdkStepperNext, CdkStepperPrevious, CdkStepIcon, - CdkStepLabelContainer], - declarations: [CdkStep, CdkStepper, CdkStepLabel, CdkStepperNext, CdkStepperPrevious, CdkStepIcon, - CdkStepLabelContainer] + exports: [CdkStep, CdkStepper, CdkStepLabel, CdkStepperNext, CdkStepperPrevious, CdkStepHeader, + CdkStepContent], + declarations: [CdkStep, CdkStepper, CdkStepLabel, CdkStepperNext, CdkStepperPrevious, + CdkStepHeader, CdkStepContent] }) export class CdkStepperModule {} export * from './stepper'; export * from './step-label'; export * from './stepper-button'; -export * from './step-icon'; -export * from './step-label-container'; +export * from './step-header'; +export * from './step-content'; diff --git a/src/cdk/stepper/step-content.ts b/src/cdk/stepper/step-content.ts new file mode 100644 index 000000000000..a399da31ea31 --- /dev/null +++ b/src/cdk/stepper/step-content.ts @@ -0,0 +1,61 @@ +/** + * @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 {Directive, Input} from '@angular/core'; +import {CdkStep, CdkStepper} from './stepper'; +import {coerceBooleanProperty, coerceNumberProperty} from '@angular/cdk/coercion'; + +@Directive({ + selector: 'cdkStepContent', + host: { + 'role': 'tabpanel', + '[attr.id]': 'contentId', + '[attr.aria-labelledby]': 'labelId', + '[attr.aria-expanded]': 'selectedIndex == index', + } +}) +export class CdkStepContent { + /** Whether the orientation of stepper is horizontal. */ + @Input() + get horizontal() { return this._horizontal; } + set horizontal(value: any) { + this._horizontal = coerceBooleanProperty(value); + } + private _horizontal: boolean; + + /** Unique label ID of step header. */ + @Input() + labelId: string; + + /** Unique content ID of step content. */ + @Input() + contentId: string; + + /** Index of the given step. */ + @Input() + get index() { return this._index; } + set index(value: any) { + this._index = coerceNumberProperty(value); + } + private _index: number; + + /** Index of selected step in stepper. */ + @Input() + get selectedIndex() { return this._selectedIndex; } + set selectedIndex(value: any) { + this._selectedIndex = coerceNumberProperty(value); + } + private _selectedIndex: number; + + /** Returns the step at the index position in stepper. */ + get step(): CdkStep { + return this._stepper._steps.toArray()[this._index]; + } + + constructor(private _stepper: CdkStepper) { } +} diff --git a/src/cdk/stepper/step-header.ts b/src/cdk/stepper/step-header.ts new file mode 100644 index 000000000000..36edecaaa089 --- /dev/null +++ b/src/cdk/stepper/step-header.ts @@ -0,0 +1,71 @@ +/** + * @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 {Directive, Input} from '@angular/core'; +import {CdkStep, CdkStepper} from './stepper'; +import {coerceBooleanProperty, coerceNumberProperty} from '@angular/cdk/coercion'; + +@Directive({ + selector: 'cdkStepHeader', + host: { + 'role': 'tab', + '[attr.id]': 'labelId', + '[attr.aria-controls]': 'contentId', + '[attr.aria-selected]': 'selected' + } +}) +export class CdkStepHeader { + /** Whether the orientation of stepper is horizontal. */ + @Input() + get horizontal() { return this._horizontal; } + set horizontal(value: any) { + this._horizontal = coerceBooleanProperty(value); + } + private _horizontal: boolean; + + /** Unique label ID of step header. */ + @Input() + labelId: string; + + /** Unique content ID of step content. */ + @Input() + contentId: string; + + /** Index of the given step. */ + @Input() + get index() { return this._index; } + set index(value: any) { + this._index = coerceNumberProperty(value); + } + private _index: number; + + /** Whether the given step is selected. */ + @Input() + get selected() { return this._selected; } + set selected(value: any) { + this._selected = coerceBooleanProperty(value); + } + private _selected: boolean; + + + /** Returns the step at the index position in stepper. */ + get step(): CdkStep { + return this._stepper._steps.toArray()[this._index]; + } + + constructor(private _stepper: CdkStepper) { } + + /** Returns the type of icon to be displayed. */ + _getIndicatorType(): 'number' | 'edit' | 'done' { + if (!this.step.completed || this.selected) { + return 'number'; + } else { + return this.step.editable ? 'edit' : 'done'; + } + } +} diff --git a/src/cdk/stepper/step-icon.ts b/src/cdk/stepper/step-icon.ts deleted file mode 100644 index 7fb2ba3e2d66..000000000000 --- a/src/cdk/stepper/step-icon.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** - * @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 {Directive, Input} from '@angular/core'; -import {CdkStep} from './stepper'; - -@Directive({ - selector: 'cdkStepIcon' -}) -export class CdkStepIcon { - /** Step of the icon to be displayed. */ - @Input() - step: CdkStep; - - /** Whether the step of the icon to be displayed is active. */ - @Input() - selected: boolean; - - /** Index of the step. */ - @Input() - index: number; - - /** Whether the user has touched the step that is not selected. */ - get notTouched() { - return this._getIndicatorType() == 'number' && !this.selected; - } - - /** Returns the type of icon to be displayed. */ - _getIndicatorType(): 'number' | 'edit' | 'done' { - if (!this.step.completed || this.selected) { - return 'number'; - } else { - return this.step.editable ? 'edit' : 'done'; - } - } -} diff --git a/src/cdk/stepper/step-label-container.ts b/src/cdk/stepper/step-label-container.ts deleted file mode 100644 index cec0f4320dee..000000000000 --- a/src/cdk/stepper/step-label-container.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * @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 {Directive, Input} from '@angular/core'; -import {CdkStep} from './stepper'; - -@Directive({ - selector: 'cdkStepLabelContainer' -}) -export class CdkStepLabelContainer { - /** Step of the label to be displayed. */ - @Input() - step: CdkStep; - - /** Whether the step of label to be displayed is selected. */ - @Input() - selected: boolean; - - /** Whether the label to be displayed is active. */ - get active() { - return this.step.completed || this.selected; - } -} diff --git a/src/cdk/stepper/stepper.ts b/src/cdk/stepper/stepper.ts index 02b47b0ad2cb..5b5751a1fbcf 100644 --- a/src/cdk/stepper/stepper.ts +++ b/src/cdk/stepper/stepper.ts @@ -164,7 +164,7 @@ export class CdkStepper { _focusIndex: number = 0; /** Used to track unique ID for each stepper component. */ - private _groupId: number; + _groupId: number; constructor() { this._groupId = nextId++; @@ -241,7 +241,7 @@ export class CdkStepper { const stepsArray = this._steps.toArray(); stepsArray[this._selectedIndex].interacted = true; if (this._linear) { - return stepsArray.slice(0, index).some(step => step.stepControl.invalid && !step.optional); + return stepsArray.slice(0, index).some(step => step.stepControl.invalid); } return false; } diff --git a/src/demo-app/stepper/stepper-demo.html b/src/demo-app/stepper/stepper-demo.html index aae8e205710f..cb96c1f281b2 100644 --- a/src/demo-app/stepper/stepper-demo.html +++ b/src/demo-app/stepper/stepper-demo.html @@ -21,11 +21,11 @@

Linear Vertical Stepper Demo using a single form

-
Fill out your phone number
+
Fill out your email address
- - This field is required + + The input is invalid.
@@ -62,12 +62,12 @@

Linear Horizontal Stepper Demo using a different form for each step

- -
+ + Fill out your phone number - - This field is required + + The input is invalid
diff --git a/src/demo-app/stepper/stepper-demo.ts b/src/demo-app/stepper/stepper-demo.ts index d220a99273a2..2c7ed6f7ce92 100644 --- a/src/demo-app/stepper/stepper-demo.ts +++ b/src/demo-app/stepper/stepper-demo.ts @@ -1,6 +1,8 @@ import {Component} from '@angular/core'; import {FormBuilder, FormGroup, Validators} from '@angular/forms'; +const EMAIL_REGEX = /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/; + @Component({ moduleId: module.id, selector: 'stepper-demo', @@ -13,7 +15,7 @@ export class StepperDemo { isNonEditable = false; nameFormGroup: FormGroup; - phoneFormGroup: FormGroup; + emailFormGroup: FormGroup; steps = [ {label: 'Confirm your name', content: 'Last name, First name.'}, @@ -35,8 +37,8 @@ export class StepperDemo { lastNameFormCtrl: ['', Validators.required], }), this._formBuilder.group({ - phoneFormCtrl: ['', Validators.required], - }) + emailFormCtrl: ['', Validators.pattern(EMAIL_REGEX)] + }), ]) }); @@ -45,8 +47,8 @@ export class StepperDemo { lastNameCtrl: ['', Validators.required], }); - this.phoneFormGroup = this._formBuilder.group({ - phoneCtrl: ['', Validators.required] + this.emailFormGroup = this._formBuilder.group({ + emailCtrl: ['', Validators.pattern(EMAIL_REGEX)] }); } } diff --git a/src/lib/stepper/_stepper-theme.scss b/src/lib/stepper/_stepper-theme.scss index a2cd4cfb23b3..b7f0467251e1 100644 --- a/src/lib/stepper/_stepper-theme.scss +++ b/src/lib/stepper/_stepper-theme.scss @@ -7,18 +7,19 @@ $background: map-get($theme, background); $primary: map-get($theme, primary); - .mat-horizontal-stepper-header, .mat-vertical-stepper-header { - + .mat-stepper-header { &:focus, &:hover { background-color: mat-color($background, hover); } + } + .mat-step-header { - .mat-stepper-label-active { + .mat-step-label-active { color: mat-color($foreground, text); } - .mat-stepper-label-inactive, + .mat-step-label-inactive, .mat-step-optional { color: mat-color($foreground, disabled-text); } @@ -28,14 +29,9 @@ color: mat-color($primary, default-contrast); } - &[aria-selected='false'] { - .mat-stepper-label { - color: mat-color($foreground, disabled-text); - } - - .mat-step-icon-not-touched { - background-color: mat-color($foreground, disabled-text); - } + .mat-step-icon-not-touched { + background-color: mat-color($foreground, disabled-text); + color: mat-color($primary, default-contrast); } } diff --git a/src/lib/stepper/index.ts b/src/lib/stepper/index.ts index ebfbd989a2b6..b5dbb19d1332 100644 --- a/src/lib/stepper/index.ts +++ b/src/lib/stepper/index.ts @@ -18,8 +18,8 @@ import {MdCommonModule} from '../core'; import {MdStepLabel} from './step-label'; import {MdStepperNext, MdStepperPrevious} from './stepper-button'; import {MdIconModule} from '../icon/index'; -import {MdStepIcon} from './step-icon'; -import {MdStepLabelContainer} from './step-label-container'; +import {MdStepHeader} from './step-header'; +import {MdStepContent} from './step-content'; @NgModule({ imports: [ @@ -31,9 +31,9 @@ import {MdStepLabelContainer} from './step-label-container'; MdIconModule ], exports: [MdCommonModule, MdHorizontalStepper, MdVerticalStepper, MdStep, MdStepLabel, MdStepper, - MdStepperNext, MdStepperPrevious, MdStepIcon, MdStepLabelContainer], + MdStepperNext, MdStepperPrevious, MdStepHeader, MdStepContent], declarations: [MdHorizontalStepper, MdVerticalStepper, MdStep, MdStepLabel, MdStepper, - MdStepperNext, MdStepperPrevious, MdStepIcon, MdStepLabelContainer], + MdStepperNext, MdStepperPrevious, MdStepHeader, MdStepContent], }) export class MdStepperModule {} @@ -42,5 +42,5 @@ export * from './stepper-vertical'; export * from './step-label'; export * from './stepper'; export * from './stepper-button'; -export * from './step-icon'; -export * from './step-label-container'; +export * from './step-header'; +export * from './step-content'; diff --git a/src/lib/stepper/step-content.html b/src/lib/stepper/step-content.html new file mode 100644 index 000000000000..93615940c98e --- /dev/null +++ b/src/lib/stepper/step-content.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/lib/stepper/step-content.scss b/src/lib/stepper/step-content.scss new file mode 100644 index 000000000000..784ea496d427 --- /dev/null +++ b/src/lib/stepper/step-content.scss @@ -0,0 +1,6 @@ +$mat-stepper-side-gap: 24px !default; + +.mat-vertical-content { + padding: 0 $mat-stepper-side-gap $mat-stepper-side-gap $mat-stepper-side-gap; + overflow: hidden; +} diff --git a/src/lib/stepper/step-content.ts b/src/lib/stepper/step-content.ts new file mode 100644 index 000000000000..8fdd34ab9dc1 --- /dev/null +++ b/src/lib/stepper/step-content.ts @@ -0,0 +1,30 @@ +/** + * @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 {Component} from '@angular/core'; +import {CdkStepContent} from '@angular/cdk/stepper'; +import {MdStepper} from './stepper'; + +@Component({ + selector: 'md-step-content, mat-step-content', + templateUrl: 'step-content.html', + styleUrls: ['step-content.css'], + host: { + '[class.mat-vertical-stepper-content]': '!horizontal', + '[class.mat-horizontal-stepper-content]': 'horizontal', + 'role': 'tabpanel', + '[attr.id]': 'contentId', + '[attr.aria-labelledby]': 'labelId', + '[attr.aria-expanded]': 'selectedIndex == index', + }, +}) +export class MdStepContent extends CdkStepContent { + constructor(mdStepper: MdStepper) { + super(mdStepper); + } +} diff --git a/src/lib/stepper/step-header.html b/src/lib/stepper/step-header.html new file mode 100644 index 000000000000..bbdd2a974f1a --- /dev/null +++ b/src/lib/stepper/step-header.html @@ -0,0 +1,20 @@ +
+
+ {{index + 1}} + create + done +
+
+ + + + +
{{step.label}}
+ +
Optional
+
+
diff --git a/src/lib/stepper/step-header.scss b/src/lib/stepper/step-header.scss new file mode 100644 index 000000000000..5ac7201681bd --- /dev/null +++ b/src/lib/stepper/step-header.scss @@ -0,0 +1,66 @@ +$mat-horizontal-stepper-header-height: 72px !default; +$mat-stepper-label-header-height: 24px !default; +$mat-stepper-label-min-width: 50px !default; +$mat-stepper-side-gap: 24px !default; +$mat-vertical-stepper-content-margin: 36px !default; +$mat-stepper-line-gap: 8px !default; +$mat-step-optional-font-size: 12px; + +:host { + display: flex; +} + +.mat-horizontal-stepper-header { + display: flex; + height: $mat-horizontal-stepper-header-height; + overflow: hidden; + align-items: center; + padding: 0 $mat-stepper-side-gap; + + .mat-step-icon, + .mat-step-icon-not-touched { + margin-right: $mat-stepper-line-gap; + flex: none; + } +} + +.mat-vertical-stepper-header { + display: flex; + align-items: center; + padding: $mat-stepper-side-gap; + max-height: $mat-stepper-label-header-height; + + .mat-step-icon, + .mat-step-icon-not-touched { + margin-right: $mat-vertical-stepper-content-margin - $mat-stepper-side-gap; + } +} + +.mat-step-optional { + font-size: $mat-step-optional-font-size; +} + +.mat-step-icon, +.mat-step-icon-not-touched { + border-radius: 50%; + height: $mat-stepper-label-header-height; + width: $mat-stepper-label-header-height; + text-align: center; + line-height: $mat-stepper-label-header-height; + display: inline-block; +} + +.mat-step-label-active, +.mat-step-label-inactive { + display: inline-block; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + min-width: $mat-stepper-label-min-width; + vertical-align: middle; +} + +.text-label { + text-overflow: ellipsis; + overflow: hidden; +} diff --git a/src/lib/stepper/step-header.ts b/src/lib/stepper/step-header.ts new file mode 100644 index 000000000000..d7612c6ccf68 --- /dev/null +++ b/src/lib/stepper/step-header.ts @@ -0,0 +1,29 @@ +/** + * @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 {Component} from '@angular/core'; +import {CdkStepHeader} from '@angular/cdk/stepper'; +import {MdStepper} from './stepper'; + +@Component({ + selector: 'md-step-header, mat-step-header', + templateUrl: 'step-header.html', + styleUrls: ['step-header.css'], + host: { + 'class': 'mat-step-header', + 'role': 'tab', + '[attr.id]': 'labelId', + '[attr.aria-controls]': 'contentId', + '[attr.aria-selected]': 'selected' + } +}) +export class MdStepHeader extends CdkStepHeader { + constructor(mdStepper: MdStepper) { + super(mdStepper); + } +} diff --git a/src/lib/stepper/step-icon.html b/src/lib/stepper/step-icon.html deleted file mode 100644 index fb0c5029efbd..000000000000 --- a/src/lib/stepper/step-icon.html +++ /dev/null @@ -1,5 +0,0 @@ -
- {{index + 1}} - create - done -
diff --git a/src/lib/stepper/step-icon.spec.ts b/src/lib/stepper/step-icon.spec.ts deleted file mode 100644 index 261047352de4..000000000000 --- a/src/lib/stepper/step-icon.spec.ts +++ /dev/null @@ -1,83 +0,0 @@ -import {MdStepperModule} from './index'; -import {async, ComponentFixture, TestBed} from '@angular/core/testing'; -import {Component, ViewChild} from '@angular/core'; -import {MdStep, MdStepper} from './stepper'; -import {MdStepIcon} from './step-icon'; -import {By} from '@angular/platform-browser'; - -describe('MdStepIcon', () => { - - beforeEach(async(() => { - TestBed.configureTestingModule({ - imports: [MdStepperModule], - declarations: [SimpleStepIconApp], - providers: [ - {provide: MdStepper, useClass: MdStepper} - ] - }); - TestBed.compileComponents(); - })); - - describe('setting icon', () => { - let stepIconComponent: MdStepIcon; - let fixture: ComponentFixture; - let testComponent: SimpleStepIconApp; - - beforeEach(() => { - fixture = TestBed.createComponent(SimpleStepIconApp); - fixture.detectChanges(); - - stepIconComponent = fixture.debugElement.query(By.css('md-step-icon')).componentInstance; - testComponent = fixture.componentInstance; - }); - - it('should set done icon if step is non-editable and completed', () => { - stepIconComponent.selected = true; - fixture.detectChanges(); - - expect(stepIconComponent._getIndicatorType()).toBe('number'); - - testComponent.mdStep.completed = true; - testComponent.mdStep.editable = false; - stepIconComponent.selected = false; - fixture.detectChanges(); - - expect(stepIconComponent._getIndicatorType()).toBe('done'); - }); - - it('should set create icon if step is editable and completed', () => { - stepIconComponent.selected = true; - fixture.detectChanges(); - - expect(stepIconComponent._getIndicatorType()).toBe('number'); - - testComponent.mdStep.completed = true; - testComponent.mdStep.editable = true; - stepIconComponent.selected = false; - fixture.detectChanges(); - - expect(stepIconComponent._getIndicatorType()).toBe('edit'); - }); - - it('should set "mat-step-icon-not-touched" class if the step ', () => { - let stepIconEl = fixture.debugElement.query(By.css('md-step-icon')).nativeElement; - - testComponent.mdStep.completed = false; - stepIconComponent.selected = false; - fixture.detectChanges(); - - expect(stepIconComponent._getIndicatorType()).toBe('number'); - expect(stepIconEl.classList).toContain('mat-step-icon-not-touched'); - }); - }); -}); - -@Component({ - template: ` - step - - ` -}) -class SimpleStepIconApp { - @ViewChild(MdStep) mdStep: MdStep; -} diff --git a/src/lib/stepper/step-icon.ts b/src/lib/stepper/step-icon.ts deleted file mode 100644 index db06752bbc04..000000000000 --- a/src/lib/stepper/step-icon.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * @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 {Component} from '@angular/core'; -import {CdkStepIcon} from '@angular/cdk/stepper'; - -@Component({ - selector: 'md-step-icon, mat-step-icon', - templateUrl: 'step-icon.html', - host: { - 'class': 'mat-step-icon', - '[class.mat-step-icon-not-touched]': 'notTouched' - } -}) -export class MdStepIcon extends CdkStepIcon { } diff --git a/src/lib/stepper/step-label-container.html b/src/lib/stepper/step-label-container.html deleted file mode 100644 index 51f933e84d52..000000000000 --- a/src/lib/stepper/step-label-container.html +++ /dev/null @@ -1,7 +0,0 @@ - - - - -
{{step.label}}
- -
Optional
\ No newline at end of file diff --git a/src/lib/stepper/step-label-container.ts b/src/lib/stepper/step-label-container.ts deleted file mode 100644 index 2e38931cf14f..000000000000 --- a/src/lib/stepper/step-label-container.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * @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 {Component} from '@angular/core'; -import {CdkStepLabelContainer} from '@angular/cdk/stepper'; - -@Component({ - selector: 'md-step-label-container, mat-step-label-container', - templateUrl: 'step-label-container.html', - host: { - 'class': 'mat-step-label-container', - '[class.mat-step-label-active]': 'active', - '[class.mat-step-label-inactive]': '!active' - } -}) -export class MdStepLabelContainer extends CdkStepLabelContainer { } diff --git a/src/lib/stepper/stepper-horizontal.html b/src/lib/stepper/stepper-horizontal.html index 640c4c1c4141..6f8c0e7b40f8 100644 --- a/src/lib/stepper/stepper-horizontal.html +++ b/src/lib/stepper/stepper-horizontal.html @@ -1,28 +1,24 @@
-
-
- -
+
diff --git a/src/lib/stepper/stepper-vertical.html b/src/lib/stepper/stepper-vertical.html index 40e2d4f49fb1..53e84e8ac1a6 100644 --- a/src/lib/stepper/stepper-vertical.html +++ b/src/lib/stepper/stepper-vertical.html @@ -1,25 +1,21 @@
- diff --git a/src/lib/stepper/stepper.scss b/src/lib/stepper/stepper.scss index 1262ebad1aea..cf13ec8f9234 100644 --- a/src/lib/stepper/stepper.scss +++ b/src/lib/stepper/stepper.scss @@ -1,35 +1,17 @@ @import '../core/style/variables'; -$mat-horizontal-stepper-header-height: 72px !default; -$mat-stepper-label-header-height: 24px !default; -$mat-stepper-label-min-width: 50px !default; $mat-stepper-side-gap: 24px !default; $mat-vertical-stepper-content-margin: 36px !default; $mat-stepper-line-width: 1px !default; $mat-stepper-line-gap: 8px !default; -$mat-step-optional-font-size: 12px; :host { display: block; } -.mat-step-label-container { - display: inline-block; - white-space: nowrap; +.mat-stepper-header { overflow: hidden; - // TODO(jwshin): text-overflow does not work as expected. - text-overflow: ellipsis; - min-width: $mat-stepper-label-min-width; - vertical-align: middle; -} - -.mat-step-icon { - border-radius: 50%; - height: $mat-stepper-label-header-height; - width: $mat-stepper-label-header-height; - text-align: center; - line-height: $mat-stepper-label-header-height; - display: inline-block; + outline: none; } .mat-horizontal-stepper-header-container { @@ -38,36 +20,6 @@ $mat-step-optional-font-size: 12px; align-items: center; } -.mat-horizontal-stepper-header { - display: flex; - height: $mat-horizontal-stepper-header-height; - overflow: hidden; - align-items: center; - outline: none; - padding: 0 $mat-stepper-side-gap; - - .mat-step-icon { - margin-right: $mat-stepper-line-gap; - flex: none; - } -} - -.mat-vertical-stepper-header { - display: flex; - align-items: center; - padding: $mat-stepper-side-gap; - outline: none; - max-height: $mat-stepper-label-header-height; - - .mat-step-icon { - margin-right: $mat-vertical-stepper-content-margin - $mat-stepper-side-gap; - } -} - -.mat-step-optional { - font-size: $mat-step-optional-font-size; -} - .mat-stepper-horizontal-line { border-top-width: $mat-stepper-line-width; border-top-style: solid; @@ -79,6 +31,7 @@ $mat-step-optional-font-size: 12px; .mat-horizontal-stepper-content { overflow: hidden; + display: flex; &[aria-expanded='false'] { height: 0; @@ -107,13 +60,10 @@ $mat-step-optional-font-size: 12px; } .mat-vertical-stepper-content { + display: flex; overflow: hidden; } -.mat-vertical-content { - padding: 0 $mat-stepper-side-gap $mat-stepper-side-gap $mat-stepper-side-gap; -} - .mat-step:last-child { .mat-vertical-content-container { border: none; diff --git a/src/lib/stepper/stepper.spec.ts b/src/lib/stepper/stepper.spec.ts index 1cea4a39ad16..333a8d06016f 100644 --- a/src/lib/stepper/stepper.spec.ts +++ b/src/lib/stepper/stepper.spec.ts @@ -11,6 +11,8 @@ import {dispatchKeyboardEvent} from '@angular/cdk/testing'; import {ENTER, LEFT_ARROW, RIGHT_ARROW, SPACE} from '@angular/cdk/keycodes'; import {MdStepper} from './stepper'; +const EMAIL_REGEX = /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/; + describe('MdHorizontalStepper', () => { beforeEach(async(() => { TestBed.configureTestingModule({ @@ -77,14 +79,20 @@ describe('MdHorizontalStepper', () => { }); it('should not set focus on header of selected step if header is not clicked', () => { - let stepHeaderEl = fixture.debugElement - .queryAll(By.css('.mat-horizontal-stepper-header'))[1].nativeElement; - assertStepHeaderFocusNotCalled(stepHeaderEl, stepperComponent, fixture); + assertStepHeaderFocusNotCalled(stepperComponent, fixture); }); it('should only be able to return to a previous step if it is editable', () => { assertEditableStepChange(stepperComponent, fixture); }); + + it('should set create icon if step is editable and completed', () => { + assertCorrectStepIcon(stepperComponent, fixture, true, 'edit'); + }); + + it('should set done icon if step is not editable and is completed', () => { + assertCorrectStepIcon(stepperComponent, fixture, false, 'done'); + }); }); describe('linear horizontal stepper', () => { @@ -117,9 +125,7 @@ describe('MdHorizontalStepper', () => { }); it('should not focus step header upon click if it is not able to be selected', () => { - let stepHeaderEl = fixture.debugElement - .queryAll(By.css('.mat-horizontal-stepper-header'))[1].nativeElement; - assertStepHeaderBlurred(stepHeaderEl, fixture); + assertStepHeaderBlurred(fixture); }); it('should be able to move to next step even when invalid if current step is optional', () => { @@ -195,14 +201,20 @@ describe('MdVerticalStepper', () => { }); it('should not set focus on header of selected step if header is not clicked', () => { - let stepHeaderEl = fixture.debugElement - .queryAll(By.css('.mat-vertical-stepper-header'))[1].nativeElement; - assertStepHeaderFocusNotCalled(stepHeaderEl, stepperComponent, fixture); + assertStepHeaderFocusNotCalled(stepperComponent, fixture); }); it('should only be able to return to a previous step if it is editable', () => { assertEditableStepChange(stepperComponent, fixture); }); + + it('should set create icon if step is editable and completed', () => { + assertCorrectStepIcon(stepperComponent, fixture, true, 'edit'); + }); + + it('should set done icon if step is not editable and is completed', () => { + assertCorrectStepIcon(stepperComponent, fixture, false, 'done'); + }); }); describe('linear vertical stepper', () => { @@ -236,9 +248,7 @@ describe('MdVerticalStepper', () => { }); it('should not focus step header upon click if it is not able to be selected', () => { - let stepHeaderEl = fixture.debugElement - .queryAll(By.css('.mat-vertical-stepper-header'))[1].nativeElement; - assertStepHeaderBlurred(stepHeaderEl, fixture); + assertStepHeaderBlurred(fixture); }); it('should be able to move to next step even when invalid if current step is optional', () => { @@ -436,9 +446,10 @@ function assertCorrectKeyboardInteraction(stepperComponent: MdStepper, } /** Asserts that step selection change using stepper buttons does not focus step header. */ -function assertStepHeaderFocusNotCalled(stepHeaderEl: HTMLElement, - stepperComponent: MdStepper, - fixture: ComponentFixture) { +function assertStepHeaderFocusNotCalled(stepperComponent: MdStepper, + fixture: ComponentFixture) { + let stepHeaderEl = fixture.debugElement + .queryAll(By.css('.mat-stepper-header'))[1].nativeElement; let nextButtonNativeEl = fixture.debugElement .queryAll(By.directive(MdStepperNext))[0].nativeElement; spyOn(stepHeaderEl, 'focus'); @@ -478,7 +489,9 @@ function assertLinearStepperValidity(stepHeaderEl: HTMLElement, } /** Asserts that step header focus is blurred if the step cannot be selected upon header click. */ -function assertStepHeaderBlurred(stepHeaderEl: HTMLElement, fixture: ComponentFixture) { +function assertStepHeaderBlurred(fixture: ComponentFixture) { + let stepHeaderEl = fixture.debugElement + .queryAll(By.css('.mat-stepper-header'))[1].nativeElement; spyOn(stepHeaderEl, 'blur'); stepHeaderEl.click(); fixture.detectChanges(); @@ -505,22 +518,62 @@ function assertEditableStepChange(stepperComponent: MdStepper, expect(stepperComponent.selectedIndex).toBe(0); } -/** Asserts that it is only possible to skip a step in linear stepper if the step is optional. */ +/** + * Asserts that it is possible to skip an optional step in linear stepper if there is no input + * or the input is valid. + */ function assertOptionalStepValidity(stepperComponent: MdStepper, testComponent: LinearMdHorizontalStepperApp | LinearMdVerticalStepperApp, fixture: ComponentFixture) { - expect(testComponent.oneGroup.get('oneCtrl')!.value).toBe(''); - expect(testComponent.oneGroup.get('oneCtrl')!.valid).toBe(false); - expect(testComponent.oneGroup.valid).toBe(false); - expect(stepperComponent.selectedIndex).toBe(0); + testComponent.oneGroup.get('oneCtrl')!.setValue('input'); + testComponent.twoGroup.get('twoCtrl')!.setValue('input'); + stepperComponent.selectedIndex = 2; + fixture.detectChanges(); + + expect(stepperComponent.selectedIndex).toBe(2); + expect(testComponent.threeGroup.get('threeCtrl')!.valid).toBe(true); + + let nextButtonNativeEl = fixture.debugElement + .queryAll(By.directive(MdStepperNext))[2].nativeElement; + nextButtonNativeEl.click(); + fixture.detectChanges(); + + expect(stepperComponent.selectedIndex) + .toBe(3, 'Expected selectedIndex to change when optional step input is empty.'); + + stepperComponent.selectedIndex = 2; + testComponent.threeGroup.get('threeCtrl')!.setValue('input'); + nextButtonNativeEl.click(); + fixture.detectChanges(); + + expect(testComponent.threeGroup.get('threeCtrl')!.valid).toBe(false); + expect(stepperComponent.selectedIndex) + .toBe(2, 'Expected selectedIndex to remain unchanged when optional step input is invalid.'); + + testComponent.threeGroup.get('threeCtrl')!.setValue('123@gmail.com'); + nextButtonNativeEl.click(); + fixture.detectChanges(); + + expect(testComponent.threeGroup.get('threeCtrl')!.valid).toBe(true); + expect(stepperComponent.selectedIndex) + .toBe(3, 'Expected selectedIndex to change when optional step input is valid.'); +} - stepperComponent._steps.toArray()[0].optional = true; +/** Asserts that step header set the correct icon depending on the state of step. */ +function assertCorrectStepIcon(stepperComponent: MdStepper, + fixture: ComponentFixture, + isEditable: boolean, + icon: String) { + let stepHeaderComponent = fixture.debugElement + .queryAll(By.css('md-step-header'))[0].componentInstance; let nextButtonNativeEl = fixture.debugElement .queryAll(By.directive(MdStepperNext))[0].nativeElement; + expect(stepHeaderComponent._getIndicatorType()).toBe('number'); + stepperComponent._steps.toArray()[0].editable = isEditable; nextButtonNativeEl.click(); fixture.detectChanges(); - expect(stepperComponent.selectedIndex).toBe(1); + expect(stepHeaderComponent._getIndicatorType()).toBe(icon); } @Component({ @@ -583,12 +636,28 @@ class SimpleMdHorizontalStepperApp {
+ +
+ Step two + + + +
+ + +
+
+
+ + Done + ` }) class LinearMdHorizontalStepperApp { oneGroup: FormGroup; twoGroup: FormGroup; + threeGroup: FormGroup; ngOnInit() { this.oneGroup = new FormGroup({ @@ -597,6 +666,9 @@ class LinearMdHorizontalStepperApp { this.twoGroup = new FormGroup({ twoCtrl: new FormControl('', Validators.required) }); + this.threeGroup = new FormGroup({ + threeCtrl: new FormControl('', Validators.pattern(EMAIL_REGEX)) + }); } } @@ -660,12 +732,28 @@ class SimpleMdVerticalStepperApp {
+ +
+ Step two + + + +
+ + +
+
+
+ + Done + ` }) class LinearMdVerticalStepperApp { oneGroup: FormGroup; twoGroup: FormGroup; + threeGroup: FormGroup; ngOnInit() { this.oneGroup = new FormGroup({ @@ -674,5 +762,8 @@ class LinearMdVerticalStepperApp { this.twoGroup = new FormGroup({ twoCtrl: new FormControl('', Validators.required) }); + this.threeGroup = new FormGroup({ + threeCtrl: new FormControl('', Validators.pattern(EMAIL_REGEX)) + }); } } diff --git a/src/lib/stepper/stepper.ts b/src/lib/stepper/stepper.ts index 5a0dd890fa89..a68241050e8d 100644 --- a/src/lib/stepper/stepper.ts +++ b/src/lib/stepper/stepper.ts @@ -20,7 +20,7 @@ import { QueryList, SkipSelf, ViewChildren -}from '@angular/core'; +} from '@angular/core'; import {MdStepLabel} from './step-label'; import { defaultErrorStateMatcher, @@ -66,7 +66,7 @@ export class MdStep extends CdkStep implements ErrorOptions { export class MdStepper extends CdkStepper implements ErrorOptions { /** The list of step headers of the steps in the stepper. */ - @ViewChildren('stepHeader') _stepHeader: QueryList; + @ViewChildren('stepperHeader') _stepHeader: QueryList; /** Steps that the stepper holds. */ @ContentChildren(MdStep) _steps: QueryList;