Skip to content

Commit

Permalink
Add ability to use specified layers for portals
Browse files Browse the repository at this point in the history
Sometimes it is very useful to be able to explicitly
set elements for Portal to be rendered into.

Example:

```jsx
const modalLayer = document.getElementById('modal')

<Portal target={modalLayer}>
  Move me to the modal layer, please.
</Portal>
```
  • Loading branch information
luchkonikita committed Jul 18, 2016
1 parent af6cd34 commit 2a924c0
Show file tree
Hide file tree
Showing 3 changed files with 44 additions and 10 deletions.
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
5 changes: 3 additions & 2 deletions lib/portal.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,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 +145,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 Down Expand Up @@ -177,6 +177,7 @@ Portal.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,
Expand Down
42 changes: 35 additions & 7 deletions test/portal_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,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 +224,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]);
});
});
});
});
});

0 comments on commit 2a924c0

Please sign in to comment.