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

IgniteUI / igniteui-angular / 29572405564

17 Jul 2026 10:07AM UTC coverage: 90.116% (-0.02%) from 90.135%
29572405564

Pull #15125

github

web-flow
Merge 637b7229e 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%)

34495.14 hits per line

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

83.33
/projects/igniteui-angular/stepper/src/stepper/step/step.component.ts
1
import {
2
    AfterViewInit,
3
    booleanAttribute,
4
    ChangeDetectorRef,
5
    Component,
6
    ContentChild,
7
    ElementRef,
8
    EventEmitter,
9
    forwardRef,
10
    HostBinding,
11
    HostListener,
12
    Input,
13
    OnDestroy,
14
    Output,
15
    Renderer2,
16
    TemplateRef,
17
    ViewChild,
18
    inject,
19
    ChangeDetectionStrategy,
20
    ViewEncapsulation
21
} from '@angular/core';
22
import { takeUntil } from 'rxjs/operators';
23
import { IgxStep, IgxStepper, IgxStepperOrientation, IgxStepType, IGX_STEPPER_COMPONENT, IGX_STEP_COMPONENT, HorizontalAnimationType } from '../stepper.common';
24
import { IgxStepContentDirective, IgxStepIndicatorDirective } from '../stepper.directive';
25
import { IgxStepperService } from '../stepper.service';
26
import { NgClass, NgTemplateOutlet } from '@angular/common';
27
import { IgxRippleDirective } from 'igniteui-angular/directives';
28
import { ToggleAnimationPlayer, ToggleAnimationSettings } from 'igniteui-angular/expansion-panel';
29
import { CarouselAnimationDirection, IgxSlideComponentBase } from 'igniteui-angular/carousel';
30
import { isLeftToRight, PlatformUtil } from 'igniteui-angular/core';
31

32
let NEXT_ID = 0;
3✔
33

34
/**
35
 * The step is used within the stepper element and it holds the content of each step.
36
 * It also supports custom indicators, title and subtitle.
37
 *
38
 * @igxModule IgxStepperModule
39
 *
40
 * @igxKeywords step
41
 *
42
 * @example
43
 * ```html
44
 *  <igx-stepper>
45
 *  ...
46
 *    <igx-step [active]="true" [completed]="true">
47
 *      ...
48
 *    </igx-step>
49
 *  ...
50
 *  </igx-stepper>
51
 * ```
52
 */
53
@Component({
54
    selector: 'igx-step',
55
    templateUrl: 'step.component.html',
56
    styleUrl: 'step.component.css',
57
    encapsulation: ViewEncapsulation.None,
58
    providers: [
59
        { provide: IGX_STEP_COMPONENT, useExisting: IgxStepComponent }
60
    ],
61
    changeDetection: ChangeDetectionStrategy.Eager,
62
    imports: [NgClass, IgxRippleDirective, NgTemplateOutlet]
63
})
64
export class IgxStepComponent extends ToggleAnimationPlayer implements IgxStep, AfterViewInit, OnDestroy, IgxSlideComponentBase {
3✔
65
    public stepper = inject<IgxStepper>(IGX_STEPPER_COMPONENT);
180✔
66
    public cdr = inject(ChangeDetectorRef);
180✔
67
    public renderer = inject(Renderer2);
180✔
68
    protected platform = inject(PlatformUtil);
180✔
69
    protected stepperService = inject(IgxStepperService);
180✔
70
    private element = inject<ElementRef<HTMLElement>>(ElementRef);
180✔
71

72

73
    /**
74
     * Get/Set the `id` of the step component.
75
     * Default value is `"igx-step-0"`;
76
     * ```html
77
     * <igx-step id="my-first-step"></igx-step>
78
     * ```
79
     * ```typescript
80
     * const stepId = this.step.id;
81
     * ```
82
     */
83
    @HostBinding('attr.id')
84
    @Input()
85
    public id = `igx-step-${NEXT_ID++}`;
180✔
86

87
    /**
88
     * Get/Set whether the step is interactable.
89
     *
90
     * ```html
91
     * <igx-stepper>
92
     * ...
93
     *     <igx-step [disabled]="true"></igx-step>
94
     * ...
95
     * </igx-stepper>
96
     * ```
97
     *
98
     * ```typescript
99
     * this.stepper.steps[1].disabled = true;
100
     * ```
101
     */
102
    @Input({ transform: booleanAttribute })
103
    public set disabled(value: boolean) {
104
        this._disabled = value;
6✔
105
        if (this.stepper.linear) {
6✔
106
            this.stepperService.calculateLinearDisabledSteps();
2✔
107
        }
108
    }
109

110
    public get disabled(): boolean {
111
        return this._disabled;
4,382✔
112
    }
113

114
    /**
115
     * Get/Set whether the step is completed.
116
     *
117
     * @remarks
118
     * When set to `true` the following separator is styled `solid`.
119
     *
120
     * ```html
121
     * <igx-stepper>
122
     * ...
123
     *     <igx-step [completed]="true"></igx-step>
124
     * ...
125
     * </igx-stepper>
126
     * ```
127
     *
128
     * ```typescript
129
     * this.stepper.steps[1].completed = true;
130
     * ```
131
     */
132
    @Input({ transform: booleanAttribute })
133
    @HostBinding('class.igx-step--completed')
134
    public completed = false;
180✔
135

136
    /**
137
     * Get/Set whether the step is valid.
138
     *```html
139
     * <igx-step [isValid]="form.form.valid">
140
     *      ...
141
     *      <div igxStepContent>
142
     *          <form #form="ngForm">
143
     *              ...
144
     *          </form>
145
     *      </div>
146
     * </igx-step>
147
     * ```
148
     */
149
    @Input({ transform: booleanAttribute })
150
    public get isValid(): boolean {
151
        return this._valid;
7,270✔
152
    }
153

154
    public set isValid(value: boolean) {
155
        this._valid = value;
18✔
156
        if (this.stepper.linear && this.index !== undefined) {
18✔
157
            this.stepperService.calculateLinearDisabledSteps();
11✔
158
        }
159
    }
160

161
    /**
162
     * Get/Set whether the step is optional.
163
     *
164
     * @remarks
165
     * Optional steps validity does not affect the default behavior when the stepper is in linear mode i.e.
166
     * if optional step is invalid the user could still move to the next step.
167
     *
168
     * ```html
169
     * <igx-step [optional]="true"></igx-step>
170
     * ```
171
     * ```typescript
172
     * this.stepper.steps[1].optional = true;
173
     * ```
174
     */
175
    @Input({ transform: booleanAttribute })
176
    public optional = false;
180✔
177

178
    /**
179
     * Get/Set the active state of the step
180
     *
181
     * ```html
182
     * <igx-step [active]="true"></igx-step>
183
     * ```
184
     *
185
     * ```typescript
186
     * this.stepper.steps[1].active = true;
187
     * ```
188
     *
189
     * @param value: boolean
190
     */
191
    @HostBinding('attr.aria-selected')
192
    @Input({ transform: booleanAttribute })
193
    public set active(value: boolean) {
194
        if (value) {
94✔
195
            this.stepperService.expandThroughApi(this);
92✔
196
        } else {
197
            this.stepperService.collapse(this);
2✔
198
        }
199
    }
200

201
    public get active(): boolean {
202
        return this.stepperService.activeStep === this;
17,134✔
203
    }
204

205
    /** @hidden @internal */
206
    @HostBinding('attr.tabindex')
207
    @Input()
208
    public set tabIndex(value: number) {
209
        this._tabIndex = value;
148✔
210
    }
211

212
    public get tabIndex(): number {
213
        return this._tabIndex;
2,134✔
214
    }
215

216
    /** @hidden @internal **/
217
    @HostBinding('attr.role')
218
    public role = 'tab';
180✔
219

220
    /** @hidden @internal */
221
    @HostBinding('attr.aria-controls')
222
    public get contentId(): string {
223
        return this.content?.id;
2,119✔
224
    }
225

226
    /** @hidden @internal */
227
    @HostBinding('class.igx-step')
228
    public cssClass = true;
180✔
229

230
    /** @hidden @internal */
231
    @HostBinding('class.igx-step--vertical')
232
    public get isVertical(): boolean {
233
        return this.stepper.orientation === 'vertical';
2,119✔
234
    }
235

236
    /** @hidden @internal */
237
    @HostBinding('class.igx-step--disabled')
238
    public get generalDisabled(): boolean {
239
        return this.disabled || this.linearDisabled;
2,119✔
240
    }
241

242
    /** @hidden @internal */
243
    @HostBinding('class')
244
    public get titlePositionTop(): string {
245
        if (this.stepper.stepType !== IgxStepType.Full) {
2,119✔
246
            return 'igx-step--simple';
30✔
247
        }
248

249
        return `igx-step--${this.titlePosition}`;
2,089✔
250
    }
251

252
    /** @hidden @internal */
253
    @HostBinding('class.igx-step--current')
254
    public get isActive(): boolean {
255
        return this.active;
2,119✔
256
    }
257

258
    /** @hidden @internal */
259
    @HostBinding('class.igx-step--invalid')
260
    public get isInvalid(): boolean {
261
        return !this.isValid
4,274✔
262
            && this.stepperService.visitedSteps.has(this)
263
            && !this.active
264
            && this.isAccessible;
265
    }
266

267
    /** @hidden @internal */
268
    public get stepHeaderClasses(): { [key: string]: boolean } {
269
        return {
2,155✔
270
            'igx-step-header--invalid': this.isInvalid,
271
            'igx-step-header--disabled': this.disabled || this.linearDisabled,
4,284✔
272
            'igx-step-header--current': this.active,
273
            'igx-step-header--completed': this.completed,
274
        };
275
    }
276

277
    /**
278
     * Emitted when the step's `active` property changes. Can be used for two-way binding.
279
     *
280
     * ```html
281
     * <igx-step [(active)]="this.isActive">
282
     * </igx-step>
283
     * ```
284
     *
285
     * ```typescript
286
     * const step: IgxStepComponent = this.stepper.step[0];
287
     * step.activeChange.subscribe((e: boolean) => console.log("Step active state change to ", e))
288
     * ```
289
     */
290
    @Output()
291
    public activeChange = new EventEmitter<boolean>();
180✔
292

293
    /** @hidden @internal */
294
    @ViewChild('contentTemplate', { static: true })
295
    public contentTemplate: TemplateRef<any>;
296

297
    /** @hidden @internal */
298
    @ViewChild('customIndicator', { static: true })
299
    public customIndicatorTemplate: TemplateRef<any>;
300

301
    /** @hidden @internal */
302
    @ViewChild('contentContainer')
303
    public contentContainer: ElementRef;
304

305
    /** @hidden @internal */
306
    @ContentChild(forwardRef(() => IgxStepIndicatorDirective))
4✔
307
    public indicator: IgxStepIndicatorDirective;
308

309
    /** @hidden @internal */
310
    @ContentChild(forwardRef(() => IgxStepContentDirective))
4✔
311
    public content: IgxStepContentDirective;
312

313
    /**
314
     * Get the step index inside of the stepper.
315
     *
316
     * ```typescript
317
     * const step = this.stepper.steps[1];
318
     * const stepIndex: number = step.index;
319
     * ```
320
     */
321
    public get index(): number {
322
        return this._index;
1,081✔
323
    }
324

325
    /** @hidden @internal */
326
    public get indicatorTemplate(): TemplateRef<any> {
327
        if (this.active && this.stepper.activeIndicatorTemplate) {
3,822✔
328
            return this.stepper.activeIndicatorTemplate;
868✔
329
        }
330

331
        if (!this.isValid && this.stepper.invalidIndicatorTemplate) {
2,954✔
332
            return this.stepper.invalidIndicatorTemplate;
20✔
333
        }
334

335
        if (this.completed && this.stepper.completedIndicatorTemplate) {
2,934✔
336
            return this.stepper.completedIndicatorTemplate;
8✔
337
        }
338

339
        if (this.indicator) {
2,926✔
340
            return this.customIndicatorTemplate;
2,458✔
341
        }
342

343
        return null;
468✔
344
    }
345

346
    /** @hidden @internal */
347
    public get direction(): CarouselAnimationDirection {
348
        return this.stepperService.previousActiveStep
64✔
349
            && this.stepperService.previousActiveStep.index > this.index
350
            ? CarouselAnimationDirection.PREV
351
            : CarouselAnimationDirection.NEXT;
352
    }
353

354
    /** @hidden @internal */
355
    public get isAccessible(): boolean {
356
        return !this.disabled && !this.linearDisabled;
76✔
357
    }
358

359
    /** @hidden @internal */
360
    @HostBinding('class.igx-step--horizontal')
361
    public get isHorizontal(): boolean {
362
        return this.stepper.orientation === IgxStepperOrientation.Horizontal;
7,883✔
363
    }
364

365
    /** @hidden @internal */
366
    public get isTitleVisible(): boolean {
367
        return this.stepper.stepType !== IgxStepType.Indicator;
4,323✔
368
    }
369

370
    /** @hidden @internal */
371
    public get isIndicatorVisible(): boolean {
372
        return this.stepper.stepType !== IgxStepType.Title;
2,171✔
373
    }
374

375
    /** @hidden @internal */
376
    public get titlePosition(): string {
377
        return this.stepper.titlePosition ? this.stepper.titlePosition : this.stepper._defaultTitlePosition;
2,109✔
378
    }
379

380
    /** @hidden @internal */
381
    public get linearDisabled(): boolean {
382
        return this.stepperService.linearDisabledSteps.has(this);
4,309✔
383
    }
384

385
    /** @hidden @internal */
386
    public get collapsing(): boolean {
387
        return this.stepperService.collapsingSteps.has(this);
2,882✔
388
    }
389

390
    /** @hidden @internal */
391
    public override get animationSettings(): ToggleAnimationSettings {
392
        return this.stepper.verticalAnimationSettings;
24✔
393
    }
394

395
    /** @hidden @internal */
396
    public get contentClasses(): any {
397
        if (this.isHorizontal) {
3,605✔
398
            return { 'igx-stepper__body-content': true, 'igx-stepper__body-content--active': this.active };
3,201✔
399
        } else {
400
            return 'igx-stepper__step-content';
404✔
401
        }
402
    }
403

404
/** @hidden @internal */
405
    public get nativeElement(): HTMLElement {
406
        return this.element.nativeElement;
573✔
407
    }
408
    /** @hidden @internal */
409
    public previous: boolean;
410
    /** @hidden @internal */
411
    public _index: number;
412
    private _tabIndex = -1;
180✔
413
    private _valid = true;
180✔
414
    private _focused = false;
180✔
415
    private _disabled = false;
180✔
416

417
    /** @hidden @internal */
418
    @HostListener('focus')
419
    public onFocus(): void {
420
        this._focused = true;
15✔
421
        this.stepperService.focusedStep = this;
15✔
422
        if (this.stepperService.focusedStep !== this.stepperService.activeStep) {
15✔
423
            this.stepperService.activeStep.tabIndex = -1;
6✔
424
        }
425
    }
426

427
    /** @hidden @internal */
428
    @HostListener('blur')
429
    public onBlur(): void {
430
        this._focused = false;
9✔
431
        this.stepperService.activeStep.tabIndex = 0;
9✔
432
    }
433

434
    /** @hidden @internal */
435
    @HostListener('keydown', ['$event'])
436
    public handleKeydown(event: KeyboardEvent): void {
437
        if (!this._focused) {
10!
438
            return;
×
439
        }
440
        const key = event.key;
10✔
441
        if (this.stepper.orientation === IgxStepperOrientation.Horizontal) {
10✔
442
            if (key === this.platform.KEYMAP.ARROW_UP || key === this.platform.KEYMAP.ARROW_DOWN) {
8!
443
                return;
×
444
            }
445
        }
446
        if (!(this.platform.isNavigationKey(key) || this.platform.isActivationKey(event))) {
10!
447
            return;
×
448
        }
449
        event.preventDefault();
10✔
450
        this.handleNavigation(key);
10✔
451
    }
452

453
    /** @hidden @internal */
454
    public ngAfterViewInit(): void {
455
        this.openAnimationDone.pipe(takeUntil(this.destroy$)).subscribe(
180✔
456
            () => {
457
                if (this.stepperService.activeStep === this) {
11✔
458
                    this.stepper.activeStepChanged.emit({ owner: this.stepper, index: this.index });
11✔
459
                }
460
            }
461
        );
462
        this.closeAnimationDone.pipe(takeUntil(this.destroy$)).subscribe(() => {
180✔
463
            this.stepperService.collapse(this);
11✔
464
            this.cdr.markForCheck();
11✔
465
        });
466
    }
467

468
    /** @hidden @internal */
469
    public onPointerDown(event: MouseEvent): void {
470
        event.stopPropagation();
×
471
        if (this.isHorizontal) {
×
472
            this.changeHorizontalActiveStep();
×
473
        } else {
474
            this.changeVerticalActiveStep();
×
475
        }
476
    }
477

478
    /** @hidden @internal */
479
    public handleNavigation(key: string): void {
480
        const rtl = !isLeftToRight(this.nativeElement);
10✔
481

482
        switch (key) {
10!
483
            case this.platform.KEYMAP.HOME:
484
                this.stepper.steps.filter(s => s.isAccessible)[0]?.nativeElement.focus();
5✔
485
                break;
1✔
486
            case this.platform.KEYMAP.END:
487
                this.stepper.steps.filter(s => s.isAccessible).pop()?.nativeElement.focus();
5✔
488
                break;
1✔
489
            case this.platform.KEYMAP.ARROW_UP:
490
                this.previousStep?.nativeElement.focus();
1✔
491
                break;
1✔
492
            case this.platform.KEYMAP.ARROW_LEFT:
493
                if (rtl && this.stepper.orientation === IgxStepperOrientation.Horizontal) {
1!
494
                    this.nextStep?.nativeElement.focus();
×
495
                } else {
496
                    this.previousStep?.nativeElement.focus();
1✔
497
                }
498
                break;
1✔
499
            case this.platform.KEYMAP.ARROW_DOWN:
500
                this.nextStep?.nativeElement.focus();
1✔
501
                break;
1✔
502
            case this.platform.KEYMAP.ARROW_RIGHT:
503
                if (rtl && this.stepper.orientation === IgxStepperOrientation.Horizontal) {
1!
504
                    this.previousStep?.nativeElement.focus();
×
505
                } else {
506
                    this.nextStep?.nativeElement.focus();
1✔
507
                }
508
                break;
1✔
509
            case this.platform.KEYMAP.SPACE:
510
            case this.platform.KEYMAP.ENTER:
511
                if (this.isHorizontal) {
4!
512
                    this.changeHorizontalActiveStep();
4✔
513
                } else {
514
                    this.changeVerticalActiveStep();
×
515
                }
516
                break;
4✔
517
            default:
518
                return;
×
519
        }
520
    }
521

522
    /** @hidden @internal */
523
    public changeHorizontalActiveStep(): void {
524
        if (this.stepper.animationType === HorizontalAnimationType.none && this.stepperService.activeStep !== this) {
27✔
525
            const argsCanceled = this.stepperService.emitActivatingEvent(this);
1✔
526
            if (argsCanceled) {
1!
527
                return;
×
528
            }
529

530
            this.active = true;
1✔
531
            this.stepper.activeStepChanged.emit({ owner: this.stepper, index: this.index });
1✔
532
            return;
1✔
533
        }
534
        this.stepperService.expand(this);
26✔
535
        if (this.stepper.animationType === HorizontalAnimationType.fade) {
26✔
536
            if (this.stepperService.collapsingSteps.has(this.stepperService.previousActiveStep)) {
2✔
537
                this.stepperService.previousActiveStep.active = false;
2✔
538
            }
539
        }
540
    }
541

542
    private get nextStep(): IgxStepComponent | null {
543
        const focusedStep = this.stepperService.focusedStep;
2✔
544
        if (focusedStep) {
2✔
545
            if (focusedStep.index === this.stepper.steps.length - 1) {
2!
546
                return this.stepper.steps.find(s => s.isAccessible);
×
547
            }
548

549
            const nextAccessible = this.stepper.steps.find((s, i) => i > focusedStep.index && s.isAccessible);
4✔
550
            return nextAccessible ? nextAccessible : this.stepper.steps.find(s => s.isAccessible);
2!
551
        }
552

553
        return null;
×
554
    }
555

556
    private get previousStep(): IgxStepComponent | null {
557
        const focusedStep = this.stepperService.focusedStep;
2✔
558
        if (focusedStep) {
2✔
559
            if (focusedStep.index === 0) {
2!
560
                return this.stepper.steps.filter(s => s.isAccessible).pop();
×
561
            }
562

563
            let prevStep;
564
            for (let i = focusedStep.index - 1; i >= 0; i--) {
2✔
565
                const step = this.stepper.steps[i];
2✔
566
                if (step.isAccessible) {
2✔
567
                    prevStep = step;
2✔
568
                    break;
2✔
569
                }
570
            }
571

572
            return prevStep ? prevStep : this.stepper.steps.filter(s => s.isAccessible).pop();
2!
573

574
        }
575

576
        return null;
×
577
    }
578

579
    private changeVerticalActiveStep(): void {
580
        this.stepperService.expand(this);
×
581

582
        if (!this.animationSettings.closeAnimation) {
×
583
            this.stepperService.previousActiveStep?.openAnimationPlayer?.finish();
×
584
        }
585

586
        if (!this.animationSettings.openAnimation) {
×
587
            this.stepperService.activeStep.closeAnimationPlayer?.finish();
×
588
        }
589
    }
590
}
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