Skip to content

Commit

Permalink
feat(stark-ui): implement Material dialogs presets: alert, confirm an…
Browse files Browse the repository at this point in the history
…d prompt

ISSUES CLOSED: #793
  • Loading branch information
christophercr committed Mar 8, 2019
1 parent 14e432d commit 34381b8
Show file tree
Hide file tree
Showing 36 changed files with 1,442 additions and 2 deletions.
4 changes: 4 additions & 0 deletions packages/stark-ui/assets/stark-ui-bundle.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
9 changes: 9 additions & 0 deletions packages/stark-ui/assets/styles/_material-fixes.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
1 change: 1 addition & 0 deletions packages/stark-ui/src/modules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
2 changes: 2 additions & 0 deletions packages/stark-ui/src/modules/dialogs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./dialogs/dialogs.module";
export * from "./dialogs/components";
6 changes: 6 additions & 0 deletions packages/stark-ui/src/modules/dialogs/components.ts
Original file line number Diff line number Diff line change
@@ -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";
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { StarkBaseDialogContent } from "./dialog-content.intf";

/**
* Content that can be shown in the {@link StarkAlertDialogComponent}
*/
export interface StarkAlertDialogContent extends StarkBaseDialogContent {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.stark-alert-dialog {
.button-ok {
color: mat-color($primary-palette, 500);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<h2 mat-dialog-title>{{ content.title || "" | translate }}</h2>

<div mat-dialog-content>{{ content.textContent || "" | translate }}</div>

<div mat-dialog-actions>
<button mat-button (click)="onOk()" class="button-ok">{{ content.ok || "OK" | translate }}</button>
</div>
Original file line number Diff line number Diff line change
@@ -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<TestHostComponent>;
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<StarkAlertDialogComponent, StarkAlertDialogResult> = dialogService.open<
StarkAlertDialogComponent,
StarkAlertDialogContent,
StarkAlertDialogResult
>(StarkAlertDialogComponent, {
data: dummyDialogContent
});
hostFixture.detectChanges();

expect(dialogRef.componentInstance instanceof StarkAlertDialogComponent).toBe(true);

const dialogElement: HTMLElement | null = (<HTMLElement>overlayContainerElement).querySelector<HTMLElement>(
"mat-dialog-container " + dialogComponentSelector
);

const dialogTitleElement: HTMLElement | null = (<HTMLElement>dialogElement).querySelector<HTMLElement>("[mat-dialog-title]");
expect(dialogTitleElement).toBeDefined();
expect((<HTMLElement>dialogTitleElement).innerHTML).toEqual(<string>dummyDialogContent.title);

const dialogContentElement: HTMLElement | null = (<HTMLElement>dialogElement).querySelector<HTMLElement>("[mat-dialog-content]");
expect(dialogContentElement).toBeDefined();
expect((<HTMLElement>dialogContentElement).innerHTML).toBe(<string>dummyDialogContent.textContent);

const dialogActionsElement: HTMLElement | null = (<HTMLElement>dialogElement).querySelector<HTMLElement>("[mat-dialog-actions]");
expect(dialogActionsElement).toBeDefined();
const dialogButtonElements: NodeListOf<HTMLElement> = (<HTMLElement>dialogActionsElement).querySelectorAll("button");
expect(dialogButtonElements.length).toBe(1);
expect(dialogButtonElements[0].innerHTML).toBe(<string>dummyDialogContent.ok);
});

it("should return 'ok' as result when the 'Ok' button is clicked", fakeAsync(() => {
expect(dialogService).toBeDefined();
const dialogRef: MatDialogRef<StarkAlertDialogComponent, StarkAlertDialogResult> = dialogService.open<
StarkAlertDialogComponent,
StarkAlertDialogContent,
StarkAlertDialogResult
>(StarkAlertDialogComponent, {
data: dummyDialogContent
});
hostFixture.detectChanges();

const dialogElement: HTMLElement | null = (<HTMLElement>overlayContainerElement).querySelector<HTMLElement>(
"mat-dialog-container " + dialogComponentSelector
);

const mockObserver: SpyObj<Observer<StarkAlertDialogResult>> = createSpyObj<Observer<StarkAlertDialogResult>>("observerSpy", [
"next",
"error",
"complete"
]);
dialogRef.afterClosed().subscribe(mockObserver);

const dialogActionsElement: HTMLElement | null = (<HTMLElement>dialogElement).querySelector<HTMLElement>("[mat-dialog-actions]");
expect(dialogActionsElement).toBeDefined();
const dialogButtonElements: NodeListOf<HTMLElement> = (<HTMLElement>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<StarkAlertDialogComponent, StarkAlertDialogResult> = dialogService.open<
StarkAlertDialogComponent,
StarkAlertDialogContent,
StarkAlertDialogResult
>(StarkAlertDialogComponent, {
data: dummyDialogContent
});
hostFixture.detectChanges();

const mockObserver: SpyObj<Observer<StarkAlertDialogResult>> = createSpyObj<Observer<StarkAlertDialogResult>>("observerSpy", [
"next",
"error",
"complete"
]);
dialogRef.afterClosed().subscribe(mockObserver);

triggerClick(<HTMLElement>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<StarkAlertDialogComponent, StarkAlertDialogResult> = dialogService.open<
StarkAlertDialogComponent,
StarkAlertDialogContent,
StarkAlertDialogResult
>(StarkAlertDialogComponent, {
data: dummyDialogContent
});
hostFixture.detectChanges();

const mockObserver: SpyObj<Observer<StarkAlertDialogResult>> = createSpyObj<Observer<StarkAlertDialogResult>>("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();
}));
});
Original file line number Diff line number Diff line change
@@ -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<StarkAlertDialogComponent, StarkAlertDialogResult>,
@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");
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.stark-confirm-dialog {
.button-ok,
.button-cancel {
color: mat-color($primary-palette, 500);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<h2 mat-dialog-title>{{ content.title || "" | translate }}</h2>

<div mat-dialog-content>{{ content.textContent || "" | translate }}</div>

<div mat-dialog-actions>
<button mat-button (click)="onCancel()" class="button-cancel">{{ content.cancel || "CANCEL" | translate }}</button>
<button mat-button cdkFocusInitial (click)="onOk()" class="button-ok">{{ content.ok || "OK" | translate }}</button>
</div>
Loading

0 comments on commit 34381b8

Please sign in to comment.