diff --git a/src/components/notification/bl-notification.mdx b/src/components/notification/bl-notification.mdx new file mode 100644 index 00000000..3be36ef9 --- /dev/null +++ b/src/components/notification/bl-notification.mdx @@ -0,0 +1,88 @@ +import { Meta, Canvas, ArgsTable, Story } from "@storybook/addon-docs"; +import * as NotificationStories from "./bl-notification.stories"; + + + +# Notification + +[ADR](https://github.com/Trendyol/baklava/issues/141) +[Figma](https://www.figma.com/file/RrcLH0mWpIUy4vwuTlDeKN/Baklava-Design-Guide?node-id=2790%3A13792) + +Notifications are messages that communicate information to the user. + +## Design Rules + +- Notification **has fixed width (396px)** by default. +- Notifications are **temporary** by default but can be set permanent. +- Temporary notifications are **dismissed automatically** after a certain period of time. +- Temporary notifications remaning time will stop when hovered. +- It can be dismissed by swiping up on mobile. +- Notification takes position on top on small screens, and on top right on large screens. +- Multiple notifications would be visible with a vertical stack. New notifications will come to top on large screens, and will come to bottom on small screens. + +## Basic Usage + +The `bl-notification` component is a versatile tool for displaying notifications. It exposes two public methods: `addNotification` and `removeNotification`. + +The `addNotification` method accepts a notification object as a parameter and returns a notification object that includes the props, an id, and a remove method. The remove method is a wrapper that calls the `removeNotification` method with the id of the notification. + +The `removeNotification` method accepts a notification id as a parameter and returns a Promise that resolves when the notification removal animation is complete. + +### Adding a Notification + +A notification could be added by calling the `addNotification` method with a notification object. + + + +#### Notification Object + + + +### Removing a Notification + +A notification could be removed by calling the `removeNotification` method with the notification's id. + + + +#### Await for Removal + +The `removeNotification` method returns a Promise that resolves when the notification removal animation is complete. This could be used to await the removal of a notification. + + + +### Actions + +A notification could have a primary and a secondary action. These actions are displayed as buttons on the notification. + + + +#### Removing a Notification on Action Click + +A notification could be removed by calling notification's remove method. + + + +### Permanent + +A notification could be permanent. Permanent notifications are not dismissed automatically. They could be dismissed by clicking the close button. + + + +### Variants + +A notification could have one of the following variants: info, success, warning, error. The variant changes the color of the notification. + + + +## Reference + + diff --git a/src/components/notification/bl-notification.stories.mdx b/src/components/notification/bl-notification.stories.mdx deleted file mode 100644 index c7991440..00000000 --- a/src/components/notification/bl-notification.stories.mdx +++ /dev/null @@ -1,40 +0,0 @@ -import { Meta, Canvas, Story } from '@storybook/addon-docs'; -import { html } from 'lit'; -import { ifDefined } from 'lit/directives/if-defined.js'; -import { repeat } from 'lit/directives/repeat.js'; - - - - -# BlNotification - -`BlNotification` is a component to display notifications. It supports adding and removing notifications dynamically. - -## Props - -- `noAnimation`: Disable animations. It will not be possible to use animations if the user has disabled them. Animations will respect the user's preferences regardless of this property. -- `duration`: Sets the default duration of notifications in seconds - -## Methods - -- `addNotification(props: NotificationProps)`: Adds a notification to the list of notifications. -- `removeNotification(id: string)`: Removes a notification from the list of notifications. - - - - {() => html` - - `} - - diff --git a/src/components/notification/bl-notification.stories.ts b/src/components/notification/bl-notification.stories.ts new file mode 100644 index 00000000..3f6d3a14 --- /dev/null +++ b/src/components/notification/bl-notification.stories.ts @@ -0,0 +1,332 @@ +import { html } from "lit"; +import { ifDefined } from "lit/directives/if-defined.js"; +import { Meta, StoryObj } from "@storybook/web-components"; +import type BlNotification from "./bl-notification"; +import type { NotificationProps, Notification } from "./bl-notification"; + +const meta: Meta = { + title: "Components/Notification", + component: "bl-notification", + argTypes: { + noAnimation: { + control: "boolean", + }, + duration: { + control: "number", + }, + }, +}; + +export default meta; + +const addNotification = (selector: string, options: NotificationProps) => { + const el = document.querySelector(selector) as BlNotification; + + return el?.addNotification(options); +}; + +export type NotificationArgs = { + noAnimation: boolean; + duration: number; +}; + +export type Story = StoryObj; + +export const AddExample: Story = { + render(args: NotificationArgs) { + return html` + + Add Notification + + + `; + }, + play: () => { + addNotification("#basic", { + description: "Notification Description", + }); + }, +}; + +export const RemoveExample: Story = { + render(args: NotificationArgs) { + return html` + + Remove Notification + + + `; + }, +}; + +export const RemoveAwaitExample: Story = { + render(args: NotificationArgs) { + return html` + + Remove Notifications + + + `; + }, +}; + +export const ActionsExample: Story = { + render(args: NotificationArgs) { + return html` + + Add Notification + + + `; + }, + play: () => { + addNotification("#basic", { + caption: "Notification Caption", + description: "Notification Description", + permanent: true, + primaryAction: { + label: "Primary Action", + onClick: () => { + window.alert("Primary Action Clicked"); + }, + }, + secondaryAction: { + label: "Secondary Action", + onClick: () => { + window.alert("Secondary Action Clicked"); + }, + }, + }); + }, +}; + +export const ActionsRemoveExample: Story = { + render(args: NotificationArgs) { + return html` + + Add Notification + + + `; + }, + play: () => { + addNotification("#basic", { + caption: "Notification Caption", + description: "Notification Description", + permanent: true, + primaryAction: { + label: "Primary Action", + onClick: (notification: Notification) => { + notification.remove(); + }, + }, + secondaryAction: { + label: "Secondary Action", + onClick: (notification: Notification) => { + notification.remove(); + }, + }, + }); + }, +}; + +export const PermanentExample: Story = { + render(args: NotificationArgs) { + return html` + + Add Notification + + + `; + }, + play: () => { + addNotification("#basic", { + caption: "Notification Caption", + description: "Notification Description", + permanent: true, + }); + }, +}; + +export const VariantsExample: Story = { + render(args: NotificationArgs) { + return html` + + Add Notifications + + + `; + }, + play: () => { + const variants = ["info", "success", "warning", "error"] as const; + + for (const variant of variants) { + addNotification("#basic", { + caption: variant, + description: variant, + permanent: true, + variant, + }); + } + }, +}; diff --git a/src/components/notification/bl-notification.test.ts b/src/components/notification/bl-notification.test.ts index cb93b6c4..e48a59bc 100644 --- a/src/components/notification/bl-notification.test.ts +++ b/src/components/notification/bl-notification.test.ts @@ -91,11 +91,11 @@ describe("bl-notification", () => { .to.have.attribute("style") .match(/height: \d+px/); assert.isTrue(notificationEl.classList.contains("removing")); - assert.lengthOf(el.notifications, 1); + assert.lengthOf(el.notificationList, 1); await animationPromise; await el.updateComplete; - assert.lengthOf(el.notifications, 0); + assert.lengthOf(el.notificationList, 0); }); it("should return false if notification does not exist", async () => { @@ -350,7 +350,7 @@ describe("bl-notification", () => { duration="5" icon="academy" variant="info" - id="${el.notifications[0].id}" + id="${el.notificationList[0].id}" > test @@ -382,7 +382,7 @@ describe("bl-notification", () => { await el.updateComplete; - assert.equal(el.notifications[0], notification); + assert.equal(el.notificationList[0], notification); }); it("should return functional remove method", async () => { @@ -404,7 +404,7 @@ describe("bl-notification", () => { await el.updateComplete; - assert.lengthOf(el.notifications, 0); + assert.lengthOf(el.notificationList, 0); }); }); }); @@ -463,7 +463,7 @@ describe("bl-notification", () => { sendTouchEvent(100, -100, notificationEl, "touchend"); - expect(removeSpy).to.have.been.calledOnceWith(el.notifications[0].id); + expect(removeSpy).to.have.been.calledOnceWith(el.notificationList[0].id); }); it("should not remove notification when user swipes up less than 50px", async () => { diff --git a/src/components/notification/bl-notification.ts b/src/components/notification/bl-notification.ts index 914c9209..d5cbe20c 100644 --- a/src/components/notification/bl-notification.ts +++ b/src/components/notification/bl-notification.ts @@ -41,9 +41,6 @@ export default class BlNotification extends LitElement { return [style]; } - @state() - notifications: Notification[] = []; - /** * Disable animations. * It will not be possible to use animations if the user has disabled them. @@ -58,6 +55,13 @@ export default class BlNotification extends LitElement { @property({ type: Number }) duration = 7; + @state() + private notifications: Notification[] = []; + + public get notificationList() { + return this.notifications; + } + private touchStartY = 0; public get touchStart() { @@ -79,9 +83,9 @@ export default class BlNotification extends LitElement { // TODO id generation const id = Math.random().toString(36).substr(2, 9); const notification: Notification = { - duration: this.duration, ...props, id, + duration: props.duration || this.duration, remove: () => this.removeNotification(id), }; diff --git a/src/components/notification/card/bl-notification-card.test.ts b/src/components/notification/card/bl-notification-card.test.ts index d126b7d9..332b0105 100644 --- a/src/components/notification/card/bl-notification-card.test.ts +++ b/src/components/notification/card/bl-notification-card.test.ts @@ -119,6 +119,14 @@ describe("bl-notification-card", () => { }); expect(style.getPropertyValue("animation-play-state")).to.equal("paused"); }); + + it("should close automatically if duration is 0", async () => { + const el = await fixture( + html` Description ` + ); + + expect(el.closed).to.be.true; + }); }); describe("Permanent", () => { diff --git a/src/components/notification/card/bl-notification-card.ts b/src/components/notification/card/bl-notification-card.ts index f787af15..cd839365 100644 --- a/src/components/notification/card/bl-notification-card.ts +++ b/src/components/notification/card/bl-notification-card.ts @@ -56,7 +56,7 @@ export default class BlNotificationCard extends LitElement { /** * Sets notification variant. * @attr variant - * @type {AlertVariant} + * @type {NotificationVariant} * @default "info" */ @property({ reflect: true }) @@ -116,6 +116,11 @@ export default class BlNotificationCard extends LitElement { return; } + if (this.duration <= 0) { + this.close(CloseSource.DurationEnded); + return; + } + setTimeout(() => { this.shadowRoot?.querySelector(".remaining")?.addEventListener( "animationend", @@ -128,6 +133,7 @@ export default class BlNotificationCard extends LitElement { } private close(source: CloseSource) { + console.log("close"); const requestCloseEvent = this.onRequestClose({ source }, { cancelable: true }); if (requestCloseEvent.defaultPrevented) {