From 34381b828177963106243ee6652c9ca48febea07 Mon Sep 17 00:00:00 2001 From: christophercr Date: Mon, 4 Mar 2019 15:21:53 +0100 Subject: [PATCH] feat(stark-ui): implement Material dialogs presets: alert, confirm and prompt ISSUES CLOSED: #793 --- packages/stark-ui/assets/stark-ui-bundle.scss | 4 + .../assets/styles/_material-fixes.scss | 9 + packages/stark-ui/src/modules.ts | 1 + packages/stark-ui/src/modules/dialogs.ts | 2 + .../src/modules/dialogs/components.ts | 6 + .../components/alert-dialog-content.intf.ts | 6 + .../components/alert-dialog-theme.scss | 5 + .../components/alert-dialog.component.html | 7 + .../components/alert-dialog.component.spec.ts | 221 ++++++++++++++ .../components/alert-dialog.component.ts | 43 +++ .../components/confirm-dialog-content.intf.ts | 11 + .../components/confirm-dialog-theme.scss | 6 + .../components/confirm-dialog.component.html | 8 + .../confirm-dialog.component.spec.ts | 270 ++++++++++++++++++ .../components/confirm-dialog.component.ts | 51 ++++ .../dialogs/components/dialog-content.intf.ts | 20 ++ .../components/prompt-dialog-content.intf.ts | 26 ++ .../components/prompt-dialog-theme.scss | 6 + .../components/prompt-dialog.component.html | 15 + .../components/prompt-dialog.component.scss | 6 + .../prompt-dialog.component.spec.ts | 268 +++++++++++++++++ .../components/prompt-dialog.component.ts | 59 ++++ .../src/modules/dialogs/dialogs.module.ts | 15 + showcase/src/app/app-menu.config.ts | 7 + showcase/src/app/demo-ui/demo-ui.module.ts | 9 +- .../pages/dialogs/dialogs-page.component.html | 24 ++ .../pages/dialogs/dialogs-page.component.scss | 16 ++ .../pages/dialogs/dialogs-page.component.ts | 110 +++++++ .../src/app/demo-ui/pages/dialogs/index.ts | 1 + showcase/src/app/demo-ui/pages/index.ts | 1 + showcase/src/app/demo-ui/routes.ts | 9 + .../examples/dialogs/predefined-dialogs.html | 9 + .../examples/dialogs/predefined-dialogs.ts | 88 ++++++ showcase/src/assets/translations/en.json | 35 +++ showcase/src/assets/translations/fr.json | 35 +++ showcase/src/assets/translations/nl.json | 35 +++ 36 files changed, 1442 insertions(+), 2 deletions(-) create mode 100644 packages/stark-ui/src/modules/dialogs.ts create mode 100644 packages/stark-ui/src/modules/dialogs/components.ts create mode 100644 packages/stark-ui/src/modules/dialogs/components/alert-dialog-content.intf.ts create mode 100644 packages/stark-ui/src/modules/dialogs/components/alert-dialog-theme.scss create mode 100644 packages/stark-ui/src/modules/dialogs/components/alert-dialog.component.html create mode 100644 packages/stark-ui/src/modules/dialogs/components/alert-dialog.component.spec.ts create mode 100644 packages/stark-ui/src/modules/dialogs/components/alert-dialog.component.ts create mode 100644 packages/stark-ui/src/modules/dialogs/components/confirm-dialog-content.intf.ts create mode 100644 packages/stark-ui/src/modules/dialogs/components/confirm-dialog-theme.scss create mode 100644 packages/stark-ui/src/modules/dialogs/components/confirm-dialog.component.html create mode 100644 packages/stark-ui/src/modules/dialogs/components/confirm-dialog.component.spec.ts create mode 100644 packages/stark-ui/src/modules/dialogs/components/confirm-dialog.component.ts create mode 100644 packages/stark-ui/src/modules/dialogs/components/dialog-content.intf.ts create mode 100644 packages/stark-ui/src/modules/dialogs/components/prompt-dialog-content.intf.ts create mode 100644 packages/stark-ui/src/modules/dialogs/components/prompt-dialog-theme.scss create mode 100644 packages/stark-ui/src/modules/dialogs/components/prompt-dialog.component.html create mode 100644 packages/stark-ui/src/modules/dialogs/components/prompt-dialog.component.scss create mode 100644 packages/stark-ui/src/modules/dialogs/components/prompt-dialog.component.spec.ts create mode 100644 packages/stark-ui/src/modules/dialogs/components/prompt-dialog.component.ts create mode 100644 packages/stark-ui/src/modules/dialogs/dialogs.module.ts create mode 100644 showcase/src/app/demo-ui/pages/dialogs/dialogs-page.component.html create mode 100644 showcase/src/app/demo-ui/pages/dialogs/dialogs-page.component.scss create mode 100644 showcase/src/app/demo-ui/pages/dialogs/dialogs-page.component.ts create mode 100644 showcase/src/app/demo-ui/pages/dialogs/index.ts create mode 100644 showcase/src/assets/examples/dialogs/predefined-dialogs.html create mode 100644 showcase/src/assets/examples/dialogs/predefined-dialogs.ts diff --git a/packages/stark-ui/assets/stark-ui-bundle.scss b/packages/stark-ui/assets/stark-ui-bundle.scss index 095e9b817e..ccb0a77012 100644 --- a/packages/stark-ui/assets/stark-ui-bundle.scss +++ b/packages/stark-ui/assets/stark-ui-bundle.scss @@ -25,6 +25,10 @@ @import "../src/modules/collapsible/components/collapsible.component"; @import "../src/modules/collapsible/components/collapsible-theme"; @import "../src/modules/date-range-picker/components/date-range-picker.component"; +@import "../src/modules/dialogs/components/alert-dialog-theme"; +@import "../src/modules/dialogs/components/confirm-dialog-theme"; +@import "../src/modules/dialogs/components/prompt-dialog.component"; +@import "../src/modules/dialogs/components/prompt-dialog-theme"; @import "../src/modules/generic-search/components/generic-search/generic-search.component"; @import "../src/modules/language-selector/components/language-selector.component"; @import "../src/modules/message-pane/components/message-pane.component"; diff --git a/packages/stark-ui/assets/styles/_material-fixes.scss b/packages/stark-ui/assets/styles/_material-fixes.scss index 20f70b11b3..99a5b4ebab 100644 --- a/packages/stark-ui/assets/styles/_material-fixes.scss +++ b/packages/stark-ui/assets/styles/_material-fixes.scss @@ -32,3 +32,12 @@ .mat-button-wrapper .mat-icon { margin-top: -2.5px; } + +// FIXME: remove as soon as the MatDialogActions is adapted to align with Material Design guidelines: https://github.com/angular/material2/issues/14736 +mat-dialog-actions, +[mat-dialog-actions], +[matDialogActions] { + justify-content: flex-end; + align-items: flex-end; + margin-right: -16px; +} diff --git a/packages/stark-ui/src/modules.ts b/packages/stark-ui/src/modules.ts index b5dce8bfb2..a87c747700 100644 --- a/packages/stark-ui/src/modules.ts +++ b/packages/stark-ui/src/modules.ts @@ -9,6 +9,7 @@ export * from "./modules/breadcrumb"; export * from "./modules/collapsible"; export * from "./modules/date-picker"; export * from "./modules/date-range-picker"; +export * from "./modules/dialogs"; export * from "./modules/dropdown"; export * from "./modules/generic-search"; export * from "./modules/input-mask-directives"; diff --git a/packages/stark-ui/src/modules/dialogs.ts b/packages/stark-ui/src/modules/dialogs.ts new file mode 100644 index 0000000000..6752571a1a --- /dev/null +++ b/packages/stark-ui/src/modules/dialogs.ts @@ -0,0 +1,2 @@ +export * from "./dialogs/dialogs.module"; +export * from "./dialogs/components"; diff --git a/packages/stark-ui/src/modules/dialogs/components.ts b/packages/stark-ui/src/modules/dialogs/components.ts new file mode 100644 index 0000000000..ecc5a7f354 --- /dev/null +++ b/packages/stark-ui/src/modules/dialogs/components.ts @@ -0,0 +1,6 @@ +export * from "./components/alert-dialog.component"; +export * from "./components/alert-dialog-content.intf"; +export * from "./components/confirm-dialog.component"; +export * from "./components/confirm-dialog-content.intf"; +export * from "./components/prompt-dialog.component"; +export * from "./components/prompt-dialog-content.intf"; diff --git a/packages/stark-ui/src/modules/dialogs/components/alert-dialog-content.intf.ts b/packages/stark-ui/src/modules/dialogs/components/alert-dialog-content.intf.ts new file mode 100644 index 0000000000..eadbeb2faf --- /dev/null +++ b/packages/stark-ui/src/modules/dialogs/components/alert-dialog-content.intf.ts @@ -0,0 +1,6 @@ +import { StarkBaseDialogContent } from "./dialog-content.intf"; + +/** + * Content that can be shown in the {@link StarkAlertDialogComponent} + */ +export interface StarkAlertDialogContent extends StarkBaseDialogContent {} diff --git a/packages/stark-ui/src/modules/dialogs/components/alert-dialog-theme.scss b/packages/stark-ui/src/modules/dialogs/components/alert-dialog-theme.scss new file mode 100644 index 0000000000..cfad2f81bb --- /dev/null +++ b/packages/stark-ui/src/modules/dialogs/components/alert-dialog-theme.scss @@ -0,0 +1,5 @@ +.stark-alert-dialog { + .button-ok { + color: mat-color($primary-palette, 500); + } +} diff --git a/packages/stark-ui/src/modules/dialogs/components/alert-dialog.component.html b/packages/stark-ui/src/modules/dialogs/components/alert-dialog.component.html new file mode 100644 index 0000000000..f2b44d7645 --- /dev/null +++ b/packages/stark-ui/src/modules/dialogs/components/alert-dialog.component.html @@ -0,0 +1,7 @@ +

{{ content.title || "" | translate }}

+ +
{{ content.textContent || "" | translate }}
+ +
+ +
diff --git a/packages/stark-ui/src/modules/dialogs/components/alert-dialog.component.spec.ts b/packages/stark-ui/src/modules/dialogs/components/alert-dialog.component.spec.ts new file mode 100644 index 0000000000..32c3b98892 --- /dev/null +++ b/packages/stark-ui/src/modules/dialogs/components/alert-dialog.component.spec.ts @@ -0,0 +1,221 @@ +/* tslint:disable:completed-docs no-big-function */ +import { async, ComponentFixture, fakeAsync, inject, TestBed, tick } from "@angular/core/testing"; +import { CommonModule } from "@angular/common"; +import { Component, ComponentFactoryResolver } from "@angular/core"; +import { MatDialog, MatDialogModule, MatDialogRef } from "@angular/material/dialog"; +import { NoopAnimationsModule } from "@angular/platform-browser/animations"; +import { BrowserDynamicTestingModule } from "@angular/platform-browser-dynamic/testing"; +import { OverlayContainer } from "@angular/cdk/overlay"; +import { ESCAPE } from "@angular/cdk/keycodes"; +import { TranslateModule } from "@ngx-translate/core"; +import { Observer } from "rxjs"; +import { StarkAlertDialogContent } from "./alert-dialog-content.intf"; +import { StarkAlertDialogComponent, StarkAlertDialogResult } from "./alert-dialog.component"; +import createSpyObj = jasmine.createSpyObj; +import SpyObj = jasmine.SpyObj; + +@Component({ + selector: `host-component`, + template: ` + no content + ` +}) +class TestHostComponent {} + +describe("AlertDialogComponent", () => { + let hostFixture: ComponentFixture; + let hostComponent: TestHostComponent; + let dialogService: MatDialog; + let overlayContainer: OverlayContainer; + let overlayContainerElement: HTMLElement; + let dialogComponentSelector: string; + + const dummyDialogContent: StarkAlertDialogContent = { + title: "This is the dialog title", + textContent: "Here goes the content", + ok: "Ok button label" + }; + + function triggerClick(element: HTMLElement): void { + element.click(); + } + + /** + * Angular Material dialogs listen to the Escape key on the keydown event + */ + function triggerKeydownEscape(element: HTMLElement): void { + // more verbose way to create and trigger an event (the only way it works in IE) + // https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Creating_and_triggering_events + const keydownEvent: Event = document.createEvent("Event"); + keydownEvent.initEvent("keydown", true, true); + keydownEvent["key"] = "Escape"; + keydownEvent["keyCode"] = ESCAPE; + element.dispatchEvent(keydownEvent); + } + + beforeEach(async(() => { + return TestBed.configureTestingModule({ + declarations: [TestHostComponent, StarkAlertDialogComponent], + imports: [CommonModule, NoopAnimationsModule, MatDialogModule, TranslateModule.forRoot()], + providers: [] + }) + .overrideModule(BrowserDynamicTestingModule, { + // add entryComponent to TestingModule (suggested in https://github.com/angular/angular/issues/12079) + set: { entryComponents: [StarkAlertDialogComponent] } + }) + .compileComponents(); + })); + + beforeEach(inject( + [MatDialog, OverlayContainer, ComponentFactoryResolver], + (d: MatDialog, oc: OverlayContainer, cfr: ComponentFactoryResolver) => { + dialogService = d; + overlayContainer = oc; + overlayContainerElement = oc.getContainerElement(); + dialogComponentSelector = cfr.resolveComponentFactory(StarkAlertDialogComponent).selector; + } + )); + + afterEach(() => { + overlayContainer.ngOnDestroy(); + }); + + beforeEach(() => { + hostFixture = TestBed.createComponent(TestHostComponent); + hostComponent = hostFixture.componentInstance; + hostFixture.detectChanges(); + }); + + it("should be correctly opened via the MatDialog service", () => { + expect(hostComponent).toBeDefined(); + + expect(dialogService).toBeDefined(); + const dialogRef: MatDialogRef = dialogService.open< + StarkAlertDialogComponent, + StarkAlertDialogContent, + StarkAlertDialogResult + >(StarkAlertDialogComponent, { + data: dummyDialogContent + }); + hostFixture.detectChanges(); + + expect(dialogRef.componentInstance instanceof StarkAlertDialogComponent).toBe(true); + + const dialogElement: HTMLElement | null = (overlayContainerElement).querySelector( + "mat-dialog-container " + dialogComponentSelector + ); + + const dialogTitleElement: HTMLElement | null = (dialogElement).querySelector("[mat-dialog-title]"); + expect(dialogTitleElement).toBeDefined(); + expect((dialogTitleElement).innerHTML).toEqual(dummyDialogContent.title); + + const dialogContentElement: HTMLElement | null = (dialogElement).querySelector("[mat-dialog-content]"); + expect(dialogContentElement).toBeDefined(); + expect((dialogContentElement).innerHTML).toBe(dummyDialogContent.textContent); + + const dialogActionsElement: HTMLElement | null = (dialogElement).querySelector("[mat-dialog-actions]"); + expect(dialogActionsElement).toBeDefined(); + const dialogButtonElements: NodeListOf = (dialogActionsElement).querySelectorAll("button"); + expect(dialogButtonElements.length).toBe(1); + expect(dialogButtonElements[0].innerHTML).toBe(dummyDialogContent.ok); + }); + + it("should return 'ok' as result when the 'Ok' button is clicked", fakeAsync(() => { + expect(dialogService).toBeDefined(); + const dialogRef: MatDialogRef = dialogService.open< + StarkAlertDialogComponent, + StarkAlertDialogContent, + StarkAlertDialogResult + >(StarkAlertDialogComponent, { + data: dummyDialogContent + }); + hostFixture.detectChanges(); + + const dialogElement: HTMLElement | null = (overlayContainerElement).querySelector( + "mat-dialog-container " + dialogComponentSelector + ); + + const mockObserver: SpyObj> = createSpyObj>("observerSpy", [ + "next", + "error", + "complete" + ]); + dialogRef.afterClosed().subscribe(mockObserver); + + const dialogActionsElement: HTMLElement | null = (dialogElement).querySelector("[mat-dialog-actions]"); + expect(dialogActionsElement).toBeDefined(); + const dialogButtonElements: NodeListOf = (dialogActionsElement).querySelectorAll("button"); + expect(dialogButtonElements.length).toBe(1); + + triggerClick(dialogButtonElements[0]); + hostFixture.detectChanges(); + + // to avoid NgZone error: "Error: 3 timer(s) still in the queue" + tick(500); // the amount of mills can be known by calling flush(): const remainingMills: number = flush(); + + expect(mockObserver.next).toHaveBeenCalledTimes(1); + expect(mockObserver.next).toHaveBeenCalledWith("ok"); + expect(mockObserver.error).not.toHaveBeenCalled(); + expect(mockObserver.complete).toHaveBeenCalled(); + })); + + it("should return undefined as result when it is cancelled by clicking outside of the dialog", fakeAsync(() => { + expect(dialogService).toBeDefined(); + const dialogRef: MatDialogRef = dialogService.open< + StarkAlertDialogComponent, + StarkAlertDialogContent, + StarkAlertDialogResult + >(StarkAlertDialogComponent, { + data: dummyDialogContent + }); + hostFixture.detectChanges(); + + const mockObserver: SpyObj> = createSpyObj>("observerSpy", [ + "next", + "error", + "complete" + ]); + dialogRef.afterClosed().subscribe(mockObserver); + + triggerClick(overlayContainerElement.querySelector(".cdk-overlay-backdrop")); // clicking on the backdrop + hostFixture.detectChanges(); + + // to avoid NgZone error: "Error: 3 timer(s) still in the queue" + tick(500); // the amount of mills can be known by calling flush(): const remainingMills: number = flush(); + + expect(mockObserver.next).toHaveBeenCalledTimes(1); + expect(mockObserver.next).toHaveBeenCalledWith(undefined); + expect(mockObserver.error).not.toHaveBeenCalled(); + expect(mockObserver.complete).toHaveBeenCalled(); + })); + + it("should return undefined as result when it is cancelled by pressing the ESC key", fakeAsync(() => { + expect(dialogService).toBeDefined(); + const dialogRef: MatDialogRef = dialogService.open< + StarkAlertDialogComponent, + StarkAlertDialogContent, + StarkAlertDialogResult + >(StarkAlertDialogComponent, { + data: dummyDialogContent + }); + hostFixture.detectChanges(); + + const mockObserver: SpyObj> = createSpyObj>("observerSpy", [ + "next", + "error", + "complete" + ]); + dialogRef.afterClosed().subscribe(mockObserver); + + triggerKeydownEscape(overlayContainerElement); // pressing Esc key in the overlay + hostFixture.detectChanges(); + + // to avoid NgZone error: "Error: 3 timer(s) still in the queue" + tick(500); // the amount of mills can be known by calling flush(): const remainingMills: number = flush(); + + expect(mockObserver.next).toHaveBeenCalledTimes(1); + expect(mockObserver.next).toHaveBeenCalledWith(undefined); + expect(mockObserver.error).not.toHaveBeenCalled(); + expect(mockObserver.complete).toHaveBeenCalled(); + })); +}); diff --git a/packages/stark-ui/src/modules/dialogs/components/alert-dialog.component.ts b/packages/stark-ui/src/modules/dialogs/components/alert-dialog.component.ts new file mode 100644 index 0000000000..6d0e19490d --- /dev/null +++ b/packages/stark-ui/src/modules/dialogs/components/alert-dialog.component.ts @@ -0,0 +1,43 @@ +import { Component, Inject, ViewEncapsulation } from "@angular/core"; +import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog"; +import { StarkAlertDialogContent } from "./alert-dialog-content.intf"; + +/** + * Possible results of the {@link StarkAlertDialogComponent} after being closed. + * + * - "ok": The user clicked on the "Ok" button + * - `undefined`: The dialog was cancelled by either clicking outside of dialog or by pressing the ESC key + */ +export type StarkAlertDialogResult = "ok" | undefined; + +/** + * Alert dialog component to be opened via the Angular Material's {@link MatDialog} service + */ +@Component({ + selector: "stark-alert-dialog", + templateUrl: "./alert-dialog.component.html", + encapsulation: ViewEncapsulation.None, + // We need to use host instead of @HostBinding: https://github.com/NationalBankBelgium/stark/issues/664 + host: { + class: "stark-alert-dialog" + } +}) +export class StarkAlertDialogComponent { + /** + * Class constructor + * @param dialogRef - Reference this dialog instance + * @param content - Content to be shown in the alert dialog (dynamically translated via the Translate service if the + * provided text is defined in the translation keys) + */ + public constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public content: StarkAlertDialogContent + ) {} + + /** + * Callback method to be triggered when the "Ok" button is clicked + */ + public onOk(): void { + this.dialogRef.close("ok"); + } +} diff --git a/packages/stark-ui/src/modules/dialogs/components/confirm-dialog-content.intf.ts b/packages/stark-ui/src/modules/dialogs/components/confirm-dialog-content.intf.ts new file mode 100644 index 0000000000..3f1948da2f --- /dev/null +++ b/packages/stark-ui/src/modules/dialogs/components/confirm-dialog-content.intf.ts @@ -0,0 +1,11 @@ +import { StarkBaseDialogContent } from "./dialog-content.intf"; + +/** + * Content that can be shown in the {@link StarkConfirmDialogComponent} + */ +export interface StarkConfirmDialogContent extends StarkBaseDialogContent { + /** + * Label to be set in the "Cancel" button. + */ + cancel?: string; +} diff --git a/packages/stark-ui/src/modules/dialogs/components/confirm-dialog-theme.scss b/packages/stark-ui/src/modules/dialogs/components/confirm-dialog-theme.scss new file mode 100644 index 0000000000..121cbb96e5 --- /dev/null +++ b/packages/stark-ui/src/modules/dialogs/components/confirm-dialog-theme.scss @@ -0,0 +1,6 @@ +.stark-confirm-dialog { + .button-ok, + .button-cancel { + color: mat-color($primary-palette, 500); + } +} diff --git a/packages/stark-ui/src/modules/dialogs/components/confirm-dialog.component.html b/packages/stark-ui/src/modules/dialogs/components/confirm-dialog.component.html new file mode 100644 index 0000000000..610b0ebf1a --- /dev/null +++ b/packages/stark-ui/src/modules/dialogs/components/confirm-dialog.component.html @@ -0,0 +1,8 @@ +

{{ content.title || "" | translate }}

+ +
{{ content.textContent || "" | translate }}
+ +
+ + +
diff --git a/packages/stark-ui/src/modules/dialogs/components/confirm-dialog.component.spec.ts b/packages/stark-ui/src/modules/dialogs/components/confirm-dialog.component.spec.ts new file mode 100644 index 0000000000..28fdffc14c --- /dev/null +++ b/packages/stark-ui/src/modules/dialogs/components/confirm-dialog.component.spec.ts @@ -0,0 +1,270 @@ +/* tslint:disable:completed-docs no-big-function */ +import { async, ComponentFixture, fakeAsync, inject, TestBed, tick } from "@angular/core/testing"; +import { CommonModule } from "@angular/common"; +import { Component, ComponentFactoryResolver } from "@angular/core"; +import { MatDialog, MatDialogModule, MatDialogRef } from "@angular/material/dialog"; +import { NoopAnimationsModule } from "@angular/platform-browser/animations"; +import { BrowserDynamicTestingModule } from "@angular/platform-browser-dynamic/testing"; +import { OverlayContainer } from "@angular/cdk/overlay"; +import { ESCAPE } from "@angular/cdk/keycodes"; +import { TranslateModule } from "@ngx-translate/core"; +import { Observer } from "rxjs"; +import { StarkConfirmDialogContent } from "./confirm-dialog-content.intf"; +import { StarkConfirmDialogComponent, StarkConfirmDialogResult } from "./confirm-dialog.component"; +import createSpyObj = jasmine.createSpyObj; +import SpyObj = jasmine.SpyObj; + +@Component({ + selector: `host-component`, + template: ` + no content + ` +}) +class TestHostComponent {} + +describe("ConfirmDialogComponent", () => { + let hostFixture: ComponentFixture; + let hostComponent: TestHostComponent; + let dialogService: MatDialog; + let overlayContainer: OverlayContainer; + let overlayContainerElement: HTMLElement; + let dialogComponentSelector: string; + + const dummyDialogContent: StarkConfirmDialogContent = { + title: "This is the dialog title", + textContent: "Here goes the content", + ok: "Ok button label", + cancel: "Cancel button label" + }; + + function triggerClick(element: HTMLElement): void { + element.click(); + } + + /** + * Angular Material dialogs listen to the Escape key on the keydown event + */ + function triggerKeydownEscape(element: HTMLElement): void { + // more verbose way to create and trigger an event (the only way it works in IE) + // https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Creating_and_triggering_events + const keydownEvent: Event = document.createEvent("Event"); + keydownEvent.initEvent("keydown", true, true); + keydownEvent["key"] = "Escape"; + keydownEvent["keyCode"] = ESCAPE; + element.dispatchEvent(keydownEvent); + } + + beforeEach(async(() => { + return TestBed.configureTestingModule({ + declarations: [TestHostComponent, StarkConfirmDialogComponent], + imports: [CommonModule, NoopAnimationsModule, MatDialogModule, TranslateModule.forRoot()], + providers: [] + }) + .overrideModule(BrowserDynamicTestingModule, { + // add entryComponent to TestingModule (suggested in https://github.com/angular/angular/issues/12079) + set: { entryComponents: [StarkConfirmDialogComponent] } + }) + .compileComponents(); + })); + + beforeEach(inject( + [MatDialog, OverlayContainer, ComponentFactoryResolver], + (d: MatDialog, oc: OverlayContainer, cfr: ComponentFactoryResolver) => { + dialogService = d; + overlayContainer = oc; + overlayContainerElement = oc.getContainerElement(); + dialogComponentSelector = cfr.resolveComponentFactory(StarkConfirmDialogComponent).selector; + } + )); + + afterEach(() => { + overlayContainer.ngOnDestroy(); + }); + + beforeEach(() => { + hostFixture = TestBed.createComponent(TestHostComponent); + hostComponent = hostFixture.componentInstance; + hostFixture.detectChanges(); + }); + + it("should be correctly opened via the MatDialog service", () => { + expect(hostComponent).toBeDefined(); + + expect(dialogService).toBeDefined(); + const dialogRef: MatDialogRef = dialogService.open< + StarkConfirmDialogComponent, + StarkConfirmDialogContent, + StarkConfirmDialogResult + >(StarkConfirmDialogComponent, { + data: dummyDialogContent + }); + hostFixture.detectChanges(); + + expect(dialogRef.componentInstance instanceof StarkConfirmDialogComponent).toBe(true); + + const dialogElement: HTMLElement | null = (overlayContainerElement).querySelector( + "mat-dialog-container " + dialogComponentSelector + ); + + const dialogTitleElement: HTMLElement | null = (dialogElement).querySelector("[mat-dialog-title]"); + expect(dialogTitleElement).toBeDefined(); + expect((dialogTitleElement).innerHTML).toEqual(dummyDialogContent.title); + + const dialogContentElement: HTMLElement | null = (dialogElement).querySelector( + "[mat-dialog-content]" + ); + expect(dialogContentElement).toBeDefined(); + expect((dialogContentElement).innerHTML).toBe(dummyDialogContent.textContent); + + const dialogActionsElement: HTMLElement | null = (dialogElement).querySelector( + "[mat-dialog-actions]" + ); + expect(dialogActionsElement).toBeDefined(); + const dialogButtonElements: NodeListOf = (dialogActionsElement).querySelectorAll("button"); + expect(dialogButtonElements.length).toBe(2); + expect(dialogButtonElements[0].innerHTML).toBe(dummyDialogContent.cancel); + expect(dialogButtonElements[1].innerHTML).toBe(dummyDialogContent.ok); + }); + + it("should return 'ok' as result when the 'Ok' button is clicked", fakeAsync(() => { + expect(dialogService).toBeDefined(); + const dialogRef: MatDialogRef = dialogService.open< + StarkConfirmDialogComponent, + StarkConfirmDialogContent, + StarkConfirmDialogResult + >(StarkConfirmDialogComponent, { + data: dummyDialogContent + }); + hostFixture.detectChanges(); + + const dialogElement: HTMLElement | null = (overlayContainerElement).querySelector( + "mat-dialog-container " + dialogComponentSelector + ); + + const mockObserver: SpyObj> = createSpyObj>("observerSpy", [ + "next", + "error", + "complete" + ]); + dialogRef.afterClosed().subscribe(mockObserver); + + const dialogActionsElement: HTMLElement | null = (dialogElement).querySelector( + "[mat-dialog-actions]" + ); + expect(dialogActionsElement).toBeDefined(); + const dialogButtonElements: NodeListOf = (dialogActionsElement).querySelectorAll("button"); + expect(dialogButtonElements.length).toBe(2); + + triggerClick(dialogButtonElements[1]); // clicking the "ok" button + hostFixture.detectChanges(); + + // to avoid NgZone error: "Error: 3 timer(s) still in the queue" + tick(500); // the amount of mills can be known by calling flush(): const remainingMills: number = flush(); + + expect(mockObserver.next).toHaveBeenCalledTimes(1); + expect(mockObserver.next).toHaveBeenCalledWith("ok"); + expect(mockObserver.error).not.toHaveBeenCalled(); + expect(mockObserver.complete).toHaveBeenCalled(); + })); + + it("should return 'cancel' as result when the 'Cancel' button is clicked", fakeAsync(() => { + expect(dialogService).toBeDefined(); + const dialogRef: MatDialogRef = dialogService.open< + StarkConfirmDialogComponent, + StarkConfirmDialogContent, + StarkConfirmDialogResult + >(StarkConfirmDialogComponent, { + data: dummyDialogContent + }); + hostFixture.detectChanges(); + + const dialogElement: HTMLElement | null = (overlayContainerElement).querySelector( + "mat-dialog-container " + dialogComponentSelector + ); + + const mockObserver: SpyObj> = createSpyObj>("observerSpy", [ + "next", + "error", + "complete" + ]); + dialogRef.afterClosed().subscribe(mockObserver); + + const dialogActionsElement: HTMLElement | null = (dialogElement).querySelector( + "[mat-dialog-actions]" + ); + expect(dialogActionsElement).toBeDefined(); + const dialogButtonElements: NodeListOf = (dialogActionsElement).querySelectorAll("button"); + expect(dialogButtonElements.length).toBe(2); + + triggerClick(dialogButtonElements[0]); // clicking the "cancel" button + hostFixture.detectChanges(); + + // to avoid NgZone error: "Error: 3 timer(s) still in the queue" + tick(500); // the amount of mills can be known by calling flush(): const remainingMills: number = flush(); + + expect(mockObserver.next).toHaveBeenCalledTimes(1); + expect(mockObserver.next).toHaveBeenCalledWith("cancel"); + expect(mockObserver.error).not.toHaveBeenCalled(); + expect(mockObserver.complete).toHaveBeenCalled(); + })); + + it("should return undefined as result when it is cancelled by clicking outside of the dialog", fakeAsync(() => { + expect(dialogService).toBeDefined(); + const dialogRef: MatDialogRef = dialogService.open< + StarkConfirmDialogComponent, + StarkConfirmDialogContent, + StarkConfirmDialogResult + >(StarkConfirmDialogComponent, { + data: dummyDialogContent + }); + hostFixture.detectChanges(); + + const mockObserver: SpyObj> = createSpyObj>("observerSpy", [ + "next", + "error", + "complete" + ]); + dialogRef.afterClosed().subscribe(mockObserver); + + triggerClick(overlayContainerElement.querySelector(".cdk-overlay-backdrop")); // clicking on the backdrop + hostFixture.detectChanges(); + + // to avoid NgZone error: "Error: 3 timer(s) still in the queue" + tick(500); // the amount of mills can be known by calling flush(): const remainingMills: number = flush(); + + expect(mockObserver.next).toHaveBeenCalledTimes(1); + expect(mockObserver.next).toHaveBeenCalledWith(undefined); + expect(mockObserver.error).not.toHaveBeenCalled(); + expect(mockObserver.complete).toHaveBeenCalled(); + })); + + it("should return undefined as result when it is cancelled by pressing the ESC key", fakeAsync(() => { + expect(dialogService).toBeDefined(); + const dialogRef: MatDialogRef = dialogService.open< + StarkConfirmDialogComponent, + StarkConfirmDialogContent, + StarkConfirmDialogResult + >(StarkConfirmDialogComponent, { + data: dummyDialogContent + }); + hostFixture.detectChanges(); + + const mockObserver: SpyObj> = createSpyObj>("observerSpy", [ + "next", + "error", + "complete" + ]); + dialogRef.afterClosed().subscribe(mockObserver); + + triggerKeydownEscape(overlayContainerElement); // pressing Esc key in the overlay + hostFixture.detectChanges(); + + // to avoid NgZone error: "Error: 3 timer(s) still in the queue" + tick(500); // the amount of mills can be known by calling flush(): const remainingMills: number = flush(); + + expect(mockObserver.next).toHaveBeenCalledTimes(1); + expect(mockObserver.next).toHaveBeenCalledWith(undefined); + expect(mockObserver.error).not.toHaveBeenCalled(); + expect(mockObserver.complete).toHaveBeenCalled(); + })); +}); diff --git a/packages/stark-ui/src/modules/dialogs/components/confirm-dialog.component.ts b/packages/stark-ui/src/modules/dialogs/components/confirm-dialog.component.ts new file mode 100644 index 0000000000..3350d8d92d --- /dev/null +++ b/packages/stark-ui/src/modules/dialogs/components/confirm-dialog.component.ts @@ -0,0 +1,51 @@ +import { Component, Inject, ViewEncapsulation } from "@angular/core"; +import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog"; +import { StarkConfirmDialogContent } from "./confirm-dialog-content.intf"; + +/** + * Possible results of the {@link StarkConfirmDialogComponent} after being closed. + * + * - "ok": The user clicked on the "Ok" button + * - "cancel": The dialog was cancelled by clicking on the "Cancel" button + * - `undefined`: The dialog was cancelled either by clicking outside of dialog or by pressing the ESC key + */ +export type StarkConfirmDialogResult = "ok" | "cancel" | undefined; + +/** + * Confirmation dialog component to be opened via the Angular Material's {@link MatDialog} service + */ +@Component({ + selector: "stark-confirm-dialog", + templateUrl: "./confirm-dialog.component.html", + encapsulation: ViewEncapsulation.None, + // We need to use host instead of @HostBinding: https://github.com/NationalBankBelgium/stark/issues/664 + host: { + class: "stark-confirm-dialog" + } +}) +export class StarkConfirmDialogComponent { + /** + * Class constructor + * @param dialogRef - Reference this dialog instance + * @param content - Content to be shown in the confirmation dialog (dynamically translated via the [@link TranslateService} service if the + * provided text is defined in the translation keys) + */ + public constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public content: StarkConfirmDialogContent + ) {} + + /** + * Callback method to be triggered when the "Cancel" button is clicked + */ + public onCancel(): void { + this.dialogRef.close("cancel"); + } + + /** + * Callback method to be triggered when the "Ok" button is clicked + */ + public onOk(): void { + this.dialogRef.close("ok"); + } +} diff --git a/packages/stark-ui/src/modules/dialogs/components/dialog-content.intf.ts b/packages/stark-ui/src/modules/dialogs/components/dialog-content.intf.ts new file mode 100644 index 0000000000..1e89efc3ca --- /dev/null +++ b/packages/stark-ui/src/modules/dialogs/components/dialog-content.intf.ts @@ -0,0 +1,20 @@ +/** + * Describes the basic content that can be shown in a predefined Dialog component from Stark. + * See: {@link StarkAlertDialogComponent}, {@link StarkConfirmDialogComponent}, {@link StarkPromptDialogComponent} + */ +export interface StarkBaseDialogContent { + /** + * Dialog's title. + */ + title?: string; + + /** + * Dialog's simple text content. + */ + textContent?: string; + + /** + * Label to be set in the "Ok" button. + */ + ok?: string; +} diff --git a/packages/stark-ui/src/modules/dialogs/components/prompt-dialog-content.intf.ts b/packages/stark-ui/src/modules/dialogs/components/prompt-dialog-content.intf.ts new file mode 100644 index 0000000000..f1e0aef5e8 --- /dev/null +++ b/packages/stark-ui/src/modules/dialogs/components/prompt-dialog-content.intf.ts @@ -0,0 +1,26 @@ +import { StarkBaseDialogContent } from "./dialog-content.intf"; + +/** + * Content that can be shown in the {@link StarkPromptDialogComponent} + */ +export interface StarkPromptDialogContent extends StarkBaseDialogContent { + /** + * Text to be shown as label of the prompt input. + */ + label?: string; + + /** + * Placeholder text of the prompt input. + */ + placeholder?: string; + + /** + * Initial value to be set to the prompt input. + */ + initialValue?: string; + + /** + * Label to be set in the "Cancel" button. + */ + cancel?: string; +} diff --git a/packages/stark-ui/src/modules/dialogs/components/prompt-dialog-theme.scss b/packages/stark-ui/src/modules/dialogs/components/prompt-dialog-theme.scss new file mode 100644 index 0000000000..d17faaf2f4 --- /dev/null +++ b/packages/stark-ui/src/modules/dialogs/components/prompt-dialog-theme.scss @@ -0,0 +1,6 @@ +.stark-prompt-dialog { + .button-ok, + .button-cancel { + color: mat-color($primary-palette, 500); + } +} diff --git a/packages/stark-ui/src/modules/dialogs/components/prompt-dialog.component.html b/packages/stark-ui/src/modules/dialogs/components/prompt-dialog.component.html new file mode 100644 index 0000000000..b90284360e --- /dev/null +++ b/packages/stark-ui/src/modules/dialogs/components/prompt-dialog.component.html @@ -0,0 +1,15 @@ +

{{ content.title || "" | translate }}

+ +
+ {{ content.textContent || "" | translate }} + + + {{ content.label || content.placeholder }} + + +
+ +
+ + +
diff --git a/packages/stark-ui/src/modules/dialogs/components/prompt-dialog.component.scss b/packages/stark-ui/src/modules/dialogs/components/prompt-dialog.component.scss new file mode 100644 index 0000000000..309eed0c4a --- /dev/null +++ b/packages/stark-ui/src/modules/dialogs/components/prompt-dialog.component.scss @@ -0,0 +1,6 @@ +.stark-prompt-dialog { + .mat-form-field.prompt-text { + display: block; + margin: 16px 0; + } +} diff --git a/packages/stark-ui/src/modules/dialogs/components/prompt-dialog.component.spec.ts b/packages/stark-ui/src/modules/dialogs/components/prompt-dialog.component.spec.ts new file mode 100644 index 0000000000..535bc6d33f --- /dev/null +++ b/packages/stark-ui/src/modules/dialogs/components/prompt-dialog.component.spec.ts @@ -0,0 +1,268 @@ +/* tslint:disable:completed-docs no-big-function */ +import { async, ComponentFixture, fakeAsync, inject, TestBed, tick } from "@angular/core/testing"; +import { CommonModule } from "@angular/common"; +import { Component, ComponentFactoryResolver } from "@angular/core"; +import { ReactiveFormsModule } from "@angular/forms"; +import { MatDialog, MatDialogModule, MatDialogRef } from "@angular/material/dialog"; +import { MatInputModule } from "@angular/material/input"; +import { NoopAnimationsModule } from "@angular/platform-browser/animations"; +import { BrowserDynamicTestingModule } from "@angular/platform-browser-dynamic/testing"; +import { OverlayContainer } from "@angular/cdk/overlay"; +import { ESCAPE } from "@angular/cdk/keycodes"; +import { TranslateModule } from "@ngx-translate/core"; +import { Observer } from "rxjs"; +import { StarkPromptDialogContent } from "./prompt-dialog-content.intf"; +import { StarkPromptDialogComponent, StarkPromptDialogResult } from "./prompt-dialog.component"; +import createSpyObj = jasmine.createSpyObj; +import SpyObj = jasmine.SpyObj; + +@Component({ + selector: `host-component`, + template: ` + no content + ` +}) +class TestHostComponent {} + +describe("PromptDialogComponent", () => { + let hostFixture: ComponentFixture; + let hostComponent: TestHostComponent; + let dialogService: MatDialog; + let overlayContainer: OverlayContainer; + let overlayContainerElement: HTMLElement; + let dialogComponentSelector: string; + + const dummyDialogContent: StarkPromptDialogContent = { + title: "This is the dialog title", + textContent: "Here goes the content", + label: "The input's label", + placeholder: "The input's placeholder", + initialValue: "The input's initial value", + ok: "Ok button label", + cancel: "Cancel button label" + }; + + function triggerClick(element: HTMLElement): void { + element.click(); + } + + /** + * Angular Material dialogs listen to the Escape key on the keydown event + */ + function triggerKeydownEscape(element: HTMLElement): void { + // more verbose way to create and trigger an event (the only way it works in IE) + // https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Creating_and_triggering_events + const keydownEvent: Event = document.createEvent("Event"); + keydownEvent.initEvent("keydown", true, true); + keydownEvent["key"] = "Escape"; + keydownEvent["keyCode"] = ESCAPE; + element.dispatchEvent(keydownEvent); + } + + beforeEach(async(() => { + return TestBed.configureTestingModule({ + declarations: [TestHostComponent, StarkPromptDialogComponent], + imports: [CommonModule, ReactiveFormsModule, NoopAnimationsModule, MatInputModule, MatDialogModule, TranslateModule.forRoot()], + providers: [] + }) + .overrideModule(BrowserDynamicTestingModule, { + // add entryComponent to TestingModule (suggested in https://github.com/angular/angular/issues/12079) + set: { entryComponents: [StarkPromptDialogComponent] } + }) + .compileComponents(); + })); + + beforeEach(inject( + [MatDialog, OverlayContainer, ComponentFactoryResolver], + (d: MatDialog, oc: OverlayContainer, cfr: ComponentFactoryResolver) => { + dialogService = d; + overlayContainer = oc; + overlayContainerElement = oc.getContainerElement(); + dialogComponentSelector = cfr.resolveComponentFactory(StarkPromptDialogComponent).selector; + } + )); + + afterEach(() => { + overlayContainer.ngOnDestroy(); + }); + + beforeEach(() => { + hostFixture = TestBed.createComponent(TestHostComponent); + hostComponent = hostFixture.componentInstance; + hostFixture.detectChanges(); + }); + + it("should be correctly opened via the MatDialog service", () => { + expect(hostComponent).toBeDefined(); + + expect(dialogService).toBeDefined(); + const dialogRef: MatDialogRef = dialogService.open< + StarkPromptDialogComponent, + StarkPromptDialogContent, + StarkPromptDialogResult + >(StarkPromptDialogComponent, { + data: dummyDialogContent + }); + hostFixture.detectChanges(); + + expect(dialogRef.componentInstance instanceof StarkPromptDialogComponent).toBe(true); + + const dialogElement: HTMLElement | null = (overlayContainerElement).querySelector( + "mat-dialog-container " + dialogComponentSelector + ); + + const dialogTitleElement: HTMLElement | null = (dialogElement).querySelector("[mat-dialog-title]"); + expect(dialogTitleElement).toBeDefined(); + expect((dialogTitleElement).innerHTML).toEqual(dummyDialogContent.title); + + const dialogContentElement: HTMLElement | null = (dialogElement).querySelector("[mat-dialog-content]"); + expect(dialogContentElement).toBeDefined(); + expect((dialogContentElement).innerHTML).toContain(dummyDialogContent.textContent); + + const dialogActionsElement: HTMLElement | null = (dialogElement).querySelector("[mat-dialog-actions]"); + expect(dialogActionsElement).toBeDefined(); + const dialogButtonElements: NodeListOf = (dialogActionsElement).querySelectorAll("button"); + expect(dialogButtonElements.length).toBe(2); + expect(dialogButtonElements[0].innerHTML).toBe(dummyDialogContent.cancel); + expect(dialogButtonElements[1].innerHTML).toBe(dummyDialogContent.ok); + }); + + // TODO: adapt this test to assert that the returned value is the value typed by the user + it("should return 'ok' as result when the 'Ok' button is clicked", fakeAsync(() => { + expect(dialogService).toBeDefined(); + const dialogRef: MatDialogRef = dialogService.open< + StarkPromptDialogComponent, + StarkPromptDialogContent, + StarkPromptDialogResult + >(StarkPromptDialogComponent, { + data: dummyDialogContent + }); + hostFixture.detectChanges(); + + const dialogElement: HTMLElement | null = (overlayContainerElement).querySelector( + "mat-dialog-container " + dialogComponentSelector + ); + + const mockObserver: SpyObj> = createSpyObj>("observerSpy", [ + "next", + "error", + "complete" + ]); + dialogRef.afterClosed().subscribe(mockObserver); + + const dialogActionsElement: HTMLElement | null = (dialogElement).querySelector("[mat-dialog-actions]"); + expect(dialogActionsElement).toBeDefined(); + const dialogButtonElements: NodeListOf = (dialogActionsElement).querySelectorAll("button"); + expect(dialogButtonElements.length).toBe(2); + + triggerClick(dialogButtonElements[1]); // clicking the "ok" button + hostFixture.detectChanges(); + + // to avoid NgZone error: "Error: 3 timer(s) still in the queue" + tick(500); // the amount of mills can be known by calling flush(): const remainingMills: number = flush(); + + expect(mockObserver.next).toHaveBeenCalledTimes(1); + expect(mockObserver.next).toHaveBeenCalledWith(dummyDialogContent.initialValue); + expect(mockObserver.error).not.toHaveBeenCalled(); + expect(mockObserver.complete).toHaveBeenCalled(); + })); + + it("should return 'cancel' as result when the 'Cancel' button is clicked", fakeAsync(() => { + expect(dialogService).toBeDefined(); + const dialogRef: MatDialogRef = dialogService.open< + StarkPromptDialogComponent, + StarkPromptDialogContent, + StarkPromptDialogResult + >(StarkPromptDialogComponent, { + data: dummyDialogContent + }); + hostFixture.detectChanges(); + + const dialogElement: HTMLElement | null = (overlayContainerElement).querySelector( + "mat-dialog-container " + dialogComponentSelector + ); + + const mockObserver: SpyObj> = createSpyObj>("observerSpy", [ + "next", + "error", + "complete" + ]); + dialogRef.afterClosed().subscribe(mockObserver); + + const dialogActionsElement: HTMLElement | null = (dialogElement).querySelector("[mat-dialog-actions]"); + expect(dialogActionsElement).toBeDefined(); + const dialogButtonElements: NodeListOf = (dialogActionsElement).querySelectorAll("button"); + expect(dialogButtonElements.length).toBe(2); + + triggerClick(dialogButtonElements[0]); // clicking the "cancel" button + hostFixture.detectChanges(); + + // to avoid NgZone error: "Error: 3 timer(s) still in the queue" + tick(500); // the amount of mills can be known by calling flush(): const remainingMills: number = flush(); + + expect(mockObserver.next).toHaveBeenCalledTimes(1); + expect(mockObserver.next).toHaveBeenCalledWith("cancel"); + expect(mockObserver.error).not.toHaveBeenCalled(); + expect(mockObserver.complete).toHaveBeenCalled(); + })); + + it("should return undefined as result when it is cancelled by clicking outside of the dialog", fakeAsync(() => { + expect(dialogService).toBeDefined(); + const dialogRef: MatDialogRef = dialogService.open< + StarkPromptDialogComponent, + StarkPromptDialogContent, + StarkPromptDialogResult + >(StarkPromptDialogComponent, { + data: dummyDialogContent + }); + hostFixture.detectChanges(); + + const mockObserver: SpyObj> = createSpyObj>("observerSpy", [ + "next", + "error", + "complete" + ]); + dialogRef.afterClosed().subscribe(mockObserver); + + triggerClick(overlayContainerElement.querySelector(".cdk-overlay-backdrop")); // clicking on the backdrop + hostFixture.detectChanges(); + + // to avoid NgZone error: "Error: 3 timer(s) still in the queue" + tick(500); // the amount of mills can be known by calling flush(): const remainingMills: number = flush(); + + expect(mockObserver.next).toHaveBeenCalledTimes(1); + expect(mockObserver.next).toHaveBeenCalledWith(undefined); + expect(mockObserver.error).not.toHaveBeenCalled(); + expect(mockObserver.complete).toHaveBeenCalled(); + })); + + it("should return undefined as result when it is cancelled by pressing the ESC key", fakeAsync(() => { + expect(dialogService).toBeDefined(); + const dialogRef: MatDialogRef = dialogService.open< + StarkPromptDialogComponent, + StarkPromptDialogContent, + StarkPromptDialogResult + >(StarkPromptDialogComponent, { + data: dummyDialogContent + }); + hostFixture.detectChanges(); + + const mockObserver: SpyObj> = createSpyObj>("observerSpy", [ + "next", + "error", + "complete" + ]); + dialogRef.afterClosed().subscribe(mockObserver); + + triggerKeydownEscape(overlayContainerElement); // pressing Esc key in the overlay + hostFixture.detectChanges(); + + // to avoid NgZone error: "Error: 3 timer(s) still in the queue" + tick(500); // the amount of mills can be known by calling flush(): const remainingMills: number = flush(); + + expect(mockObserver.next).toHaveBeenCalledTimes(1); + expect(mockObserver.next).toHaveBeenCalledWith(undefined); + expect(mockObserver.error).not.toHaveBeenCalled(); + expect(mockObserver.complete).toHaveBeenCalled(); + })); +}); diff --git a/packages/stark-ui/src/modules/dialogs/components/prompt-dialog.component.ts b/packages/stark-ui/src/modules/dialogs/components/prompt-dialog.component.ts new file mode 100644 index 0000000000..9aeef09895 --- /dev/null +++ b/packages/stark-ui/src/modules/dialogs/components/prompt-dialog.component.ts @@ -0,0 +1,59 @@ +import { Component, Inject, ViewEncapsulation } from "@angular/core"; +import { FormControl } from "@angular/forms"; +import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog"; +import { StarkPromptDialogContent } from "./prompt-dialog-content.intf"; + +/** + * Possible results of the {@link StarkPromptDialogComponent} after being closed. + * + * - `string`: The value provided by the user in the dialog's input after clikcing the "Ok" button. + * - "cancel": The dialog was cancelled by clicking on the "Cancel" button + * - `undefined`: The dialog was cancelled either by clicking outside of dialog or by pressing the ESC key + */ +export type StarkPromptDialogResult = string | "cancel" | undefined; + +/** + * Prompt dialog component to be opened via the Angular Material's {@link MatDialog} service + */ +@Component({ + selector: "stark-prompt-dialog", + templateUrl: "./prompt-dialog.component.html", + encapsulation: ViewEncapsulation.None, + // We need to use host instead of @HostBinding: https://github.com/NationalBankBelgium/stark/issues/664 + host: { + class: "stark-prompt-dialog" + } +}) +export class StarkPromptDialogComponent { + /** + * @ignore + */ + public formControl: FormControl; + + /** + * Class constructor + * @param dialogRef - Reference this dialog instance + * @param content - Content to be shown in the prompt dialog (dynamically translated via the [@link TranslateService} service if the + * provided text is defined in the translation keys) + */ + public constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public content: StarkPromptDialogContent + ) { + this.formControl = new FormControl(this.content.initialValue); + } + + /** + * Callback method to be triggered when the "Cancel" button is clicked + */ + public onCancel(): void { + this.dialogRef.close("cancel"); + } + + /** + * Callback method to be triggered when the "Ok" button is clicked + */ + public onOk(): void { + this.dialogRef.close(this.formControl.value); + } +} diff --git a/packages/stark-ui/src/modules/dialogs/dialogs.module.ts b/packages/stark-ui/src/modules/dialogs/dialogs.module.ts new file mode 100644 index 0000000000..6828091c7b --- /dev/null +++ b/packages/stark-ui/src/modules/dialogs/dialogs.module.ts @@ -0,0 +1,15 @@ +import { NgModule } from "@angular/core"; +import { FormsModule, ReactiveFormsModule } from "@angular/forms"; +import { MatButtonModule } from "@angular/material/button"; +import { MatDialogModule } from "@angular/material/dialog"; +import { TranslateModule } from "@ngx-translate/core"; +import { StarkAlertDialogComponent, StarkConfirmDialogComponent, StarkPromptDialogComponent } from "./components"; +import { MatInputModule } from "@angular/material/input"; + +@NgModule({ + declarations: [StarkAlertDialogComponent, StarkConfirmDialogComponent, StarkPromptDialogComponent], + imports: [FormsModule, ReactiveFormsModule, MatButtonModule, MatDialogModule, MatInputModule, TranslateModule], + exports: [StarkAlertDialogComponent, StarkConfirmDialogComponent, StarkPromptDialogComponent], + entryComponents: [StarkAlertDialogComponent, StarkConfirmDialogComponent, StarkPromptDialogComponent] +}) +export class StarkDialogsModule {} diff --git a/showcase/src/app/app-menu.config.ts b/showcase/src/app/app-menu.config.ts index 92e2cb3e60..97bc5d6a50 100644 --- a/showcase/src/app/app-menu.config.ts +++ b/showcase/src/app/app-menu.config.ts @@ -108,6 +108,13 @@ export const APP_MENU_CONFIG: StarkMenuConfig = { isEnabled: true, targetState: "demo-ui.date-range-picker" }, + { + id: "menu-stark-ui-components-dialogs", + label: "SHOWCASE.DEMO.DIALOGS.TITLE", + isVisible: true, + isEnabled: true, + targetState: "demo-ui.dialogs" + }, { id: "menu-stark-ui-components-dropdown", label: "SHOWCASE.DEMO.DROPDOWN.TITLE", diff --git a/showcase/src/app/demo-ui/demo-ui.module.ts b/showcase/src/app/demo-ui/demo-ui.module.ts index 924adfa9f4..cb1fc9fc4e 100644 --- a/showcase/src/app/demo-ui/demo-ui.module.ts +++ b/showcase/src/app/demo-ui/demo-ui.module.ts @@ -29,6 +29,7 @@ import { StarkCollapsibleModule, StarkDatePickerModule, StarkDateRangePickerModule, + StarkDialogsModule, StarkDropdownModule, StarkGenericSearchModule, StarkInputMaskDirectivesModule, @@ -51,6 +52,7 @@ import { DemoCollapsiblePageComponent, DemoDatePickerPageComponent, DemoDateRangePickerPageComponent, + DemoDialogsPageComponent, DemoDropdownPageComponent, DemoFooterPageComponent, DemoGenericSearchFormComponent, @@ -80,10 +82,10 @@ import { TableRegularComponent, TableWithCustomActionsComponent, TableWithCustomStylingComponent, + TableWithFixedActionsComponent, TableWithFixedHeaderComponent, TableWithSelectionComponent, - TableWithTranscludedActionBarComponent, - TableWithFixedActionsComponent + TableWithTranscludedActionBarComponent } from "./components"; @NgModule({ @@ -119,6 +121,7 @@ import { StarkCollapsibleModule, StarkDatePickerModule, StarkDateRangePickerModule, + StarkDialogsModule, StarkDropdownModule, StarkGenericSearchModule, StarkInputMaskDirectivesModule, @@ -142,6 +145,7 @@ import { DemoCollapsiblePageComponent, DemoDatePickerPageComponent, DemoDateRangePickerPageComponent, + DemoDialogsPageComponent, DemoDropdownPageComponent, DemoFooterPageComponent, DemoGenericSearchPageComponent, @@ -177,6 +181,7 @@ import { DemoCollapsiblePageComponent, DemoDatePickerPageComponent, DemoDateRangePickerPageComponent, + DemoDialogsPageComponent, DemoDropdownPageComponent, DemoFooterPageComponent, DemoGenericSearchPageComponent, diff --git a/showcase/src/app/demo-ui/pages/dialogs/dialogs-page.component.html b/showcase/src/app/demo-ui/pages/dialogs/dialogs-page.component.html new file mode 100644 index 0000000000..e08f2e7939 --- /dev/null +++ b/showcase/src/app/demo-ui/pages/dialogs/dialogs-page.component.html @@ -0,0 +1,24 @@ +

SHOWCASE.DEMO.DIALOGS.TITLE

+
+

SHOWCASE.DEMO.SHARED.EXAMPLE_VIEWER_LIST

+ +
+ + + +
+ +
+ {{ dialogStatus | translate: { result: dialogResult } }} +
+
+
+ diff --git a/showcase/src/app/demo-ui/pages/dialogs/dialogs-page.component.scss b/showcase/src/app/demo-ui/pages/dialogs/dialogs-page.component.scss new file mode 100644 index 0000000000..e22251a393 --- /dev/null +++ b/showcase/src/app/demo-ui/pages/dialogs/dialogs-page.component.scss @@ -0,0 +1,16 @@ +.dialog-demo-content { + //display: flex; + //flex-direction: row; + //justify-content: space-between; + display: block; + text-align: center; + + button { + margin: 10px; + } +} + +.demo-prompt-dialog-status { + text-align: center; + padding: 16px 0; +} diff --git a/showcase/src/app/demo-ui/pages/dialogs/dialogs-page.component.ts b/showcase/src/app/demo-ui/pages/dialogs/dialogs-page.component.ts new file mode 100644 index 0000000000..6c03baed17 --- /dev/null +++ b/showcase/src/app/demo-ui/pages/dialogs/dialogs-page.component.ts @@ -0,0 +1,110 @@ +import { Component, Inject } from "@angular/core"; +import { MatDialog } from "@angular/material/dialog"; +import { STARK_LOGGING_SERVICE, StarkLoggingService } from "@nationalbankbelgium/stark-core"; +import { + StarkAlertDialogComponent, + StarkAlertDialogContent, + StarkAlertDialogResult, + StarkConfirmDialogComponent, + StarkConfirmDialogContent, + StarkConfirmDialogResult, + StarkPromptDialogComponent, + StarkPromptDialogContent, + StarkPromptDialogResult +} from "@nationalbankbelgium/stark-ui"; +import { ReferenceLink } from "../../../shared/components/reference-block"; + +@Component({ + selector: "demo-dialogs", + templateUrl: "./dialogs-page.component.html", + styleUrls: ["./dialogs-page.component.scss"] +}) +export class DemoDialogsPageComponent { + public referenceList: ReferenceLink[]; + public dialogStatus: string; + public dialogResult: any = ""; + + public constructor(@Inject(STARK_LOGGING_SERVICE) public logger: StarkLoggingService, public dialogService: MatDialog) { + this.referenceList = [ + { + label: "Stark Alert Dialog component", + url: "blabla" + }, + { + label: "Stark Confirm Dialog component", + url: "blabla" + }, + { + label: "Stark Prompt Dialog component", + url: "blabla" + } + ]; + } + + public showAlert(): void { + this.dialogService + .open(StarkAlertDialogComponent, { + data: { + title: "SHOWCASE.DEMO.DIALOGS.ALERT.TITLE", + textContent: "SHOWCASE.DEMO.DIALOGS.ALERT.TEXT", + ok: "SHOWCASE.DEMO.DIALOGS.ALERT.OK" + }, + ariaLabel: "Alert Dialog Demo" + }) + .afterClosed() + .subscribe((result: StarkAlertDialogResult) => { + this.dialogResult = result || "undefined"; + if (result === "ok") { + this.dialogStatus = "SHOWCASE.DEMO.DIALOGS.ALERT.RESULT_OK"; + } else { + this.dialogStatus = "SHOWCASE.DEMO.DIALOGS.ALERT.RESULT_CANCEL"; + } + }); + } + + public showConfirm(): void { + this.dialogService + .open(StarkConfirmDialogComponent, { + data: { + title: "SHOWCASE.DEMO.DIALOGS.CONFIRM.TITLE", + textContent: "SHOWCASE.DEMO.DIALOGS.CONFIRM.TEXT", + ok: "SHOWCASE.DEMO.DIALOGS.CONFIRM.OK", + cancel: "SHOWCASE.DEMO.DIALOGS.CONFIRM.CANCEL" + }, + ariaLabel: "Lucky day" + }) + .afterClosed() + .subscribe((result: StarkConfirmDialogResult) => { + this.dialogResult = result || "undefined"; + if (result === "ok") { + this.dialogStatus = "SHOWCASE.DEMO.DIALOGS.CONFIRM.RESULT_OK"; + } else { + this.dialogStatus = "SHOWCASE.DEMO.DIALOGS.CONFIRM.RESULT_CANCEL"; + } + }); + } + + public showPrompt(): void { + this.dialogService + .open(StarkPromptDialogComponent, { + data: { + title: "SHOWCASE.DEMO.DIALOGS.PROMPT.TITLE", + textContent: "SHOWCASE.DEMO.DIALOGS.PROMPT.TEXT", + placeholder: "SHOWCASE.DEMO.DIALOGS.PROMPT.PLACEHOLDER", + initialValue: "", + ok: "SHOWCASE.DEMO.DIALOGS.PROMPT.OK", + cancel: "SHOWCASE.DEMO.DIALOGS.PROMPT.CANCEL" + }, + ariaLabel: "Dog name" + }) + .afterClosed() + .subscribe((result: StarkPromptDialogResult) => { + this.dialogResult = result || "undefined"; + if (result === "cancel") { + this.dialogStatus = "SHOWCASE.DEMO.DIALOGS.PROMPT.RESULT_CANCEL"; + } else { + this.dialogStatus = "SHOWCASE.DEMO.DIALOGS.PROMPT.RESULT_OK"; + } + }); + } +} diff --git a/showcase/src/app/demo-ui/pages/dialogs/index.ts b/showcase/src/app/demo-ui/pages/dialogs/index.ts new file mode 100644 index 0000000000..6d751f1ba6 --- /dev/null +++ b/showcase/src/app/demo-ui/pages/dialogs/index.ts @@ -0,0 +1 @@ +export * from "./dialogs-page.component"; diff --git a/showcase/src/app/demo-ui/pages/index.ts b/showcase/src/app/demo-ui/pages/index.ts index 5ea00dc1c8..e5b903ec07 100644 --- a/showcase/src/app/demo-ui/pages/index.ts +++ b/showcase/src/app/demo-ui/pages/index.ts @@ -4,6 +4,7 @@ export * from "./breadcrumb"; export * from "./collapsible"; export * from "./date-picker"; export * from "./date-range-picker"; +export * from "./dialogs"; export * from "./dropdown"; export * from "./footer"; export * from "./generic-search"; diff --git a/showcase/src/app/demo-ui/routes.ts b/showcase/src/app/demo-ui/routes.ts index 8086e497ca..5f8f418c3c 100644 --- a/showcase/src/app/demo-ui/routes.ts +++ b/showcase/src/app/demo-ui/routes.ts @@ -6,6 +6,7 @@ import { DemoCollapsiblePageComponent, DemoDatePickerPageComponent, DemoDateRangePickerPageComponent, + DemoDialogsPageComponent, DemoDropdownPageComponent, DemoFooterPageComponent, DemoGenericSearchPageComponent, @@ -77,6 +78,14 @@ export const DEMO_STATES: Ng2StateDeclaration[] = [ }, views: { "@": { component: DemoDateRangePickerPageComponent } } }, + { + name: "demo-ui.dialogs", + url: "/dialogs", + data: { + translationKey: "SHOWCASE.DEMO.DIALOGS.TITLE" + }, + views: { "@": { component: DemoDialogsPageComponent } } + }, { name: "demo-ui.dropdown", url: "/dropdown", diff --git a/showcase/src/assets/examples/dialogs/predefined-dialogs.html b/showcase/src/assets/examples/dialogs/predefined-dialogs.html new file mode 100644 index 0000000000..df831fbf2b --- /dev/null +++ b/showcase/src/assets/examples/dialogs/predefined-dialogs.html @@ -0,0 +1,9 @@ +
+ + + +
+ +
+ {{ dialogStatus }} +
diff --git a/showcase/src/assets/examples/dialogs/predefined-dialogs.ts b/showcase/src/assets/examples/dialogs/predefined-dialogs.ts new file mode 100644 index 0000000000..da39fcf381 --- /dev/null +++ b/showcase/src/assets/examples/dialogs/predefined-dialogs.ts @@ -0,0 +1,88 @@ +import { Component, Inject } from "@angular/core"; +import { MatDialog } from "@angular/material/dialog"; +import { STARK_LOGGING_SERVICE, StarkLoggingService } from "@nationalbankbelgium/stark-core"; +import { + StarkAlertDialogComponent, + StarkAlertDialogContent, + StarkAlertDialogResult, + StarkConfirmDialogComponent, + StarkConfirmDialogContent, + StarkConfirmDialogResult, + StarkPromptDialogComponent, + StarkPromptDialogContent, + StarkPromptDialogResult +} from "@nationalbankbelgium/stark-ui"; + +@Component({ + selector: "demo-dialogs", + templateUrl: "./demo-dialogs.component.html" +}) +export class DemoDialogsComponent { + public dialogStatus: string; + + public constructor(@Inject(STARK_LOGGING_SERVICE) public logger: StarkLoggingService, public dialogService: MatDialog) {} + + public showAlert(): void { + this.dialogService + .open(StarkAlertDialogComponent, { + data: { + title: "This is an alert title", + textContent: "You can specify some description text in here.", + ok: "Got it!" + }, + ariaLabel: "Alert Dialog Demo" + }) + .afterClosed() + .subscribe((result: StarkAlertDialogResult) => { + if (result === "ok") { + this.dialogStatus = "Alert dialog closed with value '" + result + "'"; + } else { + this.dialogStatus = "Alert dialog cancelled with value '" + result + "'"; + } + }); + } + + public showConfirm(): void { + this.dialogService + .open(StarkConfirmDialogComponent, { + data: { + title: "Would you like to delete your debt?", + textContent: "All of the banks have agreed to forgive you your debts.", + ok: "Please do it!", + cancel: "Sounds like a scam" + }, + ariaLabel: "Lucky day" + }) + .afterClosed() + .subscribe((result: StarkConfirmDialogResult) => { + if (result === "ok") { + this.dialogStatus = "You decided to get rid of your debt."; + } else { + this.dialogStatus = "You decided to keep your debt."; + } + }); + } + + public showPrompt(): void { + this.dialogService + .open(StarkPromptDialogComponent, { + data: { + title: "What would you name your dog?", + textContent: "Bowser is a common name.", + placeholder: "Dog name", + initialValue: "", + ok: "Okay!", + cancel: "I'm a cat person" + }, + ariaLabel: "Dog name" + }) + .afterClosed() + .subscribe((result: StarkPromptDialogResult) => { + if (result === "cancel") { + this.dialogStatus = "You didn't name your dog."; + } else { + this.dialogStatus = "You decided to name your dog '" + result + "'"; + } + }); + } +} diff --git a/showcase/src/assets/translations/en.json b/showcase/src/assets/translations/en.json index 237a251231..dd74462f74 100644 --- a/showcase/src/assets/translations/en.json +++ b/showcase/src/assets/translations/en.json @@ -51,6 +51,41 @@ "DATE_RANGE_PICKER": { "TITLE": "Date range picker" }, + "DIALOGS": { + "ALERT": { + "TITLE": "This is an alert title", + "TEXT": "You can specify some description text in here.", + "OK": "Got it!", + "RESULT_OK": "Alert dialog closed with value '{{ result }}'", + "RESULT_CANCEL": "Alert dialog cancelled with value '{{ result }}'" + }, + "CONFIRM": { + "TITLE": "Would you like to delete your debt?", + "TEXT": "All of the banks have agreed to forgive you your debts.", + "OK": "Please do it!", + "CANCEL": "Sounds like a scam", + "RESULT_OK": "You decided to get rid of your debt.", + "RESULT_CANCEL": "You decided to keep your debt." + }, + "PROMPT": { + "TITLE": "What would you name your dog?", + "TEXT": "Bowser is a common name.", + "PLACEHOLDER": "Dog name", + "OK": "Okay!", + "CANCEL": "I'm a cat person", + "RESULT_OK": "You decided to name your dog '{{ result }}'", + "RESULT_CANCEL": "You didn't name your dog." + }, + "FULLSCREEN": { + "TITLE": "Warning!", + "TEXT": "There’s a problem with this parcel." + }, + "TITLE": "Dialogs", + "PREDEFINED_DIALOGS": "Predefined dialogs", + "ALERT_DIALOG": "Alert dialog", + "CONFIRM_DIALOG": "Confirm dialog", + "PROMPT_DIALOG": "Prompt dialog" + }, "DROPDOWN": { "PR": "IT applications", "BLANK": "Single selection with default blank", diff --git a/showcase/src/assets/translations/fr.json b/showcase/src/assets/translations/fr.json index 9591713577..d70e185d26 100644 --- a/showcase/src/assets/translations/fr.json +++ b/showcase/src/assets/translations/fr.json @@ -51,6 +51,41 @@ "DATE_RANGE_PICKER": { "TITLE": "Date range picker" }, + "DIALOGS": { + "ALERT": { + "TITLE": "C'est un titre d'alerte", + "TEXT": "Vous pouvez spécifier un texte de description ici.", + "OK": "Je l'ai!", + "RESULT_OK": "Dialogue d'alerte fermé avec valeur '{{ result }}'", + "RESULT_CANCEL": "dialogue d'alerte annulée avec valeur '{{ result }}'" + }, + "CONFIRM": { + "TITLE": "Voulez-vous supprimer votre dette?", + "TEXT": "Toutes les banques ont accepté de vous pardonner vos dettes.", + "OK": "Faites-le!", + "CANCEL": "On dirait une arnaque", + "RESULT_OK": "Vous avez décidé de vous débarrasser de votre dette.", + "RESULT_CANCEL": "Vous avez décidé de garder votre dette." + }, + "PROMPT": { + "TITLE": "Comment nommeriez-vous votre chien?", + "TEXT": "Bowser est un nom commun.", + "PLACEHOLDER": "Nom du chien", + "OK": "D'accord!", + "CANCEL": "Je suis une personne de chat", + "RESULT_OK": "Vous avez décidé de nommer votre chien '{{ result }}'", + "RESULT_CANCEL": "Vous n'avez pas nommé votre chien." + }, + "FULLSCREEN": { + "TITLE": "Attention!", + "TEXT": "Il y a un problème avec ce colis." + }, + "TITLE": "Dialogues", + "PREDEFINED_DIALOGS": "Dialogues prédéfinis", + "ALERT_DIALOG": "Dialogue d'alerte", + "CONFIRM_DIALOG": "Dialogue de confirmation", + "PROMPT_DIALOG": "Dialogue d'invite" + }, "DROPDOWN": { "PR": "Applications informatiques", "BLANK": "Option par défaut (blank)", diff --git a/showcase/src/assets/translations/nl.json b/showcase/src/assets/translations/nl.json index 0d4d47a7d7..941aa2b21e 100644 --- a/showcase/src/assets/translations/nl.json +++ b/showcase/src/assets/translations/nl.json @@ -51,6 +51,41 @@ "DATE_RANGE_PICKER": { "TITLE": "Date range picker" }, + "DIALOGS": { + "ALERT": { + "TITLE": "Dit is een waarschuwing titel", + "TEXT": "U kunt een aantal beschrijvende tekst opgeven in hier.", + "OK": "Begrepen!", + "RESULT_OK": "Waarschuwingsdialoog gesloten met waarde '{{ result }}'", + "RESULT_CANCEL": "Waarschuwingsdialoog geannuleerd met waarde '{{ result }}'" + }, + "CONFIRM": { + "TITLE": "Wilt u uw schuld te schrappen?", + "TEXT": "Alle banken hebben afgesproken om je te vergeven uw schulden.", + "OK": "Doe het alsjeblieft!", + "CANCEL": "Klinkt als een scam", + "RESULT_OK": "Je hebt besloten om je schuld kwijt te raken.", + "RESULT_CANCEL": "Je hebt besloten je schuld te houden." + }, + "PROMPT": { + "TITLE": "Wat zou u uw hond te noemen?", + "TEXT": "Bowser is een veel voorkomende naam.", + "PLACEHOLDER": "Naam van de hond", + "OK": "Okay!", + "CANCEL": "Ik ben een kat persoon", + "RESULT_OK": "Je hebt besloten om je hond een naam te geven '{{ result }}'", + "RESULT_CANCEL": "Je hebt je hond niet genoemd." + }, + "FULLSCREEN": { + "TITLE": "Waarschuwing!", + "TEXT": "Er is een probleem met dit pakket." + }, + "TITLE": "Dialogen", + "PREDEFINED_DIALOGS": "Voorgedefinieerde dialogen", + "ALERT_DIALOG": "Waarschuwingsdialoog", + "CONFIRM_DIALOG": "bevestigingsdialoog", + "PROMPT_DIALOG": "Promptdialoog" + }, "DROPDOWN": { "PR": "Informaticatoepassingen", "BLANK": "Bij verstek keus (blank)",