import React, { forwardRef, useCallback, useEffect, useRef } from "react"; import { createPortal } from "react-dom"; import Header from "./Header/Header"; import Loading from "./Loading"; import useIphoneFocusFix from "./utils/useIphoneFocusFix"; import clsx from "clsx"; import { Theme } from "./const"; import { disableBodyScroll, enableBodyScroll } from "./utils/bodyScrollLock"; import useOnClickOutside from "./utils/useClickOutside"; export interface PropsType { className?: string; theme?: Theme; fixed?: boolean; opened: boolean; onClose?: () => void; onBack?: () => void; onScroll?: (e: any) => void; title?: string; renderTitle?: React.ReactNode; back?: boolean; // show back icon on left top close?: boolean; // show close icon on right top children: React.ReactNode; isLoading?: boolean; header?: React.ReactNode; bodyClassName?: string; bodyStyle?: React.CSSProperties; getContainer?: HTMLElement | (() => HTMLElement); } const Popup = ( { className, fixed = true, title, renderTitle, theme = Theme.LIGHT, opened, close, back, children, onClose, onBack, isLoading, header, bodyClassName, bodyStyle, onScroll, getContainer, }: PropsType, ref: React.ForwardedRef, ) => { const bodyRef = useRef(null); // prevent PullToRefresh in ios const handleTouchMove = useCallback((e: TouchEvent) => { const target = e.target as HTMLElement; const isModalOverlay = target.id === "tomo-modal"; const isScrollableArea = target.closest(".uikit-popup-enable-scroll"); const isNonScrollableArea = target.closest(".uikit-popup-prevent-scroll"); // Prevent default scroll behavior if: // 1. Target is modal overlay // 2. Target is not in a scrollable area // 3. Target is in an explicitly non-scrollable area const shouldPreventScroll = isModalOverlay || !isScrollableArea || isNonScrollableArea; if (shouldPreventScroll) { e.preventDefault(); } }, []); useEffect(() => { const isNestPopup = checkParentForId(bodyRef?.current, "tomo-modal-body"); if (isNestPopup) return; const targetElement = document.querySelector("body"); if (opened) { disableBodyScroll(targetElement); document.addEventListener("touchmove", handleTouchMove, { passive: false, }); } else { enableBodyScroll(targetElement); document.removeEventListener("touchmove", handleTouchMove); } return () => { enableBodyScroll(targetElement); document.removeEventListener("touchmove", handleTouchMove); }; }, [opened, bodyRef, handleTouchMove]); useIphoneFocusFix(opened); const handleScroll = useCallback( (e: React.UIEvent) => { onScroll?.(e); }, [onScroll], ); useOnClickOutside(bodyRef, () => { onClose?.(); }); const popupContent = (
e.stopPropagation()} onClick={(e) => e.stopPropagation()} > {header || (
)} {children} {isLoading && }
); if (!getContainer) { return popupContent; } // 获取挂载容器 let container: HTMLElement; if (typeof getContainer === "function") { container = getContainer(); } else if (getContainer instanceof HTMLElement) { container = getContainer; } else { container = document.body; } return createPortal(popupContent, container || document.body); }; export default forwardRef(Popup); /** * Checks if any parent element of the given HTML element has the specified ID. * * @param element - The starting HTML element to check. * @param id - The ID to search for in the element's ancestors. * @returns `true` if an ancestor with the specified ID is found, otherwise `false`. */ const checkParentForId = (element: HTMLElement | null, id: string): boolean => { let parent = element?.parentElement; while (parent) { if (parent.id === id) { return true; } parent = parent.parentElement; } return false; };