Skip to content

Commit

Permalink
Render to Layer component (+ squashed fixes)
Browse files Browse the repository at this point in the history
  • Loading branch information
chrismcv committed Nov 3, 2015
1 parent ac357ec commit 50a0942
Show file tree
Hide file tree
Showing 2 changed files with 127 additions and 0 deletions.
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
"url": "https://github.com/callemall/material-ui/issues"
},
"homepage": "http://material-ui.com/",
"dependencies": {
"lodash.throttle": "~3.0.4",
"lodash.debounce": "~3.1.1"
},
"peerDependencies": {
"inline-style-prefixer": "^0.3.3",
"react": "^0.14.0",
Expand Down
123 changes: 123 additions & 0 deletions src/render-to-layer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import React from 'react';
import ReactDOM from 'react-dom';
import Events from './utils/events';
import Dom from './utils/dom';
import debounce from 'lodash.debounce';

// heavily inspired by https://github.com/Khan/react-components/blob/master/js/layered-component-mixin.jsx
const RenderToLayer = React.createClass({

componentDidMount() {
this._renderLayer();
},

componentDidUpdate() {
this._renderLayer();
},

componentWillUnmount() {
this._unbindClickAway();
if (this._layer) {
this._unrenderLayer();
}
},

_checkClickAway(e) {
if (!this.canClickAway) {
return;
}
const el = this._layer;
if (e.target !== el &&
!Dom.isDescendant(el, e.target) &&
document.documentElement.contains(e.target)) {
if (this.props.componentClickAway) {
this.props.componentClickAway(e);
}
}
},

_preventClickAway(e) {
if (e.detail === this) {
return;
}
this.canClickAway = false;
},

_allowClickAway() {
this.canClickAway = true;
},

getLayer() {
return this._layer;
},

render() {
return null;
},

_renderLayer() {
if (this.props.open) {
if (!this._layer) {
this._layer = document.createElement('div');
document.body.appendChild(this._layer);
}
this._bindClickAway();
if (this.reactUnmount) {
this.reactUnmount.cancel();
}
} else if (this._layer) {
this._unbindClickAway();
this._unrenderLayer();
} else {
return;
}

// By calling this method in componentDidMount() and
// componentDidUpdate(), you're effectively creating a "wormhole" that
// funnels React's hierarchical updates through to a DOM node on an
// entirely different part of the page.

const layerElement = this.props.render();
// Renders can return null, but React.render() doesn't like being asked
// to render null. If we get null back from renderLayer(), just render
// a noscript element, like React does when an element's render returns
// null.
if (layerElement === null) {
this.layerElement = ReactDOM.unstable_renderSubtreeIntoContainer (this, <noscript />, this._layer);
} else {
this.layerElement = ReactDOM.unstable_renderSubtreeIntoContainer(this, layerElement, this._layer);
}
},

_unrenderLayer: function() {
if (this.layerWillUnmount) {
this.layerWillUnmount(this._layer);
}
if (!this.reactUnmount)
this.reactUnmount = debounce(() => {
if (this._layer) {
ReactDOM.unmountComponentAtNode(this._layer);
document.body.removeChild(this._layer);
this._layer = null;
}
}, 1000);
this.reactUnmount();
},

_bindClickAway() {
this.canClickAway = true;
Events.on(document, 'mousedown', this._checkClickAway);
Events.on(document, 'touchend', this._checkClickAway);
Events.on(document, 'popOverOnShow', this._preventClickAway);
Events.on(document, 'popOverOnHide', this._allowClickAway);
},

_unbindClickAway() {
Events.off(document, 'mousedown', this._checkClickAway);
Events.off(document, 'touchend', this._checkClickAway);
Events.off(document, 'popOverOnShow', this._preventClickAway);
Events.off(document, 'popOverOnHide', this._allowClickAway);
},
});

export default RenderToLayer;

0 comments on commit 50a0942

Please sign in to comment.