diff --git a/projects/ion/src/lib/core/types/notification.ts b/projects/ion/src/lib/core/types/notification.ts
index 1c9363e1c..6b8a0ae21 100644
--- a/projects/ion/src/lib/core/types/notification.ts
+++ b/projects/ion/src/lib/core/types/notification.ts
@@ -3,10 +3,13 @@ import { fadeInDirection, fadeOutDirection } from '../../utils/animationsTypes';
import { IconType } from './icon';
import { StatusType } from './status';
-export interface NotificationProps {
+export interface NotificationProps extends NotificationConfigOptions {
title: string;
message: string;
type?: StatusType;
+}
+
+export interface NotificationConfigOptions {
icon?: IconType;
fixed?: boolean;
fadeIn?: fadeInDirection;
diff --git a/projects/ion/src/lib/icon/mock/list-icons.module.ts b/projects/ion/src/lib/icon/mock/list-icons.module.ts
index 73e8b3d1e..67238337e 100644
--- a/projects/ion/src/lib/icon/mock/list-icons.module.ts
+++ b/projects/ion/src/lib/icon/mock/list-icons.module.ts
@@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common';
import { IonIconComponent } from '../icon.component';
import { IonInputComponent } from '../../input/input.component';
import { FormsModule } from '@angular/forms';
-import { IonNotificationComponent } from '../../notification/notification.component';
+import { IonNotificationComponent } from '../../notification/component/notification.component';
@NgModule({
declarations: [IonInputComponent, IonIconComponent, IonNotificationComponent],
diff --git a/projects/ion/src/lib/notification/notification.component.html b/projects/ion/src/lib/notification/component/notification.component.html
similarity index 100%
rename from projects/ion/src/lib/notification/notification.component.html
rename to projects/ion/src/lib/notification/component/notification.component.html
diff --git a/projects/ion/src/lib/notification/notification.component.scss b/projects/ion/src/lib/notification/component/notification.component.scss
similarity index 85%
rename from projects/ion/src/lib/notification/notification.component.scss
rename to projects/ion/src/lib/notification/component/notification.component.scss
index 6a1166a3b..29b660be4 100644
--- a/projects/ion/src/lib/notification/notification.component.scss
+++ b/projects/ion/src/lib/notification/component/notification.component.scss
@@ -1,5 +1,5 @@
-@import '../../styles/index.scss';
-@import '../utils/fadeAnimations.scss';
+@import '../../../styles/index.scss';
+@import '../../utils/fadeAnimations.scss';
@mixin icon-color($color) {
::ng-deep svg {
@@ -13,9 +13,11 @@
align-items: flex-start;
justify-content: space-between;
padding: spacing(1.5) spacing(2);
+
position: relative;
z-index: $zIndexMax;
max-width: 500px;
+ min-width: 250px;
background: rgba(255, 255, 255, 0.9);
box-shadow: 0px 8px 6px -4px rgba(0, 0, 0, 0.15),
@@ -75,3 +77,11 @@
.negative-icon {
@include icon-color($negative-color);
}
+
+.notification-container:nth-child(2) {
+ top: 30px;
+}
+
+.notification-container:nth-child(3) {
+ top: 30px;
+}
diff --git a/projects/ion/src/lib/notification/notification.component.spec.ts b/projects/ion/src/lib/notification/component/notification.component.spec.ts
similarity index 96%
rename from projects/ion/src/lib/notification/notification.component.spec.ts
rename to projects/ion/src/lib/notification/component/notification.component.spec.ts
index 2acb8b57a..8ced43bbe 100644
--- a/projects/ion/src/lib/notification/notification.component.spec.ts
+++ b/projects/ion/src/lib/notification/component/notification.component.spec.ts
@@ -1,8 +1,8 @@
import { EventEmitter } from '@angular/core';
import { fireEvent, render, screen } from '@testing-library/angular';
-import { StatusType } from '../core/types';
-import { NotificationProps } from '../core/types/notification';
-import { IonIconModule } from '../icon/icon.module';
+import { StatusType } from '../../core/types';
+import { NotificationProps } from '../../core/types/notification';
+import { IonIconModule } from '../../icon/icon.module';
import { IonNotificationComponent } from './notification.component';
const defaultNotification = {
diff --git a/projects/ion/src/lib/notification/notification.component.ts b/projects/ion/src/lib/notification/component/notification.component.ts
similarity index 93%
rename from projects/ion/src/lib/notification/notification.component.ts
rename to projects/ion/src/lib/notification/component/notification.component.ts
index 812f3ff8a..c5569042c 100644
--- a/projects/ion/src/lib/notification/notification.component.ts
+++ b/projects/ion/src/lib/notification/component/notification.component.ts
@@ -8,9 +8,9 @@ import {
ViewChild,
} from '@angular/core';
import { Subscription } from 'rxjs';
-import { IconType } from '../core/types/icon';
-import { NotificationProps } from '../core/types/notification';
-import { setTimer } from '../utils/setTimer';
+import { IconType } from '../../core/types/icon';
+import { NotificationProps } from '../../core/types/notification';
+import { setTimer } from '../../utils/setTimer';
@Component({
selector: 'ion-notification',
diff --git a/projects/ion/src/lib/notification/mock/open-notification-mock.component.ts b/projects/ion/src/lib/notification/mock/open-notification-mock.component.ts
new file mode 100644
index 000000000..0c86be2f2
--- /dev/null
+++ b/projects/ion/src/lib/notification/mock/open-notification-mock.component.ts
@@ -0,0 +1,81 @@
+import { IonNotificationService } from './../service/notification.service';
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'open-notification-button',
+ template: `
+
+ `,
+ styles: [
+ `
+ .button {
+ /deep/ button {
+ width: 150px !important;
+ }
+ }
+ `,
+ ],
+})
+export class OpenNotificationButtonComponent {
+ constructor(private ionNotificationService: IonNotificationService) {}
+
+ notificationSuccess(): void {
+ this.ionNotificationService.success(
+ 'Title...',
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit...'
+ );
+ }
+
+ notificationWarning(): void {
+ this.ionNotificationService.warning(
+ 'Title...',
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit...'
+ );
+ }
+
+ notificationInfo(): void {
+ this.ionNotificationService.info(
+ 'Title...',
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit...'
+ );
+ }
+
+ notificationError(): void {
+ this.ionNotificationService.error(
+ 'Title...',
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit...'
+ );
+ }
+}
diff --git a/projects/ion/src/lib/notification/notification.module.ts b/projects/ion/src/lib/notification/notification.module.ts
index aa8ef4faf..a9d66eae1 100644
--- a/projects/ion/src/lib/notification/notification.module.ts
+++ b/projects/ion/src/lib/notification/notification.module.ts
@@ -1,11 +1,18 @@
+import { IonNotificationContainerComponent } from './service/notification.container.component';
+import { IonNotificationService } from './service/notification.service';
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IonIconModule } from '../icon/icon.module';
-import { IonNotificationComponent } from './notification.component';
+import { IonNotificationComponent } from './component/notification.component';
@NgModule({
- declarations: [IonNotificationComponent],
+ declarations: [IonNotificationComponent, IonNotificationContainerComponent],
imports: [CommonModule, IonIconModule],
- exports: [IonNotificationComponent],
+ providers: [IonNotificationService],
+ exports: [IonNotificationComponent, IonNotificationContainerComponent],
+ entryComponents: [
+ IonNotificationComponent,
+ IonNotificationContainerComponent,
+ ],
})
export class IonNotificationModule {}
diff --git a/projects/ion/src/lib/notification/service/notification.container.component.ts b/projects/ion/src/lib/notification/service/notification.container.component.ts
new file mode 100644
index 000000000..98503bbae
--- /dev/null
+++ b/projects/ion/src/lib/notification/service/notification.container.component.ts
@@ -0,0 +1,26 @@
+import { IonNotificationComponent } from '../component/notification.component';
+import { Component, ComponentRef, Renderer2, ElementRef } from '@angular/core';
+
+@Component({
+ selector: 'notification-container',
+ template: '',
+ styleUrls: ['notification.container.scss'],
+})
+export class IonNotificationContainerComponent {
+ constructor(private renderer: Renderer2, private element: ElementRef) {}
+
+ addNotification(notification: ComponentRef): void {
+ notification.instance.ionOnClose.subscribe(() => {
+ this.removeNotification(notification.location.nativeElement);
+ });
+
+ this.renderer.appendChild(
+ this.element.nativeElement,
+ notification.location.nativeElement
+ );
+ }
+
+ removeNotification(notification: ElementRef): void {
+ this.renderer.removeChild(this.element.nativeElement, notification);
+ }
+}
diff --git a/projects/ion/src/lib/notification/service/notification.container.scss b/projects/ion/src/lib/notification/service/notification.container.scss
new file mode 100644
index 000000000..17b9e63ed
--- /dev/null
+++ b/projects/ion/src/lib/notification/service/notification.container.scss
@@ -0,0 +1,8 @@
+:host {
+ position: absolute;
+ display: flex;
+ flex-direction: column;
+ top: 0;
+ right: 0;
+ gap: 1rem;
+}
diff --git a/projects/ion/src/lib/notification/service/notification.service.spec.ts b/projects/ion/src/lib/notification/service/notification.service.spec.ts
new file mode 100644
index 000000000..c0c460b8f
--- /dev/null
+++ b/projects/ion/src/lib/notification/service/notification.service.spec.ts
@@ -0,0 +1,164 @@
+import { IonSharedModule } from './../../shared.module';
+import { IonNotificationContainerComponent } from './notification.container.component';
+import { IonNotificationComponent } from '../component/notification.component';
+import { IonNotificationService } from './notification.service';
+import { TestBed } from '@angular/core/testing';
+import { Component, NgModule } from '@angular/core';
+import { fireEvent, screen } from '@testing-library/angular';
+
+const NOTIFICATION_ICONS = {
+ success: 'success-icon',
+ info: 'info-icon',
+ warning: 'warning-icon',
+ negative: 'negative-icon',
+};
+
+const NOTIFICATION_TYPES = Object.keys(NOTIFICATION_ICONS);
+
+const DEFAULT_NOTIFICATION_OPTIONS = {
+ title: 'Titulo Padrão',
+ message: 'Mensagem Padrão',
+};
+
+@Component({
+ template: '',
+})
+class ContainerRefTestComponent {}
+
+@NgModule({
+ declarations: [
+ ContainerRefTestComponent,
+ IonNotificationContainerComponent,
+ IonNotificationComponent,
+ ],
+ imports: [IonSharedModule],
+ entryComponents: [
+ ContainerRefTestComponent,
+ IonNotificationContainerComponent,
+ IonNotificationComponent,
+ ],
+})
+class TestModule {}
+
+jest.setTimeout(1000);
+
+describe('NotificationService', () => {
+ let notificationService: IonNotificationService;
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [TestModule],
+ }).compileComponents();
+
+ notificationService = TestBed.get(IonNotificationService);
+ });
+
+ it('should remove a notification', () => {
+ notificationService.success(
+ DEFAULT_NOTIFICATION_OPTIONS.title,
+ DEFAULT_NOTIFICATION_OPTIONS.message,
+ { fixed: true }
+ );
+ const removeNotification = screen.getByTestId('btn-remove');
+ fireEvent.click(removeNotification);
+ const elements = document.getElementsByTagName('ion-notification');
+ expect(elements).toHaveLength(0);
+ });
+
+ it('should emit event when a notification is closed', () => {
+ const closeEvent = jest.fn();
+ notificationService.success(
+ DEFAULT_NOTIFICATION_OPTIONS.title,
+ DEFAULT_NOTIFICATION_OPTIONS.message,
+ { fixed: true },
+ closeEvent
+ );
+ const removeNotification = screen.getByTestId('btn-remove');
+ fireEvent.click(removeNotification);
+ expect(closeEvent).toHaveBeenCalledTimes(1);
+ });
+
+ it('should create a notification', () => {
+ notificationService.success(
+ DEFAULT_NOTIFICATION_OPTIONS.title,
+ DEFAULT_NOTIFICATION_OPTIONS.message
+ );
+ expect(screen.getByTestId('ion-notification')).toBeTruthy();
+ });
+
+ it.each(['title', 'message'])(
+ 'should render a notification with default %s',
+ (key) => {
+ expect(
+ screen.getByText(DEFAULT_NOTIFICATION_OPTIONS[key])
+ ).toBeInTheDocument();
+ }
+ );
+});
+
+describe('NotificationService -> notification types', () => {
+ let notificationService: IonNotificationService;
+
+ let currentIndex = 1;
+
+ let notificationsOnScreen = 5;
+
+ const indexToRemove = [1, 2, 0];
+
+ const NOTIFICATIONS_CALLS = {
+ success: (): void => {
+ notificationService.success('teste', 'teste', {}, () => {
+ return true;
+ });
+ },
+ info: (): void => {
+ notificationService.info('teste', 'teste', {}, () => {
+ return true;
+ });
+ },
+ warning: (): void => {
+ notificationService.warning('teste', 'teste', {}, () => {
+ return true;
+ });
+ },
+ negative: (): void => {
+ notificationService.error('teste', 'teste', {}, () => {
+ return true;
+ });
+ },
+ };
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [TestModule],
+ }).compileComponents();
+
+ notificationService = TestBed.get(IonNotificationService);
+ });
+
+ it.each(NOTIFICATION_TYPES)('should create %s notification', async (type) => {
+ NOTIFICATIONS_CALLS[type]();
+ const iconType = screen.getAllByTestId('notification-icon');
+ expect(iconType[currentIndex]).toHaveClass(NOTIFICATION_ICONS[type]);
+ currentIndex += 1;
+ });
+
+ it.each(indexToRemove)(
+ 'should remove multiple notifications',
+ async (index) => {
+ const elements = document.getElementsByTagName('ion-notification');
+ const removeNotification = screen.getAllByTestId('btn-remove');
+ fireEvent.click(removeNotification[index]);
+ notificationsOnScreen -= 1;
+ expect(elements).toHaveLength(notificationsOnScreen);
+ }
+ );
+
+ it.each(NOTIFICATION_TYPES)(
+ 'should add ionOnClose subscription when %s notification is created',
+ async (type) => {
+ notificationService.addCloseEventEmitter = jest.fn();
+ NOTIFICATIONS_CALLS[type]();
+ expect(notificationService.addCloseEventEmitter).toHaveBeenCalledTimes(1);
+ }
+ );
+});
diff --git a/projects/ion/src/lib/notification/service/notification.service.ts b/projects/ion/src/lib/notification/service/notification.service.ts
new file mode 100644
index 000000000..259f00bf9
--- /dev/null
+++ b/projects/ion/src/lib/notification/service/notification.service.ts
@@ -0,0 +1,193 @@
+import { StatusType } from './../../core/types/status';
+import { NotificationConfigOptions } from './../../core/types/notification';
+import { IonNotificationComponent } from './../component/notification.component';
+import {
+ Injectable,
+ ComponentRef,
+ Inject,
+ ComponentFactoryResolver,
+ ApplicationRef,
+ Injector,
+} from '@angular/core';
+import { DOCUMENT } from '@angular/common';
+import { Subject } from 'rxjs';
+import { SafeAny } from './../../utils/safe-any';
+import { IonNotificationContainerComponent } from './notification.container.component';
+
+enum NOTIFICATION_TYPES {
+ success = 'success',
+ info = 'info',
+ warning = 'warning',
+ negative = 'negative',
+}
+
+@Injectable({
+ providedIn: 'root',
+})
+export class IonNotificationService {
+ private notificationContainerComponentRef: ComponentRef;
+ private componentSubscriber!: Subject;
+
+ constructor(
+ @Inject(DOCUMENT) private document: SafeAny,
+ private componentFactoryResolver: ComponentFactoryResolver,
+ private appRef: ApplicationRef,
+ private injector: Injector
+ ) {}
+
+ public success(
+ title: string,
+ message: string,
+ options?: NotificationConfigOptions,
+ closeEventCall?: () => void
+ ): void {
+ this.showNotification(
+ title,
+ message,
+ options,
+ NOTIFICATION_TYPES.success,
+ closeEventCall ? closeEventCall : undefined
+ );
+ }
+
+ public info(
+ title: string,
+ message: string,
+ options?: NotificationConfigOptions,
+ closeEventCall?: () => void
+ ): void {
+ this.showNotification(
+ title,
+ message,
+ options,
+ NOTIFICATION_TYPES.info,
+ closeEventCall ? closeEventCall : undefined
+ );
+ }
+
+ public warning(
+ title: string,
+ message: string,
+ options?: NotificationConfigOptions,
+ closeEventCall?: () => void
+ ): void {
+ this.showNotification(
+ title,
+ message,
+ options,
+ NOTIFICATION_TYPES.warning,
+ closeEventCall ? closeEventCall : undefined
+ );
+ }
+
+ public error(
+ title: string,
+ message: string,
+ options?: NotificationConfigOptions,
+ closeEventCall?: () => void
+ ): void {
+ this.showNotification(
+ title,
+ message,
+ options,
+ NOTIFICATION_TYPES.negative,
+ closeEventCall ? closeEventCall : undefined
+ );
+ }
+
+ addCloseEventEmitter(
+ notification: ComponentRef,
+ closeEvent: () => void
+ ): void {
+ notification.instance.ionOnClose.subscribe(() => {
+ closeEvent();
+ });
+ }
+
+ private createComponentView(
+ viewRef: ComponentRef
+ ): void {
+ this.appRef.attachView(viewRef.hostView);
+ viewRef.changeDetectorRef.detectChanges();
+
+ const notificationElement = viewRef.location.nativeElement;
+ this.document.body.appendChild(notificationElement);
+
+ this.componentSubscriber = new Subject();
+ this.componentSubscriber.asObservable();
+ }
+
+ private createNotificationContainer(): void {
+ const containerRef = this.componentFactoryResolver
+ .resolveComponentFactory(IonNotificationContainerComponent)
+ .create(this.injector);
+
+ this.notificationContainerComponentRef = containerRef;
+
+ this.createComponentView(this.notificationContainerComponentRef);
+ }
+
+ private createNotificationInstance(): ComponentRef {
+ return this.componentFactoryResolver
+ .resolveComponentFactory(IonNotificationComponent)
+ .create(this.injector);
+ }
+
+ private showNotification(
+ title: string,
+ message: string,
+ options: NotificationConfigOptions,
+ type: StatusType = 'success',
+ closeEventCall?: () => void
+ ): void {
+ if (!this.notificationContainerComponentRef)
+ this.createNotificationContainer();
+
+ const notification = this.createNotificationInstance();
+
+ this.configNotification(
+ notification.instance,
+ title,
+ message,
+ options,
+ type
+ );
+
+ this.instanceNotification(notification);
+
+ if (closeEventCall) {
+ this.addCloseEventEmitter(notification, closeEventCall);
+ }
+ }
+
+ private configNotification(
+ notification: IonNotificationComponent,
+ title: string,
+ message: string,
+ options: NotificationConfigOptions,
+ type: StatusType = 'success'
+ ): void {
+ notification.title = title;
+ notification.message = message;
+ notification.type = type;
+
+ if (options) {
+ Object.assign(notification, options);
+ }
+ }
+
+ private instanceNotification(
+ notification: ComponentRef
+ ): void {
+ notification.hostView.detectChanges();
+ notification.changeDetectorRef.detectChanges();
+
+ this.notificationContainerComponentRef.instance.addNotification(
+ notification
+ );
+
+ this.notificationContainerComponentRef.changeDetectorRef.detectChanges();
+
+ return this.componentSubscriber.next();
+ }
+}
diff --git a/projects/ion/src/public-api.ts b/projects/ion/src/public-api.ts
index 14e04f12c..f7cd9a051 100644
--- a/projects/ion/src/public-api.ts
+++ b/projects/ion/src/public-api.ts
@@ -28,6 +28,7 @@ export * from './lib/modal/modal.module';
export * from './lib/modal/modal.service';
export * from './lib/no-data/no-data.module';
export * from './lib/notification/notification.module';
+export * from './lib/notification/service/notification.service';
export * from './lib/pagination/pagination.module';
export * from './lib/picker/date-picker/date-picker.module';
export * from './lib/popconfirm/popconfirm.module';
diff --git a/stories/Notification.stories.ts b/stories/Notification.stories.ts
index bd3e00e2c..ae45a1981 100644
--- a/stories/Notification.stories.ts
+++ b/stories/Notification.stories.ts
@@ -1,7 +1,40 @@
import { Story, Meta } from '@storybook/angular/types-6-0';
-import { IonNotificationComponent } from '../projects/ion/src/lib/notification/notification.component';
+import { IonNotificationComponent } from '../projects/ion/src/lib/notification/component/notification.component';
+import {
+ IonIconModule,
+ IonNotificationModule,
+ IonSharedModule,
+} from '../projects/ion/src/public-api';
+import { OpenNotificationButtonComponent } from '../projects/ion/src/lib/notification/mock/open-notification-mock.component';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
import { moduleMetadata } from '@storybook/angular';
-import { IonIconModule } from '../projects/ion/src/public-api';
+
+const Template: Story = (
+ args: IonNotificationComponent
+) => ({
+ component: IonNotificationComponent,
+ props: args,
+});
+
+const basicTemplate: Story = (
+ args: IonNotificationComponent
+) => ({
+ component: OpenNotificationButtonComponent,
+ props: {
+ ...args,
+ },
+ moduleMetadata: {
+ declarations: [OpenNotificationButtonComponent],
+ imports: [
+ CommonModule,
+ FormsModule,
+ IonSharedModule,
+ IonNotificationModule,
+ ],
+ entryComponents: [OpenNotificationButtonComponent],
+ },
+});
export default {
title: 'Ion/Feedback/Notification',
@@ -11,14 +44,12 @@ export default {
imports: [IonIconModule],
}),
],
-} as Meta;
+} as Meta;
-const Template: Story = (
- args: IonNotificationComponent
-) => ({
- component: IonNotificationComponent,
- props: args,
-});
+export const Service = basicTemplate.bind({});
+Service.args = {
+ componentToBody: OpenNotificationButtonComponent,
+};
export const Basic = Template.bind({});
Basic.args = {