├── .gitignore ├── README.md ├── index.js ├── package-lock.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # poppy 2 | A tiny, fast, configurable popover in 1.6kb. 🍻 3 | 4 | # Usage 5 | ```javascript 6 | import Poppy from 'poppy' 7 | 8 | const target = document.getElementById('target') 9 | 10 | const pop = new Poppy({ 11 | target: target, 12 | popover: ` 13 |
14 |
I'm a popover!
15 |
16 | `, 17 | position: 'left', // from tackjs 18 | transitionSpeed: 200, // for css transitions 19 | onChange: ({ pinned }) => {...} // boolean 20 | }) 21 | 22 | target.addEventListener('mouseenter', pop.pin) 23 | target.addEventListener('mouseleave', pop.unpin) 24 | ``` 25 | 26 | Required CSS: 27 | ```css 28 | .poppy { 29 | position: absolute; 30 | z-index: 9999; 31 | top: 0; left: 0; 32 | } 33 | ``` 34 | 35 | # The Name 36 | Huge thanks to [swathysubhash](https://github.com/swathysubhash) for letting me 37 | use the name! This library used to be called [micro-popover](https://github.com/estrattonbailey/micro-popover). 38 | 39 | ## License 40 | MIT License (c) 2018 Eric Bailey 41 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import tack from 'tackjs' 2 | import { tarry, queue } from 'tarry.js' 3 | 4 | export default class Popover { 5 | constructor ({ 6 | target, 7 | popover = null, 8 | position = 'bottom', 9 | transitionSpeed = 0, 10 | onChange = null 11 | }) { 12 | this.target = target 13 | this.popover = this.createPopover(popover) 14 | this.position = position 15 | this.transitionSpeed = transitionSpeed 16 | this.onChange = onChange 17 | 18 | this.state = { 19 | pinned: false, 20 | busy: false, 21 | requestClose: false 22 | } 23 | 24 | this.pin = this.pin.bind(this) 25 | this.unpin = this.unpin.bind(this) 26 | this.block = this.block.bind(this) 27 | this.unblock = this.unblock.bind(this) 28 | this.isExternalClick = this.isExternalClick.bind(this) 29 | this.handleKeyup = this.handleKeyup.bind(this) 30 | 31 | this.focusNode = null 32 | } 33 | 34 | setState (state, cb) { 35 | this.state = Object.assign( 36 | this.state, 37 | state 38 | ) 39 | 40 | cb && tarry(cb, 0)() 41 | } 42 | 43 | block () { 44 | this.setState({ 45 | busy: true 46 | }) 47 | } 48 | 49 | unblock () { 50 | this.setState({ 51 | busy: false 52 | }, () => { 53 | this.state.requestClose && this.unpin() 54 | }) 55 | } 56 | 57 | toggle () { 58 | this.state.pinned ? this.unpin() : this.pin() 59 | } 60 | 61 | pin () { 62 | if (this.state.busy || this.state.pinned) return 63 | 64 | this.setState({ 65 | busy: true 66 | }) 67 | 68 | this.focusNode = document.activeElement 69 | 70 | const render = tarry(() => document.body.appendChild(this.popover)) 71 | const pin = tarry(() => tack(this.popover, this.target, this.position)) 72 | const show = tarry(() => { 73 | this.popover.classList.add('is-visible') 74 | this.popover.setAttribute('tabindex', '0') 75 | this.popover.setAttribute('aria-hidden', 'false') 76 | }) 77 | const focus = tarry(() => this.popover.focus()) 78 | const done = tarry(() => this.setState({ 79 | busy: false, 80 | pinned: true 81 | })) 82 | 83 | queue(render, pin, show(0), focus(0), done)() 84 | 85 | this.popover.addEventListener('mouseenter', this.block) 86 | this.popover.addEventListener('mouseleave', this.unblock) 87 | window.addEventListener('click', this.isExternalClick) 88 | window.addEventListener('touchstart', this.isExternalClick) 89 | window.addEventListener('keyup', this.handleKeyup) 90 | window.addEventListener('resize', this.unpin) 91 | 92 | this.onChange && this.onChange({ 93 | pinned: true 94 | }) 95 | } 96 | 97 | unpin (force) { 98 | this.setState({ 99 | requestClose: true 100 | }) 101 | 102 | if (!force && (this.state.busy || !this.state.pinned)) return 103 | 104 | tarry(() => { 105 | this.setState({ 106 | busy: true 107 | }) 108 | 109 | this.popover.removeEventListener('mouseenter', this.block) 110 | this.popover.removeEventListener('mouseleave', this.unblock) 111 | window.removeEventListener('click', this.isExternalClick) 112 | window.removeEventListener('touchstart', this.isExternalClick) 113 | window.removeEventListener('keyup', this.handleKeyup) 114 | window.removeEventListener('resize', this.unpin) 115 | 116 | const hide = tarry(() => this.popover.classList.add('is-hiding')) 117 | const remove = tarry(() => document.body.removeChild(this.popover)) 118 | const blur = tarry(() => this.focusNode.focus()) 119 | const done = tarry(() => { 120 | this.popover.classList.remove('is-hiding') 121 | this.popover.classList.remove('is-visible') 122 | this.setState({ 123 | busy: false, 124 | pinned: false, 125 | requestClose: false 126 | }) 127 | }) 128 | 129 | queue(hide, remove(this.transitionSpeed), blur, done)() 130 | 131 | this.onChange && this.onChange({ 132 | pinned: false 133 | }) 134 | }, 0)() 135 | } 136 | 137 | handleKeyup (e) { 138 | e.keyCode === 27 && this.unpin() 139 | } 140 | 141 | isExternalClick (e) { 142 | if ( 143 | (e.target !== this.popover && !this.popover.contains(e.target)) && 144 | (e.target !== this.target && !this.target.contains(e.target)) 145 | ) { 146 | this.unpin() 147 | } 148 | } 149 | 150 | createPopover (pop) { 151 | const popover = document.createElement('div') 152 | popover.className = 'poppy' 153 | 154 | popover.role = 'dialog' 155 | popover.setAttribute('aria-label', 'Share Dialog') 156 | popover.setAttribute('aria-hidden', 'true') 157 | 158 | typeof pop === 'string' ? popover.innerHTML = pop : popover.appendChild(pop) 159 | 160 | return popover 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "poppy", 3 | "version": "1.0.3", 4 | "description": "A tiny, fast, configurable popover.", 5 | "source": "index.js", 6 | "module": "dist/poppy.es.js", 7 | "jsnext:main": "dist/poppy.es.js", 8 | "main": "dist/poppy.js", 9 | "umd:main": "dist/poppy.umd.js", 10 | "files": [ 11 | "dist" 12 | ], 13 | "scripts": { 14 | "build": "microbundle build --external none", 15 | "watch": "microbundle watch", 16 | "publish": "npm publish" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+ssh://git@github.com/estrattonbailey/poppy.git" 21 | }, 22 | "author": "estrattonbailey", 23 | "keywords": [ 24 | "tooltip", 25 | "popover", 26 | "popovers" 27 | ], 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/estrattonbailey/poppy/issues" 31 | }, 32 | "homepage": "https://github.com/estrattonbailey/poppy#readme", 33 | "devDependencies": { 34 | "microbundle": "^0.11.0" 35 | }, 36 | "dependencies": { 37 | "tackjs": "^1.1.2", 38 | "tarry.js": "^0.3.1" 39 | } 40 | } 41 | --------------------------------------------------------------------------------