├── .nvmrc ├── .husky └── pre-commit ├── tests ├── utils │ ├── styleMock.js │ └── components │ │ └── Simple.vue └── unit │ ├── components │ ├── __snapshots__ │ │ ├── VtTransition.spec.ts.snap │ │ ├── VtProgressBar.spec.ts.snap │ │ ├── VtCloseButton.spec.ts.snap │ │ ├── VtIcon.spec.ts.snap │ │ ├── VtToastContainer.spec.ts.snap │ │ └── VtToast.spec.ts.snap │ ├── icons │ │ ├── VtInfoIcon.spec.ts │ │ ├── VtErrorIcon.spec.ts │ │ ├── VtSuccessIcon.spec.ts │ │ ├── VtWarningIcon.spec.ts │ │ └── __snapshots__ │ │ │ ├── VtSuccessIcon.spec.ts.snap │ │ │ ├── VtWarningIcon.spec.ts.snap │ │ │ ├── VtInfoIcon.spec.ts.snap │ │ │ └── VtErrorIcon.spec.ts.snap │ ├── VtTransition.spec.ts │ ├── VtCloseButton.spec.ts │ ├── VtProgressBar.spec.ts │ └── VtIcon.spec.ts │ ├── index.spec.ts │ └── ts │ ├── composables │ ├── useHoverable.spec.ts │ ├── useFocusable.spec.ts │ ├── useToast.spec.ts │ └── useDraggable.spec.ts │ ├── eventBus.spec.ts │ ├── plugin.spec.ts │ ├── interface.spec.ts │ └── utils.spec.ts ├── demo ├── public │ └── favicon.ico ├── src │ ├── assets │ │ └── logo.png │ ├── main.ts │ └── App.vue └── index.html ├── .eslintignore ├── .prettierrc ├── tsconfig.build.json ├── src ├── types │ ├── shims-vue.d.ts │ ├── vue-helper.ts │ ├── plugin.ts │ ├── common.ts │ ├── toastContainer.ts │ └── toast.ts ├── scss │ ├── index.scss │ ├── _icon.scss │ ├── _progressBar.scss │ ├── _closeButton.scss │ ├── _variables.scss │ ├── _toast.scss │ ├── _toastContainer.scss │ └── animations │ │ ├── _fade.scss │ │ ├── _bounce.scss │ │ └── _slideBlurred.scss ├── ts │ ├── plugin.ts │ ├── constants.ts │ ├── composables │ │ ├── useFocusable.ts │ │ ├── useHoverable.ts │ │ ├── useToast.ts │ │ └── useDraggable.ts │ ├── propValidators.ts │ ├── eventBus.ts │ ├── utils.ts │ └── interface.ts ├── index.ts └── components │ ├── icons │ ├── VtSuccessIcon.vue │ ├── VtWarningIcon.vue │ ├── VtInfoIcon.vue │ └── VtErrorIcon.vue │ ├── VtTransition.vue │ ├── VtCloseButton.vue │ ├── VtProgressBar.vue │ ├── VtIcon.vue │ ├── VtToast.vue │ └── VtToastContainer.vue ├── .gitignore ├── jest.config.js ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── cicd.yml ├── tsconfig.json ├── LICENCE ├── vite.config.ts ├── .eslintrc.js ├── package.json ├── CODE_OF_CONDUCT.md └── CONTRIBUTING.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 14 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn lint-staged 5 | -------------------------------------------------------------------------------- /tests/utils/styleMock.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | process() { 3 | return "" 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /demo/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Maronato/vue-toastification/HEAD/demo/public/favicon.ico -------------------------------------------------------------------------------- /demo/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Maronato/vue-toastification/HEAD/demo/src/assets/logo.png -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | examples/ 2 | nuxt/plugin.js 3 | node_modules/ 4 | dist/ 5 | coverage/ 6 | .github/ 7 | .vscode/ 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "arrowParens": "avoid", 4 | "htmlWhitespaceSensitivity": "css" 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"] 4 | } 5 | -------------------------------------------------------------------------------- /demo/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue" 2 | 3 | import Toast from "../../src" 4 | 5 | import App from "./App.vue" 6 | 7 | createApp(App).use(Toast).mount("#app") 8 | -------------------------------------------------------------------------------- /tests/utils/components/Simple.vue: -------------------------------------------------------------------------------- 1 | 4 | 11 | -------------------------------------------------------------------------------- /src/types/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.vue" { 2 | import { DefineComponent } from "vue" 3 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types 4 | const component: DefineComponent<{}, {}, any> 5 | export default component 6 | } 7 | -------------------------------------------------------------------------------- /tests/unit/components/__snapshots__/VtTransition.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`VtTransition snapshots default values 1`] = `""`; 4 | 5 | exports[`VtTransition transition-group has custom classes 1`] = `
`; 6 | -------------------------------------------------------------------------------- /src/scss/index.scss: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | 3 | @import "variables"; 4 | @import "toastContainer"; 5 | @import "toast"; 6 | @import "closeButton"; 7 | @import "progressBar"; 8 | @import "icon"; 9 | 10 | @import "animations/bounce"; 11 | @import "animations/fade"; 12 | @import "animations/slideBlurred"; 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist 4 | coverage 5 | .nyc_output 6 | 7 | # local env files 8 | .env.local 9 | .env.*.local 10 | 11 | # Log files 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /tests/unit/components/icons/VtInfoIcon.spec.ts: -------------------------------------------------------------------------------- 1 | import { mount } from "@vue/test-utils" 2 | 3 | import VtInfoIcon from "../../../../src/components/icons/VtInfoIcon.vue" 4 | 5 | describe("VtInfoIcon", () => { 6 | it("matches snapshot", () => { 7 | const wrapper = mount(VtInfoIcon) 8 | expect(wrapper.element).toMatchSnapshot() 9 | }) 10 | }) 11 | -------------------------------------------------------------------------------- /src/scss/_icon.scss: -------------------------------------------------------------------------------- 1 | .#{$vt-namespace}__icon { 2 | margin: auto 18px auto 0px; 3 | background: transparent; 4 | outline: none; 5 | border: none; 6 | padding: 0; 7 | transition: 0.3s ease; 8 | align-items: center; 9 | width: 20px; 10 | height: 100%; 11 | .#{$vt-namespace}__toast--rtl & { 12 | margin: auto 0px auto 18px; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/unit/components/icons/VtErrorIcon.spec.ts: -------------------------------------------------------------------------------- 1 | import { mount } from "@vue/test-utils" 2 | 3 | import VtErrorIcon from "../../../../src/components/icons/VtErrorIcon.vue" 4 | 5 | describe("VtErrorIcon", () => { 6 | it("matches snapshot", () => { 7 | const wrapper = mount(VtErrorIcon) 8 | expect(wrapper.element).toMatchSnapshot() 9 | }) 10 | }) 11 | -------------------------------------------------------------------------------- /tests/unit/components/icons/VtSuccessIcon.spec.ts: -------------------------------------------------------------------------------- 1 | import { mount } from "@vue/test-utils" 2 | 3 | import VtSuccessIcon from "../../../../src/components/icons/VtSuccessIcon.vue" 4 | 5 | describe("VtSuccessIcon", () => { 6 | it("matches snapshot", () => { 7 | const wrapper = mount(VtSuccessIcon) 8 | expect(wrapper.element).toMatchSnapshot() 9 | }) 10 | }) 11 | -------------------------------------------------------------------------------- /tests/unit/components/icons/VtWarningIcon.spec.ts: -------------------------------------------------------------------------------- 1 | import { mount } from "@vue/test-utils" 2 | 3 | import VtWarningIcon from "../../../../src/components/icons/VtWarningIcon.vue" 4 | 5 | describe("VtWarningIcon", () => { 6 | it("matches snapshot", () => { 7 | const wrapper = mount(VtWarningIcon) 8 | expect(wrapper.element).toMatchSnapshot() 9 | }) 10 | }) 11 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/types/vue-helper.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-types */ 2 | // Taken from vue's own implementation 3 | 4 | type NotUndefined = T extends undefined ? never : T 5 | 6 | type InferDefault = T extends 7 | | null 8 | | number 9 | | string 10 | | boolean 11 | | symbol 12 | | Function 13 | ? T 14 | : () => T 15 | 16 | export type InferDefaults = { 17 | [K in keyof T]?: InferDefault> 18 | } 19 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | clearMocks: true, 4 | testEnvironment: "jsdom", 5 | transform: { 6 | "^.+\\.vue$": "@vue/vue3-jest", 7 | "^.+\\.(css|less|scss)$": "./tests/utils/styleMock.js", 8 | }, 9 | collectCoverage: true, 10 | collectCoverageFrom: ["src/**/*"], 11 | coverageThreshold: { 12 | global: { 13 | branches: 100, 14 | functions: 100, 15 | lines: 100, 16 | statements: 100, 17 | }, 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /src/scss/_progressBar.scss: -------------------------------------------------------------------------------- 1 | @keyframes scale-x-frames { 2 | 0% { 3 | transform: scaleX(1); 4 | } 5 | 100% { 6 | transform: scaleX(0); 7 | } 8 | } 9 | 10 | .#{$vt-namespace}__progress-bar { 11 | position: absolute; 12 | bottom: 0; 13 | left: 0; 14 | width: 100%; 15 | height: 5px; 16 | z-index: ($vt-z-index + 1); 17 | background-color: rgba(255, 255, 255, 0.7); 18 | transform-origin: left; 19 | animation: scale-x-frames linear 1 forwards; 20 | .#{$vt-namespace}__toast--rtl & { 21 | right: 0; 22 | left: unset; 23 | transform-origin: right; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/ts/plugin.ts: -------------------------------------------------------------------------------- 1 | import { Plugin } from "vue" 2 | 3 | import type { PluginOptions } from "../types/plugin" 4 | 5 | import { createToastInstance, toastInjectionKey } from "./composables/useToast" 6 | import { globalEventBus } from "./eventBus" 7 | 8 | export const VueToastificationPlugin: Plugin = ( 9 | App, 10 | options?: PluginOptions 11 | ) => { 12 | if (options?.shareAppContext === true) { 13 | options.shareAppContext = App 14 | } 15 | const inter = createToastInstance({ 16 | eventBus: globalEventBus, 17 | ...options, 18 | }) 19 | App.provide(toastInjectionKey, inter) 20 | } 21 | -------------------------------------------------------------------------------- /src/ts/constants.ts: -------------------------------------------------------------------------------- 1 | export enum TYPE { 2 | SUCCESS = "success", 3 | ERROR = "error", 4 | WARNING = "warning", 5 | INFO = "info", 6 | DEFAULT = "default", 7 | } 8 | 9 | export enum POSITION { 10 | TOP_LEFT = "top-left", 11 | TOP_CENTER = "top-center", 12 | TOP_RIGHT = "top-right", 13 | BOTTOM_LEFT = "bottom-left", 14 | BOTTOM_CENTER = "bottom-center", 15 | BOTTOM_RIGHT = "bottom-right", 16 | } 17 | 18 | export enum EVENTS { 19 | ADD = "add", 20 | DISMISS = "dismiss", 21 | UPDATE = "update", 22 | CLEAR = "clear", 23 | UPDATE_DEFAULTS = "update_defaults", 24 | } 25 | 26 | export const VT_NAMESPACE = "Vue-Toastification" 27 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type { ToastInterface } from "./ts/interface" 2 | import type { PluginOptions } from "./types/plugin" 3 | 4 | import "./scss/index.scss" 5 | import { 6 | createToastInstance, 7 | provideToast, 8 | useToast, 9 | } from "./ts/composables/useToast" 10 | import { POSITION, TYPE } from "./ts/constants" 11 | import { EventBus } from "./ts/eventBus" 12 | import { VueToastificationPlugin } from "./ts/plugin" 13 | 14 | export default VueToastificationPlugin 15 | 16 | export { 17 | createToastInstance, 18 | provideToast, 19 | useToast, 20 | EventBus, 21 | POSITION, 22 | TYPE, 23 | PluginOptions, 24 | ToastInterface, 25 | } 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: "Type: Enhancement, Status: Available" 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | ## Detailed Description 12 | 13 | 14 | ## Context 15 | 16 | 17 | 18 | ## Possible Implementation 19 | 20 | -------------------------------------------------------------------------------- /src/components/icons/VtSuccessIcon.vue: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /src/scss/_closeButton.scss: -------------------------------------------------------------------------------- 1 | .#{$vt-namespace}__close-button { 2 | font-weight: bold; 3 | font-size: 24px; 4 | line-height: 24px; 5 | background: transparent; 6 | outline: none; 7 | border: none; 8 | padding: 0; 9 | padding-left: 10px; 10 | cursor: pointer; 11 | transition: 0.3s ease; 12 | align-items: center; 13 | color: #fff; 14 | opacity: 0.3; 15 | transition: visibility 0s, opacity 0.2s linear; 16 | &:hover, 17 | &:focus { 18 | opacity: 1; 19 | } 20 | .#{$vt-namespace}__toast:not(:hover) &.show-on-hover { 21 | opacity: 0; 22 | } 23 | .#{$vt-namespace}__toast--rtl & { 24 | padding-left: unset; 25 | padding-right: 10px; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/components/icons/VtWarningIcon.vue: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /src/components/icons/VtInfoIcon.vue: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /tests/unit/components/icons/__snapshots__/VtSuccessIcon.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`VtSuccessIcon matches snapshot 1`] = ` 4 | 16 | `; 17 | -------------------------------------------------------------------------------- /src/components/icons/VtErrorIcon.vue: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /tests/unit/components/icons/__snapshots__/VtWarningIcon.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`VtWarningIcon matches snapshot 1`] = ` 4 | 16 | `; 17 | -------------------------------------------------------------------------------- /tests/unit/components/icons/__snapshots__/VtInfoIcon.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`VtInfoIcon matches snapshot 1`] = ` 4 | 16 | `; 17 | -------------------------------------------------------------------------------- /tests/unit/components/icons/__snapshots__/VtErrorIcon.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`VtErrorIcon matches snapshot 1`] = ` 4 | 16 | `; 17 | -------------------------------------------------------------------------------- /src/ts/composables/useFocusable.ts: -------------------------------------------------------------------------------- 1 | import { toRefs, ref, Ref, onMounted, onBeforeUnmount } from "vue" 2 | 3 | import type { Focusable } from "../../types/common" 4 | 5 | export const useFocusable = ( 6 | el: Ref, 7 | props: Required 8 | ) => { 9 | const { pauseOnFocusLoss } = toRefs(props) 10 | const focused = ref(true) 11 | const onFocus = () => (focused.value = true) 12 | const onBlur = () => (focused.value = false) 13 | onMounted(() => { 14 | if (el.value && pauseOnFocusLoss.value) { 15 | addEventListener("blur", onBlur) 16 | addEventListener("focus", onFocus) 17 | } 18 | }) 19 | onBeforeUnmount(() => { 20 | if (el.value && pauseOnFocusLoss.value) { 21 | removeEventListener("blur", onBlur) 22 | removeEventListener("focus", onFocus) 23 | } 24 | }) 25 | 26 | return { focused } 27 | } 28 | -------------------------------------------------------------------------------- /src/ts/composables/useHoverable.ts: -------------------------------------------------------------------------------- 1 | import { toRefs, ref, Ref, onMounted, onBeforeUnmount } from "vue" 2 | 3 | import type { Hoverable } from "../../types/common" 4 | 5 | export const useHoverable = ( 6 | el: Ref, 7 | props: Required 8 | ) => { 9 | const { pauseOnHover } = toRefs(props) 10 | const hovering = ref(false) 11 | 12 | const onEnter = () => (hovering.value = true) 13 | const onLeave = () => (hovering.value = false) 14 | onMounted(() => { 15 | if (el.value && pauseOnHover.value) { 16 | el.value.addEventListener("mouseenter", onEnter) 17 | el.value.addEventListener("mouseleave", onLeave) 18 | } 19 | }) 20 | onBeforeUnmount(() => { 21 | if (el.value && pauseOnHover.value) { 22 | el.value.removeEventListener("mouseenter", onEnter) 23 | el.value.removeEventListener("mouseleave", onLeave) 24 | } 25 | }) 26 | 27 | return { hovering } 28 | } 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "esnext", 4 | "moduleResolution": "node", 5 | "target": "esnext", 6 | "useDefineForClassFields": true, 7 | "sourceMap": true, 8 | "strict": true, 9 | "outDir": "dist", 10 | "declaration": true, 11 | "declarationDir": "dist/types", 12 | "noImplicitReturns": true, 13 | "noImplicitAny": true, 14 | "noImplicitThis": true, 15 | "strictNullChecks": true, 16 | "jsx": "preserve", 17 | "types": ["@types/jest"], 18 | "resolveJsonModule": true, 19 | "esModuleInterop": true, 20 | "skipLibCheck": true 21 | }, 22 | "include": [ 23 | "src/**/*.ts", 24 | "src/**/*.d.ts", 25 | "src/**/*.tsx", 26 | "src/**/*.vue", 27 | "demo/**/*.ts", 28 | "demo/**/*.d.ts", 29 | "demo/**/*.tsx", 30 | "demo/**/*.vue", 31 | "tests/**/*.ts", 32 | "tests/**/*.d.ts", 33 | "tests/**/*.tsx", 34 | "tests/**/*.vue" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /tests/unit/index.spec.ts: -------------------------------------------------------------------------------- 1 | import * as index from "../../src/index" 2 | import { 3 | createToastInstance, 4 | provideToast, 5 | useToast, 6 | } from "../../src/ts/composables/useToast" 7 | import { TYPE, POSITION } from "../../src/ts/constants" 8 | import { EventBus } from "../../src/ts/eventBus" 9 | import { VueToastificationPlugin } from "../../src/ts/plugin" 10 | 11 | describe("index", () => { 12 | describe("exports", () => { 13 | it("exports constants", () => { 14 | expect(index.TYPE).toBe(TYPE) 15 | expect(index.POSITION).toBe(POSITION) 16 | }) 17 | it("exports classes", () => { 18 | expect(index.EventBus).toBe(EventBus) 19 | }) 20 | it("exports functions", () => { 21 | expect(index.createToastInstance).toBe(createToastInstance) 22 | expect(index.provideToast).toBe(provideToast) 23 | expect(index.useToast).toBe(useToast) 24 | }) 25 | it("exports default", () => { 26 | expect(index.default).toBe(VueToastificationPlugin) 27 | }) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /src/types/plugin.ts: -------------------------------------------------------------------------------- 1 | import type { App, ComponentPublicInstance } from "vue" 2 | 3 | import type { BaseToastOptions } from "./toast" 4 | import type { BaseToastContainerOptions } from "./toastContainer" 5 | 6 | export declare interface BasePluginOptions 7 | extends BaseToastContainerOptions, 8 | BaseToastOptions {} 9 | 10 | export declare interface PluginOptions extends BasePluginOptions { 11 | /** 12 | * Callback executed when the toast container is mounted. 13 | * 14 | * Receives the Container vue instance as a parameter. 15 | */ 16 | onMounted?: ( 17 | containerComponent: ComponentPublicInstance, 18 | containerApp: App 19 | ) => void 20 | /** 21 | * Shares the context of your app with your toasts 22 | * 23 | * This allows toasts to use your app's plugins, mixins, global components, etc. 24 | * 25 | * If you set it to `true`, the app wherein the plugin is installed will be used. 26 | * You may also provide the app instance you wish to use. 27 | */ 28 | shareAppContext?: boolean | App 29 | } 30 | -------------------------------------------------------------------------------- /src/scss/_variables.scss: -------------------------------------------------------------------------------- 1 | $vt-namespace: "Vue-Toastification" !default; 2 | $vt-toast-min-width: 326px !default; 3 | $vt-toast-max-width: 600px !default; 4 | $vt-toast-background: #ffffff !default; 5 | 6 | $vt-toast-min-height: 64px !default; 7 | $vt-toast-max-height: 800px !default; 8 | 9 | $vt-color-default: #1976d2 !default; 10 | $vt-text-color-default: #fff !default; 11 | $vt-color-info: #2196f3 !default; 12 | $vt-text-color-info: #fff !default; 13 | $vt-color-success: #4caf50 !default; 14 | $vt-text-color-success: #fff !default; 15 | $vt-color-warning: #ffc107 !default; 16 | $vt-text-color-warning: #fff !default; 17 | $vt-color-error: #ff5252 !default; 18 | $vt-text-color-error: #fff !default; 19 | 20 | $vt-color-progress-default: linear-gradient( 21 | to right, 22 | #4cd964, 23 | #5ac8fa, 24 | #007aff, 25 | #34aadc, 26 | #5856d6, 27 | #ff2d55 28 | ) !default; 29 | 30 | $vt-mobile: "only screen and (max-width : 600px)" !default; 31 | $vt-not-mobile: "only screen and (min-width : 600px)" !default; 32 | $vt-font-family: "Lato", Helvetica, "Roboto", Arial, sans-serif !default; 33 | $vt-z-index: 9999 !default; 34 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 maronato 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /demo/src/App.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 26 | 27 | 37 | -------------------------------------------------------------------------------- /src/components/VtTransition.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 45 | -------------------------------------------------------------------------------- /src/components/VtCloseButton.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 49 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help vue-toastification improve 4 | title: '' 5 | labels: "Type: Bug, Status: Available" 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Versions 11 | 12 | - 1.0 13 | - 2.0 14 | 15 | ## Describe the bug 16 | 17 | When doing X, the page breaks... 18 | 19 | ## Expected behavior 20 | 22 | Instead of breaking the page, Y should happen... 23 | 24 | ## Steps to reproduce 25 | 26 | 27 | 28 | 29 | Reproduction [demo](link-to-demo). 30 | Steps: 31 | 1. Install normally 32 | 2. Set up plugin 33 | 3. ... 34 | 35 | ## Your Environment 36 | 37 | - Device: [e.g. iPhone6] 38 | - OS: [e.g. iOS] 39 | - Browser [e.g. chrome, safari] 40 | - Version [e.g. 22] 41 | 42 | ## Additional context 43 | 44 | -------------------------------------------------------------------------------- /tests/unit/components/__snapshots__/VtProgressBar.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`VtProgressBar matches snapshot 1`] = ` 4 |
8 | `; 9 | 10 | exports[`VtProgressBar sets opacity to 0 from from hideProgressBar 1`] = ` 11 |
15 | `; 16 | 17 | exports[`VtProgressBar sets playstate from isRunning 1`] = ` 18 |
22 | `; 23 | 24 | exports[`VtProgressBar sets style duration from timeout 1`] = ` 25 |
29 | `; 30 | 31 | exports[`VtProgressBar triggers class reset on timeout change 1`] = ` 32 |
36 | `; 37 | 38 | exports[`VtProgressBar triggers class reset on timeout change 2`] = ` 39 |
43 | `; 44 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | 6 | ## Related Issue 7 | 8 | 9 | 10 | 11 | 12 | ## Screenshots or GIFs (if appropriate): 13 | 14 | ## Types of changes 15 | 16 | - [ ] Bug fix (non-breaking change which fixes an issue) 17 | - [ ] New feature (non-breaking change which adds functionality) 18 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 19 | 20 | ## Checklist: 21 | 22 | 23 | - [ ] My code follows the code style of this project. 24 | - [ ] My change requires a change to the documentation. 25 | - [ ] I have updated the documentation accordingly. 26 | - [ ] I have read the [**CONTRIBUTING**](https://github.com/Maronato/vue-toastification/blob/master/CONTRIBUTING.md) document. 27 | - [ ] I have added tests to cover my changes. 28 | - [ ] All new and existing tests passed. 29 | -------------------------------------------------------------------------------- /src/types/common.ts: -------------------------------------------------------------------------------- 1 | import type { Component } from "vue" 2 | 3 | import type { EventBusInterface } from "../ts/eventBus" 4 | 5 | export declare type ToastID = string | number 6 | 7 | export declare type ClassNames = string | string[] 8 | 9 | export declare interface EventBusable { 10 | /** 11 | * EventBus instance used to pass events across the interface 12 | * 13 | * Created by default, but you can use your own if you want 14 | */ 15 | eventBus?: EventBusInterface 16 | } 17 | 18 | export declare interface Draggable { 19 | /** 20 | * Position of the toast on the screen. 21 | * 22 | * Can be any of top-right, top-center, top-left, bottom-right, bottom-center, bottom-left. 23 | */ 24 | draggable?: boolean 25 | /** 26 | * By how much of the toast width in percent (0 to 1) it must be dragged before being dismissed. 27 | */ 28 | draggablePercent?: number 29 | } 30 | 31 | export declare interface Hoverable { 32 | /** 33 | * Whether or not the toast is paused when it is hovered by the mouse. 34 | */ 35 | pauseOnHover?: boolean 36 | } 37 | 38 | export declare interface Focusable { 39 | /** 40 | * Whether or not the toast is paused when the window loses focus. 41 | */ 42 | pauseOnFocusLoss?: boolean 43 | } 44 | 45 | export declare type Icon = 46 | | boolean 47 | | string 48 | | { 49 | iconTag?: keyof HTMLElementTagNameMap 50 | iconChildren?: string 51 | iconClass?: string 52 | } 53 | | Component 54 | | JSX.Element 55 | 56 | export declare type Button = 57 | | false 58 | | keyof HTMLElementTagNameMap 59 | | Component 60 | | JSX.Element 61 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from "path" 2 | 3 | import vue from "@vitejs/plugin-vue" 4 | import { defineConfig } from "vite" 5 | 6 | const commonConfig = defineConfig({ 7 | plugins: [vue()], 8 | define: { 9 | __VUE_OPTIONS_API__: false, 10 | }, 11 | }) 12 | 13 | const libConfig = defineConfig({ 14 | ...commonConfig, 15 | build: { 16 | minify: true, 17 | lib: { 18 | entry: path.resolve(__dirname, "src/index.ts"), 19 | name: "VueToastification", 20 | fileName: format => `index.${format}.js`, 21 | }, 22 | rollupOptions: { 23 | // make sure to externalize deps that shouldn't be bundled 24 | // into your library 25 | external: ["vue"], 26 | output: { 27 | // Provide global variables to use in the UMD build 28 | // for externalized deps 29 | globals: { 30 | vue: "Vue", 31 | }, 32 | // Use `index.css` for css 33 | assetFileNames: assetInfo => { 34 | if (assetInfo.name == "style.css") return "index.css" 35 | return assetInfo.name 36 | }, 37 | }, 38 | }, 39 | }, 40 | }) 41 | 42 | const demoConfig = defineConfig({ 43 | ...commonConfig, 44 | root: "./demo", 45 | }) 46 | 47 | // https://vitejs.dev/config/ 48 | export default defineConfig(({ command }) => { 49 | const executionMode: "lib" | "demo" = 50 | (process.env.MODE as "lib" | "demo") || "lib" 51 | 52 | const mode = command === "build" ? "production" : "development" 53 | 54 | if (executionMode === "demo") { 55 | return { ...demoConfig, mode } 56 | } else if (executionMode === "lib") { 57 | return { ...libConfig, mode } 58 | } 59 | }) 60 | -------------------------------------------------------------------------------- /src/components/VtProgressBar.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 64 | -------------------------------------------------------------------------------- /src/scss/_toast.scss: -------------------------------------------------------------------------------- 1 | .#{$vt-namespace}__toast { 2 | display: inline-flex; 3 | position: relative; 4 | max-height: $vt-toast-max-height; 5 | min-height: $vt-toast-min-height; 6 | box-sizing: border-box; 7 | margin-bottom: 1rem; 8 | padding: 22px 24px; 9 | border-radius: 8px; 10 | box-shadow: 0 1px 10px 0 rgba(0, 0, 0, 0.1), 0 2px 15px 0 rgba(0, 0, 0, 0.05); 11 | justify-content: space-between; 12 | font-family: $vt-font-family; 13 | max-width: $vt-toast-max-width; 14 | min-width: $vt-toast-min-width; 15 | pointer-events: auto; 16 | overflow: hidden; 17 | // overflow: hidden + border-radius does not work properly on Safari 18 | // The following magic line fixes it 19 | // https://stackoverflow.com/a/58283449 20 | transform: translateZ(0); 21 | direction: ltr; 22 | &--rtl { 23 | direction: rtl; 24 | } 25 | 26 | &--default { 27 | background-color: $vt-color-default; 28 | color: $vt-text-color-default; 29 | } 30 | &--info { 31 | background-color: $vt-color-info; 32 | color: $vt-text-color-info; 33 | } 34 | &--success { 35 | background-color: $vt-color-success; 36 | color: $vt-text-color-success; 37 | } 38 | &--error { 39 | background-color: $vt-color-error; 40 | color: $vt-text-color-error; 41 | } 42 | &--warning { 43 | background-color: $vt-color-warning; 44 | color: $vt-text-color-warning; 45 | } 46 | 47 | @media #{$vt-mobile} { 48 | border-radius: 0px; 49 | margin-bottom: 0.5rem; 50 | } 51 | 52 | &-body { 53 | flex: 1; 54 | line-height: 24px; 55 | font-size: 16px; 56 | word-break: break-word; 57 | white-space: pre-wrap; 58 | } 59 | 60 | &-component-body { 61 | flex: 1; 62 | } 63 | 64 | &.disable-transition { 65 | animation: none !important; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /tests/unit/components/__snapshots__/VtCloseButton.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`VtCloseButton adds 'show-on-hover' class 1`] = ` 4 | 10 | `; 11 | 12 | exports[`VtCloseButton adds 'show-on-hover' class 2`] = ` 13 | 19 | `; 20 | 21 | exports[`VtCloseButton adds custom class array 1`] = ` 22 | 28 | `; 29 | 30 | exports[`VtCloseButton adds custom class string 1`] = ` 31 | 37 | `; 38 | 39 | exports[`VtCloseButton matches default snapshot 1`] = ` 40 | 46 | `; 47 | 48 | exports[`VtCloseButton renders custom aria label 1`] = ` 49 | 55 | `; 56 | 57 | exports[`VtCloseButton renders default aria label 1`] = ` 58 | 64 | `; 65 | 66 | exports[`VtCloseButton string custom component 1`] = ` 67 |
71 | × 72 |
73 | `; 74 | 75 | exports[`VtCloseButton vue custom component 1`] = ` 76 |
80 | Example component 81 |
82 | `; 83 | -------------------------------------------------------------------------------- /src/ts/propValidators.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-empty-function */ 2 | import { InferDefaults } from "../types/vue-helper" 3 | 4 | import type { ToastOptions } from "../types/toast" 5 | import type { ToastContainerOptions } from "../types/toastContainer" 6 | 7 | import { POSITION, VT_NAMESPACE, TYPE } from "./constants" 8 | import { EventBus } from "./eventBus" 9 | 10 | const defaultEventBus = /* istanbul ignore next */ () => new EventBus() 11 | const emptyFunction = /* istanbul ignore next */ () => {} 12 | 13 | // This wraps a method to be returned as a factory function 14 | const asFactory = (f: T) => (() => f) as unknown as T 15 | 16 | export const TOAST_DEFAULTS: Required>> = { 17 | id: 0, 18 | accessibility: () => ({ 19 | toastRole: "alert", 20 | closeButtonLabel: "close", 21 | }), 22 | bodyClassName: () => [], 23 | closeButton: () => "button", 24 | closeButtonClassName: () => [], 25 | closeOnClick: true, 26 | draggable: true, 27 | draggablePercent: 0.6, 28 | eventBus: defaultEventBus, 29 | hideProgressBar: false, 30 | icon: () => true, 31 | pauseOnFocusLoss: true, 32 | pauseOnHover: true, 33 | position: POSITION.TOP_RIGHT, 34 | rtl: false, 35 | showCloseButtonOnHover: false, 36 | timeout: 5000, 37 | toastClassName: () => [], 38 | onClick: emptyFunction, 39 | onClose: emptyFunction, 40 | type: TYPE.DEFAULT, 41 | } 42 | 43 | export const TOAST_CONTAINER_DEFAULTS: Required< 44 | InferDefaults> 45 | > = { 46 | position: TOAST_DEFAULTS.position, 47 | container: () => document.body, 48 | containerClassName: () => [], 49 | eventBus: defaultEventBus, 50 | filterBeforeCreate: asFactory(toast => toast), 51 | filterToasts: asFactory(toasts => toasts), 52 | maxToasts: 20, 53 | newestOnTop: true, 54 | toastDefaults: () => ({}), 55 | transition: `${VT_NAMESPACE}__bounce`, 56 | defaultToastProps: /* istanbul ignore next */ () => ({}), 57 | } 58 | -------------------------------------------------------------------------------- /src/types/toastContainer.ts: -------------------------------------------------------------------------------- 1 | import type { TYPE } from "../ts/constants" 2 | import type { ClassNames, EventBusable } from "./common" 3 | import type { 4 | BaseToastOptions, 5 | ToastOptions, 6 | ToastOptionsAndContent, 7 | } from "./toast" 8 | 9 | type ContainerCallback = () => HTMLElement | Promise 10 | 11 | export declare interface BaseToastContainerOptions extends EventBusable { 12 | position?: BaseToastOptions["position"] 13 | /** 14 | * Container where the toasts are mounted. 15 | */ 16 | container?: HTMLElement | ContainerCallback 17 | /** 18 | * Whether or not the newest toasts are placed on the top of the stack. 19 | */ 20 | newestOnTop?: boolean 21 | /** 22 | * Maximum number of toasts on each stack at a time. Overflows wait until older toasts are dismissed to appear. 23 | */ 24 | maxToasts?: number 25 | /** 26 | * Name of the Vue Transition or object with classes to use. 27 | * 28 | * Only `enter-active`, `leave-active` and `move` are applied. 29 | */ 30 | transition?: string | Record<"enter" | "leave" | "move", string> 31 | /** 32 | * Toast's defaults object for configuring default toast options for each toast type. 33 | * 34 | * Possible object properties can be any of `success error default info warning` 35 | */ 36 | toastDefaults?: Partial> 37 | /** 38 | * Callback to filter toasts during creation 39 | * 40 | * Takes the new toast and a list of the current toasts and returns a modified toast or false. 41 | */ 42 | filterBeforeCreate?: ( 43 | toast: ToastOptionsAndContent, 44 | toasts: ToastOptionsAndContent[] 45 | ) => ToastOptionsAndContent | false 46 | /** 47 | * Callback to filter toasts during render 48 | * 49 | * Filter toasts during render and queues filtered toasts. 50 | */ 51 | filterToasts?: (toasts: ToastOptionsAndContent[]) => ToastOptionsAndContent[] 52 | /** 53 | * Extra CSS class or classes added to each of the Toast containers. 54 | * 55 | * Keep in mind that there is one container for each possible toast position. 56 | */ 57 | containerClassName?: ClassNames 58 | } 59 | 60 | export declare interface ToastContainerOptions 61 | extends BaseToastContainerOptions { 62 | defaultToastProps?: BaseToastOptions 63 | } 64 | -------------------------------------------------------------------------------- /src/scss/_toastContainer.scss: -------------------------------------------------------------------------------- 1 | @use "sass:math"; 2 | 3 | .#{$vt-namespace}__container { 4 | z-index: $vt-z-index; 5 | position: fixed; 6 | padding: 4px; 7 | width: $vt-toast-max-width; 8 | box-sizing: border-box; 9 | display: flex; 10 | min-height: 100%; 11 | color: #fff; 12 | flex-direction: column; 13 | pointer-events: none; 14 | 15 | @media #{$vt-not-mobile} { 16 | &.top-left, 17 | &.top-right, 18 | &.top-center { 19 | top: 1em; 20 | } 21 | 22 | &.bottom-left, 23 | &.bottom-right, 24 | &.bottom-center { 25 | bottom: 1em; 26 | flex-direction: column-reverse; 27 | } 28 | 29 | &.top-left, 30 | &.bottom-left { 31 | left: 1em; 32 | .#{$vt-namespace}__toast { 33 | margin-right: auto; 34 | } 35 | // Firefox does not apply rtl rules to containers and margins, it appears. 36 | // See https://github.com/Maronato/vue-toastification/issues/179 37 | @supports not (-moz-appearance:none) { 38 | .#{$vt-namespace}__toast--rtl { 39 | margin-right: unset; 40 | margin-left: auto; 41 | } 42 | } 43 | } 44 | 45 | &.top-right, 46 | &.bottom-right { 47 | right: 1em; 48 | .#{$vt-namespace}__toast { 49 | margin-left: auto; 50 | } 51 | // Firefox does not apply rtl rules to containers and margins, it appears. 52 | // See https://github.com/Maronato/vue-toastification/issues/179 53 | @supports not (-moz-appearance:none) { 54 | .#{$vt-namespace}__toast--rtl { 55 | margin-left: unset; 56 | margin-right: auto; 57 | } 58 | } 59 | } 60 | 61 | &.top-center, 62 | &.bottom-center { 63 | left: 50%; 64 | margin-left: - math.div($vt-toast-max-width, 2); 65 | .#{$vt-namespace}__toast { 66 | margin-left: auto; 67 | margin-right: auto; 68 | } 69 | } 70 | } 71 | 72 | @media #{$vt-mobile} { 73 | width: 100vw; 74 | padding: 0; 75 | left: 0; 76 | margin: 0; 77 | .#{$vt-namespace}__toast { 78 | width: 100%; 79 | } 80 | &.top-left, 81 | &.top-right, 82 | &.top-center { 83 | top: 0; 84 | } 85 | &.bottom-left, 86 | &.bottom-right, 87 | &.bottom-center { 88 | bottom: 0; 89 | flex-direction: column-reverse; 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/ts/composables/useToast.ts: -------------------------------------------------------------------------------- 1 | import { InjectionKey, provide, inject, getCurrentInstance } from "vue" 2 | 3 | import { VT_NAMESPACE } from "../constants" 4 | import { 5 | EventBusInterface, 6 | isEventBusInterface, 7 | EventBus, 8 | globalEventBus, 9 | } from "../eventBus" 10 | import { buildInterface } from "../interface" 11 | import { isBrowser } from "../utils" 12 | 13 | import type { PluginOptions } from "../../types/plugin" 14 | import type { ToastInterface } from "../interface" 15 | 16 | import { createToastInstance as ownExports_createToastInstance } from "./useToast" 17 | 18 | const toastInjectionKey: InjectionKey = 19 | Symbol("VueToastification") 20 | 21 | /** 22 | * Creates (or recovers) a toast instance and returns 23 | * an interface to it 24 | */ 25 | interface CreateToastInstance { 26 | /** 27 | * Creates an interface to an existing instance from its interface 28 | */ 29 | (eventBus: EventBusInterface): ToastInterface 30 | /** 31 | * Creats a new instance of Vue Toastification 32 | */ 33 | (options?: PluginOptions): ToastInterface 34 | } 35 | 36 | const createMockToastInstance: CreateToastInstance = () => { 37 | const toast = () => 38 | // eslint-disable-next-line no-console 39 | console.warn(`[${VT_NAMESPACE}] This plugin does not support SSR!`) 40 | return new Proxy(toast, { 41 | get() { 42 | return toast 43 | }, 44 | }) as unknown as ToastInterface 45 | } 46 | 47 | const createToastInstance: CreateToastInstance = optionsOrEventBus => { 48 | if (!isBrowser()) { 49 | return createMockToastInstance() 50 | } 51 | if (isEventBusInterface(optionsOrEventBus)) { 52 | return buildInterface({ eventBus: optionsOrEventBus }, false) 53 | } 54 | return buildInterface(optionsOrEventBus, true) 55 | } 56 | 57 | const provideToast = (options?: PluginOptions) => { 58 | if (getCurrentInstance()) { 59 | const toast = ownExports_createToastInstance(options) 60 | provide(toastInjectionKey, toast) 61 | } 62 | } 63 | 64 | const useToast = (eventBus?: EventBus) => { 65 | if (eventBus) { 66 | return ownExports_createToastInstance(eventBus) 67 | } 68 | const toast = getCurrentInstance() 69 | ? inject(toastInjectionKey, undefined) 70 | : undefined 71 | return toast ? toast : ownExports_createToastInstance(globalEventBus) 72 | } 73 | 74 | export { useToast, provideToast, toastInjectionKey, createToastInstance } 75 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | node: true, 6 | "vue/setup-compiler-macros": true, 7 | }, 8 | extends: [ 9 | "eslint:recommended", 10 | "plugin:vue/vue3-recommended", 11 | "plugin:@typescript-eslint/recommended", 12 | "plugin:prettier/recommended", 13 | "plugin:import/recommended", 14 | "plugin:import/typescript", 15 | ], 16 | parser: "vue-eslint-parser", 17 | parserOptions: { 18 | parser: "@typescript-eslint/parser", 19 | sourceType: "module", 20 | }, 21 | plugins: ["vue", "@typescript-eslint", "jsx-a11y"], 22 | rules: { 23 | "no-console": process.env.NODE_ENV === "production" ? "error" : "off", 24 | "no-debugger": process.env.NODE_ENV === "production" ? "error" : "off", 25 | "@typescript-eslint/no-empty-function": ["off"], 26 | "import/order": [ 27 | "error", 28 | { 29 | groups: [ 30 | "builtin", 31 | "external", 32 | "internal", 33 | "parent", 34 | "object", 35 | "type", 36 | "sibling", 37 | "index", 38 | ], 39 | warnOnUnassignedImports: true, 40 | pathGroups: [ 41 | { 42 | pattern: "vue", 43 | group: "builtin", 44 | position: "before", 45 | }, 46 | { 47 | pattern: "**/*.{css,scss}", 48 | group: "index", 49 | position: "after", 50 | }, 51 | ], 52 | pathGroupsExcludedImportTypes: ["builtin", "vue", "**/*.{css,scss}"], 53 | "newlines-between": "always", 54 | alphabetize: { 55 | order: "asc", 56 | caseInsensitive: true, 57 | }, 58 | }, 59 | ], 60 | "import/first": "error", 61 | "import/no-duplicates": "error", 62 | "import/newline-after-import": "error", 63 | "import/no-unassigned-import": [ 64 | "error", 65 | { allow: ["**/*.css", "**/*.scss", "**/*.sass"] }, 66 | ], 67 | "import/no-named-default": "error", 68 | }, 69 | overrides: [ 70 | { 71 | files: [ 72 | "**/__tests__/*.{j,t}s?(x)", 73 | "**/tests/unit/**/*.spec.{j,t}s?(x)", 74 | ], 75 | env: { 76 | jest: true, 77 | }, 78 | }, 79 | ], 80 | settings: { 81 | "import/parsers": { 82 | "@typescript-eslint/parser": [".ts", ".tsx"], 83 | }, 84 | }, 85 | } 86 | -------------------------------------------------------------------------------- /src/ts/eventBus.ts: -------------------------------------------------------------------------------- 1 | import type { ToastID } from "../types/common" 2 | import type { 3 | ToastOptionsAndContent, 4 | ToastContent, 5 | ToastOptions, 6 | } from "../types/toast" 7 | import type { ToastContainerOptions } from "../types/toastContainer" 8 | import type { EVENTS } from "./constants" 9 | 10 | import { hasProp, isFunction } from "./utils" 11 | 12 | type EventData = { 13 | [EVENTS.ADD]: ToastOptionsAndContent & { 14 | id: ToastID 15 | } 16 | [EVENTS.CLEAR]: undefined 17 | [EVENTS.DISMISS]: ToastID 18 | [EVENTS.UPDATE]: 19 | | { 20 | id: ToastID 21 | options: Partial & { content?: ToastContent } 22 | create: false 23 | } 24 | | { 25 | id: ToastID 26 | options: Partial & { content: ToastContent } 27 | create: true 28 | } 29 | [EVENTS.UPDATE_DEFAULTS]: ToastContainerOptions 30 | } 31 | 32 | type Handler = (event: EventData[E]) => void 33 | 34 | export interface EventBusInterface { 35 | on(eventType: E, handler: Handler): void 36 | off(eventType: E, handler: Handler): void 37 | emit(eventType: E, event: EventData[E]): void 38 | } 39 | 40 | type HandlerList = Handler[] 41 | 42 | type HandlerMap = { 43 | [E in EVENTS]?: HandlerList 44 | } 45 | 46 | export class EventBus implements EventBusInterface { 47 | protected allHandlers: HandlerMap = {} 48 | 49 | protected getHandlers(eventType: E) { 50 | return (this.allHandlers[eventType] as HandlerList | undefined) || [] 51 | } 52 | 53 | public on(eventType: E, handler: Handler) { 54 | const handlers = this.getHandlers(eventType) 55 | handlers.push(handler) 56 | this.allHandlers[eventType] = handlers as EventBus["allHandlers"][E] 57 | } 58 | public off(eventType: E, handler: Handler) { 59 | const handlers = this.getHandlers(eventType) 60 | handlers.splice(handlers.indexOf(handler) >>> 0, 1) 61 | } 62 | public emit(eventType: E, event: EventData[E]) { 63 | const handlers = this.getHandlers(eventType) 64 | handlers.forEach(handler => handler(event)) 65 | } 66 | } 67 | 68 | export const isEventBusInterface = (e: unknown): e is EventBusInterface => 69 | ["on", "off", "emit"].every(f => hasProp(e, f) && isFunction(e[f])) 70 | 71 | export const globalEventBus = new EventBus() 72 | -------------------------------------------------------------------------------- /tests/unit/ts/composables/useHoverable.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable vue/one-component-per-file */ 2 | 3 | import { computed, defineComponent, h, reactive, ref } from "vue" 4 | 5 | import { mount } from "@vue/test-utils" 6 | 7 | import { useHoverable } from "../../../../src/ts/composables/useHoverable" 8 | 9 | const activeText = "things" 10 | const inactiveText = "stuffs" 11 | 12 | type Props = Parameters[1] 13 | 14 | const TestComponent = defineComponent({ 15 | props: { 16 | pauseOnHover: { 17 | type: Boolean, 18 | default: false, 19 | }, 20 | }, 21 | setup(props) { 22 | const el = ref() 23 | const { hovering } = useHoverable(el, props) 24 | const text = computed(() => (hovering.value ? activeText : inactiveText)) 25 | return () => 26 | h("div", { ref: el, id: "outer" }, h("p", { id: "inner" }, text.value)) 27 | }, 28 | }) 29 | 30 | describe("useHoverable", () => { 31 | beforeEach(() => { 32 | jest.resetAllMocks() 33 | jest.restoreAllMocks() 34 | }) 35 | 36 | it("Returns valid object", async () => { 37 | const consoleSpy = jest.spyOn(console, "warn").mockImplementation() 38 | 39 | const el = ref() 40 | const props = reactive({ pauseOnHover: false }) 41 | const retuned = useHoverable(el, props) 42 | 43 | // not-used-in-setup warnings 44 | expect(consoleSpy).toBeCalledTimes(2) 45 | 46 | expect(retuned.hovering.value).toBe(false) 47 | }) 48 | 49 | it("pause/resume if pauseOnHover", async () => { 50 | const props = reactive({ pauseOnHover: true }) 51 | const wrapper = mount(TestComponent, { props }) 52 | 53 | const outer = wrapper.find("#outer") 54 | const inner = wrapper.find("#inner") 55 | 56 | expect(inner.text()).toEqual(inactiveText) 57 | 58 | await outer.trigger("mouseenter") 59 | 60 | expect(inner.text()).toEqual(activeText) 61 | 62 | await outer.trigger("mouseleave") 63 | 64 | expect(inner.text()).toEqual(inactiveText) 65 | }) 66 | 67 | it("does not pause/resume if not pauseOnHover", async () => { 68 | const props = reactive({ pauseOnHover: false }) 69 | const wrapper = mount(TestComponent, { props }) 70 | 71 | const outer = wrapper.find("#outer") 72 | const inner = wrapper.find("#inner") 73 | 74 | expect(inner.text()).toEqual(inactiveText) 75 | 76 | await outer.trigger("mouseenter") 77 | 78 | expect(inner.text()).not.toEqual(activeText) 79 | 80 | wrapper.unmount() 81 | }) 82 | }) 83 | -------------------------------------------------------------------------------- /tests/unit/ts/eventBus.spec.ts: -------------------------------------------------------------------------------- 1 | import { EVENTS } from "../../../src/ts/constants" 2 | import { 3 | EventBus, 4 | isEventBusInterface, 5 | EventBusInterface, 6 | } from "../../../src/ts/eventBus" 7 | 8 | describe("EventBus", () => { 9 | it("creates eventbus", () => { 10 | expect(() => new EventBus()).not.toThrow() 11 | expect(new EventBus()).toEqual(expect.any(EventBus)) 12 | }) 13 | it("subscribes to events", () => { 14 | const eventBus = new EventBus() 15 | const handler = jest.fn() 16 | const eventName = EVENTS.DISMISS 17 | 18 | expect(eventBus["allHandlers"]).toEqual({}) 19 | 20 | eventBus.on(eventName, handler) 21 | 22 | expect(eventBus["allHandlers"]).toEqual( 23 | expect.objectContaining({ 24 | [EVENTS.DISMISS]: expect.arrayContaining([handler]), 25 | }) 26 | ) 27 | }) 28 | 29 | it("unsubscribes from events", () => { 30 | const eventBus = new EventBus() 31 | const handler = jest.fn() 32 | const eventName = EVENTS.DISMISS 33 | 34 | eventBus.on(eventName, handler) 35 | eventBus.off(eventName, handler) 36 | 37 | expect(eventBus["allHandlers"]).toEqual( 38 | expect.objectContaining({ [EVENTS.DISMISS]: [] }) 39 | ) 40 | }) 41 | 42 | it("emits events", () => { 43 | const eventBus = new EventBus() 44 | const handler1 = jest.fn() 45 | const handler2 = jest.fn() 46 | const eventName = EVENTS.DISMISS 47 | 48 | expect(eventBus["allHandlers"]).toEqual({}) 49 | 50 | eventBus.on(eventName, handler1) 51 | eventBus.on(eventName, handler2) 52 | 53 | expect(handler1).not.toHaveBeenCalled() 54 | expect(handler2).not.toHaveBeenCalled() 55 | 56 | eventBus.emit(eventName, 1) 57 | 58 | expect(handler1).toHaveBeenCalledTimes(1) 59 | expect(handler2).toHaveBeenCalledTimes(1) 60 | expect(handler1).toBeCalledWith(1) 61 | expect(handler2).toBeCalledWith(1) 62 | }) 63 | }) 64 | 65 | describe("isEventBusInterface", () => { 66 | it("detects EventBus", () => { 67 | expect(isEventBusInterface(new EventBus())).toBe(true) 68 | }) 69 | 70 | it("detects invalid interface", () => { 71 | expect(isEventBusInterface("foo")).toBe(false) 72 | }) 73 | 74 | it("detects generic interface", () => { 75 | class GenericEventBus implements EventBusInterface { 76 | on() { 77 | return 78 | } 79 | off() { 80 | return 81 | } 82 | emit() { 83 | return 84 | } 85 | } 86 | expect(isEventBusInterface(new GenericEventBus())).toBe(true) 87 | }) 88 | }) 89 | -------------------------------------------------------------------------------- /src/components/VtIcon.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 89 | -------------------------------------------------------------------------------- /.github/workflows/cicd.yml: -------------------------------------------------------------------------------- 1 | name: CICD 2 | 3 | on: 4 | push: 5 | branches: [main, next] 6 | pull_request: 7 | branches: [main, next] 8 | 9 | workflow_dispatch: 10 | 11 | jobs: 12 | ci: 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | matrix: 17 | node-version: [14, 16] 18 | 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v2 22 | 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v2 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | 28 | - name: Install Yarn 29 | run: npm install -g yarn 30 | 31 | - name: Get yarn cache directory path 32 | id: yarn-cache-dir-path 33 | run: echo "::set-output name=dir::$(yarn cache dir)" 34 | 35 | - uses: actions/cache@v2 36 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) 37 | with: 38 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 39 | key: ${{ runner.os }}-${{ matrix.node-version }}-yarn-${{ hashFiles('**/yarn.lock') }} 40 | restore-keys: | 41 | ${{ runner.os }}-${{ matrix.node-version }}-yarn- 42 | 43 | - name: Install dependencies 44 | run: yarn --frozen-lockfile 45 | 46 | - name: Lint 47 | run: yarn lint 48 | 49 | - name: Install Codecov 50 | run: yarn global add codecov 51 | 52 | - name: Test 53 | run: yarn test 54 | 55 | - name: Upload to Codecov 56 | uses: codecov/codecov-action@v2.1.0 57 | 58 | cd: 59 | needs: [ci] 60 | runs-on: ubuntu-latest 61 | 62 | strategy: 63 | matrix: 64 | node-version: [14, 16] 65 | 66 | steps: 67 | - name: Checkout 68 | uses: actions/checkout@v2 69 | 70 | - name: Use Node.js ${{ matrix.node-version }} 71 | uses: actions/setup-node@v2 72 | with: 73 | node-version: ${{ matrix.node-version }} 74 | 75 | - name: Install Yarn 76 | run: npm install -g yarn 77 | 78 | - name: Get yarn cache directory path 79 | id: yarn-cache-dir-path 80 | run: echo "::set-output name=dir::$(yarn cache dir)" 81 | 82 | - uses: actions/cache@v2 83 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) 84 | with: 85 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 86 | key: ${{ runner.os }}-${{ matrix.node-version }}-yarn-${{ hashFiles('**/yarn.lock') }} 87 | restore-keys: | 88 | ${{ runner.os }}-${{ matrix.node-version }}-yarn- 89 | 90 | - name: Install dependencies 91 | run: yarn --frozen-lockfile 92 | 93 | - name: Build lib 94 | run: yarn build 95 | 96 | - name: Build demo 97 | run: yarn build:demo 98 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-toastification", 3 | "version": "2.0.0-rc.5", 4 | "private": false, 5 | "description": "Toasts for Vue made easy!", 6 | "author": "Gustavo Maronato", 7 | "scripts": { 8 | "dev": "MODE=demo yarn vite", 9 | "prebuild": "rimraf ./dist", 10 | "build": "yarn build:code && yarn build:tsc", 11 | "build:code": "MODE=lib vite build", 12 | "build:tsc": "NODE_ENV=production tsc --emitDeclarationOnly --project tsconfig.build.json", 13 | "build:demo": "MODE=demo vite build", 14 | "test:unit": "jest", 15 | "test": "yarn test:unit", 16 | "test:watch": "yarn test --watch", 17 | "lint": "yarn lint:tsc && yarn lint:eslint .", 18 | "lint:fix": "yarn lint --fix", 19 | "lint:tsc": "NODE_ENV=production vue-tsc --noEmit", 20 | "lint:eslint": "NODE_ENV=production eslint --ext vue,ts,tsx", 21 | "lint:staged": "yarn lint:tsc && yarn lint:eslint --fix", 22 | "preview": "NODE_ENV=production vite preview --port 3000 demo", 23 | "prepublishOnly": "yarn lint && yarn test && yarn build", 24 | "prepare": "husky install" 25 | }, 26 | "main": "./dist/index.umd.js", 27 | "module": "./dist/index.es.js", 28 | "exports": { 29 | ".": { 30 | "import": "./dist/index.es.js", 31 | "require": "./dist/index.umd.js" 32 | } 33 | }, 34 | "typings": "dist/types/index.d.ts", 35 | "types": "dist/types/index.d.ts", 36 | "style": "dist/index.css", 37 | "files": [ 38 | "src", 39 | "dist" 40 | ], 41 | "sideEffects": true, 42 | "dependencies": {}, 43 | "devDependencies": { 44 | "@types/jest": "^27.4.0", 45 | "@types/lodash.merge": "^4.6.6", 46 | "@typescript-eslint/eslint-plugin": "^5.10.1", 47 | "@typescript-eslint/parser": "^5.10.1", 48 | "@vitejs/plugin-vue": "^2.1.0", 49 | "@vue/test-utils": "^2.0.0-rc.18", 50 | "@vue/vue3-jest": "^27.0.0-alpha.4", 51 | "babel-jest": "^27.4.6", 52 | "eslint": "^8.8.0", 53 | "eslint-config-prettier": "^8.3.0", 54 | "eslint-plugin-import": "^2.25.4", 55 | "eslint-plugin-jsx-a11y": "^6.5.1", 56 | "eslint-plugin-prettier": "^4.0.0", 57 | "eslint-plugin-vue": "^8.4.0", 58 | "husky": "^7.0.4", 59 | "jest": "^27.4.7", 60 | "lint-staged": "^12.3.2", 61 | "lodash.merge": "^4.6.2", 62 | "prettier": "^2.5.1", 63 | "sass": "^1.49.0", 64 | "ts-jest": "^27.1.3", 65 | "typescript": "^4.5.5", 66 | "vite": "^2.7.13", 67 | "vue": "^3", 68 | "vue-tsc": "^0.31.1" 69 | }, 70 | "lint-staged": { 71 | "*.{js,jsx,vue,ts,tsx}": [ 72 | "yarn lint:staged" 73 | ] 74 | }, 75 | "peerDependencies": { 76 | "vue": "^3.0.2" 77 | }, 78 | "bugs": { 79 | "url": "https://github.com/Maronato/vue-toastification/issues" 80 | }, 81 | "homepage": "https://github.com/Maronato/vue-toastification#readme", 82 | "keywords": [ 83 | "vue", 84 | "notification", 85 | "toast" 86 | ], 87 | "license": "MIT", 88 | "repository": { 89 | "type": "git", 90 | "url": "https://github.com/Maronato/vue-toastification.git" 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/scss/animations/_fade.scss: -------------------------------------------------------------------------------- 1 | $trans-cubic-bezier: cubic-bezier(0.215, 0.61, 0.355, 1); 2 | @mixin timing-function { 3 | animation-timing-function: $trans-cubic-bezier; 4 | } 5 | 6 | /* ---------------------------------------------- 7 | * Modified version from Animista 8 | * Animista is Licensed under FreeBSD License. 9 | * See http://animista.net/license for more info. 10 | * w: http://animista.net, t: @cssanimista 11 | * ---------------------------------------------- */ 12 | 13 | @keyframes fadeOutTop { 14 | 0% { 15 | transform: translateY(0); 16 | opacity: 1; 17 | } 18 | 100% { 19 | transform: translateY(-50px); 20 | opacity: 0; 21 | } 22 | } 23 | 24 | @keyframes fadeOutLeft { 25 | 0% { 26 | transform: translateX(0); 27 | opacity: 1; 28 | } 29 | 100% { 30 | transform: translateX(-50px); 31 | opacity: 0; 32 | } 33 | } 34 | 35 | @keyframes fadeOutBottom { 36 | 0% { 37 | transform: translateY(0); 38 | opacity: 1; 39 | } 40 | 100% { 41 | transform: translateY(50px); 42 | opacity: 0; 43 | } 44 | } 45 | 46 | @keyframes fadeOutRight { 47 | 0% { 48 | transform: translateX(0); 49 | opacity: 1; 50 | } 51 | 100% { 52 | transform: translateX(50px); 53 | opacity: 0; 54 | } 55 | } 56 | 57 | @keyframes fadeInLeft { 58 | 0% { 59 | transform: translateX(-50px); 60 | opacity: 0; 61 | } 62 | 100% { 63 | transform: translateX(0); 64 | opacity: 1; 65 | } 66 | } 67 | 68 | @keyframes fadeInRight { 69 | 0% { 70 | transform: translateX(50px); 71 | opacity: 0; 72 | } 73 | 100% { 74 | transform: translateX(0); 75 | opacity: 1; 76 | } 77 | } 78 | 79 | @keyframes fadeInTop { 80 | 0% { 81 | transform: translateY(-50px); 82 | opacity: 0; 83 | } 84 | 100% { 85 | transform: translateY(0); 86 | opacity: 1; 87 | } 88 | } 89 | 90 | @keyframes fadeInBottom { 91 | 0% { 92 | transform: translateY(50px); 93 | opacity: 0; 94 | } 95 | 100% { 96 | transform: translateY(0); 97 | opacity: 1; 98 | } 99 | } 100 | 101 | .#{$vt-namespace}__fade-enter-active { 102 | &.top-left, 103 | &.bottom-left { 104 | animation-name: fadeInLeft; 105 | } 106 | &.top-right, 107 | &.bottom-right { 108 | animation-name: fadeInRight; 109 | } 110 | &.top-center { 111 | animation-name: fadeInTop; 112 | } 113 | &.bottom-center { 114 | animation-name: fadeInBottom; 115 | } 116 | } 117 | 118 | .#{$vt-namespace}__fade-leave-active:not(.disable-transition) { 119 | &.top-left, 120 | &.bottom-left { 121 | animation-name: fadeOutLeft; 122 | } 123 | &.top-right, 124 | &.bottom-right { 125 | animation-name: fadeOutRight; 126 | } 127 | &.top-center { 128 | animation-name: fadeOutTop; 129 | } 130 | &.bottom-center { 131 | animation-name: fadeOutBottom; 132 | } 133 | } 134 | 135 | .#{$vt-namespace}__fade-leave-active, 136 | .#{$vt-namespace}__fade-enter-active { 137 | animation-duration: 750ms; 138 | animation-fill-mode: both; 139 | } 140 | 141 | .#{$vt-namespace}__fade-move { 142 | transition-timing-function: ease-in-out; 143 | transition-property: all; 144 | transition-duration: 400ms; 145 | } 146 | -------------------------------------------------------------------------------- /tests/unit/components/VtTransition.spec.ts: -------------------------------------------------------------------------------- 1 | import { TransitionGroup } from "vue" 2 | 3 | import { mount } from "@vue/test-utils" 4 | 5 | import VtTransition from "../../../src/components/VtTransition.vue" 6 | 7 | const asEmitter = (arg: unknown) => 8 | arg as { 9 | $emit: (event: string, el: Element | XMLDocument, done: () => void) => void 10 | } 11 | 12 | describe("VtTransition", () => { 13 | it("snapshots default values", () => { 14 | const wrapper = mount(VtTransition) 15 | expect(wrapper.html()).toMatchSnapshot() 16 | }) 17 | it("transition-group has default classes", () => { 18 | const wrapper = mount(VtTransition, { 19 | global: { 20 | stubs: { 21 | "transition-group": false, 22 | }, 23 | }, 24 | }) 25 | const transition = wrapper.vm.$props.transition 26 | const componentProps = wrapper.findComponent(TransitionGroup).props() 27 | expect(componentProps.enterActiveClass).toBe(`${transition}-enter-active`) 28 | expect(componentProps.moveClass).toBe(`${transition}-move`) 29 | expect(componentProps.leaveActiveClass).toBe(`${transition}-leave-active`) 30 | }) 31 | it("transition-group has custom classes", () => { 32 | const wrapper = mount(VtTransition, { 33 | props: { 34 | transition: { 35 | enter: "enter-transition", 36 | move: "move-transition", 37 | leave: "leave-transition", 38 | }, 39 | }, 40 | global: { 41 | stubs: { 42 | "transition-group": false, 43 | }, 44 | }, 45 | }) 46 | const componentProps = wrapper.findComponent(TransitionGroup).props() 47 | expect(componentProps.enterActiveClass).toBe("enter-transition") 48 | expect(componentProps.moveClass).toBe("move-transition") 49 | expect(componentProps.leaveActiveClass).toBe("leave-transition") 50 | expect(wrapper.element).toMatchSnapshot() 51 | }) 52 | it("leave", () => { 53 | const wrapper = mount(VtTransition, { 54 | global: { 55 | stubs: { 56 | "transition-group": false, 57 | }, 58 | }, 59 | }) 60 | const transition = wrapper.findComponent(TransitionGroup) 61 | 62 | const done = jest.fn() 63 | const el = document.createElement("div") 64 | 65 | asEmitter(transition.vm).$emit("leave", el, done) 66 | 67 | expect(el.style.left).toBe(el.offsetLeft + "px") 68 | expect(el.style.top).toBe(el.offsetTop + "px") 69 | expect(el.style.width).toBe(getComputedStyle(el).width) 70 | expect(el.style.position).toBe("absolute") 71 | 72 | const events = transition.emitted("leave") 73 | 74 | expect(events).toBeTruthy() 75 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 76 | expect(events![0]).toEqual([el, done]) 77 | }) 78 | it("leave not HTMLElement", () => { 79 | const wrapper = mount(VtTransition, { 80 | global: { 81 | stubs: { 82 | "transition-group": false, 83 | }, 84 | }, 85 | }) 86 | const transition = wrapper.findComponent(TransitionGroup) 87 | 88 | const done = jest.fn() 89 | const el = document.implementation.createDocument("xml", "element") 90 | 91 | asEmitter(transition.vm).$emit("leave", el, done) 92 | 93 | const events = transition.emitted("leave") 94 | 95 | expect(events).toBeTruthy() 96 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 97 | expect(events![0]).toEqual([el, done]) 98 | }) 99 | }) 100 | -------------------------------------------------------------------------------- /src/ts/composables/useDraggable.ts: -------------------------------------------------------------------------------- 1 | import { 2 | toRefs, 3 | ref, 4 | Ref, 5 | onMounted, 6 | onBeforeUnmount, 7 | computed, 8 | watch, 9 | } from "vue" 10 | 11 | import { isDOMRect, getX, getY } from "../utils" 12 | 13 | import type { Draggable } from "../../types/common" 14 | 15 | export const useDraggable = ( 16 | el: Ref, 17 | props: Required 18 | ) => { 19 | // Extract used props 20 | const { draggablePercent, draggable } = toRefs(props) 21 | 22 | // Define state 23 | const dragRect = computed(() => 24 | el.value ? el.value.getBoundingClientRect() : undefined 25 | ) 26 | const dragStarted = ref(false) 27 | const beingDragged = ref(false) 28 | const dragPos = ref({ x: 0, y: 0 }) 29 | const dragStart = ref(0) 30 | const dragDelta = computed(() => 31 | beingDragged.value ? dragPos.value.x - dragStart.value : 0 32 | ) 33 | const dragComplete = ref(false) 34 | 35 | // Computed state 36 | const removalDistance = computed(() => 37 | isDOMRect(dragRect.value) 38 | ? (dragRect.value.right - dragRect.value.left) * draggablePercent.value 39 | : 0 40 | ) 41 | 42 | // Update style to match drag 43 | watch( 44 | [el, dragStart, dragPos, dragDelta, removalDistance, beingDragged], 45 | () => { 46 | /* istanbul ignore else */ 47 | if (el.value) { 48 | el.value.style.transform = "translateX(0px)" 49 | el.value.style.opacity = "1" 50 | if (dragStart.value === dragPos.value.x) { 51 | el.value.style.transition = "" 52 | } else if (beingDragged.value) { 53 | el.value.style.transform = `translateX(${dragDelta.value}px)` 54 | el.value.style.opacity = `${ 55 | 1 - Math.abs(dragDelta.value / removalDistance.value) 56 | }` 57 | } else { 58 | el.value.style.transition = "transform 0.2s, opacity 0.2s" 59 | } 60 | } 61 | } 62 | ) 63 | 64 | // Define handlers 65 | const onDragStart = (event: TouchEvent | MouseEvent) => { 66 | dragStarted.value = true 67 | dragPos.value = { x: getX(event), y: getY(event) } 68 | dragStart.value = dragPos.value.x 69 | } 70 | const onDragMove = (event: TouchEvent | MouseEvent) => { 71 | if (dragStarted.value) { 72 | beingDragged.value = true 73 | event.preventDefault() 74 | dragPos.value = { x: getX(event), y: getY(event) } 75 | } 76 | } 77 | const onDragEnd = () => { 78 | dragStarted.value = false 79 | if (beingDragged.value) { 80 | if (Math.abs(dragDelta.value) >= removalDistance.value) { 81 | dragComplete.value = true 82 | } else { 83 | setTimeout(() => { 84 | beingDragged.value = false 85 | }) 86 | } 87 | } 88 | } 89 | 90 | onMounted(() => { 91 | if (draggable.value && el.value) { 92 | el.value.addEventListener("touchstart", onDragStart, { 93 | passive: true, 94 | }) 95 | el.value.addEventListener("mousedown", onDragStart) 96 | addEventListener("touchmove", onDragMove, { passive: false }) 97 | addEventListener("mousemove", onDragMove) 98 | addEventListener("touchend", onDragEnd) 99 | addEventListener("mouseup", onDragEnd) 100 | } 101 | }) 102 | onBeforeUnmount(() => { 103 | /* istanbul ignore else */ 104 | if (draggable.value && el.value) { 105 | el.value.removeEventListener("touchstart", onDragStart) 106 | el.value.removeEventListener("mousedown", onDragStart) 107 | removeEventListener("touchmove", onDragMove) 108 | removeEventListener("mousemove", onDragMove) 109 | removeEventListener("touchend", onDragEnd) 110 | removeEventListener("mouseup", onDragEnd) 111 | } 112 | }) 113 | 114 | return { dragComplete, beingDragged } 115 | } 116 | -------------------------------------------------------------------------------- /tests/unit/ts/composables/useFocusable.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable vue/one-component-per-file */ 2 | 3 | import { computed, defineComponent, h, nextTick, reactive, ref } from "vue" 4 | 5 | import { mount } from "@vue/test-utils" 6 | 7 | import { useFocusable } from "../../../../src/ts/composables/useFocusable" 8 | 9 | const activeText = "things" 10 | const inactiveText = "stuffs" 11 | 12 | type Props = Parameters[1] 13 | 14 | const TestComponent = defineComponent({ 15 | props: { 16 | pauseOnFocusLoss: { 17 | type: Boolean, 18 | default: false, 19 | }, 20 | }, 21 | setup(props) { 22 | const el = ref() 23 | const { focused } = useFocusable(el, props) 24 | const text = computed(() => (focused.value ? activeText : inactiveText)) 25 | return () => 26 | h("div", { ref: el, id: "outer" }, h("p", { id: "inner" }, text.value)) 27 | }, 28 | }) 29 | 30 | describe("useFocusable", () => { 31 | beforeEach(() => { 32 | jest.resetAllMocks() 33 | jest.restoreAllMocks() 34 | }) 35 | 36 | it("Returns valid object", async () => { 37 | const consoleSpy = jest.spyOn(console, "warn").mockImplementation() 38 | 39 | const el = ref() 40 | const props = reactive({ pauseOnFocusLoss: false }) 41 | const retuned = useFocusable(el, props) 42 | 43 | // not-used-in-setup warnings 44 | expect(consoleSpy).toBeCalledTimes(2) 45 | 46 | expect(retuned.focused.value).toBe(true) 47 | }) 48 | 49 | it("adds and removes event listeners", () => { 50 | const props = reactive({ pauseOnFocusLoss: true }) 51 | 52 | const addEventListenerSpy = jest.spyOn(window, "addEventListener") 53 | const removeEventListenerSpy = jest.spyOn(window, "removeEventListener") 54 | 55 | expect(addEventListenerSpy).not.toHaveBeenCalled() 56 | expect(removeEventListenerSpy).not.toHaveBeenCalled() 57 | 58 | const wrapper = mount(TestComponent, { props }) 59 | 60 | expect(addEventListenerSpy).toHaveBeenCalledTimes(2) 61 | expect(addEventListenerSpy).toHaveBeenCalledWith( 62 | "blur", 63 | expect.any(Function) 64 | ) 65 | expect(addEventListenerSpy).toHaveBeenCalledWith( 66 | "focus", 67 | expect.any(Function) 68 | ) 69 | expect(removeEventListenerSpy).not.toHaveBeenCalled() 70 | 71 | wrapper.unmount() 72 | 73 | expect(addEventListenerSpy).toHaveBeenCalledTimes(2) 74 | expect(removeEventListenerSpy).toHaveBeenCalledTimes(2) 75 | expect(removeEventListenerSpy).toHaveBeenCalledWith( 76 | "blur", 77 | expect.any(Function) 78 | ) 79 | expect(removeEventListenerSpy).toHaveBeenCalledWith( 80 | "focus", 81 | expect.any(Function) 82 | ) 83 | }) 84 | 85 | it("focus and blur if pauseOnFocusLoss", async () => { 86 | const props = reactive({ pauseOnFocusLoss: true }) 87 | const wrapper = mount(TestComponent, { props }) 88 | 89 | const inner = wrapper.find("#inner") 90 | 91 | expect(inner.text()).toEqual(activeText) 92 | 93 | window.dispatchEvent(new window.FocusEvent("blur")) 94 | await nextTick() 95 | 96 | expect(inner.text()).toEqual(inactiveText) 97 | 98 | window.dispatchEvent(new window.FocusEvent("focus")) 99 | await nextTick() 100 | 101 | expect(inner.text()).toEqual(activeText) 102 | }) 103 | 104 | it("does not focus and blur if not pauseOnFocusLoss", async () => { 105 | const props = reactive({ pauseOnFocusLoss: false }) 106 | const wrapper = mount(TestComponent, { props }) 107 | 108 | const inner = wrapper.find("#inner") 109 | 110 | expect(inner.text()).toEqual(activeText) 111 | 112 | window.dispatchEvent(new window.FocusEvent("blur")) 113 | await nextTick() 114 | 115 | expect(inner.text()).not.toEqual(inactiveText) 116 | 117 | wrapper.unmount() 118 | }) 119 | }) 120 | -------------------------------------------------------------------------------- /tests/unit/components/VtCloseButton.spec.ts: -------------------------------------------------------------------------------- 1 | import { markRaw } from "vue" 2 | 3 | import { mount } from "@vue/test-utils" 4 | 5 | import VtCloseButton from "../../../src/components/VtCloseButton.vue" 6 | import { VT_NAMESPACE } from "../../../src/ts/constants" 7 | import Simple from "../../utils/components/Simple.vue" 8 | 9 | describe("VtCloseButton", () => { 10 | it("matches default snapshot", () => { 11 | const wrapper = mount(VtCloseButton, { 12 | props: { 13 | component: false, 14 | }, 15 | }) 16 | expect(wrapper.element).toMatchSnapshot() 17 | }) 18 | it("has default class", () => { 19 | const wrapper = mount(VtCloseButton, { 20 | props: { 21 | component: false, 22 | }, 23 | }) 24 | expect(wrapper.classes()).toContain(`${VT_NAMESPACE}__close-button`) 25 | }) 26 | it("is a button by default", () => { 27 | const wrapper = mount(VtCloseButton, { 28 | props: { 29 | component: false, 30 | }, 31 | }) 32 | expect(wrapper.element.tagName).toEqual("BUTTON") 33 | }) 34 | it("string custom component", () => { 35 | const wrapper = mount(VtCloseButton, { 36 | props: { 37 | component: "div", 38 | }, 39 | }) 40 | expect(wrapper.element.tagName).toEqual("DIV") 41 | expect(wrapper.element).toMatchSnapshot() 42 | }) 43 | it("vue custom component", () => { 44 | const wrapper = mount(VtCloseButton, { 45 | props: { 46 | component: markRaw(Simple), 47 | }, 48 | }) 49 | expect(wrapper.findComponent(Simple).element).toBeTruthy() 50 | expect(wrapper.element).toMatchSnapshot() 51 | }) 52 | it("adds 'show-on-hover' class", () => { 53 | const wrapper = mount(VtCloseButton, { 54 | props: { 55 | component: false, 56 | showOnHover: false, 57 | }, 58 | }) 59 | const wrapper2 = mount(VtCloseButton, { 60 | props: { 61 | component: false, 62 | showOnHover: true, 63 | }, 64 | }) 65 | expect(wrapper.classes()).not.toContain("show-on-hover") 66 | expect(wrapper2.classes()).toContain("show-on-hover") 67 | expect(wrapper.element).toMatchSnapshot() 68 | expect(wrapper2.element).toMatchSnapshot() 69 | }) 70 | it("adds custom class string", () => { 71 | const wrapper = mount(VtCloseButton, { 72 | props: { 73 | component: false, 74 | classNames: "my-class", 75 | }, 76 | }) 77 | expect(wrapper.classes()).toContain("my-class") 78 | expect(wrapper.element).toMatchSnapshot() 79 | }) 80 | it("adds custom class array", () => { 81 | const wrapper = mount(VtCloseButton, { 82 | props: { 83 | component: false, 84 | classNames: ["my-class", "my-class2"], 85 | }, 86 | }) 87 | expect(wrapper.classes()).toContain("my-class") 88 | expect(wrapper.classes()).toContain("my-class2") 89 | expect(wrapper.element).toMatchSnapshot() 90 | }) 91 | it("attaches onClick listener", () => { 92 | const onClick = jest.fn() 93 | const wrapper = mount(VtCloseButton, { 94 | props: { 95 | component: false, 96 | }, 97 | attrs: { onClick }, 98 | }) 99 | expect(onClick).not.toHaveBeenCalled() 100 | wrapper.trigger("click") 101 | expect(onClick).toHaveBeenCalled() 102 | }) 103 | it("renders default aria label", () => { 104 | const wrapper = mount(VtCloseButton) 105 | expect(wrapper.find("button[aria-label='close']").exists()).toBe(true) 106 | expect(wrapper.element).toMatchSnapshot() 107 | }) 108 | it("renders custom aria label", () => { 109 | const wrapper = mount(VtCloseButton, { 110 | props: { 111 | ariaLabel: "text", 112 | }, 113 | }) 114 | expect(wrapper.find("button[aria-label='text']").exists()).toBe(true) 115 | expect(wrapper.element).toMatchSnapshot() 116 | }) 117 | }) 118 | -------------------------------------------------------------------------------- /src/types/toast.ts: -------------------------------------------------------------------------------- 1 | import type { Component } from "vue" 2 | 3 | import type { TYPE, POSITION } from "../ts/constants" 4 | import type { 5 | Button, 6 | ClassNames, 7 | Draggable, 8 | EventBusable, 9 | Focusable, 10 | Hoverable, 11 | Icon, 12 | ToastID, 13 | } from "./common" 14 | 15 | export declare interface BaseToastOptions 16 | extends EventBusable, 17 | Draggable, 18 | Hoverable, 19 | Focusable { 20 | /** 21 | * Position of the toast on the screen. 22 | * 23 | * Can be any of top-right, top-center, top-left, bottom-right, bottom-center, bottom-left. 24 | */ 25 | position?: POSITION 26 | 27 | /** 28 | * Whether or not the toast is closed when clicked. 29 | */ 30 | closeOnClick?: boolean 31 | /** 32 | * How many milliseconds for the toast to be auto dismissed, or false to disable. 33 | */ 34 | timeout?: number | false 35 | /** 36 | * Custom classes applied to the toast. 37 | */ 38 | toastClassName?: ClassNames 39 | /** 40 | * Custom classes applied to the body of the toast. 41 | */ 42 | bodyClassName?: ClassNames 43 | /** 44 | * Whether or not the progress bar is hidden. 45 | */ 46 | hideProgressBar?: boolean 47 | /** 48 | * Only shows the close button when hovering the toast 49 | */ 50 | showCloseButtonOnHover?: boolean 51 | /** 52 | * Custom icon class to be used. 53 | * 54 | * When set to `true`, the icon is set automatically depending on the toast type and `false` disables the icon. 55 | */ 56 | icon?: Icon 57 | /** 58 | * Custom close button component 59 | * 60 | * Alternative close button component to be displayed in toasts 61 | */ 62 | closeButton?: Button 63 | /** 64 | * Custom classes applied to the close button of the toast. 65 | */ 66 | closeButtonClassName?: ClassNames 67 | /** 68 | * Accessibility options 69 | */ 70 | accessibility?: { 71 | /** 72 | * Toast accessibility role 73 | * 74 | * Accessibility option "role" for screen readers. Defaults to "alert". 75 | */ 76 | toastRole?: string 77 | /** 78 | * Close button label 79 | * 80 | * Accessibility option of the closeButton's "label" for screen readers. Defaults to "close". 81 | */ 82 | closeButtonLabel?: string 83 | } 84 | /** 85 | * Right-to-Left support. 86 | * 87 | * If true, switches the toast contents from right to left. Defaults to false. 88 | */ 89 | rtl?: boolean 90 | } 91 | 92 | export declare interface ToastOptions extends BaseToastOptions { 93 | /** 94 | * ID of the toast. 95 | */ 96 | id?: ToastID 97 | /** 98 | * Type of the toast. 99 | * 100 | * Can be any of `success error default info warning` 101 | */ 102 | type?: TYPE 103 | /** 104 | * Callback executed when the toast is clicked. 105 | * 106 | * A closeToast callback is passed as argument to onClick when it is called. 107 | */ 108 | // eslint-disable-next-line @typescript-eslint/ban-types 109 | onClick?: (closeToast: Function) => void 110 | /** 111 | * Callback executed when the toast is closed. 112 | */ 113 | onClose?: () => void 114 | } 115 | 116 | export declare type RenderableToastContent = string | Component 117 | 118 | export declare interface ToastComponent { 119 | /** 120 | * Component that will be rendered. 121 | */ 122 | component: ToastContent 123 | /** 124 | * `propName: propValue` pairs of props that will be passed to the component. 125 | * 126 | * __These are not reactive__ 127 | */ 128 | props?: { [propName: string]: unknown } 129 | /** 130 | * `eventName: eventHandler` pairs of events that the component can emit. 131 | */ 132 | // eslint-disable-next-line @typescript-eslint/ban-types 133 | listeners?: { [listenerEvent: string]: Function } 134 | } 135 | 136 | export declare type ToastContent = 137 | | RenderableToastContent 138 | | JSX.Element 139 | | ToastComponent 140 | 141 | export declare type ToastOptionsAndContent = ToastOptions & { 142 | content: ToastContent 143 | } 144 | -------------------------------------------------------------------------------- /tests/unit/components/VtProgressBar.spec.ts: -------------------------------------------------------------------------------- 1 | import { nextTick } from "vue" 2 | 3 | import { mount } from "@vue/test-utils" 4 | 5 | import VtProgressBar from "../../../src/components/VtProgressBar.vue" 6 | import { VT_NAMESPACE } from "../../../src/ts/constants" 7 | 8 | describe("VtProgressBar", () => { 9 | it("matches snapshot", () => { 10 | const wrapper = mount(VtProgressBar) 11 | expect(wrapper.element).toMatchSnapshot() 12 | }) 13 | it("has default class", () => { 14 | const wrapper = mount(VtProgressBar) 15 | expect(wrapper.classes()).toContain(`${VT_NAMESPACE}__progress-bar`) 16 | }) 17 | it("has default style values", () => { 18 | const wrapper = mount(VtProgressBar) 19 | const vm = wrapper.vm as unknown as { [index: string]: unknown } 20 | const style = vm.style as { 21 | animationDuration: string 22 | animationPlayState: string 23 | opacity: 0 | 1 24 | } 25 | expect(style).toEqual({ 26 | animationDuration: "5000ms", 27 | animationPlayState: "paused", 28 | opacity: 1, 29 | }) 30 | }) 31 | it("sets style duration from timeout", () => { 32 | const wrapper = mount(VtProgressBar, { 33 | props: { 34 | timeout: 1000, 35 | }, 36 | }) 37 | const vm = wrapper.vm as unknown as { [index: string]: unknown } 38 | const style = vm.style as { 39 | animationDuration: string 40 | animationPlayState: string 41 | opacity: 0 | 1 42 | } 43 | expect(style).toEqual({ 44 | animationDuration: "1000ms", 45 | animationPlayState: "paused", 46 | opacity: 1, 47 | }) 48 | expect(wrapper.element).toMatchSnapshot() 49 | }) 50 | it("sets playstate from isRunning", () => { 51 | const wrapper = mount(VtProgressBar, { 52 | props: { 53 | isRunning: true, 54 | }, 55 | }) 56 | const vm = wrapper.vm as unknown as { [index: string]: unknown } 57 | const style = vm.style as { 58 | animationDuration: string 59 | animationPlayState: string 60 | opacity: 0 | 1 61 | } 62 | expect(style).toEqual({ 63 | animationDuration: "5000ms", 64 | animationPlayState: "running", 65 | opacity: 1, 66 | }) 67 | expect(wrapper.element).toMatchSnapshot() 68 | }) 69 | it("sets opacity to 0 from from hideProgressBar", () => { 70 | const wrapper = mount(VtProgressBar, { 71 | props: { 72 | hideProgressBar: true, 73 | }, 74 | }) 75 | const vm = wrapper.vm as unknown as { [index: string]: unknown } 76 | const style = vm.style as { 77 | animationDuration: string 78 | animationPlayState: string 79 | opacity: 0 | 1 80 | } 81 | expect(style).toEqual({ 82 | animationDuration: "5000ms", 83 | animationPlayState: "paused", 84 | opacity: 0, 85 | }) 86 | expect(wrapper.element).toMatchSnapshot() 87 | }) 88 | it("triggers class reset on timeout change", async () => { 89 | const wrapper = mount(VtProgressBar) 90 | expect(wrapper.classes()).toContain(`${VT_NAMESPACE}__progress-bar`) 91 | wrapper.setProps({ timeout: 1000 }) 92 | await nextTick() 93 | expect(wrapper.classes()).not.toContain(`${VT_NAMESPACE}__progress-bar`) 94 | expect(wrapper.element).toMatchSnapshot() 95 | await nextTick() 96 | await nextTick() 97 | expect(wrapper.classes()).toContain(`${VT_NAMESPACE}__progress-bar`) 98 | expect(wrapper.element).toMatchSnapshot() 99 | }) 100 | it("emits close-toast on animationend", async () => { 101 | const wrapper = mount(VtProgressBar) 102 | expect(wrapper.emitted("close-toast")).toBeFalsy() 103 | wrapper.trigger("animationend") 104 | expect(wrapper.emitted("close-toast")).toBeTruthy() 105 | }) 106 | it("removes listener on beforeDestroy", async () => { 107 | const wrapper = mount(VtProgressBar) 108 | const spyRemoveEventListener = jest.spyOn( 109 | wrapper.vm.$el, 110 | "removeEventListener" 111 | ) 112 | expect(spyRemoveEventListener).not.toHaveBeenCalled() 113 | wrapper.unmount() 114 | expect(spyRemoveEventListener).toHaveBeenCalled() 115 | }) 116 | }) 117 | -------------------------------------------------------------------------------- /tests/unit/ts/plugin.spec.ts: -------------------------------------------------------------------------------- 1 | import { App } from "vue" 2 | 3 | import { isFunction } from "@vue/shared" 4 | 5 | import { PluginOptions, ToastInterface, EventBus } from "../../../src" 6 | import * as useToast from "../../../src/ts/composables/useToast" 7 | import { globalEventBus } from "../../../src/ts/eventBus" 8 | import * as plugin from "../../../src/ts/plugin" 9 | 10 | // eslint-disable-next-line @typescript-eslint/ban-types 11 | type AsFunction = T extends Function ? T : never 12 | 13 | const pluginFunction = plugin.VueToastificationPlugin as AsFunction< 14 | typeof plugin.VueToastificationPlugin 15 | > 16 | 17 | describe("plugin", () => { 18 | beforeEach(() => { 19 | jest.resetAllMocks() 20 | jest.restoreAllMocks() 21 | }) 22 | 23 | describe("VueToastificationPlugin", () => { 24 | it("plugin is a function", () => { 25 | expect(isFunction(plugin.VueToastificationPlugin)).toBe(true) 26 | }) 27 | it("provides default if no options", () => { 28 | const toast = {} as ToastInterface 29 | const createToastInstanceSpy = jest 30 | .spyOn(useToast, "createToastInstance") 31 | .mockImplementation(() => toast) 32 | 33 | const mockApp = { provide: jest.fn() } as unknown as App 34 | 35 | expect(createToastInstanceSpy).not.toHaveBeenCalled() 36 | 37 | pluginFunction(mockApp) 38 | 39 | expect(createToastInstanceSpy).toHaveBeenCalledWith({ 40 | eventBus: globalEventBus, 41 | }) 42 | expect(mockApp.provide).toHaveBeenCalledWith( 43 | useToast.toastInjectionKey, 44 | toast 45 | ) 46 | }) 47 | 48 | it("provides with options", () => { 49 | const toast = {} as ToastInterface 50 | const createToastInstanceSpy = jest 51 | .spyOn(useToast, "createToastInstance") 52 | .mockImplementation(() => toast) 53 | 54 | const mockApp = { provide: jest.fn() } as unknown as App 55 | 56 | expect(createToastInstanceSpy).not.toHaveBeenCalled() 57 | 58 | const options: PluginOptions = { timeout: 1000 } 59 | pluginFunction(mockApp, options) 60 | 61 | expect(createToastInstanceSpy).toHaveBeenCalledWith({ 62 | eventBus: globalEventBus, 63 | timeout: 1000, 64 | }) 65 | expect(mockApp.provide).toHaveBeenCalledWith( 66 | useToast.toastInjectionKey, 67 | toast 68 | ) 69 | }) 70 | 71 | it("provides custom eventBus if provided", () => { 72 | const toast = {} as ToastInterface 73 | const createToastInstanceSpy = jest 74 | .spyOn(useToast, "createToastInstance") 75 | .mockImplementation(() => toast) 76 | 77 | const mockApp = { provide: jest.fn() } as unknown as App 78 | 79 | expect(createToastInstanceSpy).not.toHaveBeenCalled() 80 | 81 | const eventBus = new EventBus() 82 | const options: PluginOptions = { eventBus } 83 | pluginFunction(mockApp, options) 84 | 85 | expect(createToastInstanceSpy).toHaveBeenCalledWith({ 86 | eventBus, 87 | }) 88 | expect(mockApp.provide).toHaveBeenCalledWith( 89 | useToast.toastInjectionKey, 90 | toast 91 | ) 92 | }) 93 | 94 | it("does not share app context by default", () => { 95 | const createToastInstanceSpy = jest 96 | .spyOn(useToast, "createToastInstance") 97 | .mockImplementation() 98 | 99 | const mockApp = { provide: jest.fn() } as unknown as App 100 | 101 | expect(createToastInstanceSpy).not.toHaveBeenCalled() 102 | 103 | pluginFunction(mockApp) 104 | 105 | expect(createToastInstanceSpy).not.toHaveBeenCalledWith( 106 | expect.objectContaining({ shareAppContext: mockApp }) 107 | ) 108 | }) 109 | 110 | it("shares app context if required", () => { 111 | const createToastInstanceSpy = jest 112 | .spyOn(useToast, "createToastInstance") 113 | .mockImplementation() 114 | 115 | const mockApp = { provide: jest.fn() } as unknown as App 116 | 117 | expect(createToastInstanceSpy).not.toHaveBeenCalled() 118 | 119 | pluginFunction(mockApp, { shareAppContext: true }) 120 | 121 | expect(createToastInstanceSpy).toHaveBeenCalledWith( 122 | expect.objectContaining({ shareAppContext: mockApp }) 123 | ) 124 | }) 125 | }) 126 | }) 127 | -------------------------------------------------------------------------------- /src/scss/animations/_bounce.scss: -------------------------------------------------------------------------------- 1 | // Bounce Animation taken from https://github.com/fkhadra/react-toastify 2 | $trans-cubic-bezier: cubic-bezier(0.215, 0.61, 0.355, 1); 3 | @mixin timing-function { 4 | animation-timing-function: $trans-cubic-bezier; 5 | } 6 | 7 | @keyframes bounceInRight { 8 | from, 9 | 60%, 10 | 75%, 11 | 90%, 12 | to { 13 | @include timing-function; 14 | } 15 | from { 16 | opacity: 0; 17 | transform: translate3d(3000px, 0, 0); 18 | } 19 | 60% { 20 | opacity: 1; 21 | transform: translate3d(-25px, 0, 0); 22 | } 23 | 75% { 24 | transform: translate3d(10px, 0, 0); 25 | } 26 | 90% { 27 | transform: translate3d(-5px, 0, 0); 28 | } 29 | to { 30 | transform: none; 31 | } 32 | } 33 | 34 | @keyframes bounceOutRight { 35 | 40% { 36 | opacity: 1; 37 | transform: translate3d(-20px, 0, 0); 38 | } 39 | to { 40 | opacity: 0; 41 | transform: translate3d(1000px, 0, 0); 42 | } 43 | } 44 | 45 | @keyframes bounceInLeft { 46 | from, 47 | 60%, 48 | 75%, 49 | 90%, 50 | to { 51 | @include timing-function; 52 | } 53 | 0% { 54 | opacity: 0; 55 | transform: translate3d(-3000px, 0, 0); 56 | } 57 | 60% { 58 | opacity: 1; 59 | transform: translate3d(25px, 0, 0); 60 | } 61 | 75% { 62 | transform: translate3d(-10px, 0, 0); 63 | } 64 | 90% { 65 | transform: translate3d(5px, 0, 0); 66 | } 67 | to { 68 | transform: none; 69 | } 70 | } 71 | 72 | @keyframes bounceOutLeft { 73 | 20% { 74 | opacity: 1; 75 | transform: translate3d(20px, 0, 0); 76 | } 77 | to { 78 | opacity: 0; 79 | transform: translate3d(-2000px, 0, 0); 80 | } 81 | } 82 | 83 | @keyframes bounceInUp { 84 | from, 85 | 60%, 86 | 75%, 87 | 90%, 88 | to { 89 | @include timing-function; 90 | } 91 | from { 92 | opacity: 0; 93 | transform: translate3d(0, 3000px, 0); 94 | } 95 | 60% { 96 | opacity: 1; 97 | transform: translate3d(0, -20px, 0); 98 | } 99 | 75% { 100 | transform: translate3d(0, 10px, 0); 101 | } 102 | 90% { 103 | transform: translate3d(0, -5px, 0); 104 | } 105 | to { 106 | transform: translate3d(0, 0, 0); 107 | } 108 | } 109 | 110 | @keyframes bounceOutUp { 111 | 20% { 112 | transform: translate3d(0, -10px, 0); 113 | } 114 | 40%, 115 | 45% { 116 | opacity: 1; 117 | transform: translate3d(0, 20px, 0); 118 | } 119 | to { 120 | opacity: 0; 121 | transform: translate3d(0, -2000px, 0); 122 | } 123 | } 124 | 125 | @keyframes bounceInDown { 126 | from, 127 | 60%, 128 | 75%, 129 | 90%, 130 | to { 131 | @include timing-function; 132 | } 133 | 0% { 134 | opacity: 0; 135 | transform: translate3d(0, -3000px, 0); 136 | } 137 | 60% { 138 | opacity: 1; 139 | transform: translate3d(0, 25px, 0); 140 | } 141 | 75% { 142 | transform: translate3d(0, -10px, 0); 143 | } 144 | 90% { 145 | transform: translate3d(0, 5px, 0); 146 | } 147 | to { 148 | transform: none; 149 | } 150 | } 151 | 152 | @keyframes bounceOutDown { 153 | 20% { 154 | transform: translate3d(0, 10px, 0); 155 | } 156 | 40%, 157 | 45% { 158 | opacity: 1; 159 | transform: translate3d(0, -20px, 0); 160 | } 161 | to { 162 | opacity: 0; 163 | transform: translate3d(0, 2000px, 0); 164 | } 165 | } 166 | 167 | .#{$vt-namespace}__bounce-enter-active { 168 | &.top-left, 169 | &.bottom-left { 170 | animation-name: bounceInLeft; 171 | } 172 | &.top-right, 173 | &.bottom-right { 174 | animation-name: bounceInRight; 175 | } 176 | &.top-center { 177 | animation-name: bounceInDown; 178 | } 179 | &.bottom-center { 180 | animation-name: bounceInUp; 181 | } 182 | } 183 | 184 | .#{$vt-namespace}__bounce-leave-active:not(.disable-transition) { 185 | &.top-left, 186 | &.bottom-left { 187 | animation-name: bounceOutLeft; 188 | } 189 | &.top-right, 190 | &.bottom-right { 191 | animation-name: bounceOutRight; 192 | } 193 | &.top-center { 194 | animation-name: bounceOutUp; 195 | } 196 | &.bottom-center { 197 | animation-name: bounceOutDown; 198 | } 199 | } 200 | 201 | .#{$vt-namespace}__bounce-leave-active, 202 | .#{$vt-namespace}__bounce-enter-active { 203 | animation-duration: 750ms; 204 | animation-fill-mode: both; 205 | } 206 | 207 | .#{$vt-namespace}__bounce-move { 208 | transition-timing-function: ease-in-out; 209 | transition-property: all; 210 | transition-duration: 400ms; 211 | } 212 | -------------------------------------------------------------------------------- /src/scss/animations/_slideBlurred.scss: -------------------------------------------------------------------------------- 1 | $trans-cubic-bezier: cubic-bezier(0.215, 0.61, 0.355, 1); 2 | @mixin timing-function { 3 | animation-timing-function: $trans-cubic-bezier; 4 | } 5 | 6 | /* ---------------------------------------------- 7 | * Modified version from Animista 8 | * Animista is Licensed under FreeBSD License. 9 | * See http://animista.net/license for more info. 10 | * w: http://animista.net, t: @cssanimista 11 | * ---------------------------------------------- */ 12 | 13 | @keyframes slideInBlurredLeft { 14 | 0% { 15 | transform: translateX(-1000px) scaleX(2.5) scaleY(0.2); 16 | transform-origin: 100% 50%; 17 | filter: blur(40px); 18 | opacity: 0; 19 | } 20 | 100% { 21 | transform: translateX(0) scaleY(1) scaleX(1); 22 | transform-origin: 50% 50%; 23 | filter: blur(0); 24 | opacity: 1; 25 | } 26 | } 27 | 28 | @keyframes slideInBlurredTop { 29 | 0% { 30 | transform: translateY(-1000px) scaleY(2.5) scaleX(0.2); 31 | transform-origin: 50% 0%; 32 | filter: blur(240px); 33 | opacity: 0; 34 | } 35 | 100% { 36 | transform: translateY(0) scaleY(1) scaleX(1); 37 | transform-origin: 50% 50%; 38 | filter: blur(0); 39 | opacity: 1; 40 | } 41 | } 42 | 43 | @keyframes slideInBlurredRight { 44 | 0% { 45 | transform: translateX(1000px) scaleX(2.5) scaleY(0.2); 46 | transform-origin: 0% 50%; 47 | filter: blur(40px); 48 | opacity: 0; 49 | } 50 | 100% { 51 | transform: translateX(0) scaleY(1) scaleX(1); 52 | transform-origin: 50% 50%; 53 | filter: blur(0); 54 | opacity: 1; 55 | } 56 | } 57 | 58 | @keyframes slideInBlurredBottom { 59 | 0% { 60 | transform: translateY(1000px) scaleY(2.5) scaleX(0.2); 61 | transform-origin: 50% 100%; 62 | filter: blur(240px); 63 | opacity: 0; 64 | } 65 | 100% { 66 | transform: translateY(0) scaleY(1) scaleX(1); 67 | transform-origin: 50% 50%; 68 | filter: blur(0); 69 | opacity: 1; 70 | } 71 | } 72 | 73 | @keyframes slideOutBlurredTop { 74 | 0% { 75 | transform: translateY(0) scaleY(1) scaleX(1); 76 | transform-origin: 50% 0%; 77 | filter: blur(0); 78 | opacity: 1; 79 | } 80 | 100% { 81 | transform: translateY(-1000px) scaleY(2) scaleX(0.2); 82 | transform-origin: 50% 0%; 83 | filter: blur(240px); 84 | opacity: 0; 85 | } 86 | } 87 | 88 | @keyframes slideOutBlurredBottom { 89 | 0% { 90 | transform: translateY(0) scaleY(1) scaleX(1); 91 | transform-origin: 50% 50%; 92 | filter: blur(0); 93 | opacity: 1; 94 | } 95 | 100% { 96 | transform: translateY(1000px) scaleY(2) scaleX(0.2); 97 | transform-origin: 50% 100%; 98 | filter: blur(240px); 99 | opacity: 0; 100 | } 101 | } 102 | 103 | @keyframes slideOutBlurredLeft { 104 | 0% { 105 | transform: translateX(0) scaleY(1) scaleX(1); 106 | transform-origin: 50% 50%; 107 | filter: blur(0); 108 | opacity: 1; 109 | } 110 | 100% { 111 | transform: translateX(-1000px) scaleX(2) scaleY(0.2); 112 | transform-origin: 100% 50%; 113 | filter: blur(40px); 114 | opacity: 0; 115 | } 116 | } 117 | 118 | @keyframes slideOutBlurredRight { 119 | 0% { 120 | transform: translateX(0) scaleY(1) scaleX(1); 121 | transform-origin: 50% 50%; 122 | filter: blur(0); 123 | opacity: 1; 124 | } 125 | 100% { 126 | transform: translateX(1000px) scaleX(2) scaleY(0.2); 127 | transform-origin: 0% 50%; 128 | filter: blur(40px); 129 | opacity: 0; 130 | } 131 | } 132 | 133 | .#{$vt-namespace}__slideBlurred-enter-active { 134 | &.top-left, 135 | &.bottom-left { 136 | animation-name: slideInBlurredLeft; 137 | } 138 | &.top-right, 139 | &.bottom-right { 140 | animation-name: slideInBlurredRight; 141 | } 142 | &.top-center { 143 | animation-name: slideInBlurredTop; 144 | } 145 | &.bottom-center { 146 | animation-name: slideInBlurredBottom; 147 | } 148 | } 149 | 150 | .#{$vt-namespace}__slideBlurred-leave-active:not(.disable-transition) { 151 | &.top-left, 152 | &.bottom-left { 153 | animation-name: slideOutBlurredLeft; 154 | } 155 | &.top-right, 156 | &.bottom-right { 157 | animation-name: slideOutBlurredRight; 158 | } 159 | &.top-center { 160 | animation-name: slideOutBlurredTop; 161 | } 162 | &.bottom-center { 163 | animation-name: slideOutBlurredBottom; 164 | } 165 | } 166 | 167 | .#{$vt-namespace}__slideBlurred-leave-active, 168 | .#{$vt-namespace}__slideBlurred-enter-active { 169 | animation-duration: 750ms; 170 | animation-fill-mode: both; 171 | } 172 | 173 | .#{$vt-namespace}__slideBlurred-move { 174 | transition-timing-function: ease-in-out; 175 | transition-property: all; 176 | transition-duration: 400ms; 177 | } 178 | -------------------------------------------------------------------------------- /tests/unit/components/__snapshots__/VtIcon.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`VtIcon renders custom components renders custom component as icon 1`] = ` 4 |
7 | Example component 8 |
9 | `; 10 | 11 | exports[`VtIcon renders custom components renders custom icon children 1`] = ` 12 | 15 | my child 16 | 17 | `; 18 | 19 | exports[`VtIcon renders custom components renders custom icon children if empty 1`] = ` 20 | 23 | 24 | 25 | `; 26 | 27 | exports[`VtIcon renders custom components renders custom icon class string 1`] = ` 28 | 31 | 32 | 33 | `; 34 | 35 | exports[`VtIcon renders custom components renders custom icon tag 1`] = ` 36 | 39 | 40 | 41 | `; 42 | 43 | exports[`VtIcon renders custom components renders regular icon if true 1`] = ` 44 | 57 | `; 58 | 59 | exports[`VtIcon renders custom components renders string as class 1`] = ` 60 | 63 | 64 | 65 | `; 66 | 67 | exports[`VtIcon snapshots matches error icon 1`] = ` 68 | 81 | `; 82 | 83 | exports[`VtIcon snapshots matches info icon 1`] = ` 84 | 97 | `; 98 | 99 | exports[`VtIcon snapshots matches success icon 1`] = ` 100 | 113 | `; 114 | 115 | exports[`VtIcon snapshots matches warning icon 1`] = ` 116 | 129 | `; 130 | -------------------------------------------------------------------------------- /src/ts/utils.ts: -------------------------------------------------------------------------------- 1 | import { Component, defineComponent, toRaw, unref } from "vue" 2 | 3 | import type { BasePluginOptions } from "../types/plugin" 4 | import type { 5 | ToastComponent, 6 | ToastContent, 7 | RenderableToastContent, 8 | } from "../types/toast" 9 | import type { ToastContainerOptions } from "../types/toastContainer" 10 | 11 | interface DictionaryLike { 12 | [index: string]: unknown 13 | } 14 | 15 | // eslint-disable-next-line @typescript-eslint/ban-types 16 | const isFunction = (value: unknown): value is Function => 17 | typeof value === "function" 18 | 19 | const isString = (value: unknown): value is string => typeof value === "string" 20 | 21 | const isNonEmptyString = (value: unknown): value is string => 22 | isString(value) && value.trim().length > 0 23 | 24 | const isNumber = (value: unknown): value is number => typeof value === "number" 25 | 26 | const isUndefined = (value: unknown): value is undefined => 27 | typeof value === "undefined" 28 | 29 | const isObject = (value: unknown): value is DictionaryLike => 30 | typeof value === "object" && value !== null 31 | 32 | const isJSX = (obj: unknown): obj is JSX.Element => 33 | hasProp(obj, "tag") && isNonEmptyString(obj.tag) 34 | 35 | const isTouchEvent = (event: Event): event is TouchEvent => 36 | window.TouchEvent && event instanceof TouchEvent 37 | 38 | const isToastComponent = (obj: unknown): obj is ToastComponent => 39 | hasProp(obj, "component") && isToastContent(obj.component) 40 | 41 | const isVueComponent = (c: unknown): c is Component => 42 | isFunction(c) || isObject(c) 43 | 44 | const isToastContent = (obj: unknown): obj is ToastContent => 45 | // Ignore undefined 46 | !isUndefined(obj) && 47 | // Is a string 48 | (isString(obj) || 49 | // Regular Vue component 50 | isVueComponent(obj) || 51 | // Nested object 52 | isToastComponent(obj)) 53 | 54 | const isDOMRect = (obj: unknown): obj is DOMRect => 55 | isObject(obj) && 56 | ["height", "width", "right", "left", "top", "bottom"].every(p => 57 | isNumber(obj[p]) 58 | ) 59 | 60 | const hasProp = ( 61 | obj: O, 62 | propKey: K 63 | ): obj is O & { [key in K]: unknown } => 64 | (isObject(obj) || isFunction(obj)) && propKey in obj 65 | 66 | const getProp = ( 67 | obj: O, 68 | propKey: K, 69 | fallback: D 70 | ): K extends keyof O ? O[K] : D => 71 | (hasProp(obj, propKey) ? obj[propKey] : fallback) as K extends keyof O 72 | ? O[K] 73 | : D 74 | 75 | /** 76 | * ID generator 77 | */ 78 | const getId = ( 79 | i => () => 80 | i++ 81 | )(0) 82 | 83 | function getX(event: MouseEvent | TouchEvent) { 84 | return isTouchEvent(event) ? event.targetTouches[0].clientX : event.clientX 85 | } 86 | 87 | function getY(event: MouseEvent | TouchEvent) { 88 | return isTouchEvent(event) ? event.targetTouches[0].clientY : event.clientY 89 | } 90 | 91 | const removeElement = (el: Element) => { 92 | if (!isUndefined(el.remove)) { 93 | el.remove() 94 | } else if (el.parentNode) { 95 | el.parentNode.removeChild(el) 96 | } 97 | } 98 | 99 | const getVueComponentFromObj = (obj: ToastContent): RenderableToastContent => { 100 | if (isToastComponent(obj)) { 101 | // Recurse if component prop 102 | return getVueComponentFromObj(obj.component) 103 | } 104 | if (isJSX(obj)) { 105 | // Create render function for JSX 106 | return defineComponent({ 107 | render() { 108 | return obj 109 | }, 110 | }) 111 | } 112 | // Return regular string or raw object 113 | return typeof obj === "string" ? obj : toRaw(unref(obj)) 114 | } 115 | 116 | const normalizeToastComponent = (obj: ToastContent): ToastContent => { 117 | if (typeof obj === "string") { 118 | return obj 119 | } 120 | const props = hasProp(obj, "props") && isObject(obj.props) ? obj.props : {} 121 | const listeners = ( 122 | hasProp(obj, "listeners") && isObject(obj.listeners) ? obj.listeners : {} 123 | ) as ToastComponent["listeners"] 124 | return { component: getVueComponentFromObj(obj), props, listeners } 125 | } 126 | 127 | const isBrowser = () => typeof window !== "undefined" 128 | 129 | const asContainerProps = ( 130 | options: BasePluginOptions 131 | ): ToastContainerOptions => { 132 | const { 133 | position, 134 | container, 135 | newestOnTop, 136 | maxToasts, 137 | transition, 138 | toastDefaults, 139 | eventBus, 140 | filterBeforeCreate, 141 | filterToasts, 142 | containerClassName, 143 | ...defaultToastProps 144 | } = options 145 | const containerProps = { 146 | position, 147 | container, 148 | newestOnTop, 149 | maxToasts, 150 | transition, 151 | toastDefaults, 152 | eventBus, 153 | filterBeforeCreate, 154 | filterToasts, 155 | containerClassName, 156 | defaultToastProps, 157 | } 158 | const keys = Object.keys(containerProps) as (keyof ToastContainerOptions)[] 159 | keys.forEach( 160 | key => 161 | typeof containerProps[key] === "undefined" && delete containerProps[key] 162 | ) 163 | return containerProps 164 | } 165 | 166 | export { 167 | getId, 168 | getX, 169 | getY, 170 | removeElement, 171 | isString, 172 | isNonEmptyString, 173 | isToastContent, 174 | getVueComponentFromObj, 175 | normalizeToastComponent, 176 | hasProp, 177 | isUndefined, 178 | isDOMRect, 179 | isFunction, 180 | isBrowser, 181 | getProp, 182 | asContainerProps, 183 | } 184 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, religion, or sexual identity 11 | and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the 27 | overall community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or 32 | advances of any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email 36 | address, without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official e-mail address, 57 | posting via an official social media account, or acting as an appointed representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | gustavomaronato@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /src/components/VtToast.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 154 | -------------------------------------------------------------------------------- /tests/unit/ts/interface.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable vue/one-component-per-file */ 2 | import * as vue from "vue" 3 | import { App, nextTick } from "vue" 4 | 5 | import { isFunction } from "@vue/shared" 6 | 7 | import VtToastContainer from "../../../src/components/VtToastContainer.vue" 8 | import { EventBus } from "../../../src/index" 9 | import { EVENTS, TYPE } from "../../../src/ts/constants" 10 | import { buildInterface } from "../../../src/ts/interface" 11 | 12 | import type { PluginOptions } from "../../../src/types/plugin" 13 | 14 | describe("interface", () => { 15 | beforeEach(() => { 16 | jest.resetAllMocks() 17 | jest.restoreAllMocks() 18 | }) 19 | 20 | describe("buildInterface", () => { 21 | let eventBus: EventBus 22 | let eventsEmmited: Record 23 | 24 | beforeEach(() => { 25 | eventBus = new EventBus() 26 | eventsEmmited = Object.values(EVENTS).reduce((agg, eventName) => { 27 | const handler = jest.fn() 28 | eventBus.on(eventName, handler) 29 | return { ...agg, [eventName]: handler } 30 | }, {} as { [eventName in EVENTS]: jest.Mock }) 31 | }) 32 | 33 | it("creates valid interface by default", async () => { 34 | const mockApp = { mount: jest.fn() } as unknown as App 35 | jest.spyOn(vue, "createApp").mockImplementation(() => mockApp) 36 | const toast = buildInterface() 37 | await nextTick() 38 | 39 | expect(isFunction(toast)).toBe(true) 40 | expect(isFunction(toast.info)).toBe(true) 41 | expect(isFunction(toast.success)).toBe(true) 42 | expect(isFunction(toast.warning)).toBe(true) 43 | expect(isFunction(toast.error)).toBe(true) 44 | expect(isFunction(toast.dismiss)).toBe(true) 45 | expect(isFunction(toast.clear)).toBe(true) 46 | expect(isFunction(toast.update)).toBe(true) 47 | expect(isFunction(toast.updateDefaults)).toBe(true) 48 | }) 49 | 50 | it("uses provided eventBus", async () => { 51 | const mockApp = { mount: jest.fn() } as unknown as App 52 | const createAppSpy = jest 53 | .spyOn(vue, "createApp") 54 | .mockImplementation(() => mockApp) 55 | const toast = buildInterface({ eventBus }) 56 | 57 | expect(eventsEmmited.add).not.toHaveBeenCalled() 58 | 59 | const content = "hello" 60 | toast.success(content) 61 | 62 | expect(eventsEmmited.add).toHaveBeenCalledWith({ 63 | id: expect.any(Number), 64 | type: TYPE.SUCCESS, 65 | content, 66 | }) 67 | 68 | await nextTick() 69 | 70 | expect(createAppSpy).toHaveBeenCalledWith(VtToastContainer, { 71 | eventBus, 72 | defaultToastProps: {}, 73 | }) 74 | }) 75 | 76 | it("mounts container by default", async () => { 77 | const mockApp = { mount: jest.fn() } as unknown as App 78 | const createAppSpy = jest 79 | .spyOn(vue, "createApp") 80 | .mockImplementation(() => mockApp) 81 | 82 | buildInterface() 83 | 84 | expect(mockApp.mount).not.toHaveBeenCalled() 85 | expect(createAppSpy).not.toHaveBeenCalled() 86 | await nextTick() 87 | 88 | expect(createAppSpy).toHaveBeenCalledWith( 89 | VtToastContainer, 90 | expect.objectContaining({ 91 | eventBus: expect.any(EventBus), 92 | }) 93 | ) 94 | expect(mockApp.mount).toHaveBeenCalled() 95 | }) 96 | 97 | it("passes props to mounted container", async () => { 98 | const mockApp = { mount: jest.fn() } as unknown as App 99 | const createAppSpy = jest 100 | .spyOn(vue, "createApp") 101 | .mockImplementation(() => mockApp) 102 | 103 | const options: PluginOptions = { 104 | timeout: 1000, 105 | bodyClassName: "myclass", 106 | } 107 | buildInterface(options) 108 | await nextTick() 109 | 110 | expect(createAppSpy).toHaveBeenCalledWith(VtToastContainer, { 111 | eventBus: expect.any(EventBus), 112 | defaultToastProps: { ...options }, 113 | }) 114 | expect(mockApp.mount).toHaveBeenCalled() 115 | }) 116 | 117 | it("calls onMounted", async () => { 118 | const component = {} 119 | const mockApp = { mount: jest.fn(() => component) } as unknown as App 120 | jest.spyOn(vue, "createApp").mockImplementation(() => mockApp) 121 | 122 | const onMounted = jest.fn() 123 | buildInterface({ onMounted }) 124 | 125 | expect(onMounted).not.toHaveBeenCalled() 126 | await nextTick() 127 | 128 | expect(onMounted).toHaveBeenCalledWith(component, mockApp) 129 | }) 130 | 131 | it("shares app context", async () => { 132 | const mockApp = { 133 | mount: jest.fn(), 134 | _context: {}, 135 | config: {}, 136 | } as unknown as App 137 | jest.spyOn(vue, "createApp").mockImplementation(() => mockApp) 138 | 139 | const userApp = { 140 | _context: { 141 | components: "components", 142 | directives: "directives", 143 | mixins: "mixins", 144 | provides: "provides", 145 | }, 146 | config: { 147 | globalProperties: "globalProperties", 148 | }, 149 | } as unknown as App 150 | buildInterface({ shareAppContext: userApp }) 151 | 152 | await nextTick() 153 | 154 | expect(mockApp._context.components).toBe(userApp._context.components) 155 | expect(mockApp._context.directives).toBe(userApp._context.directives) 156 | expect(mockApp._context.mixins).toBe(userApp._context.mixins) 157 | expect(mockApp._context.provides).toBe(userApp._context.provides) 158 | expect(mockApp.config.globalProperties).toBe( 159 | userApp.config.globalProperties 160 | ) 161 | }) 162 | }) 163 | }) 164 | -------------------------------------------------------------------------------- /tests/unit/components/VtIcon.spec.ts: -------------------------------------------------------------------------------- 1 | import { markRaw } from "vue" 2 | 3 | import { mount } from "@vue/test-utils" 4 | 5 | import VtErrorIcon from "../../../src/components/icons/VtErrorIcon.vue" 6 | import VtInfoIcon from "../../../src/components/icons/VtInfoIcon.vue" 7 | import VtSuccessIcon from "../../../src/components/icons/VtSuccessIcon.vue" 8 | import VtWarningIcon from "../../../src/components/icons/VtWarningIcon.vue" 9 | import VtIcon from "../../../src/components/VtIcon.vue" 10 | import { TYPE, VT_NAMESPACE } from "../../../src/ts/constants" 11 | import Simple from "../../utils/components/Simple.vue" 12 | 13 | describe("VtIcon", () => { 14 | describe("snapshots", () => { 15 | it("matches success icon", () => { 16 | const wrapper = mount(VtIcon, { 17 | props: { 18 | type: TYPE.SUCCESS, 19 | }, 20 | }) 21 | expect(wrapper.element).toMatchSnapshot() 22 | }) 23 | it("matches info icon", () => { 24 | const wrapper = mount(VtIcon, { 25 | props: { 26 | type: TYPE.INFO, 27 | }, 28 | }) 29 | expect(wrapper.element).toMatchSnapshot() 30 | }) 31 | it("matches warning icon", () => { 32 | const wrapper = mount(VtIcon, { 33 | props: { 34 | type: TYPE.WARNING, 35 | }, 36 | }) 37 | expect(wrapper.element).toMatchSnapshot() 38 | }) 39 | it("matches error icon", () => { 40 | const wrapper = mount(VtIcon, { 41 | props: { 42 | type: TYPE.ERROR, 43 | }, 44 | }) 45 | expect(wrapper.element).toMatchSnapshot() 46 | }) 47 | }) 48 | describe("renders default icons", () => { 49 | it("renders success", () => { 50 | const wrapper = mount(VtIcon, { 51 | props: { 52 | type: TYPE.SUCCESS, 53 | }, 54 | }) 55 | expect(wrapper.findComponent(VtSuccessIcon).exists()).toBeTruthy() 56 | expect(wrapper.classes()).toContain(`${VT_NAMESPACE}__icon`) 57 | }) 58 | it("renders info", () => { 59 | const wrapper = mount(VtIcon, { 60 | props: { 61 | type: TYPE.INFO, 62 | }, 63 | }) 64 | expect(wrapper.findComponent(VtInfoIcon).exists()).toBeTruthy() 65 | expect(wrapper.classes()).toContain(`${VT_NAMESPACE}__icon`) 66 | }) 67 | it("renders warning", () => { 68 | const wrapper = mount(VtIcon, { 69 | props: { 70 | type: TYPE.WARNING, 71 | }, 72 | }) 73 | expect(wrapper.findComponent(VtWarningIcon).exists()).toBeTruthy() 74 | expect(wrapper.classes()).toContain(`${VT_NAMESPACE}__icon`) 75 | }) 76 | it("renders error", () => { 77 | const wrapper = mount(VtIcon, { 78 | props: { 79 | type: TYPE.ERROR, 80 | }, 81 | }) 82 | expect(wrapper.findComponent(VtErrorIcon).exists()).toBeTruthy() 83 | expect(wrapper.classes()).toContain(`${VT_NAMESPACE}__icon`) 84 | }) 85 | }) 86 | describe("renders custom components", () => { 87 | it("renders regular icon if true", () => { 88 | const wrapper = mount(VtIcon, { 89 | props: { 90 | type: TYPE.SUCCESS, 91 | customIcon: true, 92 | }, 93 | }) 94 | expect(wrapper.findComponent(VtSuccessIcon).exists()).toBeTruthy() 95 | expect(wrapper.element).toMatchSnapshot() 96 | }) 97 | it("renders string as class", () => { 98 | const wrapper = mount(VtIcon, { 99 | props: { 100 | customIcon: "myString", 101 | }, 102 | }) 103 | expect(wrapper.find("i").classes()).toContain("myString") 104 | expect(wrapper.element).toMatchSnapshot() 105 | }) 106 | it("renders custom component as icon", () => { 107 | const wrapper = mount(VtIcon, { 108 | props: { 109 | customIcon: markRaw(Simple), 110 | }, 111 | }) 112 | expect(wrapper.findComponent(Simple).exists()).toBeTruthy() 113 | expect(wrapper.findComponent(Simple).classes()).toContain( 114 | `${VT_NAMESPACE}__icon` 115 | ) 116 | expect(wrapper.element).toMatchSnapshot() 117 | }) 118 | it("renders custom icon class string", () => { 119 | const wrapper = mount(VtIcon, { 120 | props: { 121 | customIcon: { iconClass: "my-class" }, 122 | }, 123 | }) 124 | expect(wrapper.find("i").exists()).toBeTruthy() 125 | expect(wrapper.find("i").classes()).toContain(`${VT_NAMESPACE}__icon`) 126 | expect(wrapper.find("i").classes()).toContain("my-class") 127 | expect(wrapper.element).toMatchSnapshot() 128 | }) 129 | it("renders custom icon tag", () => { 130 | const wrapper = mount(VtIcon, { 131 | props: { 132 | customIcon: { iconClass: "my-class", iconTag: "span" }, 133 | }, 134 | }) 135 | expect(wrapper.find("i").exists()).toBeFalsy() 136 | expect(wrapper.find("span").exists()).toBeTruthy() 137 | expect(wrapper.find("span").classes()).toContain("my-class") 138 | expect(wrapper.element).toMatchSnapshot() 139 | }) 140 | it("renders custom icon children", () => { 141 | const wrapper = mount(VtIcon, { 142 | props: { 143 | customIcon: { iconClass: "my-class", iconChildren: "my child" }, 144 | }, 145 | }) 146 | expect(wrapper.find("i").exists()).toBeTruthy() 147 | expect(wrapper.text()).toBe("my child") 148 | expect(wrapper.element).toMatchSnapshot() 149 | }) 150 | it("renders custom icon children if empty", () => { 151 | const wrapper = mount(VtIcon, { 152 | props: { 153 | customIcon: { iconClass: "my-class", iconChildren: "" }, 154 | }, 155 | }) 156 | expect(wrapper.find("i").exists()).toBeTruthy() 157 | expect(wrapper.text()).toBe("") 158 | expect(wrapper.element).toMatchSnapshot() 159 | }) 160 | }) 161 | }) 162 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | First off, thanks for taking the time to contribute! 4 | 5 | It is truly appreciated 😊 6 | 7 | Please note we have a [code of conduct](./CODE_OF_CONDUCT.md). Follow it in all your interactions with the project. 8 | 9 | - [How to contribute](#how-to-contribute) 10 | - [General Guidelines](#general-guidelines) 11 | - [Project structure](#project-structure) 12 | - [`src/`](#src) 13 | - [`tests/`](#tests) 14 | - [`demo/`](#demo) 15 | - [Developing](#developing) 16 | - [Pre-requisites](#pre-requisites) 17 | - [Install](#install) 18 | - [Starting the demo page](#starting-the-demo-page) 19 | - [Running tests](#running-tests) 20 | - [Writing tests](#writing-tests) 21 | - [License](#license) 22 | 23 | ## General Guidelines 24 | 25 | - Before creating a new PR, please discuss the change via an issue 26 | - If adding a new feature, write the corresponding tests 27 | - Ensure that nothing is broken. You can use the demo page for that 28 | - If applicable, update the documentation 29 | - When solving a bug, please provide the steps to reproduce it. Codesandbox is your best friend for that 30 | 31 | 32 | ## Project structure 33 | 34 | This project has 6 main folders: 35 | 36 | - `build/`: Production Rollup build configuration 37 | - `examples/`: Example projects 38 | - `demo/`: Demo page's code 39 | - `src/`: Vue Toastification's code 40 | - `tests/`: All of the tests 41 | 42 | The main folders you'll probably be working with will be `src/`, `tests/` and `demo/`. 43 | 44 | ### `src/` 45 | These are all the files used by the plugin. Its folders and files are separated by type: 46 | 47 | - `components/`: Plugin's Vue components. These are the Toast itself, close button, icon, progress bar, etc 48 | - `scss/`: All of the plugin's styling files are here 49 | - `ts/`: Typescript files containing constants and other helper methods used across the components 50 | - `types/`: Main typescript types and interfaces 51 | - `index.ts`: Plugin entry point 52 | 53 | ### `tests/` 54 | Inside the `tests/` folder you'll find some test utilities under `utils/` and the unit tests under `unit/`. All of the tests are separated by the files they are testing, under the same folder structure as in `src/`. 55 | 56 | We use [Jest](https://github.com/facebook/jest) and [Vue Test Utils](https://vue-test-utils.vuejs.org/) for testing. 57 | 58 | If you've never written tests for Vue, you may find this [guide](https://lmiller1990.github.io/vue-testing-handbook/) helpful. You may also check out our [testing guide](#writing-tests) to learn how to write effective tests for this plugin. 59 | 60 | 61 | ### `demo/` 62 | This folder contains the code for the demo. 63 | 64 | ## Developing 65 | 66 | Developing a plugin is a little different than developing a website for there is no direct way to try it out. Because of that, the best way to test your changes when developing is by starting up the demo page locally. 67 | 68 | ### Pre-requisites 69 | 70 | - *Node 10 or superior* 71 | - *Yarn* 72 | 73 | ### Install 74 | 75 | Clone the repository and create a local branch: 76 | 77 | ```sh 78 | git clone https://github.com/Maronato/vue-toastification.git 79 | cd vue-toastification 80 | 81 | git checkout -b my-branch 82 | ``` 83 | 84 | Install dependencies: 85 | 86 | ```sh 87 | yarn install 88 | ``` 89 | 90 | ### Starting the demo page 91 | 92 | To start the demo page, run 93 | ```sh 94 | yarn dev 95 | ``` 96 | You can now play with it on [`localhost:8080`](http://localhost:8080) and see your changes being hot reloaded. 97 | 98 | 99 | ### Running tests 100 | To run all tests: 101 | ```sh 102 | yarn test 103 | ``` 104 | Or to watch for changes and only run tests related to changed files: 105 | ```sh 106 | yarn test:watch 107 | ``` 108 | 109 | Remember that automated test coverage may not catch all of your feature's intricacies. Write thorough tests, not just tests that reach 100% codecov. 110 | 111 | 112 | ## Writing tests 113 | If your changes are related to anything **but** `VtToastContainer`, they may be treated as regular Vue components or Typescript code during tests. 114 | 115 | If you make changes to the UI, it'll probably break some [snapshots](https://jestjs.io/docs/en/snapshot-testing). Make sure that all logic tests pass before overwriting snapshots. 116 | 117 | If you plan on changing behavior related to `VtToastContainer` or plugin initialization, you'll face issues because of the way the plugin injects the container onto the page. 118 | 119 | To solve that, the `loadPlugin` utility was created. You may import it from `tests/utils/plugin` and use it to simulate a `app.use(Toast)` call. 120 | 121 | Each of the position wrappers (`topLeft`, etc) also have a `getToasts` method that returns a [WrapperArray](https://vue-test-utils.vuejs.org/api/wrapper-array/) or its toasts. 122 | 123 | Example usage: 124 | ```ts 125 | import { ToastOptionsAndRequiredContent } from "src/types"; 126 | import { POSITION } from "src/ts/constants"; 127 | import loadPlugin from "tests/utils/plugin"; 128 | 129 | describe("test plugin", () => { 130 | it("adds toast", async () => { 131 | // Load plugin with default value for position 132 | const options = { position: POSITION.BOTTOM_LEFT }; 133 | const { 134 | toastInterface, 135 | bottomLeft, 136 | containerWrapper 137 | } = loadPlugin(options); 138 | 139 | // Get way to access list of toasts inside containerWrapper 140 | const vm = (containerWrapper.vm as unknown) as { 141 | toastArray: ToastOptionsAndRequiredContent[] 142 | } 143 | 144 | // Initially there should be no toasts 145 | expect(vm.toastArray.length).toBe(0); 146 | expect(bottomLeft.getToasts().length).toBe(0); 147 | 148 | // Create a toast 149 | const content = "I'm a toast"; 150 | toastInterface(content); 151 | 152 | // Wait for render 153 | await containerWrapper.vm.$nextTick(); 154 | 155 | // Toast should be created with correct content 156 | expect(vm.toastArray.length).toBe(1); 157 | expect(bottomLeft.getToasts().length).toBe(1); 158 | expect(bottomLeft.getToasts().at(0).props().content).toBe(content); 159 | }); 160 | }); 161 | ``` 162 | 163 | ## License 164 | By contributing, you agree that your contributions will be licensed under its MIT License. -------------------------------------------------------------------------------- /src/ts/interface.ts: -------------------------------------------------------------------------------- 1 | import { createApp, nextTick } from "vue" 2 | 3 | import ToastContainer from "../components/VtToastContainer.vue" 4 | 5 | import type { ToastID } from "../types/common" 6 | import type { BasePluginOptions, PluginOptions } from "../types/plugin" 7 | import type { 8 | ToastContent, 9 | ToastOptions, 10 | ToastOptionsAndContent, 11 | } from "../types/toast" 12 | 13 | import { TYPE, EVENTS } from "./constants" 14 | import { EventBus, EventBusInterface } from "./eventBus" 15 | import { asContainerProps, getId, isUndefined } from "./utils" 16 | 17 | /** 18 | * Display a toast 19 | */ 20 | interface ToastMethod { 21 | /** 22 | * @param content Toast content. 23 | * 24 | * Can be a string, JSX or a custom component passed directly 25 | * 26 | * To provide props and listeners to the custom component, you 27 | * do so by providing an object with the following shape: 28 | * 29 | * ```ts 30 | * { 31 | * component: JSX | VueComponent 32 | * props: Record 33 | * listeners: Record 34 | * } 35 | * ``` 36 | * 37 | * for more details, see https://github.com/Maronato/vue-toastification#toast-content-object 38 | * 39 | * @param options Toast configuration 40 | * 41 | * For details, see: https://github.com/Maronato/vue-toastification#toast-options-object 42 | * 43 | * @returns ID of the created toast 44 | */ 45 | (content: ToastContent, options?: ToastOptions & { type?: T }): ToastID 46 | } 47 | 48 | interface DismissToast { 49 | /** 50 | * @param toastID ID of the toast to be dismissed 51 | */ 52 | (toastID: ToastID): void 53 | } 54 | 55 | interface ClearToasts { 56 | (): void 57 | } 58 | 59 | interface UpdateDefaults { 60 | /** 61 | * @param update Plugin options to update 62 | * 63 | * Accepts all* options provided during plugin 64 | * registration and updates them. 65 | * 66 | * For details, see https://github.com/Maronato/vue-toastification#updating-default-options 67 | */ 68 | (update: BasePluginOptions): void 69 | } 70 | 71 | interface UpdateToast { 72 | /** 73 | * @param toastID ID of the toast to update 74 | * @param update Object that may contain the content to update, or the options to merge 75 | * @param create If set to false, this method only updates existing toasts and does 76 | * nothing if the provided `toastID` does not exist 77 | */ 78 | ( 79 | toastID: ToastID, 80 | update: { content?: ToastContent; options?: ToastOptions }, 81 | create?: false 82 | ): void 83 | /** 84 | * @param toastID ID of the toast to create / update 85 | * @param update Object that must contain the toast content and may contain the options to merge 86 | * @param create If set to true, this method updates existing toasts or creates new toasts if 87 | * the provided `toastID` does not exist 88 | */ 89 | ( 90 | toastID: ToastID, 91 | update: { content: ToastContent; options?: ToastOptions }, 92 | create: true 93 | ): void 94 | } 95 | 96 | export interface ToastInterface extends ToastMethod { 97 | /** 98 | * Display a success toast 99 | */ 100 | success: ToastMethod 101 | /** 102 | * Display an info toast 103 | */ 104 | info: ToastMethod 105 | /** 106 | * Display a warning toast 107 | */ 108 | warning: ToastMethod 109 | /** 110 | * Display an error toast 111 | */ 112 | error: ToastMethod 113 | /** 114 | * Dismiss toast specified by an id 115 | */ 116 | dismiss: DismissToast 117 | /** 118 | * Update Toast 119 | */ 120 | update: UpdateToast 121 | /** 122 | * Clear all toasts 123 | */ 124 | clear: ClearToasts 125 | /** 126 | * Update Plugin Defaults 127 | */ 128 | updateDefaults: UpdateDefaults 129 | } 130 | 131 | /** 132 | * Creates and mounts the plugin app 133 | * @param options Plugin options passed during init 134 | */ 135 | function mountPlugin(options: PluginOptions) { 136 | const { shareAppContext, onMounted, ...basePluginOptions } = options 137 | 138 | const containerProps = asContainerProps(basePluginOptions) 139 | 140 | const app = createApp(ToastContainer, { 141 | ...containerProps, 142 | }) 143 | 144 | if (shareAppContext && shareAppContext !== true) { 145 | const userApp = shareAppContext 146 | app._context.components = userApp._context.components 147 | app._context.directives = userApp._context.directives 148 | app._context.mixins = userApp._context.mixins 149 | app._context.provides = userApp._context.provides 150 | app.config.globalProperties = userApp.config.globalProperties 151 | } 152 | 153 | const component = app.mount(document.createElement("div")) 154 | 155 | if (!isUndefined(onMounted)) { 156 | onMounted(component, app) 157 | } 158 | } 159 | 160 | const createInterface = (events: EventBusInterface): ToastInterface => { 161 | const createToastMethod = ( 162 | type: T 163 | ): ToastMethod => { 164 | const method: ToastMethod = (content, options) => { 165 | const props: ToastOptionsAndContent & { 166 | id: ToastID 167 | } = Object.assign({ id: getId(), type, content }, options) 168 | events.emit(EVENTS.ADD, props) 169 | return props.id 170 | } 171 | return method 172 | } 173 | 174 | const dismiss: DismissToast = toastID => events.emit(EVENTS.DISMISS, toastID) 175 | const clear: ClearToasts = () => events.emit(EVENTS.CLEAR, undefined) 176 | const updateDefaults: UpdateDefaults = update => 177 | events.emit(EVENTS.UPDATE_DEFAULTS, asContainerProps(update)) 178 | const update: UpdateToast = (toastID, update, create) => { 179 | const { content, options } = update 180 | events.emit(EVENTS.UPDATE, { 181 | id: toastID, 182 | create: create || false, 183 | options: { ...options, content: content as ToastContent }, 184 | }) 185 | } 186 | 187 | return Object.assign(createToastMethod(TYPE.DEFAULT), { 188 | success: createToastMethod(TYPE.SUCCESS), 189 | info: createToastMethod(TYPE.INFO), 190 | warning: createToastMethod(TYPE.WARNING), 191 | error: createToastMethod(TYPE.ERROR), 192 | dismiss, 193 | clear, 194 | update, 195 | updateDefaults, 196 | }) 197 | } 198 | 199 | export const buildInterface = ( 200 | globalOptions: PluginOptions = {}, 201 | mountContainer = true 202 | ): ToastInterface => { 203 | const options = { ...globalOptions } 204 | const events = (options.eventBus = options.eventBus || new EventBus()) 205 | 206 | if (mountContainer) { 207 | nextTick(() => mountPlugin(options)) 208 | } 209 | return createInterface(events) 210 | } 211 | -------------------------------------------------------------------------------- /src/components/VtToastContainer.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 214 | 215 | 222 | -------------------------------------------------------------------------------- /tests/unit/ts/composables/useToast.spec.ts: -------------------------------------------------------------------------------- 1 | import { defineComponent, h } from "vue" 2 | 3 | import { mount } from "@vue/test-utils" 4 | 5 | import { EventBus, PluginOptions, ToastInterface } from "../../../../src" 6 | import * as useToast from "../../../../src/ts/composables/useToast" 7 | import { VT_NAMESPACE } from "../../../../src/ts/constants" 8 | import { globalEventBus } from "../../../../src/ts/eventBus" 9 | import * as interfaceModule from "../../../../src/ts/interface" 10 | import * as utils from "../../../../src/ts/utils" 11 | 12 | const consumerInjected = jest.fn() 13 | 14 | const Consumer = { 15 | setup() { 16 | const foo = useToast.useToast() 17 | consumerInjected(foo) 18 | return () => h("div", "hey") 19 | }, 20 | } 21 | 22 | const createProvider = (options?: PluginOptions) => 23 | defineComponent({ 24 | setup() { 25 | useToast.provideToast(options) 26 | return () => h(Consumer) 27 | }, 28 | }) 29 | 30 | const Provider = createProvider() 31 | 32 | describe("useToast", () => { 33 | beforeEach(() => { 34 | jest.resetAllMocks() 35 | jest.restoreAllMocks() 36 | }) 37 | 38 | describe("useToast", () => { 39 | it("returns existing toast interface if eventBus is provided", () => { 40 | const expected = {} as ToastInterface 41 | const createToastInstanceSpy = jest 42 | .spyOn(useToast, "createToastInstance") 43 | .mockImplementation(() => expected) 44 | 45 | expect(createToastInstanceSpy).not.toHaveBeenCalled() 46 | 47 | const eventBus = new EventBus() 48 | 49 | const actual = useToast.useToast(eventBus) 50 | 51 | expect(createToastInstanceSpy).toHaveBeenCalled() 52 | expect(actual).toBe(expected) 53 | }) 54 | 55 | it("returns global toast interface if not called inside setup()", () => { 56 | const expected = {} as ToastInterface 57 | const createToastInstanceSpy = jest 58 | .spyOn(useToast, "createToastInstance") 59 | .mockImplementation(() => expected) 60 | 61 | expect(createToastInstanceSpy).not.toHaveBeenCalled() 62 | 63 | const actual = useToast.useToast() 64 | 65 | expect(createToastInstanceSpy).toHaveBeenCalledWith(globalEventBus) 66 | expect(actual).toBe(expected) 67 | }) 68 | 69 | it("returns global toast interface if called in non-provided setup()", () => { 70 | const expected = {} as ToastInterface 71 | const createToastInstanceSpy = jest 72 | .spyOn(useToast, "createToastInstance") 73 | .mockImplementation(() => expected) 74 | 75 | expect(createToastInstanceSpy).not.toHaveBeenCalled() 76 | expect(consumerInjected).not.toHaveBeenCalled() 77 | 78 | mount(Consumer) 79 | 80 | expect(createToastInstanceSpy).toHaveBeenCalledWith(globalEventBus) 81 | expect(consumerInjected).toHaveBeenCalledWith(expected) 82 | }) 83 | 84 | it("returns provided toast interface if called in provided setup()", () => { 85 | const expected = {} as ToastInterface 86 | const createToastInstanceSpy = jest 87 | .spyOn(useToast, "createToastInstance") 88 | .mockImplementation(() => expected) 89 | 90 | expect(createToastInstanceSpy).not.toHaveBeenCalled() 91 | expect(consumerInjected).not.toHaveBeenCalled() 92 | 93 | const options: PluginOptions = { timeout: 1000 } 94 | mount(createProvider(options)) 95 | 96 | expect(createToastInstanceSpy).toHaveBeenCalledWith(options) 97 | expect(consumerInjected).toHaveBeenCalledWith(expected) 98 | }) 99 | }) 100 | 101 | describe("provideToast", () => { 102 | it("does nothing if not called inside setup()", () => { 103 | const createToastInstanceSpy = jest 104 | .spyOn(useToast, "createToastInstance") 105 | .mockImplementation() 106 | 107 | expect(createToastInstanceSpy).not.toHaveBeenCalled() 108 | useToast.provideToast() 109 | 110 | expect(createToastInstanceSpy).not.toHaveBeenCalled() 111 | }) 112 | 113 | it("provides default if called inside setup()", () => { 114 | const expected = {} as ToastInterface 115 | const createToastInstanceSpy = jest 116 | .spyOn(useToast, "createToastInstance") 117 | .mockImplementation(() => expected) 118 | 119 | expect(createToastInstanceSpy).not.toHaveBeenCalled() 120 | expect(consumerInjected).not.toHaveBeenCalled() 121 | 122 | mount(Provider) 123 | 124 | expect(createToastInstanceSpy).toHaveBeenCalledWith(undefined) 125 | expect(consumerInjected).toHaveBeenCalledWith(expected) 126 | }) 127 | 128 | it("provides with options if called inside setup()", () => { 129 | const expected = {} as ToastInterface 130 | const createToastInstanceSpy = jest 131 | .spyOn(useToast, "createToastInstance") 132 | .mockImplementation(() => expected) 133 | 134 | expect(createToastInstanceSpy).not.toHaveBeenCalled() 135 | expect(consumerInjected).not.toHaveBeenCalled() 136 | 137 | const options: PluginOptions = { timeout: 1000 } 138 | mount(createProvider(options)) 139 | 140 | expect(createToastInstanceSpy).toHaveBeenCalledWith(options) 141 | expect(consumerInjected).toHaveBeenCalledWith(expected) 142 | }) 143 | }) 144 | 145 | describe("createToastInstance", () => { 146 | it("uses mock interface if not in browser", () => { 147 | const isBrowserSpy = jest.spyOn(utils, "isBrowser") 148 | const consoleSpy = jest.spyOn(console, "warn").mockImplementation() 149 | isBrowserSpy.mockReturnValueOnce(false) 150 | 151 | const toast = useToast.createToastInstance() 152 | 153 | expect(consoleSpy).not.toHaveBeenCalled() 154 | toast("hey") 155 | toast.success("hey") 156 | expect(consoleSpy).toHaveBeenCalledTimes(2) 157 | expect(consoleSpy).toHaveBeenCalledWith( 158 | `[${VT_NAMESPACE}] This plugin does not support SSR!` 159 | ) 160 | }) 161 | 162 | it("builds interface using existing eventBus if provided", () => { 163 | const eventBus = new EventBus() 164 | const expected = {} as ToastInterface 165 | const buildInterfaceSpy = jest 166 | .spyOn(interfaceModule, "buildInterface") 167 | .mockImplementation(() => expected) 168 | 169 | expect(buildInterfaceSpy).not.toHaveBeenCalled() 170 | 171 | const actual = useToast.createToastInstance(eventBus) 172 | 173 | expect(buildInterfaceSpy).toHaveBeenCalledWith({ eventBus }, false) 174 | expect(actual).toBe(expected) 175 | }) 176 | 177 | it("builds new interface using options if provided", () => { 178 | const expected = {} as ToastInterface 179 | const buildInterfaceSpy = jest 180 | .spyOn(interfaceModule, "buildInterface") 181 | .mockImplementation(() => expected) 182 | 183 | expect(buildInterfaceSpy).not.toHaveBeenCalled() 184 | 185 | const options: PluginOptions = { timeout: 1000 } 186 | const actual = useToast.createToastInstance(options) 187 | 188 | expect(buildInterfaceSpy).toHaveBeenCalledWith(options, true) 189 | expect(actual).toBe(expected) 190 | }) 191 | 192 | it("builds default interface if no options are provided", () => { 193 | const expected = {} as ToastInterface 194 | const buildInterfaceSpy = jest 195 | .spyOn(interfaceModule, "buildInterface") 196 | .mockImplementation(() => expected) 197 | 198 | expect(buildInterfaceSpy).not.toHaveBeenCalled() 199 | 200 | const actual = useToast.createToastInstance() 201 | 202 | expect(buildInterfaceSpy).toHaveBeenCalledWith(undefined, true) 203 | expect(actual).toBe(expected) 204 | }) 205 | }) 206 | }) 207 | -------------------------------------------------------------------------------- /tests/unit/components/__snapshots__/VtToastContainer.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`VtToastContainer snapshots with classes 1`] = ` 4 |
5 | 6 |
7 | 10 | 11 | 12 | 13 |
14 |
15 | 18 | 19 | 20 | 21 |
22 |
23 | 26 | 27 | 28 | 29 |
30 |
31 | 34 | 35 | 36 | 37 |
38 |
39 | 42 | 43 | 44 | 45 |
46 |
47 | 50 | 51 | 52 | 53 |
54 | 55 |
56 | `; 57 | 58 | exports[`VtToastContainer snapshots with default value 1`] = ` 59 |
60 | 61 |
62 | 65 | 66 | 67 | 68 |
69 |
70 | 73 | 74 | 75 | 76 |
77 |
78 | 81 | 82 | 83 | 84 |
85 |
86 | 89 | 90 | 91 | 92 |
93 |
94 | 97 | 98 | 99 | 100 |
101 |
102 | 105 | 106 | 107 | 108 |
109 | 110 |
111 | `; 112 | 113 | exports[`VtToastContainer snapshots with toasts 1`] = ` 114 |
115 | 116 |
117 | 120 | 121 | 122 | 123 |
124 |
125 | 128 | 129 | 130 | 131 |
132 |
133 | 136 | 137 | 138 |
142 | 155 | 163 | 169 |
173 |
174 |
178 | 191 | 199 | 205 |
209 |
210 | 211 | 212 | 213 |
214 |
215 | 218 | 219 | 220 | 221 |
222 |
223 | 226 | 227 | 228 | 229 |
230 |
231 | 234 | 235 | 236 |
240 | 253 | 261 | 267 |
271 |
272 | 273 | 274 | 275 |
276 | 277 |
278 | `; 279 | -------------------------------------------------------------------------------- /tests/unit/ts/composables/useDraggable.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable vue/one-component-per-file */ 2 | 3 | import { 4 | computed, 5 | defineComponent, 6 | h, 7 | nextTick, 8 | reactive, 9 | ref, 10 | onMounted, 11 | Ref, 12 | } from "vue" 13 | 14 | import { mount } from "@vue/test-utils" 15 | 16 | import { useDraggable } from "../../../../src/ts/composables/useDraggable" 17 | 18 | const activeText = "dragging" 19 | const inactiveText = "stopped" 20 | const completeText = "complete" 21 | 22 | type Props = Parameters[1] 23 | 24 | const TestComponent = (getEl?: (el: Ref) => void) => 25 | defineComponent({ 26 | props: { 27 | draggablePercent: { 28 | type: Number, 29 | required: true, 30 | }, 31 | draggable: { 32 | type: Boolean, 33 | required: true, 34 | }, 35 | }, 36 | setup(props) { 37 | const el = ref() 38 | onMounted(() => getEl && getEl(el)) 39 | const { beingDragged, dragComplete } = useDraggable(el, props) 40 | const text = computed(() => { 41 | if (dragComplete.value) { 42 | return completeText 43 | } 44 | return beingDragged.value ? activeText : inactiveText 45 | }) 46 | return () => 47 | h("div", { ref: el, id: "outer" }, h("p", { id: "inner" }, text.value)) 48 | }, 49 | }) 50 | 51 | describe("useDraggable", () => { 52 | beforeEach(() => { 53 | jest.resetAllMocks() 54 | jest.restoreAllMocks() 55 | }) 56 | 57 | const startPos: Pick = { 58 | clientX: 0, 59 | clientY: 0, 60 | } 61 | const clientRect: Omit = { 62 | x: startPos.clientX, 63 | y: startPos.clientY, 64 | right: 10, 65 | left: 0, 66 | bottom: 0, 67 | height: 0, 68 | width: 0, 69 | top: 0, 70 | } 71 | 72 | const getEl = (el: Ref) => { 73 | if (el.value) { 74 | jest 75 | .spyOn(el.value, "getBoundingClientRect") 76 | .mockImplementation(() => clientRect as DOMRect) 77 | } 78 | } 79 | 80 | const getDragDistance = (draggablePercent: number) => 81 | (clientRect.right - clientRect.left) * draggablePercent 82 | 83 | it("Returns valid object", async () => { 84 | const consoleSpy = jest.spyOn(console, "warn").mockImplementation() 85 | 86 | const el = ref() 87 | const props = reactive({ draggable: true, draggablePercent: 0.6 }) 88 | const retuned = useDraggable(el, props) 89 | 90 | // not-used-in-setup warnings 91 | expect(consoleSpy).toBeCalledTimes(2) 92 | 93 | expect(retuned.beingDragged.value).toBe(false) 94 | expect(retuned.dragComplete.value).toBe(false) 95 | }) 96 | 97 | it("Mounts and unmounts", async () => { 98 | const props = reactive({ draggable: true, draggablePercent: 0.6 }) 99 | const wrapper = mount(TestComponent(getEl), { props }) 100 | 101 | const inner = wrapper.find("#inner") 102 | 103 | expect(inner.text()).toEqual(inactiveText) 104 | 105 | wrapper.unmount() 106 | }) 107 | 108 | it("Does not drag if not draggable", async () => { 109 | const props = reactive({ draggable: false, draggablePercent: 0.6 }) 110 | const wrapper = mount(TestComponent(getEl), { props }) 111 | 112 | const outer = wrapper.find("#outer") 113 | const inner = wrapper.find("#inner") 114 | 115 | expect(inner.text()).toEqual(inactiveText) 116 | 117 | outer.element.dispatchEvent( 118 | new window.MouseEvent("mousedown", { ...startPos }) 119 | ) 120 | await nextTick() 121 | 122 | expect(inner.text()).toEqual(inactiveText) 123 | 124 | window.dispatchEvent( 125 | new window.MouseEvent("mousemove", { 126 | clientX: startPos.clientX + getDragDistance(0.6), 127 | clientY: startPos.clientY, 128 | }) 129 | ) 130 | await nextTick() 131 | expect(inner.text()).toEqual(inactiveText) 132 | }) 133 | 134 | it("beingDragged not enough with mouse", async () => { 135 | const props = reactive({ draggable: true, draggablePercent: 0.6 }) 136 | const wrapper = mount(TestComponent(getEl), { props }) 137 | 138 | const outer = wrapper.find("#outer") 139 | const inner = wrapper.find("#inner") 140 | 141 | expect(inner.text()).toEqual(inactiveText) 142 | 143 | outer.element.dispatchEvent( 144 | new window.MouseEvent("mousedown", { ...startPos }) 145 | ) 146 | await nextTick() 147 | 148 | expect(inner.text()).toEqual(inactiveText) 149 | 150 | // Get current position and calculate a move that is not enough to complete the drag 151 | const dragDistance = getDragDistance(0.6) * 0.9 152 | window.dispatchEvent( 153 | new window.MouseEvent("mousemove", { 154 | clientX: startPos.clientX + dragDistance, 155 | clientY: startPos.clientY, 156 | }) 157 | ) 158 | await nextTick() 159 | expect(inner.text()).toEqual(activeText) 160 | 161 | // End the drag 162 | window.dispatchEvent(new window.MouseEvent("mouseup")) 163 | await nextTick() 164 | await new Promise(r => setTimeout(r)) 165 | expect(inner.text()).toEqual(inactiveText) 166 | }) 167 | 168 | it("beingDragged not enough with touch", async () => { 169 | const props = reactive({ draggable: true, draggablePercent: 0.6 }) 170 | const wrapper = mount(TestComponent(getEl), { props }) 171 | 172 | const outer = wrapper.find("#outer") 173 | const inner = wrapper.find("#inner") 174 | 175 | expect(inner.text()).toEqual(inactiveText) 176 | 177 | outer.element.dispatchEvent( 178 | new window.MouseEvent("touchstart", { ...startPos }) 179 | ) 180 | await nextTick() 181 | 182 | expect(inner.text()).toEqual(inactiveText) 183 | 184 | // Get current position and calculate a move that is not enough to complete the drag 185 | const dragDistance = getDragDistance(0.6) * 0.9 186 | window.dispatchEvent( 187 | new window.MouseEvent("touchmove", { 188 | clientX: startPos.clientX + dragDistance, 189 | clientY: startPos.clientY, 190 | }) 191 | ) 192 | await nextTick() 193 | expect(inner.text()).toEqual(activeText) 194 | 195 | // End the drag 196 | window.dispatchEvent(new window.MouseEvent("touchend")) 197 | await nextTick() 198 | await new Promise(r => setTimeout(r)) 199 | expect(inner.text()).toEqual(inactiveText) 200 | }) 201 | 202 | it("beingDragged and then dragComplete", async () => { 203 | const props = reactive({ draggable: true, draggablePercent: 0.6 }) 204 | const wrapper = mount(TestComponent(getEl), { props }) 205 | 206 | const outer = wrapper.find("#outer") 207 | const inner = wrapper.find("#inner") 208 | 209 | expect(inner.text()).toEqual(inactiveText) 210 | 211 | outer.element.dispatchEvent( 212 | new window.MouseEvent("mousedown", { ...startPos }) 213 | ) 214 | await nextTick() 215 | 216 | expect(inner.text()).toEqual(inactiveText) 217 | 218 | // Get current position and calculate a move that is enough to complete the drag 219 | const dragDistance = getDragDistance(0.6) * 1.1 220 | window.dispatchEvent( 221 | new window.MouseEvent("mousemove", { 222 | clientX: startPos.clientX + dragDistance, 223 | clientY: startPos.clientY, 224 | }) 225 | ) 226 | await nextTick() 227 | expect(inner.text()).toEqual(activeText) 228 | 229 | // End the drag 230 | window.dispatchEvent(new window.MouseEvent("mouseup")) 231 | await nextTick() 232 | await new Promise(r => setTimeout(r)) 233 | expect(inner.text()).toEqual(completeText) 234 | }) 235 | 236 | it("styles are applied", async () => { 237 | const props = reactive({ draggable: true, draggablePercent: 0.6 }) 238 | let _el: HTMLElement | undefined = undefined 239 | 240 | const wrapper = mount( 241 | TestComponent(e => { 242 | getEl(e) 243 | if (e.value) { 244 | _el = e.value 245 | } 246 | }), 247 | { props } 248 | ) 249 | 250 | const outer = wrapper.find("#outer") 251 | 252 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 253 | expect(_el).toBeDefined() 254 | if (!_el) { 255 | return 256 | } 257 | const el = _el as HTMLElement 258 | 259 | await nextTick() 260 | 261 | // Before dragging, some styles should be set 262 | expect(el.style.transform).toEqual("translateX(0px)") 263 | expect(el.style.opacity).toEqual("1") 264 | expect(el.style.transition).toEqual("") 265 | 266 | // Drag starts 267 | outer.element.dispatchEvent( 268 | new window.MouseEvent("mousedown", { ...startPos }) 269 | ) 270 | await nextTick() 271 | 272 | // Same styles are kept 273 | expect(el.style.transform).toEqual("translateX(0px)") 274 | expect(el.style.opacity).toEqual("1") 275 | expect(el.style.transition).toEqual("") 276 | 277 | // Drag a little bit 278 | const removalDistance = getDragDistance(0.6) 279 | const dragDistance = Math.floor(removalDistance * 0.8) 280 | window.dispatchEvent( 281 | new window.MouseEvent("mousemove", { 282 | clientX: startPos.clientX + dragDistance, 283 | clientY: startPos.clientY, 284 | }) 285 | ) 286 | await nextTick() 287 | 288 | // Styles reflect dragging 289 | expect(el.style.transform).toEqual(`translateX(${dragDistance}px)`) 290 | expect(el.style.opacity).toEqual( 291 | `${1 - Math.abs(dragDistance / removalDistance)}` 292 | ) 293 | expect(el.style.transition).toEqual("") 294 | 295 | // End the drag 296 | window.dispatchEvent(new window.MouseEvent("mouseup")) 297 | await nextTick() 298 | await new Promise(r => setTimeout(r)) 299 | 300 | // Return to initial styles with a transition 301 | expect(el.style.transform).toEqual(`translateX(0px)`) 302 | expect(el.style.opacity).toEqual("1") 303 | expect(el.style.transition).toEqual("transform 0.2s, opacity 0.2s") 304 | 305 | // Start again, move and end 306 | outer.element.dispatchEvent( 307 | new window.MouseEvent("mousedown", { ...startPos }) 308 | ) 309 | await nextTick() 310 | window.dispatchEvent( 311 | new window.MouseEvent("mousemove", { 312 | clientX: startPos.clientX + removalDistance, 313 | clientY: startPos.clientY, 314 | }) 315 | ) 316 | await nextTick() 317 | window.dispatchEvent(new window.MouseEvent("mouseup")) 318 | await nextTick() 319 | await new Promise(r => setTimeout(r)) 320 | 321 | // Styles are final 322 | expect(el.style.transform).toEqual(`translateX(${removalDistance}px)`) 323 | expect(el.style.opacity).toEqual("0") 324 | expect(el.style.transition).toEqual("") 325 | }) 326 | }) 327 | -------------------------------------------------------------------------------- /tests/unit/ts/utils.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable vue/one-component-per-file */ 2 | import { defineComponent, h, isProxy, isRef, reactive, ref } from "vue" 3 | 4 | import { 5 | getId, 6 | getX, 7 | getY, 8 | removeElement, 9 | isString, 10 | isNonEmptyString, 11 | isToastContent, 12 | getVueComponentFromObj, 13 | hasProp, 14 | isUndefined, 15 | isDOMRect, 16 | isBrowser, 17 | normalizeToastComponent, 18 | } from "../../../src/ts/utils" 19 | import Simple from "../../utils/components/Simple.vue" 20 | 21 | describe("getId", () => { 22 | it("Generates 100 ids", () => { 23 | for (let i = 0; i < 100; i++) { 24 | expect(getId()).toBe(i) 25 | } 26 | }) 27 | }) 28 | 29 | describe("getX", () => { 30 | it("Gets X from a mouse event", () => { 31 | const event = new MouseEvent("event", { clientX: 10 }) 32 | expect(getX(event)).toBe(10) 33 | }) 34 | it("Gets X from a touch event", () => { 35 | const touch = { clientX: 10 } as Touch 36 | const event = new TouchEvent("event", { targetTouches: [touch] }) 37 | expect(getX(event)).toBe(10) 38 | }) 39 | }) 40 | 41 | describe("getY", () => { 42 | it("Gets Y from a mouse event", () => { 43 | const event = new MouseEvent("event", { clientY: 10 }) 44 | expect(getY(event)).toBe(10) 45 | }) 46 | it("Gets Y from a touch event", () => { 47 | const touch = { clientY: 10 } as Touch 48 | const event = new TouchEvent("event", { targetTouches: [touch] }) 49 | expect(getY(event)).toBe(10) 50 | }) 51 | }) 52 | 53 | describe("removeElement", () => { 54 | it("Calls own .remove method", () => { 55 | const element = document.createElement("div") 56 | element.remove = jest.fn() 57 | expect(element.remove).not.toHaveBeenCalled() 58 | removeElement(element) 59 | expect(element.remove).toHaveBeenCalled() 60 | }) 61 | it("Calls parent .removeChild method", () => { 62 | const element = document.createElement("div") 63 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 64 | element.remove = undefined as any 65 | 66 | const parent = document.createElement("div") 67 | parent.appendChild(element) 68 | parent.removeChild = jest.fn() 69 | 70 | expect(parent.removeChild).not.toHaveBeenCalled() 71 | removeElement(element) 72 | expect(parent.removeChild).toHaveBeenCalledWith(element) 73 | }) 74 | it("does nothing if no parent node or remove", () => { 75 | const element = document.createElement("div") 76 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 77 | element.remove = undefined as any 78 | expect(removeElement(element)).toBe(undefined) 79 | }) 80 | }) 81 | 82 | describe("isString", () => { 83 | it("is string", () => { 84 | expect(isString("")).toBe(true) 85 | expect(isString("abc")).toBe(true) 86 | }) 87 | it("is not string", () => { 88 | expect(isString(false)).toBe(false) 89 | expect(isString({})).toBe(false) 90 | }) 91 | }) 92 | 93 | describe("isNonEmptyString", () => { 94 | it("is string and empty", () => { 95 | expect(isNonEmptyString("")).toBe(false) 96 | }) 97 | it("is string and not empty", () => { 98 | expect(isNonEmptyString("value")).toBe(true) 99 | }) 100 | it("is not string", () => { 101 | expect(isNonEmptyString(123)).toBe(false) 102 | }) 103 | }) 104 | 105 | describe("isToastContent", () => { 106 | it("is undefined", () => { 107 | expect(isToastContent(undefined)).toBe(false) 108 | }) 109 | it("is null", () => { 110 | expect(isToastContent(null)).toBe(false) 111 | }) 112 | it("is string", () => { 113 | expect(isToastContent("component")).toBe(true) 114 | }) 115 | it("is defineComponent", () => { 116 | expect(isToastContent(defineComponent({}))).toBe(true) 117 | }) 118 | it("is functional component", () => { 119 | expect(isToastContent(() => h("div"))).toBe(true) 120 | }) 121 | it("is sfc", () => { 122 | expect(isToastContent(Simple)).toBe(true) 123 | }) 124 | it("has render function", () => { 125 | expect( 126 | isToastContent({ 127 | render() { 128 | return h("div") 129 | }, 130 | }) 131 | ).toBe(true) 132 | }) 133 | it("is jsx", () => { 134 | const jsx = { tag: "div" } as unknown as JSX.Element 135 | expect(isToastContent(jsx)).toBe(true) 136 | }) 137 | it("is toast component", () => { 138 | const component = { component: "string" } 139 | expect(isToastContent(component)).toBe(true) 140 | }) 141 | it("is not content", () => { 142 | const component = 123 143 | expect(isToastContent(component)).toBe(false) 144 | }) 145 | it("extends or has _Ctor", () => { 146 | expect(isToastContent({ extends: true })).toBe(true) 147 | expect(isToastContent({ _Ctor: true })).toBe(true) 148 | }) 149 | it("has template string", () => { 150 | expect(isToastContent({ template: "
abc
" })).toBe(true) 151 | }) 152 | }) 153 | 154 | describe("getVueComponentFromObj", () => { 155 | it("gets string component", () => { 156 | const obj = "component" 157 | expect(getVueComponentFromObj(obj)).toBe(obj) 158 | }) 159 | it("get defineComponent", () => { 160 | const component = defineComponent({}) 161 | expect(getVueComponentFromObj(component)).toBe(component) 162 | }) 163 | it("get non reactive object", () => { 164 | const component1 = reactive(defineComponent({})) 165 | expect(isProxy(component1)).toBe(true) 166 | expect(isProxy(getVueComponentFromObj(component1))).toBe(false) 167 | const component2 = ref(defineComponent({})) 168 | expect(isRef(component2)).toBe(true) 169 | expect(isRef(getVueComponentFromObj(component2))).toBe(false) 170 | }) 171 | it("get functional component", () => { 172 | const component = () => h("div") 173 | expect(getVueComponentFromObj(component)).toBe(component) 174 | }) 175 | it("get sfc", () => { 176 | const component = Simple 177 | expect(getVueComponentFromObj(component)).toBe(component) 178 | }) 179 | it("get jsx with render", () => { 180 | const jsx = { tag: "div" } as unknown as JSX.Element 181 | const vueComp = getVueComponentFromObj(jsx) as { render(): JSX.Element } 182 | expect(vueComp.render()).toBe(jsx) 183 | }) 184 | it("get toast component", () => { 185 | const component = { component: "my component string" } 186 | expect(getVueComponentFromObj(component)).toBe(component.component) 187 | }) 188 | }) 189 | 190 | describe("normalizeToastComponent", () => { 191 | it("normalizes regular string", () => { 192 | const component = "my component string" 193 | expect(normalizeToastComponent(component)).toBe(component) 194 | }) 195 | it("normalizes shallow vue object", () => { 196 | const component = Simple 197 | expect(normalizeToastComponent(component)).toEqual({ 198 | component: getVueComponentFromObj(component), 199 | props: {}, 200 | listeners: {}, 201 | }) 202 | }) 203 | it("normalizes composite vue object", () => { 204 | const component = { 205 | component: Simple, 206 | props: { myProp: "prop" }, 207 | listeners: { myListener: () => ({}) }, 208 | } 209 | expect(normalizeToastComponent(component)).toEqual(component) 210 | }) 211 | }) 212 | 213 | describe("hasProp", () => { 214 | it("gets prop from object", () => { 215 | const object = { myProp: 123 } 216 | expect(hasProp(object, "myProp")).toBe(true) 217 | }) 218 | it("gets prop from function", () => { 219 | const func = () => ({}) 220 | func.myProp = 123 221 | expect(hasProp(func, "myProp")).toBe(true) 222 | }) 223 | it("missing prop on object", () => { 224 | const object = { myProp: 123 } 225 | expect(hasProp(object, "myOtherProp")).toBe(false) 226 | }) 227 | it("missing prop on function", () => { 228 | const func = () => ({}) 229 | expect(hasProp(func, "myProp")).toBe(false) 230 | }) 231 | it("missing prop on primitive", () => { 232 | const primitive = "my prop" 233 | expect(hasProp(primitive, "myProp")).toBe(false) 234 | }) 235 | }) 236 | 237 | describe("isUndefined", () => { 238 | it("is undefined", () => { 239 | expect(isUndefined(undefined)).toBe(true) 240 | }) 241 | it("is not undefined", () => { 242 | expect(isUndefined("abc")).toBe(false) 243 | }) 244 | }) 245 | 246 | describe("isDOMRect", () => { 247 | it("is dom rect", () => { 248 | expect( 249 | isDOMRect({ 250 | width: 10, 251 | height: 10, 252 | left: 10, 253 | right: 10, 254 | top: 10, 255 | bottom: 10, 256 | }) 257 | ).toBe(true) 258 | }) 259 | it("missing prop", () => { 260 | expect( 261 | isDOMRect({ width: 10, height: 10, left: 10, top: 10, bottom: 10 }) 262 | ).toBe(false) 263 | expect( 264 | isDOMRect({ width: 10, height: 10, right: 10, top: 10, bottom: 10 }) 265 | ).toBe(false) 266 | expect( 267 | isDOMRect({ width: 10, left: 10, right: 10, top: 10, bottom: 10 }) 268 | ).toBe(false) 269 | expect( 270 | isDOMRect({ height: 10, left: 10, right: 10, top: 10, bottom: 10 }) 271 | ).toBe(false) 272 | expect( 273 | isDOMRect({ width: 10, height: 10, left: 10, right: 10, top: 10 }) 274 | ).toBe(false) 275 | expect( 276 | isDOMRect({ width: 10, height: 10, left: 10, right: 10, bottom: 10 }) 277 | ).toBe(false) 278 | }) 279 | it("prop wrong type", () => { 280 | expect( 281 | isDOMRect({ 282 | width: 10, 283 | height: 10, 284 | left: 10, 285 | right: 10, 286 | top: 10, 287 | bottom: "abc", 288 | }) 289 | ).toBe(false) 290 | expect( 291 | isDOMRect({ 292 | width: 10, 293 | height: 10, 294 | left: 10, 295 | right: 10, 296 | top: "abc", 297 | bottom: 10, 298 | }) 299 | ).toBe(false) 300 | expect( 301 | isDOMRect({ 302 | width: 10, 303 | height: 10, 304 | left: 10, 305 | right: "abc", 306 | top: 10, 307 | bottom: 10, 308 | }) 309 | ).toBe(false) 310 | expect( 311 | isDOMRect({ 312 | width: 10, 313 | height: 10, 314 | left: "abc", 315 | right: 10, 316 | top: 10, 317 | bottom: 10, 318 | }) 319 | ).toBe(false) 320 | expect( 321 | isDOMRect({ 322 | width: 10, 323 | height: "abc", 324 | left: 10, 325 | right: 10, 326 | top: 10, 327 | bottom: 10, 328 | }) 329 | ).toBe(false) 330 | expect( 331 | isDOMRect({ 332 | width: "abc", 333 | height: 10, 334 | left: 10, 335 | right: 10, 336 | top: 10, 337 | bottom: 10, 338 | }) 339 | ).toBe(false) 340 | }) 341 | it("not an object", () => { 342 | const func = () => ({}) 343 | func.height = 10 344 | func.width = 10 345 | func.left = 10 346 | func.right = 10 347 | func.top = 10 348 | func.bottom = 10 349 | expect(isDOMRect(func)).toBe(false) 350 | }) 351 | }) 352 | 353 | describe("isBrowser", () => { 354 | it("window is defined", () => { 355 | expect(isBrowser()).toBe(true) 356 | }) 357 | }) 358 | -------------------------------------------------------------------------------- /tests/unit/components/__snapshots__/VtToast.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`VtToast snapshots renders 1`] = ` 4 |
7 | 20 | 28 | 34 |
38 |
39 | `; 40 | 41 | exports[`VtToast ui closeButton = false removes it 1`] = ` 42 |
45 | 58 | 66 | 67 |
71 |
72 | `; 73 | 74 | exports[`VtToast ui has all default sub components 1`] = ` 75 |
78 | 91 | 99 | 105 |
109 |
110 | `; 111 | 112 | exports[`VtToast ui icon = false removes it 1`] = ` 113 |
116 | 117 | 125 | 131 |
135 |
136 | `; 137 | 138 | exports[`VtToast ui renders custom aria role and button aria label 1`] = ` 139 |
142 | 155 |
159 | 160 | content 161 | 162 |
163 | 169 |
173 |
174 | `; 175 | 176 | exports[`VtToast ui renders custom component 1`] = ` 177 |
180 | 193 | 203 | 209 |
213 |
214 | `; 215 | 216 | exports[`VtToast ui renders default aria role and button aria label 1`] = ` 217 |
220 | 233 | 241 | 247 |
251 |
252 | `; 253 | 254 | exports[`VtToast ui renders ltr by default 1`] = ` 255 |
258 | 271 | 279 | 285 |
289 |
290 | `; 291 | 292 | exports[`VtToast ui renders rtl if set 1`] = ` 293 |
296 | 309 | 317 | 323 |
327 |
328 | `; 329 | 330 | exports[`VtToast ui timeout = false removes progress bar 1`] = ` 331 |
334 | 347 | 355 | 361 | 362 |
363 | `; 364 | --------------------------------------------------------------------------------