├── .github
├── FUNDING.yml
└── workflows
│ ├── publish.yml
│ └── tests.yml
├── .gitignore
├── .husky
└── pre-commit
├── .prettierrc
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── astro-playground
├── .gitignore
├── astro.config.mjs
├── package.json
├── public
│ └── favicon.svg
├── src
│ ├── components
│ │ ├── AstroComponent.astro
│ │ ├── Notivue.vue
│ │ ├── ReactComponent.tsx
│ │ └── VueComponent.vue
│ ├── env.d.ts
│ ├── layouts
│ │ └── Layout.astro
│ ├── pages
│ │ ├── _app.ts
│ │ ├── about.astro
│ │ └── index.astro
│ └── styles
│ │ └── reset.css
└── tsconfig.json
├── package.json
├── packages
└── notivue
│ ├── .gitignore
│ ├── Notifications
│ ├── Notification.vue
│ ├── NotificationProgress.vue
│ ├── constants.ts
│ ├── icons
│ │ ├── CloseIcon.vue
│ │ ├── ErrorIcon.vue
│ │ ├── ErrorOutlineIcon.vue
│ │ ├── InfoIcon.vue
│ │ ├── InfoOutlineIcon.vue
│ │ ├── PromiseIcon.vue
│ │ ├── SuccessIcon.vue
│ │ ├── SuccessOutlineIcon.vue
│ │ └── index.ts
│ ├── notifications-progress.css
│ ├── notifications.css
│ ├── themes.ts
│ └── types.ts
│ ├── Notivue
│ ├── AriaLive.vue
│ ├── Notivue.vue
│ ├── NotivueImpl.vue
│ ├── composables
│ │ ├── useMouseEvents.ts
│ │ ├── useNotivueStyles.ts
│ │ ├── useReducedMotion.ts
│ │ ├── useResizeListObserver.ts
│ │ ├── useSizes.ts
│ │ ├── useTouchEvents.ts
│ │ ├── useWindowFocus.ts
│ │ └── useWindowSize.ts
│ ├── constants.ts
│ ├── types.ts
│ └── utils.ts
│ ├── NotivueKeyboard
│ ├── NotivueKeyboard.vue
│ ├── NotivueKeyboardImpl.vue
│ ├── constants.ts
│ ├── types.ts
│ ├── useKeyboardFocus.ts
│ ├── useLastFocused.ts
│ └── useNotivueKeyboard.ts
│ ├── NotivueSwipe
│ ├── NotivueSwipe.vue
│ ├── constants.ts
│ └── types.ts
│ ├── astro
│ ├── Notivue.vue
│ ├── createNotivue.ts
│ ├── push.ts
│ └── types.ts
│ ├── core
│ ├── animations.css
│ ├── constants.ts
│ ├── createInstance.ts
│ ├── createNotivue.ts
│ ├── createPush.ts
│ ├── createStore.ts
│ ├── createStoreWatchers.ts
│ ├── symbols.ts
│ ├── types.ts
│ ├── useStore.ts
│ └── utils.ts
│ ├── index.ts
│ ├── nuxt
│ ├── README.md
│ ├── index.d.ts
│ ├── module.cjs
│ ├── module.d.ts
│ ├── module.json
│ └── module.mjs
│ ├── package.json
│ ├── scripts
│ ├── verify-exports.js
│ └── verify-tarball.sh
│ ├── shared
│ ├── ClientOnly.ts
│ └── exports.js
│ ├── tsconfig.json
│ └── vite.config.ts
├── playground
├── .gitignore
├── .npmrc
├── app.vue
├── assets
│ ├── inter-v13-latin-500.woff2
│ ├── inter-v13-latin-700.woff2
│ ├── profile-picture.jpg
│ ├── pt-sans-narrow-v17-latin-700.woff2
│ ├── pt-sans-narrow-v17-latin-regular.woff2
│ └── style.css
├── components
│ ├── custom-notifications
│ │ ├── FriendRequestNotification.vue
│ │ ├── SimpleNotification.vue
│ │ └── UploadNotification.vue
│ ├── icons
│ │ ├── ArrowIcon.vue
│ │ ├── CloseIcon.vue
│ │ ├── CustomIcon.vue
│ │ ├── DestroyIcon.vue
│ │ ├── DismissIcon.vue
│ │ ├── InfoIcon.vue
│ │ ├── PromiseIcon.vue
│ │ ├── SuccessIcon.vue
│ │ ├── VueIcon.vue
│ │ └── WarnIcon.vue
│ ├── nav
│ │ ├── Nav.vue
│ │ ├── NavActions.vue
│ │ ├── NavNotificationsCustomization.vue
│ │ ├── NavNotificationsThemes.vue
│ │ ├── NavNotivueConfig.vue
│ │ ├── NavNotivuePosition.vue
│ │ ├── NavPushBuiltIn.vue
│ │ └── NavPushHeadless.vue
│ └── shared
│ │ ├── Background.server.vue
│ │ ├── Button.vue
│ │ ├── ButtonGroup.vue
│ │ └── QueueCount.vue
├── middleware
│ └── push.global.ts
├── nuxt.config.ts
├── package.json
├── plugins
│ └── store.ts
├── public
│ ├── icon.svg
│ └── og-image.jpg
├── tsconfig.json
└── utils
│ ├── date.ts
│ ├── head.ts
│ ├── misc.ts
│ └── store.ts
├── pnpm-workspace.yaml
└── tests
├── .gitignore
├── Notifications
├── accessibility.cy.ts
├── close-button.cy.ts
├── components
│ └── Notivue.vue
├── elements.cy.ts
└── themes.cy.ts
├── Notivue
├── accessibility.cy.ts
├── attributes.cy.ts
├── components
│ └── Notivue.vue
├── config-animations.cy.ts
├── config-duplicates.cy.ts
├── config-enqueue.cy.ts
├── config-limit.cy.ts
├── config-pause-on-hover.cy.ts
├── config-pause-on-touch.cy.ts
├── config-teleport.cy.ts
├── instance.cy.ts
├── prefers-reduced-motion.cy.ts
├── push-callbacks.cy.ts
├── push-methods.cy.ts
├── push-specific-options.cy.ts
├── slot-callbacks.cy.ts
├── slot-custom-push-options.cy.ts
├── slot-custom-push-props.cy.ts
├── slot-default-options.cy.ts
├── slot-global-options.cy.ts
├── slot-internal-properties.cy.ts
└── transitions.cy.ts
├── NotivueKeyboard
├── actions.cy.ts
├── components
│ ├── Candidate.vue
│ ├── Notivue.vue
│ └── Unqualified.vue
├── entering-stream.cy.ts
├── leaving-stream.cy.ts
├── props.cy.ts
└── queue.cy.ts
├── NotivueSwipe
├── clear.cy.ts
├── components
│ └── Notivue.vue
├── debounce.cy.ts
├── props.cy.ts
├── styles.cy.ts
└── timeouts.cy.ts
├── config
└── update-config.test.ts
├── cypress.config.ts
├── cypress
└── support
│ ├── commands-keyboard.ts
│ ├── commands-notifications.ts
│ ├── commands-notivue.ts
│ ├── commands-swipe.ts
│ ├── component-index.html
│ ├── component.ts
│ ├── styles.css
│ └── utils.ts
├── package.json
├── shared-config.ts
├── tsconfig.json
└── vite.config.mts
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | polar: smastrom
2 | buy_me_a_coffee: smastrom
3 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish to NPM
2 |
3 | on:
4 | push:
5 | tags: ['v*']
6 | workflow_dispatch:
7 |
8 | jobs:
9 | tests-workflow:
10 | uses: ./.github/workflows/tests.yml
11 | publish:
12 | needs: tests-workflow
13 | runs-on: ubuntu-latest
14 | permissions:
15 | contents: read
16 | id-token: write
17 | steps:
18 | - uses: actions/checkout@v3
19 | - uses: actions/setup-node@v3
20 | with:
21 | node-version: 20
22 | registry-url: 'https://registry.npmjs.org'
23 | - uses: pnpm/action-setup@v2
24 | name: Install pnpm
25 | with:
26 | version: 8
27 | run_install: true
28 | - name: Build Notivue
29 | run: pnpm -C packages/notivue run build
30 | - name: Copy README and LICENSE
31 | run: cp README.md LICENSE packages/notivue
32 | - name: Pack and verify
33 | run: cd packages/notivue && rm -rf *.tgz && npm pack && ./scripts/verify-tarball.sh
34 | - name: Publish
35 | run: cd packages/notivue && npm publish *.tgz --provenance
36 | env:
37 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
38 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - main
7 | push:
8 | branches:
9 | - main
10 | tags-ignore:
11 | - '*'
12 | workflow_call:
13 |
14 | jobs:
15 | cypress-run:
16 | runs-on: ubuntu-latest
17 | steps:
18 | - uses: actions/checkout@v3
19 | - name: Install Node.js
20 | uses: actions/setup-node@v3
21 | with:
22 | node-version: 20
23 | - uses: pnpm/action-setup@v2
24 | name: Install pnpm
25 | with:
26 | version: 8
27 | run_install: false
28 | - name: Install Cypress binaries
29 | run: npx cypress install
30 | - name: Install dependecies
31 | run: pnpm install
32 | - name: Build Notivue
33 | run: pnpm build
34 | - name: Install Notivue
35 | run: pnpm install
36 | - name: Test with Vitest
37 | run: pnpm test:unit
38 | - name: Test with Cypress
39 | uses: cypress-io/github-action@v5
40 | timeout-minutes: 15
41 | with:
42 | env: CYPRESS_CI=true
43 | component: true
44 | install: false
45 | working-directory: tests
46 | browser: chrome
47 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | logs
2 | *.log
3 | pnpm-debug.log*
4 |
5 | .vscode
6 | .idea
7 | .DS_Store
8 |
9 | node_modules
10 | dist
11 |
12 | notivue-*.tgz
13 |
14 | .nuxt
15 | pnpm-lock.yaml
16 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | npx lint-staged
2 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 100,
3 | "semi": false,
4 | "singleQuote": true,
5 | "tabWidth": 3,
6 | "trailingComma": "es5",
7 | "useTabs": false,
8 | "plugins": ["prettier-plugin-astro"],
9 | "overrides": [
10 | {
11 | "files": "README.md",
12 | "options": {
13 | "tabWidth": 2,
14 | "trailingComma": "none",
15 | "printWidth": 90
16 | }
17 | }, {
18 | "files": "*.astro",
19 | "options": {
20 | "parser": "astro"
21 | }
22 | }
23 | ]
24 | }
25 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ### Please refer to [smastrom/contributing](https://github.com/smastrom/contributing/blob/main/README.md).
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2023-present Simone Mastromattei
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/astro-playground/.gitignore:
--------------------------------------------------------------------------------
1 | # build output
2 | dist/
3 | # generated types
4 | .astro/
5 |
6 | # dependencies
7 | node_modules/
8 |
9 | # logs
10 | npm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 | pnpm-debug.log*
14 |
15 | # environment variables
16 | .env
17 | .env.production
18 |
19 | # macOS-specific files
20 | .DS_Store
21 |
--------------------------------------------------------------------------------
/astro-playground/astro.config.mjs:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'astro/config'
2 |
3 | import vue from '@astrojs/vue'
4 | import react from '@astrojs/react'
5 |
6 | export default defineConfig({
7 | integrations: [
8 | vue({
9 | appEntrypoint: '/src/pages/_app.ts',
10 | devtools: true,
11 | }),
12 | react(),
13 | ],
14 | vite: {
15 | optimizeDeps: {
16 | include: ['notivue'],
17 | },
18 | },
19 | })
20 |
--------------------------------------------------------------------------------
/astro-playground/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "notivue-astro-playground",
3 | "type": "module",
4 | "private": true,
5 | "scripts": {
6 | "dev": "astro dev",
7 | "start": "astro dev",
8 | "build": "astro build",
9 | "preview": "astro preview",
10 | "astro": "astro"
11 | },
12 | "dependencies": {
13 | "@astrojs/react": "^3.6.2",
14 | "@astrojs/vue": "^4.5.0",
15 | "astro": "^4.15.1",
16 | "notivue": "workspace:*",
17 | "react": "^18.3.1",
18 | "react-dom": "^18.3.1",
19 | "vue": "^3.4.30"
20 | },
21 | "devDependencies": {
22 | "@types/react": "^18.3.3",
23 | "@types/react-dom": "^18.3.0"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/astro-playground/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/astro-playground/src/components/AstroComponent.astro:
--------------------------------------------------------------------------------
1 |
2 |
From a Script Tag
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
60 |
--------------------------------------------------------------------------------
/astro-playground/src/components/Notivue.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/astro-playground/src/components/ReactComponent.tsx:
--------------------------------------------------------------------------------
1 | import { push } from 'notivue/astro'
2 |
3 | function pushStatic() {
4 | push.info({
5 | title: 'React Notification',
6 | message: 'Notification from React!',
7 | })
8 | }
9 |
10 | function pushPromise() {
11 | const promise = push.promise({
12 | title: 'Loading React Notification...',
13 | message: 'Loading notification from React...',
14 | })
15 |
16 | setTimeout(() => {
17 | promise.resolve({
18 | title: 'React Notification',
19 | message: 'Loaded notification from React!',
20 | })
21 | }, 2000)
22 | }
23 |
24 | export function ReactComponent() {
25 | return (
26 |
27 |
From React
28 |
29 |
30 |
31 |
32 |
33 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/astro-playground/src/components/VueComponent.vue:
--------------------------------------------------------------------------------
1 |
25 |
26 |
27 |
28 |
From Vue
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/astro-playground/src/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/astro-playground/src/layouts/Layout.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { ViewTransitions } from 'astro:transitions'
3 |
4 | import Notivue from '@/components/Notivue.vue'
5 |
6 | import 'notivue/astro/notification.css'
7 | import 'notivue/astro/animations.css'
8 |
9 | import '@/styles/reset.css'
10 | ---
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/astro-playground/src/pages/_app.ts:
--------------------------------------------------------------------------------
1 | import { createNotivue } from 'notivue/astro'
2 |
3 | import type { App, Plugin } from 'vue'
4 |
5 | const notivue = createNotivue({
6 | teleportTo: '#notivue_teleport',
7 | })
8 |
9 | export default (app: App) => {
10 | app.use(notivue as unknown as Plugin)
11 | }
12 |
--------------------------------------------------------------------------------
/astro-playground/src/pages/about.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import Layout from '@/layouts/Layout.astro'
3 |
4 | import VueComponent from '@/components/VueComponent.vue'
5 | import AstroComponent from '@/components/AstroComponent.astro'
6 | import { ReactComponent } from '@/components/ReactComponent'
7 | ---
8 |
9 |
10 | Home
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/astro-playground/src/pages/index.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import Layout from '../layouts/Layout.astro'
3 |
4 | import VueComponent from '../components/VueComponent.vue'
5 | import AstroComponent from '@/components/AstroComponent.astro'
6 | import { ReactComponent } from '../components/ReactComponent'
7 | ---
8 |
9 |
10 | About
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/astro-playground/src/styles/reset.css:
--------------------------------------------------------------------------------
1 | /* https://andy-bell.co.uk/a-more-modern-css-reset/ */
2 |
3 | /* Box sizing rules */
4 | *,
5 | *::before,
6 | *::after {
7 | box-sizing: border-box;
8 | }
9 |
10 | html {
11 | font-family: system-ui, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji',
12 | 'Segoe UI Emoji';
13 | }
14 |
15 | /* Prevent font size inflation */
16 | html {
17 | -moz-text-size-adjust: none;
18 | -webkit-text-size-adjust: none;
19 | text-size-adjust: none;
20 | }
21 |
22 | /* Remove default margin in favour of better control in authored CSS */
23 | body,
24 | h1,
25 | h2,
26 | h3,
27 | h4,
28 | p,
29 | figure,
30 | blockquote,
31 | dl,
32 | dd {
33 | margin: 0;
34 | }
35 |
36 | /* Remove list styles on ul, ol elements with a list role, which suggests default styling will be removed */
37 | ul[role='list'],
38 | ol[role='list'] {
39 | list-style: none;
40 | }
41 |
42 | /* Set core body defaults */
43 | body {
44 | min-height: 100vh;
45 | line-height: 1.5;
46 | }
47 |
48 | /* Set shorter line heights on headings and interactive elements */
49 | h1,
50 | h2,
51 | h3,
52 | h4,
53 | button,
54 | input,
55 | label {
56 | line-height: 1.1;
57 | }
58 |
59 | /* Balance text wrapping on headings */
60 | h1,
61 | h2,
62 | h3,
63 | h4 {
64 | text-wrap: balance;
65 | }
66 |
67 | /* A elements that don't have a class get default styles */
68 | a:not([class]) {
69 | text-decoration-skip-ink: auto;
70 | color: currentColor;
71 | }
72 |
73 | /* Make images easier to work with */
74 | img,
75 | picture {
76 | max-width: 100%;
77 | display: block;
78 | }
79 |
80 | /* Inherit fonts for inputs and buttons */
81 | input,
82 | button,
83 | textarea,
84 | select {
85 | font: inherit;
86 | }
87 |
88 | /* Make sure textareas without a rows attribute are not tiny */
89 | textarea:not([rows]) {
90 | min-height: 10em;
91 | }
92 |
93 | /* Anything that has been anchored to should have extra scroll margin */
94 | :target {
95 | scroll-margin-block: 5ex;
96 | }
97 |
--------------------------------------------------------------------------------
/astro-playground/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "astro/tsconfigs/strict",
3 | "compilerOptions": {
4 | "baseUrl": ".",
5 | "paths": {
6 | "@/*": ["src/*"]
7 | },
8 | "jsx": "react-jsx"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "notivue-monorepo",
3 | "private": true,
4 | "packageManager": "pnpm@8.14.3",
5 | "engines": {
6 | "node": ">=20.0.0"
7 | },
8 | "scripts": {
9 | "dev": "pnpm build && concurrently \"pnpm -C packages/notivue run watch\" \"pnpm -C playground install && pnpm -C playground run dev --host\"",
10 | "dev:astro": "pnpm build && concurrently \"pnpm -C packages/notivue run watch\" \"pnpm -C astro-playground install && pnpm -C astro-playground run dev --host\"",
11 | "build": "pnpm -C packages/notivue run build",
12 | "build:playground": "pnpm build && pnpm install && pnpm -C playground run build",
13 | "test": "pnpm build && pnpm install && pnpm -C tests run test",
14 | "test:gui": "pnpm build && concurrently \"pnpm -C packages/notivue run watch\" \"pnpm -C tests install && pnpm -C tests run test:gui\"",
15 | "test:unit": "pnpm -C tests run test:unit",
16 | "prepare": "husky"
17 | },
18 | "devDependencies": {
19 | "concurrently": "^8.2.2",
20 | "husky": "^9.1.5",
21 | "lint-staged": "^15.2.9",
22 | "prettier": "^3.3.3",
23 | "prettier-plugin-astro": "^0.14.1"
24 | },
25 | "lint-staged": {
26 | "*.{js,ts,vue,json,css,md}": "prettier --write"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/packages/notivue/.gitignore:
--------------------------------------------------------------------------------
1 | README.md
2 | !/nuxt/README.md
3 |
4 | LICENSE
5 |
6 | vite.config.ts.timestamp*
7 |
--------------------------------------------------------------------------------
/packages/notivue/Notifications/Notification.vue:
--------------------------------------------------------------------------------
1 |
19 |
20 |
21 |
28 |
29 |
30 |
36 |
37 |
38 | {{ Icon }}
39 |
40 |
41 |
42 |
46 |
47 |
58 |
59 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/packages/notivue/Notifications/NotificationProgress.vue:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
23 |
24 |
--------------------------------------------------------------------------------
/packages/notivue/Notifications/constants.ts:
--------------------------------------------------------------------------------
1 | import { CLASS_PREFIX as CX } from '@/core/constants'
2 | import { filledIcons } from './icons'
3 | import { lightTheme } from './themes'
4 |
5 | export const Classes = {
6 | NOTIFICATION: CX + 'notification',
7 | ICON: CX + 'icon',
8 | CONTENT: CX + 'content',
9 | TITLE: CX + 'content-title',
10 | MESSAGE: CX + 'content-message',
11 | CLOSE: CX + 'close',
12 | CLOSE_ICON: CX + 'close-icon',
13 | TRANSITION: CX + 'transition',
14 | PROGRESS: CX + 'progress',
15 | DUPLICATE: CX + 'duplicate',
16 | }
17 |
18 | export const DEFAULT_NOTIFICATIONS_PROPS = {
19 | icons: () => filledIcons,
20 | theme: () => lightTheme,
21 | hideClose: false,
22 | closeAriaLabel: 'Close',
23 | } as const
24 |
--------------------------------------------------------------------------------
/packages/notivue/Notifications/icons/CloseIcon.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
10 |
11 |
--------------------------------------------------------------------------------
/packages/notivue/Notifications/icons/ErrorIcon.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
11 |
12 |
--------------------------------------------------------------------------------
/packages/notivue/Notifications/icons/ErrorOutlineIcon.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
17 |
18 |
--------------------------------------------------------------------------------
/packages/notivue/Notifications/icons/InfoIcon.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
11 |
12 |
--------------------------------------------------------------------------------
/packages/notivue/Notifications/icons/InfoOutlineIcon.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
17 |
18 |
--------------------------------------------------------------------------------
/packages/notivue/Notifications/icons/PromiseIcon.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
16 |
17 |
--------------------------------------------------------------------------------
/packages/notivue/Notifications/icons/SuccessIcon.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
11 |
12 |
--------------------------------------------------------------------------------
/packages/notivue/Notifications/icons/SuccessOutlineIcon.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
14 |
15 |
--------------------------------------------------------------------------------
/packages/notivue/Notifications/icons/index.ts:
--------------------------------------------------------------------------------
1 | import { markRaw as raw, type SVGAttributes } from 'vue'
2 |
3 | import { NotificationTypeKeys as NType } from '@/core/constants'
4 |
5 | import SuccessIcon from './SuccessIcon.vue'
6 | import ErrorIcon from './ErrorIcon.vue'
7 | import InfoIcon from './InfoIcon.vue'
8 | import SuccessOutlineIcon from './SuccessOutlineIcon.vue'
9 | import ErrorOutlineIcon from './ErrorOutlineIcon.vue'
10 | import InfoOutlineIcon from './InfoOutlineIcon.vue'
11 | import PromiseIcon from './PromiseIcon.vue'
12 | import CloseIcon from './CloseIcon.vue'
13 |
14 | import type { NotivueIcons } from 'notivue'
15 |
16 | export const svgProps: SVGAttributes = {
17 | xmlns: 'http://www.w3.org/2000/svg',
18 | viewBox: '0 0 24 24',
19 | 'aria-hidden': 'true',
20 | }
21 |
22 | export const ionProps: SVGAttributes = {
23 | ...svgProps,
24 | fill: 'currentColor',
25 | viewBox: '0 0 12 12',
26 | }
27 |
28 | export const featherProps: SVGAttributes = {
29 | ...svgProps,
30 | stroke: 'currentColor',
31 | 'stroke-width': 2,
32 | 'stroke-linecap': 'round',
33 | 'stroke-linejoin': 'round',
34 | }
35 |
36 | export const filledIcons: NotivueIcons = {
37 | [NType.SUCCESS]: raw(SuccessIcon),
38 | [NType.ERROR]: raw(ErrorIcon),
39 | [NType.INFO]: raw(InfoIcon),
40 | [NType.WARNING]: raw(ErrorIcon),
41 | [NType.PROMISE]: raw(PromiseIcon),
42 | [NType.PROMISE_RESOLVE]: raw(SuccessIcon),
43 | [NType.PROMISE_REJECT]: raw(ErrorIcon),
44 | close: raw(CloseIcon),
45 | }
46 |
47 | export const outlinedIcons: NotivueIcons = {
48 | [NType.SUCCESS]: raw(SuccessOutlineIcon),
49 | [NType.ERROR]: raw(ErrorOutlineIcon),
50 | [NType.INFO]: raw(InfoOutlineIcon),
51 | [NType.WARNING]: raw(ErrorOutlineIcon),
52 | [NType.PROMISE]: raw(PromiseIcon),
53 | [NType.PROMISE_RESOLVE]: raw(SuccessOutlineIcon),
54 | [NType.PROMISE_REJECT]: raw(ErrorOutlineIcon),
55 | close: raw(CloseIcon),
56 | }
57 |
--------------------------------------------------------------------------------
/packages/notivue/Notifications/notifications-progress.css:
--------------------------------------------------------------------------------
1 | @media (prefers-reduced-motion: no-preference) {
2 | .Notivue__notification:has(.Notivue__progress) {
3 | border-radius: var(--nv-radius, 0) var(--nv-radius, 0) var(--nv-radius, 0) 0;
4 |
5 | & .Notivue__content-message {
6 | margin-bottom: var(--nv-progress-height, 4px);
7 | }
8 | }
9 |
10 | .Notivue__progress {
11 | position: absolute;
12 | bottom: 0;
13 | left: 0;
14 | width: 100%;
15 | height: var(--nv-progress-height, 4px);
16 | background-color: var(--nv-accent);
17 | animation: Notivue__progress-kf var(--nv-anim-dur) linear forwards;
18 | transform-origin: left;
19 | border-radius: 0 var(--nv-radius, 0) var(--nv-radius, 0) 0;
20 | }
21 |
22 | [dir='rtl'] {
23 | & .Notivue__progress {
24 | transform-origin: right;
25 | border-radius: var(--nv-radius, 0) 0 0 var(--nv-radius, 0);
26 | }
27 |
28 | & .Notivue__notification:has(.Notivue__progress) {
29 | border-radius: var(--nv-radius, 0) var(--nv-radius, 0) 0 var(--nv-radius, 0);
30 | }
31 | }
32 |
33 | @keyframes Notivue__progress-kf {
34 | 0% {
35 | transform: scaleX(1);
36 | }
37 | 100% {
38 | transform: scaleX(0);
39 | }
40 | }
41 | }
42 |
43 | @media (prefers-reduced-motion: reduce) {
44 | .Notivue__progress {
45 | display: none;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/packages/notivue/Notifications/themes.ts:
--------------------------------------------------------------------------------
1 | import type { NotivueTheme } from 'notivue'
2 |
3 | const layout: NotivueTheme = {
4 | '--nv-width': '350px',
5 | '--nv-spacing': '0.625rem',
6 | '--nv-radius': '0.625rem',
7 | '--nv-icon-size': '1.25rem',
8 | '--nv-title-size': '0.925rem',
9 | '--nv-message-size': '0.925rem',
10 | '--nv-y-align': 'center',
11 | }
12 |
13 | const shadow = {
14 | '--nv-shadow': 'rgba(0, 0, 0, 0.06) 0px 4px 6px -1px, rgba(0, 0, 0, 0.03) 0px 2px 4px -1px',
15 | }
16 |
17 | export const lightTheme: NotivueTheme = {
18 | ...layout,
19 | ...shadow,
20 |
21 | '--nv-global-bg': '#FFF',
22 | '--nv-global-fg': '#171717',
23 |
24 | '--nv-success-accent': '#28B780',
25 | '--nv-error-accent': '#E74C3C',
26 | '--nv-warning-accent': '#F59E0B',
27 | '--nv-info-accent': '#3E8EFF',
28 | '--nv-promise-accent': '#171717',
29 | }
30 |
31 | export const pastelTheme: NotivueTheme = {
32 | ...layout,
33 | ...shadow,
34 |
35 | '--nv-success-bg': '#E9FAEF',
36 | '--nv-success-accent': '#059669',
37 | '--nv-success-fg': '#057452',
38 |
39 | '--nv-error-bg': '#FEEFEF',
40 | '--nv-error-accent': '#E6523C',
41 | '--nv-error-fg': '#C5412C',
42 |
43 | '--nv-warning-bg': '#FFF0D8',
44 | '--nv-warning-accent': '#F48533',
45 | '--nv-warning-fg': '#81471D',
46 |
47 | '--nv-info-bg': '#DEF0FA',
48 | '--nv-info-accent': '#1F70AC',
49 | '--nv-info-fg': '#1F70AC',
50 |
51 | '--nv-promise-bg': '#FFF',
52 | '--nv-promise-accent': '#334155',
53 | '--nv-promise-fg': '#334155',
54 | }
55 |
56 | export const materialTheme: NotivueTheme = {
57 | ...layout,
58 | ...shadow,
59 |
60 | '--nv-global-accent': '#FFF',
61 | '--nv-global-fg': '#FFF',
62 |
63 | '--nv-success-bg': '#178570',
64 | '--nv-error-bg': '#C94430',
65 | '--nv-info-bg': '#117AAE',
66 |
67 | '--nv-warning-bg': '#FFE556',
68 | '--nv-warning-fg': '#4F5358',
69 | '--nv-warning-accent': '#4F5358',
70 |
71 | '--nv-promise-bg': '#FFF',
72 | '--nv-promise-fg': '#334155',
73 | '--nv-promise-accent': '#64748B',
74 | }
75 |
76 | export const darkTheme: NotivueTheme = {
77 | ...layout,
78 | '--nv-border-width': '1px',
79 |
80 | '--nv-global-bg': '#1F1F1F',
81 | '--nv-global-border': '#414141',
82 | '--nv-global-fg': '#D0D0D0',
83 |
84 | '--nv-success-accent': '#8EF997',
85 | '--nv-error-accent': '#FF7777',
86 | '--nv-warning-accent': '#FFE554',
87 | '--nv-info-accent': '#5FD4FF',
88 | '--nv-promise-accent': '#D0D0D0',
89 | }
90 |
91 | export const slateTheme: NotivueTheme = {
92 | ...layout,
93 | '--nv-border-width': '1px',
94 |
95 | '--nv-global-bg': '#20252E',
96 | '--nv-global-border': '#353b45',
97 | '--nv-global-fg': '#dfdfdf',
98 |
99 | '--nv-success-accent': '#34D399',
100 | '--nv-error-accent': '#FF7777',
101 | '--nv-warning-accent': '#FFE554',
102 | '--nv-info-accent': '#5FD4FF',
103 | '--nv-promise-accent': '#D0D0D0',
104 | }
105 |
--------------------------------------------------------------------------------
/packages/notivue/Notifications/types.ts:
--------------------------------------------------------------------------------
1 | import type { Component } from 'vue'
2 |
3 | import type { NotificationType, NotivueItem } from 'notivue'
4 |
5 | export interface NotificationsProps {
6 | item: NotivueItem
7 | icons?: NotivueIcons
8 | theme?: NotivueTheme
9 | closeAriaLabel?: string
10 | hideClose?: boolean
11 | }
12 |
13 | export type NotivueIcons = Partial<
14 | Record
15 | >
16 |
17 | export type ThemeNames = 'lightTheme' | 'pastelTheme' | 'materialTheme' | 'darkTheme' | 'slateTheme'
18 |
19 | export type NotivueTheme = Partial>
20 |
21 | type ThemeLayoutVars =
22 | | '--nv-width'
23 | | '--nv-min-width'
24 | | '--nv-spacing'
25 | | '--nv-radius'
26 | | '--nv-border-width'
27 | | '--nv-icon-size'
28 | | '--nv-title-size'
29 | | '--nv-message-size'
30 | | '--nv-shadow'
31 | | '--nv-tip-width'
32 | | '--nv-y-align'
33 | | '--nv-y-align-has-title'
34 | | '--nv-progress-height'
35 |
36 | type ThemeGlobalColorsVars =
37 | | '--nv-global-bg'
38 | | '--nv-global-fg'
39 | | '--nv-global-accent'
40 | | '--nv-global-border'
41 |
42 | type SuccessColorsVars =
43 | | '--nv-success-fg'
44 | | '--nv-success-bg'
45 | | '--nv-success-border'
46 | | '--nv-success-accent'
47 |
48 | type ErrorColorsVars = '--nv-error-fg' | '--nv-error-bg' | '--nv-error-border' | '--nv-error-accent'
49 |
50 | type WarningColorsVars =
51 | | '--nv-warning-fg'
52 | | '--nv-warning-bg'
53 | | '--nv-warning-border'
54 | | '--nv-warning-accent'
55 |
56 | type InfoColorsVars = '--nv-info-fg' | '--nv-info-bg' | '--nv-info-border' | '--nv-info-accent'
57 |
58 | type PromiseColorsVars =
59 | | '--nv-promise-fg'
60 | | '--nv-promise-bg'
61 | | '--nv-promise-border'
62 | | '--nv-promise-accent'
63 |
64 | type ThemeVars =
65 | | ThemeLayoutVars
66 | | ThemeGlobalColorsVars
67 | | SuccessColorsVars
68 | | ErrorColorsVars
69 | | WarningColorsVars
70 | | InfoColorsVars
71 | | PromiseColorsVars
72 |
73 | // New v2.4.0 aliases
74 |
75 | export type NotificationProps = NotificationsProps
76 |
--------------------------------------------------------------------------------
/packages/notivue/Notivue/AriaLive.vue:
--------------------------------------------------------------------------------
1 |
23 |
24 |
25 |
32 | {{ getAriaLiveContent(props.item) }}
33 |
34 |
35 |
--------------------------------------------------------------------------------
/packages/notivue/Notivue/Notivue.vue:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/packages/notivue/Notivue/NotivueImpl.vue:
--------------------------------------------------------------------------------
1 |
41 |
42 |
43 |
47 |
48 |
57 |
58 | -
72 |
73 |
74 |
75 |
76 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/packages/notivue/Notivue/composables/useMouseEvents.ts:
--------------------------------------------------------------------------------
1 | import { computed } from 'vue'
2 |
3 | import { isMouse } from '@/Notivue/utils'
4 | import { useStore } from '@/core/useStore'
5 |
6 | export function useMouseEvents() {
7 | const { timeouts, config } = useStore()
8 |
9 | function pauseHover(e: PointerEvent) {
10 | if (isMouse(e)) timeouts.pause()
11 | }
12 |
13 | function resumeHover(e: PointerEvent) {
14 | if (isMouse(e)) timeouts.resume()
15 | }
16 |
17 | return computed(() =>
18 | config.pauseOnHover.value && !timeouts.isStreamFocused.value
19 | ? {
20 | onPointerenter: pauseHover,
21 | onPointerleave: resumeHover,
22 | }
23 | : {}
24 | )
25 | }
26 |
--------------------------------------------------------------------------------
/packages/notivue/Notivue/composables/useNotivueStyles.ts:
--------------------------------------------------------------------------------
1 | import { computed, type CSSProperties } from 'vue'
2 |
3 | import { useNotivue } from '@/core/useStore'
4 |
5 | import type { NotivueElements } from 'notivue'
6 |
7 | /**
8 | * The follwing styles are not defined in a CSS file because
9 | * they are needed whether user uses default or custom components.
10 | *
11 | * Hence if users choose to only use custom components they can
12 | * remove the /notifications.css import and have no CSS at all.
13 | */
14 |
15 | const boxSizing: CSSProperties = { boxSizing: 'border-box' }
16 |
17 | const baseStyles: Record = {
18 | list: {
19 | ...boxSizing,
20 | display: 'flex',
21 | justifyContent: 'center',
22 | listStyle: 'none',
23 | margin: '0 auto',
24 | maxWidth: 'var(--nv-root-width, 100%)',
25 | padding: '0',
26 | pointerEvents: 'none',
27 | position: 'fixed',
28 | zIndex: 'var(--nv-z, 500)',
29 | },
30 | listItem: {
31 | ...boxSizing,
32 | display: 'flex',
33 | margin: '0',
34 | position: 'absolute',
35 | transitionProperty: 'transform',
36 | width: '100%',
37 | },
38 | itemContainer: {
39 | ...boxSizing,
40 | maxWidth: '100%',
41 | padding: `0 0 var(--nv-gap, 0.75rem) 0`,
42 | pointerEvents: 'auto',
43 | },
44 | }
45 |
46 | export function useNotivueStyles() {
47 | const { isTopAlign, position } = useNotivue()
48 |
49 | /**
50 | * Simulates overflow-hidden only on the opposite side of the current vertical align.
51 | * This will not clip enter animations but will contain the stream vertically.
52 | */
53 | const offset = computed(() => {
54 | const isTop = isTopAlign.value
55 |
56 | // IMPORTANT: Order of values must match 'top right bottom left'
57 | const inset = [
58 | `var(--nv-root-top, ${isTop ? '1.25rem' : '0px'})`,
59 | 'var(--nv-root-right, 1.25rem)',
60 | `var(--nv-root-bottom, ${isTop ? '0px' : '1.25rem'})`,
61 | 'var(--nv-root-left, 1.25rem)',
62 | ]
63 |
64 | const clipPath = inset.map((v) => `calc(-1 * ${v})`)
65 | isTop ? clipPath.splice(2, 1, '0px') : clipPath.splice(0, 1, '0px')
66 |
67 | return { inset: inset.join(' '), clipPath: `inset(${clipPath.join(' ')})` }
68 | })
69 |
70 | const xAlignment = computed(() => ({
71 | [isTopAlign.value ? 'top' : 'bottom']: '0',
72 | justifyContent: `var(--nv-root-x-align, ${
73 | position.value.endsWith('left')
74 | ? 'flex-start'
75 | : position.value.endsWith('right')
76 | ? 'flex-end'
77 | : 'center'
78 | })`,
79 | }))
80 |
81 | return computed>(() => ({
82 | list: { ...baseStyles.list, ...offset.value },
83 | listItem: { ...baseStyles.listItem, ...xAlignment.value },
84 | itemContainer: baseStyles.itemContainer,
85 | }))
86 | }
87 |
--------------------------------------------------------------------------------
/packages/notivue/Notivue/composables/useReducedMotion.ts:
--------------------------------------------------------------------------------
1 | import { useStore } from '@/core/useStore'
2 | import { onBeforeUnmount, onMounted } from 'vue'
3 |
4 | export function useReducedMotion() {
5 | const { animations } = useStore()
6 |
7 | const query = window.matchMedia('(prefers-reduced-motion: reduce)')
8 |
9 | const onMatch = () => animations.setReducedMotion(query.matches)
10 |
11 | onMounted(() => {
12 | onMatch()
13 | query.addEventListener?.('change', onMatch)
14 | })
15 |
16 | onBeforeUnmount(() => {
17 | query.removeEventListener?.('change', onMatch)
18 | })
19 | }
20 |
--------------------------------------------------------------------------------
/packages/notivue/Notivue/composables/useResizeListObserver.ts:
--------------------------------------------------------------------------------
1 | import { onMounted, onBeforeUnmount, watch } from 'vue'
2 |
3 | export function useResizeListObserver(elements: HTMLElement[], onSizeChange: () => void) {
4 | let resizeObserver: ResizeObserver
5 |
6 | const calls = new WeakSet()
7 |
8 | onMounted(() => {
9 | resizeObserver = new ResizeObserver((entries, observer) => {
10 | for (const e of entries) {
11 | if (!calls.has(e.target)) {
12 | // The element is being added to the DOM, skip
13 | calls.add(e.target)
14 | } else {
15 | // The element is being removed from the DOM and its size never changed, remove
16 | if (Object.values(e.contentRect.toJSON()).every((val) => val === 0)) {
17 | calls.delete(e.target)
18 | observer.unobserve(e.target)
19 | } else {
20 | // The element actually changed size, trigger callback and remove
21 | console.log('ResizeObserver Triggered')
22 |
23 | onSizeChange()
24 | calls.delete(e.target)
25 | observer.unobserve(e.target)
26 | }
27 | }
28 | }
29 | })
30 | })
31 |
32 | watch(
33 | elements,
34 | (el) => {
35 | if (el.length > 0) el.forEach((el) => resizeObserver?.observe(el))
36 | },
37 | { flush: 'post' }
38 | )
39 |
40 | onBeforeUnmount(() => {
41 | resizeObserver?.disconnect()
42 | })
43 | }
44 |
--------------------------------------------------------------------------------
/packages/notivue/Notivue/composables/useSizes.ts:
--------------------------------------------------------------------------------
1 | import { useWindowSize } from './useWindowSize'
2 | import { useResizeListObserver } from './useResizeListObserver'
3 | import { useStore } from '@/core/useStore'
4 |
5 | export function useSizes() {
6 | const { elements, animations } = useStore()
7 |
8 | useWindowSize(() => animations.updatePositions({ isImmediate: true }))
9 |
10 | useResizeListObserver(elements.items.value, () => animations.updatePositions())
11 | }
12 |
--------------------------------------------------------------------------------
/packages/notivue/Notivue/composables/useTouchEvents.ts:
--------------------------------------------------------------------------------
1 | import { computed, onBeforeUnmount } from 'vue'
2 |
3 | import { useStore } from '@/core/useStore'
4 | import { isMouse } from '@/Notivue/utils'
5 |
6 | /**
7 | * The logic follows this pattern:
8 | *
9 | * Every time users tap the stream, all notifications
10 | * will pause and automatically resume after 2 seconds.
11 | *
12 | * If users keep tapping on the stream, once timeouts
13 | * are resumed, they will pause again after 2 seconds and so on.
14 | *
15 | * This is never triggered when NotivueSwipe is used as that
16 | * pointerdown event is not propagated.
17 | */
18 |
19 | export function useTouchEvents() {
20 | const { timeouts, config } = useStore()
21 |
22 | function pauseTouch(e: PointerEvent) {
23 | if (!isMouse(e)) {
24 | timeouts.clearDebounceTimeout()
25 | timeouts.pause()
26 |
27 | timeouts.resumeWithDebounce(2000)
28 | }
29 | }
30 |
31 | onBeforeUnmount(() => {
32 | timeouts.clearDebounceTimeout()
33 | })
34 |
35 | return computed(() =>
36 | config.pauseOnTouch.value && !timeouts.isStreamFocused.value
37 | ? { onPointerdown: pauseTouch }
38 | : {}
39 | )
40 | }
41 |
--------------------------------------------------------------------------------
/packages/notivue/Notivue/composables/useWindowFocus.ts:
--------------------------------------------------------------------------------
1 | import { onMounted, onBeforeUnmount } from 'vue'
2 |
3 | import { useStore } from '@/core/useStore'
4 |
5 | export function useWindowFocus() {
6 | const { config, timeouts } = useStore()
7 |
8 | function onFocus() {
9 | if (timeouts.isStreamFocused.value) return
10 | if (config.pauseOnTabChange.value) timeouts.resume()
11 | }
12 |
13 | function onBlur() {
14 | if (timeouts.isStreamFocused.value) return
15 | if (config.pauseOnTabChange.value) timeouts.pause()
16 | }
17 |
18 | onMounted(() => {
19 | window.addEventListener('focus', onFocus)
20 | window.addEventListener('blur', onBlur)
21 | })
22 |
23 | onBeforeUnmount(() => {
24 | window.removeEventListener('focus', onFocus)
25 | window.removeEventListener('blur', onBlur)
26 | })
27 | }
28 |
--------------------------------------------------------------------------------
/packages/notivue/Notivue/composables/useWindowSize.ts:
--------------------------------------------------------------------------------
1 | import { onBeforeUnmount, onMounted } from 'vue'
2 |
3 | export function useWindowSize(onResize: () => void) {
4 | function _onResize() {
5 | if (window.matchMedia('(max-width: 1100px)').matches) {
6 | onResize()
7 | }
8 | }
9 |
10 | onMounted(() => {
11 | window.addEventListener('resize', _onResize)
12 | })
13 |
14 | onBeforeUnmount(() => {
15 | window.removeEventListener('resize', _onResize)
16 | })
17 | }
18 |
--------------------------------------------------------------------------------
/packages/notivue/Notivue/constants.ts:
--------------------------------------------------------------------------------
1 | export const DEFAULT_PROPS = {
2 | listAriaLabel: 'Notifications',
3 | } as const
4 |
--------------------------------------------------------------------------------
/packages/notivue/Notivue/types.ts:
--------------------------------------------------------------------------------
1 | import type { CSSProperties, Component } from 'vue'
2 | import type { ContainersTabIndexMap } from '@/NotivueKeyboard/types'
3 | import type { NotivueItem } from 'notivue'
4 |
5 | export interface NotivueProps {
6 | class?: string | Record | (string | Record)[]
7 | /**
8 | * Notification containers reactive tabindex map. Only needed if using NotivueKeyboard.
9 | *
10 | * @default undefined
11 | */
12 | containersTabIndex?: ContainersTabIndexMap
13 | /**
14 | * Aria label for the list container. Only effective if using NotivueKeyboard.
15 | *
16 | * @default "Notifications"
17 | */
18 | listAriaLabel?: string
19 | /**
20 | * CSS styles for the list container, list items and notification containers.
21 | *
22 | * They have higher priority over the internal styles and will override them.
23 | *
24 | * ```ts
25 | * const styles = {
26 | * list: {
27 | * position: 'relative',
28 | * height: '100%',
29 | * },
30 | * listItem: {
31 | * // ...
32 | * },
33 | * itemContainer: {
34 | * // ...
35 | * },
36 | * }
37 | * ```
38 | *
39 | * @default undefined
40 | */
41 | styles?: Partial>
42 | }
43 |
44 | export interface NotivueComponentSlot {
45 | default(item: NotivueItem & { key?: string }): Component
46 | }
47 |
48 | // Elements
49 |
50 | export type NotivueElements = 'list' | 'listItem' | 'itemContainer'
51 |
--------------------------------------------------------------------------------
/packages/notivue/Notivue/utils.ts:
--------------------------------------------------------------------------------
1 | import type { NotivueItem } from 'notivue'
2 |
3 | export const isMouse = (e: PointerEvent) => e.pointerType === 'mouse'
4 |
5 | export function getAriaLabel(item: NotivueItem) {
6 | return `${item.title ? `${item.title}: ` : ''}${item.message}`
7 | }
8 |
--------------------------------------------------------------------------------
/packages/notivue/NotivueKeyboard/NotivueKeyboard.vue:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
20 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/packages/notivue/NotivueKeyboard/constants.ts:
--------------------------------------------------------------------------------
1 | import type { InjectionKey } from 'vue'
2 | import type { NotivueKeyboardData } from 'notivue'
3 |
4 | export const keyboardInjectionKey = Symbol('') as InjectionKey
5 |
6 | export const focusableEls =
7 | 'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex="0"]'
8 |
9 | export const DEFAULT_PROPS = {
10 | comboKey: 'n',
11 | handleClicks: true,
12 | leaveMessage: "You're leaving the notifications stream. Press Control + N to navigate it again.",
13 | emptyMessage: 'No notifications to navigate',
14 | renderAnnouncement: true,
15 | maxAnnouncements: 2,
16 | } as const
17 |
--------------------------------------------------------------------------------
/packages/notivue/NotivueKeyboard/types.ts:
--------------------------------------------------------------------------------
1 | import type { Component, ComputedRef, Ref } from 'vue'
2 |
3 | export type TabIndexValue = 0 | -1
4 | export type ContainersTabIndexMap = Record
5 |
6 | export interface NotivueKeyboardData {
7 | /**
8 | * Reactive tab index value for the custom notification focusable elements tabindex.
9 | *
10 | * Meant to be added to the custom notification elements via `:tabindex="elementsTabIndex"`.
11 | */
12 | elementsTabIndex: Ref
13 | /**
14 | * Reactive map of tab index values for the notification containers.
15 | *
16 | * Meant to be passed as prop to Notivue via `:containersTabIndex="containersTabIndex"`.
17 | */
18 | containersTabIndex: ComputedRef
19 | }
20 |
21 | export interface NotivueKeyboardProps {
22 | /**
23 | * Key to combine with Shift to enter or exit the stream.
24 | *
25 | * @default "n"
26 | */
27 | comboKey?: string
28 | /**
29 | * Whether to focus next candidate or exit the stream after pressing
30 | * any button or link inside a notification.
31 | *
32 | * @default true
33 | */
34 | handleClicks?: boolean
35 | /**
36 | * Text to be announced when leaving the stream
37 | *
38 | * @default "You're leaving the notifications stream. Press Control + N to navigate it again."
39 | */
40 | leaveMessage?: string
41 | /**
42 | * Text to be announced when attempting to navigate the stream but no candidates are available.
43 | *
44 | * @default "No notifications to navigate"
45 | */
46 | emptyMessage?: string
47 | /**
48 | * Whether to render the enter/leave notification or just announce it via screen reader.
49 | *
50 | * @default true
51 | */
52 | renderAnnouncement?: boolean
53 | /**
54 | * Maximum times to announce that the user is leaving the stream.
55 | *
56 | * @default 3
57 | */
58 | maxAnnouncements?: number
59 | }
60 |
61 | export interface NotivueKeyboardSlot {
62 | default(props: {
63 | elementsTabIndex: TabIndexValue
64 | containersTabIndex: ContainersTabIndexMap
65 | }): Component
66 | }
67 |
--------------------------------------------------------------------------------
/packages/notivue/NotivueKeyboard/useKeyboardFocus.ts:
--------------------------------------------------------------------------------
1 | import { ref, onMounted, onBeforeUnmount } from 'vue'
2 |
3 | export function useKeyboardFocus() {
4 | const isKeyboardFocus = ref(false)
5 |
6 | const setKeyboardFocus = () => (isKeyboardFocus.value = true)
7 | const unsetKeyboardFocus = () => (isKeyboardFocus.value = false)
8 |
9 | const events = [
10 | ['keydown', setKeyboardFocus],
11 | ['mousedown', unsetKeyboardFocus],
12 | ['touchstart', unsetKeyboardFocus],
13 | ] as const
14 |
15 | onMounted(() => {
16 | events.forEach(([e, handler]) => document.addEventListener(e, handler))
17 | })
18 |
19 | onBeforeUnmount(() => {
20 | events.forEach(([e, handler]) => document.removeEventListener(e, handler))
21 | })
22 |
23 | return { isKeyboardFocus }
24 | }
25 |
--------------------------------------------------------------------------------
/packages/notivue/NotivueKeyboard/useLastFocused.ts:
--------------------------------------------------------------------------------
1 | import { ref, onMounted, onBeforeUnmount } from 'vue'
2 |
3 | import { useStore } from '@/core/useStore'
4 |
5 | export function useLastFocused() {
6 | const { root: stream } = useStore().elements
7 |
8 | const lastFocused = ref(null)
9 |
10 | function onFocusCapture(e: FocusEvent) {
11 | const isValidTarget = e.target instanceof HTMLElement
12 |
13 | if (isValidTarget && stream.value?.contains(e.target)) return
14 |
15 | if (isValidTarget) lastFocused.value = e.target
16 | }
17 |
18 | function focusLastElement() {
19 | console.log('Focusing last element!', lastFocused.value)
20 |
21 | if (lastFocused.value) {
22 | lastFocused.value.focus()
23 | } else {
24 | /**
25 | * This may happen once in a lifetime, For example:
26 | *
27 | * - On Safari: if users never focused an element with the keyboard
28 | * before accessing the stream as clicks do not trigger focus.
29 | *
30 | * - On Chrome/Firefox: if users never interacted with any element
31 | * before accessing the stream.
32 | *
33 | */
34 | document.activeElement instanceof HTMLElement && document.activeElement.blur()
35 | document.body.focus()
36 | }
37 | }
38 |
39 | onMounted(() => {
40 | document.addEventListener('focus', onFocusCapture, true)
41 | })
42 |
43 | onBeforeUnmount(() => {
44 | document.removeEventListener('focus', onFocusCapture, true)
45 | })
46 |
47 | return { focusLastElement }
48 | }
49 |
--------------------------------------------------------------------------------
/packages/notivue/NotivueKeyboard/useNotivueKeyboard.ts:
--------------------------------------------------------------------------------
1 | import { inject, ref, computed } from 'vue'
2 |
3 | import { keyboardInjectionKey } from './constants'
4 | import { isSSR } from '@/core/utils'
5 |
6 | import type { NotivueKeyboardData } from 'notivue'
7 |
8 | export function useNotivueKeyboard(): NotivueKeyboardData {
9 | if (isSSR) {
10 | return {
11 | elementsTabIndex: ref(-1),
12 | containersTabIndex: computed(() => ({})),
13 | }
14 | }
15 |
16 | return inject(keyboardInjectionKey) as NotivueKeyboardData
17 | }
18 |
--------------------------------------------------------------------------------
/packages/notivue/NotivueSwipe/constants.ts:
--------------------------------------------------------------------------------
1 | export const DEFAULT_PROPS = {
2 | touchOnly: false,
3 | exclude: 'a, button',
4 | disabled: false,
5 | threshold: 0.5,
6 | } as const
7 |
8 | export const RETURN_DUR = 300
9 |
10 | export const DEBOUNCE = {
11 | Mouse: 200,
12 | Touch: 1000,
13 | TouchExternal: 1400,
14 | }
15 |
--------------------------------------------------------------------------------
/packages/notivue/NotivueSwipe/types.ts:
--------------------------------------------------------------------------------
1 | import type { NotivueItem } from 'notivue'
2 |
3 | export interface NotivueSwipeProps {
4 | /** Notivue's exposed notification item. */
5 | item: NotivueItem
6 | /**
7 | * Whether to enable clear on swipe only on touch interactions.
8 | */
9 | touchOnly?: boolean
10 | /**
11 | * A [querySelectorAll](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelectorAll)
12 | * string that specifies elements to be exempted from the swipe action.
13 | *
14 | * @default "a, button"
15 | */
16 | exclude?: string
17 | /**
18 | * Whether to disable the swipe gesture or not.
19 | * Useful for disabling the behavior on desktop devices, for example.
20 | *
21 | * @default false
22 | */
23 | disabled?: boolean
24 | /**
25 | * Fraction of notification's width needed to be swiped for clearing.
26 | * For instance, a threshold of 0.5 indicates 50% of the notification's width must be swiped.
27 | *
28 | * @default 0.5
29 | */
30 | threshold?: number
31 | /**
32 | * @deprecated
33 | *
34 | * @since 1.4.0
35 | *
36 | * This is no longer required.
37 | *
38 | *
39 | * Whether to call the 'destroy' item method instead of 'clear' when
40 | * the swipe threshold is met.
41 | *
42 | * @default false
43 | */
44 | destroy?: boolean
45 | }
46 |
--------------------------------------------------------------------------------
/packages/notivue/astro/Notivue.vue:
--------------------------------------------------------------------------------
1 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/packages/notivue/astro/createNotivue.ts:
--------------------------------------------------------------------------------
1 | import { NotivueConfig } from 'notivue'
2 |
3 | import type { Plugin, App } from 'vue'
4 |
5 | export function createNotivue(
6 | pluginConfig: NotivueConfig & {
7 | startOnCreation?: boolean
8 | } = {}
9 | ): Plugin {
10 | return {
11 | install(app: App) {
12 | Object.assign(app.config.globalProperties, {
13 | notivuePluginConfig: pluginConfig,
14 | })
15 | },
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/packages/notivue/astro/push.ts:
--------------------------------------------------------------------------------
1 | import type { Push, PushOptions } from 'notivue'
2 | import type { PushAstroEvent, MaybeAstroPushPromiseReturn } from './types'
3 |
4 | export function pushEvent>(
5 | detail: T
6 | ): MaybeAstroPushPromiseReturn {
7 | const eventId = Date.now()
8 |
9 | // Prepare to listen for the result of the notification that will be created by NotivueAstro...
10 | let pushResult = {} as MaybeAstroPushPromiseReturn
11 | const resultEventName = `notivue:id:${eventId}`
12 |
13 | // ...upon receival, save the result and remove the listener
14 | window.addEventListener(
15 | resultEventName,
16 | ((e: CustomEvent>) => {
17 | pushResult = e.detail
18 | }) as EventListener,
19 | { once: true }
20 | )
21 |
22 | // Dispatch the incoming push options to NotivueAstro to create the notification
23 | window.dispatchEvent(
24 | new CustomEvent('notivue:push', {
25 | detail: {
26 | ...detail,
27 | type: detail.type,
28 | resultEventName,
29 | },
30 | })
31 | )
32 |
33 | // Return the result
34 | return pushResult
35 | }
36 |
37 | export const push = {
38 | success: (options: PushOptions) => pushEvent({ ...options, type: 'success' }),
39 | info: (options: PushOptions) => pushEvent({ ...options, type: 'info' }),
40 | error: (options: PushOptions) => pushEvent({ ...options, type: 'error' }),
41 | warning: (options: PushOptions) => pushEvent({ ...options, type: 'warning' }),
42 | promise: (options: PushOptions) => pushEvent({ ...options, type: 'promise' }),
43 | load: (options: PushOptions) => pushEvent({ ...options, type: 'promise' }),
44 | clearAll() {
45 | window.dispatchEvent(new CustomEvent('notivue:clear-all'))
46 | },
47 | destroyAll() {
48 | window.dispatchEvent(new CustomEvent('notivue:destroy-all'))
49 | },
50 | } as Push
51 |
--------------------------------------------------------------------------------
/packages/notivue/astro/types.ts:
--------------------------------------------------------------------------------
1 | import type { ClearFunctions, PushPromiseReturn, NotificationType, PushOptions } from 'notivue'
2 |
3 | export type PushAstroEvent = PushOptions & {
4 | type: Exclude
5 | resultEventName: string
6 | }
7 |
8 | export type MaybeAstroPushPromiseReturn = T extends PushOptions & { type: 'promise' }
9 | ? PushPromiseReturn
10 | : T extends PushOptions
11 | ? ClearFunctions
12 | : never
13 |
14 | interface CustomEventMap {
15 | 'notivue:push': CustomEvent
16 | 'notivue:clear-all': CustomEvent
17 | 'notivue:destroy-all': CustomEvent
18 | }
19 |
20 | declare global {
21 | interface WindowEventMap extends CustomEventMap {}
22 | }
23 |
--------------------------------------------------------------------------------
/packages/notivue/core/animations.css:
--------------------------------------------------------------------------------
1 | [data-notivue-align='top'] {
2 | & .Notivue__enter,
3 | & .Notivue__leave {
4 | --notivue-ty: -200%;
5 | }
6 | }
7 |
8 | [data-notivue-align='bottom'] {
9 | & .Notivue__enter,
10 | & .Notivue__leave {
11 | --notivue-ty: 200%;
12 | }
13 | }
14 |
15 | .Notivue__enter {
16 | animation: Notivue__enter-kf 350ms cubic-bezier(0.5, 1, 0.25, 1);
17 | }
18 |
19 | .Notivue__leave {
20 | animation: Notivue__leave-kf 350ms ease;
21 | }
22 |
23 | .Notivue__clearAll {
24 | animation: Notivue__clearAll-kf 500ms cubic-bezier(0.22, 1, 0.36, 1);
25 | }
26 |
27 | @keyframes Notivue__enter-kf {
28 | 0% {
29 | transform: translate3d(0, var(--notivue-ty), 0) scale(0.25);
30 | opacity: 0;
31 | }
32 |
33 | 100% {
34 | transform: translate3d(0, 0, 0) scale(1);
35 | opacity: 1;
36 | }
37 | }
38 |
39 | @keyframes Notivue__leave-kf {
40 | 0% {
41 | transform: translate3d(0, 0, 0) scale(1);
42 | opacity: 0.7;
43 | }
44 |
45 | 100% {
46 | transform: translate3d(0, var(--notivue-ty), 0) scale(0);
47 | opacity: 0;
48 | }
49 | }
50 |
51 | @keyframes Notivue__clearAll-kf {
52 | 0% {
53 | opacity: 1;
54 | }
55 |
56 | 100% {
57 | opacity: 0;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/packages/notivue/core/constants.ts:
--------------------------------------------------------------------------------
1 | import { NotificationType as NTypeU, NotivueConfigRequired, NotificationOptions } from 'notivue'
2 |
3 | export const CLASS_PREFIX = 'Notivue__'
4 |
5 | export const DEFAULT_DURATION = 6000
6 |
7 | export const NotificationTypeKeys: Record = {
8 | SUCCESS: 'success',
9 | ERROR: 'error',
10 | WARNING: 'warning',
11 | INFO: 'info',
12 | PROMISE: 'promise',
13 | PROMISE_RESOLVE: 'promise-resolve',
14 | PROMISE_REJECT: 'promise-reject',
15 | }
16 |
17 | const success: NotificationOptions = {
18 | title: '',
19 | message: '',
20 | duration: DEFAULT_DURATION,
21 | ariaLive: 'polite',
22 | ariaRole: 'status',
23 | }
24 |
25 | const error: NotificationOptions = {
26 | ...success,
27 | ariaLive: 'assertive',
28 | ariaRole: 'alert',
29 | }
30 |
31 | const promise: NotificationOptions = {
32 | ...success,
33 | duration: Infinity,
34 | }
35 |
36 | const warning: NotificationOptions = {
37 | ...error,
38 | ariaLive: 'polite',
39 | }
40 |
41 | const info: NotificationOptions = {
42 | ...success,
43 | }
44 |
45 | export const DEFAULT_NOTIFICATION_OPTIONS = {
46 | [NotificationTypeKeys.SUCCESS]: success,
47 | [NotificationTypeKeys.ERROR]: error,
48 | [NotificationTypeKeys.WARNING]: warning,
49 | [NotificationTypeKeys.INFO]: info,
50 | [NotificationTypeKeys.PROMISE]: promise,
51 | [NotificationTypeKeys.PROMISE_RESOLVE]: success,
52 | [NotificationTypeKeys.PROMISE_REJECT]: error,
53 | } as NotivueConfigRequired['notifications']
54 |
55 | export const DEFAULT_CONFIG: NotivueConfigRequired = {
56 | pauseOnHover: true,
57 | pauseOnTouch: true,
58 | pauseOnTabChange: true,
59 | enqueue: false,
60 | position: 'top-center',
61 | teleportTo: 'body',
62 | notifications: DEFAULT_NOTIFICATION_OPTIONS,
63 | limit: Infinity,
64 | avoidDuplicates: false,
65 | transition: 'transform 0.35s cubic-bezier(0.5, 1, 0.25, 1)',
66 | animations: {
67 | enter: CLASS_PREFIX + 'enter',
68 | leave: CLASS_PREFIX + 'leave',
69 | clearAll: CLASS_PREFIX + 'clearAll',
70 | },
71 | }
72 |
--------------------------------------------------------------------------------
/packages/notivue/core/createInstance.ts:
--------------------------------------------------------------------------------
1 | import { readonly, ref } from 'vue'
2 |
3 | import { createPushMock, setPush } from './createPush'
4 | import { createStoreWatchers } from './createStoreWatchers'
5 |
6 | import type { NotivueStore, Push } from 'notivue'
7 |
8 | export let startInstance: () => void = () => {}
9 | export let stopInstance: () => void = () => {}
10 |
11 | export function createInstance(startOnCreation: boolean) {
12 | const isRunning = ref(startOnCreation)
13 | const isRunningReadonly = readonly(isRunning)
14 |
15 | function setupInstance(store: NotivueStore, push: Push) {
16 | const watchStore = () => createStoreWatchers(store)
17 |
18 | if (startOnCreation) setPush(push)
19 |
20 | let unwatchStore = startOnCreation ? watchStore() : [() => {}]
21 |
22 | const instance = {
23 | isRunning: isRunningReadonly,
24 | startInstance() {
25 | if (isRunning.value) return
26 |
27 | setPush(push)
28 | unwatchStore = watchStore()
29 |
30 | isRunning.value = true
31 | },
32 | stopInstance() {
33 | if (!isRunning.value) return
34 |
35 | store.items.clear()
36 | store.queue.clear()
37 | store.items.clearLifecycleEvents()
38 |
39 | setPush(createPushMock())
40 | unwatchStore.forEach((unwatch) => unwatch())
41 |
42 | isRunning.value = false
43 | },
44 | }
45 |
46 | startInstance = () => instance.startInstance()
47 | stopInstance = () => instance.stopInstance()
48 |
49 | return instance
50 | }
51 |
52 | return { isRunning: isRunningReadonly, setupInstance }
53 | }
54 |
--------------------------------------------------------------------------------
/packages/notivue/core/createNotivue.ts:
--------------------------------------------------------------------------------
1 | import type { App, Plugin } from 'vue'
2 | import type { NotivueConfig } from 'notivue'
3 |
4 | import { createPush } from './createPush'
5 | import { notivueInjectionKey, notivueInstanceInjectionKey } from './symbols'
6 | import { createInstance } from './createInstance'
7 | import { createPushProxies, createStore } from './createStore'
8 |
9 | export function createProvides(startOnCreation: boolean, userConfig: NotivueConfig) {
10 | const { setupInstance, isRunning } = createInstance(startOnCreation)
11 |
12 | const store = createStore(userConfig, isRunning)
13 |
14 | const proxies = createPushProxies(store)
15 | const push = Object.freeze(createPush(proxies))
16 |
17 | const instance = setupInstance(store, push)
18 |
19 | return {
20 | store,
21 | instance,
22 | push,
23 | }
24 | }
25 |
26 | export function createNotivue(
27 | pluginConfig: NotivueConfig & {
28 | startOnCreation?: boolean
29 | } = {}
30 | ): Plugin {
31 | return {
32 | install(app: App) {
33 | const { startOnCreation = true, ...userConfig } = pluginConfig
34 | const { store, instance, push } = createProvides(startOnCreation, userConfig)
35 |
36 | app.provide(notivueInstanceInjectionKey, instance)
37 | app.provide(notivueInjectionKey, store)
38 |
39 | app.config.globalProperties.$push ||= push
40 | },
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/packages/notivue/core/createPush.ts:
--------------------------------------------------------------------------------
1 | import { unref } from 'vue'
2 |
3 | import { NotificationTypeKeys as NType } from './constants'
4 | import { createPushProxies } from './createStore'
5 |
6 | import type { NotificationType, Push, PushOptions, PushParameter } from 'notivue'
7 |
8 | export const push = createPushMock()
9 |
10 | export function setPush(p: Push) {
11 | Object.assign(push, p)
12 | }
13 |
14 | export function createPush(proxies: ReturnType): Push {
15 | let createCount = 0
16 |
17 | function push(options: PushParameter, type: NotificationType, id = `${createCount++}`) {
18 | if (typeof unref(options) === 'string') {
19 | options = { message: options } as PushOptions
20 | }
21 |
22 | proxies.push({ ...(options as PushOptions), id, type })
23 |
24 | return {
25 | id,
26 | clear: () => proxies.clear(id),
27 | destroy: () => proxies.clear(id, { isDestroy: true }),
28 | }
29 | }
30 |
31 | return {
32 | success: (options) => push(options, NType.SUCCESS),
33 | error: (options) => push(options, NType.ERROR),
34 | warning: (options) => push(options, NType.WARNING),
35 | info: (options) => push(options, NType.INFO),
36 | promise: (options) => {
37 | const { id, clear, destroy } = push(options, NType.PROMISE)
38 |
39 | return {
40 | resolve: (options) => push(options, NType.PROMISE_RESOLVE, id),
41 | reject: (options) => push(options, NType.PROMISE_REJECT, id),
42 | success: (options) => push(options, NType.PROMISE_RESOLVE, id),
43 | error: (options) => push(options, NType.PROMISE_REJECT, id),
44 | clear,
45 | destroy,
46 | }
47 | },
48 | load(options) {
49 | return this.promise(options)
50 | },
51 | clearAll: () => proxies.clearAll(),
52 | destroyAll: () => proxies.destroyAll(),
53 | }
54 | }
55 |
56 | export function createPushMock(): Push {
57 | const noop = new Proxy({}, { get: () => () => {} }) as any
58 | return createPush(noop)
59 | }
60 |
--------------------------------------------------------------------------------
/packages/notivue/core/createStoreWatchers.ts:
--------------------------------------------------------------------------------
1 | import { watch } from 'vue'
2 |
3 | import type { NotivueStore } from 'notivue'
4 |
5 | export function createStoreWatchers(store: NotivueStore) {
6 | return [
7 | watch(
8 | store.items.lifecycleEventsCount,
9 | () => {
10 | store.animations.updatePositions()
11 | },
12 | { flush: 'post' }
13 | ),
14 |
15 | watch(
16 | store.config.position,
17 | () => {
18 | store.animations.updatePositions({ isImmediate: true })
19 | },
20 | { flush: 'post' }
21 | ),
22 |
23 | watch(
24 | () => store.items.length === 0 && store.queue.length === 0,
25 | (isReset) => {
26 | if (isReset) {
27 | store.timeouts.reset()
28 | store.elements.setRootAttrs({})
29 | }
30 | },
31 | { flush: 'post' }
32 | ),
33 | ]
34 | }
35 |
--------------------------------------------------------------------------------
/packages/notivue/core/symbols.ts:
--------------------------------------------------------------------------------
1 | import type { InjectionKey } from 'vue'
2 | import type { NotivueInstance, NotivueStore } from 'notivue'
3 |
4 | export const notivueInstanceInjectionKey = Symbol() as InjectionKey
5 | export const notivueInjectionKey = Symbol() as InjectionKey
6 |
--------------------------------------------------------------------------------
/packages/notivue/core/useStore.ts:
--------------------------------------------------------------------------------
1 | import { inject, computed, toRefs, reactive, readonly, ref } from 'vue'
2 |
3 | import { isSSR, getSlotItem } from './utils'
4 | import { notivueInjectionKey, notivueInstanceInjectionKey } from './symbols'
5 | import { push } from './createPush'
6 | import { DEFAULT_CONFIG } from './constants'
7 |
8 | import type {
9 | NotivueStore,
10 | UseNotivueReturn,
11 | NotivueComputedEntries,
12 | NotivueInstance,
13 | } from 'notivue'
14 |
15 | export function useStore() {
16 | return inject(notivueInjectionKey) as NotivueStore
17 | }
18 |
19 | /**
20 | * Composable to start and stop the Notivue instance.
21 | *
22 | * @returns
23 | *
24 | * - `startInstance` - Starts or restarts the Notivue instance.
25 | * - `stopInstance` - Stops the Notivue instance.
26 | * - `isRunning` - Readonly ref to check if the Notivue instance is running.
27 | *
28 | * @docs https://docs.notivue.smastrom.io/api/use-notivue-instance
29 | */
30 | export function useNotivueInstance(): NotivueInstance {
31 | if (isSSR) {
32 | return {
33 | isRunning: ref(true),
34 | startInstance: () => {},
35 | stopInstance: () => {},
36 | } as NotivueInstance
37 | }
38 |
39 | return inject(notivueInstanceInjectionKey) as NotivueInstance
40 | }
41 |
42 | /**
43 | * Composable to get and update the current Notivue config.
44 | *
45 | * @returns
46 | *
47 | * The current [configuration](https://docs.notivue.smastrom.io/customization/configuration)
48 | * where each property is a [ref](https://vuejs.org/guide/essentials/reactivity-fundamentals.html#ref)
49 | * that allows for reactive updates and side effects watching.
50 | *
51 | * @docs https://docs.notivue.smastrom.io/api/use-notivue
52 | */
53 | export function useNotivue(): UseNotivueReturn {
54 | if (isSSR) {
55 | return {
56 | ...toRefs(reactive(DEFAULT_CONFIG)),
57 | update: () => {},
58 | isTopAlign: computed(() => true),
59 | isStreamPaused: ref(false),
60 | } as UseNotivueReturn
61 | }
62 |
63 | const store = useStore()
64 |
65 | return {
66 | ...store.config,
67 | isStreamPaused: readonly(store.timeouts.isStreamPaused),
68 | isTopAlign: computed(() => store.config.position.value.indexOf('top') === 0),
69 | }
70 | }
71 |
72 | /**
73 | * @deprecated
74 | *
75 | * Since version 2.0.0, import `push` directly instead.
76 | *
77 | * ```ts
78 | * import { push } from 'notivue'
79 | * ```
80 | */
81 | export function usePush() {
82 | return push
83 | }
84 |
85 | /**
86 | * Composable to get the current displayed notifications and queue.
87 | *
88 | * @returns
89 | *
90 | * Object of two computed properties:
91 | *
92 | * - `entries` - read-only reactive array of all the current displayed notifications
93 | * - `queue` - read-only reactive array of all the notifications waiting to be displayed
94 | *
95 | * @docs https://docs.notivue.smastrom.io/api/use-notifications
96 | */
97 | export function useNotifications(): NotivueComputedEntries {
98 | if (isSSR) {
99 | return {
100 | entries: computed(() => []),
101 | queue: computed(() => []),
102 | } as NotivueComputedEntries
103 | }
104 |
105 | const store = useStore()
106 |
107 | return {
108 | entries: computed(() => store.items.entries.value.map(getSlotItem)),
109 | queue: computed(() => store.queue.entries.value.map(getSlotItem)),
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/packages/notivue/core/utils.ts:
--------------------------------------------------------------------------------
1 | import { toRaw, customRef, type Ref, type ToRefs } from 'vue'
2 |
3 | import { NotificationTypeKeys as NType } from './constants'
4 |
5 | import type {
6 | StoreItem,
7 | NotivueItem,
8 | HiddenInternalItemData as InternalKeys,
9 | NotificationType,
10 | NotivueConfigRequired,
11 | Obj,
12 | PushOptionsWithInternals,
13 | } from 'notivue'
14 |
15 | export const isSSR = typeof window === 'undefined'
16 |
17 | export function mergeDeep(target: T, source: Record): T {
18 | const merged: T = { ...target }
19 |
20 | for (const key in source) {
21 | if (source.hasOwnProperty(key)) {
22 | if (isPlainObject(source[key])) {
23 | merged[key as keyof T] = mergeDeep(target[key as keyof T], source[key]) as T[keyof T]
24 | } else {
25 | merged[key as keyof T] = source[key]
26 | }
27 | }
28 | }
29 |
30 | return merged
31 | }
32 |
33 | export function mergeNotificationOptions(
34 | configOptions: NotivueConfigRequired['notifications'],
35 | pushOptions: PushOptionsWithInternals
36 | ) {
37 | pushOptions.props ||= {} as T
38 |
39 | return {
40 | ...configOptions[pushOptions.type],
41 | ...configOptions.global,
42 | ...pushOptions,
43 | ...(pushOptions.type === 'promise' ? { duration: Infinity } : {}), // Enforce this
44 | }
45 | }
46 |
47 | // https://github.com/tailwindlabs/tailwindcss/blob/master/src/util/isPlainObject.js
48 | function isPlainObject(value: unknown) {
49 | if (Object.prototype.toString.call(value) !== '[object Object]') {
50 | return false
51 | }
52 |
53 | const prototype = Object.getPrototypeOf(value)
54 | return prototype === null || Object.getPrototypeOf(prototype) === null
55 | }
56 |
57 | export function createConfigRefs(
58 | target: T,
59 | source: Record,
60 | isRunning: Ref
61 | ) {
62 | const conf = mergeDeep(target, source) as T
63 |
64 | function configRef(value: T) {
65 | return customRef((track, trigger) => ({
66 | get() {
67 | track()
68 | return value
69 | },
70 | set(newValue) {
71 | if (!isRunning.value) return
72 |
73 | value = newValue
74 | trigger()
75 | },
76 | }))
77 | }
78 |
79 | for (const key in conf) conf[key] = configRef(conf[key]) as any
80 | return conf as ToRefs
81 | }
82 |
83 | export function toRawConfig(config: ToRefs) {
84 | return Object.entries(config).reduce(
85 | (acc, [key, { value }]) => ({ ...acc, [key]: toRaw(value) }),
86 | {}
87 | ) as T
88 | }
89 |
90 | export const isStatic = (type: NotificationType) =>
91 | type === NType.SUCCESS || type === NType.ERROR || type === NType.WARNING || type === NType.INFO
92 |
93 | export const internalKeys: (keyof InternalKeys)[] = [
94 | 'timeout',
95 | 'resumedAt',
96 | 'remaining',
97 | // Maybe in future releases these could be exposed
98 | 'animationAttrs',
99 | 'positionStyles',
100 | ]
101 |
102 | export function getSlotItem(item: StoreItem) {
103 | return Object.fromEntries(
104 | Object.entries(item).filter(([key]) => !internalKeys.includes(key as keyof InternalKeys))
105 | ) as NotivueItem
106 | }
107 |
--------------------------------------------------------------------------------
/packages/notivue/index.ts:
--------------------------------------------------------------------------------
1 | export { push } from '@/core/createPush'
2 | export { push as pushAstro } from './astro/push'
3 |
4 | export { updateConfig } from '@/core/createStore'
5 | export { createNotivue } from '@/core/createNotivue'
6 | export { createNotivue as createNotivueAstro } from './astro/createNotivue'
7 |
8 | export { startInstance, stopInstance } from '@/core/createInstance'
9 |
10 | export { usePush, useNotivue, useNotifications, useNotivueInstance } from '@/core/useStore'
11 | export { useNotivueKeyboard } from '@/NotivueKeyboard/useNotivueKeyboard'
12 |
13 | export { default as Notivue } from '@/Notivue/Notivue.vue'
14 | export { default as NotivueAstro } from './astro/Notivue.vue'
15 | export { default as NotivueSwipe } from '@/NotivueSwipe/NotivueSwipe.vue'
16 | export { default as NotivueKeyboard } from '@/NotivueKeyboard/NotivueKeyboard.vue'
17 |
18 | export { default as Notifications } from '@/Notifications/Notification.vue'
19 | export { default as Notification } from '@/Notifications/Notification.vue'
20 | export { default as NotificationProgress } from '@/Notifications/NotificationProgress.vue'
21 | export { default as NotificationsProgress } from '@/Notifications/NotificationProgress.vue'
22 |
23 | export {
24 | lightTheme,
25 | pastelTheme,
26 | materialTheme,
27 | darkTheme,
28 | slateTheme,
29 | } from '@/Notifications/themes'
30 |
31 | export { filledIcons, outlinedIcons } from '@/Notifications/icons'
32 |
33 | export { DEFAULT_CONFIG } from '@/core/constants'
34 |
35 | export * from '@/core/types'
36 | export * from '@/Notivue/types'
37 | export * from '@/NotivueSwipe/types'
38 | export * from '@/NotivueKeyboard/types'
39 | export * from '@/Notifications/types'
40 | export * from './astro/types'
41 |
--------------------------------------------------------------------------------
/packages/notivue/nuxt/index.d.ts:
--------------------------------------------------------------------------------
1 | import { ModuleOptions } from './module'
2 |
3 | declare module '@nuxt/schema' {
4 | interface NuxtConfig {
5 | ['notivue']?: ModuleOptions
6 | }
7 | interface NuxtOptions {
8 | ['notivue']?: ModuleOptions
9 | }
10 | }
11 |
12 | declare module 'nuxt/schema' {
13 | interface NuxtConfig {
14 | ['notivue']?: ModuleOptions
15 | }
16 | interface NuxtOptions {
17 | ['notivue']?: ModuleOptions
18 | }
19 | }
20 |
21 | export { ModuleOptions, default } from './module'
22 |
--------------------------------------------------------------------------------
/packages/notivue/nuxt/module.cjs:
--------------------------------------------------------------------------------
1 | module.exports = function (...args) {
2 | return import('./module.mjs').then((m) => m.default.call(this, ...args))
3 | }
4 |
5 | const _meta = (module.exports.meta = require('./module.json'))
6 | module.exports.getMeta = () => Promise.resolve(_meta)
7 |
--------------------------------------------------------------------------------
/packages/notivue/nuxt/module.d.ts:
--------------------------------------------------------------------------------
1 | import * as _nuxt_schema from '@nuxt/schema'
2 |
3 | import type { NotificationType, NotificationOptions, NotivueConfig } from 'notivue'
4 |
5 | type ModuleOptions = Omit & {
6 | startOnCreation?: boolean
7 | /**
8 | * Whether to create and inject the notivue store in the Vue app.
9 | * Equivalent of calling `createNotivue(app)` in the main.js of a non-nuxt app.
10 | */
11 | addPlugin?: boolean
12 | /** Notification options for each type. */
13 | notifications?: Partial<
14 | Record<
15 | NotificationType | 'global',
16 | Omit & {
17 | /** String to use as default title, an empty string doesn't render the title. */
18 | title?: string
19 | /** String to use as default message. */
20 | message?: string
21 | }
22 | >
23 | >
24 | }
25 |
26 | declare const _default: _nuxt_schema.NuxtModule
27 |
28 | export { type ModuleOptions, _default as default }
29 |
--------------------------------------------------------------------------------
/packages/notivue/nuxt/module.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "notivue/nuxt",
3 | "configKey": "notivue",
4 | "version": "2.4.5"
5 | }
6 |
--------------------------------------------------------------------------------
/packages/notivue/nuxt/module.mjs:
--------------------------------------------------------------------------------
1 | import { defineNuxtModule, addPluginTemplate, addImports, addComponent } from '@nuxt/kit'
2 | import { defu } from 'defu'
3 |
4 | import { getFunctions, getObjects, getComponents } from '../shared/exports'
5 |
6 | const module = defineNuxtModule({
7 | meta: {
8 | name: 'nuxt/notivue',
9 | configKey: 'notivue',
10 | compatibility: {
11 | nuxt: '>=3.5.0',
12 | },
13 | },
14 |
15 | async setup(moduleOptions, nuxt) {
16 | nuxt.options.runtimeConfig.public.notivue = defu(
17 | nuxt.options.runtimeConfig.public.notivue || {},
18 | moduleOptions
19 | )
20 |
21 | if (nuxt.options.runtimeConfig.public.notivue.addPlugin !== false) {
22 | addPluginTemplate({
23 | filename: '001.notivue.client.mjs',
24 | getContents() {
25 | return `
26 | import { createNotivue } from 'notivue'
27 | import { defineNuxtPlugin, useRuntimeConfig } from '#imports'
28 |
29 | function nullToInf(obj) {
30 | if (obj == null) return 1 / 0
31 |
32 | if (typeof obj === 'object') {
33 | for (let key in obj) obj[key] = nullToInf(obj[key])
34 | }
35 |
36 | return obj
37 | }
38 |
39 | export default defineNuxtPlugin(({ vueApp }) => {
40 | const options = useRuntimeConfig().public?.notivue || {}
41 | const deserializedOpts = nullToInf(JSON.parse(JSON.stringify(options)))
42 | delete deserializedOpts.addPlugin
43 |
44 | const notivue = createNotivue(deserializedOpts)
45 |
46 | vueApp.use(notivue)
47 | })
48 | `
49 | },
50 | })
51 | }
52 |
53 | for (const name of [...getFunctions(), ...getObjects({ omit: ['DEFAULT_CONFIG'] })]) {
54 | addImports({ name, as: name, from: 'notivue' })
55 | }
56 |
57 | for (const name of getComponents()) {
58 | await addComponent({ name, export: name, filePath: 'notivue' })
59 | }
60 | },
61 | })
62 |
63 | export { module as default }
64 |
--------------------------------------------------------------------------------
/packages/notivue/scripts/verify-exports.js:
--------------------------------------------------------------------------------
1 | import { exports } from '../shared/exports.js'
2 | import * as index from '../dist/index.js'
3 |
4 | const objExports = Object.values(exports).flat()
5 | const jsExports = Object.keys(index)
6 |
7 | if (objExports.length !== jsExports.length) {
8 | if (objExports.length < jsExports.length) {
9 | const missing = jsExports.filter((name) => !objExports.includes(name))
10 | throw new Error('Missing exports in shared/exports.js -> ' + missing.join(', '))
11 | } else {
12 | const missing = objExports.filter((name) => !jsExports.includes(name))
13 | throw new Error('Missing exports in dist/index.js -> ' + missing.join(', '))
14 | }
15 | } else {
16 | let errors = ''
17 |
18 | const jsMissing = []
19 | const objMissing = []
20 |
21 | objExports.forEach((name) => {
22 | if (!jsExports.includes(name)) jsMissing.push(name)
23 | })
24 |
25 | jsExports.forEach((name) => {
26 | if (!objExports.includes(name)) objMissing.push(name)
27 | })
28 |
29 | if (jsMissing.length > 0) {
30 | errors += 'Inconsistent exports found in shared/exports.js -> ' + jsMissing.join(', ')
31 | }
32 |
33 | if (objMissing.length > 0) {
34 | errors += '\n' + 'Inconsistent exports found in dist/index.js -> ' + objMissing.join(', ')
35 | }
36 |
37 | if (errors) throw new Error(errors)
38 | }
39 |
40 | console.log('All exports are valid.')
41 |
--------------------------------------------------------------------------------
/packages/notivue/scripts/verify-tarball.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | tar_file=$(find ./ -name "*.tgz" | head -n 1)
4 | temp_dir="./temp_tar_check"
5 |
6 | if [ ! -f "$tar_file" ]; then
7 | echo "Tarball not found!"
8 | exit 1
9 | fi
10 |
11 | # Create temp directory
12 | mkdir -p $temp_dir
13 |
14 | declare -a files=(
15 | "README.md" "package.json" "LICENSE"
16 | "dist/index.js" "dist/index.d.ts" "dist/astro.js"
17 | "dist/core/animations.css" "dist/Notifications/notifications.css" "dist/Notifications/notifications-progress.css"
18 | "nuxt/module.mjs" "nuxt/module.cjs" "nuxt/module.d.ts" "nuxt/module.json"
19 | "shared/exports.js"
20 | )
21 |
22 | for file in "${files[@]}"; do
23 | if ! tar tzf "$tar_file" | grep -qE "package/$file$"; then
24 | echo "File $file not found in tarball."
25 | exit 1
26 | fi
27 | done
28 |
29 | # Extract index.js for checking
30 | tar xzf "$tar_file" -C $temp_dir package/dist/index.js
31 |
32 | # Check if console.log exists in the extracted index.js
33 | if grep "console.log" $temp_dir/package/dist/index.js; then
34 | echo "Error: console.log found in index.js"
35 | rm -rf $temp_dir
36 | exit 1
37 | fi
38 |
39 | echo "All tarball checks passed."
40 | rm -rf $temp_dir
41 |
--------------------------------------------------------------------------------
/packages/notivue/shared/ClientOnly.ts:
--------------------------------------------------------------------------------
1 | import { createElementBlock, defineComponent, onMounted, ref } from 'vue'
2 |
3 | /**
4 | * This is a streamlined version of Nuxt's
5 | * which like it also relies on a boolean ref to render the slot.
6 | *
7 | * While this is unnecessary for client-only apps but definetely required
8 | * for Nuxt apps, users may still be using 'vite-plugin-ssr' or whatever
9 | * non-nuxt SSR solution.
10 | *
11 | * This universally removes any chance of SSR issues when importing
12 | * and while keeping the internal
13 | * store code clean without adding a bunch of (isSSR) checks.
14 | */
15 |
16 | export const NotivueClientOnly = defineComponent({
17 | setup(_, { slots, attrs }) {
18 | const isMounted = ref(false)
19 |
20 | onMounted(() => (isMounted.value = true))
21 |
22 | return () => {
23 | if (isMounted.value) return slots.default?.()
24 |
25 | return createElementBlock('span', attrs, '')
26 | }
27 | },
28 | })
29 |
--------------------------------------------------------------------------------
/packages/notivue/shared/exports.js:
--------------------------------------------------------------------------------
1 | export const exports = {
2 | functions: [
3 | 'createNotivue',
4 | 'push',
5 | 'updateConfig',
6 | 'startInstance',
7 | 'stopInstance',
8 |
9 | 'usePush',
10 | 'useNotivue',
11 | 'useNotivueInstance',
12 | 'useNotifications',
13 | 'useNotivueKeyboard',
14 | ],
15 | objects: [
16 | 'DEFAULT_CONFIG',
17 |
18 | 'lightTheme',
19 | 'pastelTheme',
20 | 'materialTheme',
21 | 'darkTheme',
22 | 'slateTheme',
23 |
24 | 'filledIcons',
25 | 'outlinedIcons',
26 | ],
27 | components: [
28 | 'Notivue',
29 | 'NotivueSwipe',
30 | 'NotivueKeyboard',
31 |
32 | 'Notification',
33 | 'Notifications', // Alias
34 | 'NotificationProgress',
35 | 'NotificationsProgress', // Alias
36 | ],
37 | astro: ['NotivueAstro', 'pushAstro', 'createNotivueAstro'],
38 | }
39 |
40 | const getExports = (type, omit) => exports[type].filter((name) => !omit.includes(name))
41 |
42 | export const getFunctions = ({ omit } = { omit: [] }) => getExports('functions', omit)
43 | export const getObjects = ({ omit } = { omit: [] }) => getExports('objects', omit)
44 | export const getComponents = ({ omit } = { omit: [] }) => getExports('components', omit)
45 |
--------------------------------------------------------------------------------
/packages/notivue/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2015",
4 | "module": "ES2020",
5 | "moduleResolution": "Node",
6 | "strict": true,
7 | "resolveJsonModule": true,
8 | "isolatedModules": true,
9 | "esModuleInterop": true,
10 | "lib": ["ES2015", "DOM"],
11 | "skipLibCheck": true,
12 | "noEmit": true,
13 | "baseUrl": ".",
14 | "paths": {
15 | "@/core/*": ["core/*"],
16 | "@/shared/*": ["shared/*"],
17 | "@/Notivue/*": ["Notivue/*"],
18 | "@/NotivueSwipe/*": ["NotivueSwipe/*"],
19 | "@/NotivueKeyboard/*": ["NotivueKeyboard/*"],
20 | "@/Notifications/*": ["Notifications/*"],
21 | "@/astro": ["astro/*"],
22 | "notivue": ["index.ts"]
23 | }
24 | },
25 | "include": ["."]
26 | }
27 |
--------------------------------------------------------------------------------
/packages/notivue/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import { fileURLToPath } from 'url'
3 | import { writeFileSync } from 'fs'
4 |
5 | import vue from '@vitejs/plugin-vue'
6 | import dts from 'vite-plugin-dts'
7 |
8 | // @ts-ignore
9 | import { getFunctions, getObjects, getComponents } from './shared/exports'
10 |
11 | const path = (url: string) => fileURLToPath(new URL(url, import.meta.url))
12 |
13 | const isFinalBundle = !process.argv.includes('--watch')
14 |
15 | export default defineConfig({
16 | resolve: {
17 | alias: {
18 | '@/core': path('./core'),
19 | '@/shared': path('./shared'),
20 | '@/Notivue': path('./Notivue'),
21 | '@/NotivueSwipe': path('./NotivueSwipe'),
22 | '@/NotivueKeyboard': path('./NotivueKeyboard'),
23 | '@/Notifications': path('./Notifications'),
24 | '@/astro': path('./astro'),
25 | notivue: path('./index.ts'),
26 | },
27 | },
28 | esbuild: {
29 | drop: isFinalBundle ? ['console'] : [],
30 | minifyIdentifiers: false,
31 | minifySyntax: false,
32 | },
33 | build: {
34 | emptyOutDir: isFinalBundle,
35 | target: 'es2015',
36 | minify: false,
37 | lib: {
38 | entry: 'index.ts',
39 | fileName: 'index',
40 | formats: ['es'],
41 | },
42 | rollupOptions: {
43 | external: ['vue'],
44 | output: {
45 | globals: {
46 | vue: 'Vue',
47 | },
48 | },
49 | },
50 | },
51 | plugins: [
52 | dts({
53 | rollupTypes: true,
54 | }),
55 | vue(),
56 | {
57 | name: 'write-astro-entry',
58 | closeBundle() {
59 | const astroReExports = [
60 | ...getFunctions({ omit: ['push', 'createNotivue'] }),
61 | ...getComponents({ omit: ['Notivue'] }),
62 | ...getObjects(),
63 | ]
64 |
65 | writeFileSync(
66 | 'dist/astro.js',
67 | [
68 | 'export { pushAstro as push } from "./index.js";',
69 | 'export { NotivueAstro as Notivue } from "./index.js";',
70 | 'export { createNotivueAstro as createNotivue } from "./index.js";',
71 | ...astroReExports.map((name) => `export { ${name} } from "./index.js";`),
72 | ].join('\n')
73 | )
74 | },
75 | },
76 | ],
77 | })
78 |
--------------------------------------------------------------------------------
/playground/.gitignore:
--------------------------------------------------------------------------------
1 | # Nuxt dev/build outputs
2 | .output
3 | .data
4 | .nuxt
5 | .nitro
6 | .cache
7 | dist
8 |
9 | node_modules
10 | .DS_Store
11 |
--------------------------------------------------------------------------------
/playground/.npmrc:
--------------------------------------------------------------------------------
1 | shamefully-hoist=true
2 |
3 |
--------------------------------------------------------------------------------
/playground/app.vue:
--------------------------------------------------------------------------------
1 |
39 |
40 |
41 |
42 |
47 |
51 |
52 |
53 |
57 |
58 |
62 |
63 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
95 |
--------------------------------------------------------------------------------
/playground/assets/inter-v13-latin-500.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smastrom/notivue/5d5b8e85b40a3271e151b27a206872fa3c19f1c0/playground/assets/inter-v13-latin-500.woff2
--------------------------------------------------------------------------------
/playground/assets/inter-v13-latin-700.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smastrom/notivue/5d5b8e85b40a3271e151b27a206872fa3c19f1c0/playground/assets/inter-v13-latin-700.woff2
--------------------------------------------------------------------------------
/playground/assets/profile-picture.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smastrom/notivue/5d5b8e85b40a3271e151b27a206872fa3c19f1c0/playground/assets/profile-picture.jpg
--------------------------------------------------------------------------------
/playground/assets/pt-sans-narrow-v17-latin-700.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smastrom/notivue/5d5b8e85b40a3271e151b27a206872fa3c19f1c0/playground/assets/pt-sans-narrow-v17-latin-700.woff2
--------------------------------------------------------------------------------
/playground/assets/pt-sans-narrow-v17-latin-regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smastrom/notivue/5d5b8e85b40a3271e151b27a206872fa3c19f1c0/playground/assets/pt-sans-narrow-v17-latin-regular.woff2
--------------------------------------------------------------------------------
/playground/components/custom-notifications/SimpleNotification.vue:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
23 |
24 | {{ item.message }}
25 |
26 |
27 |
40 |
41 |
42 |
43 |
97 |
--------------------------------------------------------------------------------
/playground/components/icons/ArrowIcon.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
25 |
26 |
--------------------------------------------------------------------------------
/playground/components/icons/CloseIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
26 |
27 |
--------------------------------------------------------------------------------
/playground/components/icons/CustomIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
--------------------------------------------------------------------------------
/playground/components/icons/DestroyIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
--------------------------------------------------------------------------------
/playground/components/icons/DismissIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
--------------------------------------------------------------------------------
/playground/components/icons/InfoIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/playground/components/icons/PromiseIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/playground/components/icons/SuccessIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
--------------------------------------------------------------------------------
/playground/components/icons/VueIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
18 |
19 |
--------------------------------------------------------------------------------
/playground/components/icons/WarnIcon.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
18 |
19 |
--------------------------------------------------------------------------------
/playground/components/nav/Nav.vue:
--------------------------------------------------------------------------------
1 |
22 |
23 |
24 |
32 |
33 | Notivue is now running again. This notice will be dismissed shortly.
35 | Notivue has been stopped. Restart it to create notifications.
36 |
37 |
38 |
71 |
72 |
73 |
123 |
--------------------------------------------------------------------------------
/playground/components/nav/NavActions.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/playground/components/nav/NavNotificationsCustomization.vue:
--------------------------------------------------------------------------------
1 |
29 |
30 |
31 |
32 |
35 |
38 |
45 |
46 |
47 |
48 |
49 |
56 |
--------------------------------------------------------------------------------
/playground/components/nav/NavNotificationsThemes.vue:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 |
21 |
31 |
32 |
33 |
34 |
41 | utils/store
42 |
--------------------------------------------------------------------------------
/playground/components/nav/NavNotivuePosition.vue:
--------------------------------------------------------------------------------
1 |
27 |
28 |
29 |
30 |
40 |
41 |
42 |
43 |
46 |
54 |
55 |
56 |
57 |
86 |
--------------------------------------------------------------------------------
/playground/components/nav/NavPushBuiltIn.vue:
--------------------------------------------------------------------------------
1 |
30 |
31 |
32 | {
37 | // console.log('AutoClear!', item)
38 | },
39 | onManualClear: (item) => {
40 | // console.log('Manual Clear!', item)
41 | },
42 | })
43 | "
44 | text="Success"
45 | >
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/playground/components/nav/NavPushHeadless.vue:
--------------------------------------------------------------------------------
1 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/playground/components/shared/Button.vue:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
17 |
18 |
19 |
24 |
--------------------------------------------------------------------------------
/playground/components/shared/ButtonGroup.vue:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
21 |
22 |
23 |
47 |
--------------------------------------------------------------------------------
/playground/components/shared/QueueCount.vue:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | {{ queue.length }}
20 |
21 |
22 |
23 | Enqueued
24 |
25 |
26 |
27 |
28 |
86 |
--------------------------------------------------------------------------------
/playground/middleware/push.global.ts:
--------------------------------------------------------------------------------
1 | export default defineNuxtRouteMiddleware(() => {
2 | updateConfig((currConf) => {
3 | console.log('Current config:', currConf)
4 | return {}
5 | })
6 |
7 | push.info('Welcome to Notivue! Use the controls below to test it out.')
8 | })
9 |
--------------------------------------------------------------------------------
/playground/nuxt.config.ts:
--------------------------------------------------------------------------------
1 | import { getHead } from './utils/head'
2 |
3 | export default defineNuxtConfig({
4 | modules: ['notivue/nuxt'],
5 | ssr: true,
6 | devtools: {
7 | enabled: false,
8 | },
9 | experimental: {
10 | componentIslands: true,
11 | },
12 | notivue: {
13 | // addPlugin: true,
14 | // startOnCreation: true,
15 | notifications: {
16 | global: {
17 | // duration: Infinity,
18 | },
19 | },
20 | },
21 | nitro: {
22 | preset: 'cloudflare-pages',
23 | },
24 | app: {
25 | head: getHead(),
26 | },
27 | vite: {
28 | esbuild: {
29 | minifyIdentifiers: !import.meta.env.DEV,
30 | minifySyntax: !import.meta.env.DEV,
31 | },
32 | build: {
33 | minify: import.meta.env.DEV ? false : 'esbuild',
34 | cssMinify: 'lightningcss',
35 | },
36 | css: {
37 | transformer: 'lightningcss',
38 | },
39 | },
40 | css: [
41 | 'assets/style.css',
42 | 'notivue/notifications.css',
43 | 'notivue/notifications-progress.css',
44 | 'notivue/animations.css',
45 | ],
46 | })
47 |
--------------------------------------------------------------------------------
/playground/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "notivue-playground",
3 | "private": true,
4 | "scripts": {
5 | "build": "nuxt build",
6 | "dev": "nuxi cleanup && nuxt dev --port=5173",
7 | "generate": "nuxt generate",
8 | "preview": "nuxt preview",
9 | "postinstall": "nuxt prepare"
10 | },
11 | "devDependencies": {
12 | "@types/luxon": "^3.4.2",
13 | "@types/node": "^22.5.1",
14 | "lightningcss": "^1.26.0",
15 | "nuxt": "^3.13.0",
16 | "vue": "^3.4.38"
17 | },
18 | "dependencies": {
19 | "luxon": "^3.5.0",
20 | "notivue": "workspace:*"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/playground/plugins/store.ts:
--------------------------------------------------------------------------------
1 | export default defineNuxtPlugin(() => {
2 | return {
3 | provide: {
4 | store: createStore(),
5 | },
6 | }
7 | })
8 |
--------------------------------------------------------------------------------
/playground/public/icon.svg:
--------------------------------------------------------------------------------
1 |
35 |
--------------------------------------------------------------------------------
/playground/public/og-image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smastrom/notivue/5d5b8e85b40a3271e151b27a206872fa3c19f1c0/playground/public/og-image.jpg
--------------------------------------------------------------------------------
/playground/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./.nuxt/tsconfig.json"
3 | }
4 |
--------------------------------------------------------------------------------
/playground/utils/date.ts:
--------------------------------------------------------------------------------
1 | import { DateTime } from 'luxon'
2 |
3 | export function toNow(date: number) {
4 | return DateTime.fromMillis(date).toRelative({ locale: 'en', padding: 60 * 1000 })
5 | }
6 |
--------------------------------------------------------------------------------
/playground/utils/head.ts:
--------------------------------------------------------------------------------
1 | const description = 'Notivue is a powerful toast notification system for Vue and Nuxt.'
2 |
3 | export function getHead() {
4 | return {
5 | title: 'Notivue - Powerful toast notification system for Vue and Nuxt',
6 | link: [
7 | {
8 | rel: 'icon',
9 | href: '/icon.svg',
10 | },
11 | ],
12 | htmlAttrs: {
13 | lang: 'en',
14 | },
15 | meta: [
16 | {
17 | hid: 'description',
18 | name: 'description',
19 | content: description,
20 | },
21 | {
22 | hid: 'og:title',
23 | property: 'og:title',
24 | content: 'Notivue - ' + description,
25 | },
26 | {
27 | hid: 'og:description',
28 | property: 'og:description',
29 | content: description,
30 | },
31 | {
32 | hid: 'og:image',
33 | property: 'og:image',
34 | content: '/og-image.jpg',
35 | },
36 | {
37 | hid: 'og:url',
38 | property: 'og:url',
39 | content: 'https://notivue.smastrom.io',
40 | },
41 | {
42 | hid: 'twitter:title',
43 | name: 'twitter:title',
44 | content: 'Notivue - ' + description,
45 | },
46 | {
47 | hid: 'twitter:description',
48 | name: 'twitter:description',
49 | content: description,
50 | },
51 |
52 | {
53 | hid: 'twitter:image',
54 | name: 'twitter:image',
55 | content: '/og-image.jpg',
56 | },
57 | {
58 | hid: 'twitter:card',
59 | name: 'twitter:card',
60 | content: 'summary_large_image',
61 | },
62 | ],
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/playground/utils/misc.ts:
--------------------------------------------------------------------------------
1 | export function getRandomInt(min: number, max: number) {
2 | min = Math.ceil(min)
3 | max = Math.floor(max)
4 | return Math.floor(Math.random() * (max - min) + min)
5 | }
6 |
7 | export const isSSR = typeof window === 'undefined'
8 |
9 | export function isMobile() {
10 | if (isSSR) return false
11 | return /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(navigator.userAgent)
12 | }
13 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - 'packages/*'
3 | - 'tests'
4 | - 'playground'
5 | - 'astro-playground'
6 |
--------------------------------------------------------------------------------
/tests/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | pnpm-debug.log*
3 |
4 | node_modules
5 | .DS_Store
6 | dist
7 | coverage
8 |
9 | /cypress/downloads/
10 | /cypress/screenshots/
11 | /cypress/videos/
12 |
13 | # Editor directories and files
14 | .vscode/*
15 | !.vscode/extensions.json
16 | .idea
17 | *.suo
18 | *.ntvs*
19 | *.njsproj
20 | *.sln
21 | *.sw?
22 |
--------------------------------------------------------------------------------
/tests/Notifications/accessibility.cy.ts:
--------------------------------------------------------------------------------
1 | import { Classes } from '@/Notifications/constants'
2 |
3 | it('All elements are accessible', () => {
4 | cy.mountNotifications({
5 | options: {
6 | ariaRole: 'alert',
7 | ariaLive: 'assertive',
8 | },
9 | })
10 |
11 | .get('.Success')
12 | .click()
13 |
14 | cy.injectAxe()
15 | cy.checkA11y(`.${Classes.NOTIFICATION}`)
16 |
17 | cy.get('.Notivue__content')
18 | .should('have.attr', 'role', 'alert')
19 | .and('have.attr', 'aria-live', 'assertive')
20 | })
21 |
--------------------------------------------------------------------------------
/tests/Notifications/close-button.cy.ts:
--------------------------------------------------------------------------------
1 | import { Classes } from '@/Notifications/constants'
2 |
3 | it('Close button dismisses notification', () => {
4 | cy.mountNotifications()
5 |
6 | .get('.Success')
7 | .click()
8 |
9 | .get(`.${Classes.CLOSE}`)
10 | .click()
11 | .getNotifications()
12 | .should('have.length', 0)
13 | })
14 |
15 | it('It is not renderer if hideClose is true', () => {
16 | cy.mountNotifications({
17 | hideClose: true,
18 | })
19 |
20 | .clickAllStatic()
21 |
22 | .get(`.${Classes.CLOSE}`)
23 | .should('not.exist')
24 | })
25 |
--------------------------------------------------------------------------------
/tests/Notifications/components/Notivue.vue:
--------------------------------------------------------------------------------
1 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/tests/Notifications/elements.cy.ts:
--------------------------------------------------------------------------------
1 | import { NotivueIcons, outlinedIcons } from 'notivue'
2 |
3 | import { Classes as _Classes } from '@/Notifications/constants'
4 | import { DEFAULT_ANIM_DURATION } from '@/support/utils'
5 |
6 | const { TRANSITION, ...Classes } = _Classes
7 |
8 | const defaultClasses = Object.values(Classes).filter((className) => className !== Classes.DUPLICATE)
9 |
10 | it('All elements are rendered and only exists one element per class', () => {
11 | cy.mountNotifications({
12 | options: {
13 | title: 'Success',
14 | message: 'This is a success message',
15 | },
16 | })
17 |
18 | .get('.Success')
19 | .click()
20 |
21 | defaultClasses.forEach((className) => {
22 | cy.get(`.${className}`).should('exist').and('have.length', 1)
23 | })
24 | })
25 |
26 | it('Duplicate class is added correctly', () => {
27 | cy.mountNotifications(undefined, {
28 | avoidDuplicates: true,
29 | })
30 |
31 | .get('.Success')
32 | .click()
33 | .wait(DEFAULT_ANIM_DURATION)
34 | .get('.Success')
35 | .click()
36 |
37 | cy.get(`.${Classes.DUPLICATE}`).should('exist').and('have.length', 1)
38 | })
39 |
40 | it('Title is not rendered by default (if empty string) while all other elements are', () => {
41 | cy.mountNotifications()
42 |
43 | .get('.Success')
44 | .click()
45 |
46 | defaultClasses.forEach((className) => {
47 | if (className === Classes.TITLE) {
48 | cy.get(`.${className}`).should('not.exist')
49 | } else {
50 | cy.get(`.${className}`).should('exist').and('have.length', 1)
51 | }
52 | })
53 | })
54 |
55 | function getIconConfig(iconObj: NotivueIcons) {
56 | return {
57 | icons: {
58 | ...outlinedIcons,
59 | ...iconObj,
60 | },
61 | }
62 | }
63 |
64 | describe('Icons', () => {
65 | it('Icon is not rendered if null', () => {
66 | cy.mountNotifications(getIconConfig({ success: null }))
67 |
68 | .get('.Success')
69 | .click()
70 |
71 | .get(`.${Classes.ICON}`)
72 | .should('not.exist')
73 | })
74 |
75 | it('Text icons are rendered properly', () => {
76 | cy.mountNotifications(getIconConfig({ success: 'SOMETEXT' }))
77 |
78 | .get('.Success')
79 | .click()
80 |
81 | .get(`.${Classes.ICON}`)
82 | .should('exist')
83 | .and('have.length', 1)
84 | .and('have.text', 'SOMETEXT')
85 | .and('have.prop', 'tagName')
86 | .and('eq', 'DIV')
87 | })
88 | })
89 |
90 | it('Close button is not render if null', () => {
91 | cy.mountNotifications(getIconConfig({ close: null }))
92 |
93 | .get('.Success')
94 | .click()
95 |
96 | cy.get(`.${Classes.CLOSE}`).should('not.exist')
97 | cy.get(`.${Classes.NOTIFICATION}`).should('not.contain', Classes.CLOSE_ICON)
98 | })
99 |
--------------------------------------------------------------------------------
/tests/Notifications/themes.cy.ts:
--------------------------------------------------------------------------------
1 | import { lightTheme, darkTheme, pastelTheme, materialTheme, slateTheme } from 'notivue'
2 | import { Classes } from '@/Notifications/constants'
3 |
4 | describe('Themes', () => {
5 | it('All themes are injected properly', () => {
6 | ;[lightTheme, darkTheme, pastelTheme, materialTheme, slateTheme].forEach((theme) => {
7 | cy.mountAndCheckTheme(theme)
8 | })
9 | })
10 |
11 | it('Light theme is applied by default', () => {
12 | cy.mountNotifications()
13 |
14 | .get('.Success')
15 | .click()
16 |
17 | .get(`.${Classes.NOTIFICATION}`)
18 | .should('have.attr', 'style')
19 |
20 | .checkTheme(lightTheme)
21 | })
22 | })
23 |
--------------------------------------------------------------------------------
/tests/Notivue/accessibility.cy.ts:
--------------------------------------------------------------------------------
1 | it('All elements should be accessible', () => {
2 | cy.mountNotivue().clickRandomStatic()
3 |
4 | cy.injectAxe()
5 | cy.checkA11y('.Root')
6 | })
7 |
8 | describe('Aria label', () => {
9 | it('Message is always renderd', () => {
10 | cy.mountNotivue({
11 | config: {
12 | notifications: {
13 | global: { message: 'This is a message' },
14 | },
15 | },
16 | })
17 |
18 | .clickRandomStatic()
19 | .getContainer()
20 | .should('have.attr', 'aria-label', 'This is a message')
21 | })
22 |
23 | it('Title is rendered along with the message if defined', () => {
24 | cy.mountNotivue({
25 | config: {
26 | notifications: {
27 | global: { title: 'This is a title', message: 'This is a message' },
28 | },
29 | },
30 | })
31 |
32 | .clickRandomStatic()
33 | .getContainer()
34 | .should('have.attr', 'aria-label', 'This is a title: This is a message')
35 | })
36 | })
37 |
--------------------------------------------------------------------------------
/tests/Notivue/attributes.cy.ts:
--------------------------------------------------------------------------------
1 | it('Notivue attributes are added correctly', () => {
2 | cy.mountNotivue({
3 | props: {
4 | class: 'CustomClass',
5 | },
6 | })
7 |
8 | .clickRandomStatic()
9 |
10 | .get('ol')
11 | .should('have.class', 'CustomClass')
12 | .invoke('attr', 'data-notivue-align')
13 | .should('exist')
14 |
15 | .get('li')
16 | .and('have.attr', 'tabindex', '-1')
17 | .invoke('attr', 'data-notivue-id')
18 |
19 | .get('li > div')
20 | .invoke('attr', 'data-notivue-container')
21 | .should('exist')
22 | .get('li > div')
23 | .invoke('attr', 'tabindex')
24 | .should('exist')
25 | })
26 |
--------------------------------------------------------------------------------
/tests/Notivue/config-animations.cy.ts:
--------------------------------------------------------------------------------
1 | import type { VueWrapper } from '@vue/test-utils'
2 |
3 | // In cypress/support/styles.css
4 | const customAnims = {
5 | enter: 'fade-in',
6 | leave: 'fade-out',
7 | clearAll: 'fade-all',
8 | }
9 |
10 | describe('Animations', () => {
11 | it('Custom animations classes are toggled properly', () => {
12 | cy.mountNotivue({ config: { animations: customAnims } }).checkAnimations(
13 | `.${customAnims.enter}`,
14 | `.${customAnims.leave}`,
15 | `.${customAnims.clearAll}`
16 | )
17 | })
18 |
19 | it('Custom animations are merged properly with defaults', () => {
20 | cy.mountNotivue({
21 | config: {
22 | animations: {
23 | leave: customAnims.leave,
24 | clearAll: customAnims.clearAll,
25 | },
26 | },
27 | }).checkAnimations('.Notivue__enter', '.fade-out', '.fade-all')
28 | })
29 |
30 | it('Should update animations config dynamically', () => {
31 | cy.mountNotivue()
32 | .get('@vue')
33 | .then((wrapper) => wrapper.setProps({ animations: customAnims }))
34 |
35 | .checkAnimations(
36 | `.${customAnims.enter}`,
37 | `.${customAnims.leave}`,
38 | `.${customAnims.clearAll}`
39 | )
40 | })
41 | })
42 |
--------------------------------------------------------------------------------
/tests/Notivue/config-duplicates.cy.ts:
--------------------------------------------------------------------------------
1 | import type { VueWrapper } from '@vue/test-utils'
2 |
3 | describe('Duplicates', () => {
4 | it('Should not create duplicates if static', () => {
5 | cy.mountNotivue({ config: { avoidDuplicates: true } })
6 |
7 | for (let i = 0; i < 10; i++) {
8 | cy.clickAllStatic()
9 | }
10 |
11 | cy.getNotifications().should('have.length', 4, { timeout: 0 })
12 | })
13 |
14 | it('Should create duplicates if dynamic', () => {
15 | cy.mountNotivue({ config: { avoidDuplicates: true } })
16 |
17 | for (let i = 0; i < 10; i++) {
18 | cy.get('.Promise').click()
19 | }
20 |
21 | cy.getNotifications().should('have.length', 10, { timeout: 0 })
22 | })
23 |
24 | it('Should replace duration correctly', () => {
25 | cy.mountNotivue({ config: { avoidDuplicates: true } })
26 |
27 | cy.get('.Success').click()
28 |
29 | cy.wait(3000) // Remaining time 3s
30 |
31 | cy.get('.Success').click() // Remaining time 6s
32 |
33 | cy.wait(5000) // Remaining time 1s
34 |
35 | cy.getNotifications().should('have.length', 1, { timeout: 0 })
36 | })
37 |
38 | it('Should update config dynamically and work', () => {
39 | cy.mountNotivue({ config: { avoidDuplicates: false } })
40 |
41 | cy.get('@vue').then((wrapper) => {
42 | wrapper.setProps({ avoidDuplicates: true })
43 |
44 | for (let i = 0; i < 10; i++) {
45 | cy.clickAllStatic()
46 | }
47 |
48 | cy.getNotifications().should('have.length', 4, { timeout: 0 })
49 | })
50 | })
51 | })
52 |
--------------------------------------------------------------------------------
/tests/Notivue/config-enqueue.cy.ts:
--------------------------------------------------------------------------------
1 | import { RESOLVE_REJECT_DELAY, getRandomInt } from '@/support/utils'
2 |
3 | import type { VueWrapper } from '@vue/test-utils'
4 |
5 | describe('Enqueue', () => {
6 | it('Should enqueue notifications according to limit', () => {
7 | const limit = getRandomInt(1, 8)
8 |
9 | cy.mountNotivue({ config: { enqueue: true, limit } })
10 |
11 | for (let i = 0; i < 20; i++) {
12 | cy.clickRandomStatic()
13 | }
14 |
15 | cy.getNotifications().should('have.length', limit, { timeout: 0 })
16 | })
17 |
18 | it('Should display enqueued notifications after dismissal', () => {
19 | const limit = 3
20 | cy.mountNotivue({ config: { enqueue: true, limit } })
21 |
22 | for (let i = 0; i < 10; i++) {
23 | cy.clickRandomStatic()
24 |
25 | if (i === 2 || i === 5 || i === 8) {
26 | cy.get('.ClearButton')
27 | .eq(0)
28 | .click()
29 | .getNotifications()
30 | .should('have.length', limit, { timeout: 0 })
31 | }
32 | }
33 | })
34 |
35 | it('Should update promises in the queue', () => {
36 | const limit = 1
37 | const resolvedMessage = 'Promise resolved in the queue!'
38 |
39 | cy.mountNotivue({
40 | config: { enqueue: true, limit },
41 | props: {
42 | newOptions: {
43 | message: 'Promise resolved in the queue!',
44 | },
45 | },
46 | })
47 | .clickRandomStatic()
48 | .get('.PushPromiseAndResolve')
49 | .click()
50 | .wait(RESOLVE_REJECT_DELAY)
51 |
52 | .get('.ClearButton')
53 | .click()
54 |
55 | .getNotifications()
56 | .should('contain.text', resolvedMessage, { timeout: 0 })
57 | })
58 |
59 | it('Should update enqueue option dynamically', () => {
60 | const limit = getRandomInt(1, 8)
61 |
62 | cy.mountNotivue()
63 | .get('@vue')
64 | .then((wrapper) => wrapper.setProps({ enqueue: true, limit }))
65 |
66 | for (let i = 0; i < 20; i++) {
67 | cy.clickRandomStatic()
68 | }
69 |
70 | cy.getNotifications().should('have.length', limit)
71 | })
72 | })
73 |
--------------------------------------------------------------------------------
/tests/Notivue/config-limit.cy.ts:
--------------------------------------------------------------------------------
1 | import { getRandomInt } from '@/support/utils'
2 |
3 | import type { VueWrapper } from '@vue/test-utils'
4 |
5 | describe('Limit', () => {
6 | it('User-defined limit works correctly', () => {
7 | const limit = getRandomInt(1, 8)
8 |
9 | cy.mountNotivue({ config: { limit } })
10 |
11 | for (let i = 0; i < 20; i++) {
12 | cy.clickRandomStatic()
13 | }
14 |
15 | cy.getNotifications().should('have.length', limit, { timeout: 0 })
16 | })
17 |
18 | it('Should update limit dynamically', () => {
19 | const newLimit = getRandomInt(1, 8)
20 |
21 | cy.mountNotivue()
22 | .get('@vue')
23 | .then((wrapper) => wrapper.setProps({ limit: newLimit }))
24 |
25 | for (let i = 0; i < 20; i++) {
26 | cy.clickRandomStatic()
27 | }
28 |
29 | cy.getNotifications().should('have.length', newLimit, { timeout: 0 })
30 | })
31 | })
32 |
--------------------------------------------------------------------------------
/tests/Notivue/config-pause-on-hover.cy.ts:
--------------------------------------------------------------------------------
1 | import type { VueWrapper } from '@vue/test-utils'
2 |
3 | describe('Pause on hover', () => {
4 | beforeEach(() => {
5 | cy.throwIfDurationMismatch(6000)
6 | })
7 |
8 | it('Can pause and resume notifications', () => {
9 | cy.mountNotivue()
10 |
11 | .clickRandomStatic()
12 | .wait(4000) // Remaining time 2000ms
13 |
14 | .get('.Notification')
15 | .realMouseMove(0, 0, { position: 'center' })
16 | .wait(4000) // Any value greater than the remaining time
17 |
18 | .get('.Notification')
19 | .should('exist')
20 |
21 | .get('body')
22 | .realMouseMove(50, 50, { position: 'bottomRight' })
23 | .wait(2000)
24 |
25 | .get('.Notification')
26 | .should('not.exist')
27 | })
28 |
29 | it('Should not pause notifications if pauseOnHover is false', () => {
30 | cy.mountNotivue({ config: { pauseOnHover: false } })
31 |
32 | .clickRandomStatic()
33 | .wait(4000) // Remaining: 2000ms
34 |
35 | .get('.Notification')
36 | .realMouseMove(0, 0, { position: 'center' })
37 | .wait(4000) // Any value greater than the remaining time
38 |
39 | .get('.Notification')
40 | .should('not.exist')
41 | })
42 |
43 | it('Should update config dynamically and work', () => {
44 | cy.mountNotivue({ config: { pauseOnHover: false } })
45 |
46 | .get('@vue')
47 | .then((wrapper) => wrapper.setProps({ pauseOnHover: true }))
48 |
49 | .clickRandomStatic()
50 | .wait(4000) // Remaining time 2000ms
51 |
52 | .get('.Notification')
53 | .realMouseMove(0, 0, { position: 'center' })
54 | .wait(4000) // Any value greater than the remaining time
55 |
56 | .get('.Notification')
57 | .should('exist')
58 | })
59 | })
60 |
--------------------------------------------------------------------------------
/tests/Notivue/config-pause-on-touch.cy.ts:
--------------------------------------------------------------------------------
1 | import type { VueWrapper } from '@vue/test-utils'
2 |
3 | describe('Pause on touch', () => {
4 | beforeEach(() => {
5 | cy.throwIfDurationMismatch(6000)
6 | })
7 |
8 | it('Can pause and resume notifications', () => {
9 | cy.mountNotivue()
10 |
11 | .clickRandomStatic()
12 | .wait(5000) // Remaining: 1000ms
13 |
14 | .get('.Notification')
15 | .trigger('pointerdown', { pointerType: 'touch' })
16 | .wait(2000) // More than the remaining time
17 |
18 | .get('.Notification')
19 | .should('exist')
20 | })
21 |
22 | it('Should not pause notifications if pauseOnTouch is false', () => {
23 | cy.mountNotivue({ config: { pauseOnTouch: false } })
24 |
25 | .clickRandomStatic()
26 | .wait(5000) // Remaining: 1000ms
27 |
28 | .get('.Notification')
29 | .trigger('pointerdown', { pointerType: 'touch' })
30 | .wait(2000) // More than the remaining time
31 |
32 | .get('.Notification')
33 | .should('not.exist')
34 | })
35 |
36 | it('Should update config dynamically and work', () => {
37 | cy.mountNotivue({ config: { pauseOnTouch: false } })
38 |
39 | .get('@vue')
40 | .then((wrapper) => wrapper.setProps({ pauseOnTouch: true }))
41 |
42 | .clickRandomStatic()
43 | .wait(5000) // Remaining: 1000ms
44 |
45 | .get('.Notification')
46 | .trigger('pointerdown', { pointerType: 'touch' })
47 | .wait(2000) // More than the remaining time
48 |
49 | .get('.Notification')
50 | .should('exist')
51 | })
52 | })
53 |
--------------------------------------------------------------------------------
/tests/Notivue/config-teleport.cy.ts:
--------------------------------------------------------------------------------
1 | import type { VueWrapper } from '@vue/test-utils'
2 |
3 | describe('Teleport', () => {
4 | it('By default is teleported to body', () => {
5 | cy.mountNotivue()
6 |
7 | .clickRandomStatic()
8 |
9 | .get('body')
10 | .children()
11 | .should('have.class', 'Root')
12 | })
13 |
14 | it('Can teleport to different element', () => {
15 | cy.mountNotivue({ config: { teleportTo: 'html' } })
16 |
17 | .clickRandomStatic()
18 |
19 | .get('body')
20 | .children()
21 | .should('not.have.class', 'Root')
22 |
23 | .get('html')
24 | .children()
25 | .should('have.class', 'Root')
26 | })
27 |
28 | it('Can teleport to custom HTMLElement', () => {
29 | cy.mountNotivue({
30 | config: { teleportTo: document.getElementById('teleport') as HTMLElement },
31 | })
32 |
33 | .clickRandomStatic()
34 |
35 | .get('body')
36 | .children()
37 | .should('not.have.class', 'Root')
38 |
39 | .get('#teleport')
40 | .children()
41 | .should('have.class', 'Root')
42 | })
43 |
44 | it('Can update teleport config dynamically', () => {
45 | cy.mountNotivue()
46 | .get('@vue')
47 | .then((wrapper) => wrapper.setProps({ teleportTo: 'html' }))
48 |
49 | .clickRandomStatic()
50 |
51 | .get('body')
52 | .children()
53 | .should('not.have.class', 'Root')
54 |
55 | .get('html')
56 | .children()
57 | .should('have.class', 'Root')
58 | })
59 | })
60 |
--------------------------------------------------------------------------------
/tests/Notivue/instance.cy.ts:
--------------------------------------------------------------------------------
1 | import type { VueWrapper } from '@vue/test-utils'
2 |
3 | function testStoppedInstance() {
4 | cy.getNotifications().should('have.length', 0, { timeout: 0 })
5 | cy.get('.QueueCount').should('have.text', '0')
6 | cy.get('.EntriesCount').should('have.text', '0')
7 | }
8 |
9 | it('Instance is stopped correctly', () => {
10 | cy.mountNotivue()
11 | cy.clickAllStatic()
12 |
13 | cy.get('.StopInstance').click()
14 | cy.clickAllStatic()
15 |
16 | testStoppedInstance()
17 | })
18 |
19 | it('No new entries are added to the store if instance is stopped', () => {
20 | cy.mountNotivue({
21 | config: {
22 | limit: 4,
23 | enqueue: true,
24 | },
25 | })
26 |
27 | for (let i = 0; i < 10; i++) {
28 | cy.clickAllStatic()
29 | }
30 |
31 | cy.get('.StopInstance').click()
32 | testStoppedInstance()
33 | })
34 |
35 | it('Plugin can be installed without starting the instance', () => {
36 | cy.mountNotivue({
37 | config: {
38 | startOnCreation: false,
39 | },
40 | })
41 |
42 | cy.clickAllStatic()
43 | testStoppedInstance()
44 | })
45 |
46 | it('Instance can be stopped and started again', () => {
47 | cy.mountNotivue()
48 |
49 | for (let i = 0; i < 10; i++) {
50 | cy.get('.StopInstance').click()
51 | cy.clickAllStatic()
52 | testStoppedInstance()
53 |
54 | cy.get('.StartInstance')
55 | .click()
56 | .clickAllStatic()
57 | .getNotifications()
58 | .should('have.length', 4, { timeout: 0 })
59 | .get('.EntriesCount')
60 | .should('have.text', '4', { timeout: 0 })
61 | }
62 | })
63 |
64 | it('Config is not updated if instance is stopped', () => {
65 | cy.mountNotivue()
66 |
67 | const differentConfig = {
68 | pauseOnHover: false,
69 | pauseOnTouch: false,
70 | pauseOnTabChange: false,
71 | enqueue: true,
72 | position: 'bottom-center',
73 | teleportTo: 'html',
74 | limit: 3,
75 | avoidDuplicates: true,
76 | }
77 |
78 | cy.get('.Config')
79 | .invoke('text')
80 | .then((initialConfig) => {
81 | cy.get('.StopInstance').click()
82 |
83 | cy.get('@vue').then((wrapper) => {
84 | wrapper.setProps(differentConfig)
85 |
86 | cy.get('.Config')
87 | .invoke('text')
88 | .should((updatedConfig) => {
89 | expect(updatedConfig).to.eq(initialConfig)
90 | })
91 | })
92 | })
93 | })
94 |
--------------------------------------------------------------------------------
/tests/Notivue/prefers-reduced-motion.cy.ts:
--------------------------------------------------------------------------------
1 | describe('prefers-reduced-motion', () => {
2 | beforeEach(() => {
3 | cy.stub(window, 'matchMedia').withArgs('(prefers-reduced-motion: reduce)').returns({
4 | matches: true,
5 | })
6 | })
7 |
8 | it('Should not add enter/leave animation classes', () => {
9 | cy.mountNotivue()
10 | .get('.PushAndRenderClear')
11 | .click()
12 | .get('.Notivue__enter')
13 | .should('not.exist')
14 |
15 | .get('.RenderedClear')
16 | .click()
17 | .get('.Notivue__leave')
18 | .should('not.exist')
19 | })
20 |
21 | it('Should not add clearAll animation', () => {
22 | cy.mountNotivue()
23 | .clickRandomStatic()
24 | .get('.ClearAll')
25 | .click()
26 | .get('.Notivue__clearAll')
27 | .should('not.exist')
28 | })
29 |
30 | it('No transition should be applied', () => {
31 | cy.mountNotivue()
32 | .clickRandomStatic()
33 | .click()
34 | .getNotifications()
35 | .should('have.css', 'transition', 'all')
36 | })
37 | })
38 |
--------------------------------------------------------------------------------
/tests/Notivue/push-callbacks.cy.ts:
--------------------------------------------------------------------------------
1 | import { DEFAULT_DURATION } from '@/core/constants'
2 |
3 | describe('Push callbacks', () => {
4 | it('onAutoClear is only called on auto clear', () => {
5 | cy.mountNotivue()
6 | .get('.PushWithAutoClearCallback')
7 | .click()
8 | .wait(DEFAULT_DURATION)
9 |
10 | .get('.PushWithAutoClearCallback')
11 | .should('have.text', '6')
12 |
13 | .get('.PushWithManualClearCallback')
14 | .should('have.text', '0')
15 | })
16 |
17 | it('onManualClear is only called on manual clear', () => {
18 | cy.mountNotivue()
19 | .get('.PushWithManualClearCallback')
20 | .click()
21 |
22 | .get('.PushWithAutoClearCallback')
23 | .should('have.text', '0')
24 |
25 | .get('.PushWithManualClearCallback')
26 | .should('have.text', '6')
27 | })
28 | })
29 |
--------------------------------------------------------------------------------
/tests/Notivue/push-methods.cy.ts:
--------------------------------------------------------------------------------
1 | import { DEFAULT_DURATION } from '@/core/constants'
2 | import { DEFAULT_ANIM_DURATION, RESOLVE_REJECT_DELAY } from '@/support/utils'
3 |
4 | describe('Push', () => {
5 | it('Can push any type of notification', () => {
6 | cy.mountNotivue()
7 | .clickAll()
8 |
9 | .getNotifications()
10 | .should('have.length', 5)
11 | })
12 |
13 | it('Dismisses any static notification', () => {
14 | cy.mountNotivue()
15 | .clickAllStatic()
16 |
17 | .getNotifications()
18 | .should('have.length', 4)
19 |
20 | .wait(DEFAULT_DURATION)
21 |
22 | .getNotifications()
23 | .should('have.length', 0)
24 | })
25 |
26 | it('Updates and dismisses promises', () => {
27 | cy.mountNotivue()
28 |
29 | .get('.PushPromiseAndResolve')
30 | .click()
31 | .wait(RESOLVE_REJECT_DELAY)
32 | .wait(DEFAULT_DURATION)
33 |
34 | .getNotifications()
35 | .should('have.length', 0)
36 |
37 | .get('.PushPromiseAndReject')
38 | .click()
39 | .wait(RESOLVE_REJECT_DELAY)
40 | .wait(DEFAULT_DURATION)
41 |
42 | .getNotifications()
43 | .should('have.length', 0)
44 | })
45 |
46 | it('Clears all notifications', () => {
47 | cy.mountNotivue()
48 | .clickAll()
49 |
50 | .get('.ClearAll')
51 | .click()
52 |
53 | .getNotifications()
54 | .should('have.length', 0)
55 | })
56 |
57 | it('Destroys all notifications', () => {
58 | cy.mountNotivue()
59 | .clickAll()
60 |
61 | .get('.DestroyAll')
62 | .click()
63 |
64 | .getNotifications()
65 | .should('have.length', 0)
66 | })
67 |
68 | it('Clears single notification', () => {
69 | cy.mountNotivue()
70 |
71 | .get('.PushAndClear')
72 | .click()
73 | .wait(RESOLVE_REJECT_DELAY)
74 | .wait(DEFAULT_ANIM_DURATION)
75 |
76 | .getNotifications()
77 | .should('have.length', 0)
78 | })
79 |
80 | it('Destroys single notification', () => {
81 | cy.mountNotivue()
82 |
83 | .get('.PushAndDestroy')
84 | .click()
85 | .wait(RESOLVE_REJECT_DELAY)
86 |
87 | .getNotifications()
88 | .should('have.length', 0)
89 | })
90 | })
91 |
--------------------------------------------------------------------------------
/tests/Notivue/push-specific-options.cy.ts:
--------------------------------------------------------------------------------
1 | describe('Push-specific options', () => {
2 | it('Can push ariaLiveOnly notifications', () => {
3 | cy.mountNotivue()
4 | .get('.PushAriaLiveOnly')
5 | .click()
6 |
7 | .getNotifications()
8 | .should('have.length', 0, { timeout: 0 })
9 |
10 | .get('li > div')
11 | .should('have.length', 1)
12 | .and('have.text', 'Title: Message')
13 | .invoke('attr', 'role')
14 | .should('not.be.undefined')
15 |
16 | .get('li > div')
17 | .invoke('attr', 'aria-live')
18 | .should('not.be.undefined')
19 | })
20 |
21 | it('Can push notifications that skip the queue', () => {
22 | cy.mountNotivue({ config: { enqueue: true, limit: 1 } })
23 |
24 | for (let i = 0; i < 10; i++) {
25 | cy.get('.PushSkipQueue').click()
26 | }
27 |
28 | cy.getNotifications().should('have.length', 10)
29 | })
30 | })
31 |
--------------------------------------------------------------------------------
/tests/Notivue/slot-callbacks.cy.ts:
--------------------------------------------------------------------------------
1 | describe('Clear and destroy callbacks work', () => {
2 | it('Clear', () => {
3 | cy.mountNotivue()
4 |
5 | .clickRandomStatic()
6 | .get('.ClearButton')
7 | .click()
8 |
9 | .getNotifications()
10 | .should('have.length', 0)
11 | })
12 |
13 | it('Destroy', () => {
14 | cy.mountNotivue()
15 |
16 | .clickRandomStatic()
17 | .get('.DestroyButton')
18 | .click()
19 |
20 | .getNotifications()
21 | .should('have.length', 0)
22 | })
23 | })
24 |
--------------------------------------------------------------------------------
/tests/Notivue/slot-custom-push-props.cy.ts:
--------------------------------------------------------------------------------
1 | import { RESOLVE_REJECT_DELAY, randomProps, randomProps2 } from '@/support/utils'
2 |
3 | describe('Custom props match the slot content', () => {
4 | describe('First-level notifications', () => {
5 | const componentConf = {
6 | props: {
7 | options: { props: randomProps },
8 | },
9 | }
10 |
11 | it('Success', () => {
12 | cy.mountNotivue(componentConf)
13 |
14 | .get('.Success')
15 | .click()
16 | .checkSlotPropsAgainst(randomProps)
17 | })
18 |
19 | it('Error', () => {
20 | cy.mountNotivue(componentConf)
21 |
22 | .get('.Error')
23 | .click()
24 | .checkSlotPropsAgainst(randomProps)
25 | })
26 |
27 | it('Warning', () => {
28 | cy.mountNotivue(componentConf)
29 |
30 | .get('.Warning')
31 | .click()
32 | .checkSlotPropsAgainst(randomProps)
33 | })
34 |
35 | it('Info', () => {
36 | cy.mountNotivue(componentConf)
37 |
38 | .get('.Info')
39 | .click()
40 | .checkSlotPropsAgainst(randomProps)
41 | })
42 |
43 | it('Promise', () => {
44 | cy.mountNotivue(componentConf)
45 |
46 | .get('.Promise')
47 | .click()
48 | .checkSlotPropsAgainst(randomProps)
49 | })
50 | })
51 |
52 | describe('Promise - Resolve / Reject', () => {
53 | const componentConf = {
54 | props: {
55 | options: { props: randomProps },
56 | // Pass new options to .resolve() and .reject()
57 | newOptions: { props: randomProps2 },
58 | },
59 | }
60 |
61 | it('Promise - Resolve', () => {
62 | cy.mountNotivue(componentConf)
63 |
64 | .get('.PushPromiseAndResolve')
65 | .click()
66 | .wait(RESOLVE_REJECT_DELAY) // Wait for resolve
67 | .checkSlotPropsAgainst(randomProps2)
68 | })
69 |
70 | it('Promise - Reject', () => {
71 | cy.mountNotivue(componentConf)
72 |
73 | .get('.PushPromiseAndReject')
74 | .click()
75 | .wait(RESOLVE_REJECT_DELAY) // Wait for reject
76 | .checkSlotPropsAgainst(randomProps2)
77 | })
78 | })
79 | })
80 |
--------------------------------------------------------------------------------
/tests/Notivue/slot-default-options.cy.ts:
--------------------------------------------------------------------------------
1 | import { DEFAULT_NOTIFICATION_OPTIONS as DEFAULT_OPTIONS } from '@/core/constants'
2 | import { RESOLVE_REJECT_DELAY } from '@/support/utils'
3 |
4 | describe('Default options match the slot content', () => {
5 | const { success, error, warning, info, promise } = DEFAULT_OPTIONS as Record<
6 | keyof typeof DEFAULT_OPTIONS,
7 | Record
8 | >
9 |
10 | describe('First-level notifications', () => {
11 | it('Success', () => {
12 | cy.mountNotivue()
13 |
14 | .get('.Success')
15 | .click()
16 | .checkSlotAgainst(success)
17 | })
18 |
19 | it('Error', () => {
20 | cy.mountNotivue()
21 |
22 | .get('.Error')
23 | .click()
24 | .checkSlotAgainst(error)
25 | })
26 |
27 | it('Warning', () => {
28 | cy.mountNotivue()
29 |
30 | .get('.Warning')
31 | .click()
32 | .checkSlotAgainst(warning)
33 | })
34 |
35 | it('Info', () => {
36 | cy.mountNotivue()
37 |
38 | .get('.Info')
39 | .click()
40 | .checkSlotAgainst(info)
41 | })
42 |
43 | it('Promise', () => {
44 | cy.mountNotivue()
45 |
46 | .get('.Promise')
47 | .click()
48 | .checkSlotAgainst({ ...promise, duration: null }) // Infinity is not valid
49 | })
50 | })
51 |
52 | describe('Promise - Resolve / Reject', () => {
53 | const promiseResolve = DEFAULT_OPTIONS['promise-resolve'] as Record
54 | const promiseReject = DEFAULT_OPTIONS['promise-reject'] as Record
55 |
56 | it('Promise - Resolve', () => {
57 | cy.mountNotivue()
58 |
59 | .get('.PushPromiseAndResolve')
60 | .click()
61 | .wait(RESOLVE_REJECT_DELAY)
62 | .checkSlotAgainst(promiseResolve)
63 | })
64 |
65 | it('Promise - Reject', () => {
66 | cy.mountNotivue()
67 | .get('.PushPromiseAndReject')
68 | .click()
69 | .wait(RESOLVE_REJECT_DELAY)
70 | .checkSlotAgainst(promiseReject)
71 | })
72 | })
73 | })
74 |
--------------------------------------------------------------------------------
/tests/Notivue/slot-internal-properties.cy.ts:
--------------------------------------------------------------------------------
1 | import { parseText } from '@/support/utils'
2 | import { internalKeys } from '@/core/utils'
3 |
4 | it('Hidden internal properties are never defined', () => {
5 | cy.mountNotivue()
6 |
7 | .clickRandomStatic()
8 | .getNotifications()
9 | .then((el) => expect(parseText(el)).to.not.have.keys(internalKeys))
10 | })
11 |
12 | it('Exposed internal properties are always defined', () => {
13 | cy.mountNotivue()
14 |
15 | .clickRandomStatic()
16 | .getNotifications()
17 | .then((el) => {
18 | expect(parseText(el)).to.include.keys(['id', 'type', 'createdAt'])
19 |
20 | expect(parseText(el).id).to.be.a('string').and.to.have.length.greaterThan(0)
21 | expect(parseText(el).type).to.be.a('string').and.to.have.length.greaterThan(0)
22 | expect(parseText(el).createdAt).to.be.a('number').and.to.be.greaterThan(0)
23 | })
24 | })
25 |
--------------------------------------------------------------------------------
/tests/Notivue/transitions.cy.ts:
--------------------------------------------------------------------------------
1 | describe('Transition styles are injected correctly', () => {
2 | it('Top alignment', () => {
3 | cy.mountNotivue()
4 |
5 | for (let i = 0; i < 20; i++) cy.clickRandomStatic()
6 |
7 | cy.get('li').then((notifications) => {
8 | let accHeights = 0
9 |
10 | notifications.each((_, notification) => {
11 | cy.checkTransitions(notification, accHeights)
12 |
13 | accHeights += notification.clientHeight
14 | })
15 | })
16 | })
17 |
18 | it('Bottom alignment', () => {
19 | cy.mountNotivue({ config: { position: 'bottom-center' } })
20 |
21 | for (let i = 0; i < 20; i++) cy.clickRandomStatic()
22 |
23 | cy.get('li').then((notifications) => {
24 | let accHeights = 0
25 |
26 | notifications.each((_, notification) => {
27 | cy.checkTransitions(notification, accHeights)
28 |
29 | accHeights += notification.clientHeight * -1
30 | })
31 | })
32 | })
33 | })
34 |
--------------------------------------------------------------------------------
/tests/NotivueKeyboard/actions.cy.ts:
--------------------------------------------------------------------------------
1 | describe('Actions', () => {
2 | it('Should focus new candidate if pushing while focusing', () => {
3 | cy.mountKeyboard()
4 | .pushCandidate()
5 | .pushCandidate()
6 | .pushCandidate()
7 |
8 | .realPress('Tab')
9 | .realPress('Tab')
10 | .realPress('Tab') // Go last action of first container
11 |
12 | .pushCandidateSilently() // id: 3
13 |
14 | .focused()
15 | .should('have.data', 'notivueContainer', 3)
16 | })
17 |
18 | it('Should focus next container if clicking an action', () => {
19 | cy.mountKeyboard()
20 | .pushCandidate()
21 | .pushCandidate()
22 | .pushCandidate()
23 |
24 | .realPress('Tab')
25 | .realPress('Tab')
26 | .realPress('Tab')
27 |
28 | .realPress('Tab')
29 | .realPress('Tab') // Go to action of 2nd container (id: 1)
30 |
31 | .realPress(Math.random() > 0.5 ? 'Space' : 'Enter')
32 |
33 | .focused()
34 | .should('have.data', 'notivueContainer', 0)
35 | })
36 |
37 | it('Should leave stream if pressing Space or Enter on action in last container', () => {
38 | cy.mountKeyboard()
39 |
40 | .pushCandidate()
41 | .pushCandidate()
42 | .as('relatedTarget')
43 |
44 | .realPress('Tab')
45 | .realPress('Tab')
46 | .realPress('Tab')
47 |
48 | .realPress('Tab')
49 | .realPress('Tab') // Go to action of last container
50 |
51 | .realPress(Math.random() > 0.5 ? 'Space' : 'Enter')
52 |
53 | cy.get('@relatedTarget').should('be.focused').checkLeaveAnnouncement()
54 | })
55 |
56 | it('Should leave stream if clicking with mouse an action in any container', () => {
57 | cy.mountKeyboard()
58 |
59 | .pushCandidate()
60 | .pushCandidate()
61 | .pushCandidate()
62 | .as('relatedTarget')
63 |
64 | .realPress('Tab')
65 | .realPress('Tab')
66 | .realPress('Tab')
67 |
68 | .realPress('Tab')
69 | .realPress('Tab') // Go to action of 2nd container
70 |
71 | .focused()
72 | .realClick()
73 |
74 | cy.get('@relatedTarget').should('be.focused').checkLeaveAnnouncement()
75 | })
76 | })
77 |
--------------------------------------------------------------------------------
/tests/NotivueKeyboard/components/Candidate.vue:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 |
21 |
22 |
{{ props.item.title }}
23 |
{{ props.item.message }}
24 |
25 |
26 |
44 |
45 |
46 |
47 |
58 |
--------------------------------------------------------------------------------
/tests/NotivueKeyboard/components/Notivue.vue:
--------------------------------------------------------------------------------
1 |
57 |
58 |
59 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
92 |
--------------------------------------------------------------------------------
/tests/NotivueKeyboard/components/Unqualified.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
12 |
{{ props.item.title }}
13 |
{{ props.item.message }}
14 |
15 |
16 |
21 |
22 |
23 |
24 |
35 |
--------------------------------------------------------------------------------
/tests/NotivueKeyboard/entering-stream.cy.ts:
--------------------------------------------------------------------------------
1 | describe('Entering the stream', () => {
2 | it('Should not enter with CTRL+N if no candidates are available', () => {
3 | cy.mountKeyboard()
4 |
5 | for (let i = 0; i < 5; i++) {
6 | cy.pushUnqualified().as('relatedTarget')
7 | }
8 |
9 | cy.realPress(['ControlLeft', 'n'])
10 |
11 | cy.get('@relatedTarget')
12 | .should('be.focused')
13 | .get('.Notification')
14 | .first()
15 | .should('contain.text', 'No notifications to navigate')
16 | })
17 |
18 | it('If a new candidate is pushed, it should be focused if tab is pressed', () => {
19 | cy.mountKeyboard().pushCandidate()
20 |
21 | cy.realPress('Tab')
22 |
23 | .focused()
24 | .should('have.data', 'notivueContainer')
25 | })
26 |
27 | it('If an unqualified notification is pushed, it should not be focused if tab is pressed', () => {
28 | cy.mountKeyboard()
29 | .pushUnqualified()
30 |
31 | .realPress('Tab')
32 |
33 | .get('.Notification')
34 | .first()
35 | .should('not.be.focused')
36 | })
37 |
38 | it('If multiple new candidates are pushed, the most recent should be focused if tab is pressed', () => {
39 | cy.mountKeyboard()
40 |
41 | const limit = 10
42 |
43 | for (let i = 0; i < limit; i++) {
44 | cy.pushCandidate()
45 | }
46 |
47 | cy.window()
48 | .focus()
49 | .realPress('Tab')
50 |
51 | .focused()
52 | .should('have.data', 'notivueContainer', limit - 1)
53 | })
54 |
55 | it('If never navigated and unqualified are pushed after candidates, the first candidate should be focused once tab is pressed', () => {
56 | cy.mountKeyboard()
57 |
58 | .pushCandidate()
59 | .pushCandidate()
60 | .pushUnqualified()
61 |
62 | .realPress('Tab')
63 |
64 | .focused()
65 | .should('have.data', 'notivueContainer', 1) // Ids starts from 0
66 | })
67 |
68 | it('Should enter with CTRL+N', () => {
69 | cy.mountKeyboard()
70 |
71 | .pushCandidate()
72 |
73 | .realPress(['ControlLeft', 'n'])
74 |
75 | .focused()
76 | .should('have.data', 'notivueContainer', 0)
77 | })
78 |
79 | it('Should not enter with Tab if already navigated and no new candidates are available', () => {
80 | cy.mountKeyboard()
81 |
82 | for (let i = 0; i < 3; i++) {
83 | cy.pushCandidate().as('relatedTarget')
84 | }
85 |
86 | const allFocusable = 2 * 3 + 3
87 |
88 | for (let i = 0; i < allFocusable; i++) {
89 | cy.realPress('Tab')
90 | }
91 |
92 | cy.realPress('Tab') // Leave the stream
93 | .realPress('Tab') // Try to enter again
94 |
95 | .get('@relatedTarget')
96 | .should('be.focused')
97 | })
98 | })
99 |
--------------------------------------------------------------------------------
/tests/NotivueKeyboard/leaving-stream.cy.ts:
--------------------------------------------------------------------------------
1 | describe('Leaving the stream', () => {
2 | it('Should leave with CTRL+N', () => {
3 | cy.mountKeyboard()
4 | .pushCandidate()
5 | .as('relatedTarget')
6 |
7 | .realPress('Tab')
8 |
9 | .realPress(['ControlLeft', 'n'])
10 |
11 | .get('@relatedTarget')
12 | .should('be.focused')
13 | .checkLeaveAnnouncement()
14 | })
15 |
16 | it('Should leave if pressing SHIFT+TAB on first candidate', () => {
17 | cy.mountKeyboard()
18 | .pushCandidate()
19 | .as('relatedTarget')
20 |
21 | .realPress('Tab')
22 |
23 | .realPress(['Shift', 'Tab'])
24 |
25 | .get('@relatedTarget')
26 | .should('be.focused')
27 | .checkLeaveAnnouncement()
28 | })
29 |
30 | it('Should navigate any element and leave if pressing TAB on last candidate', () => {
31 | cy.mountKeyboard()
32 |
33 | for (let i = 0; i < 3; i++) {
34 | cy.pushCandidate().as('relatedTarget')
35 | }
36 |
37 | const allFocusable = 2 * 3 + 3
38 |
39 | for (let i = 0; i < allFocusable; i++) {
40 | cy.realPress('Tab') // Go to last focusable element
41 | }
42 |
43 | cy.realPress('Tab') // Leave the stream
44 |
45 | .get('@relatedTarget')
46 | .should('be.focused')
47 | .checkLeaveAnnouncement()
48 | })
49 |
50 | it('Should leave with Escape', () => {
51 | cy.mountKeyboard()
52 | .pushCandidate()
53 | .as('relatedTarget')
54 |
55 | .realPress('Tab')
56 |
57 | .realPress('Escape')
58 |
59 | .get('@relatedTarget')
60 | .should('be.focused')
61 | .checkLeaveAnnouncement()
62 | })
63 |
64 | it('Should leave if clicking outside the stream', () => {
65 | cy.mountKeyboard()
66 | .pushCandidate()
67 | .as('relatedTarget')
68 |
69 | .realPress('Tab')
70 |
71 | .get('body')
72 | .realClick({ position: 'bottomRight' })
73 |
74 | .get('@relatedTarget')
75 | .should('be.focused')
76 | .checkLeaveAnnouncement()
77 | })
78 | })
79 |
--------------------------------------------------------------------------------
/tests/NotivueKeyboard/props.cy.ts:
--------------------------------------------------------------------------------
1 | import { DEFAULT_DURATION } from '@/core/constants'
2 |
3 | describe('Props', () => {
4 | it('Should apply custom leave messages', () => {
5 | cy.mountKeyboard({
6 | leaveMessage: 'Leaving!',
7 | emptyMessage: "There's nothing here!",
8 | renderAnnouncement: true,
9 | })
10 | .pushCandidate()
11 |
12 | .realPress('Tab')
13 | .realPress('Escape')
14 |
15 | .get('.Notification')
16 | .first()
17 | .should('contain.text', 'Leaving!')
18 |
19 | .wait(DEFAULT_DURATION)
20 |
21 | .realPress(['ControlLeft', 'N'])
22 |
23 | .get('.Notification')
24 | .first()
25 | .should('contain.text', "There's nothing here!")
26 | })
27 |
28 | it('Should enter/exit with a custom combo key', () => {
29 | cy.mountKeyboard({
30 | comboKey: 'u',
31 | })
32 | .pushCandidate()
33 | .as('relatedTarget')
34 |
35 | .realPress(['ControlLeft', 'u'])
36 |
37 | .focused()
38 | .should('have.data', 'notivueContainer', 0)
39 |
40 | .realPress(['ControlLeft', 'u'])
41 |
42 | .get('@relatedTarget')
43 | .should('be.focused')
44 | })
45 |
46 | it('Should not focus next element if `handleClicks` is false', () => {
47 | cy.mountKeyboard({ handleClicks: false })
48 | .pushCandidate()
49 | .pushCandidate()
50 |
51 | .realPress('Tab')
52 | .realPress('Tab')
53 | .realPress('Tab')
54 |
55 | .realPress(Math.random() > 0.5 ? 'Space' : 'Enter')
56 |
57 | .get('.Candidate')
58 | .should('not.be.focused')
59 | })
60 |
61 | it('Should not render notification if `renderAnnouncement` is false', () => {
62 | cy.mountKeyboard({ renderAnnouncement: false })
63 | .pushCandidate()
64 |
65 | .realPress('Tab')
66 | .realPress('Escape')
67 |
68 | .get('.Notification')
69 | .should('have.length', 0)
70 | })
71 |
72 | it('Should customize max number of leave announcements', () => {
73 | cy.mountKeyboard({ maxAnnouncements: 1 })
74 | .pushCandidate()
75 | .pushCandidate()
76 |
77 | .realPress('Tab')
78 | .realPress('Escape')
79 |
80 | .realPress(['ControlLeft', 'N'])
81 | .realPress('Tab')
82 | .realPress('Escape')
83 |
84 | .get('.Notification')
85 | .should('contain.text', "You're leaving the notifications stream")
86 | .should('have.length', 1)
87 | })
88 | })
89 |
--------------------------------------------------------------------------------
/tests/NotivueKeyboard/queue.cy.ts:
--------------------------------------------------------------------------------
1 | describe('Queue', () => {
2 | it('Should focus new candidate from the queue once dismissed', () => {
3 | cy.mountKeyboard({ enqueue: true, limit: 1 })
4 |
5 | .pushCandidate()
6 | .pushCandidate()
7 |
8 | .realPress('Tab')
9 | .realPress('Tab')
10 | .realPress('Space') // Dismiss first candidate (id: 0)
11 |
12 | .focused()
13 | .and('have.data', 'notivueContainer', 1)
14 | })
15 |
16 | it('Should focus first candidate available if unqualified is pushed from the queue', () => {
17 | cy.mountKeyboard({ enqueue: true, limit: 2 })
18 |
19 | .pushCandidate()
20 | .pushCandidate()
21 | .pushUnqualified()
22 |
23 | .realPress('Tab')
24 | .realPress('Tab')
25 | .realPress('Space') // Dismiss last candidate (id: 1)
26 |
27 | .focused()
28 | .and('have.data', 'notivueContainer', 0)
29 | })
30 |
31 | it('Should leave the stream if unqualified is pushed and no candidates are available', () => {
32 | cy.mountKeyboard({ enqueue: true, limit: 1 })
33 |
34 | .pushCandidate()
35 | .pushUnqualified()
36 | .as('relatedTarget')
37 |
38 | .realPress('Tab')
39 | .realPress('Tab')
40 | .realPress('Space')
41 |
42 | cy.get('@relatedTarget').should('be.focused') // We do not announce exit when last candidate is dismissed
43 | })
44 | })
45 |
--------------------------------------------------------------------------------
/tests/NotivueSwipe/clear.cy.ts:
--------------------------------------------------------------------------------
1 | import {
2 | SWIPE_NOTIFICATION_WIDTH as WIDTH,
3 | DEFAULT_ANIM_DURATION as ANIM_DUR,
4 | } from '@/support/utils'
5 |
6 | describe('Default behavior', () => {
7 | it('Should clear notification when default threshold is met', () => {
8 | cy.mountSwipe()
9 | .pushSwipeSuccess()
10 |
11 | .get('.SwipeNotification')
12 | .realSwipe('toLeft', { length: WIDTH / 2 })
13 |
14 | .get('.SwipeNotification')
15 | .should('not.exist')
16 | })
17 |
18 | it('Should clear notification by swiping left', () => {
19 | cy.mountSwipe().pushSwipeSuccess()
20 |
21 | cy.get('.SwipeNotification')
22 | .realSwipe('toLeft', { length: WIDTH / 2 })
23 |
24 | .get('.SwipeNotification')
25 | .should('not.exist')
26 | })
27 |
28 | it('Should clear notification by swiping right', () => {
29 | cy.mountSwipe().pushSwipeSuccess()
30 |
31 | cy.get('.SwipeNotification')
32 | .realSwipe('toRight', { length: WIDTH / 2 })
33 |
34 | .get('.SwipeNotification')
35 | .should('not.exist')
36 | })
37 |
38 | it('Should not clear if threshold is not met', () => {
39 | cy.mountSwipe()
40 | .pushSwipeSuccess()
41 |
42 | .get('.SwipeNotification')
43 | .realSwipe('toRight', { length: WIDTH * 0.3 })
44 |
45 | .get('.SwipeNotification')
46 | .should('exist')
47 | })
48 |
49 | it('Should not swipe if it is a promise', () => {
50 | cy.mountSwipe()
51 | .get('.Promise')
52 | .click()
53 | .wait(ANIM_DUR)
54 |
55 | .get('.SwipeNotification')
56 | .realSwipe('toRight', { length: WIDTH * 3 })
57 |
58 | .get('.SwipeNotification')
59 | .should('exist')
60 | })
61 |
62 | describe('Should not clear notification if swiping a button or a link', () => {
63 | it('Button', () => {
64 | cy.mountSwipe()
65 | .pushSwipeSuccess()
66 |
67 | .get('.CloseButton')
68 | .realSwipe('toRight', { length: WIDTH })
69 |
70 | .get('.SwipeNotification')
71 | .should('exist')
72 | })
73 |
74 | it('Link', () => {
75 | cy.mountSwipe()
76 | .pushSwipeSuccess()
77 |
78 | .get('.Link')
79 | .realSwipe('toRight', { length: WIDTH })
80 |
81 | .get('.SwipeNotification')
82 | .should('exist')
83 | })
84 | })
85 | })
86 |
--------------------------------------------------------------------------------
/tests/NotivueSwipe/components/Notivue.vue:
--------------------------------------------------------------------------------
1 |
24 |
25 |
26 |
27 |
34 |
40 |
{{ item.title }}
41 |
{{ item.message }}
42 |
43 |
Link
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
65 |
--------------------------------------------------------------------------------
/tests/NotivueSwipe/debounce.cy.ts:
--------------------------------------------------------------------------------
1 | import { DEFAULT_ANIM_DURATION as LEAVE_ANIM_DUR, getRandomInt } from '@/support/utils'
2 |
3 | import { DEBOUNCE } from '@/NotivueSwipe/constants'
4 | import { DEFAULT_DURATION } from '@/core/constants'
5 | import { SWIPE_NOTIFICATION_WIDTH as WIDTH } from '@/support/utils'
6 |
7 | const LENGTH = 5
8 |
9 | const pointerEventOptions = {
10 | force: true,
11 | eventConstructor: 'PointerEvent',
12 | pointerType: 'touch',
13 | }
14 |
15 | describe('Debounce', () => {
16 | beforeEach(() => {
17 | cy.viewport('iphone-x')
18 | })
19 |
20 | /**
21 | * In order for those tests to be accurate, we need a way to track how much time Cypress
22 | * takes to swipe the element and to trigger pointer events.
23 | *
24 | * We can use Cypress retry-ability mixed with cy.then to track it.
25 | * In the first test, this matches the moment the element is removed from the DOM.
26 | *
27 | * We also add the leave animation duration to the wait time, to be even more accurate.
28 | */
29 |
30 | it('Resume is delayed after clearing', () => {
31 | cy.mountSwipe()
32 |
33 | let elapsed = Date.now()
34 |
35 | for (let i = 0; i < LENGTH; i++) {
36 | cy.get('.Success').click()
37 | }
38 |
39 | cy.get('.SwipeNotification').eq(3).realSwipe('toLeft', { length: WIDTH })
40 |
41 | cy.get('.SwipeNotification')
42 | .should('have.length', LENGTH - 1)
43 | .then(() => {
44 | elapsed = Date.now() - elapsed
45 | console.log('Elapsed: ', elapsed)
46 |
47 | cy.wait(DEFAULT_DURATION - elapsed + DEBOUNCE.Touch + LEAVE_ANIM_DUR)
48 | cy.get('.SwipeNotification').should('have.length', LENGTH - 1)
49 | })
50 | })
51 |
52 | it('Resume is delayed after tapping an excluded element', () => {
53 | const child = getRandomInt(0, LENGTH - 1)
54 |
55 | cy.mountSwipe()
56 |
57 | let elapsed = Date.now()
58 |
59 | for (let i = 0; i < LENGTH; i++) {
60 | cy.get('.Success').click()
61 | }
62 |
63 | cy.get('.CloseButton')
64 | .eq(child)
65 | .trigger('pointerdown', pointerEventOptions)
66 | .then(() => {
67 | elapsed = Date.now() - elapsed
68 | console.log('Elapsed: ', elapsed)
69 |
70 | cy.wait(DEFAULT_DURATION - elapsed + DEBOUNCE.TouchExternal + LEAVE_ANIM_DUR)
71 | cy.get('.SwipeNotification').should('have.length', LENGTH - 1)
72 | })
73 | })
74 |
75 | it('Resume is delayed after tappping a notification', () => {
76 | const child = getRandomInt(0, LENGTH - 1)
77 |
78 | cy.mountSwipe()
79 |
80 | let elapsed = Date.now()
81 |
82 | for (let i = 0; i < LENGTH; i++) {
83 | cy.get('.Success').click()
84 | }
85 |
86 | cy.get('.SwipeNotification')
87 | .eq(child)
88 | .trigger('pointerdown', pointerEventOptions)
89 | .then(() => {
90 | cy.get('.SwipeNotification')
91 | .eq(child)
92 | .trigger('pointerup', pointerEventOptions)
93 | .then(() => {
94 | elapsed = Date.now() - elapsed
95 | console.log('Elapsed: ', elapsed)
96 |
97 | cy.wait(DEFAULT_DURATION - elapsed + DEBOUNCE.Touch + LEAVE_ANIM_DUR)
98 | cy.get('.SwipeNotification').should('have.length', LENGTH)
99 | })
100 | })
101 | })
102 | })
103 |
--------------------------------------------------------------------------------
/tests/NotivueSwipe/props.cy.ts:
--------------------------------------------------------------------------------
1 | import { SWIPE_NOTIFICATION_WIDTH as WIDTH } from '@/support/utils'
2 |
3 | /**
4 | * Don't know why this test hangs the whole github action
5 | * even if it works and succeeds locally.
6 | *
7 | * Skipping it for now.
8 | *
9 | */
10 | if (!Cypress.env('CYPRESS_CI')) {
11 | describe('Props', () => {
12 | it('Should not swipe if disabled', () => {
13 | cy.mountSwipe({ disabled: true })
14 | .pushSwipeSuccess()
15 |
16 | .get('.SwipeNotification')
17 | .realSwipe('toRight', { length: WIDTH })
18 | .should('exist')
19 | })
20 |
21 | it('Should clear with a lower threshold', () => {
22 | cy.mountSwipe({ threshold: 0.3 })
23 | .pushSwipeSuccess()
24 |
25 | .get('.SwipeNotification')
26 | .realSwipe('toRight', { length: WIDTH * 0.3 })
27 |
28 | .get('.SwipeNotification')
29 | .should('not.exist')
30 | })
31 |
32 | it('Should swipe if swiping excluded element', () => {
33 | cy.mountSwipe({ exclude: '.CloseButton' })
34 | .pushSwipeSuccess()
35 |
36 | .get('.CloseButton')
37 | .realSwipe('toRight', { length: WIDTH })
38 |
39 | .get('.SwipeNotification')
40 | .should('exist')
41 | })
42 | })
43 | }
44 |
--------------------------------------------------------------------------------
/tests/NotivueSwipe/styles.cy.ts:
--------------------------------------------------------------------------------
1 | import {
2 | DEFAULT_ANIM_DURATION as ANIM_DUR,
3 | SWIPE_NOTIFICATION_WIDTH as WIDTH,
4 | } from '@/support/utils'
5 |
6 | describe('Styles', () => {
7 | it('Should have no transforms applied once returned to initial position', () => {
8 | cy.mountSwipe()
9 | .pushSwipeSuccess()
10 |
11 | .get('.SwipeNotification')
12 | .realSwipe('toRight', { length: WIDTH * 0.2 })
13 |
14 | .get('.SwipeNotification')
15 | .invoke('css', 'transform')
16 | .should('be.eq', 'none')
17 | })
18 |
19 | it('Should force `user-select: none;` if swipeable', () => {
20 | cy.mountSwipe()
21 | .pushSwipeSuccess()
22 |
23 | .get('.SwipeNotification')
24 | .children()
25 | .should('have.css', 'user-select', 'none')
26 | })
27 |
28 | it('Should not force `user-select: none;` if not swipeable', () => {
29 | cy.mountSwipe()
30 | .get('.Promise')
31 | .click()
32 | .wait(ANIM_DUR)
33 |
34 | .get('.SwipeNotification')
35 | .should('have.css', 'user-select', 'auto')
36 | })
37 | })
38 |
--------------------------------------------------------------------------------
/tests/NotivueSwipe/timeouts.cy.ts:
--------------------------------------------------------------------------------
1 | import {
2 | SWIPE_NOTIFICATION_WIDTH as WIDTH,
3 | DEFAULT_ANIM_DURATION as ANIM_DUR,
4 | getRandomInt,
5 | } from '@/support/utils'
6 |
7 | import { DEFAULT_DURATION } from '@/core/constants'
8 |
9 | const REPEAT = 5
10 |
11 | describe('Leave timeouts', () => {
12 | it('Should resume timeouts once cleared a notification', () => {
13 | const child = getRandomInt(0, REPEAT - 1)
14 |
15 | cy.mountSwipe()
16 |
17 | for (let i = 0; i < REPEAT; i++) {
18 | cy.get('.Success').click()
19 | }
20 |
21 | cy.wait(ANIM_DUR)
22 |
23 | cy.get('.SwipeNotification').eq(child).realSwipe('toRight', { length: WIDTH })
24 |
25 | cy.wait(DEFAULT_DURATION)
26 |
27 | cy.get('.SwipeNotification').should('have.length', 0)
28 | })
29 |
30 | it('Should not resume timeouts if tapping back on a notification after clearing', () => {
31 | const child = getRandomInt(0, REPEAT - 1)
32 |
33 | cy.mountSwipe()
34 |
35 | for (let i = 0; i < REPEAT; i++) {
36 | cy.get('.Success').click()
37 | }
38 |
39 | cy.wait(ANIM_DUR)
40 |
41 | cy.get('.SwipeNotification')
42 | .eq(child)
43 | .realSwipe('toRight', { length: WIDTH / 2 })
44 |
45 | cy.get('.SwipeNotification')
46 | .eq(0)
47 | .trigger('pointerdown', {
48 | force: true,
49 | eventConstructor: 'PointerEvent',
50 | pointerType: 'touch',
51 | })
52 | .wait(DEFAULT_DURATION * 2) // Hold for very long time
53 |
54 | cy.get('.SwipeNotification').should('have.length', REPEAT - 1)
55 | })
56 | })
57 |
--------------------------------------------------------------------------------
/tests/cypress.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'cypress'
2 | import { alias } from './shared-config'
3 |
4 | import vue from '@vitejs/plugin-vue'
5 |
6 | export default defineConfig({
7 | video: false,
8 | viewportWidth: 1280,
9 | viewportHeight: 720,
10 | experimentalMemoryManagement: true,
11 |
12 | component: {
13 | devServer: {
14 | framework: 'vue',
15 | bundler: 'vite',
16 | viteConfig: {
17 | server: {
18 | port: 5176,
19 | },
20 | resolve: {
21 | alias,
22 | },
23 | plugins: [vue()],
24 | },
25 | },
26 | setupNodeEvents(on) {
27 | on('task', {
28 | log(message) {
29 | console.log(message)
30 | return null
31 | },
32 | })
33 | },
34 | },
35 | })
36 |
--------------------------------------------------------------------------------
/tests/cypress/support/commands-keyboard.ts:
--------------------------------------------------------------------------------
1 | import { mount } from 'cypress/vue'
2 |
3 | import Notivue, { CyNotivueKeyboardProps } from '@/tests/NotivueKeyboard/components/Notivue.vue'
4 |
5 | import { DEFAULT_ANIM_DURATION as ANIM_DUR } from '@/support/utils'
6 | import { createNotivue } from 'notivue'
7 |
8 | declare global {
9 | namespace Cypress {
10 | interface Chainable {
11 | mountKeyboard(props?: CyNotivueKeyboardProps): Chainable
12 | pushCandidate(): Chainable
13 | pushUnqualified(): Chainable
14 | pushCandidateSilently(): Chainable
15 | checkLeaveAnnouncement(): Chainable
16 | }
17 | }
18 | }
19 |
20 | Cypress.Commands.add('mountKeyboard', (props = {} as CyNotivueKeyboardProps) => {
21 | const notivue = createNotivue()
22 |
23 | return mount(Notivue, {
24 | global: {
25 | plugins: [notivue],
26 | },
27 | props,
28 | })
29 | })
30 |
31 | Cypress.Commands.add('pushCandidate', () => cy.get('.PushCandidate').click().wait(ANIM_DUR))
32 |
33 | Cypress.Commands.add('pushUnqualified', () => cy.get('.PushUnqualified').click().wait(ANIM_DUR))
34 |
35 | Cypress.Commands.add('pushCandidateSilently', () => cy.get('body').type('{shift}c'))
36 |
37 | Cypress.Commands.add('checkLeaveAnnouncement', () =>
38 | cy.get('.Notification').first().should('contain.text', "You're leaving the notifications stream")
39 | )
40 |
--------------------------------------------------------------------------------
/tests/cypress/support/commands-notifications.ts:
--------------------------------------------------------------------------------
1 | import { mount } from 'cypress/vue'
2 |
3 | import { Classes } from '@/Notifications/constants'
4 |
5 | import Notivue, { CyNotificationsProps } from '@/tests/Notifications/components/Notivue.vue'
6 |
7 | import { createNotivue, type NotivueTheme, type NotivueConfig } from 'notivue'
8 |
9 | declare global {
10 | namespace Cypress {
11 | interface Chainable {
12 | mountNotifications(props?: CyNotificationsProps, config?: NotivueConfig): Chainable
13 | checkTheme(theme: NotivueTheme): Chainable
14 | mountAndCheckTheme(theme: NotivueTheme): Chainable
15 | }
16 | }
17 | }
18 |
19 | Cypress.Commands.add('checkTheme', (theme: NotivueTheme) => {
20 | Object.entries(theme).forEach(([key, value]) => {
21 | cy.get(`.${Classes.NOTIFICATION}`)
22 | .should('have.attr', 'style')
23 | .and('contain', `${key}: ${value}`)
24 | })
25 | })
26 |
27 | Cypress.Commands.add('mountNotifications', (props = {}, config = {}) => {
28 | const notivue = createNotivue(config)
29 |
30 | return mount(Notivue, {
31 | global: {
32 | plugins: [notivue],
33 | },
34 | props,
35 | })
36 | })
37 |
38 | Cypress.Commands.add('mountAndCheckTheme', (theme: NotivueTheme) => {
39 | cy.mountNotifications({
40 | theme: theme,
41 | })
42 |
43 | .get('.Success')
44 | .click()
45 | .checkTheme(theme)
46 | })
47 |
--------------------------------------------------------------------------------
/tests/cypress/support/commands-swipe.ts:
--------------------------------------------------------------------------------
1 | import { mount } from 'cypress/vue'
2 |
3 | import Notivue, { type CyNotivueSwipeProps } from '@/tests/NotivueSwipe/components/Notivue.vue'
4 |
5 | import { DEFAULT_ANIM_DURATION as ANIM_DUR } from '@/support/utils'
6 | import { createNotivue } from 'notivue'
7 |
8 | declare global {
9 | namespace Cypress {
10 | interface Chainable {
11 | mountSwipe(props?: CyNotivueSwipeProps): Chainable
12 | pushSwipeSuccess(): Chainable
13 | }
14 | }
15 | }
16 |
17 | Cypress.Commands.add('mountSwipe', (props = {}) => {
18 | const notivue = createNotivue()
19 |
20 | return mount(Notivue, {
21 | global: {
22 | plugins: [notivue],
23 | },
24 | props,
25 | })
26 | })
27 |
28 | Cypress.Commands.add('pushSwipeSuccess', () => cy.get('.Success').click().wait(ANIM_DUR))
29 |
--------------------------------------------------------------------------------
/tests/cypress/support/component-index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Notivue Component Testing
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/tests/cypress/support/component.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | import 'cypress-axe'
4 | import 'cypress-real-events'
5 |
6 | import './commands-notivue'
7 | import './commands-notifications'
8 | import './commands-swipe'
9 | import './commands-keyboard'
10 |
11 | import 'notivue/animations.css'
12 | import 'notivue/notifications.css'
13 |
14 | import './styles.css'
15 |
--------------------------------------------------------------------------------
/tests/cypress/support/styles.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --easing: cubic-bezier(0.22, 1, 0.36, 1);
3 | }
4 |
5 | .fade-in {
6 | animation: fade-kf 300ms var(--easing);
7 | }
8 |
9 | .fade-out {
10 | animation: fade-kf 300ms var(--easing);
11 | }
12 |
13 | .fade-all {
14 | animation: fade-kf 600ms var(--easing) forwards;
15 | }
16 |
17 | @keyframes fade-kf {
18 | 0% {
19 | opacity: 1;
20 | }
21 | 100% {
22 | opacity: 0;
23 | }
24 | }
25 |
26 | .fade-in-test-prop {
27 | animation: fade-kf 800ms ease-in-out;
28 | }
29 |
30 | .fade-in-test-prop {
31 | animation: fade-kf 800ms ease-in-out;
32 | }
33 |
--------------------------------------------------------------------------------
/tests/cypress/support/utils.ts:
--------------------------------------------------------------------------------
1 | import type { PushOptions } from 'notivue'
2 |
3 | export function parseText(subject: any) {
4 | return JSON.parse(subject.text()) as Record
5 | }
6 |
7 | export const RESOLVE_REJECT_DELAY = 2000
8 |
9 | /**
10 | * Delay called before clearing, destroying or updating
11 | * some Notivue tests notifications.
12 | *
13 | * Needed just to see the change in the UI
14 | */
15 |
16 | export const SWIPE_NOTIFICATION_WIDTH = 300
17 |
18 | export const DEFAULT_ANIM_DURATION = 350
19 |
20 | export function getRandomInt(min: number, max: number) {
21 | return Math.floor(Math.random() * (max - min + 1)) + min
22 | }
23 |
24 | function randomId() {
25 | return Math.random().toString(36).substr(2, 9)
26 | }
27 |
28 | export function getRandomOptions() {
29 | return {
30 | title: randomId(),
31 | message: randomId(),
32 | duration: Math.floor(Math.random() * 10000),
33 | ariaLive: randomId(),
34 | ariaRole: randomId(),
35 | } as PushOptions
36 | }
37 |
38 | export const randomProps = {
39 | name: 'John',
40 | age: 30,
41 | married: true,
42 | address: {
43 | street: '123 Main St',
44 | city: 'New York',
45 | country: 'USA',
46 | },
47 | job: 'Software Engineer',
48 | salary: 5000,
49 | skills: {
50 | frontend: 'JavaScript',
51 | backend: 'Node.js',
52 | database: 'MongoDB',
53 | },
54 | }
55 |
56 | export const randomProps2 = {
57 | projects: 8,
58 | education: "Bachelor's Degree",
59 | experience: {
60 | years: 7,
61 | previousCompany: 'ABC Inc',
62 | position: 'Senior Developer',
63 | },
64 | languages: ['English', 'Spanish'],
65 | hobbies: {
66 | hobby1: 'Reading',
67 | hobby2: 'Cooking',
68 | hobby3: 'Playing Guitar',
69 | },
70 | favoriteNumber: 13,
71 | favoriteColor: 'Blue',
72 | hasPets: true,
73 | petNames: ['Max', 'Bella'],
74 | friends: {
75 | friend1: 'Sarah',
76 | friend2: 'Michael',
77 | friend3: 'Emily',
78 | },
79 | isStudent: false,
80 | school: 'University of XYZ',
81 | }
82 |
--------------------------------------------------------------------------------
/tests/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "notivue-tests",
3 | "private": true,
4 | "scripts": {
5 | "test": "cypress run --component --browser chrome",
6 | "test:gui": "cypress open --component --browser chrome",
7 | "test:unit": "npx vitest run"
8 | },
9 | "dependencies": {
10 | "notivue": "workspace:*"
11 | },
12 | "devDependencies": {
13 | "@types/node": "^22.5.1",
14 | "@vitejs/plugin-vue": "^5.1.3",
15 | "@vue/test-utils": "^2.4.6",
16 | "axe-core": "^4.10.0",
17 | "cypress": "^13.14.1",
18 | "cypress-axe": "^1.5.0",
19 | "cypress-real-events": "^1.13.0",
20 | "typescript": "^5.5.4",
21 | "vite": "^5.4.2",
22 | "vitest": "^2.0.5",
23 | "vue": "^3.4.38",
24 | "vue-tsc": "^2.1.4"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/tests/shared-config.ts:
--------------------------------------------------------------------------------
1 | import { resolve } from 'path'
2 |
3 | export const alias = {
4 | '@/support': resolve(__dirname, './cypress/support'),
5 | '@/tests': resolve(__dirname, './'),
6 | '@/core': resolve(__dirname, '../packages/notivue/core'),
7 | '@/Notivue': resolve(__dirname, '../packages/notivue/Notivue'),
8 | '@/NotivueSwipe': resolve(__dirname, '../packages/notivue/NotivueSwipe'),
9 | '@/Notifications': resolve(__dirname, '../packages/notivue/Notifications'),
10 | }
11 |
--------------------------------------------------------------------------------
/tests/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2018",
4 | "module": "ESNext",
5 | "moduleResolution": "Node",
6 | "strict": true,
7 | "resolveJsonModule": true,
8 | "isolatedModules": true,
9 | "esModuleInterop": true,
10 | "lib": ["ESNext", "DOM"],
11 | "checkJs": true,
12 | "skipLibCheck": true,
13 | "noEmit": true,
14 | "baseUrl": ".",
15 | "paths": {
16 | "@/support/*": ["./cypress/support/*"],
17 | "@/tests/*": ["./*"],
18 | "@/core/*": ["../packages/notivue/core/*"],
19 | "@/Notivue/*": ["../packages/notivue/Notivue/*"],
20 | "@/NotivueSwipe/*": ["../packages/notivue/NotivueSwipe/*"],
21 | "@/NotivueKeyboard/*": ["../packages/notivue/NotivueKeyboard/*"],
22 | "@/Notifications/*": ["../packages/notivue/Notifications/*"]
23 | }
24 | },
25 | "include": ["."]
26 | }
27 |
--------------------------------------------------------------------------------
/tests/vite.config.mts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | import { defineConfig } from 'vite'
4 | import { alias } from './shared-config'
5 |
6 | export default defineConfig({
7 | test: {
8 | include: ['config/*.test.ts'],
9 | },
10 | resolve: { alias },
11 | })
12 |
--------------------------------------------------------------------------------