From a3e4c39e6aba44581e56d207ab8db5b311d3c4e3 Mon Sep 17 00:00:00 2001 From: Chris McVittie Date: Wed, 2 Dec 2015 12:22:00 +0000 Subject: [PATCH] making clickaway more logical --- .../components/pages/components/popover.jsx | 20 ++++++ src/menus/icon-menu.jsx | 18 +++++- src/menus/menu-item.jsx | 2 +- src/popover/popover.jsx | 12 +++- src/render-to-layer.js | 64 +++++++++---------- 5 files changed, 80 insertions(+), 36 deletions(-) diff --git a/docs/src/app/components/pages/components/popover.jsx b/docs/src/app/components/pages/components/popover.jsx index dddde07f030284..a5173a76238b5e 100644 --- a/docs/src/app/components/pages/components/popover.jsx +++ b/docs/src/app/components/pages/components/popover.jsx @@ -61,6 +61,13 @@ let PopoverPage = React.createClass({ desc: 'If true, the popover (potentially) ignores targetOrigin and anchorOrigin to make itself fit on screen,' + 'which is useful for mobile devices.', }, + { + name: 'closeOnClickAway', + type: 'bool', + header: 'default: true', + desc: 'If true, the popover will close when another part of the page is used. If you have nested popovers, you ' + + 'may wish to set this to false and deal with click-away manually', + }, { name: 'open', type: 'bool', @@ -81,6 +88,19 @@ let PopoverPage = React.createClass({ }, ], }, + { + name: 'Methods', + infoArray: [ + { + name: 'contains', + type: 'bool', + header: '(DomElement el)', + desc: + 'This method will tell you whether the element passed through is contained in the Popover' + + 'This is useful because you can\'t always tell as the popover content is on a different DOM Tree', + }, + ], + }, ]; return ( diff --git a/src/menus/icon-menu.jsx b/src/menus/icon-menu.jsx index 98b5760cd5b781..e50afa5a6c6fd4 100644 --- a/src/menus/icon-menu.jsx +++ b/src/menus/icon-menu.jsx @@ -1,6 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import StylePropable from '../mixins/style-propable'; +import ClickAwayable from '../mixins/click-awayable'; import Events from '../utils/events'; import PropTypes from '../utils/prop-types'; import Menu from '../menus/menu'; @@ -10,7 +11,10 @@ import Popover from '../popover/popover'; const IconMenu = React.createClass({ - mixins: [StylePropable], + mixins: [ + StylePropable, + ClickAwayable, + ], contextTypes: { muiTheme: React.PropTypes.object, @@ -67,6 +71,16 @@ const IconMenu = React.createClass({ }; }, + componentClickAway(e) { + if (!this.state.open) { + return; + } + + if (!(this.refs.popover && this.refs.popover.contains(e.target))) { + this.close(false); + } + }, + getInitialState() { return { muiTheme: this.context.muiTheme ? this.context.muiTheme : ThemeManager.getMuiTheme(DefaultRawTheme), @@ -156,9 +170,11 @@ const IconMenu = React.createClass({ style={mergedRootStyles}> {iconButton} {React.Children.map(menuItems, this._cloneMenuItem)} @@ -213,7 +214,6 @@ const MenuItem = React.createClass({ let props = { onTouchTap: (e) => { - this._onRequestClose(); if (item.props.onTouchTap) { item.props.onTouchTap(e); } diff --git a/src/popover/popover.jsx b/src/popover/popover.jsx index 214bca4e7f09e2..9205d9dcac208f 100644 --- a/src/popover/popover.jsx +++ b/src/popover/popover.jsx @@ -10,6 +10,7 @@ import Paper from '../paper'; import throttle from 'lodash.throttle'; import AutoPrefix from '../styles/auto-prefix'; import ContextPure from '../mixins/context-pure'; +import Dom from '../utils/dom'; const Popover = React.createClass({ mixins: [ @@ -24,6 +25,7 @@ const Popover = React.createClass({ animated: React.PropTypes.bool, autoCloseWhenOffScreen: React.PropTypes.bool, canAutoPosition: React.PropTypes.bool, + closeOnClickAway: React.PropTypes.bool, children: React.PropTypes.object, className: React.PropTypes.string, open: React.PropTypes.bool, @@ -183,6 +185,14 @@ const Popover = React.createClass({ }); }, + contains(el) { + if (!this.refs.layer || !this.refs.layer.getLayer()) { + return false; + } + let layer = this.refs.layer.getLayer().children[0]; + return Dom.isDescendant(layer, el); + }, + _animateClose() { if (!this.refs.layer || !this.refs.layer.getLayer()) { return; @@ -218,7 +228,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); }, diff --git a/src/render-to-layer.js b/src/render-to-layer.js index af2a0597a3263c..487746a6d1f141 100644 --- a/src/render-to-layer.js +++ b/src/render-to-layer.js @@ -1,12 +1,23 @@ 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'; // heavily inspired by https://github.com/Khan/react-components/blob/master/js/layered-component-mixin.jsx const RenderToLayer = React.createClass({ + propTypes: { + closeOnClickAway: React.PropTypes.bool, + componentClickAway: React.PropTypes.func, + open: React.PropTypes.bool.isRequired, + }, + + getDefaultProps() { + return { + closeOnClickAway:true, + }; + }, + componentDidMount() { this._renderLayer(); }, @@ -16,36 +27,26 @@ const RenderToLayer = React.createClass({ }, componentWillUnmount() { - this._unbindClickAway(); if (this._layer) { this._unrenderLayer(); } }, - _checkClickAway(e) { - if (!this.canClickAway) { + + onClickAway(e) { + if (e.defaltPrevented) { 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.closeOnClickAway && 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; }, @@ -60,12 +61,23 @@ const RenderToLayer = React.createClass({ this._layer = document.createElement('div'); document.body.appendChild(this._layer); } - this._bindClickAway(); + if (this.props.closeOnClickAway) { + 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 = 20; + } if (this.reactUnmount) { this.reactUnmount.cancel(); } } else if (this._layer) { - this._unbindClickAway(); + this._layer.style.position = 'relative'; + this._layer.removeEventListener('touchstart', this.onClickAway); + this._layer.removeEventListener('click', this.onClickAway); this._unrenderLayer(); } else { return; @@ -103,20 +115,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;