-
Trigger Animations, or any arbitrary function, before removing the portal from the DOM, animates out on both click outside and on esc press
+
+
Trigger Animations, or any arbitrary function,{' '}
+ before removing the portal from the DOM, animates out{' '}
+ on both click outside and on esc press
);
}
-
- toggleLoadingBar(e) {
- this.setState({isPortalOpened: !this.state.isPortalOpened});
- }
-
- changeValue(e) {
- this.setState({someValue: Math.random().toString(36).substring(7)});
- }
-
}
ReactDOM.render(
-
This could be a loading bar...
-
This portal is opened by the prop isOpened.
-
... when openByClickOn is not enough.
-
Notice, that by default you cannot close this by ESC or an outside click.
-
- );
- }
-
-}
-
-LoadingBar.propTypes = {
- children: React.PropTypes.element,
- closePortal: React.PropTypes.func
-};
+export default () =>
+
+
This could be a loading bar...
+
This portal is opened by the prop isOpened.
+
... when openByClickOn is not enough.
+
Notice, that by default you cannot close this by ESC or an outside click.
+
;
diff --git a/examples/pseudomodal.js b/examples/pseudomodal.js
index 058b381..fee0275 100644
--- a/examples/pseudomodal.js
+++ b/examples/pseudomodal.js
@@ -1,22 +1,20 @@
import React from 'react';
-export default class PseudoModal extends React.Component {
-
+export default class PseudoModal extends React.Component { // eslint-disable-line
render() {
return (
-
+
{this.props.children}
);
}
-
}
PseudoModal.propTypes = {
children: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.element),
- React.PropTypes.element
+ React.PropTypes.element,
]),
- closePortal: React.PropTypes.func
+ closePortal: React.PropTypes.func,
};
diff --git a/lib/portal.js b/lib/portal.js
index 4d6ed84..d126f35 100644
--- a/lib/portal.js
+++ b/lib/portal.js
@@ -1,17 +1,17 @@
import React from 'react';
-import ReactDOM, {findDOMNode} from 'react-dom';
+import ReactDOM, { findDOMNode } from 'react-dom';
import CSSPropertyOperations from 'react/lib/CSSPropertyOperations';
import shallowCompare from 'react/lib/shallowCompare';
const KEYCODES = {
- ESCAPE: 27
+ ESCAPE: 27,
};
export default class Portal extends React.Component {
constructor() {
super();
- this.state = {active: false};
+ this.state = { active: false };
this.handleWrapperClick = this.handleWrapperClick.bind(this);
this.closePortal = this.closePortal.bind(this);
this.handleOutsideMouseClick = this.handleOutsideMouseClick.bind(this);
@@ -56,6 +56,10 @@ export default class Portal extends React.Component {
}
}
+ shouldComponentUpdate(nextProps, nextState) {
+ return shallowCompare(this, nextProps, nextState);
+ }
+
componentWillUnmount() {
if (this.props.closeOnEsc) {
document.removeEventListener('keydown', this.handleKeydown);
@@ -69,32 +73,6 @@ export default class Portal extends React.Component {
this.closePortal(true);
}
- shouldComponentUpdate(nextProps, nextState) {
- return shallowCompare(this, nextProps, nextState);
- }
-
- renderPortal(props) {
- if (!this.node) {
- this.node = document.createElement('div');
- if (props.className) {
- this.node.className = props.className;
- }
- if (props.style) {
- CSSPropertyOperations.setValueForStyles(this.node, props.style);
- }
- document.body.appendChild(this.node);
- }
- this.portal = ReactDOM.unstable_renderSubtreeIntoContainer(this, React.cloneElement(props.children, {closePortal: this.closePortal}), this.node, this.props.onUpdate);
- }
-
- render() {
- if (this.props.openByClickOn) {
- return React.cloneElement(this.props.openByClickOn, {onClick: this.handleWrapperClick});
- } else {
- return null;
- }
- }
-
handleWrapperClick(e) {
e.preventDefault();
e.stopPropagation();
@@ -103,9 +81,8 @@ export default class Portal extends React.Component {
}
openPortal(props = this.props) {
- this.setState({active: true});
+ this.setState({ active: true });
this.renderPortal(props);
-
this.props.onOpen(this.node);
}
@@ -118,7 +95,7 @@ export default class Portal extends React.Component {
this.portal = null;
this.node = null;
if (isUnmounted !== true) {
- this.setState({active: false});
+ this.setState({ active: false });
}
};
@@ -149,6 +126,31 @@ export default class Portal extends React.Component {
}
}
+ renderPortal(props) {
+ if (!this.node) {
+ this.node = document.createElement('div');
+ if (props.className) {
+ this.node.className = props.className;
+ }
+ if (props.style) {
+ CSSPropertyOperations.setValueForStyles(this.node, props.style);
+ }
+ document.body.appendChild(this.node);
+ }
+ this.portal = ReactDOM.unstable_renderSubtreeIntoContainer(
+ this,
+ React.cloneElement(props.children, { closePortal: this.closePortal }),
+ this.node,
+ this.props.onUpdate
+ );
+ }
+
+ render() {
+ if (this.props.openByClickOn) {
+ return React.cloneElement(this.props.openByClickOn, { onClick: this.handleWrapperClick });
+ }
+ return null;
+ }
}
Portal.propTypes = {
@@ -162,11 +164,11 @@ Portal.propTypes = {
onOpen: React.PropTypes.func,
onClose: React.PropTypes.func,
beforeClose: React.PropTypes.func,
- onUpdate: React.PropTypes.func
+ onUpdate: React.PropTypes.func,
};
Portal.defaultProps = {
onOpen: () => {},
onClose: () => {},
- onUpdate: () => {}
+ onUpdate: () => {},
};
diff --git a/package.json b/package.json
index 1de59e3..399621a 100644
--- a/package.json
+++ b/package.json
@@ -16,10 +16,10 @@
"author": "Vojtech Miksu
",
"license": "MIT",
"scripts": {
- "start": "node devServer.js",
+ "start": "node devServerIndex.js",
"build": "mkdir -p build && babel ./lib/portal.js --out-file ./build/portal.js",
"build:examples": "npm run clean && npm run build:examples:webpack",
- "build:examples:webpack": "cross-env NODE_ENV=production webpack --config webpack.config.prod.js",
+ "build:examples:webpack": "cross-env NODE_ENV=production webpack --config webpack.config.prod.babel.js",
"clean": "rimraf build",
"test": "mocha",
"lint": "mocha test/eslint_spec.js",
@@ -38,31 +38,34 @@
"transportation"
],
"devDependencies": {
- "babel-cli": "^6.4.0",
- "babel-core": "^6.4.0",
- "babel-eslint": "^4.1.6",
+ "babel-cli": "^6.8.0",
+ "babel-core": "^6.8.0",
+ "babel-eslint": "^6.0.4",
"babel-loader": "^6.2.1",
- "babel-plugin-add-module-exports": "^0.1.2",
+ "babel-plugin-add-module-exports": "^0.2.1",
"babel-preset-es2015": "^6.3.13",
"babel-preset-react": "^6.3.13",
"babel-preset-react-hmre": "^1.0.1",
- "babel-register": "^6.3.13",
+ "babel-register": "^6.8.0",
"cross-env": "^1.0.7",
- "enzyme": "^2.2.0",
- "eslint": "^1.10.3",
- "eslint-plugin-react": "^3.14.0",
+ "enzyme": "^2.3.0",
+ "eslint": "^2.9.0",
+ "eslint-config-airbnb": "^9.0.1",
+ "eslint-plugin-import": "^1.8.0",
+ "eslint-plugin-jsx-a11y": "^1.2.0",
+ "eslint-plugin-react": "^5.1.1",
"express": "^4.13.3",
- "jsdom": "^7.2.2",
+ "jsdom": "^9.0.0",
"mocha": "^2.3.4",
- "mocha-eslint": "^1.0.0",
- "react": "^15.0.1",
- "react-addons-test-utils": "^15.0.1",
- "react-dom": "^15.0.1",
+ "mocha-eslint": "^2.0.2",
+ "react": "^15.0.2",
+ "react-addons-test-utils": "^15.0.2",
+ "react-dom": "^15.0.2",
"rimraf": "^2.5.0",
- "sinon": "^1.17.2",
+ "sinon": "^1.17.4",
"tween.js": "^16.3.1",
- "webpack": "^1.12.11",
- "webpack-dev-middleware": "^1.4.0",
+ "webpack": "^1.13.0",
+ "webpack-dev-middleware": "^1.6.1",
"webpack-hot-middleware": "^2.6.0"
}
}
diff --git a/test/portal_spec.js b/test/portal_spec.js
index 8f0820e..73881c2 100644
--- a/test/portal_spec.js
+++ b/test/portal_spec.js
@@ -1,11 +1,9 @@
import jsdom from 'jsdom';
import Portal from '../lib/portal';
import assert from 'assert';
-import {spy} from 'sinon';
-import {render, unmountComponentAtNode} from 'react-dom';
-import {
- mount
-} from 'enzyme';
+import { spy } from 'sinon';
+import { render, unmountComponentAtNode } from 'react-dom';
+import { mount } from 'enzyme';
describe('react-portal', () => {
let React;
@@ -13,9 +11,11 @@ describe('react-portal', () => {
// Set up JSDOM
global.document = jsdom.jsdom('');
global.window = document.defaultView;
- global.navigator = {userAgent: 'node.js'};
+ global.navigator = { userAgent: 'node.js' };
// Enzyme library uses React
+ /*eslint-disable */
React = require('react');
+ /*eslint-enable */
});
it('propTypes.children should be required', () => {
@@ -47,7 +47,7 @@ describe('react-portal', () => {
const wrapper = mount(Hi
);
assert.equal(document.body.childElementCount, 0);
// Enzyme docs say it merges previous props but without children, react complains
- wrapper.setProps({isOpened: true, children: Hi
});
+ wrapper.setProps({ isOpened: true, children: Hi
});
assert.equal(document.body.lastElementChild, wrapper.instance().node);
assert.equal(document.body.childElementCount, 1);
});
@@ -56,12 +56,11 @@ describe('react-portal', () => {
const wrapper = mount(Hi
);
assert.equal(document.body.lastElementChild, wrapper.instance().node);
assert.equal(document.body.childElementCount, 1);
- wrapper.setProps({isOpened: false, children: Hi
});
+ wrapper.setProps({ isOpened: false, children: Hi
});
assert.equal(document.body.childElementCount, 0);
});
it('should pass Portal.closePortal to child component', () => {
- let wrapper;
let closePortal;
/*eslint-disable*/
const Child = (props) => {
@@ -69,7 +68,7 @@ describe('react-portal', () => {
return Hi
;
};
/*eslint-enable*/
- wrapper = mount();
+ const wrapper = mount();
assert.equal(closePortal, wrapper.instance().closePortal);
});
@@ -79,21 +78,21 @@ describe('react-portal', () => {
});
it('should add inline style to portal\'s wrapping node', () => {
- mount(Hi
);
+ mount(Hi
);
assert.equal(document.body.lastElementChild.style.color, 'blue');
});
describe('callbacks', () => {
it('should call props.beforeClose() if passed when calling Portal.closePortal()', () => {
- const props = {isOpened: true, beforeClose: spy()};
+ const props = { isOpened: true, beforeClose: spy() };
const wrapper = mount(Hi
);
wrapper.instance().closePortal();
assert(props.beforeClose.calledOnce);
assert(props.beforeClose.calledWith(wrapper.instance().node));
});
- it('should call props.beforeClose() only once even if closePortal is called multiple times', () => {
- const props = {isOpened: true, beforeClose: spy((node, cb) => cb())};
+ it('should call props.beforeClose() only 1x even if closePortal is called more times', () => {
+ const props = { isOpened: true, beforeClose: spy((node, cb) => cb()) };
const wrapper = mount(Hi
);
wrapper.instance().closePortal();
wrapper.instance().closePortal();
@@ -101,37 +100,37 @@ describe('react-portal', () => {
});
it('should call props.onOpen() when portal opens', () => {
- const props = {isOpened: true, onOpen: spy()};
+ const props = { isOpened: true, onOpen: spy() };
const wrapper = mount(Hi
);
assert(props.onOpen.calledOnce);
assert(props.onOpen.calledWith(wrapper.instance().node));
});
it('should not call props.onOpen() when portal receives props', () => {
- const props = {isOpened: true, onOpen: spy(), className: 'old'};
+ const props = { isOpened: true, onOpen: spy(), className: 'old' };
const wrapper = mount(Hi
);
assert(props.onOpen.calledOnce);
- wrapper.setProps({isOpened: true, children: Hi
, className: 'new'});
+ wrapper.setProps({ isOpened: true, children: Hi
, className: 'new' });
assert(props.onOpen.calledOnce);
});
it('should call props.onUpdate() when portal is opened or receives props', () => {
- const props = {isOpened: true, onUpdate: spy(), className: 'old'};
+ const props = { isOpened: true, onUpdate: spy(), className: 'old' };
const wrapper = mount(Hi
);
assert(props.onUpdate.calledOnce);
- wrapper.setProps({isOpened: true, children: Hi
, className: 'new'});
+ wrapper.setProps({ isOpened: true, children: Hi
, className: 'new' });
assert(props.onUpdate.calledTwice);
});
it('should call props.onClose() when portal closes', () => {
- const props = {isOpened: true, onClose: spy()};
+ const props = { isOpened: true, onClose: spy() };
const wrapper = mount(Hi
);
wrapper.instance().closePortal();
assert(props.onClose.calledOnce);
});
it('should call props.onClose() only once even if closePortal is called multiple times', () => {
- const props = {isOpened: true, onClose: spy()};
+ const props = { isOpened: true, onClose: spy() };
const wrapper = mount(Hi
);
wrapper.instance().closePortal();
wrapper.instance().closePortal();
@@ -140,7 +139,7 @@ describe('react-portal', () => {
it('should call props.onClose() only once when portal closes and then is unmounted', () => {
const div = document.createElement('div');
- const props = {isOpened: true, onClose: spy()};
+ const props = { isOpened: true, onClose: spy() };
const component = render(Hi
, div);
component.closePortal();
unmountComponentAtNode(div);
@@ -149,7 +148,7 @@ describe('react-portal', () => {
it('should call props.onClose() only once when directly unmounting', () => {
const div = document.createElement('div');
- const props = {isOpened: true, onClose: spy()};
+ const props = { isOpened: true, onClose: spy() };
render(Hi
, div);
unmountComponentAtNode(div);
assert(props.onClose.calledOnce);
@@ -157,7 +156,7 @@ describe('react-portal', () => {
it('should not call this.setState() if portal is unmounted', () => {
const div = document.createElement('div');
- const props = {isOpened: true};
+ const props = { isOpened: true };
const wrapper = render(Hi
, div);
spy(wrapper, 'setState');
unmountComponentAtNode(div);
@@ -174,7 +173,7 @@ describe('react-portal', () => {
});
it('should render the props.openByClickOn element', () => {
- const text = `open by click on me`;
+ const text = 'open by click on me';
const openByClickOn = ;
const wrapper = mount(Hi
);
assert(wrapper.text(text));
@@ -200,7 +199,7 @@ describe('react-portal', () => {
assert.equal(document.body.childElementCount, 1);
// Had to use actual event since simulating wasn't working due to subtree
// rendering and actual component returns null
- const kbEvent = new window.KeyboardEvent('keydown', {keyCode: 27});
+ const kbEvent = new window.KeyboardEvent('keydown', { keyCode: 27 });
document.dispatchEvent(kbEvent);
assert.equal(document.body.childElementCount, 0);
});
@@ -210,12 +209,12 @@ describe('react-portal', () => {
assert.equal(document.body.childElementCount, 1);
// Should not close when outside click isn't a main click
- const rightClickMouseEvent = new window.MouseEvent('mouseup', {view: window, button: 2});
+ const rightClickMouseEvent = new window.MouseEvent('mouseup', { view: window, button: 2 });
document.dispatchEvent(rightClickMouseEvent);
assert.equal(document.body.childElementCount, 1);
// Should close when outside click is a main click (typically left button click)
- const leftClickMouseEvent = new window.MouseEvent('mouseup', {view: window, button: 0});
+ const leftClickMouseEvent = new window.MouseEvent('mouseup', { view: window, button: 0 });
document.dispatchEvent(leftClickMouseEvent);
assert.equal(document.body.childElementCount, 0);
});
diff --git a/webpack.config.dev.js b/webpack.config.dev.babel.js
similarity index 64%
rename from webpack.config.dev.js
rename to webpack.config.dev.babel.js
index 508a458..a632021 100644
--- a/webpack.config.dev.js
+++ b/webpack.config.dev.babel.js
@@ -1,20 +1,20 @@
-var path = require('path');
-var webpack = require('webpack');
+import path from 'path';
+import webpack from 'webpack';
-module.exports = {
+export default {
devtool: 'cheap-module-eval-source-map',
entry: [
'webpack-hot-middleware/client',
- './examples/index'
+ './examples/index',
],
output: {
path: path.join(__dirname, 'build'),
filename: 'examples_bundle.js',
- publicPath: '/static/'
+ publicPath: '/static/',
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
- new webpack.NoErrorsPlugin()
+ new webpack.NoErrorsPlugin(),
],
module: {
loaders: [{
@@ -22,8 +22,8 @@ module.exports = {
loaders: ['babel'],
include: [
path.join(__dirname, 'examples'),
- path.join(__dirname, 'lib')
- ]
- }]
- }
+ path.join(__dirname, 'lib'),
+ ],
+ }],
+ },
};
diff --git a/webpack.config.prod.js b/webpack.config.prod.babel.js
similarity index 62%
rename from webpack.config.prod.js
rename to webpack.config.prod.babel.js
index 1e14f05..98779fd 100644
--- a/webpack.config.prod.js
+++ b/webpack.config.prod.babel.js
@@ -1,28 +1,28 @@
-var path = require('path');
-var webpack = require('webpack');
+import path from 'path';
+import webpack from 'webpack';
-module.exports = {
+export default {
devtool: 'source-map',
entry: [
- './examples/index'
+ './examples/index',
],
output: {
path: path.join(__dirname, 'build'),
filename: 'examples_bundle.js',
- publicPath: '/static/'
+ publicPath: '/static/',
},
plugins: [
new webpack.optimize.OccurenceOrderPlugin(),
new webpack.DefinePlugin({
'process.env': {
- 'NODE_ENV': JSON.stringify('production')
- }
+ NODE_ENV: JSON.stringify('production'),
+ },
}),
new webpack.optimize.UglifyJsPlugin({
compressor: {
- warnings: false
- }
- })
+ warnings: false,
+ },
+ }),
],
module: {
loaders: [{
@@ -30,8 +30,8 @@ module.exports = {
loaders: ['babel'],
include: [
path.join(__dirname, 'examples'),
- path.join(__dirname, 'lib')
- ]
- }]
- }
+ path.join(__dirname, 'lib'),
+ ],
+ }],
+ },
};