2021-01-01 22:20:55 -06:00

355 lines
8.7 KiB
TypeScript

import React, { useRef, createContext, RefObject, useState, useContext, useEffect } from 'react';
import { Cross, AngleLeft } from 'shared/icons';
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
import { createPortal } from 'react-dom';
import NOOP from 'shared/utils/noop';
import produce from 'immer';
import theme from 'App/ThemeStyles';
import {
Container,
ContainerDiamond,
Header,
HeaderTitle,
Content,
CloseButton,
PreviousButton,
Wrapper,
} from './Styles';
function getPopupOptions(options?: PopupOptions) {
const popupOptions: PopupOptionsInternal = {
borders: true,
diamondColor: theme.colors.bg.secondary,
targetPadding: '10px',
showDiamond: true,
width: 316,
};
if (options) {
if (options.borders) {
popupOptions.borders = options.borders;
}
if (options.width) {
popupOptions.width = options.width;
}
if (options.targetPadding) {
popupOptions.targetPadding = options.targetPadding;
}
if (typeof options.showDiamond !== 'undefined' && options.showDiamond !== null) {
popupOptions.showDiamond = options.showDiamond;
}
if (options.diamondColor) {
popupOptions.diamondColor = options.diamondColor;
}
if (options.onClose) {
popupOptions.onClose = options.onClose;
}
}
return popupOptions;
}
type PopupContextState = {
show: (target: RefObject<HTMLElement>, content: JSX.Element, options?: PopupOptions) => void;
setTab: (newTab: number, options?: PopupOptions) => void;
getCurrentTab: () => number;
hide: () => void;
};
type PopupProps = {
title: string | null;
onClose?: () => void;
tab: number;
padding?: boolean;
borders?: boolean;
diamondColor?: string;
};
type PopupContainerProps = {
top: number;
left: number;
invert: boolean;
targetPadding: string;
invertY: boolean;
onClose: () => void;
width?: string | number;
};
const PopupContainer: React.FC<PopupContainerProps> = ({
width,
top,
left,
onClose,
children,
invert,
invertY,
targetPadding,
}) => {
const $containerRef = useRef<HTMLDivElement>(null);
const [currentTop, setCurrentTop] = useState(top);
useOnOutsideClick($containerRef, true, onClose, null);
return (
<Container
targetPadding={targetPadding}
width={width ?? 316}
left={left}
top={currentTop}
ref={$containerRef}
invert={invert}
invertY={invertY}
>
{children}
</Container>
);
};
PopupContainer.defaultProps = {
width: 316,
};
const PopupContext = createContext<PopupContextState>({
show: NOOP,
setTab: NOOP,
getCurrentTab: () => 0,
hide: NOOP,
});
export const usePopup = () => {
const ctx = useContext<PopupContextState>(PopupContext);
return { showPopup: ctx.show, setTab: ctx.setTab, getCurrentTab: ctx.getCurrentTab, hidePopup: ctx.hide };
};
type PopupState = {
isOpen: boolean;
left: number;
top: number;
invertY: boolean;
invert: boolean;
currentTab: number;
previousTab: number;
content: JSX.Element | null;
options: PopupOptionsInternal | null;
};
const { Provider, Consumer } = PopupContext;
const canUseDOM = !!(typeof window !== 'undefined' && window.document && window.document.createElement);
type PopupOptionsInternal = {
width: number;
borders: boolean;
targetPadding: string;
diamondColor: string;
showDiamond: boolean;
onClose?: () => void;
};
type PopupOptions = {
targetPadding?: string | null;
showDiamond?: boolean | null;
width?: number | null;
borders?: boolean | null;
diamondColor?: string | null;
onClose?: () => void;
};
const defaultState = {
isOpen: false,
left: 0,
top: 0,
invert: false,
invertY: false,
currentTab: 0,
previousTab: 0,
content: null,
options: null,
};
export const PopupProvider: React.FC = ({ children }) => {
const [currentState, setState] = useState<PopupState>(defaultState);
const show = (target: RefObject<HTMLElement>, content: JSX.Element, options?: PopupOptions) => {
if (target && target.current) {
const bounds = target.current.getBoundingClientRect();
let top = bounds.top + bounds.height;
let invertY = false;
if (window.innerHeight / 2 < top) {
top = window.innerHeight - bounds.top;
invertY = true;
}
const popupOptions = getPopupOptions(options);
if (bounds.left + 304 + 30 > window.innerWidth) {
setState({
isOpen: true,
left: bounds.left + bounds.width,
top,
invertY,
invert: true,
currentTab: 0,
previousTab: 0,
content,
options: popupOptions,
});
} else {
setState({
isOpen: true,
left: bounds.left,
top,
invert: false,
invertY,
currentTab: 0,
previousTab: 0,
content,
options: popupOptions,
});
}
}
};
const hide = () => {
setState({
isOpen: false,
left: 0,
top: 0,
invert: true,
invertY: false,
currentTab: 0,
previousTab: 0,
content: null,
options: null,
});
};
const portalTarget = canUseDOM ? document.body : null; // appease flow
const setTab = (newTab: number, options?: PopupOptions) => {
setState((prevState: PopupState) =>
produce(prevState, draftState => {
draftState.previousTab = currentState.currentTab;
draftState.currentTab = newTab;
if (options) {
draftState.options = getPopupOptions(options);
}
}),
);
};
const getCurrentTab = () => {
return currentState.currentTab;
};
return (
<Provider value={{ hide, show, setTab, getCurrentTab }}>
{portalTarget &&
currentState.isOpen &&
currentState.options &&
createPortal(
<PopupContainer
invertY={currentState.invertY}
invert={currentState.invert}
top={currentState.top}
targetPadding={currentState.options.targetPadding}
left={currentState.left}
onClose={() => {
if (currentState.options && currentState.options.onClose) {
currentState.options.onClose();
}
setState(defaultState);
}}
width={currentState.options.width}
>
{currentState.content}
{currentState.options.showDiamond && (
<ContainerDiamond
color={currentState.options.diamondColor}
borders={currentState.options.borders}
invertY={currentState.invertY}
invert={currentState.invert}
/>
)}
</PopupContainer>,
portalTarget,
)}
{children}
</Provider>
);
};
type Props = {
title: string | null;
top: number;
left: number;
onClose: () => void;
onPrevious?: () => void | null;
noHeader?: boolean | null;
width?: string | number;
};
const PopupMenu: React.FC<Props> = ({ width, title, top, left, onClose, noHeader, children, onPrevious }) => {
const $containerRef = useRef<HTMLDivElement>(null);
useOnOutsideClick($containerRef, true, onClose, null);
return (
<Container
targetPadding="10px"
invertY={false}
width={width ?? 316}
invert={false}
left={left}
top={top}
ref={$containerRef}
>
<Wrapper padding borders>
{onPrevious && (
<PreviousButton onClick={onPrevious}>
<AngleLeft color="#c2c6dc" />
</PreviousButton>
)}
{noHeader ? (
<CloseButton onClick={() => onClose()}>
<Cross width={16} height={16} />
</CloseButton>
) : (
<Header>
<HeaderTitle>{title}</HeaderTitle>
<CloseButton onClick={() => onClose()}>
<Cross width={16} height={16} />
</CloseButton>
</Header>
)}
<Content>{children}</Content>
</Wrapper>
</Container>
);
};
export const Popup: React.FC<PopupProps> = ({ borders = true, padding = true, title, onClose, tab, children }) => {
const { getCurrentTab, setTab } = usePopup();
if (getCurrentTab() !== tab) {
return null;
}
return (
<>
<Wrapper borders={borders} padding={padding}>
{tab > 0 && (
<PreviousButton
onClick={() => {
setTab(0);
}}
>
<AngleLeft color="#c2c6dc" />
</PreviousButton>
)}
{title && (
<Header>
<HeaderTitle>{title}</HeaderTitle>
</Header>
)}
{onClose && (
<CloseButton onClick={() => onClose()}>
<Cross width={16} height={16} />
</CloseButton>
)}
<Content>{children}</Content>
</Wrapper>
</>
);
};
export default PopupMenu;