From c33957d232cdc8e807a03e6c297b8ae709c75fab Mon Sep 17 00:00:00 2001 From: Konstantinos Paparas Date: Wed, 12 Jun 2024 10:42:31 +0200 Subject: [PATCH] feat: introduces Notification component (#207) --- .../cypress/e2e/overlays/notification.cy.ts | 41 +++++++ example/src/App.vue | 1 + example/src/router/index.ts | 5 + example/src/views/NotificationView.vue | 63 +++++++++++ src/components/index.ts | 3 + .../notification/Notification.spec.ts | 103 ++++++++++++++++++ .../notification/Notification.stories.ts | 90 +++++++++++++++ .../overlays/notification/Notification.vue | 80 ++++++++++++++ 8 files changed, 386 insertions(+) create mode 100644 example/cypress/e2e/overlays/notification.cy.ts create mode 100644 example/src/views/NotificationView.vue create mode 100644 src/components/overlays/notification/Notification.spec.ts create mode 100644 src/components/overlays/notification/Notification.stories.ts create mode 100644 src/components/overlays/notification/Notification.vue 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 @@ + + + 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 @@ + + +