Reputation: 3675
I have build a component using material ui default components. It is a menu. I want to close this menu once the user clicks anywhere outside the menu.
How can I do it? Icon Menu does that. When you click anywhere else on body the menu closes but the Menu component does not do so. After looking into Icon Menu code i have noticed how it renders Menu Component with some changes but could not locate the function which triggers close on click outside body.
Below is the code for Icon Menus
const React = require('react');
const ReactDOM = require('react-dom');
const ReactTransitionGroup = require('react-addons-transition-group');
const ClickAwayable = require('../mixins/click-awayable');
const StylePropable = require('../mixins/style-propable');
const Events = require('../utils/events');
const PropTypes = require('../utils/prop-types');
const Menu = require('../menus/menu');
const DefaultRawTheme = require('../styles/raw-themes/light-raw-theme');
const ThemeManager = require('../styles/theme-manager');
const IconMenu = React.createClass({
mixins: [StylePropable, ClickAwayable],
contextTypes: {
muiTheme: React.PropTypes.object,
},
propTypes: {
closeOnItemTouchTap: React.PropTypes.bool,
iconButtonElement: React.PropTypes.element.isRequired,
iconStyle: React.PropTypes.object,
openDirection: PropTypes.corners,
onItemTouchTap: React.PropTypes.func,
onKeyboardFocus: React.PropTypes.func,
onMouseDown: React.PropTypes.func,
onMouseLeave: React.PropTypes.func,
onMouseEnter: React.PropTypes.func,
onMouseUp: React.PropTypes.func,
onTouchTap: React.PropTypes.func,
menuStyle: React.PropTypes.object,
style: React.PropTypes.object,
touchTapCloseDelay: React.PropTypes.number,
},
getDefaultProps() {
return {
closeOnItemTouchTap: true,
openDirection: 'bottom-left',
onItemTouchTap: () => {},
onKeyboardFocus: () => {},
onMouseDown: () => {},
onMouseLeave: () => {},
onMouseEnter: () => {},
onMouseUp: () => {},
onTouchTap: () => {},
touchTapCloseDelay: 200,
};
},
//for passing default theme context to children
childContextTypes: {
muiTheme: React.PropTypes.object,
},
getChildContext () {
return {
muiTheme: this.state.muiTheme,
};
},
getInitialState () {
return {
muiTheme: this.context.muiTheme ? this.context.muiTheme : ThemeManager.getMuiTheme(DefaultRawTheme),
iconButtonRef: this.props.iconButtonElement.props.ref || 'iconButton',
menuInitiallyKeyboardFocused: false,
open: false,
};
},
//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});
},
componentWillUnmount() {
if (this._timeout) clearTimeout(this._timeout);
},
componentClickAway() {
this.close();
},
render() {
let {
className,
closeOnItemTouchTap,
iconButtonElement,
iconStyle,
openDirection,
onItemTouchTap,
onKeyboardFocus,
onMouseDown,
onMouseLeave,
onMouseEnter,
onMouseUp,
onTouchTap,
menuStyle,
style,
...other,
} = this.props;
let open = this.state.open;
let openDown = openDirection.split('-')[0] === 'bottom';
let openLeft = openDirection.split('-')[1] === 'left';
let styles = {
root: {
display: 'inline-block',
position: 'relative',
},
menu: {
top: openDown ? 12 : null,
bottom: !openDown ? 12 : null,
left: !openLeft ? 12 : null,
right: openLeft ? 12 : null,
},
};
let mergedRootStyles = this.prepareStyles(styles.root, style);
let mergedMenuStyles = this.mergeStyles(styles.menu, menuStyle);
let iconButton = React.cloneElement(iconButtonElement, {
onKeyboardFocus: this.props.onKeyboardFocus,
iconStyle: this.mergeStyles(iconStyle, iconButtonElement.props.iconStyle),
onTouchTap: (e) => {
this.open(Events.isKeyboard(e));
if (iconButtonElement.props.onTouchTap) iconButtonElement.props.onTouchTap(e);
},
ref: this.state.iconButtonRef,
});
let menu = open ? (
<Menu
{...other}
animated={true}
initiallyKeyboardFocused={this.state.menuInitiallyKeyboardFocused}
onEscKeyDown={this._handleMenuEscKeyDown}
onItemTouchTap={this._handleItemTouchTap}
openDirection={openDirection}
style={mergedMenuStyles}>
{this.props.children}
</Menu>
) : null;
return (
<div
className={className}
onMouseDown={onMouseDown}
onMouseLeave={onMouseLeave}
onMouseEnter={onMouseEnter}
onMouseUp={onMouseUp}
onTouchTap={onTouchTap}
style={mergedRootStyles}>
{iconButton}
<ReactTransitionGroup>{menu}</ReactTransitionGroup>
</div>
);
},
isOpen() {
return this.state.open;
},
close(isKeyboard) {
if (this.state.open) {
this.setState({open: false}, () => {
//Set focus on the icon button when the menu close
if (isKeyboard) {
let iconButton = this.refs[this.state.iconButtonRef];
ReactDOM.findDOMNode(iconButton).focus();
iconButton.setKeyboardFocus();
}
});
}
},
open(menuInitiallyKeyboardFocused) {
if (!this.state.open) {
this.setState({
open: true,
menuInitiallyKeyboardFocused: menuInitiallyKeyboardFocused,
});
}
},
_handleItemTouchTap(e, child) {
if (this.props.closeOnItemTouchTap) {
let isKeyboard = Events.isKeyboard(e);
this._timeout = setTimeout(() => {
this.close(isKeyboard);
}, this.props.touchTapCloseDelay);
}
this.props.onItemTouchTap(e, child);
},
_handleMenuEscKeyDown() {
this.close(true);
},
});
module.exports = IconMenu;
Upvotes: 3
Views: 2964
Reputation: 35846
This behavior is given by the ClickAwayable
mixin you can find the source here
Simply import (or require) it in your custom component and add a componentClickAway
method that will trigger the close of your menu.
const ClickAwayable = require('material-ui/lib/mixins/click-awayable')
const CustomMenu = React.createClass({
mixins: [ClickAwayable],
componentClickAway () {
// close your menu
}
})
Upvotes: 2