This repository has been archived on 2022-08-14. You can view files and clone it, but cannot push or open issues or pull requests.
outline/app/components/DropdownMenu/DropdownMenu.js

153 lines
4.2 KiB
JavaScript

// @flow
import * as React from 'react';
import invariant from 'invariant';
import { observable } from 'mobx';
import { observer } from 'mobx-react';
import { PortalWithState } from 'react-portal';
import { MoreIcon } from 'outline-icons';
import styled from 'styled-components';
import Flex from 'shared/components/Flex';
import { fadeAndScaleIn } from 'shared/styles/animations';
import NudeButton from 'components/NudeButton';
let previousClosePortal;
type Children =
| React.Node
| ((options: { closePortal: () => void }) => React.Node);
type Props = {
label?: React.Node,
onOpen?: () => void,
onClose?: () => void,
children?: Children,
className?: string,
style?: Object,
position?: 'left' | 'right' | 'center',
};
@observer
class DropdownMenu extends React.Component<Props> {
@observable top: number;
@observable right: number;
@observable left: number;
handleOpen = (
openPortal: (SyntheticEvent<>) => void,
closePortal: () => void
) => {
return (ev: SyntheticMouseEvent<HTMLElement>) => {
ev.preventDefault();
const currentTarget = ev.currentTarget;
invariant(document.body, 'why you not here');
if (currentTarget instanceof HTMLDivElement) {
const bodyRect = document.body.getBoundingClientRect();
const targetRect = currentTarget.getBoundingClientRect();
this.top = targetRect.bottom - bodyRect.top;
if (this.props.position === 'left') {
this.left = targetRect.left;
} else if (this.props.position === 'center') {
this.left = targetRect.left + targetRect.width / 2;
} else {
this.right = bodyRect.width - targetRect.left - targetRect.width;
}
// attempt to keep only one flyout menu open at once
if (previousClosePortal) {
previousClosePortal();
}
previousClosePortal = closePortal;
openPortal(ev);
}
};
};
render() {
const { className, label, position, children } = this.props;
return (
<div className={className}>
<PortalWithState
onOpen={this.props.onOpen}
onClose={this.props.onClose}
closeOnOutsideClick
closeOnEsc
>
{({ closePortal, openPortal, portal }) => (
<React.Fragment>
<Label onClick={this.handleOpen(openPortal, closePortal)}>
{label || (
<NudeButton>
<MoreIcon />
</NudeButton>
)}
</Label>
{portal(
<Position
position={position}
top={this.top}
left={this.left}
right={this.right}
>
<Menu
onClick={
typeof children === 'function'
? undefined
: ev => {
ev.stopPropagation();
closePortal();
}
}
style={this.props.style}
>
{typeof children === 'function'
? children({ closePortal })
: children}
</Menu>
</Position>
)}
</React.Fragment>
)}
</PortalWithState>
</div>
);
}
}
const Label = styled(Flex).attrs({
justify: 'center',
align: 'center',
})`
z-index: 1000;
cursor: pointer;
`;
const Position = styled.div`
position: absolute;
${({ left }) => (left !== undefined ? `left: ${left}px` : '')};
${({ right }) => (right !== undefined ? `right: ${right}px` : '')};
top: ${({ top }) => top}px;
z-index: 1000;
transform: ${props =>
props.position === 'center' ? 'translateX(-50%)' : 'initial'};
`;
const Menu = styled.div`
animation: ${fadeAndScaleIn} 200ms ease;
transform-origin: ${props => (props.left !== undefined ? '25%' : '75%')} 0;
background: ${props => props.theme.menuBackground};
border-radius: 2px;
padding: 0.5em 0;
min-width: 180px;
overflow: hidden;
box-shadow: ${props => props.theme.menuShadow};
@media print {
display: none;
}
`;
export default DropdownMenu;