Skip to content

Commit

Permalink
Merge pull request #1 from luchkonikita/add-multiple-layers-support
Browse files Browse the repository at this point in the history
Add ability to use specified layers for portals
  • Loading branch information
luchkonikita authored Jul 19, 2016
2 parents af6cd34 + ffe9b4d commit f5a61f6
Show file tree
Hide file tree
Showing 7 changed files with 81 additions and 37 deletions.
2 changes: 1 addition & 1 deletion .babelrc
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"presets": ["react", "es2015"],
"presets": ["react", "es2015", "stage-0"],
"plugins": ["add-module-exports"],
"env": {
"development": {
Expand Down
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ React-portal
## Features

- transports its child into a new React component and appends it to the **document.body** (creates a new independent React tree)
- transports its child into a new React component and appends it to specified DOM element (**document.body** by default)
- creates a new independent React tree
- can be opened by the prop **isOpened**
- can be opened after a click on an element that you pass through the prop **openByClickOn** (and then it takes care of the open/close state)
- doesn't leave any mess in DOM after closing
Expand Down Expand Up @@ -37,6 +38,7 @@ npm install react react-dom react-portal --save
```

## Usage

```jsx
import React from 'react';
import ReactDOM from 'react-dom';
Expand Down Expand Up @@ -92,6 +94,9 @@ with `onClick` handler that triggers portal opening. **How to close the portal t

### Optional

#### target: HTMLElement
The element you want portal to be appended to.

#### closeOnEsc: bool
If true, the portal can be closed by the key ESC.

Expand Down
45 changes: 23 additions & 22 deletions lib/portal.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,27 @@ const KEYCODES = {

export default class Portal extends React.Component {

static defaultProps = {
onOpen: () => {},
onClose: () => {},
onUpdate: () => {},
};

static propTypes = {
className: React.PropTypes.string,
style: React.PropTypes.object,
children: React.PropTypes.element.isRequired,
target: React.PropTypes.instanceOf(window.HTMLElement),
openByClickOn: React.PropTypes.element,
closeOnEsc: React.PropTypes.bool,
closeOnOutsideClick: React.PropTypes.bool,
isOpened: React.PropTypes.bool,
onOpen: React.PropTypes.func,
onClose: React.PropTypes.func,
beforeClose: React.PropTypes.func,
onUpdate: React.PropTypes.func,
};

constructor() {
super();
this.state = { active: false };
Expand Down Expand Up @@ -90,7 +111,7 @@ export default class Portal extends React.Component {
const resetPortalState = () => {
if (this.node) {
ReactDOM.unmountComponentAtNode(this.node);
document.body.removeChild(this.node);
(this.props.target || document.body).removeChild(this.node);
}
this.portal = null;
this.node = null;
Expand Down Expand Up @@ -145,7 +166,7 @@ export default class Portal extends React.Component {
this.node = document.createElement('div');
// apply CSS before the node is added to the DOM to avoid needless reflows
this.applyClassNameAndStyle(props);
document.body.appendChild(this.node);
(props.target || document.body).appendChild(this.node);
} else {
// update CSS when new props arrive
this.applyClassNameAndStyle(props);
Expand All @@ -172,23 +193,3 @@ export default class Portal extends React.Component {
return null;
}
}

Portal.propTypes = {
className: React.PropTypes.string,
style: React.PropTypes.object,
children: React.PropTypes.element.isRequired,
openByClickOn: React.PropTypes.element,
closeOnEsc: React.PropTypes.bool,
closeOnOutsideClick: React.PropTypes.bool,
isOpened: React.PropTypes.bool,
onOpen: React.PropTypes.func,
onClose: React.PropTypes.func,
beforeClose: React.PropTypes.func,
onUpdate: React.PropTypes.func,
};

Portal.defaultProps = {
onOpen: () => {},
onClose: () => {},
onUpdate: () => {},
};
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"build:examples": "npm run clean && npm run build:examples:webpack",
"build:examples:webpack": "cross-env NODE_ENV=production webpack --config webpack.config.prod.babel.js",
"clean": "rimraf build",
"test": "mocha",
"test": "mocha ./test/portal_spec.js",
"lint": "mocha test/eslint_spec.js",
"prepublish": "cross-env NODE_ENV=production npm run build"
},
Expand All @@ -46,6 +46,7 @@
"babel-preset-es2015": "^6.3.13",
"babel-preset-react": "^6.3.13",
"babel-preset-react-hmre": "^1.0.1",
"babel-preset-stage-0": "^6.5.0",
"babel-register": "^6.8.0",
"cross-env": "^1.0.7",
"enzyme": "^2.3.0",
Expand Down
5 changes: 5 additions & 0 deletions test/mocha.js
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
import setup from './setup';

process.env.NODE_ENV = 'test';
// Calling setup befere any components are imported in specs
// Otherwise window object can not be used in static methods
setup();
49 changes: 37 additions & 12 deletions test/portal_spec.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
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 setup from './setup';

describe('react-portal', () => {
let React;
beforeEach(() => {
// Set up JSDOM
global.document = jsdom.jsdom('<!doctype html><html><body></body></html>');
global.window = document.defaultView;
global.navigator = { userAgent: 'node.js' };
// Enzyme library uses React
setup();
/*eslint-disable */
React = require('react');
/*eslint-enable */
Expand All @@ -29,13 +26,6 @@ describe('react-portal', () => {
/*eslint-enable */
});

it('should append portal with children to the document.body', () => {
const wrapper = mount(<Portal isOpened><p>Hi</p></Portal>);
assert.equal(wrapper.instance().node.firstElementChild.tagName, 'P');
assert.equal(document.body.lastElementChild, wrapper.instance().node);
assert.equal(document.body.childElementCount, 1);
});

it('should open when this.openPortal() is called (used to programmatically open portal)', () => {
const wrapper = mount(<Portal><p>Hi</p></Portal>);
assert.equal(document.body.childElementCount, 0);
Expand Down Expand Up @@ -231,4 +221,39 @@ describe('react-portal', () => {
assert.equal(document.body.childElementCount, 0);
});
});

describe('target', () => {
context('when target is not set', () => {
it('should append portal with children to the document.body', () => {
const wrapper = mount(<Portal isOpened><p>Hi</p></Portal>);
assert.equal(wrapper.instance().node.firstElementChild.tagName, 'P');
assert.equal(document.body.lastElementChild, wrapper.instance().node);
assert.equal(document.body.childElementCount, 1);
});
});

context('when target is set', () => {
context('when layer passed to component as a prop', () => {
it('should append portal with children to the target', () => {
const modalLayer = document.createElement('div');
document.body.appendChild(modalLayer);

const wrapper = mount(<Portal isOpened target={modalLayer}><p>Hi</p></Portal>);
assert.equal(modalLayer.getElementsByTagName('p')[0].textContent, 'Hi');
assert.equal(modalLayer.lastElementChild, wrapper.instance().node);
assert.equal(modalLayer.childElementCount, 1);
});

it('should remove portal from the target when isOpened set to false', () => {
const modalLayer = document.createElement('div');
document.body.appendChild(modalLayer);

const wrapper = mount(<Portal isOpened target={modalLayer}><p>Hi</p></Portal>);
assert.equal(modalLayer.getElementsByTagName('p')[0].textContent, 'Hi');
wrapper.setProps({ isOpened: false });
assert(!modalLayer.getElementsByTagName('p')[0]);
});
});
});
});
});
7 changes: 7 additions & 0 deletions test/setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import jsdom from 'jsdom';

export default function setup() {
global.document = jsdom.jsdom('<!doctype html><html><body></body></html>');
global.window = document.defaultView;
global.navigator = { userAgent: 'node.js' };
}

0 comments on commit f5a61f6

Please sign in to comment.