Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: create notification component #40

Open
wants to merge 24 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
fd3eb90
feat: create notification component
wermeson-lopes-brisa Feb 2, 2024
6c73e1a
feat: adjustment at class variable
wermeson-lopes-brisa Feb 5, 2024
efbf414
feat: adjustment on service and his test
wermeson-lopes-brisa Feb 5, 2024
f9104b7
feat: adjustments checked by lint
wermeson-lopes-brisa Feb 5, 2024
444cfde
Merge branch 'main' of github.com:Brisanet/ion-plus into 39-create-no…
wermeson-lopes-brisa Feb 5, 2024
b9b250f
feat: new adjustments at css classes an types
wermeson-lopes-brisa Feb 6, 2024
bbe29e7
feat: change duplicate status icon
wermeson-lopes-brisa Feb 6, 2024
5c3d174
feat: adjustment at test
wermeson-lopes-brisa Feb 6, 2024
e277b09
Merge branch 'main' into 39-create-notification-component
wermeson-lopes-brisa Feb 7, 2024
6d37cfd
Merge branch 'main' into 39-create-notification-component
wermeson-lopes-brisa Feb 8, 2024
67e7e24
feat: adjustments at typing
wermeson-lopes-brisa Feb 8, 2024
1e138c0
Merge branch 'main' into 39-create-notification-component
iurynogueira Feb 9, 2024
161d0ea
feat: update in file organization and change story description
wermeson-lopes-brisa Feb 12, 2024
459df5d
Merge branch 'main' of github.com:Brisanet/ion-plus into 39-create-no…
wermeson-lopes-brisa Feb 12, 2024
770d39b
Merge branch 'main' into 39-create-notification-component
wermeson-lopes-brisa Feb 14, 2024
06b9ac6
Merge branch 'main' of github.com:Brisanet/ion-plus into 39-create-no…
wermeson-lopes-brisa Feb 19, 2024
a22d81e
reorganize public and private methods
wermeson-lopes-brisa Feb 19, 2024
931c02f
changed for only re-run lint test
wermeson-lopes-brisa Feb 20, 2024
d812125
Merge branch 'main' of github.com:Brisanet/ion-plus into 39-create-no…
wermeson-lopes-brisa Feb 20, 2024
66b25ad
Merge branch 'main' into 39-create-notification-component
wermeson-lopes-brisa Feb 20, 2024
38e8e7b
Merge branch 'main' of github.com:Brisanet/ion-plus into 39-create-no…
danilo-moreira-brisa Jul 2, 2024
252710b
refactor: use signals on ion-notification
danilo-moreira-brisa Jul 2, 2024
96d4584
Merge branch 'main' into 39-create-notification-component
danilo-moreira-brisa Jul 29, 2024
3e17c18
Merge branch 'main' into 39-create-notification-component
danilo-moreira-brisa Aug 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<div
[class]="'ion-notification ' + fadeIn()"
data-testid="ion-notification"
(mouseenter)="mouseEnter()"
(mouseleave)="mouseLeave()"
#notificationRef>
<section class="ion-notification__inner">
<ion-icon
data-testid="notification-icon"
class="ion-notification__icon"
[type]="icon() || 'check-solid'"
[color]="iconColor()" />
<div class="ion-notification__text">
<strong class="ion-notification__title">
{{ title() }}
</strong>
<p class="ion-notification__message">{{ message() }}</p>
</div>
</section>

<ion-icon
data-testid="btn-remove"
type="close"
[size]="16"
class="ion-notification__close-icon"
(click)="closeNotification()" />
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
@import '../../../styles/index.scss';
@import '../../utils/fadeAnimations.scss';

.ion-notification {
display: flex;
flex-direction: row;
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),
0px 0px 2px rgba(0, 0, 0, 0.15);
border-radius: 8px;
font-family: 'Source Sans Pro', sans-serif;
font-style: normal;

&__inner {
display: flex;
}

&__text {
gap: 4px;
display: flex;
flex-direction: column;
}

&__title {
font-weight: 600;
font-size: 16px;
line-height: 24px;
color: $neutral-8;
}

&__message {
font-weight: 400;
font-size: 14px;
line-height: 20px;
color: $neutral-7;
margin: 0;
}

&__icon {
margin-right: spacing(1);
}

&__close-icon {
cursor: pointer;
::ng-deep svg {
fill: $primary-color;
}
}
}

ion-icon {
display: inline-block;
}

.ion-notification:nth-child(2) {
top: 30px;
}

.ion-notification:nth-child(3) {
top: 30px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import { fireEvent, render, screen } from '@testing-library/angular';
import { SafeAny } from '../../utils/safe-any';
import { StatusType, statusColor } from '../../utils/statusTypes';
import { IonNotificationProps } from '../types';
import { IonNotificationComponent } from './notification.component';

const indexChangeMock = jest.fn();

const defaultNotification: IonNotificationProps = {
title: 'Editado',
message: 'cadastro',
type: 'success' as StatusType,
ionOnClose: {
emit: indexChangeMock,
} as SafeAny,
};

const sut = async (
customProps: IonNotificationProps = defaultNotification
): Promise<void> => {
const { ionOnClose, ...rest } = customProps;
await render(IonNotificationComponent, {
componentInputs: rest,
componentOutputs: {
ionOnClose,
},
});
};

describe('IonNotificationComponent', () => {
it('should show title', async () => {
await sut();
expect(screen.getByText(defaultNotification.title)).toBeInTheDocument();
});

it('should show message', async () => {
await sut();
expect(screen.getByText(defaultNotification.message)).toBeInTheDocument();
});

it('should render close icon', async () => {
await sut();
expect(document.getElementById(`ion-icon-close`)).toBeInTheDocument();
});

it('should render success icon by default', async () => {
await sut({
title: 'Editado',
message: 'cadastro',
type: 'success',
ionOnClose: {
emit: indexChangeMock,
} as SafeAny,
});
expect(document.getElementById('ion-icon-check-solid')).toBeInTheDocument();
});

it('should render a custom icon', async () => {
const icon = 'pencil';
await sut({
...defaultNotification,
icon,
});
expect(document.getElementById(`ion-icon-${icon}`)).toBeInTheDocument();
});

it('should render a custom icon in gray scale', async () => {
const icon = 'star-solid';
await sut({
...defaultNotification,
icon,
type: 'neutral',
});
expect(screen.getByTestId('notification-icon')).toHaveClass(
'ion-notification__icon'
);
});

it.each([
{
type: 'success',
icon: 'check-solid',
},
{
type: 'info',
icon: 'info-solid',
},
{
type: 'warning',
icon: 'exclamation-solid',
},
{
type: 'negative',
icon: 'close-solid',
},
{
type: 'neutral',
icon: 'pencil',
},
])('should render $type class and $icon icon', async ({ type, icon }) => {
await sut({
...defaultNotification,
type: type as StatusType,
icon: icon,
});
expect(document.getElementById(`ion-icon-${icon}`)).toBeInTheDocument();
expect(screen.getByTestId('notification-icon')).toHaveAttribute(
'ng-reflect-color',
statusColor[type as StatusType]
);
});

it.each(['title', 'message'])(
'should remove %s notification of screen',
async () => {
await sut();
const btnRemove = screen.getByTestId('btn-remove');
fireEvent.click(btnRemove);
await sleep(1000);
expect(screen.queryAllByText(defaultNotification.title)).toHaveLength(0);
}
);

it('should not auto close when is fixed', async () => {
await sut({ ...defaultNotification, fixed: true });
await sleep(2000);
expect(screen.queryAllByText(defaultNotification.message)).toHaveLength(1);
});
});

describe('Time by words', () => {
it('should emit event when call closeNotification function', async () => {
const onCloseFunction = { emit: jest.fn() };
await sut({
...defaultNotification,
fixed: true,
ionOnClose: onCloseFunction as SafeAny,
});
jest.spyOn(onCloseFunction, 'emit');
const closeButton = document.getElementById(`ion-icon-close`);
fireEvent.click(closeButton || new HTMLElement());
expect(onCloseFunction.emit).toHaveBeenCalledTimes(1);
});

describe('Time by words', () => {
it('should not has timer when is fixed and mouse enter', async () => {
await sut({ ...defaultNotification, fixed: true });
const notificationIcon = screen.getByTestId('ion-notification');
fireEvent.mouseEnter(notificationIcon);
expect(screen.queryAllByText(defaultNotification.message)).toHaveLength(
1
);
});

it('should remove component after 2s', async () => {
await sut();
await sleep(3000);
expect(screen.queryAllByText(defaultNotification.message)).toHaveLength(
0
);
});

it('should not remove the component when on mouse enter', async () => {
await sut();
const notificationIcon = screen.getByTestId('ion-notification');
fireEvent.mouseEnter(notificationIcon);
await sleep(2000);
expect(screen.queryAllByText(defaultNotification.message)).toHaveLength(
1
);
});

it('should not remove the component when on mouse leave by 500ms', async () => {
await sut();
const notificationIcon = screen.getByTestId('ion-notification');
fireEvent.mouseEnter(notificationIcon);
await sleep(1000);
fireEvent.mouseLeave(notificationIcon);
await sleep(500);
expect(screen.queryAllByText(defaultNotification.message)).toHaveLength(
1
);
});
});
});

const sleep = (ms: number): Promise<unknown> => {
return new Promise(resolve => setTimeout(resolve, ms));
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { CommonModule, NgClass } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
ElementRef,
OnInit,
computed,
input,
model,
output,
viewChild,
} from '@angular/core';
import { Subscription } from 'rxjs';
import { IonIconComponent } from '../../icon';
import { setTimer } from '../../utils/setTimer';
import { statusColor, statusIcon } from '../../utils/statusTypes';
import { IonNotificationProps } from '../types';

@Component({
standalone: true,
selector: 'ion-notification',
imports: [CommonModule, IonIconComponent, NgClass],
templateUrl: './notification.component.html',
styleUrls: ['./notification.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class IonNotificationComponent implements OnInit {
title = input.required<IonNotificationProps['title']>();
message = input.required<IonNotificationProps['message']>();
type = input<IonNotificationProps['type']>('success');
icon = model<IonNotificationProps['icon']>();
fixed = input<IonNotificationProps['fixed']>(false);
fadeIn = input<IonNotificationProps['fadeIn']>('fadeIn');
fadeOut = input<IonNotificationProps['fadeOut']>('fadeOut');

notification = viewChild<ElementRef>('notificationRef');

ionOnClose = output();

iconColor = computed(() => statusColor[this.type()]);

private timer$!: Subscription;

ngOnInit(): void {
this.setIcon();
this.closeAuto();
}

public timeByWords(message: string = ''): number {
const wordsBySecond = 3;

// margin is one second
const marginOfError = 1;
const second = 1000;
const result = message.split(' ').length / wordsBySecond + marginOfError;
return Number(result.toFixed(0)) * second;
}

public closeNotification(): void {
this.notification()!.nativeElement.classList.add(this.fadeOut());
this.ionOnClose.emit();
setTimer().subscribe(() => {
this.notification()!.nativeElement.remove();
});
}

public closeAuto(closeIn: number = this.timeByWords(this.message())): void {
if (this.fixed()) {
return;
}
this.timer$ = setTimer(closeIn).subscribe(() => {
this.closeNotification();
});
}

public mouseEnter(): void {
if (this.fixed()) {
return;
}
this.timer$.unsubscribe();
}

public mouseLeave(): void {
this.closeAuto();
}

private setIcon(): void {
if (this.icon()) {
return;
}
this.icon.set(statusIcon[this.type()]);
}
}
1 change: 1 addition & 0 deletions projects/ion/src/lib/notification/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './public-api';
Loading
Loading