├── 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 | ,
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 | ,
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 | [](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 | 
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 |
--------------------------------------------------------------------------------