• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

IgniteUI / igniteui-angular / 14101962945

27 Mar 2025 08:22AM UTC coverage: 91.596% (-0.07%) from 91.669%
14101962945

push

github

web-flow
fix(circular-progress): make sure that the fill-color-default can be used by the user and flip the gradient for star and end to follow the progressbar progress direction (#15560)

13358 of 15619 branches covered (85.52%)

26920 of 29390 relevant lines covered (91.6%)

33937.04 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.83
/projects/igniteui-angular/src/lib/stepper/stepper.component.ts
1
import { AnimationReferenceMetadata, useAnimation } from '@angular/animations';
2
import { NgTemplateOutlet } from '@angular/common';
3
import {
4
    AfterContentInit, ChangeDetectorRef, Component, ContentChild, ContentChildren,
5
    ElementRef, EventEmitter, HostBinding, Inject, Input, OnChanges, OnDestroy,
6
    OnInit, Output, QueryList, SimpleChanges, TemplateRef, booleanAttribute
7
} from '@angular/core';
8
import { Subject } from 'rxjs';
9
import { takeUntil } from 'rxjs/operators';
10
import { IgxCarouselComponentBase } from '../carousel/carousel-base';
11

12
import { ToggleAnimationSettings } from '../expansion-panel/toggle-animation-component';
13
import { IgxAngularAnimationService } from '../services/animation/angular-animation-service';
14
import { AnimationService } from '../services/animation/animation';
15
import { IgxStepComponent } from './step/step.component';
16
import {
17
    IgxStepper, IgxStepperOrientation, IgxStepperTitlePosition, IgxStepType,
18
    IGX_STEPPER_COMPONENT, IStepChangedEventArgs, IStepChangingEventArgs, VerticalAnimationType,
19
    HorizontalAnimationType
20
} from './stepper.common';
21
import {
22
    IgxStepActiveIndicatorDirective,
23
    IgxStepCompletedIndicatorDirective,
24
    IgxStepInvalidIndicatorDirective
25
} from './stepper.directive';
26
import { IgxStepperService } from './stepper.service';
27
import { fadeIn, growVerIn, growVerOut } from 'igniteui-angular/animations';
28

29

30
// TODO: common interface between IgxCarouselComponentBase and ToggleAnimationPlayer?
31

32
/**
33
 * IgxStepper provides a wizard-like workflow by dividing content into logical steps.
34
 *
35
 * @igxModule IgxStepperModule
36
 *
37
 * @igxKeywords stepper
38
 *
39
 * @igxGroup Layouts
40
 *
41
 * @remarks
42
 * The Ignite UI for Angular Stepper component allows the user to navigate between multiple steps.
43
 * It supports horizontal and vertical orientation as well as keyboard navigation and provides API methods to control the active step.
44
 * The component offers keyboard navigation and API to control the active step.
45
 *
46
 * @example
47
 * ```html
48
 * <igx-stepper>
49
 *  <igx-step [active]="true">
50
 *      <igx-icon igxStepIndicator>home</igx-icon>
51
 *      <p igxStepTitle>Home</p>
52
 *      <div igxStepContent>
53
 *         ...
54
 *      </div>
55
 *  </igx-step>
56
 *  <igx-step [optional]="true">
57
 *      <div igxStepContent>
58
 *          ...
59
 *      </div>
60
 *  </igx-step>
61
 *  <igx-step>
62
 *      <div igxStepContent>
63
 *          ...
64
 *      </div>
65
 *  </igx-step>
66
 * </igx-stepper>
67
 * ```
68
 */
69
@Component({
70
    selector: 'igx-stepper',
71
    templateUrl: 'stepper.component.html',
72
    providers: [
73
        IgxStepperService,
74
        { provide: IGX_STEPPER_COMPONENT, useExisting: IgxStepperComponent },
75
    ],
76
    imports: [NgTemplateOutlet]
77
})
78
export class IgxStepperComponent extends IgxCarouselComponentBase implements IgxStepper, OnChanges, OnInit, AfterContentInit, OnDestroy {
3✔
79

80
    /**
81
     * Get/Set the animation type of the stepper when the orientation direction is vertical.
82
     *
83
     * @remarks
84
     * Default value is `grow`. Other possible values are `fade` and `none`.
85
     *
86
     * ```html
87
     * <igx-stepper verticalAnimationType="none">
88
     * <igx-stepper>
89
     * ```
90
     */
91
    @Input()
92
    public get verticalAnimationType(): VerticalAnimationType {
93
        return this._verticalAnimationType;
×
94
    }
95

96
    public set verticalAnimationType(value: VerticalAnimationType) {
97
        // TODO: activeChange event is not emitted for the collapsing steps (loop through collapsing steps and emit)
98
        this.stepperService.collapsingSteps.clear();
73✔
99
        this._verticalAnimationType = value;
73✔
100

101
        switch (value) {
73✔
102
            case 'grow':
103
                this.verticalAnimationSettings = this.updateVerticalAnimationSettings(growVerIn, growVerOut);
65✔
104
                break;
65✔
105
            case 'fade':
106
                this.verticalAnimationSettings = this.updateVerticalAnimationSettings(fadeIn, null);
1✔
107
                break;
1✔
108
            case 'none':
109
                this.verticalAnimationSettings = this.updateVerticalAnimationSettings(null, null);
1✔
110
                break;
1✔
111
        }
112
    }
113

114
    /**
115
     * Get/Set the animation type of the stepper when the orientation direction is horizontal.
116
     *
117
     * @remarks
118
     * Default value is `grow`. Other possible values are `fade` and `none`.
119
     *
120
     * ```html
121
     * <igx-stepper animationType="none">
122
     * <igx-stepper>
123
     * ```
124
     */
125
    @Input()
126
    public get horizontalAnimationType(): HorizontalAnimationType {
127
        return this.animationType;
×
128
    }
129

130
    public set horizontalAnimationType(value: HorizontalAnimationType) {
131
        // TODO: activeChange event is not emitted for the collapsing steps (loop through collapsing steps and emit)
132
        this.stepperService.collapsingSteps.clear();
36✔
133
        this.animationType = value;
36✔
134
    }
135

136
    /**
137
     * Get/Set the animation duration.
138
     * ```html
139
     * <igx-stepper [animationDuration]="500">
140
     * <igx-stepper>
141
     * ```
142
     */
143
    @Input()
144
    public get animationDuration(): number {
145
        return this.defaultAnimationDuration;
141✔
146
    }
147

148
    public set animationDuration(value: number) {
149
        if (value && value > 0) {
37✔
150
            this.defaultAnimationDuration = value;
30✔
151
            return;
30✔
152
        }
153
        this.defaultAnimationDuration = this._defaultAnimationDuration;
7✔
154
    }
155

156
    /**
157
     * Get/Set whether the stepper is linear.
158
     *
159
     * @remarks
160
     * If the stepper is in linear mode and if the active step is valid only then the user is able to move forward.
161
     *
162
     * ```html
163
     * <igx-stepper [linear]="true"></igx-stepper>
164
     * ```
165
     */
166
    @Input({ transform: booleanAttribute })
167
    public get linear(): boolean {
168
        return this._linear;
59✔
169
    }
170

171
    public set linear(value: boolean) {
172
        this._linear = value;
3✔
173
        if (this._linear && this.steps.length > 0) {
3✔
174
            // when the stepper is in linear mode we should calculate which steps should be disabled
175
            // and which are visited i.e. their validity should be correctly displayed.
176
            this.stepperService.calculateVisitedSteps();
2✔
177
            this.stepperService.calculateLinearDisabledSteps();
2✔
178
        } else {
179
            this.stepperService.linearDisabledSteps.clear();
1✔
180
        }
181
    }
182

183
    /**
184
     * Get/Set the stepper orientation.
185
     *
186
     * ```typescript
187
     * this.stepper.orientation = IgxStepperOrientation.Vertical;
188
     * ```
189
     */
190
    @HostBinding('attr.aria-orientation')
191
    @Input()
192
    public get orientation(): IgxStepperOrientation {
193
        return this._orientation;
6,902✔
194
    }
195

196
    public set orientation(value: IgxStepperOrientation) {
197
        if (this._orientation === value) {
44✔
198
            return;
37✔
199
        }
200

201
        // TODO: activeChange event is not emitted for the collapsing steps
202
        this.stepperService.collapsingSteps.clear();
7✔
203
        this._orientation = value;
7✔
204
        this._defaultTitlePosition = this._orientation === IgxStepperOrientation.Horizontal ?
7!
205
            IgxStepperTitlePosition.Bottom : IgxStepperTitlePosition.End;
206
    }
207

208
    /**
209
     * Get/Set the type of the steps.
210
     *
211
     * ```typescript
212
     * this.stepper.stepType = IgxStepType.Indicator;
213
     * ```
214
     */
215
    @Input()
216
    public stepType: IgxStepType = IgxStepType.Full;
34✔
217

218
    /**
219
     * Get/Set whether the content is displayed above the steps.
220
     *
221
     * @remarks
222
     * Default value is `false` and the content is below the steps.
223
     *
224
     * ```typescript
225
     * this.stepper.contentTop = true;
226
     * ```
227
     */
228
    @Input({ transform: booleanAttribute })
229
    public contentTop = false;
34✔
230

231
    /**
232
     * Get/Set the position of the steps title.
233
     *
234
     * @remarks
235
     * The default value when the stepper is horizontally orientated is `bottom`.
236
     * In vertical layout the default title position is `end`.
237
     *
238
     * ```typescript
239
     * this.stepper.titlePosition = IgxStepperTitlePosition.Top;
240
     * ```
241
     */
242
    @Input()
243
    public titlePosition: IgxStepperTitlePosition = null;
34✔
244

245
    /** @hidden @internal **/
246
    @HostBinding('class.igx-stepper')
247
    public cssClass = 'igx-stepper';
34✔
248

249
    /** @hidden @internal **/
250
    @HostBinding('attr.role')
251
    public role = 'tablist';
34✔
252

253
    /** @hidden @internal **/
254
    @HostBinding('class.igx-stepper--horizontal')
255
    public get directionClass() {
256
        return this.orientation === IgxStepperOrientation.Horizontal;
405✔
257
    }
258

259
    /**
260
     * Emitted when the stepper's active step is changing.
261
     *
262
     *```html
263
     * <igx-stepper (activeStepChanging)="handleActiveStepChanging($event)">
264
     * </igx-stepper>
265
     * ```
266
     *
267
     *```typescript
268
     * public handleActiveStepChanging(event: IStepChangingEventArgs) {
269
     *  if (event.newIndex < event.oldIndex) {
270
     *      event.cancel = true;
271
     *  }
272
     * }
273
     *```
274
     */
275
    @Output()
276
    public activeStepChanging = new EventEmitter<IStepChangingEventArgs>();
34✔
277

278
    /**
279
     * Emitted when the active step is changed.
280
     *
281
     * @example
282
     * ```
283
     * <igx-stepper (activeStepChanged)="handleActiveStepChanged($event)"></igx-stepper>
284
     * ```
285
     */
286
    @Output()
287
    public activeStepChanged = new EventEmitter<IStepChangedEventArgs>();
34✔
288

289
    /** @hidden @internal */
290
    @ContentChild(IgxStepInvalidIndicatorDirective, { read: TemplateRef })
291
    public invalidIndicatorTemplate: TemplateRef<IgxStepInvalidIndicatorDirective>;
292

293
    /** @hidden @internal */
294
    @ContentChild(IgxStepCompletedIndicatorDirective, { read: TemplateRef })
295
    public completedIndicatorTemplate: TemplateRef<IgxStepCompletedIndicatorDirective>;
296

297
    /** @hidden @internal */
298
    @ContentChild(IgxStepActiveIndicatorDirective, { read: TemplateRef })
299
    public activeIndicatorTemplate: TemplateRef<IgxStepActiveIndicatorDirective>;
300

301
    /** @hidden @internal */
302
    @ContentChildren(IgxStepComponent, { descendants: false })
303
    private _steps: QueryList<IgxStepComponent>;
304

305
    /**
306
     * Get all steps.
307
     *
308
     * ```typescript
309
     * const steps: IgxStepComponent[] = this.stepper.steps;
310
     * ```
311
     */
312
    public get steps(): IgxStepComponent[] {
313
        return this._steps?.toArray() || [];
1,040✔
314
    }
315

316
    /** @hidden @internal */
317
    public get nativeElement(): HTMLElement {
318
        return this.element.nativeElement;
15✔
319
    }
320

321
    /** @hidden @internal */
322
    public verticalAnimationSettings: ToggleAnimationSettings = {
34✔
323
        openAnimation: growVerIn,
324
        closeAnimation: growVerOut,
325
    };
326
    /** @hidden @internal */
327
    public _defaultTitlePosition: IgxStepperTitlePosition = IgxStepperTitlePosition.Bottom;
34✔
328
    private destroy$ = new Subject<void>();
34✔
329
    private _orientation: IgxStepperOrientation = IgxStepperOrientation.Horizontal;
34✔
330
    private _verticalAnimationType: VerticalAnimationType = VerticalAnimationType.Grow;
34✔
331
    private _linear = false;
34✔
332
    private readonly _defaultAnimationDuration = 350;
34✔
333

334
    constructor(
335
        cdr: ChangeDetectorRef,
336
        @Inject(IgxAngularAnimationService) animationService: AnimationService,
337
        private stepperService: IgxStepperService,
34✔
338
        private element: ElementRef<HTMLElement>) {
34✔
339
        super(animationService, cdr);
34✔
340
        this.stepperService.stepper = this;
34✔
341
    }
342

343
    /** @hidden @internal */
344
    public ngOnChanges(changes: SimpleChanges): void {
345
        if (changes['animationDuration']) {
55✔
346
            this.verticalAnimationType = this._verticalAnimationType;
37✔
347
        }
348
    }
349

350
    /** @hidden @internal */
351
    public ngOnInit(): void {
352
        this.enterAnimationDone.pipe(takeUntil(this.destroy$)).subscribe(() => {
28✔
353
            this.activeStepChanged.emit({ owner: this, index: this.stepperService.activeStep.index });
17✔
354
        });
355
        this.leaveAnimationDone.pipe(takeUntil(this.destroy$)).subscribe(() => {
28✔
356
            if (this.stepperService.collapsingSteps.size === 1) {
15✔
357
                this.stepperService.collapse(this.stepperService.previousActiveStep);
14✔
358
            } else {
359
                Array.from(this.stepperService.collapsingSteps).slice(0, this.stepperService.collapsingSteps.size - 1)
1✔
360
                    .forEach(step => this.stepperService.collapse(step));
1✔
361
            }
362
        });
363

364

365
    }
366

367
    /** @hidden @internal */
368
    public ngAfterContentInit(): void {
369
        let activeStep;
370
        this.steps.forEach((step, index) => {
28✔
371
            this.updateStepAria(step, index);
138✔
372
            if (!activeStep && step.active) {
138✔
373
                activeStep = step;
27✔
374
            }
375
        });
376
        if (!activeStep) {
28✔
377
            this.activateFirstStep(true);
1✔
378
        }
379

380
        if (this.linear) {
28✔
381
            this.stepperService.calculateLinearDisabledSteps();
1✔
382
        }
383

384
        this.handleStepChanges();
28✔
385
    }
386

387
    /** @hidden @internal */
388
    public override ngOnDestroy(): void {
389
        super.ngOnDestroy();
28✔
390
        this.destroy$.next();
28✔
391
        this.destroy$.complete();
28✔
392
    }
393

394
    /**
395
     * Activates the step at a given index.
396
     *
397
     *```typescript
398
     * this.stepper.navigateTo(1);
399
     *```
400
     */
401
    public navigateTo(index: number): void {
402
        const step = this.steps[index];
7✔
403
        if (!step || this.stepperService.activeStep === step) {
7✔
404
            return;
5✔
405
        }
406
        this.activateStep(step);
2✔
407
    }
408

409
    /**
410
     * Activates the next enabled step.
411
     *
412
     *```typescript
413
     * this.stepper.next();
414
     *```
415
     */
416
    public next(): void {
417
        this.moveToNextStep();
33✔
418
    }
419

420
    /**
421
     * Activates the previous enabled step.
422
     *
423
     *```typescript
424
     * this.stepper.prev();
425
     *```
426
     */
427
    public prev(): void {
428
        this.moveToNextStep(false);
×
429
    }
430

431
    /**
432
     * Resets the stepper to its initial state i.e. activates the first step.
433
     *
434
     * @remarks
435
     * The steps' content will not be automatically reset.
436
     *```typescript
437
     * this.stepper.reset();
438
     *```
439
     */
440
    public reset(): void {
441
        this.stepperService.visitedSteps.clear();
1✔
442
        const activeStep = this.steps.find(s => !s.disabled);
1✔
443
        if (activeStep) {
1✔
444
            this.activateStep(activeStep);
1✔
445
        }
446
    }
447

448
    /** @hidden @internal */
449
    public playHorizontalAnimations(): void {
450
        this.previousItem = this.stepperService.previousActiveStep;
24✔
451
        this.currentItem = this.stepperService.activeStep;
24✔
452
        this.triggerAnimations();
24✔
453
    }
454

455
    protected getPreviousElement(): HTMLElement {
456
        return this.stepperService.previousActiveStep?.contentContainer.nativeElement;
16✔
457
    }
458

459
    protected getCurrentElement(): HTMLElement {
460
        return this.stepperService.activeStep.contentContainer.nativeElement;
18✔
461
    }
462

463
    private updateVerticalAnimationSettings(
464
        openAnimation: AnimationReferenceMetadata,
465
        closeAnimation: AnimationReferenceMetadata): ToggleAnimationSettings {
466
        const customCloseAnimation = useAnimation(closeAnimation, {
67✔
467
            params: {
468
                duration: this.animationDuration + 'ms'
469
            }
470
        });
471
        const customOpenAnimation = useAnimation(openAnimation, {
67✔
472
            params: {
473
                duration: this.animationDuration + 'ms'
474
            }
475
        });
476

477
        return {
67✔
478
            openAnimation: openAnimation ? customOpenAnimation : null,
67✔
479
            closeAnimation: closeAnimation ? customCloseAnimation : null
67✔
480
        };
481
    }
482

483
    private updateStepAria(step: IgxStepComponent, index: number): void {
484
        step._index = index;
178✔
485
        step.renderer.setAttribute(step.nativeElement, 'aria-setsize', (this.steps.length).toString());
178✔
486
        step.renderer.setAttribute(step.nativeElement, 'aria-posinset', (index + 1).toString());
178✔
487
    }
488

489
    private handleStepChanges(): void {
490
        this._steps.changes.pipe(takeUntil(this.destroy$)).subscribe(steps => {
28✔
491
            Promise.resolve().then(() => {
7✔
492
                steps.forEach((step, index) => {
7✔
493
                    this.updateStepAria(step, index);
40✔
494
                });
495

496
                // when the active step is removed
497
                const hasActiveStep = this.steps.find(s => s === this.stepperService.activeStep);
20✔
498
                if (!hasActiveStep) {
7✔
499
                    this.activateFirstStep();
2✔
500
                }
501
                // TO DO: mark step added before the active as visited?
502
                if (this.linear) {
7✔
503
                    this.stepperService.calculateLinearDisabledSteps();
3✔
504
                }
505
            });
506
        });
507
    }
508

509
    private activateFirstStep(activateInitially = false) {
2✔
510
        const firstEnabledStep = this.steps.find(s => !s.disabled);
4✔
511
        if (firstEnabledStep) {
3✔
512
            firstEnabledStep.active = true;
3✔
513
            if (activateInitially) {
3✔
514
                firstEnabledStep.activeChange.emit(true);
1✔
515
                this.activeStepChanged.emit({ owner: this, index: firstEnabledStep.index });
1✔
516
            }
517
        }
518
    }
519

520
    private activateStep(step: IgxStepComponent) {
521
        if (this.orientation === IgxStepperOrientation.Horizontal) {
33✔
522
            step.changeHorizontalActiveStep();
23✔
523
        } else {
524
            this.stepperService.expand(step);
10✔
525
        }
526
    }
527

528
    private moveToNextStep(next = true) {
33✔
529
        let steps: IgxStepComponent[] = this.steps;
33✔
530
        let activeStepIndex = this.stepperService.activeStep.index;
33✔
531
        if (!next) {
33!
532
            steps = this.steps.reverse();
×
533
            activeStepIndex = steps.findIndex(s => s === this.stepperService.activeStep);
×
534
        }
535

536
        const nextStep = steps.find((s, i) => i > activeStepIndex && s.isAccessible);
75✔
537
        if (nextStep) {
33✔
538
            this.activateStep(nextStep);
30✔
539
        }
540
    }
541
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc