Skip to content

Commit f799110

Browse files
committed
[added] Portal component; replaces OverlayMixin
1 parent 1a4c5ec commit f799110

File tree

5 files changed

+167
-13
lines changed

5 files changed

+167
-13
lines changed

Diff for: docs/src/ComponentsPage.js

+17
Original file line numberDiff line numberDiff line change
@@ -788,8 +788,24 @@ const ComponentsPage = React.createClass({
788788
<PropTable component='InputBase'/>
789789
</div>
790790

791+
{/* Utilities */}
792+
<div className='bs-docs-section'>
793+
<h1 id='utilities' className='page-header'>Utilities <small>Portal</small></h1>
794+
795+
<h2 id='utilities-Portal'>Portal</h2>
796+
<p>
797+
A Component that renders its children into a new React "subtree" or <code>container</code>. The Portal component kind of like the React
798+
equivillent to jQuery's <code>.appendTo()</code>, which is helpful for components that need to be appended to a DOM node other than
799+
the component's direct parent. The Modal, and Overlay components use the Portal component internally.
800+
</p>
801+
<h3 id='utilities-props'>Props</h3>
802+
803+
<PropTable component='Portal'/>
804+
</div>
791805
</div>
792806

807+
808+
793809
<div className='col-md-3'>
794810
<Affix
795811
className='bs-docs-sidebar hidden-print'
@@ -829,6 +845,7 @@ const ComponentsPage = React.createClass({
829845
<NavItem href='#glyphicons' key={24}>Glyphicons</NavItem>
830846
<NavItem href='#tables' key={25}>Tables</NavItem>
831847
<NavItem href='#input' key={26}>Input</NavItem>
848+
<NavItem href='#utilities' key={26}>Input</NavItem>
832849
</Nav>
833850
<a className='back-to-top' href='#top'>
834851
Back to top

Diff for: src/OverlayMixin.js

+35-13
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,42 @@
11
import React from 'react';
22
import CustomPropTypes from './utils/CustomPropTypes';
33
import domUtils from './utils/domUtils';
4+
import deprecationWarning from './utils/deprecationWarning';
45

5-
export default {
6+
export const OverlayMixin = {
67
propTypes: {
8+
79
container: CustomPropTypes.mountable
810
},
911

10-
componentWillUnmount() {
11-
this._unrenderOverlay();
12-
if (this._overlayTarget) {
13-
this.getContainerDOMNode()
14-
.removeChild(this._overlayTarget);
15-
this._overlayTarget = null;
16-
}
12+
13+
componentDidMount() {
14+
this._renderOverlay();
1715
},
1816

1917
componentDidUpdate() {
2018
this._renderOverlay();
2119
},
2220

23-
componentDidMount() {
24-
this._renderOverlay();
21+
componentWillUnmount() {
22+
this._unrenderOverlay();
23+
this._mountOverlayTarget();
2524
},
2625

2726
_mountOverlayTarget() {
28-
this._overlayTarget = document.createElement('div');
29-
this.getContainerDOMNode()
30-
.appendChild(this._overlayTarget);
27+
if (!this._overlayTarget) {
28+
this._overlayTarget = document.createElement('div');
29+
this.getContainerDOMNode()
30+
.appendChild(this._overlayTarget);
31+
}
32+
},
33+
34+
_unmountOverlayTarget() {
35+
if (this._overlayTarget) {
36+
this.getContainerDOMNode()
37+
.removeChild(this._overlayTarget);
38+
this._overlayTarget = null;
39+
}
3140
},
3241

3342
_renderOverlay() {
@@ -39,10 +48,12 @@ export default {
3948

4049
// Save reference to help testing
4150
if (overlay !== null) {
51+
this._mountOverlayTarget();
4252
this._overlayInstance = React.render(overlay, this._overlayTarget);
4353
} else {
4454
// Unrender if the component is null for transitions to null
4555
this._unrenderOverlay();
56+
this._unmountOverlayTarget();
4657
}
4758
},
4859

@@ -67,3 +78,14 @@ export default {
6778
return React.findDOMNode(this.props.container) || domUtils.ownerDocument(this).body;
6879
}
6980
};
81+
82+
export default {
83+
84+
...OverlayMixin,
85+
86+
componentWillMount() {
87+
deprecationWarning(
88+
'Overlay mixin', 'the `<Portal/>` Component'
89+
, 'http://react-bootstrap.github.io/components.html#modals-custom');
90+
}
91+
};

Diff for: src/Portal.js

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import React from 'react';
2+
import CustomPropTypes from './utils/CustomPropTypes';
3+
import { OverlayMixin } from './OverlayMixin';
4+
5+
let Portal = React.createClass({
6+
7+
displayName: 'Portal',
8+
9+
propTypes: {
10+
/**
11+
* The DOM Node that the Component will render it's children into
12+
*/
13+
container: CustomPropTypes.mountable
14+
},
15+
16+
// we use the mixin for now, to avoid duplicating a bunch of code.
17+
// when the deprecation is removed we need to move the logic here from OverlayMixin
18+
mixins: [ OverlayMixin ],
19+
20+
renderOverlay() {
21+
if (!this.props.children) {
22+
return null;
23+
}
24+
25+
return React.Children.only(this.props.children);
26+
},
27+
28+
render() {
29+
return null;
30+
}
31+
});
32+
33+
34+
export default Portal;

Diff for: src/index.js

+3
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ import utils from './utils';
5454
import Well from './Well';
5555
import styleMaps from './styleMaps';
5656

57+
import Portal from './Portal';
58+
5759
export default {
5860
Accordion,
5961
Affix,
@@ -98,6 +100,7 @@ export default {
98100
Pager,
99101
Pagination,
100102
Popover,
103+
Portal,
101104
ProgressBar,
102105
Row,
103106
SplitButton,

Diff for: test/PortalSpec.js

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import React from 'react';
2+
import ReactTestUtils from 'react/lib/ReactTestUtils';
3+
import Portal from '../src/Portal';
4+
5+
describe('Portal', function () {
6+
let instance;
7+
8+
let Overlay = React.createClass({
9+
render() {
10+
return (
11+
<div>
12+
<Portal ref='p' {...this.props}>{this.props.overlay}</Portal>
13+
</div>
14+
);
15+
},
16+
getOverlayDOMNode(){
17+
return this.refs.p.getOverlayDOMNode();
18+
}
19+
});
20+
21+
afterEach(function() {
22+
if (instance && ReactTestUtils.isCompositeComponent(instance) && instance.isMounted()) {
23+
React.unmountComponentAtNode(React.findDOMNode(instance));
24+
}
25+
});
26+
27+
it('Should render overlay into container (DOMNode)', function() {
28+
let container = document.createElement('div');
29+
30+
instance = ReactTestUtils.renderIntoDocument(
31+
<Overlay container={container} overlay={<div id="test1" />} />
32+
);
33+
34+
assert.equal(container.querySelectorAll('#test1').length, 1);
35+
});
36+
37+
it('Should render overlay into container (ReactComponent)', function() {
38+
let Container = React.createClass({
39+
render() {
40+
return <Overlay container={this} overlay={<div id="test1" />} />;
41+
}
42+
});
43+
44+
instance = ReactTestUtils.renderIntoDocument(
45+
<Container />
46+
);
47+
48+
assert.equal(React.findDOMNode(instance).querySelectorAll('#test1').length, 1);
49+
});
50+
51+
it('Should not render a null overlay', function() {
52+
let Container = React.createClass({
53+
render() {
54+
return <Overlay ref='overlay' container={this} overlay={null} />;
55+
}
56+
});
57+
58+
instance = ReactTestUtils.renderIntoDocument(
59+
<Container />
60+
);
61+
62+
assert.equal(instance.refs.overlay.getOverlayDOMNode(), null);
63+
});
64+
65+
it('Should render only an overlay', function() {
66+
let OnlyOverlay = React.createClass({
67+
render() {
68+
return <Portal ref='p' {...this.props}>{this.props.overlay}</Portal>;
69+
}
70+
});
71+
72+
let overlayInstance = ReactTestUtils.renderIntoDocument(
73+
<OnlyOverlay overlay={<div id="test1" />} />
74+
);
75+
76+
assert.equal(overlayInstance.refs.p.getOverlayDOMNode().nodeName, 'DIV');
77+
});
78+
});

0 commit comments

Comments
 (0)