├── .browserslistrc ├── .github └── ISSUE_TEMPLATE │ └── bug_report.md ├── .gitignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── babel.config.js ├── build └── rollup.config.js ├── dev ├── serve.js └── serve.vue ├── docs ├── .vitepress │ ├── config.js │ └── theme │ │ └── index.js ├── components │ ├── Button.vue │ ├── CodeBlock.vue │ ├── CodeGroup.ts │ ├── PopperDeep.vue │ ├── PopperDemo.vue │ ├── PopperDynamicTheme.vue │ ├── PopperEvents.vue │ ├── PopperManual.vue │ ├── PopperScopedSlots.vue │ ├── PopperStyled.vue │ └── code-group.css ├── guide │ ├── api.md │ ├── getting-started.md │ └── what-is-vue-3-popper.md ├── index.md └── public │ └── popper.svg ├── index.d.ts ├── package-lock.json ├── package.json └── src ├── component ├── Arrow.vue └── Popper.vue ├── composables ├── index.js ├── useClickAway.js ├── useContent.js ├── useEventListener.js └── usePopper.js ├── entry.esm.js └── entry.js /.browserslistrc: -------------------------------------------------------------------------------- 1 | current node 2 | last 2 versions and > 2% 3 | ie > 10 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Description of the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Reproduction link** 14 | If you can, create a reproduction on [CodeSandbox](https://codesandbox.io/) 15 | 16 | **To Reproduce** 17 | Steps to reproduce the behavior: 18 | 1. Go to '...' 19 | 2. Click on '....' 20 | 3. Scroll down to '....' 21 | 4. See error 22 | 23 | **Expected behavior** 24 | A clear and concise description of what you expected to happen. 25 | 26 | **Screenshots** 27 | If applicable, add screenshots to help explain your problem. 28 | 29 | **Additional context** 30 | Add any other context about the problem here. 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | dist 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "vueIndentScriptAndStyle": true 4 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Valgeir Björnsson 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | # vue3-popper 6 | 7 | > A popover component for Vue 3 8 | 9 | ## Documentation 10 | 11 | Check out the [documentation](https://valgeirb.github.io/vue3-popper/) 12 | 13 | - [Getting started](https://valgeirb.github.io/vue3-popper/guide/getting-started.html) 14 | - [Usage](https://valgeirb.github.io/vue3-popper/guide/getting-started.html#usage) 15 | 16 | ## Install 17 | 18 | ### NPM 19 | 20 | ```bash 21 | npm install vue3-popper 22 | ``` 23 | 24 | ### Yarn 25 | 26 | ```bash 27 | yarn add vue3-popper 28 | ``` 29 | 30 | ## Usage 31 | 32 | ### Vue environment 33 | 34 | ```html 35 | 36 | 41 | 42 | 43 | 51 | 52 | 62 | ``` 63 | 64 | ## Props 65 | 66 | | Name | Default | Description | 67 | | ------------------ | -------- | ----------------------------------------------------------------------------------------------------------- | 68 | | `placement` | `bottom` | Preferred placement of the Popper | 69 | | `disableClickAway` | `false` | Disables automatically closing the Popper when the user clicks away from it | 70 | | `offsetSkid` | `0` | Offset in pixels along the trigger element | 71 | | `offsetDistance` | `12` | Offset in pixels away from the trigger element | 72 | | `hover` | `false` | Trigger the Popper on hover | 73 | | `arrow` | `false` | Display an arrow on the Popper | 74 | | `arrowPadding` | `0` | Stop arrow from reaching the edge of the Popper (in pixels) | 75 | | `disabled` | `false` | Disables the Popper. If it was already open, it will be closed. | 76 | | `openDelay` | `0` | Open the Popper after a delay (ms) | 77 | | `closeDelay` | `0` | Close the Popper after a delay (ms) | 78 | | `interactive` | `true` | If the Popper should be interactive, it will close when clicked/hovered if false | 79 | | `content` | `null` | If your content is just a simple string, you can pass it as a prop | 80 | | `show` | `null` | Control the Popper **manually**, other events (click, hover) are ignored if this is set to `true/false` | 81 | | `zIndex` | `9999` | The z-index of the Popper | 82 | | `locked` | `false` | Lock the Popper into place, it will not flip dynamically when it runs out of space if this is set to `true` | 83 | 84 | ## Events 85 | 86 | | Name | Description | 87 | | -------------- | ------------------------- | 88 | | `open:popper` | When the Popper is opened | 89 | | `close:popper` | When the Popper is hidden | 90 | 91 | ## Slots 92 | 93 | | Name | Description | 94 | | --------- | ---------------------- | 95 | | `content` | For the Popper content | 96 | 97 | ## Slot props 98 | 99 | The `content` slot gives you access to useful variables and functions. 100 | 101 | | Name | Type | Description | 102 | | -------- | -------- | ------------------------------ | 103 | | `close` | function | A function to close the Popper | 104 | | `isOpen` | boolean | The `open` state of the Popper | 105 | 106 | ## CSS variables 107 | 108 | `Popper` only comes with some barebones styling by default, but it also uses a list of predefined CSS variables. You can overwrite these variables to suit your needs. 109 | 110 | | CSS variable | Example value | 111 | | --------------------------------------- | ----------------------------------- | 112 | | `--popper-theme-background-color` | #ffffff | 113 | | `--popper-theme-background-color-hover` | #ffffff | 114 | | `--popper-theme-text-color` | inherit | 115 | | `--popper-theme-border-width` | 1px | 116 | | `--popper-theme-border-style` | solid | 117 | | `--popper-theme-border-color` | #eeeeee | 118 | | `--popper-theme-border-radius` | 6px | 119 | | `--popper-theme-padding` | 16px | 120 | | `--popper-theme-box-shadow` | 0 6px 30px -6px rgba(0, 0, 0, 0.25) | 121 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | const devPresets = ["@vue/babel-preset-app"]; 2 | const buildPresets = [ 3 | [ 4 | "@babel/preset-env", 5 | // Config for @babel/preset-env 6 | { 7 | include: [/(optional-chaining|nullish-coalescing)/], 8 | }, 9 | ], 10 | ]; 11 | module.exports = { 12 | presets: process.env.NODE_ENV === "development" ? devPresets : buildPresets, 13 | }; 14 | -------------------------------------------------------------------------------- /build/rollup.config.js: -------------------------------------------------------------------------------- 1 | // rollup.config.js 2 | import fs from "fs"; 3 | import path from "path"; 4 | import vue from "rollup-plugin-vue"; 5 | import alias from "@rollup/plugin-alias"; 6 | import commonjs from "@rollup/plugin-commonjs"; 7 | import resolve from "@rollup/plugin-node-resolve"; 8 | import replace from "@rollup/plugin-replace"; 9 | import babel from "@rollup/plugin-babel"; 10 | import PostCSS from "rollup-plugin-postcss"; 11 | import { terser } from "rollup-plugin-terser"; 12 | import minimist from "minimist"; 13 | 14 | // Get browserslist config and remove ie from es build targets 15 | const esbrowserslist = fs 16 | .readFileSync("./.browserslistrc") 17 | .toString() 18 | .split("\n") 19 | .filter(entry => entry && entry.substring(0, 2) !== "ie"); 20 | 21 | // Extract babel preset-env config, to combine with esbrowserslist 22 | const babelPresetEnvConfig = require("../babel.config").presets.filter( 23 | entry => entry[0] === "@babel/preset-env", 24 | )[0][1]; 25 | 26 | const argv = minimist(process.argv.slice(2)); 27 | 28 | const projectRoot = path.resolve(__dirname, ".."); 29 | 30 | const baseConfig = { 31 | input: "src/entry.js", 32 | plugins: { 33 | preVue: [ 34 | alias({ 35 | entries: [ 36 | { 37 | find: "@", 38 | replacement: `${path.resolve(projectRoot, "src")}`, 39 | }, 40 | ], 41 | }), 42 | ], 43 | replace: { 44 | preventAssignment: true, 45 | "process.env.NODE_ENV": JSON.stringify("production"), 46 | }, 47 | vue: {}, 48 | postVue: [ 49 | resolve({ 50 | extensions: [".js", ".jsx", ".ts", ".tsx", ".vue"], 51 | }), 52 | // Process only ` 46 | -------------------------------------------------------------------------------- /docs/.vitepress/config.js: -------------------------------------------------------------------------------- 1 | const { description } = require("../../package"); 2 | 3 | module.exports = { 4 | base: "/vue3-popper/", 5 | title: "Vue 3 Popper", 6 | description: description, 7 | /** 8 | * Theme configuration, here is the default theme configuration for VuePress. 9 | * 10 | * ref:https://v1.vuepress.vuejs.org/theme/default-theme-config.html 11 | */ 12 | themeConfig: { 13 | repo: "", 14 | editLinks: false, 15 | docsDir: "", 16 | editLinkText: "", 17 | lastUpdated: false, 18 | nav: [ 19 | { 20 | text: "Guide", 21 | link: "/guide/what-is-vue-popper", 22 | }, 23 | { 24 | text: "Github", 25 | link: "https://github.com/valgeirb/vue-popper", 26 | }, 27 | ], 28 | sidebar: { 29 | "/guide/": getSidebar(), 30 | "/": getSidebar(), 31 | }, 32 | }, 33 | }; 34 | 35 | function getSidebar() { 36 | return [ 37 | { 38 | text: "Introduction", 39 | children: [ 40 | { text: "What is Vue 3 Popper?", link: "/guide/what-is-vue-3-popper" }, 41 | { text: "Getting Started", link: "/guide/getting-started" }, 42 | ], 43 | }, 44 | { 45 | text: "Advanced", 46 | children: [{ text: "API", link: "/guide/api" }], 47 | }, 48 | ]; 49 | } 50 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.js: -------------------------------------------------------------------------------- 1 | import DefaultTheme from "vitepress/theme"; 2 | import PopperEvents from "../../components/PopperEvents.vue"; 3 | import PopperStyled from "../../components/PopperStyled.vue"; 4 | import PopperDynamicTheme from "../../components/PopperDynamicTheme.vue"; 5 | import PopperDeep from "../../components/PopperDeep.vue"; 6 | import PopperScopedSlots from "../../components/PopperScopedSlots.vue"; 7 | import PopperDemo from "../../components/PopperDemo.vue"; 8 | import PopperManual from "../../components/PopperManual.vue"; 9 | import CodeBlock from "../../components/CodeBlock.vue"; 10 | import CodeGroup from "../../components/CodeGroup.ts"; 11 | import Button from "../../components/Button.vue"; 12 | 13 | export default { 14 | ...DefaultTheme, 15 | enhanceApp({ app }) { 16 | app.component("PopperEvents", PopperEvents); 17 | app.component("PopperStyled", PopperStyled); 18 | app.component("PopperDynamicTheme", PopperDynamicTheme); 19 | app.component("PopperDeep", PopperDeep); 20 | app.component("PopperScopedSlots", PopperScopedSlots); 21 | app.component("PopperDemo", PopperDemo); 22 | app.component("PopperManual", PopperManual); 23 | app.component("CodeBlock", CodeBlock); 24 | app.component("CodeGroup", CodeGroup); 25 | app.component("Button", Button); 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /docs/components/Button.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 26 | -------------------------------------------------------------------------------- /docs/components/CodeBlock.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 28 | -------------------------------------------------------------------------------- /docs/components/CodeGroup.ts: -------------------------------------------------------------------------------- 1 | import { defineComponent, h, ref } from 'vue' 2 | import type { Component, VNode } from 'vue' 3 | import "./code-group.css"; 4 | 5 | export default defineComponent({ 6 | name: 'CodeGroup', 7 | 8 | setup(_, { slots }) { 9 | // index of current active item 10 | const activeIndex = ref(-1) 11 | 12 | // refs of the tab buttons 13 | const tabRefs = ref([]) 14 | 15 | // activate next tab 16 | const activateNext = (i = activeIndex.value): void => { 17 | if (i < tabRefs.value.length - 1) { 18 | activeIndex.value = i + 1 19 | } else { 20 | activeIndex.value = 0 21 | } 22 | tabRefs.value[activeIndex.value].focus() 23 | } 24 | 25 | // activate previous tab 26 | const activatePrev = (i = activeIndex.value): void => { 27 | if (i > 0) { 28 | activeIndex.value = i - 1 29 | } else { 30 | activeIndex.value = tabRefs.value.length - 1 31 | } 32 | tabRefs.value[activeIndex.value].focus() 33 | } 34 | 35 | // handle keyboard event 36 | const keyboardHandler = (event: KeyboardEvent, i: number): void => { 37 | if (event.key === ' ' || event.key === 'Enter') { 38 | event.preventDefault() 39 | activeIndex.value = i 40 | } else if (event.key === 'ArrowRight') { 41 | event.preventDefault() 42 | activateNext(i) 43 | } else if (event.key === 'ArrowLeft') { 44 | event.preventDefault() 45 | activatePrev(i) 46 | } 47 | } 48 | 49 | return () => { 50 | // NOTICE: here we put the `slots.default()` inside the render function to make 51 | // the slots reactive, otherwise the slot content won't be changed once the 52 | // `setup()` function of current component is called 53 | 54 | // get children code-group-item 55 | const items = (slots.default?.() || []) 56 | .filter((vnode) => (vnode.type as Component).name === 'CodeGroupItem') 57 | .map((vnode) => { 58 | if (vnode.props === null) { 59 | vnode.props = {} 60 | } 61 | return vnode as VNode & { props: Exclude } 62 | }) 63 | 64 | // clear tabRefs for HMR 65 | tabRefs.value = [] 66 | 67 | // do not render anything if there is no code-group-item 68 | if (items.length === 0) { 69 | return null 70 | } 71 | 72 | if (activeIndex.value < 0 || activeIndex.value > items.length - 1) { 73 | // if `activeIndex` is invalid 74 | 75 | // find the index of the code-group-item with `active` props 76 | activeIndex.value = items.findIndex( 77 | (vnode) => vnode.props.active === '' || vnode.props.active === true 78 | ) 79 | 80 | // if there is no `active` props on code-group-item, set the first item active 81 | if (activeIndex.value === -1) { 82 | activeIndex.value = 0 83 | } 84 | } else { 85 | // set the active item 86 | items.forEach((vnode, i) => { 87 | vnode.props.active = i === activeIndex.value 88 | }) 89 | } 90 | 91 | return h('div', { class: 'code-group' }, [ 92 | h( 93 | 'div', 94 | { class: 'code-group__nav' }, 95 | h( 96 | 'ul', 97 | { class: 'code-group__ul' }, 98 | items.map((vnode, i) => { 99 | const isActive = i === activeIndex.value 100 | 101 | return h( 102 | 'li', 103 | { class: 'code-group__li' }, 104 | h( 105 | 'button', 106 | { 107 | ref: (element) => { 108 | if (element) { 109 | tabRefs.value[i] = element as HTMLButtonElement 110 | } 111 | }, 112 | class: { 113 | 'code-group__nav-tab': true, 114 | 'code-group__nav-tab-active': isActive, 115 | }, 116 | ariaPressed: isActive, 117 | ariaExpanded: isActive, 118 | onClick: () => (activeIndex.value = i), 119 | onKeydown: (e) => keyboardHandler(e, i), 120 | }, 121 | vnode.props.title 122 | ) 123 | ) 124 | }) 125 | ) 126 | ), 127 | items, 128 | ]) 129 | } 130 | }, 131 | }) -------------------------------------------------------------------------------- /docs/components/PopperDeep.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 20 | 21 | 48 | -------------------------------------------------------------------------------- /docs/components/PopperDemo.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 23 | 24 | 53 | -------------------------------------------------------------------------------- /docs/components/PopperDynamicTheme.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 39 | 40 | 74 | -------------------------------------------------------------------------------- /docs/components/PopperEvents.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 34 | 35 | 57 | -------------------------------------------------------------------------------- /docs/components/PopperManual.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 34 | 35 | 59 | -------------------------------------------------------------------------------- /docs/components/PopperScopedSlots.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 23 | 24 | 46 | -------------------------------------------------------------------------------- /docs/components/PopperStyled.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 20 | 21 | 42 | -------------------------------------------------------------------------------- /docs/components/code-group.css: -------------------------------------------------------------------------------- 1 | /** * code-group */ 2 | .code-group__nav { 3 | margin-top: 0.85rem; 4 | margin-bottom: calc(-1.7rem - 6px); 5 | padding-bottom: calc(1.7rem - 6px); 6 | padding-left: 10px; 7 | padding-top: 10px; 8 | border-top-left-radius: 6px; 9 | border-top-right-radius: 6px; 10 | background-color: black; 11 | } 12 | .code-group__ul { 13 | margin: auto 0; 14 | padding-left: 0; 15 | display: inline-flex; 16 | list-style: none; 17 | } 18 | .code-group__nav-tab { 19 | border: 0; 20 | padding: 5px; 21 | cursor: pointer; 22 | background-color: transparent; 23 | font-size: 0.85em; 24 | line-height: 1.4; 25 | color: rgba(255, 255, 255, 0.9); 26 | font-weight: 600; 27 | } 28 | .code-group__nav-tab:focus { 29 | outline: none; 30 | } 31 | .code-group__nav-tab:focus-visible { 32 | outline: 1px solid rgba(255, 255, 255, 0.9); 33 | } 34 | .code-group__nav-tab-active { 35 | border-bottom: var(--c-brand) 1px solid; 36 | } 37 | @media (max-width: 419px) { 38 | .code-group__nav { 39 | margin-left: -1.5rem; 40 | margin-right: -1.5rem; 41 | border-radius: 0; 42 | } 43 | } 44 | /** * code-group-item */ 45 | .code-group-item { 46 | display: none; 47 | } 48 | .code-group-item__active { 49 | display: block; 50 | } 51 | .code-group-item > pre { 52 | background-color: orange; 53 | } 54 | -------------------------------------------------------------------------------- /docs/guide/api.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | ## Props 4 | 5 | | Name | Default | Description | 6 | | ------------------ | -------- | ----------------------------------------------------------------------------------------------------------- | 7 | | `placement` | `bottom` | Preferred placement of the Popper | 8 | | `disableClickAway` | `false` | Disables automatically closing the Popper when the user clicks away from it | 9 | | `offsetSkid` | `0` | Offset in pixels along the trigger element | 10 | | `offsetDistance` | `12` | Offset in pixels away from the trigger element | 11 | | `hover` | `false` | Trigger the Popper on hover | 12 | | `arrow` | `false` | Display an arrow on the Popper | 13 | | `arrowPadding` | `0` | Stop arrow from reaching the edge of the Popper (in pixels) | 14 | | `disabled` | `false` | Disables the Popper. If it was already open, it will be closed. | 15 | | `openDelay` | `0` | Open the Popper after a delay (ms) | 16 | | `closeDelay` | `0` | Close the Popper after a delay (ms) | 17 | | `interactive` | `true` | If the Popper should be interactive, it will close when clicked/hovered if false | 18 | | `content` | `null` | If your content is just a simple string, you can pass it as a prop | 19 | | `show` | `null` | Control the Popper **manually**, other events (click, hover) are ignored if this is set to `true/false` | 20 | | `zIndex` | `9999` | The z-index of the Popper | 21 | | `locked` | `false` | Lock the Popper into place, it will not flip dynamically when it runs out of space if this is set to `true` | 22 | 23 | ## Events 24 | 25 | | Name | Description | 26 | | -------------- | ------------------------- | 27 | | `open:popper` | When the Popper is opened | 28 | | `close:popper` | When the Popper is hidden | 29 | 30 | ## Slots 31 | 32 | | Name | Description | 33 | | --------- | ---------------------- | 34 | | `content` | For the Popper content | 35 | 36 | ## Slot props 37 | 38 | The `content` slot gives you access to useful variables and functions. 39 | 40 | | Name | Type | Description | 41 | | -------- | -------- | ------------------------------ | 42 | | `close` | function | A function to close the Popper | 43 | | `isOpen` | boolean | The `open` state of the Popper | 44 | 45 | ## CSS variables 46 | 47 | `Popper` only comes with some barebones styling by default, but it also uses a list of predefined CSS variables. You can overwrite these variables to suit your needs. 48 | 49 | | CSS variable | Example value | 50 | | --------------------------------------- | ----------------------------------- | 51 | | `--popper-theme-background-color` | #ffffff | 52 | | `--popper-theme-background-color-hover` | #ffffff | 53 | | `--popper-theme-text-color` | inherit | 54 | | `--popper-theme-border-width` | 1px | 55 | | `--popper-theme-border-style` | solid | 56 | | `--popper-theme-border-color` | #eeeeee | 57 | | `--popper-theme-border-radius` | 6px | 58 | | `--popper-theme-padding` | 16px | 59 | | `--popper-theme-box-shadow` | 0 6px 30px -6px rgba(0, 0, 0, 0.25) | 60 | -------------------------------------------------------------------------------- /docs/guide/getting-started.md: -------------------------------------------------------------------------------- 1 | ## Installation 2 | 3 | ::: tip 4 | Like the name suggests, `vue3-popper` is written for Vue 3. There are no plans to support both Vue 2.x and Vue 3.x at the moment. 5 | ::: 6 | 7 | You can install `Vue 3 Popper` by opening your terminal in your project and running the following command: 8 | 9 | With yarn: 10 | 11 | ```bash 12 | $ yarn add vue3-popper 13 | ``` 14 | 15 | With NPM: 16 | 17 | ```bash 18 | $ npm i vue3-popper 19 | ``` 20 | 21 | ### Global 22 | 23 | You can import and register the component globally: 24 | 25 | ```javascript 26 | import { createApp } from "vue"; 27 | import Popper from "vue3-popper"; 28 | 29 | const app = Vue.createApp({}); 30 | app.component("Popper", Popper); 31 | ``` 32 | 33 | ### Component 34 | 35 | Or use it on a case by case basis: 36 | 37 | ```html 38 | 43 | 44 | 54 | ``` 55 | 56 | 61 | 62 | ## Usage 63 | 64 | You can add Popper to any of your elements or components. Just wrap them with `Popper` and use the `content` prop or slot for your popover. 65 | 66 | ### Using the content `prop` 67 | 68 | If your content is only a simple string, you can use the `content` prop: 69 | 70 | ```vue 71 | 76 | ``` 77 | 78 | ### Using the content `slot` 79 | 80 | If your content is more complex, you can use the `#content` slot: 81 | 82 | ```vue 83 | 91 | ``` 92 | 93 | ## What about styles? 94 | 95 | `Popper` only comes with some barebones styling by default, but it also uses a list of predefined CSS variables. You can overwrite these variables to suit your needs. 96 | 97 | ### CSS variables 98 | 99 | | CSS variable | Example value | 100 | | --------------------------------------- | ----------------------------------- | 101 | | `--popper-theme-background-color` | #ffffff | 102 | | `--popper-theme-background-color-hover` | #ffffff | 103 | | `--popper-theme-text-color` | inherit | 104 | | `--popper-theme-border-width` | 1px | 105 | | `--popper-theme-border-style` | solid | 106 | | `--popper-theme-border-color` | #eeeeee | 107 | | `--popper-theme-border-radius` | 6px | 108 | | `--popper-theme-padding` | 16px | 109 | | `--popper-theme-box-shadow` | 0 6px 30px -6px rgba(0, 0, 0, 0.25) | 110 | 111 | You can overwrite them any way you like, for example in a Vue component: 112 | 113 | ```vue 114 | 119 | 120 | 132 | ``` 133 | 134 | You could also create a `theme.css` file: 135 | 136 | ```css 137 | :root { 138 | --popper-theme-background-color: #333333; 139 | --popper-theme-background-color-hover: #333333; 140 | --popper-theme-text-color: #ffffff; 141 | --popper-theme-border-width: 0px; 142 | --popper-theme-border-style: solid; 143 | --popper-theme-border-radius: 6px; 144 | --popper-theme-padding: 32px; 145 | --popper-theme-box-shadow: 0 6px 30px -6px rgba(0, 0, 0, 0.25); 146 | } 147 | ``` 148 | 149 | Import it: 150 | 151 | ```javascript 152 | import { createApp } from "vue"; 153 | import App from "./App.vue"; 154 | import "./theme.css"; // Magic happens here 155 | 156 | createApp(App).mount("#app"); 157 | ``` 158 | 159 | And your Popper is styled! 160 | 161 | 162 | 163 | ### Dynamic theming 164 | 165 | Using the CSS variables you could even add multiple themes to your popover. 166 | 167 | ```css 168 | .dark { 169 | --popper-theme-background-color: #333333; 170 | --popper-theme-background-color-hover: #333333; 171 | --popper-theme-text-color: white; 172 | --popper-theme-border-width: 0px; 173 | --popper-theme-border-radius: 6px; 174 | --popper-theme-padding: 32px; 175 | --popper-theme-box-shadow: 0 6px 30px -6px rgba(0, 0, 0, 0.25); 176 | } 177 | 178 | .light { 179 | --popper-theme-background-color: #ffffff; 180 | --popper-theme-background-color-hover: #ffffff; 181 | --popper-theme-text-color: #333333; 182 | --popper-theme-border-width: 1px; 183 | --popper-theme-border-style: solid; 184 | --popper-theme-border-color: #eeeeee; 185 | --popper-theme-border-radius: 6px; 186 | --popper-theme-padding: 32px; 187 | --popper-theme-box-shadow: 0 6px 30px -6px rgba(0, 0, 0, 0.25); 188 | } 189 | ``` 190 | 191 | ```vue 192 | 202 | ``` 203 | 204 | 205 | 206 | ### I don't want to use CSS variables 207 | 208 | That's fine, you can always just apply your own styles, just make sure it's `scoped` and you use the `:deep` selector: 209 | 210 | ```vue 211 | 216 | 217 | 236 | ``` 237 | 238 | 239 | 240 | ## How can I wrap `Popper` with my own component? 241 | 242 | It is generally a good idea to wrap 3rd party components like `vue3-popper` with your own local component. It gives you things like: 243 | 244 | - Modularity 245 | - Scalability 246 | - Ability to add custom styles or extensions 247 | - **If you need to change something (even swap out vue3-popper for something else) you only need to do that once in your wrapper component.** 248 | 249 | Here's an example of how you can wrap `vue3-popper` with your own component: 250 | 251 | ::: tip 252 | Notice that in this example, `hover`, `openDelay` and `closeDelay` are all hardcoded. This is just to show how you can create an **opinionated** wrapper. 253 | ::: 254 | 255 | ```vue 256 | 265 | 266 | 277 | ``` 278 | 279 | You could then go on to define your styles etc. and use your component just like you would `Popper`: 280 | 281 | ```vue 282 | 290 | ``` 291 | 292 | ## Reacting to `Popper` events 293 | 294 | Sometimes you need to add some side-effects when closing/opening Poppers. You can use the built-in events for that: 295 | 296 | ```vue 297 | 307 | 308 | 322 | ``` 323 | 324 | 325 | 326 | ## Using scoped slot properties 327 | 328 | You can gain access to the `close` function for those edge cases. In this example we use the `close` function to dismiss the Popper when a button is clicked inside of it. 329 | 330 | ```vue 331 | 339 | ``` 340 | 341 | 342 | 343 | ## Manually controlling the Popper 344 | 345 | You can use the `show` prop to manually control the Popper. Other events (click, hover) are ignored when in manual mode. 346 | 347 | ```vue 348 | 359 | 360 | 370 | ``` 371 | 372 | 373 | 374 | ::: tip 375 | `Vue 3 Popper` has multiple useful props as well, check out the API docs for more info. 376 | ::: 377 | -------------------------------------------------------------------------------- /docs/guide/what-is-vue-3-popper.md: -------------------------------------------------------------------------------- 1 | # What is Vue 3 Popper 2 | 3 | `Vue 3 Popper` is a component for displaying popovers. It's written in Vue 3 and uses the latest version of [PopperJS](https://popper.js.org/) 4 | 5 | 6 | 7 | ## Features 8 | 9 | - Simple API 10 | - Customizable theme 11 | - Can display arrows 12 | - Up to date with Vue 3 and Popper 2 13 | 14 | ::: tip 15 | Like the name suggests, `vue3-popper` is written for Vue 3. There are no plans to support both Vue 2.x and Vue 3.x at the moment. 16 | ::: 17 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | heroImage: /popper.svg 4 | tagline: Add popovers to your project quickly. 5 | actionText: Quick Start → 6 | actionLink: /guide/getting-started 7 | features: 8 | - title: ✨ Up to date 9 | details: Written in Vue 3 using the latest version of PopperJS. 10 | - title: 🔌 Simple API 11 | details: Made to be easy to use. 12 | - title: 🎨 Themable 13 | details: Use CSS properties to quickly theme your popper or write your own CSS. 14 | --- 15 | 16 | 17 | -------------------------------------------------------------------------------- /docs/public/popper.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "vue3-popper" { 2 | import { DefineComponent } from "vue"; 3 | 4 | interface Props { 5 | placement?: 6 | | "auto" 7 | | "auto-start" 8 | | "auto-end" 9 | | "top" 10 | | "top-start" 11 | | "top-end" 12 | | "bottom" 13 | | "bottom-start" 14 | | "bottom-end" 15 | | "right" 16 | | "right-start" 17 | | "right-end" 18 | | "left" 19 | | "left-start" 20 | | "left-end"; 21 | disableClickAway?: boolean; 22 | offsetSkid?: string; 23 | offsetDistance?: string; 24 | hover?: boolean; 25 | show?: boolean; 26 | disabled?: boolean; 27 | openDelay?: number | string; 28 | closeDelay?: number | string; 29 | zIndex?: number | string; 30 | arrow?: boolean; 31 | arrowPadding?: string; 32 | interactive?: boolean; 33 | locked?: boolean; 34 | content?: string; 35 | "onOpen:popper"?: () => void; 36 | "onClose:popper"?: () => void; 37 | } 38 | 39 | const Popper: DefineComponent; 40 | 41 | export default Popper; 42 | } 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue3-popper", 3 | "version": "1.4.2", 4 | "author": { 5 | "name": "Valgeir Björnsson", 6 | "email": "valgeir@hey.com", 7 | "url": "https://github.com/valgeirb" 8 | }, 9 | "repository": "github:valgeirb/vue3-popper", 10 | "homepage": "https://valgeirb.github.io/vue3-popper", 11 | "license": "MIT", 12 | "description": "A Vue 3 popper component. Uses PopperJS v2.", 13 | "keywords": [ 14 | "vue-popper-component", 15 | "vue-3-popper", 16 | "vue-3-popover", 17 | "vue-3-popper-SFC", 18 | "popper-2", 19 | "vue-3-popper-2" 20 | ], 21 | "main": "dist/popper.ssr.js", 22 | "browser": "dist/popper.esm.js", 23 | "module": "dist/popper.esm.js", 24 | "unpkg": "dist/popper.min.js", 25 | "files": [ 26 | "dist/*", 27 | "src/**/*.vue", 28 | "index.d.ts" 29 | ], 30 | "sideEffects": false, 31 | "scripts": { 32 | "dev": "vue-cli-service serve dev/serve.js", 33 | "dev:docs": "vitepress dev docs", 34 | "build:docs": "vitepress build docs", 35 | "serve:docs": "vitepress serve docs", 36 | "deploy:docs": "npm run build:docs && npx gh-pages -d docs/.vitepress/dist", 37 | "build": "cross-env NODE_ENV=production rollup --config build/rollup.config.js", 38 | "build:ssr": "cross-env NODE_ENV=production rollup --config build/rollup.config.js --format cjs", 39 | "build:es": "cross-env NODE_ENV=production rollup --config build/rollup.config.js --format es", 40 | "build:unpkg": "cross-env NODE_ENV=production rollup --config build/rollup.config.js --format iife" 41 | }, 42 | "dependencies": { 43 | "@popperjs/core": "^2.9.2", 44 | "debounce": "^1.2.1" 45 | }, 46 | "devDependencies": { 47 | "@babel/core": "^7.12.10", 48 | "@babel/preset-env": "^7.12.11", 49 | "@rollup/plugin-alias": "^3.1.1", 50 | "@rollup/plugin-babel": "^5.2.2", 51 | "@rollup/plugin-commonjs": "^17.0.0", 52 | "@rollup/plugin-node-resolve": "^11.0.1", 53 | "@rollup/plugin-replace": "^2.3.4", 54 | "@vue/cli-plugin-babel": "^4.5.10", 55 | "@vue/cli-service": "^4.5.10", 56 | "@vue/compiler-sfc": "^3.0.5", 57 | "cross-env": "^7.0.3", 58 | "minimist": "^1.2.5", 59 | "patch-vue-directive-ssr": "^0.0.1", 60 | "postcss": "^8.2.3", 61 | "prettier": "^1.19.1", 62 | "rollup": "^2.36.1", 63 | "rollup-plugin-postcss": "^4.0.0", 64 | "rollup-plugin-terser": "^7.0.2", 65 | "rollup-plugin-vue": "^6.0.0", 66 | "vitepress": "^0.20.0", 67 | "vue": "^3.2.20" 68 | }, 69 | "peerDependencies": { 70 | "vue": "^3.2.20" 71 | }, 72 | "engines": { 73 | "node": ">=12" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/component/Arrow.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 67 | -------------------------------------------------------------------------------- /src/component/Popper.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 302 | 303 | 339 | -------------------------------------------------------------------------------- /src/composables/index.js: -------------------------------------------------------------------------------- 1 | export { default as useClickAway } from "./useClickAway.js"; 2 | export { default as useContent } from "./useContent.js"; 3 | export { default as useEventListener } from "./useEventListener.js"; 4 | export { default as usePopper } from "./usePopper.js"; 5 | -------------------------------------------------------------------------------- /src/composables/useClickAway.js: -------------------------------------------------------------------------------- 1 | import { unref } from "vue"; 2 | import useEventListener from "./useEventListener"; 3 | 4 | export default function useClickAway(target, handler) { 5 | const event = "pointerdown"; 6 | 7 | if (typeof window === 'undefined' || !window) { 8 | return; 9 | } 10 | 11 | const listener = event => { 12 | const el = unref(target); 13 | if (!el) { 14 | return; 15 | } 16 | 17 | if (el === event.target || event.composedPath().includes(el)) { 18 | return; 19 | } 20 | 21 | handler(event); 22 | }; 23 | 24 | return useEventListener(window, event, listener); 25 | } 26 | -------------------------------------------------------------------------------- /src/composables/useContent.js: -------------------------------------------------------------------------------- 1 | import { ref, onMounted, onBeforeUnmount, watch } from "vue"; 2 | export default function useContent(slots, popperNode, content) { 3 | let observer = null; 4 | const hasContent = ref(false); 5 | 6 | onMounted(() => { 7 | if (slots.content !== undefined || content.value) { 8 | hasContent.value = true; 9 | } 10 | 11 | observer = new MutationObserver(checkContent); 12 | observer.observe(popperNode.value, { 13 | childList: true, 14 | subtree: true, 15 | }); 16 | }); 17 | 18 | onBeforeUnmount(() => observer.disconnect()); 19 | 20 | /** 21 | * Watch the content prop 22 | */ 23 | watch(content, content => { 24 | if (content) { 25 | hasContent.value = true; 26 | } else { 27 | hasContent.value = false; 28 | } 29 | }); 30 | 31 | /** 32 | * Check the content slot 33 | */ 34 | const checkContent = () => { 35 | if (slots.content) { 36 | hasContent.value = true; 37 | } else { 38 | hasContent.value = false; 39 | } 40 | }; 41 | 42 | return { 43 | hasContent, 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /src/composables/useEventListener.js: -------------------------------------------------------------------------------- 1 | import { isRef, watch, unref, onMounted, onBeforeUnmount } from "vue"; 2 | 3 | export default function useEventListener(target, event, handler) { 4 | if (isRef(target)) { 5 | watch(target, (value, oldValue) => { 6 | oldValue?.removeEventListener(event, handler); 7 | value?.addEventListener(event, handler); 8 | }); 9 | } else { 10 | onMounted(() => { 11 | target.addEventListener(event, handler); 12 | }); 13 | } 14 | 15 | onBeforeUnmount(() => { 16 | unref(target)?.removeEventListener(event, handler); 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /src/composables/usePopper.js: -------------------------------------------------------------------------------- 1 | import { toRefs, watch, nextTick, onBeforeUnmount, reactive } from "vue"; 2 | import { createPopper } from "@popperjs/core/lib/popper-lite.js"; 3 | import preventOverflow from "@popperjs/core/lib/modifiers/preventOverflow.js"; 4 | import flip from "@popperjs/core/lib/modifiers/flip.js"; 5 | import offset from "@popperjs/core/lib/modifiers/offset"; 6 | import arrow from "@popperjs/core/lib/modifiers/arrow"; 7 | 8 | const toInt = x => parseInt(x, 10); 9 | 10 | export default function usePopper({ 11 | arrowPadding, 12 | emit, 13 | locked, 14 | offsetDistance, 15 | offsetSkid, 16 | placement, 17 | popperNode, 18 | triggerNode, 19 | }) { 20 | const state = reactive({ 21 | isOpen: false, 22 | popperInstance: null, 23 | }); 24 | 25 | // Enable or disable event listeners to optimize performance. 26 | const setPopperEventListeners = enabled => { 27 | state.popperInstance?.setOptions(options => ({ 28 | ...options, 29 | modifiers: [...options.modifiers, { name: "eventListeners", enabled }], 30 | })); 31 | }; 32 | 33 | const enablePopperEventListeners = () => setPopperEventListeners(true); 34 | const disablePopperEventListeners = () => setPopperEventListeners(false); 35 | 36 | const close = () => { 37 | if (!state.isOpen) { 38 | return; 39 | } 40 | 41 | state.isOpen = false; 42 | emit("close:popper"); 43 | }; 44 | 45 | const open = () => { 46 | if (state.isOpen) { 47 | return; 48 | } 49 | 50 | state.isOpen = true; 51 | emit("open:popper"); 52 | }; 53 | 54 | // When isOpen or placement change 55 | watch([() => state.isOpen, placement], async ([isOpen]) => { 56 | if (isOpen) { 57 | await initializePopper(); 58 | enablePopperEventListeners(); 59 | } else { 60 | disablePopperEventListeners(); 61 | } 62 | }); 63 | 64 | const initializePopper = async () => { 65 | await nextTick(); 66 | state.popperInstance = createPopper(triggerNode.value, popperNode.value, { 67 | placement: placement.value, 68 | modifiers: [ 69 | preventOverflow, 70 | flip, 71 | { 72 | name: "flip", 73 | enabled: !locked.value, 74 | }, 75 | arrow, 76 | { 77 | name: "arrow", 78 | options: { 79 | padding: toInt(arrowPadding.value), 80 | }, 81 | }, 82 | offset, 83 | { 84 | name: "offset", 85 | options: { 86 | offset: [toInt(offsetSkid.value), toInt(offsetDistance.value)], 87 | }, 88 | }, 89 | ], 90 | }); 91 | 92 | // Update its position 93 | state.popperInstance.update(); 94 | }; 95 | 96 | onBeforeUnmount(() => { 97 | state.popperInstance?.destroy(); 98 | }); 99 | 100 | return { 101 | ...toRefs(state), 102 | open, 103 | close, 104 | }; 105 | } 106 | -------------------------------------------------------------------------------- /src/entry.esm.js: -------------------------------------------------------------------------------- 1 | import component from "@/component/Popper.vue"; 2 | 3 | // Default export is installable instance of component. 4 | // IIFE injects install function into component, allowing component 5 | // to be registered via Vue.use() as well as Vue.component(), 6 | export default /*#__PURE__*/ (() => { 7 | // Get component instance 8 | const installable = component; 9 | 10 | // Attach install function executed by Vue.use() 11 | installable.install = app => { 12 | app.component("Popper", installable); 13 | }; 14 | return installable; 15 | })(); 16 | 17 | // It's possible to expose named exports when writing components that can 18 | // also be used as directives, etc. - eg. import { RollupDemoDirective } from 'rollup-demo'; 19 | // export const RollupDemoDirective = directive; 20 | -------------------------------------------------------------------------------- /src/entry.js: -------------------------------------------------------------------------------- 1 | // iife/cjs usage extends esm default export - so import it all 2 | import component, * as namedExports from "@/entry.esm"; 3 | // Attach named exports directly to component. IIFE/CJS will 4 | // only expose one global var, with named exports exposed as properties of 5 | // that global var (eg. plugin.namedExport) 6 | Object.entries(namedExports).forEach(([exportName, exported]) => { 7 | if (exportName !== "default") component[exportName] = exported; 8 | }); 9 | 10 | export default component; 11 | --------------------------------------------------------------------------------