diff --git a/src/ClickAwayListener.jsx b/src/ClickAwayListener.jsx
new file mode 100644
index 00000000000000..3f2becc06e029e
--- /dev/null
+++ b/src/ClickAwayListener.jsx
@@ -0,0 +1,57 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import events from './utils/events';
+
+const isDescendant = (el, target) => {
+ if (target !== null) {
+ return el === target || isDescendant(el, target.parentNode);
+ }
+ return false;
+};
+
+const clickAwayEvents = ['mousedown', 'touchstart'];
+const bind = (callback) => clickAwayEvents.forEach((event) => events.on(document, event, callback));
+const unbind = (callback) => clickAwayEvents.forEach((event) => events.off(document, event, callback));
+
+export default class ClickAwayListener extends React.Component {
+
+ static propTypes = {
+ children: React.PropTypes.node,
+ onClickAway: React.PropTypes.any,
+ };
+
+ componentDidMount() {
+ if (this.props.onClickAway) {
+ bind(this.handleClickAway);
+ }
+ }
+
+ componentDidUpdate(prevProps) {
+ if (prevProps.onClickAway !== this.props.onClickAway) {
+ unbind(this.handleClickAway);
+ if (this.props.onClickAway) {
+ bind(this.handleClickAway);
+ }
+ }
+ }
+
+ componentWillUnmount() {
+ unbind(this.handleClickAway);
+ }
+
+ handleClickAway = (event) => {
+ if (event.defaultPrevented) {
+ return;
+ }
+
+ const el = ReactDOM.findDOMNode(this);
+
+ if (document.documentElement.contains(event.target) && !isDescendant(el, event.target)) {
+ this.props.onClickAway(event);
+ }
+ };
+
+ render() {
+ return this.props.children;
+ }
+}
diff --git a/src/menus/menu.jsx b/src/menus/menu.jsx
index 528bbfaaed09fc..7473f8b47cd1c1 100644
--- a/src/menus/menu.jsx
+++ b/src/menus/menu.jsx
@@ -1,7 +1,7 @@
import React from 'react';
import ReactDOM from 'react-dom';
import update from 'react-addons-update';
-import ClickAwayable from '../mixins/click-awayable';
+import ClickAwayListener from '../ClickAwayListener';
import autoPrefix from '../styles/auto-prefix';
import Transitions from '../styles/transitions';
import KeyCode from '../utils/key-code';
@@ -132,10 +132,6 @@ const Menu = React.createClass({
muiTheme: React.PropTypes.object,
},
- mixins: [
- ClickAwayable,
- ],
-
getDefaultProps() {
return {
autoWidth: true,
@@ -564,24 +560,23 @@ const Menu = React.createClass({
});
return (
-
-
-
+
+
+ {newChildren}
+
+
+
+
);
},
diff --git a/src/mixins/click-awayable.js b/src/mixins/click-awayable.js
deleted file mode 100644
index 5f2350029a8414..00000000000000
--- a/src/mixins/click-awayable.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import ReactDOM from 'react-dom';
-import Events from '../utils/events';
-import Dom from '../utils/dom';
-
-export default {
-
- //When the component mounts, listen to click events and check if we need to
- //Call the componentClickAway function.
- componentDidMount() {
- if (!this.manuallyBindClickAway) this._bindClickAway();
- },
-
- componentWillUnmount() {
- this._unbindClickAway();
- },
-
- _checkClickAway(event) {
- if (this.isMounted()) {
- const el = ReactDOM.findDOMNode(this);
-
- // Check if the target is inside the current component
- if (event.target !== el &&
- !Dom.isDescendant(el, event.target) &&
- document.documentElement.contains(event.target)) {
- if (this.componentClickAway) this.componentClickAway(event);
- }
- }
- },
-
- _bindClickAway() {
- // On touch-enabled devices, both events fire, and the handler is called twice,
- // but it's fine since all operations for which the mixin is used
- // are idempotent.
- Events.on(document, 'mouseup', this._checkClickAway);
- Events.on(document, 'touchend', this._checkClickAway);
- },
-
- _unbindClickAway() {
- Events.off(document, 'mouseup', this._checkClickAway);
- Events.off(document, 'touchend', this._checkClickAway);
- },
-
-};
diff --git a/src/mixins/index.js b/src/mixins/index.js
index 272b08160ca445..21ba89e0b4b987 100644
--- a/src/mixins/index.js
+++ b/src/mixins/index.js
@@ -1,13 +1,10 @@
-import ClickAwayable from './click-awayable';
import StylePropable from './style-propable';
import StyleResizable from './style-resizable';
-export {ClickAwayable};
export {StylePropable};
export {StyleResizable};
export default {
- ClickAwayable,
StylePropable,
StyleResizable,
};
diff --git a/src/snackbar.jsx b/src/snackbar.jsx
index 09a0b3626bddee..56776148ec0fd2 100644
--- a/src/snackbar.jsx
+++ b/src/snackbar.jsx
@@ -1,6 +1,6 @@
import React from 'react';
import Transitions from './styles/transitions';
-import ClickAwayable from './mixins/click-awayable';
+import ClickAwayListener from './ClickAwayListener';
import FlatButton from './flat-button';
import getMuiTheme from './styles/getMuiTheme';
import StyleResizable from './mixins/style-resizable';
@@ -138,7 +138,6 @@ const Snackbar = React.createClass({
mixins: [
StyleResizable,
- ClickAwayable,
],
getInitialState() {
@@ -158,12 +157,8 @@ const Snackbar = React.createClass({
componentDidMount() {
if (this.state.open) {
- this._setAutoHideTimer();
-
- //Only Bind clickaway after transition finishes
- this.timerTransitionId = setTimeout(() => {
- this._bindClickAway();
- }, 400);
+ this.setAutoHideTimer();
+ this.setTransitionTimer();
}
},
@@ -200,15 +195,10 @@ const Snackbar = React.createClass({
componentDidUpdate(prevProps, prevState) {
if (prevState.open !== this.state.open) {
if (this.state.open) {
- this._setAutoHideTimer();
-
- //Only Bind clickaway after transition finishes
- this.timerTransitionId = setTimeout(() => {
- this._bindClickAway();
- }, 400);
+ this.setAutoHideTimer();
+ this.setTransitionTimer();
} else {
clearTimeout(this.timerAutoHideId);
- this._unbindClickAway();
}
}
},
@@ -217,7 +207,6 @@ const Snackbar = React.createClass({
clearTimeout(this.timerAutoHideId);
clearTimeout(this.timerTransitionId);
clearTimeout(this.timerOneAtTheTimeId);
- this._unbindClickAway();
},
manuallyBindClickAway: true,
@@ -227,6 +216,8 @@ const Snackbar = React.createClass({
timerOneAtTheTimeId: undefined,
componentClickAway() {
+ if (this.timerTransitionId) return; // If transitioning, don't close snackbar
+
if (this.props.open !== null && this.props.onRequestClose) {
this.props.onRequestClose('clickaway');
} else {
@@ -234,7 +225,8 @@ const Snackbar = React.createClass({
}
},
- _setAutoHideTimer() {
+ // Timer that controls delay before snackbar auto hides
+ setAutoHideTimer() {
const autoHideDuration = this.props.autoHideDuration;
if (autoHideDuration > 0) {
@@ -249,6 +241,13 @@ const Snackbar = React.createClass({
}
},
+ // Timer that controls delay before click-away events are captured (based on when animation completes)
+ setTransitionTimer() {
+ this.timerTransitionId = setTimeout(() => {
+ this.timerTransitionId = undefined;
+ }, 400);
+ },
+
render() {
const {
onActionTouchTap,
@@ -263,6 +262,7 @@ const Snackbar = React.createClass({
muiTheme: {
prepareStyles,
},
+ open,
} = this.state;
const styles = getStyles(this.props, this.state);
@@ -276,14 +276,16 @@ const Snackbar = React.createClass({
);
return (
-
-
-
-
{message}
- {actionButton}
+
+
+
+
+ {message}
+ {actionButton}
+
-
+
);
},
diff --git a/src/table/table-body.jsx b/src/table/table-body.jsx
index 3d9e75653ed64d..f57fe21aeab5dc 100644
--- a/src/table/table-body.jsx
+++ b/src/table/table-body.jsx
@@ -1,7 +1,7 @@
import React from 'react';
import Checkbox from '../checkbox';
import TableRowColumn from './table-row-column';
-import ClickAwayable from '../mixins/click-awayable';
+import ClickAwayListener from '../ClickAwayListener';
import getMuiTheme from '../styles/getMuiTheme';
const TableBody = React.createClass({
@@ -121,10 +121,6 @@ const TableBody = React.createClass({
muiTheme: React.PropTypes.object,
},
- mixins: [
- ClickAwayable,
- ],
-
getDefaultProps() {
return {
allRowsSelected: false,
@@ -419,9 +415,11 @@ const TableBody = React.createClass({
const rows = this._createRows();
return (
-
- {rows}
-
+
+
+ {rows}
+
+
);
},