Skip to content

Commit bf40260

Browse files
Added a default Modal implementation accessible through modalService.show().
Added new storybook sections. Updated Modal styles to match Carbon's. Fixed docs.
1 parent 51ebc13 commit bf40260

File tree

7 files changed

+287
-11
lines changed

7 files changed

+287
-11
lines changed

src/modal/alert-modal.component.ts

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import {
2+
Component,
3+
Injector,
4+
OnInit,
5+
ElementRef,
6+
AfterViewInit
7+
} from "@angular/core";
8+
import {
9+
trigger,
10+
state,
11+
style,
12+
transition,
13+
animate
14+
} from "@angular/animations";
15+
import Modal from "./modal.decorator";
16+
import { ModalService } from "./modal.service";
17+
18+
/**
19+
* Component to create standard modals for presenting content or asking for user's input.
20+
* It can show as a passive modal showing only text or show as a transactional modal with
21+
* multiple buttons for different actions for the user to choose from.
22+
*
23+
* Using a modal in your application requires `ibm-modal-placeholder` which would generally be
24+
* placed near the end of your app component template (app.component.ts or app.component.html) as:
25+
*
26+
* ```html
27+
* <ibm-modal-placeholder></ibm-modal-placeholder>
28+
* ```
29+
*
30+
* Example of opening the modal:
31+
*
32+
* ```typescript
33+
* \@Component({
34+
* selector: "app-modal-demo",
35+
* template: `
36+
* <button class="btn--primary" (click)="openModal()">Open modal</button>
37+
* <ibm-modal-placeholder></ibm-modal-placeholder>`
38+
* })
39+
* export class ModalDemo {
40+
*
41+
* openModal() {
42+
* this.modalService.show({
43+
* modalType: "default" | "danger",
44+
* headerText: "optional header text",
45+
* title: "Modal title",
46+
* text: "Modal text",
47+
* buttons: [{
48+
* text: "Button text",
49+
* type: "primary" | "secondary" | "tertiary" | "ghost" | "danger" | "danger--primary" = "primary",
50+
* click: clickFunction,
51+
* }]
52+
* });
53+
* }
54+
* }
55+
* ```
56+
*
57+
* @export
58+
* @class AlertModalComponent
59+
*/
60+
@Modal()
61+
@Component({
62+
selector: "ibm-alert-modal",
63+
template: `
64+
<ibm-modal [modalType]="modalType">
65+
<ibm-modal-header (closeSelect)="closeModal()">
66+
<p class="bx--modal-header__label bx--type-delta">{{headerText}}</p>
67+
<p class="bx--modal-header__heading bx--type-beta">{{title}}</p>
68+
</ibm-modal-header>
69+
<div class="bx--modal-content">
70+
<p>{{text}}</p>
71+
</div>
72+
<ibm-modal-footer *ngIf="buttons.length > 0">
73+
<button ibmButton="{{button.type}}" *ngFor="let button of buttons; let i = index"
74+
(click)="buttonClicked(i)" id="{{button.id}}">{{button.text}}</button>
75+
</ibm-modal-footer>
76+
</ibm-modal>
77+
`,
78+
})
79+
export class AlertModalComponent implements AfterViewInit {
80+
81+
modalType = "default";
82+
headerText: string;
83+
title: string;
84+
text: string;
85+
buttons = [];
86+
87+
/**
88+
* Creates an instance of `AlertModalComponent`.
89+
* @param {ModalService} modalService
90+
* @memberof AlertModalComponent
91+
*/
92+
constructor(
93+
private injector: Injector,
94+
private elRef: ElementRef,
95+
) {
96+
this.modalType = this.injector.get("modalType");
97+
this.headerText = this.injector.get("headerText");
98+
this.title = this.injector.get("title");
99+
this.text = this.injector.get("text");
100+
101+
this.buttons = this.injector.get("buttons") || [];
102+
for (let i = 0; i < this.buttons.length; i++) {
103+
const button = this.buttons[i];
104+
if (!button.id) {
105+
button.id = `alert-modal-button-${i}`;
106+
}
107+
if (!button.type) {
108+
button.type = "secondary";
109+
}
110+
}
111+
}
112+
113+
ngAfterViewInit(): void {
114+
// focus the primary button if there's one
115+
if (this.buttons && this.buttons.length > 0) {
116+
const primaryButtonIndex = this.buttons.findIndex(
117+
button => button.type.indexOf("primary") !== -1) || 0;
118+
const primaryButton = this.buttons[primaryButtonIndex];
119+
const buttonNode = this.elRef.nativeElement.querySelector(`#${primaryButton.id}`);
120+
if (buttonNode) {
121+
buttonNode.focus();
122+
}
123+
}
124+
}
125+
126+
buttonClicked(buttonIndex) {
127+
const button = this.buttons[buttonIndex];
128+
if (button.click) {
129+
button.click();
130+
}
131+
132+
this.closeModal();
133+
}
134+
135+
closeModal() {
136+
this["destroy"]();
137+
}
138+
}

src/modal/modal-header.component.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ import { Component, Output, EventEmitter, Input } from "@angular/core";
1919
selector: "ibm-modal-header",
2020
template: `
2121
<header class="{{modalType}} bx--modal-header" role="banner">
22-
<h5 class="bx--modal-header__heading">
22+
<div class="bx--modal-header">
2323
<ng-content></ng-content>
24-
</h5>
24+
</div>
2525
<button
2626
class="bx--modal-close"
2727
attr.aria-label="{{'MODAL.CLOSE' | translate}}"
@@ -35,7 +35,7 @@ import { Component, Output, EventEmitter, Input } from "@angular/core";
3535
width="10"
3636
aria-label="close the modal"
3737
alt="close the modal">
38-
<title>close the modal</title>
38+
<title>{{'MODAL.CLOSE' | translate}}</title>
3939
<path d="M6.32 5L10 8.68 8.68 10 5 6.32 1.32 10 0 8.68 3.68 5 0 1.32 1.32 0 5 3.68 8.68 0 10 1.32 6.32 5z"></path>
4040
</svg>
4141
</button>

src/modal/modal.component.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { cycleTabs } from "./../common/tab.service";
2323
/**
2424
* Component to create modals for presenting content.
2525
*
26-
* Using a modal in your application requires `n-modal-placeholder` which would generally be
26+
* Using a modal in your application requires `ibm-modal-placeholder` which would generally be
2727
* placed near the end of your app component template (app.component.ts or app.component.html) as:
2828
*
2929
* ```html
@@ -48,7 +48,7 @@ import { cycleTabs } from "./../common/tab.service";
4848
* </button>
4949
* {{modalText}}
5050
* </section>
51-
* <ibm-modal-footer><button class="btn--primary cancel-button" (click)="closeModal()">Close</button></ibm-modal-footer>
51+
* <ibm-modal-footer><button class="bx--btn bx--btn--primary" (click)="closeModal()">Close</button></ibm-modal-footer>
5252
* </ibm-modal>`,
5353
* styleUrls: ["./sample-modal.component.scss"]
5454
* })
@@ -85,7 +85,7 @@ import { cycleTabs } from "./../common/tab.service";
8585
@Component({
8686
selector: "ibm-modal",
8787
template: `
88-
<ibm-overlay (overlaySelect)="overlaySelected.emit()">
88+
<ibm-overlay [modalType]="modalType" (overlaySelect)="overlaySelected.emit()">
8989
<div
9090
class="bx--modal-container"
9191
[@modalState]="modalState"
@@ -120,7 +120,7 @@ export class ModalComponent implements OnInit, OnDestroy {
120120
@Input() size = "default";
121121
/**
122122
* Classification of the modal.
123-
* @type {"default" | "warning" | "error"}
123+
* @type {"default" | "danger"}
124124
* @memberof ModalComponent
125125
*/
126126
@Input() modalType = "default";

src/modal/modal.module.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import { ModalComponent } from "./modal.component";
1717
import { ModalFooterComponent } from "./modal-footer.component";
1818
import { OverlayComponent } from "./overlay.component";
1919
import { ModalHeaderComponent } from "./modal-header.component";
20+
import { AlertModalComponent } from "./alert-modal.component";
21+
import { ButtonModule } from "../forms/forms.module";
2022

2123
// exports
2224
export { default as Modal } from "./modal.decorator";
@@ -36,19 +38,22 @@ export const MODAL_PLACEHOLDER_SERVICE_PROVIDER = {
3638

3739
@NgModule({
3840
declarations: [
41+
AlertModalComponent,
3942
ModalPlaceholderComponent,
4043
ModalComponent,
4144
ModalHeaderComponent,
4245
ModalFooterComponent,
4346
OverlayComponent
4447
],
4548
exports: [
49+
AlertModalComponent,
4650
ModalPlaceholderComponent,
4751
ModalComponent,
4852
ModalHeaderComponent,
4953
ModalFooterComponent
5054
],
5155
entryComponents: [
56+
AlertModalComponent,
5257
ModalComponent,
5358
ModalFooterComponent,
5459
ModalHeaderComponent
@@ -61,6 +66,7 @@ export const MODAL_PLACEHOLDER_SERVICE_PROVIDER = {
6166
],
6267
imports: [
6368
CommonModule,
69+
ButtonModule,
6470
TranslateModule,
6571
StaticIconModule
6672
]

src/modal/modal.service.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { ModalComponent } from "./modal.component";
88
import { ModalPlaceholderService } from "./modal-placeholder.service";
99
import { ReplaySubject } from "rxjs";
1010
import { Injectable } from "@angular/core";
11+
import { AlertModalComponent } from "./alert-modal.component";
1112

1213

1314
/**
@@ -67,6 +68,33 @@ export class ModalService {
6768
return component;
6869
}
6970

71+
/**
72+
* Creates and renders a new alert modal component.
73+
* @param data You can pass in `title`, `text` and `buttons` to be used in the modal.
74+
* `buttons` is an array of objects
75+
* ```
76+
* {
77+
* text: "Button text",
78+
* type: "primary" | "secondary" | "tertiary" | "ghost" | "danger" | "danger--primary" = "primary",
79+
* click: clickFunction,
80+
* }
81+
* ```
82+
* @returns {ComponentRef<any>}
83+
* @memberof ModalService
84+
*/
85+
show(data: {modalType?: string, headerText?: string, title: string, text: string, buttons?: null}) {
86+
return this.create({
87+
component: AlertModalComponent,
88+
inputs: {
89+
modalType: data.modalType,
90+
headerText: data.headerText,
91+
title: data.title,
92+
text: data.text,
93+
buttons: data.buttons || [],
94+
}
95+
});
96+
}
97+
7098
/**
7199
* Destroys the modal on the supplied index.
72100
* When called without parameters it destroys the most recently created/top most modal.

src/modal/modal.stories.ts

Lines changed: 99 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { storiesOf, moduleMetadata } from "@storybook/angular";
2-
import { withKnobs, text } from "@storybook/addon-knobs/angular";
2+
import { withKnobs, text, select } from "@storybook/addon-knobs/angular";
33

44
import { TranslateModule } from "@ngx-translate/core";
55

@@ -54,6 +54,35 @@ class ModalStory {
5454
}
5555
}
5656

57+
58+
@Modal()
59+
@Component({
60+
selector: "app-alert-modal-story",
61+
template: `
62+
<button class="bx--btn bx--btn--primary" (click)="openModal()">Open Modal</button>
63+
`
64+
})
65+
class AlertModalStory {
66+
67+
@Input() modalType: string;
68+
@Input() headerText: string;
69+
@Input() title: string;
70+
@Input() text: string;
71+
@Input() buttons: any;
72+
73+
constructor(private modalService: ModalService) { }
74+
75+
openModal() {
76+
this.modalService.show({
77+
modalType: this.modalType,
78+
headerText: this.headerText,
79+
title: this.title,
80+
text: this.text,
81+
buttons: this.buttons
82+
});
83+
}
84+
}
85+
5786
storiesOf("Modal", module)
5887
.addDecorator(
5988
moduleMetadata({
@@ -80,5 +109,72 @@ storiesOf("Modal", module)
80109
props: {
81110
modalText: text("modalText", "Hello, World!")
82111
}
83-
}
84-
));
112+
}))
113+
.addDecorator(
114+
moduleMetadata({
115+
declarations: [
116+
AlertModalStory,
117+
],
118+
imports: [
119+
ModalModule,
120+
BrowserAnimationsModule,
121+
TranslateModule.forRoot()
122+
],
123+
entryComponents: [
124+
SampleModalComponent
125+
]
126+
})
127+
)
128+
.addDecorator(withKnobs)
129+
.add("Transactional", () => ({
130+
template: `
131+
<app-alert-modal-story [modalType]="modalType" [headerText]="headerText" [title]="title" [text]="text"
132+
[buttons]="buttons"></app-alert-modal-story>
133+
<ibm-modal-placeholder></ibm-modal-placeholder>
134+
`,
135+
props: {
136+
modalType: select("modalType", ["default", "danger"], "default"),
137+
headerText: text("headerText", "optional header text"),
138+
title: text("title", "Delete service from application"),
139+
text: text("text", `Are you sure you want to remove the Speech to Text service from the node-test app?`),
140+
buttons: [{
141+
text: "Cancel",
142+
type: "secondary"
143+
}, {
144+
text: "Delete",
145+
type: "primary",
146+
click: () => alert("Delete button clicked")
147+
}],
148+
}
149+
}))
150+
.addDecorator(
151+
moduleMetadata({
152+
declarations: [
153+
AlertModalStory,
154+
],
155+
imports: [
156+
ModalModule,
157+
BrowserAnimationsModule,
158+
TranslateModule.forRoot()
159+
],
160+
entryComponents: [
161+
SampleModalComponent
162+
]
163+
})
164+
)
165+
.addDecorator(withKnobs)
166+
.add("Passive", () => ({
167+
template: `
168+
<app-alert-modal-story [modalType]="modalType" [headerText]="headerText" [title]="title" [text]="text"
169+
></app-alert-modal-story>
170+
<ibm-modal-placeholder></ibm-modal-placeholder>
171+
`,
172+
props: {
173+
modalType: select("modalType", ["default", "danger"], "default"),
174+
headerText: text("headerText", "optional header text"),
175+
title: text("title", "Passive modal title"),
176+
text: text("text", "Passive modal notifications should only appear if there is an action " +
177+
"the user needs to address immediately. Passive modal notifications are persistent on screen"),
178+
}
179+
}))
180+
;

0 commit comments

Comments
 (0)