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

IgniteUI / igniteui-angular / 28013325390

23 Jun 2026 08:34AM UTC coverage: 90.139% (-0.02%) from 90.154%
28013325390

Pull #17324

github

web-flow
Merge 690ff31c5 into 01244911c
Pull Request #17324: fix(skills): omit column widths by default in generated grid code

14880 of 17339 branches covered (85.82%)

Branch coverage included in aggregate %.

29947 of 32392 relevant lines covered (92.45%)

34656.81 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 { AfterContentInit, Component, ContentChild, ContentChildren, ElementRef, EventEmitter, HostBinding, Input, OnChanges, OnDestroy, OnInit, Output, QueryList, SimpleChanges, TemplateRef, booleanAttribute, inject, ChangeDetectionStrategy } from '@angular/core';
4
import { Subject } from 'rxjs';
5
import { takeUntil } from 'rxjs/operators';
6

7
import { IgxCarouselComponentBase } from 'igniteui-angular/carousel';
8
import { IgxStepComponent } from './step/step.component';
9
import {
10
    IgxStepper, IgxStepperOrientation, IgxStepperTitlePosition, IgxStepType,
11
    IGX_STEPPER_COMPONENT, IStepChangedEventArgs, IStepChangingEventArgs, VerticalAnimationType,
12
    HorizontalAnimationType
13
} from './stepper.common';
14
import {
15
    IgxStepActiveIndicatorDirective,
16
    IgxStepCompletedIndicatorDirective,
17
    IgxStepInvalidIndicatorDirective
18
} from './stepper.directive';
19
import { IgxStepperService } from './stepper.service';
20
import { fadeIn, growVerIn, growVerOut } from 'igniteui-angular/animations';
21
import { ToggleAnimationSettings } from 'igniteui-angular/expansion-panel';
22

23

24
// TODO: common interface between IgxCarouselComponentBase and ToggleAnimationPlayer?
25

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

77

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

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

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

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

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

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

146
    public set animationDuration(value: number) {
147
        if (value && value > 0) {
39✔
148
            this.defaultAnimationDuration = value;
32✔
149
            return;
32✔
150
        }
151
        this.defaultAnimationDuration = this._defaultAnimationDuration;
7✔
152
    }
153

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

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

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

194
    public set orientation(value: IgxStepperOrientation) {
195
        if (this._orientation === value) {
48✔
196
            return;
39✔
197
        }
198

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

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

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

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

243
    /** @hidden @internal **/
244
    @HostBinding('class.igx-stepper')
245
    public cssClass = 'igx-stepper';
38✔
246

247
    /** @hidden @internal **/
248
    @HostBinding('attr.role')
249
    public role = 'tablist';
38✔
250

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

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

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

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

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

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

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

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

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

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

332
    constructor() {
333
        super();
38✔
334
        this.stepperService.stepper = this;
38✔
335
    }
336

337
    /** @hidden @internal */
338
    public ngOnChanges(changes: SimpleChanges): void {
339
        if (changes['animationDuration']) {
59✔
340
            this.verticalAnimationType = this._verticalAnimationType;
39✔
341
        }
342
    }
343

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

358

359
    }
360

361
    /** @hidden @internal */
362
    public ngAfterContentInit(): void {
363
        let activeStep;
364
        this.steps.forEach((step, index) => {
32✔
365
            this.updateStepAria(step, index);
152✔
366
            if (!activeStep && step.active) {
152✔
367
                activeStep = step;
31✔
368
            }
369
        });
370
        if (!activeStep) {
32✔
371
            this.activateFirstStep(true);
1✔
372
        }
373

374
        if (this.linear) {
32✔
375
            this.stepperService.calculateLinearDisabledSteps();
1✔
376
        }
377

378
        this.handleStepChanges();
32✔
379
    }
380

381
    /** @hidden @internal */
382
    public override ngOnDestroy(): void {
383
        super.ngOnDestroy();
38✔
384
        this.destroy$.next();
38✔
385
        this.destroy$.complete();
38✔
386
    }
387

388
    /**
389
     * Activates the step at a given index.
390
     *
391
     *```typescript
392
     * this.stepper.navigateTo(1);
393
     *```
394
     */
395
    public navigateTo(index: number): void {
396
        const step = this.steps[index];
8✔
397
        if (!step || this.stepperService.activeStep === step) {
8✔
398
            return;
5✔
399
        }
400
        this.activateStep(step);
3✔
401
    }
402

403
    /**
404
     * Activates the next enabled step.
405
     *
406
     *```typescript
407
     * this.stepper.next();
408
     *```
409
     */
410
    public next(): void {
411
        this.moveToNextStep();
33✔
412
    }
413

414
    /**
415
     * Activates the previous enabled step.
416
     *
417
     *```typescript
418
     * this.stepper.prev();
419
     *```
420
     */
421
    public prev(): void {
422
        this.moveToNextStep(false);
×
423
    }
424

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

442
    /** @hidden @internal */
443
    public playHorizontalAnimations(): void {
444
        this.previousItem = this.stepperService.previousActiveStep;
24✔
445
        this.currentItem = this.stepperService.activeStep;
24✔
446
        this.triggerAnimations();
24✔
447
    }
448

449
    protected getPreviousElement(): HTMLElement {
450
        return this.stepperService.previousActiveStep?.contentContainer.nativeElement;
16✔
451
    }
452

453
    protected getCurrentElement(): HTMLElement {
454
        return this.stepperService.activeStep.contentContainer.nativeElement;
18✔
455
    }
456

457
    private updateVerticalAnimationSettings(
458
        openAnimation: AnimationReferenceMetadata,
459
        closeAnimation: AnimationReferenceMetadata): ToggleAnimationSettings {
460
        const customCloseAnimation = useAnimation(closeAnimation, {
71✔
461
            params: {
462
                duration: this.animationDuration + 'ms'
463
            }
464
        });
465
        const customOpenAnimation = useAnimation(openAnimation, {
71✔
466
            params: {
467
                duration: this.animationDuration + 'ms'
468
            }
469
        });
470

471
        return {
71✔
472
            openAnimation: openAnimation ? customOpenAnimation : null,
71✔
473
            closeAnimation: closeAnimation ? customCloseAnimation : null
71✔
474
        };
475
    }
476

477
    private updateStepAria(step: IgxStepComponent, index: number): void {
478
        step._index = index;
192✔
479
        step.renderer.setAttribute(step.nativeElement, 'aria-setsize', (this.steps.length).toString());
192✔
480
        step.renderer.setAttribute(step.nativeElement, 'aria-posinset', (index + 1).toString());
192✔
481
    }
482

483
    private handleStepChanges(): void {
484
        this._steps.changes.pipe(takeUntil(this.destroy$)).subscribe(steps => {
32✔
485
            Promise.resolve().then(() => {
7✔
486
                steps.forEach((step, index) => {
7✔
487
                    this.updateStepAria(step, index);
40✔
488
                });
489

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

503
    private activateFirstStep(activateInitially = false) {
2✔
504
        const firstEnabledStep = this.steps.find(s => !s.disabled);
4✔
505
        if (firstEnabledStep) {
3✔
506
            firstEnabledStep.active = true;
3✔
507
            if (activateInitially) {
3✔
508
                firstEnabledStep.activeChange.emit(true);
1✔
509
                this.activeStepChanged.emit({ owner: this, index: firstEnabledStep.index });
1✔
510
            }
511
        }
512
    }
513

514
    private activateStep(step: IgxStepComponent) {
515
        if (this.orientation === IgxStepperOrientation.Horizontal) {
34✔
516
            step.changeHorizontalActiveStep();
23✔
517
        } else {
518
            this.stepperService.expand(step);
11✔
519
        }
520
    }
521

522
    private moveToNextStep(next = true) {
33✔
523
        let steps: IgxStepComponent[] = this.steps;
33✔
524
        let activeStepIndex = this.stepperService.activeStep.index;
33✔
525
        if (!next) {
33!
526
            steps = this.steps.reverse();
×
527
            activeStepIndex = steps.findIndex(s => s === this.stepperService.activeStep);
×
528
        }
529

530
        const nextStep = steps.find((s, i) => i > activeStepIndex && s.isAccessible);
75✔
531
        if (nextStep) {
33✔
532
            this.activateStep(nextStep);
30✔
533
        }
534
    }
535
}
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

© 2026 Coveralls, Inc