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

IgniteUI / igniteui-angular / 26023601418

18 May 2026 08:57AM UTC coverage: 4.854% (-85.3%) from 90.174%
26023601418

Pull #17281

github

web-flow
Merge e7ce7a18e into 5a85df190
Pull Request #17281: feat: Added virtual scroll component and sample implementation

400 of 17347 branches covered (2.31%)

Branch coverage included in aggregate %.

63 of 222 new or added lines in 4 files covered. (28.38%)

27932 existing lines in 341 files now uncovered.

2022 of 32547 relevant lines covered (6.21%)

0.72 hits per line

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

0.87
/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 } 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 IgxStepComponent is used within the `igx-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
    imports: [NgClass, IgxRippleDirective, NgTemplateOutlet]
40
})
41
export class IgxStepComponent extends ToggleAnimationPlayer implements IgxStep, AfterViewInit, OnDestroy, IgxSlideComponentBase {
3✔
UNCOV
42
    public stepper = inject<IgxStepper>(IGX_STEPPER_COMPONENT);
×
UNCOV
43
    public cdr = inject(ChangeDetectorRef);
×
UNCOV
44
    public renderer = inject(Renderer2);
×
UNCOV
45
    protected platform = inject(PlatformUtil);
×
UNCOV
46
    protected stepperService = inject(IgxStepperService);
×
UNCOV
47
    private element = inject<ElementRef<HTMLElement>>(ElementRef);
×
48

49

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

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

87
    public get disabled(): boolean {
UNCOV
88
        return this._disabled;
×
89
    }
90

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

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

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

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

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

178
    public get active(): boolean {
UNCOV
179
        return this.stepperService.activeStep === this;
×
180
    }
181

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

189
    public get tabIndex(): number {
UNCOV
190
        return this._tabIndex;
×
191
    }
192

193
    /** @hidden @internal **/
194
    @HostBinding('attr.role')
UNCOV
195
    public role = 'tab';
×
196

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

203
    /** @hidden @internal */
204
    @HostBinding('class.igx-stepper__step')
UNCOV
205
    public cssClass = true;
×
206

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

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

UNCOV
220
        return `igx-stepper__step--${this.titlePosition}`;
×
221
    }
222

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

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

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

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

251
    /** @hidden @internal */
UNCOV
252
    @ContentChild(forwardRef(() => IgxStepIndicatorDirective))
×
253
    public indicator: IgxStepIndicatorDirective;
254

255
    /** @hidden @internal */
UNCOV
256
    @ContentChild(forwardRef(() => IgxStepContentDirective))
×
257
    public content: IgxStepContentDirective;
258

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

271
    /** @hidden @internal */
272
    public get indicatorTemplate(): TemplateRef<any> {
UNCOV
273
        if (this.active && this.stepper.activeIndicatorTemplate) {
×
UNCOV
274
            return this.stepper.activeIndicatorTemplate;
×
275
        }
276

UNCOV
277
        if (!this.isValid && this.stepper.invalidIndicatorTemplate) {
×
UNCOV
278
            return this.stepper.invalidIndicatorTemplate;
×
279
        }
280

UNCOV
281
        if (this.completed && this.stepper.completedIndicatorTemplate) {
×
UNCOV
282
            return this.stepper.completedIndicatorTemplate;
×
283
        }
284

UNCOV
285
        if (this.indicator) {
×
UNCOV
286
            return this.customIndicatorTemplate;
×
287
        }
288

UNCOV
289
        return null;
×
290
    }
291

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

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

305
    /** @hidden @internal */
306
    public get isHorizontal(): boolean {
UNCOV
307
        return this.stepper.orientation === IgxStepperOrientation.Horizontal;
×
308
    }
309

310
    /** @hidden @internal */
311
    public get isTitleVisible(): boolean {
UNCOV
312
        return this.stepper.stepType !== IgxStepType.Indicator;
×
313
    }
314

315
    /** @hidden @internal */
316
    public get isIndicatorVisible(): boolean {
UNCOV
317
        return this.stepper.stepType !== IgxStepType.Title;
×
318
    }
319

320
    /** @hidden @internal */
321
    public get titlePosition(): string {
UNCOV
322
        return this.stepper.titlePosition ? this.stepper.titlePosition : this.stepper._defaultTitlePosition;
×
323
    }
324

325
    /** @hidden @internal */
326
    public get linearDisabled(): boolean {
UNCOV
327
        return this.stepperService.linearDisabledSteps.has(this);
×
328
    }
329

330
    /** @hidden @internal */
331
    public get collapsing(): boolean {
UNCOV
332
        return this.stepperService.collapsingSteps.has(this);
×
333
    }
334

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

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

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

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

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

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

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

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

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

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

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

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

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

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

UNCOV
504
            const nextAccessible = this.stepper.steps.find((s, i) => i > focusedStep.index && s.isAccessible);
×
UNCOV
505
            return nextAccessible ? nextAccessible : this.stepper.steps.find(s => s.isAccessible);
×
506
        }
507

508
        return null;
×
509
    }
510

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

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

UNCOV
527
            return prevStep ? prevStep : this.stepper.steps.filter(s => s.isAccessible).pop();
×
528

529
        }
530

531
        return null;
×
532
    }
533

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

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

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