import React, { FC, ReactNode, useEffect, useLayoutEffect, useRef, useState } from "react"; import { AnimatePresence, motion } from "framer-motion"; import { Drawer } from "vaul"; import { useDashboardDimensions, useViewPortDimensionsStyle } from "./tree/hooks"; import { PopupWindowHeader } from "./PopupWindowHeader"; import { BottomButtons, ButtonMeta } from "./BottomButtons"; import { PopupWindowContent, PopupWindowContentProps } from "./PopupWindowContent"; import { usePreviousDistinct } from "react-use"; import { useAtomValue, useSetAtom } from "jotai"; import { ActivePopupWindowIdsAtom } from "./tree/atoms"; import {Color, ColorWithCustomTint} from "./tree/Color"; export type NavigationDirection = "forwards" | "backwards"; export interface ScreenProps { id: number; title: string | (() => ReactNode); icon?: () => ReactNode; description?: string | ReactNode | (() => ReactNode); content: PopupWindowContentProps['content']; contentWidth?: () => number; showBottomNavigation?: boolean | (() => boolean); //When going back to THIS screen primaryBottom?: () => ButtonMeta; secondaryBottom?: () => ButtonMeta | undefined; onBackToThisScreen?: () => void; backTo?: () => null | { id: number, label?: string, }, canNavigateToThisScreen?: (direction: NavigationDirection) => boolean | (() => any), disableScreenTransition?: boolean, } export type ScreenNavigation = { screenId: number, setCurrentScreenId: (id: number) => void, } export type PopupWindowProps = ScreenNavigation & { id: string, isOpen: boolean, onClose: (context: 'success' | 'fail') => void, defaultScreenId: number, screens: ScreenProps[]/* | (([currentScreenId, setCurrentScreenId]: [number, (id: number) => void]) => ScreenProps[]),*/ canGoBack?: (to: number) => boolean, showBackNavigation?: boolean, zIndex?: number, customTop?: (existingTop: number) => number, customWidth?: (existingWidth: number) => number, headerCenteredContent?: ReactNode, color?: Color | ColorWithCustomTint, } type PopupCloseContext = 'success' | 'fail' type ScreenTransitionDirection = -1 | 0 | 1 const DRAWER_OPEN_ANIMATION_MS = 480 export const POPUP_CLOSE_ANIMATION_MS = 520 const DRAWER_CLOSE_ANIMATION_MS = POPUP_CLOSE_ANIMATION_MS const DRAWER_OPEN_EASING = 'cubic-bezier(0.22, 1, 0.36, 1)' const DRAWER_CLOSE_EASING = 'cubic-bezier(0.22, 1, 0.36, 1)' const STACKED_POPUP_DROP_PX = 80 const STACKED_POPUP_SHIFT_ANIMATION_MS = 590 const STACKED_POPUP_SHIFT_EASING = 'cubic-bezier(0.16, 1, 0.3, 1)' const SCREEN_TRANSITION_PUSH_OFFSET = '120%' const SCREEN_TRANSITION_PUSH_OFFSET_NEGATIVE = '-120%' const SCREEN_TRANSITION_EASING = [0.32, 0.72, 0, 1] const SCREEN_TRANSITION_DURATION = .3 const SCREEN_TRANSITION_ENTER_OPACITY = .8 const SCREEN_TRANSITION_EXIT_OPACITY = .3 const SCREEN_TRANSITION_VARIANTS = { initial: (direction: ScreenTransitionDirection) => ({ zIndex: direction > 0 ? 2 : 1, opacity: direction === 0 ? 1 : SCREEN_TRANSITION_ENTER_OPACITY, x: direction === 0 ? 0 : direction > 0 ? SCREEN_TRANSITION_PUSH_OFFSET : SCREEN_TRANSITION_PUSH_OFFSET_NEGATIVE, }), animate: (direction: ScreenTransitionDirection) => ({ zIndex: direction > 0 ? 2 : 1, opacity: 1, x: 0, transition: { duration: SCREEN_TRANSITION_DURATION, ease: SCREEN_TRANSITION_EASING, }, }), exit: (direction: ScreenTransitionDirection) => ({ zIndex: direction > 0 ? 1 : 2, opacity: direction === 0 ? 1 : SCREEN_TRANSITION_EXIT_OPACITY, x: direction === 0 ? 0 : direction > 0 ? SCREEN_TRANSITION_PUSH_OFFSET_NEGATIVE : SCREEN_TRANSITION_PUSH_OFFSET, transition: { duration: SCREEN_TRANSITION_DURATION, ease: SCREEN_TRANSITION_EASING, }, }), } export const PopupWindow: FC = ({ id, defaultScreenId, screens, isOpen, onClose, canGoBack, screenId: currentScreenId, setCurrentScreenId, showBackNavigation, zIndex = 1000000, customTop, customWidth, headerCenteredContent, color, }) => { const { width, height, y, x } = useDashboardDimensions() const activePopupWindowIds = useAtomValue(ActivePopupWindowIdsAtom) const setActivePopupWindowIds = useSetAtom(ActivePopupWindowIdsAtom) const activePopupsCount = activePopupWindowIds.length const { left: wpAdminLeftOffset } = useViewPortDimensionsStyle() const [headerHeight, setHeaderHeight] = useState(0) const [footerHeight, setFooterHeight] = useState(0) const [isDrawerOpen, setIsDrawerOpen] = useState(isOpen) const [isClosing, setIsClosing] = useState(false) const [areScreenTransitionsEnabled, setAreScreenTransitionsEnabled] = useState(false) const [renderedScreenId, setRenderedScreenId] = useState(currentScreenId) const [isScreenTransitioning, setIsScreenTransitioning] = useState(false) const canAnimateScreenTransitions = areScreenTransitionsEnabled && isDrawerOpen && isOpen && !isClosing const closeTimeoutRef = useRef(null) const enableScreenTransitionsTimeoutRef = useRef(null) const openedAtPopupCountRef = useRef(null) //const [currentScreenId, setCurrentScreenId] = useState(defaultScreenId || 1) /*const currentScreenId = UseCurrentScreenId(id, defaultScreenId) const setCurrentScreenId = useSetCurrentScreenId(id)*/ const screensArray = (screens).filter(Boolean) const previousScreenId = usePreviousDistinct(currentScreenId) ?? currentScreenId const shouldAnimateBetweenScreens = (fromScreenId: number, toScreenId: number) => { if (!canAnimateScreenTransitions || fromScreenId === toScreenId) { return false } const fromScreen = screensArray.find(screen => screen.id === fromScreenId) const toScreen = screensArray.find(screen => screen.id === toScreenId) return !fromScreen?.disableScreenTransition && !toScreen?.disableScreenTransition } const resolveNavigableScreenId = (targetID: number, fromScreenId?: number) => { let idToNavigateTo: number = targetID let checkedScreens = 0 while (checkedScreens < screensArray.length) { const direction: NavigationDirection = idToNavigateTo < (fromScreenId ?? currentScreenId) ? "backwards" : "forwards" const targetScreen = screensArray.find(screen => screen.id === idToNavigateTo) if (!targetScreen?.canNavigateToThisScreen) { break } const canNavigateToThisScreenResult = targetScreen.canNavigateToThisScreen(direction) if (canNavigateToThisScreenResult === false) { idToNavigateTo = direction === "forwards" ? idToNavigateTo + 1 : idToNavigateTo - 1 checkedScreens += 1 continue } if (typeof canNavigateToThisScreenResult === 'function') { return fromScreenId ?? targetID } break } return idToNavigateTo } const resolvedCurrentScreenId = resolveNavigableScreenId(currentScreenId, previousScreenId) useLayoutEffect(() => { navigateToScreen(currentScreenId, previousScreenId) }, [currentScreenId, previousScreenId]) const shouldBeActiveInStack = isOpen && !isClosing useEffect(() => { setActivePopupWindowIds((previousIds) => { const hasCurrentPopup = previousIds.includes(id) if (shouldBeActiveInStack && !hasCurrentPopup) { return [...previousIds, id] } if (!shouldBeActiveInStack && hasCurrentPopup) { return previousIds.filter((popupId) => popupId !== id) } return previousIds }) return () => { setActivePopupWindowIds((previousIds) => { if (!previousIds.includes(id)) { return previousIds } return previousIds.filter((popupId) => popupId !== id) }) } }, [id, setActivePopupWindowIds, shouldBeActiveInStack]) useEffect(() => { if (isOpen) { if (closeTimeoutRef.current !== null) { window.clearTimeout(closeTimeoutRef.current) closeTimeoutRef.current = null } if (openedAtPopupCountRef.current === null) { openedAtPopupCountRef.current = activePopupsCount + 1 } setIsClosing(false) setIsDrawerOpen(true) return } setIsClosing(false) openedAtPopupCountRef.current = null setIsDrawerOpen(false) }, [isOpen]) useEffect(() => { if (enableScreenTransitionsTimeoutRef.current !== null) { window.clearTimeout(enableScreenTransitionsTimeoutRef.current) enableScreenTransitionsTimeoutRef.current = null } if (!isDrawerOpen) { setAreScreenTransitionsEnabled(false) return } enableScreenTransitionsTimeoutRef.current = window.setTimeout(() => { setAreScreenTransitionsEnabled(true) enableScreenTransitionsTimeoutRef.current = null }, DRAWER_OPEN_ANIMATION_MS) return () => { if (enableScreenTransitionsTimeoutRef.current !== null) { window.clearTimeout(enableScreenTransitionsTimeoutRef.current) enableScreenTransitionsTimeoutRef.current = null } } }, [isDrawerOpen]) const closeWithAnimation = (context: PopupCloseContext) => { if (!isDrawerOpen || closeTimeoutRef.current !== null) return setIsClosing(true) setIsDrawerOpen(false) closeTimeoutRef.current = window.setTimeout(() => { closeTimeoutRef.current = null onClose(context) }, DRAWER_CLOSE_ANIMATION_MS) } useEffect(() => { return () => { if (closeTimeoutRef.current !== null) { window.clearTimeout(closeTimeoutRef.current) } if (enableScreenTransitionsTimeoutRef.current !== null) { window.clearTimeout(enableScreenTransitionsTimeoutRef.current) } } }, []) const navigateToScreen = (targetID: number, fromScreenId?: number) => { let idToNavigateTo: number = targetID let checkedScreens = 0 while (checkedScreens < screensArray.length) { const direction: NavigationDirection = idToNavigateTo < (fromScreenId ?? currentScreenId) ? "backwards" : "forwards" const targetScreen = screensArray.find(screen => screen.id === idToNavigateTo) if (!targetScreen?.canNavigateToThisScreen) { break } const canNavigateToThisScreenResult = targetScreen.canNavigateToThisScreen(direction) if (canNavigateToThisScreenResult === false) { idToNavigateTo = direction === "forwards" ? idToNavigateTo + 1 : idToNavigateTo - 1 checkedScreens += 1 continue } if (typeof canNavigateToThisScreenResult === 'function') { canNavigateToThisScreenResult() return } break } if (currentScreenId !== idToNavigateTo) { setIsScreenTransitioning(shouldAnimateBetweenScreens(currentScreenId, idToNavigateTo)) setCurrentScreenId(idToNavigateTo) } } useLayoutEffect(() => { // here we need to call the onbacktothissrceen if it exists on the screen let wereBack = resolvedCurrentScreenId <= previousScreenId; if (wereBack) { const targetScreen = screensArray.find(screen => screen.id === resolvedCurrentScreenId) || {} const { onBackToThisScreen } = targetScreen as ScreenProps //const {onBackToThisScreen} = screens[toScreenId - 2] || {} if (onBackToThisScreen) { onBackToThisScreen() } } }, [previousScreenId, resolvedCurrentScreenId]); const currentScreen = screensArray.find(screen => screen.id === resolvedCurrentScreenId) useEffect(() => { if (currentScreen && renderedScreenId !== resolvedCurrentScreenId) { setRenderedScreenId(resolvedCurrentScreenId) } }, [currentScreen, resolvedCurrentScreenId, renderedScreenId]) useEffect(() => { if (!canAnimateScreenTransitions) { setIsScreenTransitioning(false) } }, [canAnimateScreenTransitions]) useLayoutEffect(() => { if (!shouldAnimateBetweenScreens(previousScreenId, currentScreenId)) { setIsScreenTransitioning(false) return } setIsScreenTransitioning(true) }, [currentScreenId, previousScreenId, shouldAnimateBetweenScreens]) const renderedScreen = currentScreen || screensArray.find(screen => screen.id === renderedScreenId) if (!renderedScreen) { return null; } const { title, description, icon, content, showBottomNavigation = true, contentWidth, backTo, primaryBottom, secondaryBottom, } = renderedScreen const { label: customBackLabel, id: idToBackTo } = backTo?.() || {} const canShowBottomNavigation = !!(typeof showBottomNavigation === 'function' ? showBottomNavigation() : showBottomNavigation); const reservedFooterHeight = canShowBottomNavigation ? footerHeight : 0 useEffect(() => { if (canShowBottomNavigation || footerHeight === 0) { return } setFooterHeight(0) }, [canShowBottomNavigation, footerHeight]) let top = 16 * 3 let defaultWidth = 132 * 4 const stackedDrop = openedAtPopupCountRef.current === null ? 0 : Math.max(0, activePopupsCount - openedAtPopupCountRef.current) * STACKED_POPUP_DROP_PX const renderedTop = (customTop?.(top) ?? top) + stackedDrop const renderedWidth = customWidth?.(defaultWidth) ?? defaultWidth; const renderedLeft = wpAdminLeftOffset + ((window.innerWidth - wpAdminLeftOffset - renderedWidth) / 2) const drawerAnimationMs = isDrawerOpen ? DRAWER_OPEN_ANIMATION_MS : DRAWER_CLOSE_ANIMATION_MS const drawerAnimationEasing = isDrawerOpen ? DRAWER_OPEN_EASING : DRAWER_CLOSE_EASING const drawerBackdropFilter = isDrawerOpen ? 'blur(16px)' : 'blur(12px)' const drawerBoxShadow = isDrawerOpen ? '0 14px 220px rgba(0, 0, 0, 0.08)' : '0 10px 140px rgba(0, 0, 0, 0.07)' const animationStyle = { animationDuration: `${drawerAnimationMs}ms`, transitionDuration: `${drawerAnimationMs}ms, ${drawerAnimationMs}ms, ${STACKED_POPUP_SHIFT_ANIMATION_MS}ms`, animationTimingFunction: drawerAnimationEasing, transitionTimingFunction: `${drawerAnimationEasing}, ${drawerAnimationEasing}, ${STACKED_POPUP_SHIFT_EASING}`, transitionProperty: 'transform, opacity, top', willChange: 'transform, opacity', } const screenTransitionDirection: ScreenTransitionDirection = renderedScreen.id === previousScreenId ? 0 : renderedScreen.id > previousScreenId ? 1 : -1 const shouldAnimateRenderedScreenTransition = shouldAnimateBetweenScreens(previousScreenId, renderedScreen.id) return ( closeWithAnimation('fail')} />
setIsScreenTransitioning(false)}> closeWithAnimation('fail')} canGoBack={canGoBack} backLabel={customBackLabel} onBack={(fromScreenId, toScreenId) => { /* const targetScreen = screensArray.find(screen => screen.id === toScreenId) || {} const {onBackToThisScreen} = targetScreen as ScreenProps //const {onBackToThisScreen} = screens[toScreenId - 2] || {} if (onBackToThisScreen) { onBackToThisScreen() } */ }} backId={idToBackTo} onHeight={(height) => { if (headerHeight === height) return setHeaderHeight(height) }} showNavigationButtons={showBackNavigation} centeredContent={headerCenteredContent} /> {canShowBottomNavigation && { if (footerHeight === height) return setFooterHeight(height) }} contentWidth={contentWidth && contentWidth()} />}
) }