Skip to content

Commit

Permalink
Merge pull request #41 from josemarluedke/feat/impl-notificationst
Browse files Browse the repository at this point in the history
Implement Notifications
  • Loading branch information
josemarluedke authored Mar 3, 2020
2 parents 20abca9 + bc2c17d commit a176169
Show file tree
Hide file tree
Showing 42 changed files with 1,355 additions and 29 deletions.
6 changes: 3 additions & 3 deletions packages/buttons/tests/dummy/app/templates/application.hbs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<div class="container p-4 mx-auto ">
<div class="container p-4 mx-auto">
<div class="flex flex-wrap items-center justify-between mb-6">
<h2 class="inline-block text-3xl font-bold text-teal-900">
<h2 class="inline-block text-3xl font-bold text-teal-600">
@frontile/buttons
</h2>

<a href="/tests?hidepassed" class="px-4 py-2 border border-teal-600 rounded hover:bg-teal-700 hover:text-white">
<a href="/tests?hidepassed" class="px-4 py-2 text-teal-600 border border-teal-600 rounded hover:bg-teal-700 hover:text-white">
Go to Tests
</a>
</div>
Expand Down
1 change: 1 addition & 0 deletions packages/buttons/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"dummy/tests/*": ["tests/*"],
"dummy/*": ["tests/dummy/app/*", "app/*"],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<div class="container mx-auto p-4 ">
<div class="flex flex-wrap justify-between items-center mb-6">
<h2 class="font-bold text-3xl inline-block text-teal-900">
<div class="container p-4 mx-auto">
<div class="flex flex-wrap items-center justify-between mb-6">
<h2 class="inline-block text-3xl font-bold text-teal-600">
@frontile/changeset-form
</h2>

<a href="/tests?hidepassed" class="px-4 py-2 border border-teal-600 rounded hover:bg-teal-700 hover:text-white">
<a href="/tests?hidepassed" class="px-4 py-2 text-teal-600 border border-teal-600 rounded hover:bg-teal-700 hover:text-white">
Go to Tests
</a>
</div>
Expand Down
1 change: 1 addition & 0 deletions packages/changeset-form/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"dummy/tests/*": ["tests/*"],
"dummy/*": ["tests/dummy/app/*", "app/*"],
Expand Down
1 change: 1 addition & 0 deletions packages/core/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"dummy/tests/*": ["tests/*"],
"dummy/*": ["tests/dummy/app/*", "app/*"],
Expand Down
8 changes: 4 additions & 4 deletions packages/forms/tests/dummy/app/templates/application.hbs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<div class="container mx-auto p-4 ">
<div class="flex flex-wrap justify-between items-center mb-6">
<h2 class="font-bold text-3xl inline-block text-teal-900">
<div class="container p-4 mx-auto">
<div class="flex flex-wrap items-center justify-between mb-6">
<h2 class="inline-block text-3xl font-bold text-teal-600">
@frontile/forms
</h2>

<a href="/tests?hidepassed" class="px-4 py-2 border border-teal-600 rounded hover:bg-teal-700 hover:text-white">
<a href="/tests?hidepassed" class="px-4 py-2 text-teal-600 border border-teal-600 rounded hover:bg-teal-700 hover:text-white">
Go to Tests
</a>
</div>
Expand Down
1 change: 1 addition & 0 deletions packages/forms/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"dummy/tests/*": ["tests/*"],
"dummy/*": ["tests/dummy/app/*", "app/*"],
Expand Down
49 changes: 49 additions & 0 deletions packages/notifications/addon/-private/manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import Notification from './notification';
import Timer from './timer';
import { tracked } from '@glimmer/tracking';
import { later } from '@ember/runloop';
import { NotificationOptions } from './types';

export default class NotificationsManager {
@tracked notifications: Notification[] = [];

add(message: string, options: NotificationOptions = {}): Notification {
const notification = new Notification(message, options);
this.notifications = [...this.notifications, notification];

if (options.preserve !== true) {
this.setupAutoRemoval(notification, notification.duration);
}
return notification;
}

remove(notification?: Notification): void {
if (!notification) {
return;
}

notification.isRemoving = true;

later(
this,
() => {
this.notifications = this.notifications.filter(n => {
return n !== notification;
});
},
notification.transitionDuration
);
}

removeAll(): void {
this.notifications.forEach(notification => {
this.remove(notification);
});
}

private setupAutoRemoval(notification: Notification, duration: number): void {
notification.timer = new Timer(duration, () => {
this.remove(notification);
});
}
}
46 changes: 46 additions & 0 deletions packages/notifications/addon/-private/notification.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { tracked } from '@glimmer/tracking';
import Timer from './timer';
import { NotificationOptions, CustomAction } from './types';
import config from 'ember-get-config';
import { getWithDefault } from '@ember/object';

function getConfigOption<T extends keyof NotificationOptions>(
key: T,
defaultValue: NonNullable<NotificationOptions[T]>
): NonNullable<NotificationOptions[T]> {
return getWithDefault(
config['@frontile/notifications'] || ({} as never),
key as never,
defaultValue as never
);
}

export default class Notification {
readonly message: string;
readonly transitionDuration: number;
readonly appearance: NonNullable<NotificationOptions['appearance']>;
readonly customActions?: CustomAction[];
readonly allowClosing: boolean;
readonly duration: number;

@tracked timer?: Timer;
@tracked isRemoving = false;

constructor(message: string, options: NotificationOptions = {}) {
this.message = message;
this.appearance =
options.appearance || getConfigOption('appearance', 'info');
this.customActions = options.customActions;
this.duration = options.duration || getConfigOption('duration', 5000);
this.transitionDuration =
typeof options.transitionDuration !== 'undefined'
? options.transitionDuration
: getConfigOption('transitionDuration', 200);

if (options.allowClosing === false) {
this.allowClosing = false;
} else {
this.allowClosing = true;
}
}
}
51 changes: 51 additions & 0 deletions packages/notifications/addon/-private/timer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { tracked } from '@glimmer/tracking';
import { later, cancel } from '@ember/runloop';
import { EmberRunTimer } from '@ember/runloop/types';
import { action } from '@ember/object';

export default class Timer {
@tracked remaining: number;
@tracked isRunning = true;

readonly onFinish: () => void;
private timer?: EmberRunTimer;
private start!: number;

constructor(duration: number, onFinish: () => void) {
this.remaining = duration;
this.onFinish = onFinish;
this.setup();
}

@action clear(): void {
this.isRunning = false;

if (this.timer) {
cancel(this.timer);
}
}

@action pause(): void {
this.clear();
this.remaining -= Date.now() - this.start;
}

@action resume(): void {
this.clear();
this.setup();
}

private setup(): void {
this.start = Date.now();
this.isRunning = true;

this.timer = later(
this,
() => {
this.onFinish();
this.isRunning = false;
},
this.remaining
);
}
}
38 changes: 38 additions & 0 deletions packages/notifications/addon/-private/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
export interface CustomAction {
label: string;
onClick: () => void;
}

export interface NotificationOptions {
/* If set to false, the close button will not be displayed.
*
* @defaultValue true
*/
allowClosing?: boolean;

/*
* @defaultValue false
*/
preserve?: boolean;

/*
* @defaultValue 5000
*/
duration?: number;

/*
* The duration for the transition on removal
* @defaultValue 200
*/
transitionDuration?: number;

/*
* @defaultValue 'info'
*/
appearance?: 'info' | 'success' | 'warning' | 'error';

/*
* @defaultValue undefined
*/
customActions?: CustomAction[];
}
Empty file.
49 changes: 49 additions & 0 deletions packages/notifications/addon/components/notification-card.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{{!-- template-lint-disable no-invalid-interactive --}}
{{!-- template-lint-disable no-unnecessary-concat --}}
{{!-- template-lint-disable no-inline-styles --}}
{{!-- template-lint-disable style-concatenation --}}
{{!-
appearances:
notification-card-info
notification-card-success
notification-card-warning
notification-card-error
-}}

<div
{{on "mouseenter" this.pause}}
{{on "mouseleave" this.resume}}
class="notification-card
notification-card-{{@notification.appearance}}{{if @notification.isRemoving "is-removing"}}"
style="{{this.styles}}"
...attributes
>
<div class="notification-card-message">
{{@notification.message}}
</div>

{{#if @notification.customActions}}
<div class="notification-card-custom-actions">
{{#each @notification.customActions as |customAction|}}
<button
type="button"
class="notification-card-custom-action-btn"
{{on "click" (fn this.handleClickCustomAction customAction)}}
>
{{customAction.label}}
</button>
{{/each}}
</div>
{{/if}}

{{#if @notification.allowClosing}}
<button
type="button"
class="notification-card-close-btn"
{{on "click" this.remove}}
>
<div class="notification-card-close-btn-icon"></div>
<VisuallyHidden>Close</VisuallyHidden>
</button>
{{/if}}
</div>
40 changes: 40 additions & 0 deletions packages/notifications/addon/components/notification-card.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import { htmlSafe } from '@ember/string';
import { NotificationsService, Notification, CustomAction } from '../.';

interface NotificationCardArgs {
notification: Notification;
}

export default class NotificationCard extends Component<NotificationCardArgs> {
@service notifications!: NotificationsService;

get styles(): unknown {
return htmlSafe(
`transition-duration: ${this.args.notification.transitionDuration}ms`
);
}

@action remove(): void {
this.notifications.remove(this.args.notification);
}

@action pause(): void {
if (this.args.notification.timer) {
this.args.notification.timer.pause();
}
}

@action resume(): void {
if (this.args.notification.timer) {
this.args.notification.timer.resume();
}
}

@action handleClickCustomAction(customAction: CustomAction): void {
customAction.onClick();
this.remove();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{{!--
Placements:
top-left
top-center
top-right
bottom-left
bottom-center
bottom-right
--}}

{{#if this.notifications.notifications}}
<div
class="notifications-container {{if @placement @placement "bottom-right"}}"
role="alert"
aria-live="assertive"
aria-atomic="true"
...attributes
>
{{#each this.notifications.notifications as |notification|}}
<NotificationCard @notification={{notification}} />
{{/each}}
</div>
{{/if}}
Loading

0 comments on commit a176169

Please sign in to comment.