diff --git a/example/cypress/e2e/overlays/notification.cy.ts b/example/cypress/e2e/overlays/notification.cy.ts
new file mode 100644
index 0000000..88613d7
--- /dev/null
+++ b/example/cypress/e2e/overlays/notification.cy.ts
@@ -0,0 +1,41 @@
+// https://docs.cypress.io/api/introduction/api.html
+
+describe('notification', () => {
+ beforeEach(() => {
+ cy.visit('/notification');
+ });
+
+ it('toggles through button', () => {
+ cy.get('[data-cy="content"]').should('not.exist');
+ cy.get('[data-cy="visibility-toggle"]').click();
+ cy.get('[data-cy="content"]').should('exist');
+ cy.get('[data-cy="content"]').should('contain.text', 'This is a notification');
+ cy.get('[data-cy="visibility-toggle"]').click();
+ cy.get('[data-cy="content"]').should('not.exist');
+ });
+
+ it('dismisses by click', () => {
+ cy.get('[data-cy="content"]').should('not.exist');
+ cy.get('[data-cy="visibility-toggle"]').click();
+ cy.get('[data-cy="content"]').should('exist');
+ cy.get('[data-cy="content"]').click();
+ cy.get('[data-cy="content"]').should('not.exist');
+ });
+
+ it('does not dismisses by click if timeout is negative', () => {
+ cy.get('[data-cy="timeout"]').type('-1');
+ cy.get('[data-cy="content"]').should('not.exist');
+ cy.get('[data-cy="visibility-toggle"]').click();
+ cy.get('[data-cy="content"]').should('exist');
+ cy.get('[data-cy="content"]').click();
+ cy.get('[data-cy="content"]').should('exist');
+ });
+
+ it('auto dismisses on timeout', () => {
+ cy.get('[data-cy="timeout"]').type('100');
+ cy.get('[data-cy="content"]').should('not.exist');
+ cy.get('[data-cy="visibility-toggle"]').click();
+ cy.get('[data-cy="content"]').should('exist');
+ cy.get('[data-cy="content"]').should('not.exist');
+ });
+});
diff --git a/example/src/App.vue b/example/src/App.vue
index aab6121..75273d8 100644
--- a/example/src/App.vue
+++ b/example/src/App.vue
@@ -35,6 +35,7 @@ const navigation = ref([
{ to: { name: 'color-pickers' }, title: 'Color Picker' },
{ to: { name: 'auto-completes' }, title: 'Auto Completes' },
{ to: { name: 'navigation-drawers' }, title: 'Navigation Drawer' },
+ { to: { name: 'notification' }, title: 'Notification' },
],
},
{
diff --git a/example/src/router/index.ts b/example/src/router/index.ts
index d07f768..161ca70 100644
--- a/example/src/router/index.ts
+++ b/example/src/router/index.ts
@@ -157,6 +157,11 @@ const router = new VueRouter({
name: 'navigation-drawers',
component: () => import('@/views/NavigationDrawerView.vue'),
},
+ {
+ path: '/notification',
+ name: 'notification',
+ component: () => import('@/views/NotificationView.vue'),
+ },
{
path: '/breakpoint',
name: 'breakpoint',
diff --git a/example/src/views/NotificationView.vue b/example/src/views/NotificationView.vue
new file mode 100644
index 0000000..00a21a9
--- /dev/null
+++ b/example/src/views/NotificationView.vue
@@ -0,0 +1,63 @@
+
+
+
+
+
+ Notification
+
+
+
+ {{ visible ? 'Hide' : 'Show' }}
+
+
+
+
+
+
+
+ This is a notification
+
+
+
+
diff --git a/src/components/index.ts b/src/components/index.ts
index 62338fa..237b678 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -155,6 +155,7 @@ import {
type Props as NavigationDrawerProps,
default as RuiNavigationDrawer,
} from '@/components/overlays/navigation-drawer/NavigationDrawer.vue';
+import RuiNotification, { type NotificationProps } from '@/components/overlays/notification/Notification.vue';
import type {
TableColumn as DataTableColumn,
SortColumn as DataTableSortColumn,
@@ -202,6 +203,7 @@ export {
RuiColorPicker,
RuiAutoComplete,
RuiNavigationDrawer,
+ RuiNotification,
ProgressProps,
ChipProps,
TextFieldProps,
@@ -245,4 +247,5 @@ export {
ColorPickerProps,
AutoCompleteProps,
NavigationDrawerProps,
+ NotificationProps,
};
diff --git a/src/components/overlays/notification/Notification.spec.ts b/src/components/overlays/notification/Notification.spec.ts
new file mode 100644
index 0000000..7f7fe94
--- /dev/null
+++ b/src/components/overlays/notification/Notification.spec.ts
@@ -0,0 +1,103 @@
+import Vue from 'vue';
+import { mount } from '@vue/test-utils';
+import { describe, expect, it, vi } from 'vitest';
+import { TeleportPlugin } from '@/components/overlays/teleport-container';
+import Notification from '@/components/overlays/notification/Notification.vue';
+
+Vue.use(TeleportPlugin);
+
+function createWrapper(options?: any) {
+ return mount(Notification, {
+ ...options,
+ scopedSlots: {
+ default: `
Notification
`,
+ },
+ stubs: {
+ Transition: true,
+ },
+ });
+}
+
+describe('notification', () => {
+ it('renders properly', async () => {
+ const wrapper = createWrapper({
+ propsData: {
+ timeout: 0,
+ value: true,
+ },
+ });
+
+ await nextTick();
+ const notification = document.body.querySelector('#content') as HTMLDivElement;
+ expect(notification).toBeTruthy();
+ wrapper.destroy();
+ });
+
+ it('does not render if value is false', async () => {
+ const wrapper = createWrapper({
+ propsData: {
+ timeout: 0,
+ value: false,
+ },
+ });
+
+ await nextTick();
+ const notification = document.body.querySelector('#content') as HTMLDivElement;
+ expect(notification).toBeFalsy();
+ wrapper.destroy();
+ });
+
+ it('closes on click', async () => {
+ const wrapper = createWrapper({
+ propsData: {
+ timeout: 0,
+ value: true,
+ },
+ });
+
+ await nextTick();
+ const notification = document.body.querySelector('#content') as HTMLDivElement;
+ expect(notification).toBeTruthy();
+ notification.click();
+ await nextTick();
+ expect(wrapper.emitted()).toHaveProperty('input', [[false]]);
+ wrapper.destroy();
+ });
+
+ it('does not close on click if timeout is negative', async () => {
+ const wrapper = createWrapper({
+ propsData: {
+ timeout: -1,
+ value: true,
+ },
+ });
+
+ await nextTick();
+ const notification = document.body.querySelector('#content') as HTMLDivElement;
+ expect(notification).toBeTruthy();
+ notification.click();
+ await nextTick();
+ expect(wrapper.emitted()).toEqual({});
+ wrapper.destroy();
+ });
+
+ it('closes automatically after timeout', async () => {
+ vi.useFakeTimers();
+ const wrapper = createWrapper({
+ propsData: {
+ timeout: 5000,
+ value: true,
+ },
+ });
+
+ await nextTick();
+ vi.advanceTimersByTime(3000);
+ const notification = document.body.querySelector('#content') as HTMLDivElement;
+ expect(notification).toBeTruthy();
+ expect(wrapper.emitted()).toEqual({});
+ vi.advanceTimersByTime(2000);
+ expect(wrapper.emitted()).toHaveProperty('input', [[false]]);
+ wrapper.destroy();
+ vi.useRealTimers();
+ });
+});
diff --git a/src/components/overlays/notification/Notification.stories.ts b/src/components/overlays/notification/Notification.stories.ts
new file mode 100644
index 0000000..97ab2fd
--- /dev/null
+++ b/src/components/overlays/notification/Notification.stories.ts
@@ -0,0 +1,90 @@
+import Button from '@/components/buttons/button/Button.vue';
+import Card from '@/components/cards/Card.vue';
+import Notification, { type NotificationProps } from '@/components/overlays/notification/Notification.vue';
+import type { Meta, StoryFn, StoryObj } from '@storybook/vue';
+
+const render: StoryFn = args => ({
+ components: { Button, Card, Notification },
+ setup() {
+ const value = computed({
+ get() {
+ return args.value;
+ },
+ set(val) {
+ args.value = val;
+ },
+ });
+
+ return { args, value };
+ },
+ template: `
+
+
+
+ I am a notification
+
+
+ `,
+});
+
+const meta: Meta = {
+ args: {},
+ argTypes: {
+ theme: {
+ control: 'select',
+ options: ['light', 'dark'],
+ },
+ timeout: { control: 'number' },
+ width: { control: 'text' },
+ },
+ component: Notification,
+ parameters: {
+ docs: {
+ controls: { exclude: ['default'] },
+ },
+ },
+ render,
+ tags: ['autodocs'],
+ title: 'Components/Overlays/Notification',
+};
+
+type Story = StoryObj;
+
+export const Default: Story = {
+ args: {
+ timeout: 0,
+ value: false,
+ },
+};
+
+export const NonPersistent: Story = {
+ args: {
+ timeout: 0,
+ value: false,
+ },
+};
+
+export const Light: Story = {
+ args: {
+ theme: 'light',
+ timeout: 0,
+ value: false,
+ },
+};
+
+export const Dark: Story = {
+ args: {
+ theme: 'dark',
+ timeout: 0,
+ value: false,
+ },
+};
+
+export const Persistent: Story = {
+ args: {
+ timeout: -1,
+ value: false,
+ },
+};
+
+export default meta;
diff --git a/src/components/overlays/notification/Notification.vue b/src/components/overlays/notification/Notification.vue
new file mode 100644
index 0000000..768c43b
--- /dev/null
+++ b/src/components/overlays/notification/Notification.vue
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
+
+