-
Notifications
You must be signed in to change notification settings - Fork 6.8k
feat(stepper): Create MAT_STEPPER_GLOBAL_OPTIONS InjectionToken #11457
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
Changes from 17 commits
8712df9
48b3f1f
4adb591
beda45f
1cc2f91
4d671f6
820dc54
b3d5134
fdbf799
ebf59a4
f1e3b02
9f48437
726ce77
ed4e121
4e952fe
ea4ded3
b3d11ef
e5bceb3
f2ccadc
8cd1910
10fc780
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -31,6 +31,7 @@ import { | |
TemplateRef, | ||
ViewChild, | ||
ViewEncapsulation, | ||
InjectionToken, | ||
} from '@angular/core'; | ||
import {DOCUMENT} from '@angular/common'; | ||
import {AbstractControl} from '@angular/forms'; | ||
|
@@ -65,6 +66,37 @@ export class StepperSelectionEvent { | |
previouslySelectedStep: CdkStep; | ||
} | ||
|
||
/** The state of each step. */ | ||
export type StepState = 'number' | 'edit' | 'done' | 'error' | string; | ||
|
||
/** Enum to represent the different states of the steps. */ | ||
export const STEP_STATE = { | ||
arodr967 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
NUMBER: 'number', | ||
EDIT: 'edit', | ||
DONE: 'done', | ||
ERROR: 'error' | ||
}; | ||
|
||
/** InjectionToken that can be used to specify the global stepper options. */ | ||
export const MAT_STEPPER_GLOBAL_OPTIONS = | ||
new InjectionToken<StepperOptions>('mat-stepper-global-options'); | ||
|
||
/** Configurable options for stepper. */ | ||
export interface StepperOptions { | ||
/** | ||
* Whether the stepper should display an error state or not. | ||
* Default behavior is assumed to be false. | ||
*/ | ||
showError?: boolean; | ||
|
||
/** | ||
* Whether the stepper should display the default indicator type | ||
* or not. | ||
* Default behavior is assumed to be true. | ||
*/ | ||
displayDefaultIndicatorType?: boolean; | ||
} | ||
|
||
@Component({ | ||
moduleId: module.id, | ||
selector: 'cdk-step', | ||
|
@@ -74,6 +106,10 @@ export class StepperSelectionEvent { | |
changeDetection: ChangeDetectionStrategy.OnPush, | ||
}) | ||
export class CdkStep implements OnChanges { | ||
private _stepperOptions: StepperOptions; | ||
_showError: boolean; | ||
arodr967 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
_displayDefaultIndicatorType: boolean; | ||
|
||
/** Template for step label if it exists. */ | ||
@ContentChild(CdkStepLabel) stepLabel: CdkStepLabel; | ||
|
||
|
@@ -89,6 +125,9 @@ export class CdkStep implements OnChanges { | |
/** Plain text label of the step. */ | ||
@Input() label: string; | ||
|
||
/** Error message to display when there's an error. */ | ||
@Input() errorMessage: string; | ||
|
||
/** Aria label for the tab. */ | ||
@Input('aria-label') ariaLabel: string; | ||
|
||
|
@@ -98,6 +137,9 @@ export class CdkStep implements OnChanges { | |
*/ | ||
@Input('aria-labelledby') ariaLabelledby: string; | ||
|
||
/** State of the step. */ | ||
@Input() state: StepState; | ||
|
||
/** Whether the user can return to this step once it has been marked as complted. */ | ||
@Input() | ||
get editable(): boolean { return this._editable; } | ||
|
@@ -117,18 +159,39 @@ export class CdkStep implements OnChanges { | |
/** Whether step is marked as completed. */ | ||
@Input() | ||
get completed(): boolean { | ||
return this._customCompleted == null ? this._defaultCompleted() : this._customCompleted; | ||
return this._customCompleted == null ? this._getDefaultCompleted() : this._customCompleted; | ||
} | ||
set completed(value: boolean) { | ||
this._customCompleted = coerceBooleanProperty(value); | ||
} | ||
private _customCompleted: boolean | null = null; | ||
|
||
private _defaultCompleted() { | ||
private _getDefaultCompleted() { | ||
return this.stepControl ? this.stepControl.valid && this.interacted : this.interacted; | ||
} | ||
|
||
constructor(@Inject(forwardRef(() => CdkStepper)) private _stepper: CdkStepper) { } | ||
/** Whether step has an error. */ | ||
@Input() | ||
get hasError(): boolean { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should this be an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. I will also be creating a |
||
return this._customError || this._getDefaultError(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should this be |
||
} | ||
set hasError(value: boolean) { | ||
this._customError = coerceBooleanProperty(value); | ||
} | ||
private _customError: boolean | null = null; | ||
|
||
private _getDefaultError() { | ||
return this.stepControl && this.stepControl.invalid && this.interacted; | ||
} | ||
|
||
/** @breaking-change 8.0.0 remove the `?` after `stepperOptions` */ | ||
constructor( | ||
@Inject(forwardRef(() => CdkStepper)) private _stepper: CdkStepper, | ||
@Optional() @Inject(MAT_STEPPER_GLOBAL_OPTIONS) stepperOptions?: StepperOptions) { | ||
this._stepperOptions = stepperOptions ? stepperOptions : {}; | ||
this._displayDefaultIndicatorType = this._stepperOptions.displayDefaultIndicatorType !== false; | ||
this._showError = !!this._stepperOptions.showError; | ||
} | ||
|
||
/** Selects this step component. */ | ||
select(): void { | ||
|
@@ -143,6 +206,10 @@ export class CdkStep implements OnChanges { | |
this._customCompleted = false; | ||
} | ||
|
||
if (this._customError != null) { | ||
arodr967 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
this._customError = false; | ||
} | ||
|
||
if (this.stepControl) { | ||
this.stepControl.reset(); | ||
} | ||
|
@@ -301,15 +368,46 @@ export class CdkStepper implements AfterViewInit, OnDestroy { | |
} | ||
|
||
/** Returns the type of icon to be displayed. */ | ||
_getIndicatorType(index: number): 'number' | 'edit' | 'done' { | ||
_getIndicatorType(index: number, state: StepState = STEP_STATE.NUMBER): StepState { | ||
const step = this._steps.toArray()[index]; | ||
if (!step.completed || this._selectedIndex == index) { | ||
return 'number'; | ||
const isCurrentStep = this._isCurrentStep(index); | ||
|
||
return step._displayDefaultIndicatorType | ||
? this._getDefaultIndicatorLogic(step, isCurrentStep) | ||
: this._getGuidelineLogic(step, isCurrentStep, state); | ||
} | ||
|
||
private _getDefaultIndicatorLogic(step: CdkStep, isCurrentStep: boolean): StepState { | ||
if (step._showError && step.hasError && !isCurrentStep) { | ||
return STEP_STATE.ERROR; | ||
} else if (!step.completed || isCurrentStep) { | ||
return STEP_STATE.NUMBER; | ||
} else { | ||
return step.editable ? STEP_STATE.EDIT : STEP_STATE.DONE; | ||
} | ||
} | ||
|
||
private _getGuidelineLogic( | ||
step: CdkStep, | ||
isCurrentStep: boolean, | ||
state: StepState = STEP_STATE.NUMBER): StepState { | ||
if (step._showError && step.hasError && !isCurrentStep) { | ||
return STEP_STATE.ERROR; | ||
arodr967 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} else if (step.completed && !isCurrentStep) { | ||
return STEP_STATE.DONE; | ||
} else if (step.completed && isCurrentStep) { | ||
return state; | ||
} else if (step.editable && isCurrentStep) { | ||
return STEP_STATE.EDIT; | ||
} else { | ||
return step.editable ? 'edit' : 'done'; | ||
return state; | ||
} | ||
} | ||
|
||
private _isCurrentStep(index: number) { | ||
return this._selectedIndex === index; | ||
} | ||
|
||
/** Returns the index of the currently-focused step header. */ | ||
_getFocusIndex() { | ||
return this._keyManager ? this._keyManager.activeItemIndex : this._selectedIndex; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,6 +6,7 @@ | |
$foreground: map-get($theme, foreground); | ||
$background: map-get($theme, background); | ||
$primary: map-get($theme, primary); | ||
$warn: map-get($theme, warn); | ||
|
||
.mat-step-header { | ||
&.cdk-keyboard-focused, | ||
|
@@ -20,18 +21,37 @@ | |
} | ||
|
||
.mat-step-icon { | ||
color: mat-color($primary, default-contrast); | ||
} | ||
|
||
.mat-step-icon-selected { | ||
background-color: mat-color($primary); | ||
color: mat-color($primary, default-contrast); | ||
} | ||
|
||
/** @breaking-change 8.0.0 remove `mat-step-icon-not-touched` */ | ||
.mat-step-icon-not-touched { | ||
background-color: mat-color($foreground, disabled-text); | ||
color: mat-color($primary, default-contrast); | ||
} | ||
|
||
.mat-step-icon-not-selected { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rather than having a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Initially, it was like that but I was having problems overriding the styles. |
||
background-color: mat-color($foreground, disabled-text); | ||
color: mat-color($primary, default-contrast); | ||
} | ||
|
||
.mat-step-icon-error { | ||
background-color: transparent; | ||
color: mat-color($warn); | ||
} | ||
|
||
.mat-step-label.mat-step-label-active { | ||
color: mat-color($foreground, text); | ||
} | ||
|
||
.mat-step-label.mat-step-label-error { | ||
arodr967 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
color: mat-color($warn); | ||
} | ||
} | ||
|
||
.mat-stepper-horizontal, .mat-stepper-vertical { | ||
|
@@ -59,6 +79,14 @@ | |
}; | ||
} | ||
|
||
.mat-step-sub-label-error { | ||
font-weight: normal; | ||
} | ||
|
||
.mat-step-label-error { | ||
font-size: mat-font-size($config, body-2); | ||
} | ||
|
||
.mat-step-label-selected { | ||
font: { | ||
size: mat-font-size($config, body-2); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,10 @@ | ||
<div class="mat-step-header-ripple" mat-ripple [matRippleTrigger]="_getHostElement()"></div> | ||
<div [class.mat-step-icon]="state !== 'number' || selected" | ||
<!-- @breaking-change 8.0.0 remove `mat-step-icon-not-touched` --> | ||
<div class="mat-step-icon" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @arodr967 Hmm ok, it looks like maybe the touched class wasn't the issue after all. I'm still seeing screenshot diffs where the pencil is changed from blue to gray. Looking a little deeper I think the issue may be related to the <div class="mat-step-icon mat-step-icon-state-{{state}}"
[class.mat-step-icon-selected]="selected"
[ngSwitch]="state">
... To me this seems nicer anyways, because each state will get its own class that users can target. If making the pencil gray by default was intentional, I can just add CSS in google to target the Sorry for all the back and forth on this, sorting out the issues in google3 turned out to be more difficult than I anticipated There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No worries! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why would it not work for custom states? As long as they provide a string it should just insert it into the class name right? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes but I wouldn't be able to predefine the classes since there could be many different types. Or would that be something that the developer could do on their side? Define the class? 🤔 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You don't need to target styles at all of them, you might want to for the predefined states like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Simple way to fix the problem you're describing with the gray pencil is by just adding additional logic to
Let me know what you think! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would rather change the classes to be like I suggested. It also makes the meaning of the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Okay, I'll see what I can do in that case, thanks! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry took a while to get this done, been really busy! I have pushed the changes. Let me know what you think.
Otherwise, the styles wouldn't get applied correctly! |
||
[class.mat-step-icon-selected]="selected || state == 'done'" | ||
[class.mat-step-icon-not-touched]="state == 'number' && !selected" | ||
arodr967 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
[class.mat-step-icon-not-selected]="!selected && state != 'done'" | ||
[class.mat-step-icon-error]="state == 'error'" | ||
[ngSwitch]="state"> | ||
|
||
<ng-container *ngSwitchCase="'number'" [ngSwitch]="!!(iconOverrides && iconOverrides.number)"> | ||
|
@@ -26,16 +30,35 @@ | |
[ngTemplateOutletContext]="_getIconContext()"></ng-container> | ||
<mat-icon *ngSwitchDefault>done</mat-icon> | ||
</ng-container> | ||
|
||
<ng-container *ngSwitchCase="'error'" [ngSwitch]="!!(iconOverrides && iconOverrides.error)"> | ||
<ng-container | ||
*ngSwitchCase="true" | ||
[ngTemplateOutlet]="iconOverrides.error" | ||
[ngTemplateOutletContext]="_getIconContext()"></ng-container> | ||
<mat-icon *ngSwitchDefault>warning</mat-icon> | ||
</ng-container> | ||
|
||
<!-- Custom state. --> | ||
<ng-container *ngSwitchDefault [ngSwitch]="!!(iconOverrides && iconOverrides[state])"> | ||
<ng-container | ||
*ngSwitchCase="true" | ||
[ngTemplateOutlet]="iconOverrides[state]" | ||
[ngTemplateOutletContext]="_getIconContext()"></ng-container> | ||
<mat-icon *ngSwitchDefault>{{state}}</mat-icon> | ||
</ng-container> | ||
</div> | ||
<div class="mat-step-label" | ||
[class.mat-step-label-active]="active" | ||
[class.mat-step-label-selected]="selected"> | ||
[class.mat-step-label-selected]="selected" | ||
[class.mat-step-label-error]="state == 'error'"> | ||
<!-- If there is a label template, use it. --> | ||
<ng-container *ngIf="_templateLabel()" [ngTemplateOutlet]="_templateLabel()!.template"> | ||
</ng-container> | ||
<!-- It there is no label template, fall back to the text label. --> | ||
<!-- If 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">{{_intl.optionalLabel}}</div> | ||
<div class="mat-step-optional" *ngIf="optional && state != 'error'">{{_intl.optionalLabel}}</div> | ||
<div class="mat-step-sub-label-error" *ngIf="state == 'error'">{{errorMessage}}</div> | ||
</div> | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isn't this just the same as
string
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, but like this it is clear to the developer that the default states that we are using are
'number' | 'edit' | 'done' | 'error'
and that the developer also has the option to define their ownstate
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can reference the enum here instead of repeating all the literals
STEP_STATE | string