soiz1's picture
Upload 2891 files
6bcb42f verified
import PropTypes from 'prop-types';
import React from 'react';
import classNames from 'classnames';
import bindAll from 'lodash.bindall';
import ReactTooltip from 'react-tooltip';
import styles from './action-menu.css';
const CLOSE_DELAY = 300; // ms
class ActionMenu extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'clickDelayer',
'handleClosePopover',
'handleToggleOpenState',
'handleTouchStart',
'handleTouchOutside',
'setButtonRef',
'setContainerRef'
]);
this.state = {
isOpen: false,
forceHide: false
};
this.mainTooltipId = `tooltip-${Math.random()}`;
}
componentDidMount () {
// Touch start on the main button is caught to trigger open and not click
this.buttonRef.addEventListener('touchstart', this.handleTouchStart);
// Touch start on document is used to trigger close if it is outside
document.addEventListener('touchstart', this.handleTouchOutside);
}
shouldComponentUpdate (newProps, newState) {
// This check prevents re-rendering while the project is updating.
// @todo check only the state and the title because it is enough to know
// if anything substantial has changed
// This is needed because of the sloppy way the props are passed as a new object,
// which should be refactored.
return newState.isOpen !== this.state.isOpen ||
newState.forceHide !== this.state.forceHide ||
newProps.title !== this.props.title;
}
componentWillUnmount () {
this.buttonRef.removeEventListener('touchstart', this.handleTouchStart);
document.removeEventListener('touchstart', this.handleTouchOutside);
}
handleClosePopover () {
this.closeTimeoutId = setTimeout(() => {
this.setState({isOpen: false});
this.closeTimeoutId = null;
}, CLOSE_DELAY);
}
handleToggleOpenState () {
// Mouse enter back in after timeout was started prevents it from closing.
if (this.closeTimeoutId) {
clearTimeout(this.closeTimeoutId);
this.closeTimeoutId = null;
} else if (!this.state.isOpen) {
this.setState({
isOpen: true,
forceHide: false
});
}
}
handleTouchOutside (e) {
if (this.state.isOpen && !this.containerRef.contains(e.target)) {
this.setState({isOpen: false});
ReactTooltip.hide();
}
}
clickDelayer (fn) {
// Return a wrapped action that manages the menu closing.
// @todo we may be able to use react-transition for this in the future
// for now all this work is to ensure the menu closes BEFORE the
// (possibly slow) action is started.
return event => {
ReactTooltip.hide();
if (fn) fn(event);
// Blur the button so it does not keep focus after being clicked
// This prevents keyboard events from triggering the button
this.buttonRef.blur();
this.setState({forceHide: true, isOpen: false}, () => {
setTimeout(() => this.setState({forceHide: false}));
});
};
}
handleTouchStart (e) {
// Prevent this touch from becoming a click if menu is closed
if (!this.state.isOpen) {
e.preventDefault();
this.handleToggleOpenState();
}
}
setButtonRef (ref) {
this.buttonRef = ref;
}
setContainerRef (ref) {
this.containerRef = ref;
}
render () {
const {
className,
img: mainImg,
title: mainTitle,
moreButtons,
tooltipPlace,
onClick
} = this.props;
return (
<div
className={classNames(styles.menuContainer, className, {
[styles.expanded]: this.state.isOpen,
[styles.forceHidden]: this.state.forceHide
})}
ref={this.setContainerRef}
onMouseEnter={this.handleToggleOpenState}
onMouseLeave={this.handleClosePopover}
>
<button
aria-label={mainTitle}
className={classNames(styles.button, styles.mainButton)}
data-for={this.mainTooltipId}
data-tip={mainTitle}
ref={this.setButtonRef}
onClick={this.clickDelayer(onClick)}
>
<img
className={styles.mainIcon}
draggable={false}
src={mainImg}
/>
</button>
<ReactTooltip
className={styles.tooltip}
effect="solid"
id={this.mainTooltipId}
place={tooltipPlace || 'left'}
/>
<div className={styles.moreButtonsOuter}>
<div className={styles.moreButtons}>
{(moreButtons || []).map(({img, title, onClick: handleClick,
fileAccept, fileChange, fileInput, fileMultiple}, keyId) => {
const isComingSoon = !handleClick;
const hasFileInput = fileInput;
const tooltipId = `${this.mainTooltipId}-${title}`;
return (
<div key={`${tooltipId}-${keyId}`}>
<button
aria-label={title}
className={classNames(styles.button, styles.moreButton, {
[styles.comingSoon]: isComingSoon
})}
data-for={tooltipId}
data-tip={title}
onClick={hasFileInput ? handleClick : this.clickDelayer(handleClick)}
>
<img
className={styles.moreIcon}
draggable={false}
src={img}
/>
{hasFileInput ? (
<input
accept={fileAccept}
className={styles.fileInput}
multiple={fileMultiple}
ref={fileInput}
type="file"
onChange={fileChange}
/>) : null}
</button>
<ReactTooltip
className={classNames(styles.tooltip, {
[styles.comingSoonTooltip]: isComingSoon
})}
effect="solid"
id={tooltipId}
place={tooltipPlace || 'left'}
/>
</div>
);
})}
</div>
</div>
</div>
);
}
}
ActionMenu.propTypes = {
className: PropTypes.string,
img: PropTypes.string,
moreButtons: PropTypes.arrayOf(PropTypes.shape({
img: PropTypes.string,
title: PropTypes.node.isRequired,
onClick: PropTypes.func, // Optional, "coming soon" if no callback provided
fileAccept: PropTypes.string, // Optional, only for file upload
fileChange: PropTypes.func, // Optional, only for file upload
fileInput: PropTypes.func, // Optional, only for file upload
fileMultiple: PropTypes.bool // Optional, only for file upload
})),
onClick: PropTypes.func.isRequired,
title: PropTypes.node.isRequired,
tooltipPlace: PropTypes.string
};
export default ActionMenu;