Guest User

react-mobile-navigation

a guest
Jul 30th, 2025
17
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
TypeScript 8.63 KB | Source Code | 0 0
  1. import React, {
  2.   createContext,
  3.   useContext,
  4.   useState,
  5.   useCallback,
  6.   useEffect,
  7.   useRef,
  8.   useMemo,
  9. } from "react";
  10. import { motion, AnimatePresence, type Variants } from "motion/react";
  11. import { easeOutExpo } from "easing-utils";
  12.  
  13. // ----------------------
  14. // 🔁 Preset Transitions (Optional)
  15. // ----------------------
  16.  
  17. export const PresetTransitions = {
  18.   slide: {
  19.     initial: { x: "100%" },
  20.     animate: { x: 0 },
  21.     exit: { x: "100%" },
  22.   },
  23.   slideLeft: {
  24.     initial: { x: "-100%" },
  25.     animate: { x: 0 },
  26.     exit: { x: "-100%" },
  27.   },
  28.   slideUp: {
  29.     initial: { y: "100%" },
  30.     animate: { y: 0 },
  31.     exit: { y: "100%" },
  32.   },
  33.   slideDown: {
  34.     initial: { y: "-100%" },
  35.     animate: { y: 0 },
  36.     exit: { y: "-100%" },
  37.   },
  38.   fade: {
  39.     initial: { opacity: 0 },
  40.     animate: { opacity: 1 },
  41.     exit: { opacity: 0 },
  42.   },
  43.   scale: {
  44.     initial: { scale: 0.8, opacity: 0 },
  45.     animate: { scale: 1, opacity: 1 },
  46.     exit: { scale: 0.8, opacity: 0 },
  47.   },
  48.   scaleUp: {
  49.     initial: { scale: 0.5, opacity: 0 },
  50.     animate: { scale: 1, opacity: 1 },
  51.     exit: { scale: 0.5, opacity: 0 },
  52.   },
  53.   scaleDown: {
  54.     initial: { scale: 1.2, opacity: 0 },
  55.     animate: { scale: 1, opacity: 1 },
  56.     exit: { scale: 1.2, opacity: 0 },
  57.   },
  58.   flip: {
  59.     initial: { rotateY: 90, opacity: 0 },
  60.     animate: { rotateY: 0, opacity: 1 },
  61.     exit: { rotateY: 90, opacity: 0 },
  62.   },
  63.   rotate: {
  64.     initial: { rotate: -90, opacity: 0 },
  65.     animate: { rotate: 0, opacity: 1 },
  66.     exit: { rotate: 90, opacity: 0 },
  67.   },
  68. } satisfies Record<string, Variants>;
  69.  
  70. // ----------------------
  71. // 🔠 Type Declarations
  72. // ----------------------
  73.  
  74. interface PageStackItem {
  75.   id: number;
  76.   component: React.ReactElement;
  77.   transition?: Variants;
  78. }
  79.  
  80. interface NavigationContextType {
  81.   push: (component: React.ReactElement, transition?: Variants) => void;
  82.   pop: () => void;
  83.   replace: (component: React.ReactElement, transition?: Variants) => void;
  84.   popToRoot: () => void;
  85.   canGoBack: boolean;
  86.   stackLength: number;
  87.   _registerPagePopState: (
  88.     id: number,
  89.     setPopped: (isPopped: boolean) => void
  90.   ) => () => void;
  91. }
  92.  
  93. // ---------------------------
  94. // 🌐 Navigation Context Setup
  95. // ---------------------------
  96.  
  97. const NavigationContext = createContext<NavigationContextType | null>(null);
  98.  
  99. // ------------------------------
  100. // 🧭 Stack Navigator Component
  101. // ------------------------------
  102.  
  103. export const StackNavigator = ({
  104.   children,
  105.   initialRoute,
  106. }: {
  107.   children?: React.ReactElement;
  108.   initialRoute: React.ReactElement;
  109. }) => {
  110.   const initialPage: PageStackItem = useMemo(
  111.     () => ({
  112.       id: Date.now(),
  113.       component: initialRoute || children!,
  114.     }),
  115.     [initialRoute, children]
  116.   );
  117.  
  118.   const stackRef = useRef<PageStackItem[]>([initialPage]);
  119.   const [, forceUpdate] = useState(0);
  120.  
  121.   const pagePopStateSetters = useRef<Map<number, (isPopped: boolean) => void>>(
  122.     new Map()
  123.   );
  124.  
  125.   const setStackAndRef = useCallback(
  126.     (updater: (prev: PageStackItem[]) => PageStackItem[]) => {
  127.       stackRef.current = updater(stackRef.current);
  128.       forceUpdate((n) => n + 1);
  129.     },
  130.     []
  131.   );
  132.  
  133.   const push = useCallback(
  134.     (component: React.ReactElement, transition?: Variants) => {
  135.       const newPage: PageStackItem = {
  136.         id: Date.now(),
  137.         component,
  138.         transition,
  139.       };
  140.       if (typeof window !== "undefined") {
  141.         window.history.pushState({ page: "pushed" }, "");
  142.       }
  143.       setStackAndRef((prev) => [...prev, newPage]);
  144.     },
  145.     [setStackAndRef]
  146.   );
  147.  
  148.   const pop = useCallback(() => {
  149.     if (stackRef.current.length > 1 && typeof window !== "undefined") {
  150.       window.history.back();
  151.     }
  152.   }, []);
  153.  
  154.   const replace = useCallback(
  155.     (component: React.ReactElement, transition?: Variants) => {
  156.       const newPage: PageStackItem = {
  157.         id: Date.now(),
  158.         component,
  159.         transition,
  160.       };
  161.       setStackAndRef((prev) => {
  162.         const oldPage = prev.at(-1);
  163.         oldPage && pagePopStateSetters.current.get(oldPage.id)?.(true);
  164.         return [...prev.slice(0, -1), newPage];
  165.       });
  166.     },
  167.     [setStackAndRef]
  168.   );
  169.  
  170.   const popToRoot = useCallback(() => {
  171.     setStackAndRef((prev) => {
  172.       const popsNeeded = prev.length - 1;
  173.       if (typeof window !== "undefined" && popsNeeded > 0) {
  174.         for (let i = 1; i < prev.length; i++) {
  175.           pagePopStateSetters.current.get(prev[i].id)?.(true);
  176.         }
  177.         window.history.go(-popsNeeded);
  178.       }
  179.       return prev;
  180.     });
  181.   }, [setStackAndRef]);
  182.  
  183.   const _registerPagePopState = useCallback(
  184.     (id: number, setPopped: (isPopped: boolean) => void) => {
  185.       pagePopStateSetters.current.set(id, setPopped);
  186.       return () => pagePopStateSetters.current.delete(id);
  187.     },
  188.     []
  189.   );
  190.  
  191.   useEffect(() => {
  192.     if (typeof window !== "undefined") {
  193.       window.history.replaceState({ page: "initial" }, "");
  194.     }
  195.  
  196.     const onPopState = () => {
  197.       setStackAndRef((prev) => {
  198.         if (prev.length > 1) {
  199.           const poppedPage = prev.at(-1);
  200.           poppedPage && pagePopStateSetters.current.get(poppedPage.id)?.(true);
  201.           return prev.slice(0, -1);
  202.         }
  203.         return prev;
  204.       });
  205.     };
  206.  
  207.     window.addEventListener("popstate", onPopState);
  208.     return () => window.removeEventListener("popstate", onPopState);
  209.   }, [setStackAndRef]);
  210.  
  211.   const stack = stackRef.current;
  212.  
  213.   const contextValue = useMemo<NavigationContextType>(
  214.     () => ({
  215.       push,
  216.       pop,
  217.       replace,
  218.       popToRoot,
  219.       canGoBack: stack.length > 1,
  220.       stackLength: stack.length,
  221.       _registerPagePopState,
  222.     }),
  223.     [push, pop, replace, popToRoot, stack.length, _registerPagePopState]
  224.   );
  225.  
  226.   return (
  227.     <NavigationContext.Provider value={contextValue}>
  228.       <div className="relative w-full h-full overflow-clip">
  229.         <AnimatePresence>
  230.           {stack.map((page, index) => (
  231.             <MemoizedPageContainer
  232.               key={page.id}
  233.               page={page}
  234.               isTop={index === stack.length - 1}
  235.             />
  236.           ))}
  237.         </AnimatePresence>
  238.       </div>
  239.     </NavigationContext.Provider>
  240.   );
  241. };
  242.  
  243. // ------------------------
  244. // 📦 Page Container
  245. // ------------------------
  246.  
  247. interface PageContainerProps {
  248.   page: PageStackItem;
  249.   isTop: boolean;
  250. }
  251.  
  252. const PageContainer = ({ page, isTop }: PageContainerProps) => {
  253.   const { _registerPagePopState } = useNavigator();
  254.   const [isPopped, setIsPopped] = useState(false);
  255.  
  256.   useEffect(() => {
  257.     const unregister = _registerPagePopState(page.id, setIsPopped);
  258.     return unregister;
  259.   }, [page.id, _registerPagePopState]);
  260.  
  261.   const fallbackVariant: Variants = {
  262.     initial: { opacity: 0 },
  263.     animate: { opacity: 1 },
  264.     exit: { opacity: 0 },
  265.   };
  266.  
  267.   const variants = page.transition ?? fallbackVariant;
  268.  
  269.   const dimVariants: Variants = useMemo(
  270.     () => ({
  271.       initial: { backgroundColor: "rgba(0,0,0,0)" },
  272.       animate: { backgroundColor: "rgba(0,0,0,0.5)" },
  273.       exit: { backgroundColor: "rgba(0,0,0,0)" },
  274.     }),
  275.     []
  276.   );
  277.  
  278.   return (
  279.     <div
  280.       className="absolute top-0 left-0 w-full h-full pointer-events-none"
  281.       style={{ zIndex: isTop ? 10 : 1 }}
  282.     >
  283.       <motion.div
  284.         className={`absolute top-0 left-0 w-full h-full`}
  285.         variants={dimVariants}
  286.         initial="initial"
  287.         animate="animate"
  288.         exit="exit"
  289.         transition={{ duration: 0.6, ease: easeOutExpo }}
  290.         style={{ zIndex: 1 }}
  291.       />
  292.       <motion.div
  293.         className={`h-full w-full absolute top-0 left-0 ${
  294.           !isPopped ? "pointer-events-auto" : "pointer-events-none"
  295.         }`}
  296.         initial="initial"
  297.         animate="animate"
  298.         exit="exit"
  299.         variants={variants}
  300.         transition={{ duration: 0.6, ease: easeOutExpo }}
  301.         style={{ zIndex: 2 }}
  302.       >
  303.         {React.isValidElement(page.component)
  304.           ? React.cloneElement(page.component, { isPopped } as any)
  305.           : React.createElement(page.component, { isPopped } as any)}
  306.       </motion.div>
  307.     </div>
  308.   );
  309. };
  310.  
  311. const MemoizedPageContainer = React.memo(PageContainer);
  312.  
  313. // ----------------------------
  314. // 🪝 useNavigator & usePage
  315. // ----------------------------
  316.  
  317. export const useNavigator = (): NavigationContextType => {
  318.   const context = useContext(NavigationContext);
  319.   if (!context) {
  320.     throw new Error("useNavigator must be used within a StackNavigator");
  321.   }
  322.   return context;
  323. };
  324.  
  325. export const usePage = () => {
  326.   const [isPopped, setIsPopped] = useState(false);
  327.   return { isPopped, _setIsPopped: setIsPopped };
  328. };
  329.  
Advertisement
Add Comment
Please, Sign In to add comment