Skip to content

Commit

Permalink
layer clickaway option for render-to-layer/popover
Browse files Browse the repository at this point in the history
  • Loading branch information
chrismcv committed Dec 3, 2015
1 parent 03cdf02 commit 1141870
Show file tree
Hide file tree
Showing 8 changed files with 103 additions and 38 deletions.
7 changes: 7 additions & 0 deletions docs/src/app/components/pages/components/popover.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,13 @@ let PopoverPage = React.createClass({
header: 'default: no-op',
desc: 'This is a callback that fires when the popover thinks it should close. (e.g. clickAway or offScreen)',
},
{
name: 'useLayerForClickAway',
type: 'bool',
header: 'default: false',
desc: 'If true, the popover will render on top of an invisible layer, which ' +
'will prevent clicks to the underlying elements, and trigger an onRequestClose(clickAway) event.',
},
{
name: 'zDepth',
type: 'oneOf [0,1,2,3,4,5]',
Expand Down
2 changes: 1 addition & 1 deletion src/menus/icon-menu.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ const IconMenu = React.createClass({
open={open}
anchorEl={anchorEl}
childContextTypes={this.constructor.childContextTypes}
useLayerForClickAway={false}
onRequestClose={this.close}
context={this.context}>
{menu}
Expand Down Expand Up @@ -200,7 +201,6 @@ const IconMenu = React.createClass({
if (this.props.closeOnItemTouchTap) {
let isKeyboard = Events.isKeyboard(event);


this._timeout = setTimeout(() => {
if (!this.isMounted()) {
return;
Expand Down
1 change: 1 addition & 0 deletions src/menus/menu-item.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ const MenuItem = React.createClass({
anchorOrigin={{horizontal: 'right', vertical: 'top'}}
anchorEl={this.state.anchorEl}
open={this.state.open}
useLayerForClickAway={false}
onRequestClose={this._onRequestClose}>
<Menu desktop={desktop} disabled={disabled} style={nestedMenuStyle}>
{React.Children.map(menuItems, this._cloneMenuItem)}
Expand Down
22 changes: 18 additions & 4 deletions src/popover/popover.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import Transitions from '../styles/transitions';
import Paper from '../paper';
import throttle from 'lodash.throttle';
import AutoPrefix from '../styles/auto-prefix';
import DefaultRawTheme from '../styles/raw-themes/light-raw-theme';
import ThemeManager from '../styles/theme-manager';

const Popover = React.createClass({
mixins: [
Expand All @@ -27,6 +29,7 @@ const Popover = React.createClass({
open: React.PropTypes.bool,
style: React.PropTypes.object,
targetOrigin: PropTypes.origin,
useLayerForClickAway: React.PropTypes.bool,
zDepth: PropTypes.zDepth,
},

Expand All @@ -46,6 +49,7 @@ const Popover = React.createClass({
vertical: 'top',
horizontal: 'left',
},
useLayerForClickAway:true,
zDepth: 1,
};
},
Expand All @@ -55,28 +59,38 @@ const Popover = React.createClass({

return {
open: this.props.open,
muiTheme: this.context.muiTheme ? this.context.muiTheme : ThemeManager.getMuiTheme(DefaultRawTheme),
};
},

contextTypes: {
//for passing default theme context to children
childContextTypes: {
muiTheme: React.PropTypes.object,
},

getChildContext() {
return {
};
},

windowListeners: {
resize: 'setPlacementThrottled',
scroll: 'setPlacementThrottled',
},

componentWillReceiveProps(nextProps) {
componentWillReceiveProps(nextProps, nextContext) {
let newMuiTheme = nextContext.muiTheme ? nextContext.muiTheme : this.state.muiTheme;
if (nextProps.open !== this.state.open) {
if (nextProps.open) {
this.anchorEl = nextProps.anchorEl || this.props.anchorEl;
this.setState({
open: true,
muiTheme: newMuiTheme,
});
} else {
this.setState({
open: false,
muiTheme: newMuiTheme,
}, () => {
this._animateClose();
});
Expand Down Expand Up @@ -118,7 +132,7 @@ const Popover = React.createClass({
position: 'fixed',
top: anchor.top,
left: anchor.left,
zIndex: 20,
zIndex: this.state.muiTheme.zIndex.popover,
opacity:1,
overflow:'auto',
maxHeight:'100%',
Expand Down Expand Up @@ -201,7 +215,7 @@ const Popover = React.createClass({
AutoPrefix.set(innerInnerInner.style, 'transform', `scaleY(${value})`);
AutoPrefix.set(rootStyle, 'opacity', value);
AutoPrefix.set(innerStyle, 'opacity', value);
AutoPrefix.set(innerInnerInner, 'opacity', value);
AutoPrefix.set(innerInnerInner.style, 'opacity', value);
AutoPrefix.set(el.style, 'opacity', value);
},

Expand Down
100 changes: 67 additions & 33 deletions src/render-to-layer.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,49 @@
import React from 'react';
import ReactDOM from 'react-dom';
import Events from './utils/events';
import Dom from './utils/dom';
import debounce from 'lodash.debounce';
import Dom from './utils/dom';
import DefaultRawTheme from './styles/raw-themes/light-raw-theme';
import ThemeManager from './styles/theme-manager';

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

propTypes: {
componentClickAway: React.PropTypes.func,
open: React.PropTypes.bool.isRequired,
useLayerForClickAway: React.PropTypes.bool,
},

getDefaultProps() {
return {
useLayerForClickAway:true,
};
},

getInitialState() {
return {
muiTheme: this.context.muiTheme ? this.context.muiTheme : ThemeManager.getMuiTheme(DefaultRawTheme),
};
},

//for passing default theme context to children
childContextTypes: {
muiTheme: React.PropTypes.object,
},

getChildContext() {
return {
muiTheme: this.state.muiTheme,
};
},

//to update theme inside state whenever a new theme is passed down
//from the parent / owner using context
componentWillReceiveProps(nextProps, nextContext) {
let newMuiTheme = nextContext.muiTheme ? nextContext.muiTheme : this.state.muiTheme;
this.setState({muiTheme: newMuiTheme});
},

componentDidMount() {
this._renderLayer();
},
Expand All @@ -16,36 +53,25 @@ const RenderToLayer = React.createClass({
},

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

_checkClickAway(e) {
if (!this.canClickAway) {
onClickAway(e) {
if (e.defaultPrevented) {
return;
}

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

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

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

getLayer() {
return this._layer;
},
Expand All @@ -60,12 +86,34 @@ const RenderToLayer = React.createClass({
this._layer = document.createElement('div');
document.body.appendChild(this._layer);
}
this._bindClickAway();
if (this.props.useLayerForClickAway) {
this._layer.addEventListener('touchstart', this.onClickAway);
this._layer.addEventListener('click', this.onClickAway);
this._layer.style.position = 'fixed';
this._layer.style.top = 0;
this._layer.style.bottom = 0;
this._layer.style.left = 0;
this._layer.style.right = 0;
this._layer.style.zIndex = this.state.muiTheme.zIndex.layer;
}
else {
setTimeout(() => {
window.addEventListener('touchstart', this.onClickAway);
window.addEventListener('click', this.onClickAway);
}, 0);
}
if (this.reactUnmount) {
this.reactUnmount.cancel();
}
} else if (this._layer) {
this._unbindClickAway();
if (this.props.useLayerForClickAway) {
this._layer.style.position = 'relative';
this._layer.removeEventListener('touchstart', this.onClickAway);
this._layer.removeEventListener('click', this.onClickAway);
} else {
window.removeEventListener('touchstart', this.onClickAway);
window.removeEventListener('click', this.onClickAway);
}
this._unrenderLayer();
} else {
return;
Expand Down Expand Up @@ -103,20 +151,6 @@ const RenderToLayer = React.createClass({
this.reactUnmount();
},

_bindClickAway() {
if (typeof (this.canClickAway) === 'undefined') {
this.canClickAway = true;
}
Events.on(window, 'focus', this._checkClickAway);
Events.on(document, 'mousedown', this._checkClickAway);
Events.on(document, 'touchend', this._checkClickAway);
},

_unbindClickAway() {
Events.off(window, 'focus', this._checkClickAway);
Events.off(document, 'mousedown', this._checkClickAway);
Events.off(document, 'touchend', this._checkClickAway);
},
});

export default RenderToLayer;
4 changes: 4 additions & 0 deletions src/styles/raw-themes/dark-raw-theme.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import Spacing from '../spacing';
export default {
spacing: Spacing,
fontFamily: 'Roboto, sans-serif',
zIndex: {
layer: 20,
popover: 20,
},
palette: {
primary1Color: Colors.cyan700,
primary2Color: Colors.cyan700,
Expand Down
4 changes: 4 additions & 0 deletions src/styles/raw-themes/light-raw-theme.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import Spacing from '../spacing';
export default {
spacing: Spacing,
fontFamily: 'Roboto, sans-serif',
zIndex: {
layer: 20,
popover: 20,
},
palette: {
primary1Color: Colors.cyan500,
primary2Color: Colors.cyan700,
Expand Down
1 change: 1 addition & 0 deletions src/styles/theme-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ export default {
borderColor: rawTheme.palette.borderColor,
},
isRtl: false,
zIndex: rawTheme.zIndex,
};

//add properties to objects inside 'returnObj' that depend on existing properties
Expand Down

0 comments on commit 1141870

Please sign in to comment.