├── tests ├── .eslintrc └── index-test.js ├── docs └── magic.png ├── .gitignore ├── src ├── index.js ├── Overlay.js └── Popup.js ├── demo └── src │ ├── style.css │ └── index.js ├── .travis.yml ├── nwb.config.js ├── package.json └── README.md /tests/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /docs/magic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taskworld/react-overlay-popup/HEAD/docs/magic.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | /demo/dist 3 | /es6 4 | /lib 5 | /node_modules 6 | /umd 7 | npm-debug.log 8 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 2 | import Overlay from './Overlay' 3 | import Popup from './Popup' 4 | 5 | export { Popup, Overlay } 6 | -------------------------------------------------------------------------------- /demo/src/style.css: -------------------------------------------------------------------------------- 1 | .Demo { 2 | border: 2px solid #65B8BE; 3 | padding: 1em; 4 | font-family: sans-serif; 5 | } 6 | 7 | .Demo h1 { 8 | margin-top: 0; 9 | } 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: node_js 4 | node_js: 5 | - 4.2 6 | 7 | cache: 8 | directories: 9 | - node_modules 10 | 11 | before_install: 12 | - npm install codecov.io coveralls 13 | 14 | after_success: 15 | - cat ./coverage/lcov.info | ./node_modules/codecov.io/bin/codecov.io.js 16 | - cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js 17 | 18 | branches: 19 | only: 20 | - master 21 | -------------------------------------------------------------------------------- /nwb.config.js: -------------------------------------------------------------------------------- 1 | module.exports = nwb => ({ 2 | // Let nwb know this is a React component module when generic build commands 3 | // are used. 4 | type: 'react-component', 5 | 6 | build: { 7 | umd: false, 8 | global: '', 9 | externals: { 10 | 'react': 'React' 11 | }, 12 | jsNext: true 13 | }, 14 | 15 | webpack: { 16 | extra: { 17 | plugins: [ 18 | new nwb.webpack.IgnorePlugin(/react\/lib\/(?:ExecutionEnvironment|ReactContext)/) 19 | ] 20 | } 21 | } 22 | }) 23 | -------------------------------------------------------------------------------- /src/Overlay.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | 4 | var Overlay = React.createClass({ 5 | propTypes: { 6 | children: React.PropTypes.node 7 | }, 8 | 9 | componentDidMount: function () { 10 | this.updateOverlay() 11 | }, 12 | 13 | componentDidUpdate: function () { 14 | this.updateOverlay() 15 | }, 16 | 17 | componentWillUnmount: function () { 18 | if (this._element) { 19 | ReactDOM.unmountComponentAtNode(this._element) 20 | if (this._element.parentNode) { 21 | this._element.parentNode.removeChild(this._element) 22 | } 23 | } 24 | }, 25 | 26 | updateOverlay: function () { 27 | if (!this._element) { 28 | this._element = document.createElement('div') 29 | this._element.className = Overlay.CONTAINER_CLASS_NAME 30 | document.body.appendChild(this._element) 31 | } 32 | var overlay = ( 33 |
34 | {this.props.children} 35 |
36 | ) 37 | ReactDOM.render(overlay, this._element) 38 | }, 39 | 40 | render: function () { 41 | return ( 42 | 43 | ) 44 | } 45 | }) 46 | 47 | Overlay.CONTAINER_CLASS_NAME = 'tw-overlay-container' 48 | Overlay.OVERLAY_CLASS_NAME = 'tw-overlay' 49 | 50 | module.exports = Overlay 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-overlay-popup", 3 | "version": "4.0.2", 4 | "description": "Overlay and popup components for React.", 5 | "main": "lib/index.js", 6 | "jsnext:main": "es6/index.js", 7 | "files": [ 8 | "css", 9 | "es6", 10 | "lib", 11 | "umd" 12 | ], 13 | "standard": { 14 | "parser": "babel-eslint" 15 | }, 16 | "scripts": { 17 | "build": "nwb build", 18 | "clean": "nwb clean", 19 | "start": "nwb serve", 20 | "test": "nwb test" 21 | }, 22 | "dependencies": { 23 | "invariant": "^2.2.0" 24 | }, 25 | "peerDependencies": { 26 | "react": "0.14.x || ^15.0.0-rc.2" 27 | }, 28 | "devDependencies": { 29 | "babel-eslint": "^6.0.0", 30 | "enzyme": "^2.2.0", 31 | "nwb": "^0.9.2", 32 | "phantomjs": "^2.1.3", 33 | "phantomjs-prebuilt": "^2.1.7", 34 | "react": "0.14.x || ^15.0.0-rc.2", 35 | "react-addons-test-utils": "^0.14.7", 36 | "react-dom": "0.14.x", 37 | "standard": "^6.0.5" 38 | }, 39 | "author": "Thai Pangsakulyanont @ Taskworld ", 40 | "homepage": "https://github.com/taskworld/react-overlay-popup#readme", 41 | "license": "MIT", 42 | "repository": { 43 | "type": "git", 44 | "url": "git+https://github.com/taskworld/react-overlay-popup.git" 45 | }, 46 | "keywords": [ 47 | "react-component" 48 | ], 49 | "directories": { 50 | "doc": "docs", 51 | "test": "tests" 52 | }, 53 | "bugs": { 54 | "url": "https://github.com/taskworld/react-overlay-popup/issues" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/index-test.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import React from 'react' 3 | import { mount } from 'enzyme' 4 | 5 | import { Overlay, Popup } from '../src' 6 | 7 | describe('react-overlay-popup', function () { 8 | describe('', function () { 9 | it('puts overlay in detached DOM node', function () { 10 | mount( 11 |
12 | 13 |
14 |
15 |
, 16 | { attachTo: document.body } 17 | ) 18 | assertElementExists('#a') 19 | assertElementExists('#b') 20 | assertElementNotExists('#b #a') 21 | }) 22 | }) 23 | 24 | // x=120 25 | // | x=150 26 | // | | 27 | // 28 | // +---+ -- y=100 29 | // | |: 30 | // | |: 31 | // | |: 32 | // +---+: 33 | // ::::: -- y = 150 34 | // +-+ -- y = 150 35 | // | |: 36 | // +-+: 37 | // ::: 38 | describe('', function () { 39 | it('sets position relative to element', function () { 40 | mount( 41 |
42 | 43 |
44 |
45 |
, 46 | { attachTo: document.body } 47 | ) 48 | assertElementExists('#x') 49 | assertElementExists('#y') 50 | const y = document.querySelector('#y') 51 | const container = y.parentNode 52 | assert.equal(container.style.top, '160px') 53 | assert.equal(container.style.left, '120px') 54 | }) 55 | }) 56 | }) 57 | 58 | function assertElementNotExists (selector) { 59 | assert(!document.querySelector(selector), 'Element "' + selector + '" must not exist') 60 | } 61 | function assertElementExists (selector) { 62 | assert(document.querySelector(selector), 'Element "' + selector + '" must exist') 63 | } 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-overlay-popup 2 | 3 | [![Travis][build-badge]][build] 4 | [![npm package][npm-badge]][npm] 5 | [![codecov.io](https://codecov.io/github/taskworld/react-overlay-popup/coverage.svg?branch=master)](https://codecov.io/github/taskworld/react-overlay-popup?branch=master) 6 | 7 | 8 | Overlay and Popup components for React. Brought to you by Taskworld Inc. 9 | 10 | 11 | Synopsis 12 | -------- 13 | 14 | ```jsx 15 | const { Overlay, Popup } = require('react-overlay-popup'); 16 | ``` 17 | 18 | See: [src/App.js](src/App.js) for example. 19 | 20 | 21 | Overlay 22 | ------- 23 | 24 | Anything inside `` will be added to a separate DOM tree appended to `document.body`. 25 | Just that. 26 | 27 | 28 | Popup 29 | ----- 30 | 31 | A special kind of Overlay that automatically positions itself relative to its parent. 32 | The position is specified through the `strategy` prop. 33 | 34 | ![The strategy and the formula behind the magic.](docs/magic.png) 35 | 36 | The `gap` prop specifies how far should the popup be to its parent. 37 | 38 | 39 | 40 | ## Prerequisites 41 | 42 | You will need the following things properly installed on your computer. 43 | 44 | * [Git](http://git-scm.com/) 45 | * [Node.js](http://nodejs.org/) (with npm) 46 | * [nwb](https://github.com/insin/nwb/) - `npm install -g nwb` 47 | 48 | 49 | ## Installation 50 | 51 | * `git clone ` this repository 52 | * change into the new directory 53 | * `npm install` 54 | 55 | 56 | ## Running / Development 57 | 58 | * `nwb serve` will run the component's demo app 59 | * Visit the demo at [http://localhost:3000](http://localhost:3000) 60 | 61 | 62 | ### Running Tests 63 | 64 | * `nwb test` will run the tests once 65 | * `nwb test --server` will run the tests on every change 66 | 67 | 68 | ### Building 69 | 70 | * `nwb build` 71 | 72 | [build-badge]: https://img.shields.io/travis/taskworld/react-overlay-popup/master.svg?style=flat-square 73 | [build]: https://travis-ci.org/taskworld/react-overlay-popup 74 | 75 | [npm-badge]: https://img.shields.io/npm/v/react-overlay-popup.svg?style=flat-square 76 | [npm]: https://www.npmjs.org/package/react-overlay-popup 77 | -------------------------------------------------------------------------------- /demo/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM, { render } from 'react-dom' 3 | import './style.css' 4 | 5 | import { Overlay, Popup } from '../../src' 6 | 7 | var _strategies = [ 8 | 'bottom left', 'bottom center', 'bottom right', 9 | 'top left', 'top center', 'top right', 10 | 'left top', 'left center', 'left bottom', 11 | 'right top', 'right center', 'right bottom' 12 | ] 13 | 14 | const Demo = React.createClass({ 15 | getInitialState: function () { 16 | return { 17 | x: 100, 18 | y: 100, 19 | strategy: _strategies[0] 20 | } 21 | }, 22 | 23 | componentDidMount: function () { 24 | window.addEventListener('mousemove', this.move) 25 | }, 26 | 27 | move: function (e) { 28 | var button = ReactDOM.findDOMNode(this.refs.button) 29 | this.setState({ 30 | x: e.clientX - button.offsetWidth / 2, 31 | y: e.clientY - button.offsetHeight / 2 32 | }) 33 | }, 34 | 35 | nextStrategy: function () { 36 | this.setState({ 37 | strategy: _strategies[(_strategies.indexOf(this.state.strategy) + 1) % _strategies.length] 38 | }) 39 | }, 40 | 41 | render: function () { 42 | return ( 43 |
44 |

Taskworld Popup and Overlay component.

45 |

react-overlay-popup is a simple component kit that helps with creating popup menus and stuff.

46 |
    47 |
  • {''} renders its children in a separate DOM node.
  • 48 |
  • {''} renders its children in a separate DOM node with fixed position relative to the parent element.
  • 49 |
50 | 51 | 52 | {''} example: I am plucked out to another layer! 53 | 54 | 55 | 66 |
67 | ) 68 | } 69 | }) 70 | 71 | render(, document.querySelector('#demo')) 72 | -------------------------------------------------------------------------------- /src/Popup.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import invar from 'invariant' 4 | import Overlay from './Overlay' 5 | 6 | var _strategies = { } 7 | 8 | function calculate (vp, lp, lc, kp, kc, Δv) { 9 | return vp + kp * lp - kc * lc + Δv 10 | } 11 | 12 | function calculateWithFallback (vp, lp, lc, kp, kc, vm, Δv) { 13 | var primary = kp !== kc 14 | var vc = calculate(vp, lp, lc, kp, kc, Δv) 15 | 16 | if (primary) { 17 | if ((kp > 0.5 && vc + lc > vm) || (kp < 0.5 && vc < 0)) { 18 | return calculate(vp, lp, lc, 1 - kp, 1 - kc, -Δv) 19 | } else { 20 | return vc 21 | } 22 | } else { 23 | if (vc < 0) { 24 | return calculate(vp, lp, lc, 0, 0, Δv) 25 | } else if (vc + lc > vm) { 26 | return calculate(vp, lp, lc, 1, 1, Δv) 27 | } else { 28 | return vc 29 | } 30 | } 31 | } 32 | 33 | function createStrategy (parentX, childX, parentY, childY, gapX, gapY) { 34 | return function (parent, child, options) { 35 | var rect = parent.getBoundingClientRect() 36 | var childWidth = child.offsetWidth 37 | var childHeight = child.offsetHeight 38 | 39 | var left = calculateWithFallback(rect.left, rect.width, childWidth, parentX, childX, window.innerWidth, gapX * options.gap) 40 | var top = calculateWithFallback(rect.top, rect.height, childHeight, parentY, childY, window.innerHeight, gapY * options.gap) 41 | 42 | setPosition(child, left, top) 43 | } 44 | } 45 | 46 | function createStrategyFromFunction (positionFunc) { 47 | return function (parent, child, options) { 48 | var position = positionFunc(parent, child, options) 49 | setPosition(child, position.left, position.top) 50 | } 51 | } 52 | 53 | function setPosition (child, left, top) { 54 | child.style.visibility = 'visible' 55 | child.style.left = left + 'px' 56 | child.style.top = top + 'px' 57 | } 58 | 59 | _strategies['top left'] = createStrategy(0, 0, 0, 1, 0, -1) 60 | _strategies['top'] = _strategies['top center'] = createStrategy(0.5, 0.5, 0, 1, 0, -1) 61 | _strategies['top right'] = createStrategy(1, 1, 0, 1, 0, -1) 62 | 63 | _strategies['bottom left'] = createStrategy(0, 0, 1, 0, 0, 1) 64 | _strategies['bottom'] = _strategies['bottom center'] = createStrategy(0.5, 0.5, 1, 0, 0, 1) 65 | _strategies['bottom right'] = createStrategy(1, 1, 1, 0, 0, 1) 66 | 67 | _strategies['left top'] = createStrategy(0, 1, 0, 0, -1, 0) 68 | _strategies['left'] = _strategies['left center'] = createStrategy(0, 1, 0.5, 0.5, -1, 0) 69 | _strategies['left bottom'] = createStrategy(0, 1, 1, 1, -1, 0) 70 | 71 | _strategies['right top'] = createStrategy(1, 0, 0, 0, 1, 0) 72 | _strategies['right'] = _strategies['right center'] = createStrategy(1, 0, 0.5, 0.5, 1, 0) 73 | _strategies['right bottom'] = createStrategy(1, 0, 1, 1, 1, 0) 74 | 75 | var Popup = React.createClass({ 76 | propTypes: { 77 | strategy: React.PropTypes.oneOfType([ 78 | React.PropTypes.string, 79 | React.PropTypes.func 80 | ]), 81 | children: React.PropTypes.node, 82 | gap: React.PropTypes.number 83 | }, 84 | 85 | componentDidMount: function () { 86 | this.reposition() 87 | window.addEventListener('resize', this.reposition, true) 88 | }, 89 | 90 | componentDidUpdate: function () { 91 | this.reposition() 92 | }, 93 | 94 | componentWillUnmount: function () { 95 | window.removeEventListener('resize', this.reposition, true) 96 | }, 97 | 98 | reposition: function () { 99 | var parent = ReactDOM.findDOMNode(this).parentNode 100 | var child = ReactDOM.findDOMNode(this.refs.popup) 101 | 102 | if (parent && child) { 103 | var strategy 104 | 105 | if (typeof this.props.strategy === 'function') { 106 | strategy = createStrategyFromFunction(this.props.strategy) 107 | } 108 | 109 | if (typeof this.props.strategy === 'string') { 110 | invar( 111 | _strategies.hasOwnProperty(this.props.strategy), 112 | 'The strategy %s must exist.', 113 | this.props.strategy 114 | ) 115 | strategy = _strategies[this.props.strategy] 116 | } 117 | 118 | invar(typeof strategy === 'function', 'Strategy must be a function.') 119 | strategy(parent, child, { gap: this.props.gap || 0 }) 120 | } 121 | }, 122 | 123 | render: function () { 124 | return ( 125 | 126 |
127 | {this.props.children} 128 |
129 |
130 | ) 131 | } 132 | }) 133 | 134 | Popup.POPUP_CLASS_NAME = 'tw-popup' 135 | 136 | module.exports = Popup 137 | --------------------------------------------------------------------------------