UNPKG

igniteui-angular

Version:

Ignite UI for Angular is a dependency-free Angular toolkit for building modern web apps

1,305 lines (1,300 loc) 60.1 kB
import * as i0 from '@angular/core'; import { InjectionToken, Injectable, Directive, HostBinding, inject, ElementRef, Input, ChangeDetectorRef, Renderer2, EventEmitter, booleanAttribute, HostListener, forwardRef, ContentChild, ViewChild, Output, Component, TemplateRef, ContentChildren, NgModule } from '@angular/core'; import { takeUntil } from 'rxjs/operators'; import { CarouselAnimationType, CarouselAnimationDirection, IgxCarouselComponentBase } from 'igniteui-angular/carousel'; import { NgClass, NgTemplateOutlet } from '@angular/common'; import { IgxRippleDirective } from 'igniteui-angular/directives'; import { ToggleAnimationPlayer } from 'igniteui-angular/expansion-panel'; import { PlatformUtil, ɵIgxDirectionality as _IgxDirectionality } from 'igniteui-angular/core'; import { useAnimation } from '@angular/animations'; import { Subject } from 'rxjs'; import { fadeIn, growVerIn, growVerOut } from 'igniteui-angular/animations'; // Enums const IgxStepperOrientation = { Horizontal: 'horizontal', Vertical: 'vertical' }; const IgxStepType = { Indicator: 'indicator', Title: 'title', Full: 'full' }; const IgxStepperTitlePosition = { Bottom: 'bottom', Top: 'top', End: 'end', Start: 'start' }; const VerticalAnimationType = { Grow: 'grow', Fade: 'fade', None: 'none' }; const HorizontalAnimationType = { ...CarouselAnimationType }; // Token const IGX_STEPPER_COMPONENT = /*@__PURE__*/ new InjectionToken('IgxStepperToken'); const IGX_STEP_COMPONENT = /*@__PURE__*/ new InjectionToken('IgxStepToken'); /** @hidden @internal */ class IgxStepperService { constructor() { this.collapsingSteps = new Set(); this.linearDisabledSteps = new Set(); this.visitedSteps = new Set(); } /** * Activates the step, fires the steps change event and plays animations. */ expand(step) { if (this.activeStep === step) { return; } const cancel = this.emitActivatingEvent(step); if (cancel) { return; } this.collapsingSteps.delete(step); this.previousActiveStep = this.activeStep; this.activeStep = step; this.activeStep.activeChange.emit(true); this.collapsingSteps.add(this.previousActiveStep); this.visitedSteps.add(this.activeStep); if (this.stepper.orientation === IgxStepperOrientation.Vertical) { this.previousActiveStep.playCloseAnimation(this.previousActiveStep.contentContainer); this.activeStep.cdr.detectChanges(); this.activeStep.playOpenAnimation(this.activeStep.contentContainer); } else { this.activeStep.cdr.detectChanges(); this.stepper.playHorizontalAnimations(); } } /** * Activates the step and fires the steps change event without playing animations. */ expandThroughApi(step) { if (this.activeStep === step) { return; } this.collapsingSteps.clear(); this.previousActiveStep = this.activeStep; this.activeStep = step; if (this.previousActiveStep) { this.previousActiveStep.tabIndex = -1; } this.activeStep.tabIndex = 0; this.visitedSteps.add(this.activeStep); this.activeStep.cdr.markForCheck(); this.previousActiveStep?.cdr.markForCheck(); this.activeStep.activeChange.emit(true); this.previousActiveStep?.activeChange.emit(false); } /** * Collapses the currently active step and fires the change event. */ collapse(step) { if (this.activeStep === step) { return; } step.activeChange.emit(false); this.collapsingSteps.delete(step); } /** * Determines the steps that should be marked as visited based on the active step. */ calculateVisitedSteps() { this.stepper.steps.forEach(step => { if (step.index <= this.activeStep.index) { this.visitedSteps.add(step); } else { this.visitedSteps.delete(step); } }); } /** * Determines the steps that should be disabled in linear mode based on the validity of the active step. */ calculateLinearDisabledSteps() { if (!this.activeStep) { return; } if (this.activeStep.isValid) { const firstRequiredIndex = this.getNextRequiredStep(); if (firstRequiredIndex !== -1) { this.updateLinearDisabledSteps(firstRequiredIndex); } else { this.linearDisabledSteps.clear(); } } else { this.stepper.steps.forEach(s => { if (s.index > this.activeStep.index) { this.linearDisabledSteps.add(s); } }); } } emitActivatingEvent(step) { const args = { owner: this.stepper, newIndex: step.index, oldIndex: this.activeStep.index, cancel: false }; this.stepper.activeStepChanging.emit(args); return args.cancel; } /** * Updates the linearDisabled steps from the current active step to the next required invalid step. * * @param toIndex the index of the last step that should be enabled. */ updateLinearDisabledSteps(toIndex) { this.stepper.steps.forEach(s => { if (s.index > this.activeStep.index) { if (s.index <= toIndex) { this.linearDisabledSteps.delete(s); } else { this.linearDisabledSteps.add(s); } } }); } getNextRequiredStep() { if (!this.activeStep) { return; } return this.stepper.steps.findIndex(s => s.index > this.activeStep.index && !s.optional && !s.disabled && !s.isValid); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxStepperService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxStepperService }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxStepperService, decorators: [{ type: Injectable }] }); /** * Allows a custom element to be added as an active step indicator. * * @igxModule IgxStepperModule * @igxTheme igx-stepper-theme * @igxKeywords stepper * @igxGroup Layouts * * @example * <igx-stepper> * <ng-template igxStepActiveIndicator> * <igx-icon>edit</igx-icon> * </ng-template> * </igx-stepper> */ class IgxStepActiveIndicatorDirective { static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxStepActiveIndicatorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.2", type: IgxStepActiveIndicatorDirective, isStandalone: true, selector: "[igxStepActiveIndicator]", ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxStepActiveIndicatorDirective, decorators: [{ type: Directive, args: [{ selector: '[igxStepActiveIndicator]', standalone: true }] }] }); /** * Allows a custom element to be added as a complete step indicator. * * @igxModule IgxStepperModule * @igxTheme igx-stepper-theme * @igxKeywords stepper * @igxGroup Layouts * * @example * <igx-stepper> * <ng-template igxStepCompletedIndicator> * <igx-icon>check</igx-icon> * </ng-template> * </igx-stepper> */ class IgxStepCompletedIndicatorDirective { static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxStepCompletedIndicatorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.2", type: IgxStepCompletedIndicatorDirective, isStandalone: true, selector: "[igxStepCompletedIndicator]", ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxStepCompletedIndicatorDirective, decorators: [{ type: Directive, args: [{ selector: '[igxStepCompletedIndicator]', standalone: true }] }] }); /** * Allows a custom element to be added as an invalid step indicator. * * @igxModule IgxStepperModule * @igxTheme igx-stepper-theme * @igxKeywords stepper * @igxGroup Layouts * * @example * <igx-stepper> * <ng-template igxStepInvalidIndicator> * <igx-icon>error</igx-icon> * </ng-template> * </igx-stepper> */ class IgxStepInvalidIndicatorDirective { static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxStepInvalidIndicatorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.2", type: IgxStepInvalidIndicatorDirective, isStandalone: true, selector: "[igxStepInvalidIndicator]", ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxStepInvalidIndicatorDirective, decorators: [{ type: Directive, args: [{ selector: '[igxStepInvalidIndicator]', standalone: true }] }] }); /** * Allows a custom element to be added as a step indicator. * * @igxModule IgxStepperModule * @igxTheme igx-stepper-theme * @igxKeywords stepper * @igxGroup Layouts * * @example * <igx-stepper> * <igx-step> * <igx-icon igxStepIndicator>home</igx-icon> * </igx-step> * </igx-stepper> */ class IgxStepIndicatorDirective { static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxStepIndicatorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.2", type: IgxStepIndicatorDirective, isStandalone: true, selector: "[igxStepIndicator]", ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxStepIndicatorDirective, decorators: [{ type: Directive, args: [{ selector: '[igxStepIndicator]', standalone: true }] }] }); /** * Allows a custom element to be added as a step title. * * @igxModule IgxStepperModule * @igxTheme igx-stepper-theme * @igxKeywords stepper * @igxGroup Layouts * * @example * <igx-stepper> * <igx-step> * <p igxStepTitle>Home</p> * </igx-step> * </igx-stepper> */ class IgxStepTitleDirective { constructor() { this.defaultClass = true; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxStepTitleDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.2", type: IgxStepTitleDirective, isStandalone: true, selector: "[igxStepTitle]", host: { properties: { "class.igx-stepper__step-title": "this.defaultClass" } }, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxStepTitleDirective, decorators: [{ type: Directive, args: [{ selector: '[igxStepTitle]', standalone: true }] }], propDecorators: { defaultClass: [{ type: HostBinding, args: ['class.igx-stepper__step-title'] }] } }); /** * Allows a custom element to be added as a step subtitle. * * @igxModule IgxStepperModule * @igxTheme igx-stepper-theme * @igxKeywords stepper * @igxGroup Layouts * * @example * <igx-stepper> * <igx-step> * <p igxStepSubtitle>Home Subtitle</p> * </igx-step> * </igx-stepper> */ class IgxStepSubtitleDirective { constructor() { this.defaultClass = true; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxStepSubtitleDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.2", type: IgxStepSubtitleDirective, isStandalone: true, selector: "[igxStepSubtitle]", host: { properties: { "class.igx-stepper__step-subtitle": "this.defaultClass" } }, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxStepSubtitleDirective, decorators: [{ type: Directive, args: [{ selector: '[igxStepSubtitle]', standalone: true }] }], propDecorators: { defaultClass: [{ type: HostBinding, args: ['class.igx-stepper__step-subtitle'] }] } }); /** * Allows a custom element to be added as a step content. * * @igxModule IgxStepperModule * @igxTheme igx-stepper-theme * @igxKeywords stepper * @igxGroup Layouts * * @example * <igx-stepper> * <igx-step> * <div igxStepContent>...</div> * </igx-step> * </igx-stepper> */ class IgxStepContentDirective { constructor() { this.step = inject(IGX_STEP_COMPONENT); this.stepperService = inject(IgxStepperService); this.elementRef = inject(ElementRef); this.defaultClass = true; this.role = 'tabpanel'; this.id = this.target.id.replace('step', 'content'); this._tabIndex = null; } get target() { return this.step; } get stepId() { return this.target.id; } get tabIndex() { if (this._tabIndex !== null) { return this._tabIndex; } return this.stepperService.activeStep === this.target ? 0 : -1; } set tabIndex(val) { this._tabIndex = val; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxStepContentDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.2", type: IgxStepContentDirective, isStandalone: true, selector: "[igxStepContent]", inputs: { id: "id", tabIndex: "tabIndex" }, host: { properties: { "class.igx-stepper__step-content": "this.defaultClass", "attr.role": "this.role", "attr.aria-labelledby": "this.stepId", "attr.id": "this.id", "attr.tabindex": "this.tabIndex" } }, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxStepContentDirective, decorators: [{ type: Directive, args: [{ selector: '[igxStepContent]', standalone: true }] }], propDecorators: { defaultClass: [{ type: HostBinding, args: ['class.igx-stepper__step-content'] }], role: [{ type: HostBinding, args: ['attr.role'] }], stepId: [{ type: HostBinding, args: ['attr.aria-labelledby'] }], id: [{ type: HostBinding, args: ['attr.id'] }, { type: Input }], tabIndex: [{ type: HostBinding, args: ['attr.tabindex'] }, { type: Input }] } }); let NEXT_ID = 0; /** * The IgxStepComponent is used within the `igx-stepper` element and it holds the content of each step. * It also supports custom indicators, title and subtitle. * * @igxModule IgxStepperModule * * @igxKeywords step * * @example * ```html * <igx-stepper> * ... * <igx-step [active]="true" [completed]="true"> * ... * </igx-step> * ... * </igx-stepper> * ``` */ class IgxStepComponent extends ToggleAnimationPlayer { constructor() { super(...arguments); this.stepper = inject(IGX_STEPPER_COMPONENT); this.cdr = inject(ChangeDetectorRef); this.renderer = inject(Renderer2); this.platform = inject(PlatformUtil); this.stepperService = inject(IgxStepperService); this.element = inject(ElementRef); this.dir = inject(_IgxDirectionality); /** * Get/Set the `id` of the step component. * Default value is `"igx-step-0"`; * ```html * <igx-step id="my-first-step"></igx-step> * ``` * ```typescript * const stepId = this.step.id; * ``` */ this.id = `igx-step-${NEXT_ID++}`; /** * Get/Set whether the step is completed. * * @remarks * When set to `true` the following separator is styled `solid`. * * ```html * <igx-stepper> * ... * <igx-step [completed]="true"></igx-step> * ... * </igx-stepper> * ``` * * ```typescript * this.stepper.steps[1].completed = true; * ``` */ this.completed = false; /** * Get/Set whether the step is optional. * * @remarks * Optional steps validity does not affect the default behavior when the stepper is in linear mode i.e. * if optional step is invalid the user could still move to the next step. * * ```html * <igx-step [optional]="true"></igx-step> * ``` * ```typescript * this.stepper.steps[1].optional = true; * ``` */ this.optional = false; /** @hidden @internal **/ this.role = 'tab'; /** @hidden @internal */ this.cssClass = true; /** * Emitted when the step's `active` property changes. Can be used for two-way binding. * * ```html * <igx-step [(active)]="this.isActive"> * </igx-step> * ``` * * ```typescript * const step: IgxStepComponent = this.stepper.step[0]; * step.activeChange.subscribe((e: boolean) => console.log("Step active state change to ", e)) * ``` */ this.activeChange = new EventEmitter(); this._tabIndex = -1; this._valid = true; this._focused = false; this._disabled = false; } /** * Get/Set whether the step is interactable. * * ```html * <igx-stepper> * ... * <igx-step [disabled]="true"></igx-step> * ... * </igx-stepper> * ``` * * ```typescript * this.stepper.steps[1].disabled = true; * ``` */ set disabled(value) { this._disabled = value; if (this.stepper.linear) { this.stepperService.calculateLinearDisabledSteps(); } } get disabled() { return this._disabled; } /** * Get/Set whether the step is valid. *```html * <igx-step [isValid]="form.form.valid"> * ... * <div igxStepContent> * <form #form="ngForm"> * ... * </form> * </div> * </igx-step> * ``` */ get isValid() { return this._valid; } set isValid(value) { this._valid = value; if (this.stepper.linear && this.index !== undefined) { this.stepperService.calculateLinearDisabledSteps(); } } /** * Get/Set the active state of the step * * ```html * <igx-step [active]="true"></igx-step> * ``` * * ```typescript * this.stepper.steps[1].active = true; * ``` * * @param value: boolean */ set active(value) { if (value) { this.stepperService.expandThroughApi(this); } else { this.stepperService.collapse(this); } } get active() { return this.stepperService.activeStep === this; } /** @hidden @internal */ set tabIndex(value) { this._tabIndex = value; } get tabIndex() { return this._tabIndex; } /** @hidden @internal */ get contentId() { return this.content?.id; } /** @hidden @internal */ get generalDisabled() { return this.disabled || this.linearDisabled; } /** @hidden @internal */ get titlePositionTop() { if (this.stepper.stepType !== IgxStepType.Full) { return 'igx-stepper__step--simple'; } return `igx-stepper__step--${this.titlePosition}`; } /** * Get the step index inside of the stepper. * * ```typescript * const step = this.stepper.steps[1]; * const stepIndex: number = step.index; * ``` */ get index() { return this._index; } /** @hidden @internal */ get indicatorTemplate() { if (this.active && this.stepper.activeIndicatorTemplate) { return this.stepper.activeIndicatorTemplate; } if (!this.isValid && this.stepper.invalidIndicatorTemplate) { return this.stepper.invalidIndicatorTemplate; } if (this.completed && this.stepper.completedIndicatorTemplate) { return this.stepper.completedIndicatorTemplate; } if (this.indicator) { return this.customIndicatorTemplate; } return null; } /** @hidden @internal */ get direction() { return this.stepperService.previousActiveStep && this.stepperService.previousActiveStep.index > this.index ? CarouselAnimationDirection.PREV : CarouselAnimationDirection.NEXT; } /** @hidden @internal */ get isAccessible() { return !this.disabled && !this.linearDisabled; } /** @hidden @internal */ get isHorizontal() { return this.stepper.orientation === IgxStepperOrientation.Horizontal; } /** @hidden @internal */ get isTitleVisible() { return this.stepper.stepType !== IgxStepType.Indicator; } /** @hidden @internal */ get isIndicatorVisible() { return this.stepper.stepType !== IgxStepType.Title; } /** @hidden @internal */ get titlePosition() { return this.stepper.titlePosition ? this.stepper.titlePosition : this.stepper._defaultTitlePosition; } /** @hidden @internal */ get linearDisabled() { return this.stepperService.linearDisabledSteps.has(this); } /** @hidden @internal */ get collapsing() { return this.stepperService.collapsingSteps.has(this); } /** @hidden @internal */ get animationSettings() { return this.stepper.verticalAnimationSettings; } /** @hidden @internal */ get contentClasses() { if (this.isHorizontal) { return { 'igx-stepper__body-content': true, 'igx-stepper__body-content--active': this.active }; } else { return 'igx-stepper__step-content'; } } /** @hidden @internal */ get stepHeaderClasses() { return { 'igx-stepper__step--optional': this.optional, 'igx-stepper__step-header--current': this.active, 'igx-stepper__step-header--invalid': !this.isValid && this.stepperService.visitedSteps.has(this) && !this.active && this.isAccessible }; } /** @hidden @internal */ get nativeElement() { return this.element.nativeElement; } /** @hidden @internal */ onFocus() { this._focused = true; this.stepperService.focusedStep = this; if (this.stepperService.focusedStep !== this.stepperService.activeStep) { this.stepperService.activeStep.tabIndex = -1; } } /** @hidden @internal */ onBlur() { this._focused = false; this.stepperService.activeStep.tabIndex = 0; } /** @hidden @internal */ handleKeydown(event) { if (!this._focused) { return; } const key = event.key; if (this.stepper.orientation === IgxStepperOrientation.Horizontal) { if (key === this.platform.KEYMAP.ARROW_UP || key === this.platform.KEYMAP.ARROW_DOWN) { return; } } if (!(this.platform.isNavigationKey(key) || this.platform.isActivationKey(event))) { return; } event.preventDefault(); this.handleNavigation(key); } /** @hidden @internal */ ngAfterViewInit() { this.openAnimationDone.pipe(takeUntil(this.destroy$)).subscribe(() => { if (this.stepperService.activeStep === this) { this.stepper.activeStepChanged.emit({ owner: this.stepper, index: this.index }); } }); this.closeAnimationDone.pipe(takeUntil(this.destroy$)).subscribe(() => { this.stepperService.collapse(this); this.cdr.markForCheck(); }); } /** @hidden @internal */ onPointerDown(event) { event.stopPropagation(); if (this.isHorizontal) { this.changeHorizontalActiveStep(); } else { this.changeVerticalActiveStep(); } } /** @hidden @internal */ handleNavigation(key) { switch (key) { case this.platform.KEYMAP.HOME: this.stepper.steps.filter(s => s.isAccessible)[0]?.nativeElement.focus(); break; case this.platform.KEYMAP.END: this.stepper.steps.filter(s => s.isAccessible).pop()?.nativeElement.focus(); break; case this.platform.KEYMAP.ARROW_UP: this.previousStep?.nativeElement.focus(); break; case this.platform.KEYMAP.ARROW_LEFT: if (this.dir.rtl && this.stepper.orientation === IgxStepperOrientation.Horizontal) { this.nextStep?.nativeElement.focus(); } else { this.previousStep?.nativeElement.focus(); } break; case this.platform.KEYMAP.ARROW_DOWN: this.nextStep?.nativeElement.focus(); break; case this.platform.KEYMAP.ARROW_RIGHT: if (this.dir.rtl && this.stepper.orientation === IgxStepperOrientation.Horizontal) { this.previousStep?.nativeElement.focus(); } else { this.nextStep?.nativeElement.focus(); } break; case this.platform.KEYMAP.SPACE: case this.platform.KEYMAP.ENTER: if (this.isHorizontal) { this.changeHorizontalActiveStep(); } else { this.changeVerticalActiveStep(); } break; default: return; } } /** @hidden @internal */ changeHorizontalActiveStep() { if (this.stepper.animationType === HorizontalAnimationType.none && this.stepperService.activeStep !== this) { const argsCanceled = this.stepperService.emitActivatingEvent(this); if (argsCanceled) { return; } this.active = true; this.stepper.activeStepChanged.emit({ owner: this.stepper, index: this.index }); return; } this.stepperService.expand(this); if (this.stepper.animationType === HorizontalAnimationType.fade) { if (this.stepperService.collapsingSteps.has(this.stepperService.previousActiveStep)) { this.stepperService.previousActiveStep.active = false; } } } get nextStep() { const focusedStep = this.stepperService.focusedStep; if (focusedStep) { if (focusedStep.index === this.stepper.steps.length - 1) { return this.stepper.steps.find(s => s.isAccessible); } const nextAccessible = this.stepper.steps.find((s, i) => i > focusedStep.index && s.isAccessible); return nextAccessible ? nextAccessible : this.stepper.steps.find(s => s.isAccessible); } return null; } get previousStep() { const focusedStep = this.stepperService.focusedStep; if (focusedStep) { if (focusedStep.index === 0) { return this.stepper.steps.filter(s => s.isAccessible).pop(); } let prevStep; for (let i = focusedStep.index - 1; i >= 0; i--) { const step = this.stepper.steps[i]; if (step.isAccessible) { prevStep = step; break; } } return prevStep ? prevStep : this.stepper.steps.filter(s => s.isAccessible).pop(); } return null; } changeVerticalActiveStep() { this.stepperService.expand(this); if (!this.animationSettings.closeAnimation) { this.stepperService.previousActiveStep?.openAnimationPlayer?.finish(); } if (!this.animationSettings.openAnimation) { this.stepperService.activeStep.closeAnimationPlayer?.finish(); } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxStepComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.2", type: IgxStepComponent, isStandalone: true, selector: "igx-step", inputs: { id: "id", disabled: ["disabled", "disabled", booleanAttribute], completed: ["completed", "completed", booleanAttribute], isValid: ["isValid", "isValid", booleanAttribute], optional: ["optional", "optional", booleanAttribute], active: ["active", "active", booleanAttribute], tabIndex: "tabIndex" }, outputs: { activeChange: "activeChange" }, host: { listeners: { "focus": "onFocus()", "blur": "onBlur()", "keydown": "handleKeydown($event)" }, properties: { "attr.id": "this.id", "class.igx-stepper__step--completed": "this.completed", "attr.aria-selected": "this.active", "attr.tabindex": "this.tabIndex", "attr.role": "this.role", "attr.aria-controls": "this.contentId", "class.igx-stepper__step": "this.cssClass", "class.igx-stepper__step--disabled": "this.generalDisabled", "class": "this.titlePositionTop" } }, providers: [ { provide: IGX_STEP_COMPONENT, useExisting: IgxStepComponent } ], queries: [{ propertyName: "indicator", first: true, predicate: i0.forwardRef(() => IgxStepIndicatorDirective), descendants: true }, { propertyName: "content", first: true, predicate: i0.forwardRef(() => IgxStepContentDirective), descendants: true }], viewQueries: [{ propertyName: "contentTemplate", first: true, predicate: ["contentTemplate"], descendants: true, static: true }, { propertyName: "customIndicatorTemplate", first: true, predicate: ["customIndicator"], descendants: true, static: true }, { propertyName: "contentContainer", first: true, predicate: ["contentContainer"], descendants: true }], usesInheritance: true, ngImport: i0, template: "<ng-template #defaultTitle>\n @if (isTitleVisible) {\n <ng-content select=\"[igxStepTitle]\"></ng-content>\n }\n @if (isTitleVisible) {\n <ng-content select=\"[igxStepSubtitle]\"></ng-content>\n }\n</ng-template>\n\n<ng-template #contentTemplate>\n <div [ngClass]=\"contentClasses\" #contentContainer>\n @if (active || collapsing) {\n <ng-content select=\"[igxStepContent]\"></ng-content>\n }\n </div>\n</ng-template>\n\n<ng-template #defaultIndicator>\n <span>{{ index + 1 }}</span>\n</ng-template>\n\n<ng-template #customIndicator>\n <ng-content select=\"[igxStepIndicator]\"></ng-content>\n</ng-template>\n\n<div class=\"igx-stepper__step-header\" igxRipple [ngClass]=\"stepHeaderClasses\" (keydown)=\"handleKeydown($event)\"\n (click)=\"onPointerDown($event)\">\n\n @if (isIndicatorVisible) {\n <div class=\"igx-stepper__step-indicator\">\n <ng-container *ngTemplateOutlet=\"indicatorTemplate ? indicatorTemplate : defaultIndicator\"></ng-container>\n </div>\n }\n\n <div class=\"igx-stepper__step-title-wrapper\">\n <ng-container *ngTemplateOutlet=\"defaultTitle\"></ng-container>\n </div>\n</div>\n\n@if (!isHorizontal) {\n <div class=\"igx-stepper__step-content-wrapper\">\n <ng-container *ngTemplateOutlet=\"contentTemplate\"></ng-container>\n </div>\n}\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: IgxRippleDirective, selector: "[igxRipple]", inputs: ["igxRippleTarget", "igxRipple", "igxRippleDuration", "igxRippleCentered", "igxRippleDisabled"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxStepComponent, decorators: [{ type: Component, args: [{ selector: 'igx-step', providers: [ { provide: IGX_STEP_COMPONENT, useExisting: IgxStepComponent } ], imports: [NgClass, IgxRippleDirective, NgTemplateOutlet], template: "<ng-template #defaultTitle>\n @if (isTitleVisible) {\n <ng-content select=\"[igxStepTitle]\"></ng-content>\n }\n @if (isTitleVisible) {\n <ng-content select=\"[igxStepSubtitle]\"></ng-content>\n }\n</ng-template>\n\n<ng-template #contentTemplate>\n <div [ngClass]=\"contentClasses\" #contentContainer>\n @if (active || collapsing) {\n <ng-content select=\"[igxStepContent]\"></ng-content>\n }\n </div>\n</ng-template>\n\n<ng-template #defaultIndicator>\n <span>{{ index + 1 }}</span>\n</ng-template>\n\n<ng-template #customIndicator>\n <ng-content select=\"[igxStepIndicator]\"></ng-content>\n</ng-template>\n\n<div class=\"igx-stepper__step-header\" igxRipple [ngClass]=\"stepHeaderClasses\" (keydown)=\"handleKeydown($event)\"\n (click)=\"onPointerDown($event)\">\n\n @if (isIndicatorVisible) {\n <div class=\"igx-stepper__step-indicator\">\n <ng-container *ngTemplateOutlet=\"indicatorTemplate ? indicatorTemplate : defaultIndicator\"></ng-container>\n </div>\n }\n\n <div class=\"igx-stepper__step-title-wrapper\">\n <ng-container *ngTemplateOutlet=\"defaultTitle\"></ng-container>\n </div>\n</div>\n\n@if (!isHorizontal) {\n <div class=\"igx-stepper__step-content-wrapper\">\n <ng-container *ngTemplateOutlet=\"contentTemplate\"></ng-container>\n </div>\n}\n" }] }], propDecorators: { id: [{ type: HostBinding, args: ['attr.id'] }, { type: Input }], disabled: [{ type: Input, args: [{ transform: booleanAttribute }] }], completed: [{ type: Input, args: [{ transform: booleanAttribute }] }, { type: HostBinding, args: ['class.igx-stepper__step--completed'] }], isValid: [{ type: Input, args: [{ transform: booleanAttribute }] }], optional: [{ type: Input, args: [{ transform: booleanAttribute }] }], active: [{ type: HostBinding, args: ['attr.aria-selected'] }, { type: Input, args: [{ transform: booleanAttribute }] }], tabIndex: [{ type: HostBinding, args: ['attr.tabindex'] }, { type: Input }], role: [{ type: HostBinding, args: ['attr.role'] }], contentId: [{ type: HostBinding, args: ['attr.aria-controls'] }], cssClass: [{ type: HostBinding, args: ['class.igx-stepper__step'] }], generalDisabled: [{ type: HostBinding, args: ['class.igx-stepper__step--disabled'] }], titlePositionTop: [{ type: HostBinding, args: ['class'] }], activeChange: [{ type: Output }], contentTemplate: [{ type: ViewChild, args: ['contentTemplate', { static: true }] }], customIndicatorTemplate: [{ type: ViewChild, args: ['customIndicator', { static: true }] }], contentContainer: [{ type: ViewChild, args: ['contentContainer'] }], indicator: [{ type: ContentChild, args: [forwardRef(() => IgxStepIndicatorDirective)] }], content: [{ type: ContentChild, args: [forwardRef(() => IgxStepContentDirective)] }], onFocus: [{ type: HostListener, args: ['focus'] }], onBlur: [{ type: HostListener, args: ['blur'] }], handleKeydown: [{ type: HostListener, args: ['keydown', ['$event']] }] } }); // TODO: common interface between IgxCarouselComponentBase and ToggleAnimationPlayer? /** * IgxStepper provides a wizard-like workflow by dividing content into logical steps. * * @igxModule IgxStepperModule * * @igxKeywords stepper * * @igxGroup Layouts * * @remarks * The Ignite UI for Angular Stepper component allows the user to navigate between multiple steps. * It supports horizontal and vertical orientation as well as keyboard navigation and provides API methods to control the active step. * The component offers keyboard navigation and API to control the active step. * * @example * ```html * <igx-stepper> * <igx-step [active]="true"> * <igx-icon igxStepIndicator>home</igx-icon> * <p igxStepTitle>Home</p> * <div igxStepContent> * ... * </div> * </igx-step> * <igx-step [optional]="true"> * <div igxStepContent> * ... * </div> * </igx-step> * <igx-step> * <div igxStepContent> * ... * </div> * </igx-step> * </igx-stepper> * ``` */ class IgxStepperComponent extends IgxCarouselComponentBase { /** * Get/Set the animation type of the stepper when the orientation direction is vertical. * * @remarks * Default value is `grow`. Other possible values are `fade` and `none`. * * ```html * <igx-stepper verticalAnimationType="none"> * <igx-stepper> * ``` */ get verticalAnimationType() { return this._verticalAnimationType; } set verticalAnimationType(value) { // TODO: activeChange event is not emitted for the collapsing steps (loop through collapsing steps and emit) this.stepperService.collapsingSteps.clear(); this._verticalAnimationType = value; switch (value) { case 'grow': this.verticalAnimationSettings = this.updateVerticalAnimationSettings(growVerIn, growVerOut); break; case 'fade': this.verticalAnimationSettings = this.updateVerticalAnimationSettings(fadeIn, null); break; case 'none': this.verticalAnimationSettings = this.updateVerticalAnimationSettings(null, null); break; } } /** * Get/Set the animation type of the stepper when the orientation direction is horizontal. * * @remarks * Default value is `grow`. Other possible values are `fade` and `none`. * * ```html * <igx-stepper animationType="none"> * <igx-stepper> * ``` */ get horizontalAnimationType() { return this.animationType; } set horizontalAnimationType(value) { // TODO: activeChange event is not emitted for the collapsing steps (loop through collapsing steps and emit) this.stepperService.collapsingSteps.clear(); this.animationType = value; } /** * Get/Set the animation duration. * ```html * <igx-stepper [animationDuration]="500"> * <igx-stepper> * ``` */ get animationDuration() { return this.defaultAnimationDuration; } set animationDuration(value) { if (value && value > 0) { this.defaultAnimationDuration = value; return; } this.defaultAnimationDuration = this._defaultAnimationDuration; } /** * Get/Set whether the stepper is linear. * * @remarks * If the stepper is in linear mode and if the active step is valid only then the user is able to move forward. * * ```html * <igx-stepper [linear]="true"></igx-stepper> * ``` */ get linear() { return this._linear; } set linear(value) { this._linear = value; if (this._linear && this.steps.length > 0) { // when the stepper is in linear mode we should calculate which steps should be disabled // and which are visited i.e. their validity should be correctly displayed. this.stepperService.calculateVisitedSteps(); this.stepperService.calculateLinearDisabledSteps(); } else { this.stepperService.linearDisabledSteps.clear(); } } /** * Get/Set the stepper orientation. * * ```typescript * this.stepper.orientation = IgxStepperOrientation.Vertical; * ``` */ get orientation() { return this._orientation; } set orientation(value) { if (this._orientation === value) { return; } // TODO: activeChange event is not emitted for the collapsing steps this.stepperService.collapsingSteps.clear(); this._orientation = value; this._defaultTitlePosition = this._orientation === IgxStepperOrientation.Horizontal ? IgxStepperTitlePosition.Bottom : IgxStepperTitlePosition.End; } /** @hidden @internal **/ get directionClass() { return this.orientation === IgxStepperOrientation.Horizontal; } /** * Get all steps. * * ```typescript * const steps: IgxStepComponent[] = this.stepper.steps; * ``` */ get steps() { return this._steps?.toArray() || []; } /** @hidden @internal */ get nativeElement() { return this.element.nativeElement; } constructor() { super(); this.stepperService = inject(IgxStepperService); this.element = inject(ElementRef); /** * Get/Set the type of the steps. * * ```typescript * this.stepper.stepType = IgxStepType.Indicator; * ``` */ this.stepType = IgxStepType.Full; /** * Get/Set whether the content is displayed above the steps. * * @remarks * Default value is `false` and the content is below the steps. * * ```typescript * this.stepper.contentTop = true; * ``` */ this.contentTop = false; /** * Get/Set the position of the steps title. * * @remarks * The default value when the stepper is horizontally orientated is `bottom`. * In vertical layout the default title position is `end`. * * ```typescript * this.stepper.titlePosition = IgxStepperTitlePosition.Top; * ``` */ this.titlePosition = null; /** @hidden @internal **/ this.cssClass = 'igx-stepper'; /** @hidden @internal **/ this.role = 'tablist'; /** * Emitted when the stepper's active step is changing. * *```html * <igx-stepper (activeStepChanging)="handleActiveStepChanging($event)"> * </igx-stepper> * ``` * *```typescript * public handleActiveStepChanging(event: IStepChangingEventArgs) { * if (event.newIndex < event.oldIndex) { * event.cancel = true; * } * } *``` */ this.activeStepChanging = new EventEmitter(); /** * Emitted when the active step is changed. * * @example * ``` * <igx-stepper (activeStepChanged)="handleActiveStepChanged($event)"></igx-stepper> * ``` */ this.activeStepChanged = new EventEmitter(); /** @hidden @internal */ this.verticalAnimationSettings = { openAnimation: growVerIn, closeAnimation: growVerOut, }; /** @hidden @internal */ this._defaultTitlePosition = IgxStepperTitlePosition.Bottom; this.destroy$ = new Subject(); this._orientation = IgxStepperOrientation.Horizontal; this._verticalAnimationType = VerticalAnimationType.Grow; this._linear = false; this._defaultAnimationDuration = 350; this.stepperService.stepper = this; } /** @hidden @internal */ ngOnChanges(changes) { if (changes['animationDuration']) { this.verticalAnimationType = this._verticalAnimationType; } } /** @hidden @internal */ ngOnInit() { this.enterAnimationDone.pipe(takeUntil(this.destroy$)).subscribe(() => { this.activeStepChanged.emit({ owner: this, index: this.stepperService.activeStep.index }); }); this.leaveAnimationDone.pipe(takeUntil(this.destroy$)).subscribe(() => { if (this.stepperService.collapsingSteps.size === 1) { this.stepperService.collapse(this.stepperService.previousActiveStep); } else { Array.from(this.stepperService.collapsingSteps).slice(0, this.stepperService.collapsingSteps.size - 1) .forEach(step => this.stepperService.collapse(step)); } }); } /** @hidden @internal */ ngAfterContentInit() { let activeStep; this.steps.forEach((step, index) => { this.updateStepAria(step, index); if (!activeStep && step.active) { activeStep = step; } }); if (!activeStep) { this.activateFirstStep(true); } if (this.linear) { this.stepperService.calculateLinearDisabledSteps(); } this.handleStepChanges(); } /** @hidden @internal */ ngOnDestroy() { super.ngOnDestroy(); this.destroy$.next(); this.destroy$.complete(); } /** * Activates the step at a given index. * *```typescript * this.stepper.navigateTo(1); *``` */ navigateTo(index) { const step = this.steps[index]; if (!step || this.stepperService.activeStep === step) { return; } this.activateStep(step); } /** * Activates the next enabled step. * *```typescript * this.stepper.next(); *``` */ next() { this.moveToNextStep(); } /** * Activates the previous enabled step. * *```typescript * this.stepper.prev(); *``` */ prev() { this.moveToNextStep(false); } /** * Resets the stepper to its initial state i.e. activates the first step. * * @remarks * The steps' content will not be automatically reset. *```typescript * this.stepper.reset(); *``` */ reset() { this.stepperService.visitedSteps.clear(); const activeStep = this.steps.find(s => !s.disabled); if (activeStep) { this.activateStep(activeStep); } } /** @hidden @internal */ playHorizontalAnimations() { this.previousItem = this.stepperService.previousActiveStep; this.currentItem = this.stepperService.activeStep; this.triggerAnimations(); } getPreviousElement() { return this.stepperService.previousActiveStep?.contentContainer.nativeElement; } getCurrentElement() { return this.stepperService.activeStep.contentContainer.nativeElement; } updateVerticalAnimationSettings(openAnimation, closeAnimation) { const customCloseAnimation = useAnimation(closeAnimation, { params: { duration: this.animationDuration + 'ms' } }); const customOpenAnimation = useAnimation(openAnimation, { params: { duration: this.animationDuration + 'ms' } }); return { openAnimation: openAnimation ? customOpenAnimation : null, closeAnimation: closeAnimation ? customCloseAnimation : null }; } updateStepAria(step, index) { step._index = index; step.renderer.setAttribute(step.nativeElement, 'aria-se