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

IgniteUI / igniteui-angular / 29569403286

17 Jul 2026 09:16AM UTC coverage: 90.116% (-0.02%) from 90.135%
29569403286

Pull #15125

github

web-flow
Merge baede978b into 8ebabbb38
Pull Request #15125: refactor(*): bundle styles with components

14920 of 17397 branches covered (85.76%)

Branch coverage included in aggregate %.

30030 of 32483 relevant lines covered (92.45%)

34497.24 hits per line

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

95.71
/projects/igniteui-angular/stepper/src/stepper/stepper.component.ts
1
import { AnimationReferenceMetadata, useAnimation } from '@angular/animations';
2
import { NgTemplateOutlet } from '@angular/common';
3
import {
4
    AfterContentInit,
5
    Component,
6
    ContentChild,
7
    ContentChildren,
8
    ElementRef,
9
    EventEmitter,
10
    HostBinding,
11
    Input,
12
    OnChanges,
13
    OnDestroy,
14
    OnInit,
15
    Output,
16
    QueryList,
17
    SimpleChanges,
18
    TemplateRef,
19
    booleanAttribute,
20
    inject,
21
    ChangeDetectionStrategy,
22
    ViewEncapsulation
23
} from '@angular/core';
24
import { Subject } from 'rxjs';
25
import { takeUntil } from 'rxjs/operators';
26
import { IgxCarouselComponentBase } from 'igniteui-angular/carousel';
27
import { IgxStepComponent } from './step/step.component';
28
import {
29
    IgxStepper, IgxStepperOrientation, IgxStepperTitlePosition, IgxStepType,
30
    IGX_STEPPER_COMPONENT, IStepChangedEventArgs, IStepChangingEventArgs, VerticalAnimationType,
31
    HorizontalAnimationType
32
} from './stepper.common';
33
import {
34
    IgxStepActiveIndicatorDirective,
35
    IgxStepCompletedIndicatorDirective,
36
    IgxStepInvalidIndicatorDirective
37
} from './stepper.directive';
38
import { IgxStepperService } from './stepper.service';
39
import { fadeIn, growVerIn, growVerOut } from 'igniteui-angular/animations';
40
import { ToggleAnimationSettings } from 'igniteui-angular/expansion-panel';
41

42

43
// TODO: common interface between IgxCarouselComponentBase and ToggleAnimationPlayer?
44

45
/**
46
 * Stepper provides a wizard-like workflow by dividing content into logical steps.
47
 *
48
 * @igxModule IgxStepperModule
49
 *
50
 * @igxKeywords stepper
51
 *
52
 * @igxGroup Layouts
53
 *
54
 * @remarks
55
 * The Ignite UI for Angular Stepper component allows the user to navigate between multiple steps.
56
 * It supports horizontal and vertical orientation as well as keyboard navigation and provides API methods to control the active step.
57
 * The component offers keyboard navigation and API to control the active step.
58
 *
59
 * @example
60
 * ```html
61
 * <igx-stepper>
62
 *  <igx-step [active]="true">
63
 *      <igx-icon igxStepIndicator>home</igx-icon>
64
 *      <p igxStepTitle>Home</p>
65
 *      <div igxStepContent>
66
 *         ...
67
 *      </div>
68
 *  </igx-step>
69
 *  <igx-step [optional]="true">
70
 *      <div igxStepContent>
71
 *          ...
72
 *      </div>
73
 *  </igx-step>
74
 *  <igx-step>
75
 *      <div igxStepContent>
76
 *          ...
77
 *      </div>
78
 *  </igx-step>
79
 * </igx-stepper>
80
 * ```
81
 */
82
@Component({
83
    selector: 'igx-stepper',
84
    templateUrl: 'stepper.component.html',
85
    styleUrl: 'stepper.component.css',
86
    encapsulation: ViewEncapsulation.None,
87
    providers: [
88
        IgxStepperService,
89
        { provide: IGX_STEPPER_COMPONENT, useExisting: IgxStepperComponent },
90
    ],
91
    changeDetection: ChangeDetectionStrategy.Eager,
92
    imports: [NgTemplateOutlet]
93
})
94
export class IgxStepperComponent extends IgxCarouselComponentBase implements IgxStepper, OnChanges, OnInit, AfterContentInit, OnDestroy {
3✔
95
    private stepperService = inject(IgxStepperService);
38✔
96
    private element = inject<ElementRef<HTMLElement>>(ElementRef);
38✔
97

98

99
    /**
100
     * Get/Set the animation type of the stepper when the orientation direction is vertical.
101
     *
102
     * @remarks
103
     * Default value is `grow`. Other possible values are `fade` and `none`.
104
     *
105
     * ```html
106
     * <igx-stepper verticalAnimationType="none">
107
     * <igx-stepper>
108
     * ```
109
     */
110
    @Input()
111
    public get verticalAnimationType(): VerticalAnimationType {
112
        return this._verticalAnimationType;
×
113
    }
114

115
    public set verticalAnimationType(value: VerticalAnimationType) {
116
        // TODO: activeChange event is not emitted for the collapsing steps (loop through collapsing steps and emit)
117
        this.stepperService.collapsingSteps.clear();
77✔
118
        this._verticalAnimationType = value;
77✔
119

120
        switch (value) {
77✔
121
            case 'grow':
122
                this.verticalAnimationSettings = this.updateVerticalAnimationSettings(growVerIn, growVerOut);
69✔
123
                break;
69✔
124
            case 'fade':
125
                this.verticalAnimationSettings = this.updateVerticalAnimationSettings(fadeIn, null);
1✔
126
                break;
1✔
127
            case 'none':
128
                this.verticalAnimationSettings = this.updateVerticalAnimationSettings(null, null);
1✔
129
                break;
1✔
130
        }
131
    }
132

133
    /**
134
     * Get/Set the animation type of the stepper when the orientation direction is horizontal.
135
     *
136
     * @remarks
137
     * Default value is `grow`. Other possible values are `fade` and `none`.
138
     *
139
     * ```html
140
     * <igx-stepper animationType="none">
141
     * <igx-stepper>
142
     * ```
143
     */
144
    @Input()
145
    public get horizontalAnimationType(): HorizontalAnimationType {
146
        return this.animationType;
×
147
    }
148

149
    public set horizontalAnimationType(value: HorizontalAnimationType) {
150
        // TODO: activeChange event is not emitted for the collapsing steps (loop through collapsing steps and emit)
151
        this.stepperService.collapsingSteps.clear();
38✔
152
        this.animationType = value;
38✔
153
    }
154

155
    /**
156
     * Get/Set the animation duration.
157
     * ```html
158
     * <igx-stepper [animationDuration]="500">
159
     * <igx-stepper>
160
     * ```
161
     */
162
    @Input()
163
    public get animationDuration(): number {
164
        return this.defaultAnimationDuration;
149✔
165
    }
166

167
    public set animationDuration(value: number) {
168
        if (value && value > 0) {
39✔
169
            this.defaultAnimationDuration = value;
32✔
170
            return;
32✔
171
        }
172
        this.defaultAnimationDuration = this._defaultAnimationDuration;
7✔
173
    }
174

175
    /**
176
     * Get/Set whether the stepper is linear.
177
     *
178
     * @remarks
179
     * If the stepper is in linear mode and if the active step is valid only then the user is able to move forward.
180
     *
181
     * ```html
182
     * <igx-stepper [linear]="true"></igx-stepper>
183
     * ```
184
     */
185
    @Input({ transform: booleanAttribute })
186
    public get linear(): boolean {
187
        return this._linear;
63✔
188
    }
189

190
    public set linear(value: boolean) {
191
        this._linear = value;
3✔
192
        if (this._linear && this.steps.length > 0) {
3✔
193
            // when the stepper is in linear mode we should calculate which steps should be disabled
194
            // and which are visited i.e. their validity should be correctly displayed.
195
            this.stepperService.calculateVisitedSteps();
2✔
196
            this.stepperService.calculateLinearDisabledSteps();
2✔
197
        } else {
198
            this.stepperService.linearDisabledSteps.clear();
1✔
199
        }
200
    }
201

202
    /**
203
     * Get/Set the stepper orientation.
204
     *
205
     * ```typescript
206
     * this.stepper.orientation = IgxStepperOrientation.Vertical;
207
     * ```
208
     */
209
    @HostBinding('attr.aria-orientation')
210
    @Input()
211
    public get orientation(): IgxStepperOrientation {
212
        return this._orientation;
11,332✔
213
    }
214

215
    public set orientation(value: IgxStepperOrientation) {
216
        if (this._orientation === value) {
48✔
217
            return;
39✔
218
        }
219

220
        // TODO: activeChange event is not emitted for the collapsing steps
221
        this.stepperService.collapsingSteps.clear();
9✔
222
        this._orientation = value;
9✔
223
        this._defaultTitlePosition = this._orientation === IgxStepperOrientation.Horizontal ?
9!
224
            IgxStepperTitlePosition.Bottom : IgxStepperTitlePosition.End;
225
    }
226

227
    /**
228
     * Get/Set the type of the steps.
229
     *
230
     * ```typescript
231
     * this.stepper.stepType = IgxStepType.Indicator;
232
     * ```
233
     */
234
    @Input()
235
    public stepType: IgxStepType = IgxStepType.Full;
38✔
236

237
    /**
238
     * Get/Set whether the content is displayed above the steps.
239
     *
240
     * @remarks
241
     * Default value is `false` and the content is below the steps.
242
     *
243
     * ```typescript
244
     * this.stepper.contentTop = true;
245
     * ```
246
     */
247
    @Input({ transform: booleanAttribute })
248
    public contentTop = false;
38✔
249

250
    /**
251
     * Get/Set the position of the steps title.
252
     *
253
     * @remarks
254
     * The default value when the stepper is horizontally orientated is `bottom`.
255
     * In vertical layout the default title position is `end`.
256
     *
257
     * ```typescript
258
     * this.stepper.titlePosition = IgxStepperTitlePosition.Top;
259
     * ```
260
     */
261
    @Input()
262
    public titlePosition: IgxStepperTitlePosition = null;
38✔
263

264
    /** @hidden @internal **/
265
    @HostBinding('class.igx-stepper')
266
    public cssClass = 'igx-stepper';
38✔
267

268
    /** @hidden @internal **/
269
    @HostBinding('attr.role')
270
    public role = 'tablist';
38✔
271

272
    /** @hidden @internal **/
273
    @HostBinding('class.igx-stepper--horizontal')
274
    public get directionClass() {
275
        return this.orientation === IgxStepperOrientation.Horizontal;
417✔
276
    }
277

278
    /**
279
     * Emitted when the stepper's active step is changing.
280
     *
281
     *```html
282
     * <igx-stepper (activeStepChanging)="handleActiveStepChanging($event)">
283
     * </igx-stepper>
284
     * ```
285
     *
286
     *```typescript
287
     * public handleActiveStepChanging(event: IStepChangingEventArgs) {
288
     *  if (event.newIndex < event.oldIndex) {
289
     *      event.cancel = true;
290
     *  }
291
     * }
292
     *```
293
     */
294
    @Output()
295
    public activeStepChanging = new EventEmitter<IStepChangingEventArgs>();
38✔
296

297
    /**
298
     * Emitted when the active step is changed.
299
     *
300
     * @example
301
     * ```
302
     * <igx-stepper (activeStepChanged)="handleActiveStepChanged($event)"></igx-stepper>
303
     * ```
304
     */
305
    @Output()
306
    public activeStepChanged = new EventEmitter<IStepChangedEventArgs>();
38✔
307

308
    /** @hidden @internal */
309
    @ContentChild(IgxStepInvalidIndicatorDirective, { read: TemplateRef })
310
    public invalidIndicatorTemplate: TemplateRef<IgxStepInvalidIndicatorDirective>;
311

312
    /** @hidden @internal */
313
    @ContentChild(IgxStepCompletedIndicatorDirective, { read: TemplateRef })
314
    public completedIndicatorTemplate: TemplateRef<IgxStepCompletedIndicatorDirective>;
315

316
    /** @hidden @internal */
317
    @ContentChild(IgxStepActiveIndicatorDirective, { read: TemplateRef })
318
    public activeIndicatorTemplate: TemplateRef<IgxStepActiveIndicatorDirective>;
319

320
    /** @hidden @internal */
321
    @ContentChildren(IgxStepComponent, { descendants: false })
322
    private _steps: QueryList<IgxStepComponent>;
323

324
    /**
325
     * Get all steps.
326
     *
327
     * ```typescript
328
     * const steps: IgxStepComponent[] = this.stepper.steps;
329
     * ```
330
     */
331
    public get steps(): IgxStepComponent[] {
332
        return this._steps?.toArray() || [];
1,068✔
333
    }
334

335
    /** @hidden @internal */
336
    public get nativeElement(): HTMLElement {
337
        return this.element.nativeElement;
15✔
338
    }
339

340
    /** @hidden @internal */
341
    public verticalAnimationSettings: ToggleAnimationSettings = {
38✔
342
        openAnimation: growVerIn,
343
        closeAnimation: growVerOut,
344
    };
345
    /** @hidden @internal */
346
    public _defaultTitlePosition: IgxStepperTitlePosition = IgxStepperTitlePosition.Bottom;
38✔
347
    private destroy$ = new Subject<void>();
38✔
348
    private _orientation: IgxStepperOrientation = IgxStepperOrientation.Horizontal;
38✔
349
    private _verticalAnimationType: VerticalAnimationType = VerticalAnimationType.Grow;
38✔
350
    private _linear = false;
38✔
351
    private readonly _defaultAnimationDuration = 350;
38✔
352

353
    constructor() {
354
        super();
38✔
355
        this.stepperService.stepper = this;
38✔
356
    }
357

358
    /** @hidden @internal */
359
    public ngOnChanges(changes: SimpleChanges): void {
360
        if (changes['animationDuration']) {
59✔
361
            this.verticalAnimationType = this._verticalAnimationType;
39✔
362
        }
363
    }
364

365
    /** @hidden @internal */
366
    public ngOnInit(): void {
367
        this.enterAnimationDone.pipe(takeUntil(this.destroy$)).subscribe(() => {
32✔
368
            this.activeStepChanged.emit({ owner: this, index: this.stepperService.activeStep.index });
17✔
369
        });
370
        this.leaveAnimationDone.pipe(takeUntil(this.destroy$)).subscribe(() => {
32✔
371
            if (this.stepperService.collapsingSteps.size === 1) {
15✔
372
                this.stepperService.collapse(this.stepperService.previousActiveStep);
14✔
373
            } else {
374
                Array.from(this.stepperService.collapsingSteps).slice(0, this.stepperService.collapsingSteps.size - 1)
1✔
375
                    .forEach(step => this.stepperService.collapse(step));
1✔
376
            }
377
        });
378

379

380
    }
381

382
    /** @hidden @internal */
383
    public ngAfterContentInit(): void {
384
        let activeStep;
385
        this.steps.forEach((step, index) => {
32✔
386
            this.updateStepAria(step, index);
152✔
387
            if (!activeStep && step.active) {
152✔
388
                activeStep = step;
31✔
389
            }
390
        });
391
        if (!activeStep) {
32✔
392
            this.activateFirstStep(true);
1✔
393
        }
394

395
        if (this.linear) {
32✔
396
            this.stepperService.calculateLinearDisabledSteps();
1✔
397
        }
398

399
        this.handleStepChanges();
32✔
400
    }
401

402
    /** @hidden @internal */
403
    public override ngOnDestroy(): void {
404
        super.ngOnDestroy();
38✔
405
        this.destroy$.next();
38✔
406
        this.destroy$.complete();
38✔
407
    }
408

409
    /**
410
     * Activates the step at a given index.
411
     *
412
     *```typescript
413
     * this.stepper.navigateTo(1);
414
     *```
415
     */
416
    public navigateTo(index: number): void {
417
        const step = this.steps[index];
8✔
418
        if (!step || this.stepperService.activeStep === step) {
8✔
419
            return;
5✔
420
        }
421
        this.activateStep(step);
3✔
422
    }
423

424
    /**
425
     * Activates the next enabled step.
426
     *
427
     *```typescript
428
     * this.stepper.next();
429
     *```
430
     */
431
    public next(): void {
432
        this.moveToNextStep();
33✔
433
    }
434

435
    /**
436
     * Activates the previous enabled step.
437
     *
438
     *```typescript
439
     * this.stepper.prev();
440
     *```
441
     */
442
    public prev(): void {
443
        this.moveToNextStep(false);
×
444
    }
445

446
    /**
447
     * Resets the stepper to its initial state i.e. activates the first step.
448
     *
449
     * @remarks
450
     * The steps' content will not be automatically reset.
451
     *```typescript
452
     * this.stepper.reset();
453
     *```
454
     */
455
    public reset(): void {
456
        this.stepperService.visitedSteps.clear();
1✔
457
        const activeStep = this.steps.find(s => !s.disabled);
1✔
458
        if (activeStep) {
1✔
459
            this.activateStep(activeStep);
1✔
460
        }
461
    }
462

463
    /** @hidden @internal */
464
    public playHorizontalAnimations(): void {
465
        this.previousItem = this.stepperService.previousActiveStep;
24✔
466
        this.currentItem = this.stepperService.activeStep;
24✔
467
        this.triggerAnimations();
24✔
468
    }
469

470
    protected getPreviousElement(): HTMLElement {
471
        return this.stepperService.previousActiveStep?.contentContainer.nativeElement;
16✔
472
    }
473

474
    protected getCurrentElement(): HTMLElement {
475
        return this.stepperService.activeStep.contentContainer.nativeElement;
18✔
476
    }
477

478
    private updateVerticalAnimationSettings(
479
        openAnimation: AnimationReferenceMetadata,
480
        closeAnimation: AnimationReferenceMetadata): ToggleAnimationSettings {
481
        const customCloseAnimation = useAnimation(closeAnimation, {
71✔
482
            params: {
483
                duration: this.animationDuration + 'ms'
484
            }
485
        });
486
        const customOpenAnimation = useAnimation(openAnimation, {
71✔
487
            params: {
488
                duration: this.animationDuration + 'ms'
489
            }
490
        });
491

492
        return {
71✔
493
            openAnimation: openAnimation ? customOpenAnimation : null,
71✔
494
            closeAnimation: closeAnimation ? customCloseAnimation : null
71✔
495
        };
496
    }
497

498
    private updateStepAria(step: IgxStepComponent, index: number): void {
499
        step._index = index;
192✔
500
        step.renderer.setAttribute(step.nativeElement, 'aria-setsize', (this.steps.length).toString());
192✔
501
        step.renderer.setAttribute(step.nativeElement, 'aria-posinset', (index + 1).toString());
192✔
502
    }
503

504
    private handleStepChanges(): void {
505
        this._steps.changes.pipe(takeUntil(this.destroy$)).subscribe(steps => {
32✔
506
            Promise.resolve().then(() => {
7✔
507
                steps.forEach((step, index) => {
7✔
508
                    this.updateStepAria(step, index);
40✔
509
                });
510

511
                // when the active step is removed
512
                const hasActiveStep = this.steps.find(s => s === this.stepperService.activeStep);
20✔
513
                if (!hasActiveStep) {
7✔
514
                    this.activateFirstStep();
2✔
515
                }
516
                // TO DO: mark step added before the active as visited?
517
                if (this.linear) {
7✔
518
                    this.stepperService.calculateLinearDisabledSteps();
3✔
519
                }
520
            });
521
        });
522
    }
523

524
    private activateFirstStep(activateInitially = false) {
2✔
525
        const firstEnabledStep = this.steps.find(s => !s.disabled);
4✔
526
        if (firstEnabledStep) {
3✔
527
            firstEnabledStep.active = true;
3✔
528
            if (activateInitially) {
3✔
529
                firstEnabledStep.activeChange.emit(true);
1✔
530
                this.activeStepChanged.emit({ owner: this, index: firstEnabledStep.index });
1✔
531
            }
532
        }
533
    }
534

535
    private activateStep(step: IgxStepComponent) {
536
        if (this.orientation === IgxStepperOrientation.Horizontal) {
34✔
537
            step.changeHorizontalActiveStep();
23✔
538
        } else {
539
            this.stepperService.expand(step);
11✔
540
        }
541
    }
542

543
    private moveToNextStep(next = true) {
33✔
544
        let steps: IgxStepComponent[] = this.steps;
33✔
545
        let activeStepIndex = this.stepperService.activeStep.index;
33✔
546
        if (!next) {
33!
547
            steps = this.steps.reverse();
×
548
            activeStepIndex = steps.findIndex(s => s === this.stepperService.activeStep);
×
549
        }
550

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

© 2026 Coveralls, Inc