1266c66f79
DropdownMenu has ariaLabel property, but it is actually applied to title property of IconButton. Keep it consistent.
212 lines
5.3 KiB
JavaScript
212 lines
5.3 KiB
JavaScript
import React from 'react';
|
|
import PropTypes from 'prop-types';
|
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
import IconButton from './icon_button';
|
|
import Overlay from 'react-overlays/lib/Overlay';
|
|
import Motion from '../features/ui/util/optional_motion';
|
|
import spring from 'react-motion/lib/spring';
|
|
import detectPassiveEvents from 'detect-passive-events';
|
|
|
|
const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false;
|
|
|
|
class DropdownMenu extends React.PureComponent {
|
|
|
|
static contextTypes = {
|
|
router: PropTypes.object,
|
|
};
|
|
|
|
static propTypes = {
|
|
items: PropTypes.array.isRequired,
|
|
onClose: PropTypes.func.isRequired,
|
|
style: PropTypes.object,
|
|
placement: PropTypes.string,
|
|
arrowOffsetLeft: PropTypes.string,
|
|
arrowOffsetTop: PropTypes.string,
|
|
};
|
|
|
|
static defaultProps = {
|
|
style: {},
|
|
placement: 'bottom',
|
|
};
|
|
|
|
handleDocumentClick = e => {
|
|
if (this.node && !this.node.contains(e.target)) {
|
|
this.props.onClose();
|
|
}
|
|
}
|
|
|
|
componentDidMount () {
|
|
document.addEventListener('click', this.handleDocumentClick, false);
|
|
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
|
}
|
|
|
|
componentWillUnmount () {
|
|
document.removeEventListener('click', this.handleDocumentClick, false);
|
|
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
|
}
|
|
|
|
setRef = c => {
|
|
this.node = c;
|
|
}
|
|
|
|
handleClick = e => {
|
|
const i = Number(e.currentTarget.getAttribute('data-index'));
|
|
const { action, to } = this.props.items[i];
|
|
|
|
this.props.onClose();
|
|
|
|
if (typeof action === 'function') {
|
|
e.preventDefault();
|
|
action();
|
|
} else if (to) {
|
|
e.preventDefault();
|
|
this.context.router.history.push(to);
|
|
}
|
|
}
|
|
|
|
renderItem (option, i) {
|
|
if (option === null) {
|
|
return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
|
|
}
|
|
|
|
const { text, href = '#' } = option;
|
|
|
|
return (
|
|
<li className='dropdown-menu__item' key={`${text}-${i}`}>
|
|
<a href={href} target='_blank' rel='noopener' role='button' tabIndex='0' autoFocus={i === 0} onClick={this.handleClick} data-index={i}>
|
|
{text}
|
|
</a>
|
|
</li>
|
|
);
|
|
}
|
|
|
|
render () {
|
|
const { items, style, placement, arrowOffsetLeft, arrowOffsetTop } = this.props;
|
|
|
|
return (
|
|
<Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}>
|
|
{({ opacity, scaleX, scaleY }) => (
|
|
<div className='dropdown-menu' style={{ ...style, opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }} ref={this.setRef}>
|
|
<div className={`dropdown-menu__arrow ${placement}`} style={{ left: arrowOffsetLeft, top: arrowOffsetTop }} />
|
|
|
|
<ul>
|
|
{items.map((option, i) => this.renderItem(option, i))}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
</Motion>
|
|
);
|
|
}
|
|
|
|
}
|
|
|
|
export default class Dropdown extends React.PureComponent {
|
|
|
|
static contextTypes = {
|
|
router: PropTypes.object,
|
|
};
|
|
|
|
static propTypes = {
|
|
icon: PropTypes.string.isRequired,
|
|
items: PropTypes.array.isRequired,
|
|
size: PropTypes.number.isRequired,
|
|
title: PropTypes.string,
|
|
disabled: PropTypes.bool,
|
|
status: ImmutablePropTypes.map,
|
|
isUserTouching: PropTypes.func,
|
|
isModalOpen: PropTypes.bool.isRequired,
|
|
onModalOpen: PropTypes.func,
|
|
onModalClose: PropTypes.func,
|
|
};
|
|
|
|
static defaultProps = {
|
|
title: 'Menu',
|
|
};
|
|
|
|
state = {
|
|
expanded: false,
|
|
};
|
|
|
|
handleClick = () => {
|
|
if (!this.state.expanded && this.props.isUserTouching() && this.props.onModalOpen) {
|
|
const { status, items } = this.props;
|
|
|
|
this.props.onModalOpen({
|
|
status,
|
|
actions: items,
|
|
onClick: this.handleItemClick,
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
this.setState({ expanded: !this.state.expanded });
|
|
}
|
|
|
|
handleClose = () => {
|
|
if (this.props.onModalClose) {
|
|
this.props.onModalClose();
|
|
}
|
|
|
|
this.setState({ expanded: false });
|
|
}
|
|
|
|
handleKeyDown = e => {
|
|
switch(e.key) {
|
|
case 'Enter':
|
|
this.handleClick();
|
|
break;
|
|
case 'Escape':
|
|
this.handleClose();
|
|
break;
|
|
}
|
|
}
|
|
|
|
handleItemClick = e => {
|
|
const i = Number(e.currentTarget.getAttribute('data-index'));
|
|
const { action, to } = this.props.items[i];
|
|
|
|
this.handleClose();
|
|
|
|
if (typeof action === 'function') {
|
|
e.preventDefault();
|
|
action();
|
|
} else if (to) {
|
|
e.preventDefault();
|
|
this.context.router.history.push(to);
|
|
}
|
|
}
|
|
|
|
setTargetRef = c => {
|
|
this.target = c;
|
|
}
|
|
|
|
findTarget = () => {
|
|
return this.target;
|
|
}
|
|
|
|
render () {
|
|
const { icon, items, size, title, disabled } = this.props;
|
|
const { expanded } = this.state;
|
|
|
|
return (
|
|
<div onKeyDown={this.handleKeyDown}>
|
|
<IconButton
|
|
icon={icon}
|
|
title={title}
|
|
active={expanded}
|
|
disabled={disabled}
|
|
size={size}
|
|
ref={this.setTargetRef}
|
|
onClick={this.handleClick}
|
|
/>
|
|
|
|
<Overlay show={expanded} placement='bottom' target={this.findTarget}>
|
|
<DropdownMenu items={items} onClose={this.handleClose} />
|
|
</Overlay>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
}
|