diff --git a/projects/ui/src/lib/components/po-stepper/po-step/po-step.component.ts b/projects/ui/src/lib/components/po-stepper/po-step/po-step.component.ts index ed929fd4d..4ca0c6ceb 100644 --- a/projects/ui/src/lib/components/po-stepper/po-step/po-step.component.ts +++ b/projects/ui/src/lib/components/po-stepper/po-step/po-step.component.ts @@ -1,4 +1,4 @@ -import { AfterContentInit, Component, ElementRef, Input } from '@angular/core'; +import { AfterContentInit, Component, ElementRef, Input, TemplateRef } from '@angular/core'; import { Observable } from 'rxjs'; import { uuid } from '../../../utils/util'; @@ -75,7 +75,7 @@ export class PoStepComponent implements AfterContentInit { | ((currentStep) => Observable); /** Título que será exibido descrevendo o passo (*step*). */ - @Input('p-label') label: string; + @Input('p-label') label: string = ''; // ID do step id?: string = uuid(); @@ -93,6 +93,42 @@ export class PoStepComponent implements AfterContentInit { return this._status; } + /** + * @optional + * + * @description + * Define o ícone padrão do step em seu status *default*. + * Esta propriedade permite usar ícones da [Biblioteca de ícones](/guides/icons) ou da biblioteca [Phosphor](https://phosphoricons.com/). + * + * Exemplo usando a biblioteca de ícones padrão: + * ``` + * + * ... + * + * + * ``` + * Exemplo usando a biblioteca *Phosphor*: + * ``` + * + * ... + * + * + * ``` + * Outra opção seria a customização do ícone através do `TemplateRef`, conforme exemplo abaixo: + * ``` + * + * ... + * + * + * + * + * + * Deve-se usar `font-size: inherit` para ajustar ícones que não se ajustam automaticamente. + */ + @Input('p-icon-default') iconDefault?: string | TemplateRef; + constructor(private elementRef: ElementRef) {} ngAfterContentInit() { diff --git a/projects/ui/src/lib/components/po-stepper/po-stepper-base.component.spec.ts b/projects/ui/src/lib/components/po-stepper/po-stepper-base.component.spec.ts index 22c128514..d3cc91117 100644 --- a/projects/ui/src/lib/components/po-stepper/po-stepper-base.component.spec.ts +++ b/projects/ui/src/lib/components/po-stepper/po-stepper-base.component.spec.ts @@ -34,10 +34,15 @@ describe('PoStepperBaseComponent:', () => { it('p-step: shouldn`t set property with invalid values', () => { const invalidValues = [0, 3, undefined, null, {}, '']; + const validStep = 1; component.steps = [{ label: 'Step 1' }]; + component.step = validStep; - expectPropertiesValues(component, 'step', invalidValues, 1); + invalidValues.forEach(value => { + component.step = value as any; + expect(component.step).toBe(validStep); + }); }); it('p-steps: should update property with empty `array` if invalid values', () => { @@ -46,14 +51,6 @@ describe('PoStepperBaseComponent:', () => { expectPropertiesValues(component, 'steps', invalidValues, []); }); - it('p-steps: should update property with `array` with status default and initial step 1', () => { - component.steps = [{ label: 'Step 1' }, { label: 'Step 2' }]; - - expect(component.steps[0].status).toBe(PoStepperStatus.Active); - expect(component.steps[1].status).toBe(PoStepperStatus.Default); - expect(component.step).toBe(1); - }); - it('p-steps: should respect status disabled', () => { component.steps = [ { label: 'Step 1', status: PoStepperStatus.Active }, diff --git a/projects/ui/src/lib/components/po-stepper/po-stepper-base.component.ts b/projects/ui/src/lib/components/po-stepper/po-stepper-base.component.ts index afd6a3c76..2e692ace1 100644 --- a/projects/ui/src/lib/components/po-stepper/po-stepper-base.component.ts +++ b/projects/ui/src/lib/components/po-stepper/po-stepper-base.component.ts @@ -1,4 +1,4 @@ -import { EventEmitter, Input, Output, Directive } from '@angular/core'; +import { EventEmitter, Input, Output, Directive, TemplateRef } from '@angular/core'; import { convertToBoolean } from '../../utils/util'; @@ -49,6 +49,35 @@ const poStepperOrientationDefault = PoStepperOrientation.Horizontal; * * - Evite `labels` extensos que quebram o layout do `po-stepper`, use `labels` diretos, curtos e intuitivos. * - Utilize apenas um `po-stepper` por página. + * + * #### Tokens customizáveis + * + * É possível alterar o estilo do componente usando os seguintes tokens (CSS): + * + * > Para maiores informações, acesse o guia [Personalizando o Tema Padrão com Tokens CSS](https://po-ui.io/guides/theme-customization). + * + * | Propriedade | Descrição | Valor Padrão | + * |------------------------------------------|-------------------------------------------------------|---------------------------------------------------| + * | **Label** | | | + * | `--font-family` | Família tipográfica usada | `var(--font-family-theme)` | + * | `--font-size` | Tamanho da fonte | `var(--font-size-default)` | + * | `--font-weight` | Peso da fonte | `var(--font-weight-normal)` | + * | **Step - Done** | | | + * | `--text-color` | Cor do texto no step concluído | `var(--color-neutral-dark-70)` | + * | `--color-icon-done` | Cor do ícone no step concluído | `var(--color-neutral-dark-70)` | + * | `--background-done` | Cor de fundo no step concluído | `var(--color-neutral-light-00)` | + * | **Line - Done** | | | + * | `--color-line-done` | Cor da linha no step concluído | `var(--color-neutral-mid-40)` | + * | **Step - Current** | | | + * | `--color-icon-current` | Cor do ícone no step atual | `var(--color-neutral-light-00)` | + * | `--background-current` | Cor de fundo no step atual | `var(--color-action-default)` | + * | `--font-weight-current` | Peso da fonte no step atual | `var(--font-weight-bold)` | + * | **Step - Next** | | | + * | `--font-size-circle` | Tamanho da fonte no círculo do próximo step | `var(--font-size-sm)` | + * | `--color-next` | Cor do ícone no próximo step | `var(--color-action-disabled)` | + * | `--text-color-next` | Cor do texto no próximo step | `var(--color-neutral-light-30)` | + * | **Focused** | | | + * | `--outline-color-focused` | Cor do outline do estado de focus | `var(--color-action-focus)` | */ @Directive() export class PoStepperBaseComponent { @@ -146,7 +175,7 @@ export class PoStepperBaseComponent { @Input('p-steps') set steps(steps: Array) { this._steps = Array.isArray(steps) ? steps : []; this._steps.forEach(step => (step.status = step.status ?? PoStepperStatus.Default)); - this.step = 1; + this.initializeSteps(); } get steps(): Array { @@ -171,4 +200,74 @@ export class PoStepperBaseComponent { get sequential(): boolean { return this._sequential; } + + /** + * @optional + * + * @description + * Permite definir o ícone do step no status concluído. + * Esta propriedade permite usar ícones da [Biblioteca de ícones](/guides/icons) ou da biblioteca [Phosphor](https://phosphoricons.com/). + * + * Exemplo usando a biblioteca de ícones padrão: + * ``` + * + * ... + * + * ``` + * Exemplo usando a biblioteca *Phosphor*: + * ``` + * + * ... + * + * ``` + * Outra opção seria a customização do ícone através do `TemplateRef`, conforme exemplo abaixo: + * ``` + * + * ... + * + * + * + * + * + * ``` + * > Deve-se usar `font-size: inherit` para ajustar ícones que não se ajustam automaticamente. + * + * @default `po-icon-ok` + */ + @Input('p-step-icon-done') iconDone?: string | TemplateRef; + + /** + * @optional + * + * @description + * Permite definir o ícone do step no status ativo. + * Esta propriedade permite usar ícones da [Biblioteca de ícones](/guides/icons) ou da biblioteca [Phosphor](https://phosphoricons.com/). + * + * Exemplo usando a biblioteca de ícones padrão: + * ``` + * + * ... + * + * ``` + * Exemplo usando a biblioteca *Phosphor*: + * ``` + * + * ... + * + * ``` + * Para customizar o ícone através do `TemplateRef`, veja a documentação da propriedade `p-step-icon-done`. + * + * > Deve-se usar `font-size: inherit` para ajustar ícones que não se ajustam automaticamente. + * + * @default `po-icon-edit` + */ + @Input('p-step-icon-active') iconActive?: string | TemplateRef; + + private initializeSteps(): void { + const hasStatus = this._steps.some(step => step.status !== PoStepperStatus.Default); + + if (!hasStatus && this.step === 1) { + this.step = 1; + } + } } diff --git a/projects/ui/src/lib/components/po-stepper/po-stepper-circle/po-stepper-circle.component.html b/projects/ui/src/lib/components/po-stepper/po-stepper-circle/po-stepper-circle.component.html index 610acaf89..cd3ee045d 100644 --- a/projects/ui/src/lib/components/po-stepper/po-stepper-circle/po-stepper-circle.component.html +++ b/projects/ui/src/lib/components/po-stepper/po-stepper-circle/po-stepper-circle.component.html @@ -1,22 +1,30 @@ -
- - {{ !icons && !isDone ? content : '' }} - - -
+
+ +
+ +
+
+ + + {{ !icons && !isDone && !iconDefault ? content : '' }} + +
diff --git a/projects/ui/src/lib/components/po-stepper/po-stepper-circle/po-stepper-circle.component.spec.ts b/projects/ui/src/lib/components/po-stepper/po-stepper-circle/po-stepper-circle.component.spec.ts index f2e55135d..99e6a615c 100644 --- a/projects/ui/src/lib/components/po-stepper/po-stepper-circle/po-stepper-circle.component.spec.ts +++ b/projects/ui/src/lib/components/po-stepper/po-stepper-circle/po-stepper-circle.component.spec.ts @@ -168,24 +168,6 @@ describe('PoStepperCircleComponent:', () => { }); describe('Templates:', () => { - it('should change `tabindex` to `-1` if component is disabled', () => { - component.status = PoStepperStatus.Disabled; - fixture.detectChanges(); - - const poStepperCircleElement = nativeElement.querySelector('.po-stepper-circle[tabindex="-1"]'); - - expect(poStepperCircleElement).toBeTruthy(); - }); - - it('should change `tabindex` to `0` if component isn`t disabled', () => { - component.status = PoStepperStatus.Active; - fixture.detectChanges(); - - const poStepperCircleElement = nativeElement.querySelector('.po-stepper-circle[tabindex="0"]'); - - expect(poStepperCircleElement).toBeTruthy(); - }); - it('should find `po-stepper-circle-content-md` in `span` if `isMediumStep` is `true`.', () => { spyOnProperty(component, 'isMediumStep').and.returnValue(true); @@ -206,41 +188,54 @@ describe('PoStepperCircleComponent:', () => { expect(stepperCircleContentLg).toBeTruthy(); }); - it('shouldn`t find `po-stepper-circle-content` if `isActive` is true', () => { + it('should have `po-stepper-circle-border` when not active and not error', () => { + component.status = PoStepperStatus.Default; + fixture.detectChanges(); + + const circleElement = fixture.nativeElement.querySelector('.po-stepper-circle'); + expect(circleElement.classList).toContain('po-stepper-circle-border'); + expect(circleElement.classList).not.toContain('po-stepper-circle-done'); + }); + + it('should have `po-stepper-circle-done` when isDone', () => { + component.status = PoStepperStatus.Done; + fixture.detectChanges(); + + const circleElement = fixture.nativeElement.querySelector('.po-stepper-circle'); + expect(circleElement.classList).toContain('po-stepper-circle-done'); + }); + + it('should find `po-stepper-circle-active` if `isActive` is true.', () => { + spyOnProperty(component, 'isError').and.returnValue(false); spyOnProperty(component, 'isActive').and.returnValue(true); fixture.detectChanges(); - const stepperCircleContent = nativeElement.querySelector('.po-stepper-circle-content'); + const stepperCircleActive = nativeElement.querySelector('.po-stepper-circle-active'); - expect(stepperCircleContent).toBeNull(); + expect(stepperCircleActive).toBeTruthy(); }); - it('shouldn`t find `po-stepper-circle-with-icon` and `po-icon` if `icons` is `false`.', () => { - component.icons = false; + it('should find `po-stepper-circle-active` if `isError` is true.', () => { + spyOnProperty(component, 'isError').and.returnValue(true); + spyOnProperty(component, 'isActive').and.returnValue(false); fixture.detectChanges(); - const StepperCircleWitchIcon = nativeElement.querySelector('.po-stepper-circle-with-icon'); - const iconClass = nativeElement.querySelector('.po-icon'); - expect(StepperCircleWitchIcon).toBeNull(); - expect(iconClass).toBeNull(); + const stepperCircleActive = nativeElement.querySelector('.po-stepper-circle-active'); + + expect(stepperCircleActive).toBeTruthy(); }); - it('should find `po-icon-info` if `status` is `Active`, `Default` or `Disabled` and `icons` is true.', () => { - component.icons = true; + it('shouldn`t find `po-stepper-circle-active` if `isError` and `isActive` is false.', () => { + spyOnProperty(component, 'isError').and.returnValue(false); + spyOnProperty(component, 'isActive').and.returnValue(false); - component.status = PoStepperStatus.Default; fixture.detectChanges(); - expect(PoIconInfo()).toBeTruthy(); - component.status = PoStepperStatus.Disabled; - fixture.detectChanges(); - expect(PoIconInfo()).toBeTruthy(); + const stepperCircleActive = nativeElement.querySelector('.po-stepper-circle-active'); - component.status = PoStepperStatus.Error; - fixture.detectChanges(); - expect(PoIconInfo()).toBeNull(); + expect(stepperCircleActive).toBeNull(); }); it('should find `po-icon` if `isDone` is true.', () => { @@ -263,85 +258,145 @@ describe('PoStepperCircleComponent:', () => { expect(poIcon).toBeNull(); }); - it('should find `po-stepper-circle-active` if `isActive` is true.', () => { - spyOnProperty(component, 'isError').and.returnValue(false); - spyOnProperty(component, 'isActive').and.returnValue(true); - + it('should apply `iconActive` from Phosphor library if `status` is `Active` and `iconActive` is set.', () => { + component.status = PoStepperStatus.Active; + component.iconActive = 'ph ph-anchor'; fixture.detectChanges(); - const stepperCircleActive = nativeElement.querySelector('.po-stepper-circle-active'); - - expect(stepperCircleActive).toBeTruthy(); + const activeIcon = nativeElement.querySelector('po-icon')?.querySelector('i'); + expect(activeIcon).toBeTruthy(); + expect(activeIcon.classList.contains('ph-anchor')).toBeTrue(); }); - it('should find `po-stepper-circle-active` if `isError` is true.', () => { - spyOnProperty(component, 'isError').and.returnValue(true); - spyOnProperty(component, 'isActive').and.returnValue(false); + it('should apply `iconActive` from Icons library if `status` is `Active` and `iconActive` is set.', () => { + component.status = PoStepperStatus.Active; + component.iconActive = 'po-icon po-icon-device-notebook'; + fixture.detectChanges(); + + const activeIcon = nativeElement.querySelector('po-icon')?.querySelector('i'); + expect(activeIcon).toBeTruthy(); + expect(activeIcon.classList.contains('po-icon-device-notebook')).toBeTrue(); + }); + it('should apply `ICON_EDIT` if `status` is `Active` and `iconActive` is not set.', () => { + component.status = PoStepperStatus.Active; + component.iconActive = undefined; fixture.detectChanges(); - const stepperCircleActive = nativeElement.querySelector('.po-stepper-circle-active'); + const activeIcon = PoIconEdit(); - expect(stepperCircleActive).toBeTruthy(); + expect(activeIcon).toBeTruthy(); + expect(activeIcon.classList.contains('ph-pencil-simple')).toBeTrue(); }); - it('shouldn`t find `po-stepper-circle-active` if `isError` and `isActive` is false.', () => { - spyOnProperty(component, 'isError').and.returnValue(false); - spyOnProperty(component, 'isActive').and.returnValue(false); - + it('should apply `iconDone` from Phosphor library if `status` is `Done` and `iconDone` is set.', () => { + component.status = PoStepperStatus.Done; + component.iconDone = 'ph ph-check-circle'; fixture.detectChanges(); - const stepperCircleActive = nativeElement.querySelector('.po-stepper-circle-active'); + const doneIcon = nativeElement.querySelector('po-icon')?.querySelector('i'); - expect(stepperCircleActive).toBeNull(); + expect(doneIcon).toBeTruthy(); + expect(doneIcon.classList.contains('ph-check-circle')).toBeTrue(); }); - it('should find `po-icon-ok` if `status` is `Done` and `icons` is true.', () => { - component.icons = true; + it('should apply `iconDone` from Icons library if `status` is `Done` and `iconDone` is set.', () => { + component.status = PoStepperStatus.Done; + component.iconDone = 'po-icon-clock'; + fixture.detectChanges(); + + const doneIcon = nativeElement.querySelector('po-icon')?.querySelector('i'); + expect(doneIcon.classList.contains('po-icon-clock')).toBeTrue(); + }); + + it('should apply `ICON_OK` if `status` is `Done` and `iconDone` is not set.', () => { component.status = PoStepperStatus.Done; + component.iconDone = undefined; fixture.detectChanges(); - expect(PoIconOk()).toBeTruthy(); + const doneIcon = PoIconOk; + expect(doneIcon).toBeTruthy(); + }); + + it('should apply `iconDefault` from Phosphor library if `status` is `Default` or `Disabled` and `iconDefault` is set.', () => { component.status = PoStepperStatus.Default; + component.iconDefault = 'ph ph-first-aid'; fixture.detectChanges(); - expect(PoIconOk()).toBeNull(); + + let defaultIcon = nativeElement.querySelector('po-icon')?.querySelector('i'); + expect(defaultIcon).toBeTruthy(); + expect(defaultIcon.classList.contains('ph-first-aid')).toBeTrue(); component.status = PoStepperStatus.Disabled; fixture.detectChanges(); - expect(PoIconOk()).toBeNull(); - component.status = PoStepperStatus.Error; + defaultIcon = nativeElement.querySelector('po-icon')?.querySelector('i'); + expect(defaultIcon).toBeTruthy(); + expect(defaultIcon.classList.contains('ph-first-aid')).toBeTrue(); + }); + + it('should apply `iconDefault` from Icons library if `status` is `Default` or `Disabled` and `iconDefault` is set.', () => { + component.status = PoStepperStatus.Default; + component.iconDefault = 'po-icon po-icon-user'; + fixture.detectChanges(); + + let defaultIcon = nativeElement.querySelector('po-icon')?.querySelector('i'); + expect(defaultIcon).toBeTruthy(); + expect(defaultIcon.classList.contains('po-icon-user')).toBeTrue(); + + component.status = PoStepperStatus.Disabled; fixture.detectChanges(); - expect(PoIconOk()).toBeNull(); + + defaultIcon = nativeElement.querySelector('po-icon')?.querySelector('i'); + expect(defaultIcon).toBeTruthy(); + expect(defaultIcon.classList.contains('po-icon-user')).toBeTrue(); }); - it('should find `icon-exclamation` if `status` is `Error` and `icons` is true.', () => { + it('should apply `ICON_INFO` if `status` is `Default` or `Disabled`, `iconDefault` is not set, and `icons` is true.', () => { + component.status = PoStepperStatus.Default; + component.iconDefault = undefined; component.icons = true; + fixture.detectChanges(); - component.status = PoStepperStatus.Error; + const defaultIcon = PoIconInfo(); + expect(defaultIcon).toBeTruthy(); + expect(defaultIcon.classList.contains('ph-info')).toBeTrue(); + + component.status = PoStepperStatus.Disabled; fixture.detectChanges(); - expect(PoIconExclamation()).toBeTruthy(); + const disabledIcon = PoIconInfo(); + expect(disabledIcon).toBeTruthy(); + expect(disabledIcon.classList.contains('ph-info')).toBeTrue(); + }); + + it('should display an empty string if `status` is `Default` or `Disabled`, `iconDefault` is not set, and `icons` is false.', () => { component.status = PoStepperStatus.Default; + component.iconDefault = undefined; + component.icons = false; fixture.detectChanges(); - expect(PoIconExclamation()).toBeNull(); + + const defaultIcon = nativeElement.querySelector('.po-stepper-circle-content'); + expect(defaultIcon.textContent.trim()).toBe(''); component.status = PoStepperStatus.Disabled; fixture.detectChanges(); - expect(PoIconExclamation()).toBeNull(); + + const disabledIcon = nativeElement.querySelector('.po-stepper-circle-content'); + expect(disabledIcon.textContent.trim()).toBe(''); }); }); function PoIconInfo() { - return nativeElement.querySelector('.ph-info'); + return nativeElement.querySelector('po-icon')?.querySelector('i.ph.ph-info'); } function PoIconOk() { return nativeElement.querySelector('.ph-check'); } - function PoIconExclamation() { - return nativeElement.querySelector('.ph-warning-circle'); + function PoIconEdit() { + return nativeElement.querySelector('po-icon')?.querySelector('i.ph-pencil-simple'); } }); diff --git a/projects/ui/src/lib/components/po-stepper/po-stepper-circle/po-stepper-circle.component.ts b/projects/ui/src/lib/components/po-stepper/po-stepper-circle/po-stepper-circle.component.ts index 2a62181bf..525cd8fba 100644 --- a/projects/ui/src/lib/components/po-stepper/po-stepper-circle/po-stepper-circle.component.ts +++ b/projects/ui/src/lib/components/po-stepper/po-stepper-circle/po-stepper-circle.component.ts @@ -1,4 +1,4 @@ -import { Component, Input } from '@angular/core'; +import { Component, Input, TemplateRef } from '@angular/core'; import { PoStepperStatus } from '../enums/po-stepper-status.enum'; @@ -20,6 +20,15 @@ export class PoStepperCircleComponent { // Conteúdo que irá aparecer no círculo do *step*. @Input('p-content') content: any; + // Ícone para o status Active do *step*. + @Input('p-step-icon-active') iconActive?: string | TemplateRef; + + // Ícone para o status Done do *step*. + @Input('p-step-icon-done') iconDone?: string | TemplateRef; + + // Ícone para o status default do *step*. + @Input('p-icon-default') iconDefault?: string | TemplateRef; + // Define se serão exibidos ícones no lugar de números nos steps. @Input('p-icons') icons: boolean; diff --git a/projects/ui/src/lib/components/po-stepper/po-stepper-item.interface.ts b/projects/ui/src/lib/components/po-stepper/po-stepper-item.interface.ts index 12758ef7d..b7304948b 100644 --- a/projects/ui/src/lib/components/po-stepper/po-stepper-item.interface.ts +++ b/projects/ui/src/lib/components/po-stepper/po-stepper-item.interface.ts @@ -1,3 +1,4 @@ +import { TemplateRef } from '@angular/core'; import { PoStepperStatus } from './enums/po-stepper-status.enum'; /** @@ -8,8 +9,17 @@ import { PoStepperStatus } from './enums/po-stepper-status.enum'; * Interface para definição dos *steps* do componente `po-stepper` quando utilizada a propriedade `p-steps`. */ export interface PoStepperItem { + /** Define o ícone do *step* ativo. */ + iconActive?: string | TemplateRef; + + /** Define o ícone do *step* concluído. */ + iconDone?: string | TemplateRef; + + /** Define o ícone do *step* default. */ + iconDefault?: string | TemplateRef; + /** Texto do item do stepper. */ - label: string; + label?: string; /** Define o estado de exibição do *step*. */ status?: PoStepperStatus; diff --git a/projects/ui/src/lib/components/po-stepper/po-stepper-label/po-stepper-label.component.html b/projects/ui/src/lib/components/po-stepper/po-stepper-label/po-stepper-label.component.html index cef55d853..943e8af69 100644 --- a/projects/ui/src/lib/components/po-stepper/po-stepper-label/po-stepper-label.component.html +++ b/projects/ui/src/lib/components/po-stepper/po-stepper-label/po-stepper-label.component.html @@ -1,3 +1,14 @@ -
- {{ content }} + diff --git a/projects/ui/src/lib/components/po-stepper/po-stepper-label/po-stepper-label.component.spec.ts b/projects/ui/src/lib/components/po-stepper/po-stepper-label/po-stepper-label.component.spec.ts index 4f203d28c..4d0e695ca 100644 --- a/projects/ui/src/lib/components/po-stepper/po-stepper-label/po-stepper-label.component.spec.ts +++ b/projects/ui/src/lib/components/po-stepper/po-stepper-label/po-stepper-label.component.spec.ts @@ -1,9 +1,10 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { configureTestSuite } from './../../../util-test/util-expect.spec'; - import { PoStepperLabelComponent } from './po-stepper-label.component'; +import { configureTestSuite } from '../../../util-test/util-expect.spec'; import { PoStepperModule } from '../po-stepper.module'; +import { By } from '@angular/platform-browser'; +import { SimpleChange } from '@angular/core'; describe('PoStepperLabelComponent: ', () => { let component: PoStepperLabelComponent; @@ -25,4 +26,245 @@ describe('PoStepperLabelComponent: ', () => { it('should be created', () => { expect(component instanceof PoStepperLabelComponent).toBeTruthy(); }); + + describe('Properties:', () => { + it('should set content property correctly', () => { + component.content = 'Step 1'; + fixture.detectChanges(); + expect(component.content).toBe('Step 1'); + }); + + it('should handle vertical orientation property', () => { + component.isVerticalOrientation = true; + fixture.detectChanges(); + expect(component.isVerticalOrientation).toBeTrue(); + }); + + it('should handle status property', () => { + component.status = 'active'; + fixture.detectChanges(); + expect(component.status).toBe('active'); + }); + }); + + describe('Methods:', () => { + it('should call `updateLabel` and detectChanges when content changes', () => { + const updateLabelSpy = spyOn(component as any, 'updateLabel').and.callThrough(); + + const changes = { + content: new SimpleChange(null, 'Step 1', true), + isVerticalOrientation: null + }; + + component.ngOnChanges(changes); + + fixture.detectChanges(); + + expect(updateLabelSpy).toHaveBeenCalled(); + }); + + it('should call `updateLabel` and detectChanges when isVerticalOrientation changes', () => { + const updateLabelSpy = spyOn(component as any, 'updateLabel').and.callThrough(); + + const changes = { + content: null, + isVerticalOrientation: new SimpleChange(null, true, true) + }; + + component.ngOnChanges(changes); + + fixture.detectChanges(); + + expect(updateLabelSpy).toHaveBeenCalled(); + }); + + it('should call updateLabel and detectChanges when both content and isVerticalOrientation change', () => { + const updateLabelSpy = spyOn(component as any, 'updateLabel').and.callThrough(); + + const changes = { + content: new SimpleChange(null, 'New Content', true), + isVerticalOrientation: new SimpleChange(null, true, true) + }; + + component.ngOnChanges(changes); + + fixture.detectChanges(); + + expect(updateLabelSpy).toHaveBeenCalled(); + }); + + it('should not update label if labelElement is null', () => { + const updateLabelSpy = spyOn(component, 'updateLabel').and.callThrough(); + component.labelElement = null; + + (component as any).updateLabel(); + + expect(updateLabelSpy).toHaveBeenCalled(); + expect(component.tooltipContent).toBeNull(); + }); + + it('should truncate content when isVerticalOrientation is true and content length exceeds maxLabelLength', () => { + const originalContent = 'This is a long content that needs truncation'; + component.content = originalContent; + component.isVerticalOrientation = true; + (component as any).maxLabelLength = 10; + + (component as any).updateLabel(); + + expect(component.displayedContent).toBe('This is a ...'); + }); + + it('should set tooltipContent to content if text overflows horizontally', () => { + const labelElement = document.createElement('div'); + component.labelElement = { nativeElement: labelElement } as any; + + component.content = 'Content that will overflow horizontally'; + component.isVerticalOrientation = false; + (component as any).maxLabelLength = 50; + + Object.defineProperty(labelElement, 'scrollWidth', { value: 200 }); + Object.defineProperty(labelElement, 'clientWidth', { value: 50 }); + + (component as any).updateTooltip(); + + expect(component.tooltipContent).toBe(component.content); + }); + + it('should set tooltipContent to null if content length does not exceed maxLabelLength and no text overflow', () => { + const labelElement = document.createElement('div'); + component.labelElement = { nativeElement: labelElement } as any; + + component.content = 'Short content'; + component.isVerticalOrientation = true; + (component as any).maxLabelLength = 20; + + Object.defineProperty(labelElement, 'scrollWidth', { value: 50 }); + Object.defineProperty(labelElement, 'clientWidth', { value: 50 }); + Object.defineProperty(labelElement, 'scrollHeight', { value: 20 }); + Object.defineProperty(labelElement, 'clientHeight', { value: 20 }); + + (component as any).updateTooltip(); + + expect(component.tooltipContent).toBeNull(); + }); + + it('should set tooltipContent to content if content length exceeds maxLabelLength', () => { + const labelElement = document.createElement('div'); + component.labelElement = { nativeElement: labelElement } as any; + + component.content = 'This is a long content that needs truncation'; + component.isVerticalOrientation = true; + (component as any).maxLabelLength = 20; + + Object.defineProperty(labelElement, 'scrollWidth', { value: 50 }); + Object.defineProperty(labelElement, 'clientWidth', { value: 50 }); + Object.defineProperty(labelElement, 'scrollHeight', { value: 20 }); + Object.defineProperty(labelElement, 'clientHeight', { value: 20 }); + + (component as any).updateTooltip(); + + expect(component.tooltipContent).toBe(component.content); + }); + + it('should call updateTooltip and add class on mouseover', () => { + const updateTooltipSpy = spyOn(component, 'updateTooltip').and.callThrough(); + const addClassSpy = spyOn((component as any).renderer, 'addClass').and.callThrough(); + + component.onMouseOver(); + + expect(updateTooltipSpy).toHaveBeenCalled(); + expect(addClassSpy).toHaveBeenCalledWith(component.labelElement.nativeElement, 'hovered'); + }); + + it('should remove class on mouseout', () => { + const removeClassSpy = spyOn((component as any).renderer, 'removeClass').and.callThrough(); + + component.onMouseOut(); + + expect(removeClassSpy).toHaveBeenCalledWith(component.labelElement.nativeElement, 'hovered'); + }); + + it('should set tooltipContent to content if text overflows vertically', () => { + const labelElement = document.createElement('div'); + + component.labelElement = { nativeElement: labelElement } as any; + + component.content = + 'Content that is very long and will overflow horizontally or exceed 100 characters vertically'; + component.isVerticalOrientation = true; + (component as any).maxLabelLength = 100; + + Object.defineProperty(labelElement, 'scrollWidth', { value: 150 }); + Object.defineProperty(labelElement, 'clientWidth', { value: 100 }); + + (component as any).updateTooltip(); + + expect(component.tooltipContent).toBe(component.content); + }); + }); + + describe('Templates:', () => { + it('should apply `po-stepper-label-vertical` when isVerticalOrientation is true', () => { + component.isVerticalOrientation = true; + fixture.detectChanges(); + + const element = fixture.debugElement.query(By.css('div')); + + expect(element.nativeElement.classList.contains('po-stepper-label-vertical')).toBeTrue(); + }); + + it('should apply `po-stepper-label-active` when status is active', () => { + component.status = 'active'; + fixture.detectChanges(); + + const element = fixture.debugElement.query(By.css('div')); + + expect(element.nativeElement.classList.contains('po-stepper-label-active')).toBeTrue(); + }); + + it('should apply `po-stepper-label-active` when status is error', () => { + component.status = 'error'; + fixture.detectChanges(); + + const element = fixture.debugElement.query(By.css('div')); + + expect(element.nativeElement.classList.contains('po-stepper-label-active')).toBeTrue(); + }); + + it('should apply `po-stepper-label` when content is set', () => { + component.content = 'Step 1'; + fixture.detectChanges(); + + const element = fixture.debugElement.query(By.css('div')); + + expect(element.nativeElement.classList.contains('po-stepper-label')).toBeTrue(); + }); + + it('should apply `po-stepper-label-done` when status is done', () => { + component.status = 'done'; + fixture.detectChanges(); + + const element = fixture.debugElement.query(By.css('div')); + + expect(element.nativeElement.classList.contains('po-stepper-label-done')).toBeTrue(); + }); + + it('should add `po-link` class when status is not `disabled`.', () => { + component.status = 'active'; + fixture.detectChanges(); + + const element = fixture.debugElement.query(By.css('div')); + + expect(element.nativeElement.classList.contains('po-link')).toBeTrue(); + }); + + it('should not add `po-link` class when status is `disabled`.', () => { + component.status = 'disabled'; + fixture.detectChanges(); + + const element = fixture.debugElement.query(By.css('div')); + + expect(element.nativeElement.classList.contains('po-link')).toBeFalse(); + }); + }); }); diff --git a/projects/ui/src/lib/components/po-stepper/po-stepper-label/po-stepper-label.component.ts b/projects/ui/src/lib/components/po-stepper/po-stepper-label/po-stepper-label.component.ts index 7c1880f29..04448e83c 100644 --- a/projects/ui/src/lib/components/po-stepper/po-stepper-label/po-stepper-label.component.ts +++ b/projects/ui/src/lib/components/po-stepper/po-stepper-label/po-stepper-label.component.ts @@ -1,4 +1,15 @@ -import { Component, Input } from '@angular/core'; +import { + AfterViewInit, + ChangeDetectorRef, + Component, + ElementRef, + HostListener, + Input, + OnChanges, + Renderer2, + SimpleChanges, + ViewChild +} from '@angular/core'; /** * @docsPrivate @@ -11,7 +22,73 @@ import { Component, Input } from '@angular/core'; selector: 'po-stepper-label', templateUrl: './po-stepper-label.component.html' }) -export class PoStepperLabelComponent { +export class PoStepperLabelComponent implements AfterViewInit, OnChanges { // Conteúdo da label. @Input('p-content') content: string; + + // Informa o status da etapa. + @Input('p-status') status: string; + + // Informa se a orientação do stepper é vertical. + @Input('p-vertical-orientation') isVerticalOrientation: boolean; + + @ViewChild('labelElement') labelElement: ElementRef; + + displayedContent: string; + tooltipContent: string; + + private maxLabelLength: number = 100; + + constructor( + private renderer: Renderer2, + private changeDetectorRef: ChangeDetectorRef + ) {} + + ngAfterViewInit() { + this.updateLabel(); + this.changeDetectorRef.detectChanges(); + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes['content'] || changes['isVerticalOrientation']) { + this.updateLabel(); + this.changeDetectorRef.detectChanges(); + } + } + + @HostListener('mouseover') + onMouseOver() { + this.updateTooltip(); + this.renderer.addClass(this.labelElement.nativeElement, 'hovered'); + } + + @HostListener('mouseout') + onMouseOut() { + this.renderer.removeClass(this.labelElement.nativeElement, 'hovered'); + } + + private updateLabel(): void { + if (!this.labelElement) return; + + const originalContent = this.content; + let displayedContent = originalContent; + + if (this.isVerticalOrientation && originalContent.length > this.maxLabelLength) { + displayedContent = originalContent.substring(0, this.maxLabelLength) + '...'; + } + + this.displayedContent = displayedContent; + + this.updateTooltip(); + } + + private updateTooltip(): void { + if (this.labelElement) { + const element = this.labelElement.nativeElement; + const isTextOverflowing = element.scrollWidth > element.clientWidth; + const isTextTooLong = this.isVerticalOrientation && this.content.length > this.maxLabelLength; + + this.tooltipContent = isTextOverflowing || isTextTooLong ? this.content : null; + } + } } diff --git a/projects/ui/src/lib/components/po-stepper/po-stepper-step/po-stepper-step.component.html b/projects/ui/src/lib/components/po-stepper/po-stepper-step/po-stepper-step.component.html index 22429df24..8f94852ce 100644 --- a/projects/ui/src/lib/components/po-stepper/po-stepper-step/po-stepper-step.component.html +++ b/projects/ui/src/lib/components/po-stepper/po-stepper-step/po-stepper-step.component.html @@ -1,22 +1,38 @@ -
-
-
- - + diff --git a/projects/ui/src/lib/components/po-stepper/po-stepper-step/po-stepper-step.component.spec.ts b/projects/ui/src/lib/components/po-stepper/po-stepper-step/po-stepper-step.component.spec.ts index f01865194..e60701c8d 100644 --- a/projects/ui/src/lib/components/po-stepper/po-stepper-step/po-stepper-step.component.spec.ts +++ b/projects/ui/src/lib/components/po-stepper/po-stepper-step/po-stepper-step.component.spec.ts @@ -86,51 +86,50 @@ describe('PoStepperStepComponent:', () => { expectPropertiesValues(component, 'stepSize', invalidValues, poStepperStepSizeDefault); }); - it('halfStepSize: should return half of step size.', () => { - const expectedValue = 24; - - component.stepSize = 48; + it('p-step-icons: should update property with valid values to `true`.', () => { + const booleanValidTrueValues = [true, 'true', 1, '']; - expect(component.halfStepSize).toBe(expectedValue); + expectPropertiesValues(component, 'stepIcons', booleanValidTrueValues, true); }); - it('isVerticalOrientation: should return `true` if `orientation` is `vertical`.', () => { - component.orientation = PoStepperOrientation.Vertical; + it('p-step-icons: should update property with invalid values to `false`.', () => { + const booleanInvalidValues = [undefined, null, 2, 'string']; - expect(component.isVerticalOrientation).toBeTruthy(); + expectPropertiesValues(component, 'stepIcons', booleanInvalidValues, false); }); - it('isVerticalOrientation: should return `false` if `orientation` is `horizontal`.', () => { - component.orientation = PoStepperOrientation.Horizontal; + it('minHeightCircle: should return 32 if `stepSize` is less than or equal to 24 and orientation is vertical.', () => { + component.stepSize = 24; + component.isVerticalOrientation = true; - expect(component.isVerticalOrientation).toBeFalsy(); + expect(component.minHeightCircle).toBe(32); }); - it('marginHorizontalBar: should return half of step size if `orientation` is `horizontal`.', () => { - const defaultHalftStepSize = 12; + it('minHeightCircle: should return `stepSize + 8` if `stepSize` is greater than 24 and orientation is vertical.', () => { + component.stepSize = 40; + component.isVerticalOrientation = true; - component.stepSize = 24; - component.orientation = PoStepperOrientation.Horizontal; - - expect(component.marginHorizontalBar).toBe(defaultHalftStepSize); + expect(component.minHeightCircle).toBe(48); }); - it('marginHorizontalBar: should return undefined if `orientation` is `vertical`.', () => { - component.orientation = PoStepperOrientation.Vertical; + it('minHeightCircle: should return 32 if `stepSize` is 24 and the orientation is horizontal.', () => { + component.stepSize = 24; + component.isVerticalOrientation = false; - expect(component.marginHorizontalBar).toBeUndefined(); + expect(component.minHeightCircle).toBe(32); }); - it('p-step-icons: should update property with valid values to `true`.', () => { - const booleanValidTrueValues = [true, 'true', 1, '']; + it('minWidthCircle: should return 32 if `stepSize` is 24 and the orientation is vertical.', () => { + component.stepSize = 24; + component.isVerticalOrientation = true; - expectPropertiesValues(component, 'stepIcons', booleanValidTrueValues, true); + expect(component.minWidthCircle).toBe(32); }); - it('p-step-icons: should update property with invalid values to `false`.', () => { - const booleanInvalidValues = [undefined, null, 2, 'string']; + it('minWidthCircle: should return null if orientation is horizontal.', () => { + component.isVerticalOrientation = false; - expectPropertiesValues(component, 'stepIcons', booleanInvalidValues, false); + expect(component.minWidthCircle).toBeNull(); }); }); @@ -212,79 +211,102 @@ describe('PoStepperStepComponent:', () => { expect(component.enter.emit).not.toHaveBeenCalled(); }); - }); - describe('Templates:', () => { - const elementByClass = (className: string) => nativeElement.querySelector(`.${className}`); + it('setDefaultStepSize: should increase step size by 8px if status is `Active` and step size is default.', () => { + (component as any)._stepSize = 24; + component.status = PoStepperStatus.Active; - it(`should find 'po-stepper-step-container' and style.width is stepSize value if 'isVerticalOrientation' is 'true'.`, () => { - component.orientation = PoStepperOrientation.Vertical; - component.stepSize = 60; + component.setDefaultStepSize(); - fixture.detectChanges(); + expect((component as any)._stepSize).toBe(32); + }); - const stepperContainer = elementByClass('po-stepper-step-container'); + it('setDefaultStepSize: should increase step size by 8px if status is `Error` and stepSizeOriginal is default.', () => { + component.stepSizeOriginal = 24; + component.status = PoStepperStatus.Error; - expect(stepperContainer).toBeTruthy(); - expect(stepperContainer.style.width).toBe('60px'); + component.setDefaultStepSize(); + + expect((component as any)._stepSize).toBe(32); }); - it('should find `po-stepper-step-container` and it not contains width if `orientation` is `horizontal`.', () => { - component.orientation = PoStepperOrientation.Horizontal; - component.stepSize = 60; + it('setDefaultStepSize: should keep the original step size if step size is not default.', () => { + component.stepSizeOriginal = 64; - fixture.detectChanges(); + component.setDefaultStepSize(); - const stepperContainer = elementByClass('po-stepper-step-container'); + expect((component as any)._stepSize).toBe(64); + }); - expect(stepperContainer).toBeTruthy(); - expect(stepperContainer.style.width).toBe(''); + it('should update stepSizeOriginal and call setDefaultStepSize if stepSize changes and stepSizeOriginal is undefined', () => { + (component as any)._stepSize = 40; + component.stepSizeOriginal = undefined; + + spyOn(component, 'setDefaultStepSize'); + + const changes = { + stepSize: { currentValue: 40, previousValue: 30, firstChange: false, isFirstChange: () => false } + }; + + component.ngOnChanges(changes); + + expect(component.stepSizeOriginal).toBe((component as any)._stepSize); + expect(component.setDefaultStepSize).toHaveBeenCalled(); }); - it(`should create class 'po-stepper-step-bar-left' and 'po-stepper-step-bar-right' if 'p-orientation' is 'horizontal'.`, () => { - component.orientation = PoStepperOrientation.Horizontal; - component.label = 'Step 1'; + it('should call setDefaultStepSize if status changes', () => { + component.stepSizeOriginal = 30; - fixture.detectChanges(); + spyOn(component, 'setDefaultStepSize'); - const stepperBarLeft = elementByClass('po-stepper-step-bar-left'); - const stepperBarRight = elementByClass('po-stepper-step-bar-right'); + const changes = { + status: { + currentValue: PoStepperStatus.Active, + previousValue: PoStepperStatus.Default, + firstChange: false, + isFirstChange: () => false + } + }; - expect(stepperBarLeft).toBeTruthy(); - expect(stepperBarRight).toBeTruthy(); - expect(elementByClass('po-stepper-bar-top')).toBeFalsy(); - expect(elementByClass('po-stepper-bar-bottom')).toBeFalsy(); + component.ngOnChanges(changes); + + expect(component.setDefaultStepSize).toHaveBeenCalled(); }); - it(`should create class 'po-stepper-step-bar-top' and 'po-stepper-step-bar-bottom' if 'p-orientation' is 'vertical'.`, () => { - component.orientation = PoStepperOrientation.Vertical; - component.label = 'Step 1'; + it('should call setDefaultStepSize if both stepSize and status change', () => { + component.stepSizeOriginal = 30; - fixture.detectChanges(); + spyOn(component, 'setDefaultStepSize'); + + const changes = { + stepSize: { currentValue: 40, previousValue: 30, firstChange: false, isFirstChange: () => false }, + status: { + currentValue: PoStepperStatus.Active, + previousValue: PoStepperStatus.Default, + firstChange: false, + isFirstChange: () => false + } + }; - const stepperBarLeft = elementByClass('po-stepper-step-bar-left'); - const stepperBarRight = elementByClass('po-stepper-step-bar-right'); + component.ngOnChanges(changes); - expect(stepperBarLeft).toBeFalsy(); - expect(stepperBarRight).toBeFalsy(); - expect(elementByClass('po-stepper-step-bar-top')).toBeTruthy(); - expect(elementByClass('po-stepper-step-bar-bottom')).toBeTruthy(); + expect(component.setDefaultStepSize).toHaveBeenCalled(); }); + }); - it(`should add margin-left and margin-right in 'step-bar-left' and 'step-bar-right' with 'marginHorizontalBar'.`, () => { - const marginHorizontalBar = 12; - component.orientation = PoStepperOrientation.Horizontal; - component.label = 'Step 1'; + describe('Templates:', () => { + const elementByClass = (className: string) => nativeElement.querySelector(`.${className}`); - spyOnProperty(component, 'marginHorizontalBar').and.returnValue(marginHorizontalBar); + it('should find `po-stepper-step-container` and it not contains width if `orientation` is `horizontal`.', () => { + component.orientation = PoStepperOrientation.Horizontal; + component.stepSize = 60; fixture.detectChanges(); - const stepperBarLeft = elementByClass('po-stepper-step-bar-left'); - const stepperBarRight = elementByClass('po-stepper-step-bar-right'); + const stepperContainer = elementByClass('po-stepper-step-container'); - expect(stepperBarLeft.style.marginRight).toBe(`${marginHorizontalBar}px`); - expect(stepperBarRight.style.marginLeft).toBe(`${marginHorizontalBar}px`); + expect(stepperContainer).toBeTruthy(); + expect(stepperContainer.style.width).toBe(''); }); it('should find `po-stepper-circle` and it contains height and width with 24px if stepSize is greater than 64.', () => { @@ -352,26 +374,22 @@ describe('PoStepperStepComponent:', () => { expect(elementByClass('po-stepper-step-default')).toBeTruthy(); }); - it(`should create class 'po-stepper-step-dashed-border' if 'nextStatus' is disabled and position is not vertical`, () => { - component.orientation = PoStepperOrientation.Horizontal; - component.nextStatus = 'disabled'; - + it('should change `tabindex` to `-1` if component is disabled', () => { + component.status = PoStepperStatus.Disabled; fixture.detectChanges(); - const stepperDashedBorder = elementByClass('po-stepper-step-dashed-border'); + const poStepperStepElement = nativeElement.querySelector('.po-stepper-step[tabindex="-1"]'); - expect(stepperDashedBorder).toBeTruthy(); + expect(poStepperStepElement).toBeTruthy(); }); - it(`should create class 'po-stepper-step-dashed-border-vertical' if 'nextStatus' is disabled and position is vertical`, () => { - component.orientation = PoStepperOrientation.Vertical; - component.nextStatus = 'disabled'; - + it('should change `tabindex` to `0` if component isn’t disabled', () => { + component.status = PoStepperStatus.Active; fixture.detectChanges(); - const stepperDashedBorderVertical = elementByClass('po-stepper-step-dashed-border-vertical'); + const poStepperStepElement = nativeElement.querySelector('.po-stepper-step[tabindex="0"]'); - expect(stepperDashedBorderVertical).toBeTruthy(); + expect(poStepperStepElement).toBeTruthy(); }); }); }); diff --git a/projects/ui/src/lib/components/po-stepper/po-stepper-step/po-stepper-step.component.ts b/projects/ui/src/lib/components/po-stepper/po-stepper-step/po-stepper-step.component.ts index 20c7a2ad1..d7f3737a5 100644 --- a/projects/ui/src/lib/components/po-stepper/po-stepper-step/po-stepper-step.component.ts +++ b/projects/ui/src/lib/components/po-stepper/po-stepper-step/po-stepper-step.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges, TemplateRef } from '@angular/core'; import { getShortBrowserLanguage, convertToBoolean, isTypeof } from './../../../utils/util'; import { poLocaleDefault } from './../../../services/po-language/po-language.constant'; @@ -24,34 +24,15 @@ const poStepLiteralsDefault = { selector: 'po-stepper-step', templateUrl: 'po-stepper-step.component.html' }) -export class PoStepperStepComponent { +export class PoStepperStepComponent implements OnChanges { // Conteúdo que será repassado para o componente `p-circle-content` através da propriedade `p-content`. @Input('p-circle-content') circleContent: any; - // Define a orientação de exibição. - @Input('p-orientation') orientation: PoStepperOrientation; - - // Informa o status da proxima etapa. + // Informa o status da próxima etapa. @Input('p-next-status') nextStatus; - // Evento que será emitido quando o status do *step* estiver ativo (`PoStepperStatus.Active`). - @Output('p-activated') activated = new EventEmitter(); - - // Evento que será emitido ao clicar no *step*. - @Output('p-click') click = new EventEmitter(); - - // Evento que será emitido ao focar no *step* e pressionar a tecla *enter*. - @Output('p-enter') enter = new EventEmitter(); - - readonly literals = { - ...poStepLiteralsDefault[poLocaleDefault], - ...poStepLiteralsDefault[getShortBrowserLanguage()] - }; - - private _label: string; - private _status: PoStepperStatus; - private _stepIcons?: boolean = false; - private _stepSize: number = poStepperStepSizeDefault; + // Define a orientação de exibição. + @Input('p-orientation') orientation: PoStepperOrientation; // Label do *step*. @Input('p-label') set label(value: string) { @@ -94,16 +75,79 @@ export class PoStepperStepComponent { return this._stepSize; } - get halfStepSize(): number { - return this.stepSize / 2; + @Input('p-icon-default') set iconDefault(value: string | TemplateRef) { + this._iconDefault = value; + } + + get iconDefault(): string | TemplateRef { + return this._iconDefault; + } + + @Input('p-step-icon-done') set iconDone(value: string | TemplateRef) { + this._iconDone = value; + } + + get iconDone(): string | TemplateRef { + return this._iconDone; + } + + @Input('p-step-icon-active') set iconActive(value: string | TemplateRef) { + this._iconActive = value; + } + + get iconActive(): string | TemplateRef { + return this._iconActive; + } + + // Informa se a orientação do stepper é vertical. + @Input('p-vertical-orientation') isVerticalOrientation: boolean; + + // Evento que será emitido quando o status do *step* estiver ativo (`PoStepperStatus.Active`). + @Output('p-activated') activated = new EventEmitter(); + + // Evento que será emitido ao clicar no *step*. + @Output('p-click') click = new EventEmitter(); + + // Evento que será emitido ao focar no *step* e pressionar a tecla *enter*. + @Output('p-enter') enter = new EventEmitter(); + + readonly literals = { + ...poStepLiteralsDefault[poLocaleDefault], + ...poStepLiteralsDefault[getShortBrowserLanguage()] + }; + + stepSizeOriginal: number; + private _label: string; + private _status: PoStepperStatus; + private _stepIcons?: boolean = false; + private _stepSize: number = poStepperStepSizeDefault; + private _iconDefault?: string | TemplateRef; + private _iconDone?: string | TemplateRef; + private _iconActive?: string | TemplateRef; + + get minHeightCircle(): number | null { + if (this.stepSize === 24) { + return 32; + } + + return this.isVerticalOrientation ? Math.max(this.stepSize, 24) + 8 : null; } - get isVerticalOrientation(): boolean { - return this.orientation === PoStepperOrientation.Vertical; + get minWidthCircle(): number | null { + if (this.isVerticalOrientation && this.stepSize === 24) { + return 32; + } + return null; } - get marginHorizontalBar(): number { - return this.isVerticalOrientation ? undefined : this.halfStepSize; + ngOnChanges(changes: SimpleChanges): void { + if (this.stepSizeOriginal === undefined || changes['stepSize']) { + this.stepSizeOriginal = this._stepSize; + } + + if (changes['status'] || changes['stepSize']) { + this.setDefaultStepSize(); + } } getStatusClass(status: string): string { @@ -132,4 +176,14 @@ export class PoStepperStepComponent { this.enter.emit(); } } + + setDefaultStepSize(): void { + if (this._stepSize === poStepperStepSizeDefault && this._status === PoStepperStatus.Active) { + this._stepSize = poStepperStepSizeDefault + 8; + } else if (this.stepSizeOriginal === poStepperStepSizeDefault && this._status === PoStepperStatus.Error) { + this._stepSize = poStepperStepSizeDefault + 8; + } else { + this._stepSize = this.stepSizeOriginal; + } + } } diff --git a/projects/ui/src/lib/components/po-stepper/po-stepper.component.html b/projects/ui/src/lib/components/po-stepper/po-stepper.component.html index dd041d581..8da86c195 100644 --- a/projects/ui/src/lib/components/po-stepper/po-stepper.component.html +++ b/projects/ui/src/lib/components/po-stepper/po-stepper.component.html @@ -1,20 +1,39 @@ -
-
- - +
+
+ +
+ + + +
+
+
diff --git a/projects/ui/src/lib/components/po-stepper/po-stepper.component.spec.ts b/projects/ui/src/lib/components/po-stepper/po-stepper.component.spec.ts index 618ffc4f1..99511e355 100644 --- a/projects/ui/src/lib/components/po-stepper/po-stepper.component.spec.ts +++ b/projects/ui/src/lib/components/po-stepper/po-stepper.component.spec.ts @@ -86,7 +86,7 @@ describe('PoStepperComponent:', () => { component.ngAfterContentInit(); - expect(spyOncontrolStepsStatus).toHaveBeenCalledWith(0, component.poSteps.first); + expect(spyOncontrolStepsStatus).toHaveBeenCalledWith(component.poSteps.first); }); it('active: shouldn`t call `getPoSteps` and `changeStep` if `usePoSteps` is false', () => { @@ -116,6 +116,32 @@ describe('PoStepperComponent:', () => { expect(spyOnChangeStep).toHaveBeenCalledWith(position, poSteps[position]); }); + it('active: should return if index is less than 0', () => { + spyOnProperty(component, 'usePoSteps').and.returnValue(true); + + const poSteps = [{}, {}]; + const spyOnGetPoSteps = spyOn(component, 'getPoSteps').and.returnValue(poSteps); + const spyOnChangeStep = spyOn(component, 'changeStep'); + + component.active(-1); + + expect(spyOnGetPoSteps).toHaveBeenCalled(); + expect(spyOnChangeStep).not.toHaveBeenCalled(); + }); + + it('active: should return if index is greater than or equal to steps length', () => { + spyOnProperty(component, 'usePoSteps').and.returnValue(true); + + const poSteps = [{}, {}]; + const spyOnGetPoSteps = spyOn(component, 'getPoSteps').and.returnValue(poSteps); + const spyOnChangeStep = spyOn(component, 'changeStep'); + + component.active(3); + + expect(spyOnGetPoSteps).toHaveBeenCalled(); + expect(spyOnChangeStep).not.toHaveBeenCalled(); + }); + it('first: shouldn`t call `changeStep` if `usePoSteps` is false', () => { spyOnProperty(component, 'usePoSteps').and.returnValue(false); @@ -165,6 +191,18 @@ describe('PoStepperComponent:', () => { expect(spyOnChangeStep).toHaveBeenCalledWith(nextStepIndex, nextStep); }); + it('next: should return if next index is greater than or equal to steps length', () => { + spyOnProperty(component, 'usePoSteps').and.returnValue(true); + + const steps = [{}, {}]; + spyOn(component, 'getStepsAndIndex').and.returnValue({ steps, stepIndex: 1 }); + const spyOnChangeStep = spyOn(component, 'changeStep'); + + component.next(); + + expect(spyOnChangeStep).not.toHaveBeenCalled(); + }); + it('previous: shouldn`t call `changeStep` if `usePoSteps` is false', () => { spyOnProperty(component, 'usePoSteps').and.returnValue(false); @@ -194,10 +232,22 @@ describe('PoStepperComponent:', () => { expect(spyOnChangeStep).toHaveBeenCalledWith(previousStepIndex, previousStep); }); + it('previous: should return if previous index is less than 0', () => { + spyOnProperty(component, 'usePoSteps').and.returnValue(true); + + const steps = [{}, {}]; + spyOn(component, 'getStepsAndIndex').and.returnValue({ steps, stepIndex: 0 }); + const spyOnChangeStep = spyOn(component, 'changeStep'); + + component.previous(); + + expect(spyOnChangeStep).not.toHaveBeenCalled(); + }); + it(`changeStep: should call 'controlStepsStatus' and 'onChangeStep.emit' with 'step' if usePoSteps and allowNextStep are true and 'step' is different of 'currentActiveStep'`, () => { const poStepMock = { id: 'A1BC' }; - const poStepCurrentMock = { id: 2 }; + const poStepCurrentMock = { id: 'XYZ' }; const stepIndex = 1; component['currentActiveStep'] = poStepCurrentMock; @@ -210,7 +260,7 @@ describe('PoStepperComponent:', () => { component.changeStep(stepIndex, poStepMock); - expect(spyOncontrolStepsStatus).toHaveBeenCalledWith(stepIndex, poStepMock); + expect(spyOncontrolStepsStatus).toHaveBeenCalledWith(poStepMock); expect(spyOnChangeStep).toHaveBeenCalledWith(poStepMock); }); @@ -229,7 +279,7 @@ describe('PoStepperComponent:', () => { component.changeStep(stepIndex, poStepMock); - expect(spyOncontrolStepsStatus).toHaveBeenCalledWith(stepIndex, poStepMock); + expect(spyOncontrolStepsStatus).toHaveBeenCalledWith(poStepMock); expect(spyOnChangeStep).toHaveBeenCalledWith(poStepMock); }); @@ -383,43 +433,80 @@ describe('PoStepperComponent:', () => { }); }); - it('allowNextStep: should return true if `sequential`, `usePoSteps`, `isBeforeStep` are true', (done: DoneFn) => { + it(`allowNextStep: should return true if 'sequential', 'usePoSteps' and 'canActiveNextStep' are true and + 'isBeforeStep' is false`, (done: DoneFn) => { component.sequential = true; const nextStepIndex = 1; spyOnProperty(component, 'usePoSteps').and.returnValue(true); - const spyOnIsBeforeStep = spyOn(component, 'isBeforeStep').and.returnValue(true); + const spyOnCanActiveNextStep = spyOn(component, 'canActiveNextStep').and.returnValue(of(true)); + spyOn(component, 'isBeforeStep').and.returnValue(false); component['allowNextStep'](nextStepIndex).subscribe(value => { expect(value).toBe(true); done(); }); - expect(spyOnIsBeforeStep).toHaveBeenCalledWith(nextStepIndex); + expect(spyOnCanActiveNextStep).toHaveBeenCalledWith(component['currentActiveStep'], nextStepIndex); }); - it(`allowNextStep: should return true if 'sequential', 'usePoSteps' and 'canActiveNextStep' are true and - 'isBeforeStep' is false`, (done: DoneFn) => { + it('allowNextStep: should return false if `isBeforeStep` and `canActiveNextStep` are false', (done: DoneFn) => { component.sequential = true; const nextStepIndex = 1; spyOnProperty(component, 'usePoSteps').and.returnValue(true); - const spyOnCanActiveNextStep = spyOn(component, 'canActiveNextStep').and.returnValue(of(true)); + spyOn(component, 'canActiveNextStep').and.returnValue(of(false)); spyOn(component, 'isBeforeStep').and.returnValue(false); component['allowNextStep'](nextStepIndex).subscribe(value => { - expect(value).toBe(true); + expect(value).toBe(false); done(); }); - expect(spyOnCanActiveNextStep).toHaveBeenCalledWith(component['currentActiveStep']); }); - it('allowNextStep: should return false if `isBeforeStep` and `canActiveNextStep` are false', (done: DoneFn) => { + it('allowNextStep: should continue if there is no step with status Default before the next step index', () => { + const nextStepIndex = 2; + + spyOn(component as any, 'hasDefaultBeforeDone').and.returnValue(false); + + spyOn(component as any, 'checkAllowNextStep').and.returnValue(of(true)); + + (component as any).allowNextStep(nextStepIndex).subscribe(result => { + expect(result).toBe(true); + }); + }); + + it('allowNextStep: should return of(true) when checkAllowNextStep returns a boolean', () => { + const nextStepIndex = 2; + + spyOn(component as any, 'hasDefaultBeforeDone').and.returnValue(false); + + spyOn(component as any, 'checkAllowNextStep').and.returnValue(true); + + (component as any).allowNextStep(nextStepIndex).subscribe(result => { + expect(result).toBe(true); + }); + }); + + it('allowNextStep: should return the observable when checkAllowNextStep returns an observable', () => { + const nextStepIndex = 2; + + spyOn(component as any, 'hasDefaultBeforeDone').and.returnValue(false); + + const observableMock = of(true); + spyOn(component as any, 'checkAllowNextStep').and.returnValue(observableMock); + + (component as any).allowNextStep(nextStepIndex).subscribe(result => { + expect(result).toBe(true); + }); + }); + + it('allowNextStep: should return of(false) if `sequential` is true, `hasStepWithCanActiveNextStep` is true, and `hasDefaultBeforeDone` is true', (done: DoneFn) => { component.sequential = true; const nextStepIndex = 1; - spyOnProperty(component, 'usePoSteps').and.returnValue(true); - spyOn(component, 'canActiveNextStep').and.returnValue(of(false)); - spyOn(component, 'isBeforeStep').and.returnValue(false); + spyOn(component as any, 'hasStepWithCanActiveNextStep').and.returnValue(true); + + spyOn(component as any, 'hasDefaultBeforeDone').and.returnValue(true); component['allowNextStep'](nextStepIndex).subscribe(value => { expect(value).toBe(false); @@ -427,6 +514,23 @@ describe('PoStepperComponent:', () => { }); }); + it('allowNextStep: should return of(true) if `sequential` is true, `hasStepWithCanActiveNextStep` is true, but `hasDefaultBeforeDone` is false', (done: DoneFn) => { + component.sequential = true; + const nextStepIndex = 1; + + spyOn(component as any, 'hasStepWithCanActiveNextStep').and.returnValue(true); + + spyOn(component as any, 'hasDefaultBeforeDone').and.returnValue(false); + + spyOn(component as any, 'checkAllowNextStep').and.returnValue(true); + + component['allowNextStep'](nextStepIndex).subscribe(value => { + expect(value).toBe(true); + done(); + }); + expect(component['checkAllowNextStep']).toHaveBeenCalledWith(nextStepIndex); + }); + it('canActiveNextStep: should return true if `currentActiveStep.canActiveNextStep` function return true', (done: DoneFn) => { const currentActiveStep = { canActiveNextStep: currentStep => true }; @@ -499,6 +603,40 @@ describe('PoStepperComponent:', () => { }); }); + it(`canActiveNextStep: should return true and update the status to 'Done' if 'isBefore' is true and 'isCurrentStep' is false`, (done: DoneFn) => { + const currentActiveStep = { + status: PoStepperStatus.Default, + canActiveNextStep: currentStep => of(true) + }; + const nextStepIndex = 1; + + spyOn(component as any, 'isBeforeStep').and.returnValue(true); + spyOn(component as any, 'isCurrentStep').and.returnValue(false); + + component['canActiveNextStep'](currentActiveStep, nextStepIndex).subscribe(result => { + expect(result).toBe(true); + expect(currentActiveStep.status).toBe(PoStepperStatus.Done); + done(); + }); + }); + + it(`canActiveNextStep: should return true and update the status to 'Default' if 'isCanActiveNextStep' is false`, (done: DoneFn) => { + const currentActiveStep = { + status: PoStepperStatus.Default, + canActiveNextStep: currentStep => of(false) + }; + const nextStepIndex = 1; + + spyOn(component as any, 'isBeforeStep').and.returnValue(true); + spyOn(component as any, 'isCurrentStep').and.returnValue(false); + + component['canActiveNextStep'](currentActiveStep, nextStepIndex).subscribe(result => { + expect(result).toBe(true); + expect(currentActiveStep.status).toBe(PoStepperStatus.Default); + done(); + }); + }); + it(`controlStepsStatus: shouldn't call 'isBeforeStep', 'setStepAsActive', 'setNextStepAsDefault' and 'changeDetector.detectChanges' if 'usePoSteps' is false`, () => { spyOnProperty(component, 'usePoSteps').and.returnValue(false); @@ -508,7 +646,7 @@ describe('PoStepperComponent:', () => { const spyOnSetNextStepAsDefault = spyOn(component, 'setNextStepAsDefault'); const spyOnDetectChanges = spyOn(component['changeDetector'], 'detectChanges'); - component['controlStepsStatus'](0, undefined); + component['controlStepsStatus'](undefined); expect(spyOnIsBeforeStep).not.toHaveBeenCalled(); expect(spyOnSetStepAsActive).not.toHaveBeenCalled(); @@ -516,38 +654,104 @@ describe('PoStepperComponent:', () => { expect(spyOnDetectChanges).not.toHaveBeenCalled(); }); - it(`controlStepsStatus: should call 'isBeforeStep', 'setStepAsActive', 'setNextStepAsDefault' and - 'changeDetector.detectChanges' if 'usePoSteps' is true`, () => { - const stepIndex = 2; - const step = poSteps[0]; + it(`controlStepsStatus: should set previous step status to 'Done' when previousActiveStepIndex is different from currentStepIndex`, () => { + spyOnProperty(component, 'usePoSteps').and.returnValue(true); + + const poStepsMock = new QueryList(); + const poStepList = [ + { id: '1', label: 'Step 1', status: PoStepperStatus.Active }, + { id: '2', label: 'Step 2', status: PoStepperStatus.Active }, + { id: '3', label: 'Step 3', status: PoStepperStatus.Disabled } + ]; + poStepsMock['_results'] = poStepList; + Object.defineProperty(poStepsMock, 'length', { value: poStepList.length }); + component.poSteps = poStepsMock; + component['previousActiveStepIndex'] = 0; + + const stepIndex = 1; + const step = poStepsMock['_results'][stepIndex]; + + component['controlStepsStatus'](step); + + expect(poStepsMock['_results'][0].status).toBe(PoStepperStatus.Done); + }); + + it(`controlStepsStatus: shouldn't change previous step status if previousActiveStepIndex is the same as currentStepIndex`, () => { spyOnProperty(component, 'usePoSteps').and.returnValue(true); - const spyOnIsBeforeStep = spyOn(component, 'isBeforeStep'); - const spyOnSetStepAsActive = spyOn(component, 'setStepAsActive'); - const spyOnSetNextStepAsDefault = spyOn(component, 'setNextStepAsDefault'); - const spyOnDetectChanges = spyOn(component['changeDetector'], 'detectChanges'); + const poStepsMock = new QueryList(); + const poStepList = [ + { id: '1', label: 'Step 1', status: PoStepperStatus.Done }, + { id: '2', label: 'Step 2', status: PoStepperStatus.Active }, + { id: '3', label: 'Step 3', status: PoStepperStatus.Disabled } + ]; + poStepsMock['_results'] = poStepList; + Object.defineProperty(poStepsMock, 'length', { value: poStepList.length }); - component['controlStepsStatus'](stepIndex, step); + component.poSteps = poStepsMock; + component['previousActiveStepIndex'] = 1; - expect(spyOnIsBeforeStep).toHaveBeenCalledWith(stepIndex); - expect(spyOnSetStepAsActive).toHaveBeenCalledWith(step); - expect(spyOnSetNextStepAsDefault).toHaveBeenCalledWith(step); - expect(spyOnDetectChanges).toHaveBeenCalled(); + const stepIndex = 1; + const step = poStepsMock['_results'][stepIndex]; + + component['controlStepsStatus'](step); + + expect(poStepsMock['_results'][1].status).toBe(PoStepperStatus.Active); }); - it(`controlStepsStatus: should call 'setFinalSteppersAsDisabled' if 'usePoSteps' and 'isBeforeStep' are true`, () => { - const stepIndex = 2; - const step = poSteps[0]; + it(`controlStepsStatus: should call 'setFinalSteppersAsDisabled' if 'usePoSteps' is true, + 'isBeforeStep' returns true, and the next step is disabled`, () => { + const currentStep = { + id: '1', + label: 'Step 1', + status: PoStepperStatus.Active, + canActiveNextStep: () => of(true) + } as unknown as PoStepComponent; + + const nextStep = { + id: '2', + label: 'Step 2', + status: PoStepperStatus.Disabled + } as unknown as PoStepComponent; + + component.poSteps = new QueryList(); + component.poSteps['_results'] = [currentStep, nextStep]; + Object.defineProperty(component.poSteps, 'length', { value: 2 }); + component['previousActiveStepIndex'] = 0; // Passo atual é o primeiro spyOnProperty(component, 'usePoSteps').and.returnValue(true); spyOn(component, 'isBeforeStep').and.returnValue(true); const spyOnSetFinalSteppersAsDisabled = spyOn(component, 'setFinalSteppersAsDisabled'); - component['controlStepsStatus'](stepIndex, step); + component['controlStepsStatus'](currentStep); + + expect(spyOnSetFinalSteppersAsDisabled).toHaveBeenCalledWith(0); + }); + + it('calculateDividerPosition: should return the stepSize if it is between 24 and 64', () => { + component.stepSize = 32; + + const result = component['calculateDividerPosition'](); + + expect(result).toBe(32); + }); + + it('calculateDividerPosition: should return 24 if stepSize is less than 24', () => { + component.stepSize = 20; - expect(spyOnSetFinalSteppersAsDisabled).toHaveBeenCalledWith(stepIndex); + const result = component['calculateDividerPosition'](); + + expect(result).toBe(24); + }); + + it('calculateDividerPosition: should return 24 if stepSize is greater than 64', () => { + component.stepSize = 70; + + const result = component['calculateDividerPosition'](); + + expect(result).toBe(24); }); it('getStepsAndIndex: should return the `steps` and the index of the step parameter', () => { @@ -582,6 +786,35 @@ describe('PoStepperComponent:', () => { expect(result).toEqual(poSteps); }); + it('handleNextStep: should call `setFinalSteppersAsDisabled` if `isBeforeStep` returns true', () => { + const currentStepIndex = 0; + const steps = [ + { status: PoStepperStatus.Active } as PoStepComponent, + { status: PoStepperStatus.Disabled } as PoStepComponent + ]; + + spyOn(component, 'isBeforeStep').and.returnValue(true); + const spyOnSetFinalSteppersAsDisabled = spyOn(component, 'setFinalSteppersAsDisabled'); + + component['handleNextStep'](steps, currentStepIndex); + + expect(spyOnSetFinalSteppersAsDisabled).toHaveBeenCalledWith(currentStepIndex); + }); + + it('hasDefaultBeforeDone: should return true if there is a step with status Default before the next step index', () => { + const nextStepIndex = 2; + + spyOn(component as any, 'getPoSteps').and.returnValue([ + { status: PoStepperStatus.Active } as PoStepComponent, + { status: PoStepperStatus.Default } as PoStepComponent, + { status: PoStepperStatus.Done } as PoStepComponent + ]); + + const result = (component as any).hasDefaultBeforeDone(nextStepIndex); + + expect(result).toBe(true); + }); + it(`isBeforeStep: should return true if 'currentActiveStep' is defined and 'currentActiveStepIndex' is greater than 'stepIndex'`, () => { const stepIndex = 2; @@ -627,6 +860,20 @@ describe('PoStepperComponent:', () => { expect(result).toBe(false); }); + it('isCurrentStep: should return true when the currentActiveStep id matches the stepIndex', () => { + spyOn(component as any, 'getPoSteps').and.returnValue([ + { id: 'step-1', status: PoStepperStatus.Active } as PoStepComponent, + { id: 'step-2', status: PoStepperStatus.Default } as PoStepComponent, + { id: 'step-3', status: PoStepperStatus.Disabled } as PoStepComponent + ]); + + component['currentActiveStep'] = { id: 'step-1' } as PoStepComponent; + const stepIndex = 0; + + const result = component['isCurrentStep'](stepIndex); + expect(result).toBe(true); + }); + it('setFinalSteppersAsDisabled: should set steppers as `disabled` if stepper index is greather than stepper active + 2.', () => { const stepIndex = 0; const poStepsMock = new QueryList(); @@ -854,8 +1101,7 @@ describe('PoStepperComponent:', () => { expect(steps[4].classList.contains('po-stepper-step-disabled')).toBeTruthy(); })); - it(`should initialize step 4 as active, step 5 as default and others as disabled, - and should set step clicked as 'active', the previous as 'done', the next as 'default'and the last as 'disabled'.`, fakeAsync(() => { + it(`should initialize step 4 as active, step 5 as default and others as disabled`, fakeAsync(() => { const poStepsMock = new QueryList(); const poStepList = [ { id: '1', label: 'Step 1', status: PoStepperStatus.Done }, @@ -867,9 +1113,6 @@ describe('PoStepperComponent:', () => { poStepsMock['_results'] = poStepList; Object.defineProperty(poStepsMock, 'length', { value: 5 }); - const eventClick = document.createEvent('MouseEvents'); - eventClick.initEvent('click', true, true); - component.poSteps = poStepsMock; component['currentActiveStep'] = poStepsMock['_results'][0]; @@ -885,17 +1128,6 @@ describe('PoStepperComponent:', () => { expect(steps[2].classList.contains('po-stepper-step-default')).toBeTruthy(); expect(steps[3].classList.contains('po-stepper-step-default')).toBeTruthy(); expect(steps[4].classList.contains('po-stepper-step-default')).toBeTruthy(); - - steps[1].dispatchEvent(eventClick); - - fixture.detectChanges(); - flush(); - - expect(steps[0].classList.contains('po-stepper-step-default')).toBeTruthy(); - expect(steps[1].classList.contains('po-stepper-step-default')).toBeTruthy(); - expect(steps[2].classList.contains('po-stepper-step-default')).toBeTruthy(); - expect(steps[3].classList.contains('po-stepper-step-disabled')).toBeTruthy(); - expect(steps[4].classList.contains('po-stepper-step-disabled')).toBeTruthy(); })); }); }); diff --git a/projects/ui/src/lib/components/po-stepper/po-stepper.component.ts b/projects/ui/src/lib/components/po-stepper/po-stepper.component.ts index 04c6a4ba6..ae877fb38 100644 --- a/projects/ui/src/lib/components/po-stepper/po-stepper.component.ts +++ b/projects/ui/src/lib/components/po-stepper/po-stepper.component.ts @@ -1,7 +1,7 @@ import { AfterContentInit, ChangeDetectorRef, Component, ContentChildren, QueryList } from '@angular/core'; import { Observable, of, throwError } from 'rxjs'; -import { take, tap, catchError } from 'rxjs/operators'; +import { take, tap, catchError, map, mergeMap } from 'rxjs/operators'; import { PoStepperStatus } from './enums/po-stepper-status.enum'; import { PoStepComponent } from './po-step/po-step.component'; @@ -33,6 +33,11 @@ import { PoStepperItem } from './po-stepper-item.interface'; * * * + * + * + * + * + * */ @Component({ selector: 'po-stepper', @@ -40,17 +45,27 @@ import { PoStepperItem } from './po-stepper-item.interface'; }) export class PoStepperComponent extends PoStepperBaseComponent implements AfterContentInit { @ContentChildren(PoStepComponent) poSteps: QueryList; + nextStepDone: boolean = false; private currentActiveStep: PoStepComponent; + private previousActiveStepIndex: number | null = null; get currentStepIndex(): number { return this.step - 1; } + get isVerticalOrientation(): boolean { + return this.orientation === 'vertical'; + } + get stepList(): QueryList | Array { return (this.usePoSteps && this.poSteps) || this.steps; } + get stepSizeCircle(): number { + return this.calculateDividerPosition(); + } + get usePoSteps(): boolean { return !!this.poSteps.length; } @@ -63,7 +78,7 @@ export class PoStepperComponent extends PoStepperBaseComponent implements AfterC this.activeFirstStep(); this.poSteps.changes.subscribe(() => { - this.controlStepsStatus(0, this.poSteps.first); + this.controlStepsStatus(this.poSteps.first); }); } @@ -80,6 +95,11 @@ export class PoStepperComponent extends PoStepperBaseComponent implements AfterC } const stepsArray = this.getPoSteps(); + + if (index < 0 || index >= stepsArray.length) { + return; + } + const step = stepsArray[index]; this.changeStep(index, step); } @@ -112,6 +132,11 @@ export class PoStepperComponent extends PoStepperBaseComponent implements AfterC const { steps, stepIndex } = this.getStepsAndIndex(this.currentActiveStep); const nextIndex = stepIndex + 1; + + if (nextIndex >= steps.length) { + return; + } + const nextStep = steps[nextIndex]; this.changeStep(nextIndex, nextStep); @@ -129,6 +154,11 @@ export class PoStepperComponent extends PoStepperBaseComponent implements AfterC const { steps, stepIndex } = this.getStepsAndIndex(this.currentActiveStep); const previousIndex = stepIndex - 1; + + if (previousIndex < 0) { + return; + } + const previousStep = steps[previousIndex]; this.changeStep(previousIndex, previousStep); @@ -140,18 +170,34 @@ export class PoStepperComponent extends PoStepperBaseComponent implements AfterC .subscribe(nextStepAllowed => { if (nextStepAllowed) { const isDifferentStep = !this.currentActiveStep || step.id !== this.currentActiveStep.id; + const nextStep = this.getNextSteps(stepIndex); if (this.usePoSteps && isDifferentStep) { - this.controlStepsStatus(stepIndex, step); + this.controlStepsStatus(step); this.onChangeStep.emit(step); } else if (!this.usePoSteps && stepIndex !== this.currentStepIndex) { // if para tratamento do modelo antigo do po-stepper this.onChangeStep.emit(stepIndex + 1); + this.nextStepDone = this.isNextStepsDone(nextStep); } } }); } + getNextPoSteps(stepIndex: number): PoStepperItem { + const poSteps = this.getPoSteps(); + return poSteps[stepIndex + 1]; + } + + isDashedBorder(step: PoStepComponent, index: number): boolean { + const nextStep = this.getNextPoSteps(index); + return ( + !(step.status === 'active' && (nextStep?.status === 'done' || this.nextStepDone)) && + step.status !== 'done' && + (this.usePoSteps || this.sequential) + ); + } + onStepActive(step: PoStepComponent) { this.currentActiveStep = step; @@ -181,47 +227,99 @@ export class PoStepperComponent extends PoStepperBaseComponent implements AfterC return of(true); } - const isAllowNextStep$ = this.usePoSteps - ? this.isBeforeStep(nextStepIndex) || this.canActiveNextStep(this.currentActiveStep) - : this.steps.slice(this.step, nextStepIndex).every(step => step.status === PoStepperStatus.Done); + if (this.hasStepWithCanActiveNextStep() && this.hasDefaultBeforeDone(nextStepIndex)) { + return of(false); + } + + const isAllowNextStep$ = this.checkAllowNextStep(nextStepIndex); return typeof isAllowNextStep$ === 'boolean' ? of(isAllowNextStep$) : isAllowNextStep$; } - private canActiveNextStep(currentActiveStep = {}): Observable { + private canActiveNextStep(currentActiveStep = {}, nextStepIndex?: number): Observable { + const isCurrentStep = this.isCurrentStep(nextStepIndex); + if (!currentActiveStep.canActiveNextStep) { + if (!isCurrentStep) { + currentActiveStep.status = PoStepperStatus.Done; + } return of(true); } - const canActiveNextStep = currentActiveStep.canActiveNextStep(currentActiveStep); - - const canActiveNextStep$ = canActiveNextStep instanceof Observable ? canActiveNextStep : of(canActiveNextStep); - - return canActiveNextStep$.pipe( + const canActiveNextStep$ = this.getCanActiveNextStepObservable(currentActiveStep); + + return of(this.isBeforeStep(nextStepIndex)).pipe( + mergeMap(isBefore => { + if (isBefore && !isCurrentStep) { + return canActiveNextStep$.pipe( + tap(isCanActiveNextStep => { + currentActiveStep.status = isCanActiveNextStep ? PoStepperStatus.Done : PoStepperStatus.Default; + }), + map(() => true) + ); + } else { + return canActiveNextStep$; + } + }), tap(isCanActiveNextStep => { - currentActiveStep.status = this.getStepperStatusByCanActive(isCanActiveNextStep); + if (!this.isBeforeStep(nextStepIndex) && !isCurrentStep) { + this.updateStepStatus(currentActiveStep, isCanActiveNextStep); + } }), catchError(err => { currentActiveStep.status = PoStepperStatus.Error; - return throwError(err); }) ); } - private controlStepsStatus(stepIndex: number, step: PoStepComponent): void { + private checkAllowNextStep(nextStepIndex: number): Observable { + return this.usePoSteps + ? this.canActiveNextStep(this.currentActiveStep, nextStepIndex) + : of(this.steps.slice(this.step, nextStepIndex).every(step => step.status === PoStepperStatus.Done)); + } + + private getCanActiveNextStepObservable(currentActiveStep: PoStepComponent): Observable { + const canActiveNextStep = currentActiveStep.canActiveNextStep(currentActiveStep); + return canActiveNextStep instanceof Observable ? canActiveNextStep : of(canActiveNextStep); + } + + private hasDefaultBeforeDone(nextStepIndex: number): boolean { + return this.getPoSteps() + .slice(this.step, nextStepIndex) + .some(step => step.status === PoStepperStatus.Default); + } + + private isCurrentStep(stepIndex: number): boolean { + return ( + this.currentActiveStep && this.getPoSteps().findIndex(step => step.id === this.currentActiveStep.id) === stepIndex + ); + } + + private controlStepsStatus(step: PoStepComponent): void { if (this.usePoSteps) { - this.setStepAsActive(step); - this.setNextStepAsDefault(step); + const { steps, stepIndex: currentStepIndex } = this.getStepsAndIndex(step); - if (this.isBeforeStep(stepIndex)) { - this.setFinalSteppersAsDisabled(stepIndex); + if (!this.hasStepWithCanActiveNextStep()) { + this.updatePreviousStepStatus(steps, currentStepIndex); } + this.setStepAsActive(step); + this.handleNextStep(steps, currentStepIndex); + + this.previousActiveStepIndex = currentStepIndex; this.changeDetector.detectChanges(); } } + private calculateDividerPosition(): number { + return this.stepSize >= 24 && this.stepSize <= 64 ? this.stepSize : 24; + } + + private getNextSteps(stepIndex: number): PoStepperItem { + return this.steps[stepIndex + 1]; + } + private getStepperStatusByCanActive(canActiveNextStep: boolean): PoStepperStatus { return canActiveNextStep ? PoStepperStatus.Done : PoStepperStatus.Error; } @@ -237,12 +335,37 @@ export class PoStepperComponent extends PoStepperBaseComponent implements AfterC return this.poSteps.toArray(); } + private handleNextStep(steps: Array, currentStepIndex: number): void { + const nextStep = steps[currentStepIndex + 1]; + const currentStep = steps[currentStepIndex]; + const isNextStepDisabled = nextStep && nextStep.status === PoStepperStatus.Disabled; + + if (!this.hasStepWithCanActiveNextStep() && isNextStepDisabled) { + this.setNextStepAsDefault(steps[currentStepIndex]); + if (this.isBeforeStep(currentStepIndex)) { + this.setFinalSteppersAsDisabled(currentStepIndex); + } + } + if (this.hasStepWithCanActiveNextStep() && isNextStepDisabled) { + this.setNextStepAsDefault(currentStep); + this.setFinalSteppersAsDisabled(currentStepIndex); + } + } + + private hasStepWithCanActiveNextStep(): boolean { + return this.getPoSteps().some(step => step.canActiveNextStep && step.canActiveNextStep(step)); + } + private isBeforeStep(stepIndex: number): boolean { const currentActiveStepIndex = () => this.getPoSteps().findIndex(step => step.id === this.currentActiveStep.id); return !!this.currentActiveStep && currentActiveStepIndex() >= stepIndex; } + private isNextStepsDone(nextStep?: PoStepperItem): boolean { + return nextStep?.status === PoStepperStatus.Done; + } + private setFinalSteppersAsDisabled(stepIndex: number): void { this.getPoSteps() .filter((step, index) => step && index >= stepIndex + 2) @@ -261,4 +384,14 @@ export class PoStepperComponent extends PoStepperBaseComponent implements AfterC steps[nextIndex].status = PoStepperStatus.Default; } } + + private updatePreviousStepStatus(steps: Array, currentStepIndex: number): void { + if (this.previousActiveStepIndex !== null && this.previousActiveStepIndex !== currentStepIndex) { + steps[this.previousActiveStepIndex].status = PoStepperStatus.Done; + } + } + + private updateStepStatus(currentActiveStep: PoStepComponent, isCanActiveNextStep: boolean): void { + currentActiveStep.status = this.getStepperStatusByCanActive(isCanActiveNextStep); + } } diff --git a/projects/ui/src/lib/components/po-stepper/po-stepper.module.ts b/projects/ui/src/lib/components/po-stepper/po-stepper.module.ts index c62bb8b5f..11052543f 100644 --- a/projects/ui/src/lib/components/po-stepper/po-stepper.module.ts +++ b/projects/ui/src/lib/components/po-stepper/po-stepper.module.ts @@ -6,14 +6,15 @@ import { PoStepperCircleComponent } from './po-stepper-circle/po-stepper-circle. import { PoStepperComponent } from './po-stepper.component'; import { PoStepperLabelComponent } from './po-stepper-label/po-stepper-label.component'; import { PoStepperStepComponent } from './po-stepper-step/po-stepper-step.component'; -import { PoIconModule } from '../po-icon'; +import { PoIconModule } from '../po-icon/po-icon.module'; +import { PoTooltipModule } from '../../directives/po-tooltip/index'; /** * @description * Módulo do componente po-stepper */ @NgModule({ - imports: [CommonModule, PoIconModule], + imports: [CommonModule, PoIconModule, PoTooltipModule], declarations: [ PoStepComponent, PoStepperCircleComponent, diff --git a/projects/ui/src/lib/components/po-stepper/samples/sample-po-stepper-active/sample-po-stepper-active.component.html b/projects/ui/src/lib/components/po-stepper/samples/sample-po-stepper-active/sample-po-stepper-active.component.html index 2cc2c4c54..724c2ca29 100644 --- a/projects/ui/src/lib/components/po-stepper/samples/sample-po-stepper-active/sample-po-stepper-active.component.html +++ b/projects/ui/src/lib/components/po-stepper/samples/sample-po-stepper-active/sample-po-stepper-active.component.html @@ -1,103 +1,112 @@ - - -
-
- - - - - - - - - - + + + + +
+ + + + + + + + + + +
+ +
+ +
+
+ + + + + + + +
+
+ + + + + + + +
+
+
+ +
+
+ + + +
+
+
+ +
+ +

{{ experience.description }}

+
+
- -
- -
-
- - - - - - - -
-
- - - - - - - -
-
-
- -
-
- - - -
-
-
- -
- -

{{ experience.description }}

-
-
-
-
-
+ + +
diff --git a/projects/ui/src/lib/components/po-stepper/samples/sample-po-stepper-labs/sample-po-stepper-labs.component.html b/projects/ui/src/lib/components/po-stepper/samples/sample-po-stepper-labs/sample-po-stepper-labs.component.html index fd572e6c8..c90d1b1c9 100644 --- a/projects/ui/src/lib/components/po-stepper/samples/sample-po-stepper-labs/sample-po-stepper-labs.component.html +++ b/projects/ui/src/lib/components/po-stepper/samples/sample-po-stepper-labs/sample-po-stepper-labs.component.html @@ -1,37 +1,45 @@ - - -

Step Content {{ step.label }}

-
-
+ + + +

Step Content {{ step.label }}

+
+
-
+
- + -
- + + -
- - -
-
+
+ + +
+ -
- - -
- - -
-
+
+ +
+
+ + +
+
+
diff --git a/projects/ui/src/lib/components/po-stepper/samples/sample-po-stepper-labs/sample-po-stepper-labs.component.ts b/projects/ui/src/lib/components/po-stepper/samples/sample-po-stepper-labs/sample-po-stepper-labs.component.ts index 699638834..e8b922341 100644 --- a/projects/ui/src/lib/components/po-stepper/samples/sample-po-stepper-labs/sample-po-stepper-labs.component.ts +++ b/projects/ui/src/lib/components/po-stepper/samples/sample-po-stepper-labs/sample-po-stepper-labs.component.ts @@ -25,7 +25,7 @@ export class SamplePoStepperLabsComponent implements OnInit { { property: 'orientation', options: [ - { value: 'vertical', label: 'Vertical' }, + { value: 'vertical', label: 'Vertical', checked: true }, { value: 'horizontal', label: 'Horizontal' } ], gridLgColumns: 4 @@ -35,6 +35,18 @@ export class SamplePoStepperLabsComponent implements OnInit { gridLgColumns: 4, property: 'stepIcons', type: 'boolean' + }, + { + label: 'Step Icon Active Custom', + help: 'Ex.: ph ph-pencil-simple-line', + gridLgColumns: 4, + property: 'iconActive' + }, + { + label: 'Step Icon Done Custom', + help: 'Ex.: ph ph-check-fat', + gridLgColumns: 4, + property: 'iconDone' } ]; @@ -43,7 +55,13 @@ export class SamplePoStepperLabsComponent implements OnInit { divider: 'Step form', property: 'label', label: 'Step Label', - required: true, + gridMdColumns: 6, + gridXlColumns: 6 + }, + { + property: 'iconDefault', + label: 'Step Icon Default Custom', + help: 'Ex.: ph ph-question', gridMdColumns: 6, gridXlColumns: 6 } @@ -57,6 +75,8 @@ export class SamplePoStepperLabsComponent implements OnInit { addItem(stepItem: PoStepperItem) { this.steps = [...this.steps, { ...stepItem }]; + this.stepItem = {}; + this.changeDetector.detectChanges(); } changeStep(event) { @@ -69,5 +89,6 @@ export class SamplePoStepperLabsComponent implements OnInit { this.properties = {}; this.steps = []; this.event = undefined; + this.properties.orientation = 'horizontal'; } } diff --git a/projects/ui/src/lib/components/po-stepper/samples/sample-po-stepper-steps/sample-po-stepper-steps.component.html b/projects/ui/src/lib/components/po-stepper/samples/sample-po-stepper-steps/sample-po-stepper-steps.component.html new file mode 100644 index 000000000..c41c021e5 --- /dev/null +++ b/projects/ui/src/lib/components/po-stepper/samples/sample-po-stepper-steps/sample-po-stepper-steps.component.html @@ -0,0 +1,7 @@ + + diff --git a/projects/ui/src/lib/components/po-stepper/samples/sample-po-stepper-steps/sample-po-stepper-steps.component.ts b/projects/ui/src/lib/components/po-stepper/samples/sample-po-stepper-steps/sample-po-stepper-steps.component.ts new file mode 100644 index 000000000..efd42c35e --- /dev/null +++ b/projects/ui/src/lib/components/po-stepper/samples/sample-po-stepper-steps/sample-po-stepper-steps.component.ts @@ -0,0 +1,45 @@ +import { AfterViewInit, ChangeDetectorRef, Component } from '@angular/core'; +import { PoStepperItem, PoStepperStatus } from '@po-ui/ng-components'; + +@Component({ + selector: 'sample-po-stepper-steps', + templateUrl: './sample-po-stepper-steps.component.html' +}) +export class SamplePoStepperStepsComponent implements AfterViewInit { + currentStep: number; + stepsWithStatus: Array = [ + { label: 'Step 1', status: PoStepperStatus.Done }, + { label: 'Step 2', status: PoStepperStatus.Active }, + { label: 'Step 3', status: PoStepperStatus.Default }, + { label: 'Step 4', status: PoStepperStatus.Disabled } + ]; + + constructor(private changeDetector: ChangeDetectorRef) {} + + ngAfterViewInit(): void { + this.currentStep = 2; + this.changeDetector.detectChanges(); + } + + onChangeStatus(event: number): void { + this.currentStep = event; + + this.stepsWithStatus.forEach(step => { + if (step.status === PoStepperStatus.Active) { + step.status = PoStepperStatus.Done; + } + }); + + this.stepsWithStatus.forEach((step, index) => { + if (index > this.currentStep && step.status === PoStepperStatus.Active) { + step.status = PoStepperStatus.Default; + } + }); + if ( + this.currentStep < this.stepsWithStatus.length && + this.stepsWithStatus[this.currentStep].status === PoStepperStatus.Disabled + ) { + this.stepsWithStatus[this.currentStep].status = PoStepperStatus.Default; + } + } +}