taskcafe/web/src/shared/components/PopupMenu/index.tsx

243 lines
5.9 KiB
TypeScript
Raw Normal View History

2020-05-27 02:53:31 +02:00
import React, { useRef, createContext, RefObject, useState, useContext } from 'react';
import { Cross, AngleLeft } from 'shared/icons';
2020-04-10 04:40:22 +02:00
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
2020-05-27 02:53:31 +02:00
import { createPortal } from 'react-dom';
import produce from 'immer';
import {
Container,
ContainerDiamond,
Header,
HeaderTitle,
Content,
CloseButton,
PreviousButton,
Wrapper,
} from './Styles';
type PopupContextState = {
show: (target: RefObject<HTMLElement>, content: JSX.Element, width?: string | number) => void;
2020-05-27 02:53:31 +02:00
setTab: (newTab: number) => void;
getCurrentTab: () => number;
hide: () => void;
2020-05-27 02:53:31 +02:00
};
type PopupProps = {
title: string | null;
onClose?: () => void;
2020-05-27 02:53:31 +02:00
tab: number;
};
type PopupContainerProps = {
top: number;
left: number;
invert: boolean;
onClose: () => void;
width?: string | number;
2020-05-27 02:53:31 +02:00
};
const PopupContainer: React.FC<PopupContainerProps> = ({ width, top, left, onClose, children, invert }) => {
2020-05-27 02:53:31 +02:00
const $containerRef = useRef();
useOnOutsideClick($containerRef, true, onClose, null);
return (
<Container width={width ?? 316} left={left} top={top} ref={$containerRef} invert={invert}>
2020-05-27 02:53:31 +02:00
{children}
</Container>
);
};
PopupContainer.defaultProps = {
width: 316,
};
2020-05-27 02:53:31 +02:00
const PopupContext = createContext<PopupContextState>({
show: () => {},
setTab: () => {},
getCurrentTab: () => 0,
hide: () => {},
2020-05-27 02:53:31 +02:00
});
export const usePopup = () => {
const ctx = useContext<PopupContextState>(PopupContext);
return { showPopup: ctx.show, setTab: ctx.setTab, getCurrentTab: ctx.getCurrentTab, hidePopup: ctx.hide };
2020-05-27 02:53:31 +02:00
};
type PopupState = {
isOpen: boolean;
left: number;
top: number;
invert: boolean;
currentTab: number;
previousTab: number;
content: JSX.Element | null;
width?: string | number;
2020-05-27 02:53:31 +02:00
};
const { Provider, Consumer } = PopupContext;
const canUseDOM = !!(typeof window !== 'undefined' && window.document && window.document.createElement);
const defaultState = {
isOpen: false,
left: 0,
top: 0,
invert: false,
currentTab: 0,
previousTab: 0,
content: null,
};
export const PopupProvider: React.FC = ({ children }) => {
const [currentState, setState] = useState<PopupState>(defaultState);
const show = (target: RefObject<HTMLElement>, content: JSX.Element, width?: number | string) => {
2020-05-27 02:53:31 +02:00
if (target && target.current) {
const bounds = target.current.getBoundingClientRect();
if (bounds.left + 304 + 30 > window.innerWidth) {
setState({
isOpen: true,
left: bounds.left + bounds.width,
top: bounds.top + bounds.height,
invert: true,
currentTab: 0,
previousTab: 0,
content,
width: width ?? 316,
2020-05-27 02:53:31 +02:00
});
} else {
setState({
isOpen: true,
left: bounds.left,
top: bounds.top + bounds.height,
invert: false,
currentTab: 0,
previousTab: 0,
content,
width: width ?? 316,
2020-05-27 02:53:31 +02:00
});
}
}
};
const hide = () => {
setState({
isOpen: false,
left: 0,
top: 0,
invert: true,
currentTab: 0,
previousTab: 0,
content: null,
});
};
2020-05-27 02:53:31 +02:00
const portalTarget = canUseDOM ? document.body : null; // appease flow
const setTab = (newTab: number) => {
setState((prevState: PopupState) => {
return {
...prevState,
previousTab: currentState.currentTab,
currentTab: newTab,
};
});
};
const getCurrentTab = () => {
return currentState.currentTab;
};
return (
<Provider value={{ hide, show, setTab, getCurrentTab }}>
2020-05-27 02:53:31 +02:00
{portalTarget &&
currentState.isOpen &&
createPortal(
<PopupContainer
invert={currentState.invert}
top={currentState.top}
left={currentState.left}
onClose={() => setState(defaultState)}
width={currentState.width ?? 316}
2020-05-27 02:53:31 +02:00
>
{currentState.content}
<ContainerDiamond invert={currentState.invert} />
</PopupContainer>,
portalTarget,
)}
{children}
</Provider>
);
};
2020-04-10 04:40:22 +02:00
type Props = {
2020-05-27 02:53:31 +02:00
title: string | null;
2020-04-10 04:40:22 +02:00
top: number;
left: number;
onClose: () => void;
2020-05-27 02:53:31 +02:00
onPrevious?: () => void | null;
noHeader?: boolean | null;
width?: string | number;
2020-04-10 04:40:22 +02:00
};
const PopupMenu: React.FC<Props> = ({ width, title, top, left, onClose, noHeader, children, onPrevious }) => {
2020-04-10 04:40:22 +02:00
const $containerRef = useRef();
useOnOutsideClick($containerRef, true, onClose, null);
return (
<Container width={width ?? 316} invert={false} left={left} top={top} ref={$containerRef}>
2020-05-27 02:53:31 +02:00
<Wrapper>
{onPrevious && (
<PreviousButton onClick={onPrevious}>
<AngleLeft color="#c2c6dc" />
</PreviousButton>
)}
{noHeader ? (
<CloseButton onClick={() => onClose()}>
<Cross color="#c2c6dc" />
</CloseButton>
) : (
<Header>
<HeaderTitle>{title}</HeaderTitle>
<CloseButton onClick={() => onClose()}>
<Cross color="#c2c6dc" />
</CloseButton>
</Header>
)}
<Content>{children}</Content>
</Wrapper>
</Container>
);
};
export const Popup: React.FC<PopupProps> = ({ title, onClose, tab, children }) => {
const { getCurrentTab, setTab } = usePopup();
if (getCurrentTab() !== tab) {
return null;
}
return (
<>
<Wrapper>
{tab > 0 && (
<PreviousButton
onClick={() => {
setTab(0);
}}
>
<AngleLeft color="#c2c6dc" />
</PreviousButton>
)}
{title && (
<Header>
<HeaderTitle>{title}</HeaderTitle>
</Header>
)}
{onClose && (
<CloseButton onClick={() => onClose()}>
<Cross color="#c2c6dc" />
</CloseButton>
)}
2020-05-27 02:53:31 +02:00
<Content>{children}</Content>
</Wrapper>
</>
2020-04-10 04:40:22 +02:00
);
};
export default PopupMenu;