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, 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 = ({ width, top, left, onClose, children, invert, invertY, targetPadding, }) => { const $containerRef = useRef(null); const [currentTop, setCurrentTop] = useState(top); useOnOutsideClick($containerRef, true, onClose, null); return ( {children} ); }; PopupContainer.defaultProps = { width: 316, }; const PopupContext = createContext({ show: NOOP, setTab: NOOP, getCurrentTab: () => 0, hide: NOOP, }); export const usePopup = () => { const ctx = useContext(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(defaultState); const show = (target: RefObject, 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 ( {portalTarget && currentState.isOpen && currentState.options && createPortal( { if (currentState.options && currentState.options.onClose) { currentState.options.onClose(); } setState(defaultState); }} width={currentState.options.width} > {currentState.content} {currentState.options.showDiamond && ( )} , portalTarget, )} {children} ); }; type Props = { title: string | null; top: number; left: number; onClose: () => void; onPrevious?: () => void | null; noHeader?: boolean | null; width?: string | number; }; const PopupMenu: React.FC = ({ width, title, top, left, onClose, noHeader, children, onPrevious }) => { const $containerRef = useRef(null); useOnOutsideClick($containerRef, true, onClose, null); return ( {onPrevious && ( )} {noHeader ? ( onClose()}> ) : (
{title} onClose()}>
)} {children}
); }; export const Popup: React.FC = ({ borders = true, padding = true, title, onClose, tab, children }) => { const { getCurrentTab, setTab } = usePopup(); if (getCurrentTab() !== tab) { return null; } return ( <> {tab > 0 && ( { setTab(0); }} > )} {title && (
{title}
)} {onClose && ( onClose()}> )} {children}
); }; export default PopupMenu;