From 889b6fd05cb22dabe9d678185cd2640c53e644b2 Mon Sep 17 00:00:00 2001 From: allan-chagas-brisa Date: Mon, 25 Sep 2023 15:04:19 -0300 Subject: [PATCH 1/8] feat: modal header button --- .../ion/src/lib/modal/component/modal.component.html | 11 +++++++++++ .../ion/src/lib/modal/component/modal.component.scss | 2 ++ .../ion/src/lib/modal/component/modal.component.ts | 6 ++++++ projects/ion/src/lib/modal/modal.service.ts | 6 ++++++ projects/ion/src/lib/modal/models/modal.interface.ts | 10 +++++++++- stories/Modal.stories.mdx | 8 ++++++++ 6 files changed, 42 insertions(+), 1 deletion(-) diff --git a/projects/ion/src/lib/modal/component/modal.component.html b/projects/ion/src/lib/modal/component/modal.component.html index 975a5be1c..c672a9a90 100644 --- a/projects/ion/src/lib/modal/component/modal.component.html +++ b/projects/ion/src/lib/modal/component/modal.component.html @@ -16,6 +16,17 @@ aria-modal="true" >
+

{{ configuration.title }}

(); ionOnClose = new EventEmitter(); public DEFAULT_WIDTH = 500; @@ -88,6 +90,10 @@ export class IonModalComponent implements OnInit, OnDestroy { Object.assign(instance, params); } + emitHeaderButtonAction(): void { + this.ionOnHeaderButtonAction.emit(); + } + ngOnInit(): void { this.setDefaultConfig(); const factory = this.resolver.resolveComponentFactory(this.componentToBody); diff --git a/projects/ion/src/lib/modal/modal.service.ts b/projects/ion/src/lib/modal/modal.service.ts index 5d2bbe73e..e4cda62ef 100644 --- a/projects/ion/src/lib/modal/modal.service.ts +++ b/projects/ion/src/lib/modal/modal.service.ts @@ -21,6 +21,7 @@ import { providedIn: 'root', }) export class IonModalService { + public ionOnHeaderButtonAction = new Subject(); private modalComponentRef!: ComponentRef; private componentSubscriber!: Subject; @@ -62,6 +63,11 @@ export class IonModalService { this.emitValueAndCloseModal(valueFromModal); } ); + + this.modalComponentRef.instance.ionOnHeaderButtonAction.subscribe(() => { + this.ionOnHeaderButtonAction.next(); + }); + this.componentSubscriber = new Subject(); return this.componentSubscriber.asObservable(); } diff --git a/projects/ion/src/lib/modal/models/modal.interface.ts b/projects/ion/src/lib/modal/models/modal.interface.ts index a254decd4..d2f9fcb27 100644 --- a/projects/ion/src/lib/modal/models/modal.interface.ts +++ b/projects/ion/src/lib/modal/models/modal.interface.ts @@ -1,4 +1,4 @@ -import { IonButtonProps } from '../../core/types'; +import { IconType, IonButtonProps } from '../../core/types'; import { SafeAny } from '../../utils/safe-any'; export interface IonModalConfiguration { @@ -8,6 +8,7 @@ export interface IonModalConfiguration { showOverlay?: boolean; overlayCanDismiss?: boolean; ionParams?: SafeAny; + headerButton?: IonModalHeaderButton; footer?: IonModalFooterConfiguration; } @@ -20,6 +21,13 @@ export interface IonModalFooterConfiguration { secondaryButton?: IonButtonProps; } +interface IonModalHeaderButton { + label: string; + icon: IconType; + disabled?: () => boolean; + hidden?: () => boolean; +} + export interface IonModalResponse { [key: string]: unknown; } diff --git a/stories/Modal.stories.mdx b/stories/Modal.stories.mdx index 92827c5ad..6967a7560 100644 --- a/stories/Modal.stories.mdx +++ b/stories/Modal.stories.mdx @@ -49,6 +49,14 @@ Função responsável pela renderização do modal. Ela recebe como parâmetros: height="350px" args={{ componentToBody: SelectMockComponent, + modalConfig: { + headerButton: { + icon: 'left', + label: 'test', + disabled: () => false, + hidden: () => false, + }, + }, }} decorators={[ moduleMetadata({ From 9f21faaea2246e950ed864ad2bb37a93840ed09a Mon Sep 17 00:00:00 2001 From: allan-chagas-brisa Date: Mon, 25 Sep 2023 15:14:27 -0300 Subject: [PATCH 2/8] fix: changes on mock --- .../ion/src/lib/modal/mock/select-mock.component.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/projects/ion/src/lib/modal/mock/select-mock.component.ts b/projects/ion/src/lib/modal/mock/select-mock.component.ts index 82470226f..a3141014a 100644 --- a/projects/ion/src/lib/modal/mock/select-mock.component.ts +++ b/projects/ion/src/lib/modal/mock/select-mock.component.ts @@ -1,4 +1,5 @@ -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; +import { IonModalService } from '../modal.service'; @Component({ template: ` @@ -18,6 +19,13 @@ import { Component } from '@angular/core'; `, }) -export class SelectMockComponent { +export class SelectMockComponent implements OnInit { state = 'ceara'; + constructor(private ionModalService: IonModalService) {} + + ngOnInit(): void { + this.ionModalService.ionOnHeaderButtonAction.subscribe(() => { + this.state = 'espirito-santo'; + }); + } } From c0db48dfcfe080099ab1ab52ed1af3d3bba0182a Mon Sep 17 00:00:00 2001 From: allan-chagas-brisa Date: Tue, 26 Sep 2023 09:41:56 -0300 Subject: [PATCH 3/8] fix: header spacing --- projects/ion/src/lib/modal/component/modal.component.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/ion/src/lib/modal/component/modal.component.scss b/projects/ion/src/lib/modal/component/modal.component.scss index c823369e1..126153b81 100644 --- a/projects/ion/src/lib/modal/component/modal.component.scss +++ b/projects/ion/src/lib/modal/component/modal.component.scss @@ -44,7 +44,7 @@ display: flex; justify-content: space-between; align-items: center; - gap: 12px; + gap: 8px; h4 { margin: 0; From 2becbf7ba468e605963fc4d0ecb1dc61ce18f06c Mon Sep 17 00:00:00 2001 From: allan-chagas-brisa Date: Tue, 26 Sep 2023 10:28:47 -0300 Subject: [PATCH 4/8] feat: modal body content emition and test adjustments --- .../lib/modal/component/modal.component.html | 16 +- .../modal/component/modal.component.spec.ts | 157 ++++++++++++++++++ .../lib/modal/component/modal.component.ts | 4 +- .../lib/modal/mock/select-mock.component.ts | 12 +- .../ion/src/lib/modal/modal.service.spec.ts | 18 ++ projects/ion/src/lib/modal/modal.service.ts | 12 +- 6 files changed, 201 insertions(+), 18 deletions(-) diff --git a/projects/ion/src/lib/modal/component/modal.component.html b/projects/ion/src/lib/modal/component/modal.component.html index c672a9a90..c952b0427 100644 --- a/projects/ion/src/lib/modal/component/modal.component.html +++ b/projects/ion/src/lib/modal/component/modal.component.html @@ -18,14 +18,24 @@

{{ configuration.title }}

{ fixture.nativeElement.querySelector('.modal-container').style.width; expect(modalElement).toBe(`${modalConfig.width}px`); }); + + describe('IonModalComponent - Header left button', () => { + it('should not be rendered as default', () => { + expect(screen.queryByTestId('btn-voltar')).not.toBeInTheDocument(); + }); + + it('should emit event when call closeModal function', () => { + jest.spyOn(component.ionOnHeaderButtonAction, 'emit'); + component.emitHeaderButtonAction( + component.getChildComponentPropertiesValue() + ); + expect(component.ionOnHeaderButtonAction.emit).toHaveBeenCalled(); + }); + + it('should be visible as default if the config is informed', () => { + const configuration: IonModalConfiguration = { + id: '1', + title: 'Ion Test', + + footer: { + showDivider: false, + primaryButton: { + label: 'Ion Cancel', + iconType: 'icon', + }, + secondaryButton: { + label: 'Ion Confirm', + iconType: 'icon', + }, + }, + + headerButton: { + icon: 'left', + label: 'voltar', + }, + }; + + component.setConfig(configuration); + fixture.detectChanges(); + expect(screen.getByTestId('btn-voltar')).toBeVisible(); + }); + + it('should be hidden when informed', () => { + const configuration: IonModalConfiguration = { + id: '1', + title: 'Ion Test', + + footer: { + showDivider: false, + primaryButton: { + label: 'Ion Cancel', + iconType: 'icon', + }, + secondaryButton: { + label: 'Ion Confirm', + iconType: 'icon', + }, + }, + + headerButton: { + icon: 'left', + label: 'voltar', + hidden: () => true, + }, + }; + + component.setConfig(configuration); + fixture.detectChanges(); + expect(screen.queryByTestId('btn-voltar')).not.toBeInTheDocument(); + }); + + it('should not be disabled as default', () => { + const configuration: IonModalConfiguration = { + id: '1', + title: 'Ion Test', + + footer: { + showDivider: false, + primaryButton: { + label: 'Ion Cancel', + iconType: 'icon', + }, + secondaryButton: { + label: 'Ion Confirm', + iconType: 'icon', + }, + }, + + headerButton: { + icon: 'left', + label: 'voltar', + }, + }; + + component.setConfig(configuration); + fixture.detectChanges(); + expect(screen.getByTestId('btn-voltar')).not.toBeDisabled(); + }); + + it('should be disabled when informed', () => { + const configuration: IonModalConfiguration = { + id: '1', + title: 'Ion Test', + + footer: { + showDivider: false, + primaryButton: { + label: 'Ion Cancel', + iconType: 'icon', + }, + secondaryButton: { + label: 'Ion Confirm', + iconType: 'icon', + }, + }, + + headerButton: { + icon: 'left', + label: 'voltar', + disabled: () => true, + }, + }; + + component.setConfig(configuration); + fixture.detectChanges(); + expect(screen.getByTestId('btn-voltar')).toBeDisabled(); + }); + + it('should render the specified icon', () => { + const configuration: IonModalConfiguration = { + id: '1', + title: 'Ion Test', + + footer: { + showDivider: false, + primaryButton: { + label: 'Ion Cancel', + iconType: 'icon', + }, + secondaryButton: { + label: 'Ion Confirm', + iconType: 'icon', + }, + }, + + headerButton: { + icon: 'left', + label: 'voltar', + }, + }; + + component.setConfig(configuration); + fixture.detectChanges(); + const icon = document.getElementById('ion-icon-left'); + expect(icon).toBeVisible(); + }); + }); }); diff --git a/projects/ion/src/lib/modal/component/modal.component.ts b/projects/ion/src/lib/modal/component/modal.component.ts index 36522354e..c3f33e339 100644 --- a/projects/ion/src/lib/modal/component/modal.component.ts +++ b/projects/ion/src/lib/modal/component/modal.component.ts @@ -90,8 +90,8 @@ export class IonModalComponent implements OnInit, OnDestroy { Object.assign(instance, params); } - emitHeaderButtonAction(): void { - this.ionOnHeaderButtonAction.emit(); + emitHeaderButtonAction(valueToEmit: IonModalResponse | undefined): void { + this.ionOnHeaderButtonAction.emit(valueToEmit); } ngOnInit(): void { diff --git a/projects/ion/src/lib/modal/mock/select-mock.component.ts b/projects/ion/src/lib/modal/mock/select-mock.component.ts index a3141014a..82470226f 100644 --- a/projects/ion/src/lib/modal/mock/select-mock.component.ts +++ b/projects/ion/src/lib/modal/mock/select-mock.component.ts @@ -1,5 +1,4 @@ -import { Component, OnInit } from '@angular/core'; -import { IonModalService } from '../modal.service'; +import { Component } from '@angular/core'; @Component({ template: ` @@ -19,13 +18,6 @@ import { IonModalService } from '../modal.service'; `, }) -export class SelectMockComponent implements OnInit { +export class SelectMockComponent { state = 'ceara'; - constructor(private ionModalService: IonModalService) {} - - ngOnInit(): void { - this.ionModalService.ionOnHeaderButtonAction.subscribe(() => { - this.state = 'espirito-santo'; - }); - } } diff --git a/projects/ion/src/lib/modal/modal.service.spec.ts b/projects/ion/src/lib/modal/modal.service.spec.ts index b68b08a7d..c203ac137 100644 --- a/projects/ion/src/lib/modal/modal.service.spec.ts +++ b/projects/ion/src/lib/modal/modal.service.spec.ts @@ -95,4 +95,22 @@ describe('ModalService', () => { state: 'ceara', }); }); + + it('should call emitHeaderAction when ionOnHeaderButtonAction fires', () => { + jest.spyOn(modalService, 'emitHeaderAction'); + + modalService.open(SelectMockComponent, { + headerButton: { + icon: 'left', + label: 'voltar', + }, + }); + + fireEvent.click(screen.getByTestId('btn-voltar')); + fixture.detectChanges(); + + expect(modalService.emitHeaderAction).toHaveBeenCalledWith({ + state: 'ceara', + }); + }); }); diff --git a/projects/ion/src/lib/modal/modal.service.ts b/projects/ion/src/lib/modal/modal.service.ts index e4cda62ef..335d0728a 100644 --- a/projects/ion/src/lib/modal/modal.service.ts +++ b/projects/ion/src/lib/modal/modal.service.ts @@ -64,9 +64,11 @@ export class IonModalService { } ); - this.modalComponentRef.instance.ionOnHeaderButtonAction.subscribe(() => { - this.ionOnHeaderButtonAction.next(); - }); + this.modalComponentRef.instance.ionOnHeaderButtonAction.subscribe( + (valueFromModal: IonModalResponse) => { + this.emitHeaderAction(valueFromModal); + } + ); this.componentSubscriber = new Subject(); return this.componentSubscriber.asObservable(); @@ -77,6 +79,10 @@ export class IonModalService { this.closeModal(); } + emitHeaderAction(valueToEmit: IonModalResponse | unknown): void { + this.ionOnHeaderButtonAction.next(valueToEmit); + } + closeModal(): void { if (this.modalComponentRef) { this.appRef.detachView(this.modalComponentRef.hostView); From bd86bd5da4254aafe2aa20073647e776cd7d6840 Mon Sep 17 00:00:00 2001 From: allan-chagas-brisa Date: Tue, 26 Sep 2023 14:37:07 -0300 Subject: [PATCH 5/8] refactor: readonly added to the subject --- projects/ion/src/lib/modal/modal.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/ion/src/lib/modal/modal.service.ts b/projects/ion/src/lib/modal/modal.service.ts index 335d0728a..dcfb07943 100644 --- a/projects/ion/src/lib/modal/modal.service.ts +++ b/projects/ion/src/lib/modal/modal.service.ts @@ -21,7 +21,7 @@ import { providedIn: 'root', }) export class IonModalService { - public ionOnHeaderButtonAction = new Subject(); + public readonly ionOnHeaderButtonAction = new Subject(); private modalComponentRef!: ComponentRef; private componentSubscriber!: Subject; From 7b6f94f99bf67b66d70967ddef0a747f4b0f19d7 Mon Sep 17 00:00:00 2001 From: allan-chagas-brisa Date: Tue, 26 Sep 2023 14:48:15 -0300 Subject: [PATCH 6/8] docs: service subscription --- stories/Modal.stories.mdx | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/stories/Modal.stories.mdx b/stories/Modal.stories.mdx index 6967a7560..c9074a076 100644 --- a/stories/Modal.stories.mdx +++ b/stories/Modal.stories.mdx @@ -72,6 +72,26 @@ Função responsável pela renderização do modal. Ela recebe como parâmetros:
+## ionOnHeaderButtonAction + +Um subject que irá informar ao Body Component do clique no HeaderLeftButton. O usuário deve instanciar o ModalService no componente usado como body e se inscrever nesse subject. + +> Ideal para casos onde precisamos que o bodyComponent altere seu estado a partir da action do headerButton. Veja o exemplo abaixo: + + + ## EmitValueAndCloseModal Recebe como parâmetro um valor, seguindo a interface _IonModalResponse_, a ser emitido e faz o fechamento do modal. From e0e0d6745dab0cf13bce74b12021856743f1b7e0 Mon Sep 17 00:00:00 2001 From: allan-chagas-brisa Date: Thu, 28 Sep 2023 09:39:34 -0300 Subject: [PATCH 7/8] test: pr sugested adjustments --- .../ion/src/lib/modal/component/modal.component.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/projects/ion/src/lib/modal/component/modal.component.spec.ts b/projects/ion/src/lib/modal/component/modal.component.spec.ts index 1836fdb20..9ec417b8a 100644 --- a/projects/ion/src/lib/modal/component/modal.component.spec.ts +++ b/projects/ion/src/lib/modal/component/modal.component.spec.ts @@ -202,7 +202,7 @@ describe('IonModalComponent', () => { expect(screen.queryByTestId('btn-voltar')).not.toBeInTheDocument(); }); - it('should emit event when call closeModal function', () => { + it('should emit event when call emitHeaderButtonAction function', () => { jest.spyOn(component.ionOnHeaderButtonAction, 'emit'); component.emitHeaderButtonAction( component.getChildComponentPropertiesValue() @@ -267,7 +267,7 @@ describe('IonModalComponent', () => { expect(screen.queryByTestId('btn-voltar')).not.toBeInTheDocument(); }); - it('should not be disabled as default', () => { + it('should be enabled as default', () => { const configuration: IonModalConfiguration = { id: '1', title: 'Ion Test', @@ -292,7 +292,7 @@ describe('IonModalComponent', () => { component.setConfig(configuration); fixture.detectChanges(); - expect(screen.getByTestId('btn-voltar')).not.toBeDisabled(); + expect(screen.getByTestId('btn-voltar')).toBeEnabled(); }); it('should be disabled when informed', () => { From 33bd5bd40ad7fcaa13ae6b6727fce3eef7e8df1b Mon Sep 17 00:00:00 2001 From: allan-chagas-brisa Date: Thu, 28 Sep 2023 10:57:26 -0300 Subject: [PATCH 8/8] refactor: pr sugested adjustments --- .../modal/component/modal.component.spec.ts | 149 ++++-------------- .../lib/modal/component/modal.component.ts | 3 +- .../src/lib/modal/models/modal.interface.ts | 2 +- 3 files changed, 33 insertions(+), 121 deletions(-) diff --git a/projects/ion/src/lib/modal/component/modal.component.spec.ts b/projects/ion/src/lib/modal/component/modal.component.spec.ts index 9ec417b8a..857454ba9 100644 --- a/projects/ion/src/lib/modal/component/modal.component.spec.ts +++ b/projects/ion/src/lib/modal/component/modal.component.spec.ts @@ -198,6 +198,28 @@ describe('IonModalComponent', () => { }); describe('IonModalComponent - Header left button', () => { + const configuration: IonModalConfiguration = { + id: '1', + title: 'Ion Test', + + footer: { + showDivider: false, + primaryButton: { + label: 'Ion Cancel', + iconType: 'icon', + }, + secondaryButton: { + label: 'Ion Confirm', + iconType: 'icon', + }, + }, + + headerButton: { + icon: 'left', + label: 'voltar', + }, + }; + it('should not be rendered as default', () => { expect(screen.queryByTestId('btn-voltar')).not.toBeInTheDocument(); }); @@ -211,146 +233,37 @@ describe('IonModalComponent', () => { }); it('should be visible as default if the config is informed', () => { - const configuration: IonModalConfiguration = { - id: '1', - title: 'Ion Test', - - footer: { - showDivider: false, - primaryButton: { - label: 'Ion Cancel', - iconType: 'icon', - }, - secondaryButton: { - label: 'Ion Confirm', - iconType: 'icon', - }, - }, - - headerButton: { - icon: 'left', - label: 'voltar', - }, - }; - component.setConfig(configuration); fixture.detectChanges(); expect(screen.getByTestId('btn-voltar')).toBeVisible(); }); - it('should be hidden when informed', () => { - const configuration: IonModalConfiguration = { - id: '1', - title: 'Ion Test', - - footer: { - showDivider: false, - primaryButton: { - label: 'Ion Cancel', - iconType: 'icon', - }, - secondaryButton: { - label: 'Ion Confirm', - iconType: 'icon', - }, - }, - - headerButton: { - icon: 'left', - label: 'voltar', - hidden: () => true, - }, - }; - - component.setConfig(configuration); - fixture.detectChanges(); - expect(screen.queryByTestId('btn-voltar')).not.toBeInTheDocument(); - }); - it('should be enabled as default', () => { - const configuration: IonModalConfiguration = { - id: '1', - title: 'Ion Test', - - footer: { - showDivider: false, - primaryButton: { - label: 'Ion Cancel', - iconType: 'icon', - }, - secondaryButton: { - label: 'Ion Confirm', - iconType: 'icon', - }, - }, - - headerButton: { - icon: 'left', - label: 'voltar', - }, - }; - component.setConfig(configuration); fixture.detectChanges(); expect(screen.getByTestId('btn-voltar')).toBeEnabled(); }); it('should be disabled when informed', () => { - const configuration: IonModalConfiguration = { - id: '1', - title: 'Ion Test', - - footer: { - showDivider: false, - primaryButton: { - label: 'Ion Cancel', - iconType: 'icon', - }, - secondaryButton: { - label: 'Ion Confirm', - iconType: 'icon', - }, - }, - - headerButton: { - icon: 'left', - label: 'voltar', - disabled: () => true, - }, - }; - + configuration.headerButton.disabled = (): boolean => true; component.setConfig(configuration); fixture.detectChanges(); expect(screen.getByTestId('btn-voltar')).toBeDisabled(); }); it('should render the specified icon', () => { - const configuration: IonModalConfiguration = { - id: '1', - title: 'Ion Test', - - footer: { - showDivider: false, - primaryButton: { - label: 'Ion Cancel', - iconType: 'icon', - }, - secondaryButton: { - label: 'Ion Confirm', - iconType: 'icon', - }, - }, - - headerButton: { - icon: 'left', - label: 'voltar', - }, - }; - component.setConfig(configuration); fixture.detectChanges(); const icon = document.getElementById('ion-icon-left'); expect(icon).toBeVisible(); }); + + it('should be hidden when informed', () => { + configuration.headerButton.hidden = (): boolean => true; + + component.setConfig(configuration); + fixture.detectChanges(); + expect(screen.queryByTestId('btn-voltar')).not.toBeInTheDocument(); + }); }); }); diff --git a/projects/ion/src/lib/modal/component/modal.component.ts b/projects/ion/src/lib/modal/component/modal.component.ts index c3f33e339..b83585328 100644 --- a/projects/ion/src/lib/modal/component/modal.component.ts +++ b/projects/ion/src/lib/modal/component/modal.component.ts @@ -18,7 +18,6 @@ import { IonModalConfiguration, IonModalResponse, } from '../models/modal.interface'; -import { SafeAny } from '../../utils/safe-any'; @Component({ selector: 'ion-modal', @@ -35,7 +34,7 @@ export class IonModalComponent implements OnInit, OnDestroy { @Input() configuration: IonModalConfiguration = {}; @Output() - ionOnHeaderButtonAction = new EventEmitter(); + ionOnHeaderButtonAction = new EventEmitter(); ionOnClose = new EventEmitter(); public DEFAULT_WIDTH = 500; diff --git a/projects/ion/src/lib/modal/models/modal.interface.ts b/projects/ion/src/lib/modal/models/modal.interface.ts index d2f9fcb27..e6fb34b31 100644 --- a/projects/ion/src/lib/modal/models/modal.interface.ts +++ b/projects/ion/src/lib/modal/models/modal.interface.ts @@ -22,8 +22,8 @@ export interface IonModalFooterConfiguration { } interface IonModalHeaderButton { - label: string; icon: IconType; + label?: string; disabled?: () => boolean; hidden?: () => boolean; }