diff --git a/projects/igniteui-angular/src/lib/stepper/common.ts b/projects/igniteui-angular/src/lib/stepper/common.ts new file mode 100644 index 00000000000..d6514ce371d --- /dev/null +++ b/projects/igniteui-angular/src/lib/stepper/common.ts @@ -0,0 +1,46 @@ +import { InjectionToken } from '@angular/core'; +import { IBaseCancelableBrowserEventArgs, IBaseEventArgs, mkenum } from '../core/utils'; +import { IgxStepperComponent } from './igx-stepper.component'; +import { IgxStepComponent } from './step/igx-step.component'; + +// Events +export interface IStepTogglingEventArgs extends IBaseEventArgs, IBaseCancelableBrowserEventArgs { + activeStep: IgxStepComponent; + previousActiveStep: IgxStepComponent; + owner: IgxStepperComponent; +} + +export interface IStepToggledEventArgs extends IBaseEventArgs { + activeStep: IgxStepComponent; + owner: IgxStepperComponent; +} + +// Enums + +export const IgxStepperOrienatation = mkenum({ + Horizontal: 'Horizontal', + Vertical: 'Vertical' +}); +export type IgxStepperOrienatation = (typeof IgxStepperOrienatation)[keyof typeof IgxStepperOrienatation]; + + +export enum IgxStepType { + Indicator, + Label, + Full +} + +export enum IgxStepperLabelPosition { + Bottom, + Top, + End, + Start +} + +export enum IgxStepperProgressLine { + Solid, + Dashed +} + +// Token +export const IGX_STEPPER_COMPONENT = new InjectionToken('IgxStepperToken'); diff --git a/projects/igniteui-angular/src/lib/stepper/igx-stepper.component.html b/projects/igniteui-angular/src/lib/stepper/igx-stepper.component.html new file mode 100644 index 00000000000..8b5c6059363 --- /dev/null +++ b/projects/igniteui-angular/src/lib/stepper/igx-stepper.component.html @@ -0,0 +1,11 @@ + +
+ +
+ +
+
+ +
+
+ diff --git a/projects/igniteui-angular/src/lib/stepper/igx-stepper.component.scss b/projects/igniteui-angular/src/lib/stepper/igx-stepper.component.scss new file mode 100644 index 00000000000..6c6c0be2496 --- /dev/null +++ b/projects/igniteui-angular/src/lib/stepper/igx-stepper.component.scss @@ -0,0 +1,15 @@ +.igx-stepper { + display: flex; + width: 100%; + overflow: hidden; +} + +.igx-stepper--horizontal { + flex-direction: row; + justify-content: space-between; +} + +.igx-stepper--vertical { + flex-direction: column; + gap: 20px; +} diff --git a/projects/igniteui-angular/src/lib/stepper/igx-stepper.component.ts b/projects/igniteui-angular/src/lib/stepper/igx-stepper.component.ts new file mode 100644 index 00000000000..f187eab3064 --- /dev/null +++ b/projects/igniteui-angular/src/lib/stepper/igx-stepper.component.ts @@ -0,0 +1,317 @@ +import { CommonModule } from '@angular/common'; +import { + AfterViewInit, Component, HostBinding, OnDestroy, OnInit, + Input, Output, EventEmitter, ContentChildren, QueryList, ElementRef, Optional, Inject, NgModule, ViewChild +} from '@angular/core'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +import { growVerIn, growVerOut } from '../animations/grow'; +import { slideInLeft, slideOutRight } from '../animations/slide'; +import { DisplayDensityBase, DisplayDensityToken, IDisplayDensityOptions } from '../core/displayDensity'; +import { ToggleAnimationSettings } from '../expansion-panel/toggle-animation-component'; +import { IgxStepperOrienatation, IGX_STEPPER_COMPONENT, IStepToggledEventArgs, IStepTogglingEventArgs } from './common'; +import { + IgxStepIconDirective, IgxStepInvalidIconDirective, + IgxStepLabelDirective, IgxStepValidIconDirective +} from './igx-stepper.directive'; +import { IgxStepComponent } from './step/igx-step.component'; +import { IgxStepperService } from './stepper.service'; + + +@Component({ + selector: 'igx-stepper', + templateUrl: 'igx-stepper.component.html', + styleUrls: ['igx-stepper.component.scss'], + providers: [ + IgxStepperService, + { provide: IGX_STEPPER_COMPONENT, useExisting: IgxStepperComponent }, + ] +}) +export class IgxStepperComponent extends DisplayDensityBase implements OnInit, AfterViewInit, OnDestroy { + + @ViewChild('horizontalContentContainer') public horizontalContentContainer: ElementRef; + + @HostBinding('class.igx-stepper') + public cssClass = 'igx-stepper'; + + /** Get/Set the animation settings that branches should use when expanding/collpasing. + * + * ```html + * + * + * ``` + * + * ```typescript + * const animationSettings: ToggleAnimationSettings = { + * openAnimation: growVerIn, + * closeAnimation: growVerOut + * }; + * + * this.tree.animationSettings = animationSettings; + * ``` + */ + @Input() + public animationSettings: ToggleAnimationSettings = { + openAnimation: growVerIn, + closeAnimation: growVerOut + }; + + /** + * Get/Set the stepper orientation. + * + * ```typescript + * this.stepper.orientation = IgxStepperOrienatation.Vertical; + * ``` + */ + @Input() + public get orientation(): IgxStepperOrienatation | string { + return this._orientation; + } + + public set orientation(value: IgxStepperOrienatation | string) { + if (this._orientation === value) { + return; + } + if (value === IgxStepperOrienatation.Horizontal) { + this.animationSettings = { + openAnimation: slideInLeft, + closeAnimation: slideOutRight + }; + } else { + this.animationSettings = { + openAnimation: growVerIn, + closeAnimation: growVerOut + }; + } + this._orientation = value; + } + + /** Emitted when a node is expanding, before it finishes + * + * ```html + * + * + * ``` + * + *```typescript + * public handleNodeExpanding(event: ITreeNodeTogglingEventArgs) { + * const expandedNode: IgxTreeNode = event.node; + * if (expandedNode.disabled) { + * event.cancel = true; + * } + * } + *``` + */ + @Output() + public stepExpanding = new EventEmitter(); + + /** Emitted when a node is expanded, after it finishes + * + * ```html + * + * + * ``` + * + *```typescript + * public handleNodeExpanded(event: ITreeNodeToggledEventArgs) { + * const expandedNode: IgxTreeNode = event.node; + * console.log("Node is expanded: ", expandedNode.data); + * } + *``` + */ + @Output() + public stepExpanded = new EventEmitter(); + + /** Emitted when a node is collapsing, before it finishes + * + * ```html + * + * + * ``` + * + *```typescript + * public handleNodeCollapsing(event: ITreeNodeTogglingEventArgs) { + * const collapsedNode: IgxTreeNode = event.node; + * if (collapsedNode.alwaysOpen) { + * event.cancel = true; + * } + * } + *``` + */ + @Output() + public stepCollapsing = new EventEmitter(); + + /** Emitted when a node is collapsed, after it finishes + * + * @example + * ```html + * + * + * ``` + * ```typescript + * public handleNodeCollapsed(event: ITreeNodeToggledEventArgs) { + * const collapsedNode: IgxTreeNode = event.node; + * console.log("Node is collapsed: ", collapsedNode.data); + * } + * ``` + */ + @Output() + public stepCollapsed = new EventEmitter(); + + /** + * Emitted when the active node is changed. + * + * @example + * ``` + * + * ``` + */ + @Output() + public activeStepChanged = new EventEmitter(); + + /** @hidden @internal */ + @ContentChildren(IgxStepComponent, { descendants: false }) + public steps: QueryList; + + // /** @hidden @internal */ + // public disabledChange = new EventEmitter(); + + private destroy$ = new Subject(); + private unsubChildren$ = new Subject(); + private _orientation: IgxStepperOrienatation | string = IgxStepperOrienatation.Vertical; + + constructor( + public stepperService: IgxStepperService, + private element: ElementRef, + @Optional() @Inject(DisplayDensityToken) protected _displayDensityOptions?: IDisplayDensityOptions) { + super(_displayDensityOptions); + this.stepperService.register(this); + // this.navService.register(this); + } + + /** @hidden @internal */ + public get nativeElement() { + return this.element.nativeElement; + } + + /** @hidden @internal */ + public handleKeydown(event: KeyboardEvent) { + // this.navService.handleKeydown(event); + } + + /** @hidden @internal */ + public ngOnInit() { + super.ngOnInit(); + // this.disabledChange.pipe(takeUntil(this.destroy$)).subscribe((e) => { + // this.navService.update_disabled_cache(e); + // }); + + + //dali ni trqbva + // this.activeNodeBindingChange.pipe(takeUntil(this.destroy$)).subscribe((node) => { + // this.expandToNode(this.navService.activeNode); + // this.scrollNodeIntoView(node?.header?.nativeElement); + // }); + + + + // this.onDensityChanged.pipe(takeUntil(this.destroy$)).subscribe(() => { + // requestAnimationFrame(() => { + // this.scrollNodeIntoView(this.navService.activeStep?.header.nativeElement); + // }); + // }); + this.subToCollapsing(); + } + + /** @hidden @internal */ + public ngAfterViewInit() { + this.steps.changes.pipe(takeUntil(this.destroy$)).subscribe(() => { + this.subToChanges(); + }); + // this.scrollNodeIntoView(this.navService.activeNode?.header?.nativeElement); + this.subToChanges(); + this.steps.forEach(s => { + s.horizontalContentContainer = this.horizontalContentContainer; + }); + } + + /** @hidden @internal */ + public ngOnDestroy() { + this.unsubChildren$.next(); + this.unsubChildren$.complete(); + this.destroy$.next(); + this.destroy$.complete(); + } + + private subToCollapsing() { + this.stepCollapsing.pipe(takeUntil(this.destroy$)).subscribe(event => { + if (event.cancel) { + return; + } + // this.navService.update_visible_cache(event.node, false); + }); + this.stepExpanding.pipe(takeUntil(this.destroy$)).subscribe(event => { + if (event.cancel) { + return; + } + // this.navService.update_visible_cache(event.node, true); + }); + } + + private subToChanges() { + this.unsubChildren$.next(); + this.steps.forEach(step => { + step.expandedChange.pipe(takeUntil(this.unsubChildren$)).subscribe(nodeState => { + // this.navService.update_visible_cache(node, nodeState); + }); + step.closeAnimationDone.pipe(takeUntil(this.unsubChildren$)).subscribe(() => { + // const targetElement = this.navService.focusedNode?.header.nativeElement; + // this.scrollNodeIntoView(targetElement); + }); + step.openAnimationDone.pipe(takeUntil(this.unsubChildren$)).subscribe(() => { + // const targetElement = this.navService.focusedNode?.header.nativeElement; + // this.scrollNodeIntoView(targetElement); + }); + }); + // this.navService.init_invisible_cache(); + } + + private scrollNodeIntoView(el: HTMLElement) { + if (!el) { + return; + } + const nodeRect = el.getBoundingClientRect(); + const treeRect = this.nativeElement.getBoundingClientRect(); + const topOffset = treeRect.top > nodeRect.top ? nodeRect.top - treeRect.top : 0; + const bottomOffset = treeRect.bottom < nodeRect.bottom ? nodeRect.bottom - treeRect.bottom : 0; + const shouldScroll = !!topOffset || !!bottomOffset; + if (shouldScroll && this.nativeElement.scrollHeight > this.nativeElement.clientHeight) { + // this.nativeElement.scrollTop = nodeRect.y - treeRect.y - nodeRect.height; + this.nativeElement.scrollTop = + this.nativeElement.scrollTop + bottomOffset + topOffset + (topOffset ? -1 : +1) * nodeRect.height; + } + } +} + +@NgModule({ + imports: [ + CommonModule + ], + declarations: [ + IgxStepComponent, + IgxStepperComponent, + IgxStepLabelDirective, + IgxStepIconDirective, + IgxStepValidIconDirective, + IgxStepInvalidIconDirective, + ], + exports: [ + IgxStepComponent, + IgxStepperComponent, + IgxStepLabelDirective, + IgxStepIconDirective, + IgxStepValidIconDirective, + IgxStepInvalidIconDirective, + ] +}) +export class IgxStepperModule { } diff --git a/projects/igniteui-angular/src/lib/stepper/igx-stepper.directive.ts b/projects/igniteui-angular/src/lib/stepper/igx-stepper.directive.ts new file mode 100644 index 00000000000..7dd36d69b03 --- /dev/null +++ b/projects/igniteui-angular/src/lib/stepper/igx-stepper.directive.ts @@ -0,0 +1,56 @@ +import { Directive, ElementRef, HostBinding, Input, TemplateRef } from '@angular/core'; +import { IgxStepperLabelPosition } from './common'; + +@Directive({ + selector: '[igxStepValidIcon]' +}) +export class IgxStepValidIconDirective { + @HostBinding('class.igx-step__icon--valid') + public defaultClass = true; +} + +@Directive({ + selector: '[igxStepInvalidIcon]' +}) +export class IgxStepInvalidIconDirective { + @HostBinding('class.igx-step__icon--invalid') + public defaultClass = true; +} +@Directive({ + selector: '[igxStepIcon]' +}) +export class IgxStepIconDirective { + @HostBinding('class.igx-step__icon') + public defaultClass = true; +} + +@Directive({ + selector: '[igxStepLabel]' +}) + +export class IgxStepLabelDirective { + @Input() public position = IgxStepperLabelPosition.Bottom; + + @HostBinding('class.igx-step__label') + public defaultClass = true; + + @HostBinding('class.igx-step__label--bottom') + public get bottomClass() { + return this.position === IgxStepperLabelPosition.Bottom; + } + + @HostBinding('class.igx-step__label--top') + public get topClass() { + return this.position === IgxStepperLabelPosition.Top; + } + + @HostBinding('class.igx-step__label--end') + public get endClass() { + return this.position === IgxStepperLabelPosition.End; + } + + @HostBinding('class.igx-step__label--start') + public get startClass() { + return this.position === IgxStepperLabelPosition.Start; + } +} diff --git a/projects/igniteui-angular/src/lib/stepper/step/igx-step.component.html b/projects/igniteui-angular/src/lib/stepper/step/igx-step.component.html new file mode 100644 index 00000000000..1121ab99727 --- /dev/null +++ b/projects/igniteui-angular/src/lib/stepper/step/igx-step.component.html @@ -0,0 +1,19 @@ + +
+ +
+
+ + + + + + +
+ + +
+ +
+ +
diff --git a/projects/igniteui-angular/src/lib/stepper/step/igx-step.component.scss b/projects/igniteui-angular/src/lib/stepper/step/igx-step.component.scss new file mode 100644 index 00000000000..52efaca5ea6 --- /dev/null +++ b/projects/igniteui-angular/src/lib/stepper/step/igx-step.component.scss @@ -0,0 +1,18 @@ +.igx-step--content { + display: flex; +} + +// .igx-step::after { +// content: '\21E2'; +// color: white; +// position: relative; +// top: -50%; +// left: 100%; +// z-index: 1; +// } + +// :host ::ng-deep { +// .igx-step__content { +// display: flex; +// } +// } diff --git a/projects/igniteui-angular/src/lib/stepper/step/igx-step.component.ts b/projects/igniteui-angular/src/lib/stepper/step/igx-step.component.ts new file mode 100644 index 00000000000..73e741d016c --- /dev/null +++ b/projects/igniteui-angular/src/lib/stepper/step/igx-step.component.ts @@ -0,0 +1,331 @@ +import { AnimationBuilder } from '@angular/animations'; +import { + AfterViewInit, ChangeDetectorRef, Component, ElementRef, + EventEmitter, HostBinding, Inject, Input, OnDestroy, OnInit, Output, TemplateRef, ViewChild +} from '@angular/core'; +import { takeUntil } from 'rxjs/operators'; +import { DisplayDensity } from '../../core/displayDensity'; +import { ToggleAnimationPlayer, ToggleAnimationSettings } from '../../expansion-panel/toggle-animation-component'; +import { IgxStepperOrienatation, IGX_STEPPER_COMPONENT, IStepTogglingEventArgs } from '../common'; +import { IgxStepperComponent } from '../igx-stepper.component'; +import { IgxStepperService } from '../stepper.service'; + +let NEXT_ID = 0; + +@Component({ + selector: 'igx-step', + templateUrl: 'igx-step.component.html', + styleUrls: ['igx-step.component.scss'] +}) +export class IgxStepComponent extends ToggleAnimationPlayer implements OnInit, AfterViewInit, OnDestroy { + + /** + * Get/Set the `id` of the step component. + * Default value is `"igx-step-0"`; + * ```html + * + * ``` + * ```typescript + * const stepId = this.step.id; + * ``` + */ + @HostBinding('attr.id') + @Input() + public id = `igx-step-${NEXT_ID++}`; + + // /** + // * To be used for load-on-demand scenarios in order to specify whether the node is loading data. + // * + // * @remarks + // * Loading nodes do not render children. + // */ + // @Input() + // public loading = false; + + + /** @hidden @internal */ + public get animationSettings(): ToggleAnimationSettings { + return this.stepper.animationSettings; + } + + /** + * Get the step index inside of the stepper. + * + * ```typescript + * const step = this.stepper.steps[1]; + * const stepIndex: number = step.index; + * ``` + */ + public get index(): number { + return this._index; + } + + /** + * Gets/Sets the active state of the node + * + * @param value: boolean + */ + @Input() + public set active(value: boolean) { + if (value) { + this.stepperService.expand(this, false); + } else { + this.stepperService.collapse(this); + } + } + + public get active(): boolean { + return this.stepperService.activeStep === this; + } + + + /** + * Emitted when the node's `expanded` property changes. + * + * ```html + * + * + * + * + * ``` + * + * ```typescript + * const node: IgxTreeNode = this.tree.findNodes(data[0])[0]; + * node.expandedChange.pipe(takeUntil(this.destroy$)).subscribe((e: boolean) => console.log("Node expansion state changed to ", e)) + * ``` + */ + @Output() + public expandedChange = new EventEmitter(); + + // /** @hidden @internal */ + // public get focused() { + // return this.isFocused && + // this.navService.focusedStep === this; + // } + + // // TODO: bind to disabled state when node is dragged + // /** + // * Gets/Sets the disabled state of the node + // * + // * @param value: boolean + // */ + // @Input() + // @HostBinding('class.igx-tree-node--disabled') + // public get disabled(): boolean { + // return this._disabled; + // } + + // public set disabled(value: boolean) { + // if (value !== this._disabled) { + // this._disabled = value; + // this.tree.disabledChange.emit(this); + // } + // } + + /** @hidden @internal */ + @HostBinding('class.igx-step') + public cssClass = 'igx-step'; + + // // TODO: will be used in Drag and Drop implementation + // /** @hidden @internal */ + // @ViewChild('ghostTemplate', { read: ElementRef }) + // public header: ElementRef; + + // @ViewChild('defaultIndicator', { read: TemplateRef, static: true }) + // private _defaultExpandIndicatorTemplate: TemplateRef; + + /** + * @hidden + * @internal + */ + @ViewChild('contentTemplate',) + public contentTemplate: TemplateRef; + + @ViewChild('verticalContentContainer', { read: ElementRef }) + public verticalContentContainer: ElementRef; + + public horizontalContentContainer: ElementRef; + + /** @hidden @internal */ + public get isCompact(): boolean { + return this.stepper?.displayDensity === DisplayDensity.compact; + } + + /** @hidden @internal */ + public get isCosy(): boolean { + return this.stepper?.displayDensity === DisplayDensity.cosy; + } + + /** @hidden @internal */ + public isFocused: boolean; + + // private _disabled = false; + private _index = NEXT_ID - 1; + + constructor( + @Inject(IGX_STEPPER_COMPONENT) public stepper: IgxStepperComponent, + protected stepperService: IgxStepperService, + protected cdr: ChangeDetectorRef, + protected builder: AnimationBuilder, + private element: ElementRef + ) { + super(builder); + } + + public get isHorizontal() { + return this.stepper.orientation === IgxStepperOrienatation.Horizontal; + } + + /** + * The native DOM element representing the node. Could be null in certain environments. + * + * ```typescript + * // get the nativeElement of the second node + * const node: IgxTreeNode = this.tree.nodes.first(); + * const nodeElement: HTMLElement = node.nativeElement; + * ``` + */ + /** @hidden @internal */ + public get nativeElement() { + return this.element.nativeElement; + } + + /** @hidden @internal */ + public ngOnInit() { + this.openAnimationDone.pipe(takeUntil(this.destroy$)).subscribe( + () => { + this.stepper.stepExpanded.emit({ owner: this.stepper, activeStep: this }); + } + ); + this.closeAnimationDone.pipe(takeUntil(this.destroy$)).subscribe(() => { + this.stepper.stepCollapsed.emit({ owner: this.stepper, activeStep: this }); + this.stepperService.collapse(this); + this.cdr.markForCheck(); + }); + } + + /** @hidden @internal */ + public ngAfterViewInit() { } + + // /** + // * @hidden @internal + // * Sets the focus to the node's child, if present + // * Sets the node as the tree service's focusedNode + // * Marks the node as the current active element + // */ + // public handleFocus(): void { + // // if (this.disabled) { + // // return; + // // } + // if (this.navService.focusedStep !== this) { + // this.navService.focusedStep = this; + // } + // this.isFocused = true; + // } + + /** + * @hidden @internal + * Clear the node's focused status + */ + public clearFocus(): void { + this.isFocused = false; + } + + /** + * @hidden @internal + */ + public onPointerDown(event) { + event.stopPropagation(); + if (this.stepperService.activeStep === this) { + return; + } + // this.navService.focusedStep = this; + this.expand(); + } + + public ngOnDestroy() { + super.ngOnDestroy(); + } + + /** + * Expands the node, triggering animation + * + * ```html + * + * My Node + * + * + * ``` + * + * ```typescript + * const myNode: IgxTreeNode = this.tree.findNodes(data[0])[0]; + * myNode.expand(); + * ``` + */ + public expand() { + const args: IStepTogglingEventArgs = { + owner: this.stepper, + activeStep: this, + previousActiveStep: this.stepperService.previousActiveStep, + cancel: false + + }; + this.stepper.stepExpanding.emit(args); + if (!args.cancel) { + this.stepperService.expand(this, true); + this.cdr.detectChanges(); + if (!this.isHorizontal) { + this.playOpenAnimation( + this.verticalContentContainer + ); + } else { + this.playOpenAnimation( + this.horizontalContentContainer + ); + } + + } + } + + /** + * Collapses the node, triggering animation + * + * ```html + * + * My Node + * + * + * ``` + * + * ```typescript + * const myNode: IgxTreeNode = this.tree.findNodes(data[0])[0]; + * myNode.collapse(); + * ``` + */ + public collapse() { + const args: IStepTogglingEventArgs = { + owner: this.stepper, + activeStep: this, + previousActiveStep: this.stepperService.previousActiveStep, + cancel: false + + }; + this.stepper.stepCollapsing.emit(args); + if (!args.cancel) { + this.stepperService.collapsing(this); + if (!this.isHorizontal) { + this.playCloseAnimation( + this.verticalContentContainer + ); + } else { + this.playCloseAnimation( + this.horizontalContentContainer + ); + } + } + } + + public get collapsing() { + return this.stepperService.collapsingSteps.has(this); + } +} diff --git a/projects/igniteui-angular/src/lib/stepper/stepper-navigation.service.ts b/projects/igniteui-angular/src/lib/stepper/stepper-navigation.service.ts new file mode 100644 index 00000000000..cfee58fbb66 --- /dev/null +++ b/projects/igniteui-angular/src/lib/stepper/stepper-navigation.service.ts @@ -0,0 +1,217 @@ +// import { Injectable, OnDestroy } from '@angular/core'; +// import { Subject } from 'rxjs'; +// import { IgxStepperComponent } from './igx-stepper.component'; +// import { IgxStepComponent } from './step/igx-step.component'; + + +// /** @hidden @internal */ +// @Injectable() +// export class IgxStepperNavigationService implements OnDestroy { +// private stepper: IgxStepperComponent; + +// private _focusedStep: IgxStepComponent = null; + +// private _visibleChildren: IgxStepComponent[] = []; +// // private _invisibleChildren: Set = new Set(); +// // private _disabledChildren: Set = new Set(); + +// private _cacheChange = new Subject(); + +// constructor() { +// this._cacheChange.subscribe(() => { +// // this._visibleChildren = +// // this.stepper?.steps ? +// // this.stepper.steps.filter(e => !(this._invisibleChildren.has(e) || this._disabledChildren.has(e))) : +// // []; +// }); +// } + +// public register(stepper: IgxStepperComponent) { +// this.stepper = stepper; +// } + +// public get focusedStep() { +// return this._focusedStep; +// } + +// public set focusedStep(value: IgxStepComponent) { +// if (this._focusedStep === value) { +// return; +// } +// this._focusedStep = value; +// } + +// public get visibleChildren(): IgxStepComponent[] { +// return this._visibleChildren; +// } + +// // public update_disabled_cache(step: IgxStepComponent): void { +// // if (step.disabled) { +// // this._disabledChildren.add(step); +// // } else { +// // this._disabledChildren.delete(step); +// // } +// // this._cacheChange.next(); +// // } + +// // public init_invisible_cache() { +// // this.stepper.steps.filter(e => e.level === 0).forEach(step => { +// // this.update_visible_cache(step, step.expanded, false); +// // }); +// // this._cacheChange.next(); +// // } + +// // public update_visible_cache(step: IgxStepComponent, expanded: boolean, shouldEmit = true): void { +// // if (expanded) { +// // step._children.forEach(child => { +// // this._invisibleChildren.delete(child); +// // this.update_visible_cache(child, child.expanded, false); +// // }); +// // } else { +// // step.allChildren.forEach(c => this._invisibleChildren.add(c)); +// // } + +// // if (shouldEmit) { +// // this._cacheChange.next(); +// // } +// // } + +// // /** Handler for keydown events. Used in stepper.component.ts */ +// // public handleKeydown(event: KeyboardEvent) { +// // const key = event.key.toLowerCase(); +// // if (!this.focusedStep) { +// // return; +// // } +// // if (!(NAVIGATION_KEYS.has(key) || key === '*')) { +// // if (key === 'enter') { +// // this.activeStep = this.focusedStep; +// // } +// // return; +// // } +// // event.preventDefault(); +// // if (event.repeat) { +// // setTimeout(() => this.handleNavigation(event), 1); +// // } else { +// // this.handleNavigation(event); +// // } +// // } + +// public ngOnDestroy() { +// this._cacheChange.next(); +// this._cacheChange.complete(); +// } + +// // private handleNavigation(event: KeyboardEvent) { +// // switch (event.key.toLowerCase()) { +// // case 'home': +// // this.setFocusedAndActiveStep(this.visibleChildren[0]); +// // break; +// // case 'end': +// // this.setFocusedAndActiveStep(this.visibleChildren[this.visibleChildren.length - 1]); +// // break; +// // case 'arrowleft': +// // case 'left': +// // this.handleArrowLeft(); +// // break; +// // case 'arrowright': +// // case 'right': +// // this.handleArrowRight(); +// // break; +// // case 'arrowup': +// // case 'up': +// // this.handleUpDownArrow(true, event); +// // break; +// // case 'arrowdown': +// // case 'down': +// // this.handleUpDownArrow(false, event); +// // break; +// // case '*': +// // this.handleAsterisk(); +// // break; +// // case ' ': +// // case 'spacebar': +// // case 'space': +// // this.handleSpace(event.shiftKey); +// // break; +// // default: +// // return; +// // } +// // } + +// // private handleArrowLeft(): void { +// // if (this.focusedStep.expanded && +// // !this.stepperService.collapsingSteps.has(this.focusedStep) && this.focusedStep._children?.length) { +// // this.activeStep = this.focusedStep; +// // this.focusedStep.collapse(); +// // } else { +// // const parentStep = this.focusedStep.parentStep; +// // if (parentStep && !parentStep.disabled) { +// // this.setFocusedAndActiveStep(parentStep); +// // } +// // } +// // } + +// // private handleArrowRight(): void { +// // if (this.focusedStep._children.length > 0) { +// // if (!this.focusedStep.expanded) { +// // this.activeStep = this.focusedStep; +// // this.focusedStep.expand(); +// // } else { +// // if (this.stepperService.collapsingSteps.has(this.focusedStep)) { +// // this.focusedStep.expand(); +// // return; +// // } +// // const firstChild = this.focusedStep._children.find(step => !step.disabled); +// // if (firstChild) { +// // this.setFocusedAndActiveStep(firstChild); +// // } +// // } +// // } +// // } + +// // private handleUpDownArrow(isUp: boolean, event: KeyboardEvent): void { +// // const next = this.getVisibleStep(this.focusedStep, isUp ? -1 : 1); +// // if (next === this.focusedStep) { +// // return; +// // } + +// // if (event.ctrlKey) { +// // this.setFocusedAndActiveStep(next, false); +// // } else { +// // this.setFocusedAndActiveStep(next); +// // } +// // } + +// // private handleAsterisk(): void { +// // const steps = this.focusedStep.parentStep ? this.focusedStep.parentStep._children : this.stepper.rootSteps; +// // steps?.forEach(step => { +// // if (!step.disabled && (!step.expanded || this.stepperService.collapsingSteps.has(step))) { +// // step.expand(); +// // } +// // }); +// // } + +// // private handleSpace(shiftKey = false): void { +// // if (this.stepper.selection === IgxStepperSelectionType.None) { +// // return; +// // } + +// // this.activeStep = this.focusedStep; +// // if (shiftKey) { +// // this.selectionService.selectMultipleSteps(this.focusedStep); +// // return; +// // } + +// // if (this.focusedStep.selected) { +// // this.selectionService.deselectStep(this.focusedStep); +// // } else { +// // this.selectionService.selectStep(this.focusedStep); +// // } +// // } + +// // /** Gets the next visible step in the given direction - 1 -> next, -1 -> previous */ +// // private getVisibleStep(step: IgxStepComponent, dir: 1 | -1 = 1): IgxStepComponent { +// // const stepIndex = this.visibleChildren.indexOf(step); +// // return this.visibleChildren[stepIndex + dir] || step; +// // } +// } diff --git a/projects/igniteui-angular/src/lib/stepper/stepper.service.ts b/projects/igniteui-angular/src/lib/stepper/stepper.service.ts new file mode 100644 index 00000000000..157b6e8d9d7 --- /dev/null +++ b/projects/igniteui-angular/src/lib/stepper/stepper.service.ts @@ -0,0 +1,67 @@ +import { Injectable } from '@angular/core'; +import { IgxStepperComponent } from './igx-stepper.component'; +import { IgxStepComponent } from './step/igx-step.component'; + +/** @hidden @internal */ +@Injectable() +export class IgxStepperService { + + public activeStep: IgxStepComponent; + public previousActiveStep: IgxStepComponent; + + public collapsingSteps: Set = new Set(); + public stepper: IgxStepperComponent; + + /** + * Adds the step to the `expandedSteps` set and fires the steps change event + * + * @param step target step + * @param uiTrigger is the event triggered by a ui interraction (so we know if we should animate) + * @returns void + */ + public expand(step: IgxStepComponent, uiTrigger?: boolean): void { + this.collapsingSteps.delete(step); + + if (this.activeStep === step) { + return; + } + step.expandedChange.emit(true); + this.previousActiveStep = this.activeStep; + this.activeStep = step; + if (uiTrigger) { + this.previousActiveStep?.collapse(); + } else { + if (this.previousActiveStep) { + this.previousActiveStep.active = false; + } + } + } + + /** + * Adds a step to the `collapsing` collection + * + * @param step target step + */ + public collapsing(step: IgxStepComponent): void { + this.collapsingSteps.add(step); + } + + /** + * Removes the step from the 'expandedSteps' set and emits the step's change event + * + * @param step target step + * @returns void + */ + public collapse(step: IgxStepComponent): void { + this.collapsingSteps.delete(step); + if (this.activeStep === step) { + step.expandedChange.emit(false); + this.previousActiveStep = step; + this.activeStep = null; + } + } + + public register(stepper: IgxStepperComponent) { + this.stepper = stepper; + } +} diff --git a/src/app/app.component.ts b/src/app/app.component.ts index d4100bff3c6..13260d3399e 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -449,6 +449,11 @@ export class AppComponent implements OnInit { icon: 'feedback', name: 'Snackbar' }, + { + link: '/stepper', + icon: 'format_list_bulleted', + name: 'Stepper' + }, { link: '/tabs', icon: 'tab', diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 289f8e1b686..afaef4d96ff 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -153,6 +153,8 @@ import { GridLocalizationSampleComponent } from './grid-localization/grid-locali import { TreeGridGroupBySampleComponent } from './tree-grid-groupby/tree-grid-groupby.sample'; import { PaginationSampleComponent } from './pagination/pagination.component'; import { GridCellAPISampleComponent } from './grid-cell-api/grid-cell-api.sample'; +import { IgxStepperModule } from 'projects/igniteui-angular/src/lib/stepper/igx-stepper.component'; +import { IgxStepperSampleComponent } from './stepper/stepper.sample'; const components = [ AccordionSampleComponent, @@ -286,7 +288,8 @@ const components = [ GridNestedPropsSampleComponent, IgxColumnGroupingDirective, GridColumnTypesSampleComponent, - GridLocalizationSampleComponent + GridLocalizationSampleComponent, + IgxStepperSampleComponent ]; @NgModule({ @@ -317,7 +320,8 @@ const components = [ routing, HammerModule, IgxDateTimeEditorModule, - IgxButtonModule + IgxButtonModule, + IgxStepperModule ], providers: [ LocalService, diff --git a/src/app/app.routing.ts b/src/app/app.routing.ts index 196a0b79083..e2cc1ac12cc 100644 --- a/src/app/app.routing.ts +++ b/src/app/app.routing.ts @@ -95,6 +95,7 @@ import { GridLocalizationSampleComponent } from './grid-localization/grid-locali import { TreeGridGroupBySampleComponent } from './tree-grid-groupby/tree-grid-groupby.sample'; import { PaginationSampleComponent } from './pagination/pagination.component'; import { GridCellAPISampleComponent } from './grid-cell-api/grid-cell-api.sample'; +import { IgxStepperSampleComponent } from './stepper/stepper.sample'; const appRoutes = [ { @@ -446,6 +447,10 @@ const appRoutes = [ { path: 'pagination', Comment: PaginationSampleComponent + }, + { + path: 'stepper', + component: IgxStepperSampleComponent } ]; diff --git a/src/app/routing.ts b/src/app/routing.ts index 95add95642a..d22f4e8827e 100644 --- a/src/app/routing.ts +++ b/src/app/routing.ts @@ -125,6 +125,7 @@ import { GridLocalizationSampleComponent } from './grid-localization/grid-locali import { TreeGridGroupBySampleComponent } from './tree-grid-groupby/tree-grid-groupby.sample'; import { PaginationSampleComponent } from './pagination/pagination.component'; import { GridCellAPISampleComponent } from './grid-cell-api/grid-cell-api.sample'; +import { IgxStepperSampleComponent as StepperSampleComponent } from './stepper/stepper.sample'; const appRoutes = [ { @@ -598,6 +599,10 @@ const appRoutes = [ },{ path: 'pagination', component: PaginationSampleComponent + }, + { + path: 'stepper', + component: StepperSampleComponent } ]; diff --git a/src/app/stepper/stepper.sample.html b/src/app/stepper/stepper.sample.html new file mode 100644 index 00000000000..49ace62858e --- /dev/null +++ b/src/app/stepper/stepper.sample.html @@ -0,0 +1,53 @@ + + +
+ + + + done + +
Test me daddy
+
+ + + calendar_today + + + + + + alarm + + + + + + check_circle + + + Apple + Orange + Grapes + Banana + + + + + delete + +
+ +
+
+
+
+ + + + diff --git a/src/app/stepper/stepper.sample.scss b/src/app/stepper/stepper.sample.scss new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/app/stepper/stepper.sample.ts b/src/app/stepper/stepper.sample.ts new file mode 100644 index 00000000000..99c4f4e709a --- /dev/null +++ b/src/app/stepper/stepper.sample.ts @@ -0,0 +1,65 @@ +import { AfterViewInit, Component, ViewChild } from '@angular/core'; +import { + IgxStepperLabelPosition, IgxStepperOrienatation, IgxStepperProgressLine, IgxStepType +} from 'projects/igniteui-angular/src/lib/stepper/common'; +import { IgxStepperComponent } from 'projects/igniteui-angular/src/lib/stepper/igx-stepper.component'; + +@Component({ + templateUrl: 'stepper.sample.html', + styleUrls: ['stepper.sample.scss'] +}) +export class IgxStepperSampleComponent implements AfterViewInit { + @ViewChild('stepper', { static: true }) public stepper: IgxStepperComponent; + // public stepType = IgxStepType.Full; + // public labelPos = IgxStepperLabelPosition.Bottom; + // public stepTypes = [ + // { label: 'Indicator', stepType: IgxStepType.Indicator, selected: this.stepType === IgxStepType.Indicator, togglable: true }, + // { label: 'Label', stepType: IgxStepType.Label, selected: this.stepType === IgxStepType.Label, togglable: true }, + // { label: 'Full', stepType: IgxStepType.Full, selected: this.stepType === IgxStepType.Full, togglable: true } + // ]; + // public labelPositions = [ + // { label: 'Bottom', labelPos: IgxStepperLabelPosition.Bottom, + // selected: this.labelPos === IgxStepperLabelPosition.Bottom, togglable: true }, + // { label: 'Top', labelPos: IgxStepperLabelPosition.Top, + // selected: this.labelPos === IgxStepperLabelPosition.Top, togglable: true }, + // { label: 'End', labelPos: IgxStepperLabelPosition.End, + // selected: this.labelPos === IgxStepperLabelPosition.End, togglable: true }, + // { label: 'Start', labelPos: IgxStepperLabelPosition.Start, + // selected: this.labelPos === IgxStepperLabelPosition.Start, togglable: true } + // ]; + public ngAfterViewInit() { + // requestAnimationFrame(() => { + // this.stepper.steps[1].completedStyle = IgxStepperProgressLine.Dashed; + // }); + } + // public toggleStepTypes(event){ + // this.stepType = this.stepTypes[event.index].stepType; + // } + // public toggleLabelPos(event){ + // this.labelPos = this.labelPositions[event.index].labelPos; + // } + + public activeChanged(event, step) { + console.log('ACTIVE CHANGED'); + console.log(event, step); + } + + public activeStepChanged(ev) { + console.log('ACTIVE STEP CHANGED'); + console.log(ev); + } + + public activeStepChanging(ev) { + // ev.cancel = true; + console.log('ACTIVE STEP CHANGING'); + console.log(ev); + } + + public changeOrientation() { + if (this.stepper.orientation === IgxStepperOrienatation.Horizontal) { + this.stepper.orientation = IgxStepperOrienatation.Vertical; + } else { + this.stepper.orientation = IgxStepperOrienatation.Horizontal; + } + } +}