• 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

82.97
/projects/igniteui-angular/stepper/src/stepper/step/step.component.ts
1
import { AfterViewInit, booleanAttribute, ChangeDetectorRef, Component, ContentChild, ElementRef, EventEmitter, forwardRef, HostBinding, HostListener, Input, OnDestroy, Output, Renderer2, TemplateRef, ViewChild, inject, ChangeDetectionStrategy } from '@angular/core';
2
import { takeUntil } from 'rxjs/operators';
3
import { IgxStep, IgxStepper, IgxStepperOrientation, IgxStepType, IGX_STEPPER_COMPONENT, IGX_STEP_COMPONENT, HorizontalAnimationType } from '../stepper.common';
4
import { IgxStepContentDirective, IgxStepIndicatorDirective } from '../stepper.directive';
5
import { IgxStepperService } from '../stepper.service';
6
import { NgClass, NgTemplateOutlet } from '@angular/common';
7
import { IgxRippleDirective } from 'igniteui-angular/directives';
8
import { ToggleAnimationPlayer, ToggleAnimationSettings } from 'igniteui-angular/expansion-panel';
9
import { CarouselAnimationDirection, IgxSlideComponentBase } from 'igniteui-angular/carousel';
10
import { isLeftToRight, PlatformUtil } from 'igniteui-angular/core';
11

12
let NEXT_ID = 0;
3✔
13

14
/**
15
 * The step is used within the stepper element and it holds the content of each step.
16
 * It also supports custom indicators, title and subtitle.
17
 *
18
 * @igxModule IgxStepperModule
19
 *
20
 * @igxKeywords step
21
 *
22
 * @example
23
 * ```html
24
 *  <igx-stepper>
25
 *  ...
26
 *    <igx-step [active]="true" [completed]="true">
27
 *      ...
28
 *    </igx-step>
29
 *  ...
30
 *  </igx-stepper>
31
 * ```
32
 */
33
@Component({
34
    selector: 'igx-step',
35
    templateUrl: 'step.component.html',
36
    providers: [
37
        { provide: IGX_STEP_COMPONENT, useExisting: IgxStepComponent }
38
    ],
39
    changeDetection: ChangeDetectionStrategy.Eager,
40
    imports: [NgClass, IgxRippleDirective, NgTemplateOutlet]
41
})
42
export class IgxStepComponent extends ToggleAnimationPlayer implements IgxStep, AfterViewInit, OnDestroy, IgxSlideComponentBase {
3✔
43
    public stepper = inject<IgxStepper>(IGX_STEPPER_COMPONENT);
180✔
44
    public cdr = inject(ChangeDetectorRef);
180✔
45
    public renderer = inject(Renderer2);
180✔
46
    protected platform = inject(PlatformUtil);
180✔
47
    protected stepperService = inject(IgxStepperService);
180✔
48
    private element = inject<ElementRef<HTMLElement>>(ElementRef);
180✔
49

50

51
    /**
52
     * Get/Set the `id` of the step component.
53
     * Default value is `"igx-step-0"`;
54
     * ```html
55
     * <igx-step id="my-first-step"></igx-step>
56
     * ```
57
     * ```typescript
58
     * const stepId = this.step.id;
59
     * ```
60
     */
61
    @HostBinding('attr.id')
62
    @Input()
63
    public id = `igx-step-${NEXT_ID++}`;
180✔
64

65
    /**
66
     * Get/Set whether the step is interactable.
67
     *
68
     * ```html
69
     * <igx-stepper>
70
     * ...
71
     *     <igx-step [disabled]="true"></igx-step>
72
     * ...
73
     * </igx-stepper>
74
     * ```
75
     *
76
     * ```typescript
77
     * this.stepper.steps[1].disabled = true;
78
     * ```
79
     */
80
    @Input({ transform: booleanAttribute })
81
    public set disabled(value: boolean) {
82
        this._disabled = value;
6✔
83
        if (this.stepper.linear) {
6✔
84
            this.stepperService.calculateLinearDisabledSteps();
2✔
85
        }
86
    }
87

88
    public get disabled(): boolean {
89
        return this._disabled;
2,221✔
90
    }
91

92
    /**
93
     * Get/Set whether the step is completed.
94
     *
95
     * @remarks
96
     * When set to `true` the following separator is styled `solid`.
97
     *
98
     * ```html
99
     * <igx-stepper>
100
     * ...
101
     *     <igx-step [completed]="true"></igx-step>
102
     * ...
103
     * </igx-stepper>
104
     * ```
105
     *
106
     * ```typescript
107
     * this.stepper.steps[1].completed = true;
108
     * ```
109
     */
110
    @Input({ transform: booleanAttribute })
111
    @HostBinding('class.igx-stepper__step--completed')
112
    public completed = false;
180✔
113

114
    /**
115
     * Get/Set whether the step is valid.
116
     *```html
117
     * <igx-step [isValid]="form.form.valid">
118
     *      ...
119
     *      <div igxStepContent>
120
     *          <form #form="ngForm">
121
     *              ...
122
     *          </form>
123
     *      </div>
124
     * </igx-step>
125
     * ```
126
     */
127
    @Input({ transform: booleanAttribute })
128
    public get isValid(): boolean {
129
        return this._valid;
5,151✔
130
    }
131

132
    public set isValid(value: boolean) {
133
        this._valid = value;
18✔
134
        if (this.stepper.linear && this.index !== undefined) {
18✔
135
            this.stepperService.calculateLinearDisabledSteps();
11✔
136
        }
137
    }
138

139
    /**
140
     * Get/Set whether the step is optional.
141
     *
142
     * @remarks
143
     * Optional steps validity does not affect the default behavior when the stepper is in linear mode i.e.
144
     * if optional step is invalid the user could still move to the next step.
145
     *
146
     * ```html
147
     * <igx-step [optional]="true"></igx-step>
148
     * ```
149
     * ```typescript
150
     * this.stepper.steps[1].optional = true;
151
     * ```
152
     */
153
    @Input({ transform: booleanAttribute })
154
    public optional = false;
180✔
155

156
    /**
157
     * Get/Set the active state of the step
158
     *
159
     * ```html
160
     * <igx-step [active]="true"></igx-step>
161
     * ```
162
     *
163
     * ```typescript
164
     * this.stepper.steps[1].active = true;
165
     * ```
166
     *
167
     * @param value: boolean
168
     */
169
    @HostBinding('attr.aria-selected')
170
    @Input({ transform: booleanAttribute })
171
    public set active(value: boolean) {
172
        if (value) {
94✔
173
            this.stepperService.expandThroughApi(this);
92✔
174
        } else {
175
            this.stepperService.collapse(this);
2✔
176
        }
177
    }
178

179
    public get active(): boolean {
180
        return this.stepperService.activeStep === this;
14,989✔
181
    }
182

183
    /** @hidden @internal */
184
    @HostBinding('attr.tabindex')
185
    @Input()
186
    public set tabIndex(value: number) {
187
        this._tabIndex = value;
148✔
188
    }
189

190
    public get tabIndex(): number {
191
        return this._tabIndex;
2,134✔
192
    }
193

194
    /** @hidden @internal **/
195
    @HostBinding('attr.role')
196
    public role = 'tab';
180✔
197

198
    /** @hidden @internal */
199
    @HostBinding('attr.aria-controls')
200
    public get contentId(): string {
201
        return this.content?.id;
2,119✔
202
    }
203

204
    /** @hidden @internal */
205
    @HostBinding('class.igx-stepper__step')
206
    public cssClass = true;
180✔
207

208
    /** @hidden @internal */
209
    @HostBinding('class.igx-stepper__step--disabled')
210
    public get generalDisabled(): boolean {
211
        return this.disabled || this.linearDisabled;
2,119✔
212
    }
213

214
    /** @hidden @internal */
215
    @HostBinding('class')
216
    public get titlePositionTop(): string {
217
        if (this.stepper.stepType !== IgxStepType.Full) {
2,119✔
218
            return 'igx-stepper__step--simple';
30✔
219
        }
220

221
        return `igx-stepper__step--${this.titlePosition}`;
2,089✔
222
    }
223

224
    /**
225
     * Emitted when the step's `active` property changes. Can be used for two-way binding.
226
     *
227
     * ```html
228
     * <igx-step [(active)]="this.isActive">
229
     * </igx-step>
230
     * ```
231
     *
232
     * ```typescript
233
     * const step: IgxStepComponent = this.stepper.step[0];
234
     * step.activeChange.subscribe((e: boolean) => console.log("Step active state change to ", e))
235
     * ```
236
     */
237
    @Output()
238
    public activeChange = new EventEmitter<boolean>();
180✔
239

240
    /** @hidden @internal */
241
    @ViewChild('contentTemplate', { static: true })
242
    public contentTemplate: TemplateRef<any>;
243

244
    /** @hidden @internal */
245
    @ViewChild('customIndicator', { static: true })
246
    public customIndicatorTemplate: TemplateRef<any>;
247

248
    /** @hidden @internal */
249
    @ViewChild('contentContainer')
250
    public contentContainer: ElementRef;
251

252
    /** @hidden @internal */
253
    @ContentChild(forwardRef(() => IgxStepIndicatorDirective))
4✔
254
    public indicator: IgxStepIndicatorDirective;
255

256
    /** @hidden @internal */
257
    @ContentChild(forwardRef(() => IgxStepContentDirective))
4✔
258
    public content: IgxStepContentDirective;
259

260
    /**
261
     * Get the step index inside of the stepper.
262
     *
263
     * ```typescript
264
     * const step = this.stepper.steps[1];
265
     * const stepIndex: number = step.index;
266
     * ```
267
     */
268
    public get index(): number {
269
        return this._index;
1,081✔
270
    }
271

272
    /** @hidden @internal */
273
    public get indicatorTemplate(): TemplateRef<any> {
274
        if (this.active && this.stepper.activeIndicatorTemplate) {
3,822✔
275
            return this.stepper.activeIndicatorTemplate;
868✔
276
        }
277

278
        if (!this.isValid && this.stepper.invalidIndicatorTemplate) {
2,954✔
279
            return this.stepper.invalidIndicatorTemplate;
20✔
280
        }
281

282
        if (this.completed && this.stepper.completedIndicatorTemplate) {
2,934✔
283
            return this.stepper.completedIndicatorTemplate;
8✔
284
        }
285

286
        if (this.indicator) {
2,926✔
287
            return this.customIndicatorTemplate;
2,458✔
288
        }
289

290
        return null;
468✔
291
    }
292

293
    /** @hidden @internal */
294
    public get direction(): CarouselAnimationDirection {
295
        return this.stepperService.previousActiveStep
64✔
296
            && this.stepperService.previousActiveStep.index > this.index
297
            ? CarouselAnimationDirection.PREV
298
            : CarouselAnimationDirection.NEXT;
299
    }
300

301
    /** @hidden @internal */
302
    public get isAccessible(): boolean {
303
        return !this.disabled && !this.linearDisabled;
70✔
304
    }
305

306
    /** @hidden @internal */
307
    public get isHorizontal(): boolean {
308
        return this.stepper.orientation === IgxStepperOrientation.Horizontal;
5,764✔
309
    }
310

311
    /** @hidden @internal */
312
    public get isTitleVisible(): boolean {
313
        return this.stepper.stepType !== IgxStepType.Indicator;
4,323✔
314
    }
315

316
    /** @hidden @internal */
317
    public get isIndicatorVisible(): boolean {
318
        return this.stepper.stepType !== IgxStepType.Title;
2,171✔
319
    }
320

321
    /** @hidden @internal */
322
    public get titlePosition(): string {
323
        return this.stepper.titlePosition ? this.stepper.titlePosition : this.stepper._defaultTitlePosition;
2,109✔
324
    }
325

326
    /** @hidden @internal */
327
    public get linearDisabled(): boolean {
328
        return this.stepperService.linearDisabledSteps.has(this);
2,176✔
329
    }
330

331
    /** @hidden @internal */
332
    public get collapsing(): boolean {
333
        return this.stepperService.collapsingSteps.has(this);
2,882✔
334
    }
335

336
    /** @hidden @internal */
337
    public override get animationSettings(): ToggleAnimationSettings {
338
        return this.stepper.verticalAnimationSettings;
24✔
339
    }
340

341
    /** @hidden @internal */
342
    public get contentClasses(): any {
343
        if (this.isHorizontal) {
3,605✔
344
            return { 'igx-stepper__body-content': true, 'igx-stepper__body-content--active': this.active };
3,201✔
345
        } else {
346
            return 'igx-stepper__step-content';
404✔
347
        }
348
    }
349

350
    /** @hidden @internal */
351
    public get stepHeaderClasses(): any {
352
        return {
2,155✔
353
            'igx-stepper__step--optional': this.optional,
354
            'igx-stepper__step-header--current': this.active,
355
            'igx-stepper__step-header--invalid': !this.isValid
2,225✔
356
                && this.stepperService.visitedSteps.has(this) && !this.active && this.isAccessible
357
        };
358
    }
359

360
    /** @hidden @internal */
361
    public get nativeElement(): HTMLElement {
362
        return this.element.nativeElement;
573✔
363
    }
364
    /** @hidden @internal */
365
    public previous: boolean;
366
    /** @hidden @internal */
367
    public _index: number;
368
    private _tabIndex = -1;
180✔
369
    private _valid = true;
180✔
370
    private _focused = false;
180✔
371
    private _disabled = false;
180✔
372

373
    /** @hidden @internal */
374
    @HostListener('focus')
375
    public onFocus(): void {
376
        this._focused = true;
15✔
377
        this.stepperService.focusedStep = this;
15✔
378
        if (this.stepperService.focusedStep !== this.stepperService.activeStep) {
15✔
379
            this.stepperService.activeStep.tabIndex = -1;
6✔
380
        }
381
    }
382

383
    /** @hidden @internal */
384
    @HostListener('blur')
385
    public onBlur(): void {
386
        this._focused = false;
9✔
387
        this.stepperService.activeStep.tabIndex = 0;
9✔
388
    }
389

390
    /** @hidden @internal */
391
    @HostListener('keydown', ['$event'])
392
    public handleKeydown(event: KeyboardEvent): void {
393
        if (!this._focused) {
10!
394
            return;
×
395
        }
396
        const key = event.key;
10✔
397
        if (this.stepper.orientation === IgxStepperOrientation.Horizontal) {
10✔
398
            if (key === this.platform.KEYMAP.ARROW_UP || key === this.platform.KEYMAP.ARROW_DOWN) {
8!
399
                return;
×
400
            }
401
        }
402
        if (!(this.platform.isNavigationKey(key) || this.platform.isActivationKey(event))) {
10!
403
            return;
×
404
        }
405
        event.preventDefault();
10✔
406
        this.handleNavigation(key);
10✔
407
    }
408

409
    /** @hidden @internal */
410
    public ngAfterViewInit(): void {
411
        this.openAnimationDone.pipe(takeUntil(this.destroy$)).subscribe(
180✔
412
            () => {
413
                if (this.stepperService.activeStep === this) {
11✔
414
                    this.stepper.activeStepChanged.emit({ owner: this.stepper, index: this.index });
11✔
415
                }
416
            }
417
        );
418
        this.closeAnimationDone.pipe(takeUntil(this.destroy$)).subscribe(() => {
180✔
419
            this.stepperService.collapse(this);
11✔
420
            this.cdr.markForCheck();
11✔
421
        });
422
    }
423

424
    /** @hidden @internal */
425
    public onPointerDown(event: MouseEvent): void {
426
        event.stopPropagation();
×
427
        if (this.isHorizontal) {
×
428
            this.changeHorizontalActiveStep();
×
429
        } else {
430
            this.changeVerticalActiveStep();
×
431
        }
432
    }
433

434
    /** @hidden @internal */
435
    public handleNavigation(key: string): void {
436
        const rtl = !isLeftToRight(this.nativeElement);
10✔
437

438
        switch (key) {
10!
439
            case this.platform.KEYMAP.HOME:
440
                this.stepper.steps.filter(s => s.isAccessible)[0]?.nativeElement.focus();
5✔
441
                break;
1✔
442
            case this.platform.KEYMAP.END:
443
                this.stepper.steps.filter(s => s.isAccessible).pop()?.nativeElement.focus();
5✔
444
                break;
1✔
445
            case this.platform.KEYMAP.ARROW_UP:
446
                this.previousStep?.nativeElement.focus();
1✔
447
                break;
1✔
448
            case this.platform.KEYMAP.ARROW_LEFT:
449
                if (rtl && this.stepper.orientation === IgxStepperOrientation.Horizontal) {
1!
450
                    this.nextStep?.nativeElement.focus();
×
451
                } else {
452
                    this.previousStep?.nativeElement.focus();
1✔
453
                }
454
                break;
1✔
455
            case this.platform.KEYMAP.ARROW_DOWN:
456
                this.nextStep?.nativeElement.focus();
1✔
457
                break;
1✔
458
            case this.platform.KEYMAP.ARROW_RIGHT:
459
                if (rtl && this.stepper.orientation === IgxStepperOrientation.Horizontal) {
1!
460
                    this.previousStep?.nativeElement.focus();
×
461
                } else {
462
                    this.nextStep?.nativeElement.focus();
1✔
463
                }
464
                break;
1✔
465
            case this.platform.KEYMAP.SPACE:
466
            case this.platform.KEYMAP.ENTER:
467
                if (this.isHorizontal) {
4!
468
                    this.changeHorizontalActiveStep();
4✔
469
                } else {
470
                    this.changeVerticalActiveStep();
×
471
                }
472
                break;
4✔
473
            default:
474
                return;
×
475
        }
476
    }
477

478
    /** @hidden @internal */
479
    public changeHorizontalActiveStep(): void {
480
        if (this.stepper.animationType === HorizontalAnimationType.none && this.stepperService.activeStep !== this) {
27✔
481
            const argsCanceled = this.stepperService.emitActivatingEvent(this);
1✔
482
            if (argsCanceled) {
1!
483
                return;
×
484
            }
485

486
            this.active = true;
1✔
487
            this.stepper.activeStepChanged.emit({ owner: this.stepper, index: this.index });
1✔
488
            return;
1✔
489
        }
490
        this.stepperService.expand(this);
26✔
491
        if (this.stepper.animationType === HorizontalAnimationType.fade) {
26✔
492
            if (this.stepperService.collapsingSteps.has(this.stepperService.previousActiveStep)) {
2✔
493
                this.stepperService.previousActiveStep.active = false;
2✔
494
            }
495
        }
496
    }
497

498
    private get nextStep(): IgxStepComponent | null {
499
        const focusedStep = this.stepperService.focusedStep;
2✔
500
        if (focusedStep) {
2✔
501
            if (focusedStep.index === this.stepper.steps.length - 1) {
2!
502
                return this.stepper.steps.find(s => s.isAccessible);
×
503
            }
504

505
            const nextAccessible = this.stepper.steps.find((s, i) => i > focusedStep.index && s.isAccessible);
4✔
506
            return nextAccessible ? nextAccessible : this.stepper.steps.find(s => s.isAccessible);
2!
507
        }
508

509
        return null;
×
510
    }
511

512
    private get previousStep(): IgxStepComponent | null {
513
        const focusedStep = this.stepperService.focusedStep;
2✔
514
        if (focusedStep) {
2✔
515
            if (focusedStep.index === 0) {
2!
516
                return this.stepper.steps.filter(s => s.isAccessible).pop();
×
517
            }
518

519
            let prevStep;
520
            for (let i = focusedStep.index - 1; i >= 0; i--) {
2✔
521
                const step = this.stepper.steps[i];
2✔
522
                if (step.isAccessible) {
2✔
523
                    prevStep = step;
2✔
524
                    break;
2✔
525
                }
526
            }
527

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

530
        }
531

532
        return null;
×
533
    }
534

535
    private changeVerticalActiveStep(): void {
536
        this.stepperService.expand(this);
×
537

538
        if (!this.animationSettings.closeAnimation) {
×
539
            this.stepperService.previousActiveStep?.openAnimationPlayer?.finish();
×
540
        }
541

542
        if (!this.animationSettings.openAnimation) {
×
543
            this.stepperService.activeStep.closeAnimationPlayer?.finish();
×
544
        }
545
    }
546
}
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