diff --git a/README.md b/README.md index a6ac5f2..7cfb847 100644 --- a/README.md +++ b/README.md @@ -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 @@ -37,6 +38,7 @@ npm install react react-dom react-portal --save ``` ## Usage + ```jsx import React from 'react'; import ReactDOM from 'react-dom'; @@ -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. diff --git a/lib/portal.js b/lib/portal.js index fcfb7cc..9f44981 100644 --- a/lib/portal.js +++ b/lib/portal.js @@ -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; @@ -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); @@ -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, diff --git a/test/portal_spec.js b/test/portal_spec.js index d634eca..2237438 100644 --- a/test/portal_spec.js +++ b/test/portal_spec.js @@ -29,13 +29,6 @@ describe('react-portal', () => { /*eslint-enable */ }); - it('should append portal with children to the document.body', () => { - const wrapper = mount(

Hi

); - 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(

Hi

); assert.equal(document.body.childElementCount, 0); @@ -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(

Hi

); + 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(

Hi

); + 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(

Hi

); + assert.equal(modalLayer.getElementsByTagName('p')[0].textContent, 'Hi'); + wrapper.setProps({ isOpened: false }); + assert(!modalLayer.getElementsByTagName('p')[0]); + }); + }); + }); + }); });