UNPKG

drawer-stack

Version:
178 lines (177 loc) 10.7 kB
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; import { Drawer } from "vaul"; import { useDrawerStack } from "./useDrawerStack"; import { useEffect, useState, createContext, useContext } from "react"; import { findRouteAndMatch, flattenRoutes } from "./routeUtils"; import { UNSAFE_RouteContext as RouteContext, } from "react-router"; // Context for drawer search parameters const DrawerSearchParamsContext = createContext(null); // Context to expose the scrollable container element for the drawer content const DrawerScrollContainerContext = createContext(null); export const useDrawerScrollContainerRef = () => useContext(DrawerScrollContainerContext); // Hook to access drawer search parameters export const useDrawerSearchParams = () => { const drawerSearchParams = useContext(DrawerSearchParamsContext); return drawerSearchParams || new URLSearchParams(); }; // This component renders route components inside drawers function DrawerContent({ path, // level, onClose, // onNavigateInDrawer, routes, closeButton: CloseButton, borderRadius, }) { const [scrollContainerEl, setScrollContainerEl] = useState(null); // Ignore root route to prevent recursive rendering if (path === "/") { return (_jsxs("div", { className: "flex flex-col h-full", children: [_jsxs("div", { className: "flex items-center justify-between p-4", children: [_jsx("div", { className: "flex-1 text-center", children: _jsx("h1", { className: "font-medium", children: "Root Route" }) }), CloseButton && _jsx(CloseButton, { onClick: onClose })] }), _jsxs("div", { className: "flex-1 overflow-auto p-4", children: [_jsx("p", { children: "The root route cannot be displayed in a drawer." }), _jsx("p", { className: "text-sm text-gray-600 mt-2", children: "Try opening other routes instead." })] })] })); } const flatRoutes = flattenRoutes(routes); const result = findRouteAndMatch(path, flatRoutes); if (!result || !result.route.element) { return (_jsxs("div", { className: "flex flex-col h-full", children: [_jsxs("div", { className: "flex items-center justify-between p-4", children: [_jsx("div", { className: "flex-1 text-center", children: _jsx("h1", { className: "font-medium", children: "Not Found" }) }), CloseButton && _jsx(CloseButton, { onClick: onClose })] }), _jsx("div", { className: "flex-1 overflow-auto p-4", children: _jsxs("p", { children: ["No route found for path: ", path] }) })] })); } const { route, match } = result; // Parse search parameters from the drawer path const [, drawerSearch] = path.split("?"); const drawerSearchParams = new URLSearchParams(drawerSearch || ""); const routeContextValue = { outlet: null, matches: [ { params: match.params, pathname: match.pathname, pathnameBase: match.pathname, route: route, }, ], isDataRoute: false, }; return (_jsxs("div", { className: "flex flex-col h-full flex-1 min-h-0 relative", children: [CloseButton && _jsx(CloseButton, { onClick: onClose }), _jsx("div", { className: "overflow-x-auto overflow-y-auto flex-1", style: { borderTopLeftRadius: borderRadius, borderTopRightRadius: borderRadius, }, ref: setScrollContainerEl, children: _jsx(DrawerSearchParamsContext.Provider, { value: drawerSearchParams, children: _jsx(DrawerScrollContainerContext.Provider, { value: scrollContainerEl, children: _jsx(RouteContext.Provider, { value: routeContextValue, children: route.element }) }) }) })] })); } export function DrawerStack({ routes, STACK_GAP = 40, STACK_SQUEEZE = 0.04, closeButton, height = "85%", backgroundColor = "white", borderRadius = "10px", handleClassName, }) { const { drawerStack, hasDrawers, popDrawerInternal, closeAllDrawers, pushDrawer, } = useDrawerStack(); const [openDrawers, setOpenDrawers] = useState([]); const [closingDrawers, setClosingDrawers] = useState(new Set()); const [draggingDrawers, setDraggingDrawers] = useState(new Set()); // Sync open state with drawer stack useEffect(() => { setOpenDrawers(drawerStack.map(() => true)); }, [drawerStack.length]); // Handle drawer close with animation delay const handleDrawerClose = (level) => { // Mark this drawer as closing immediately so other drawers can start moving setClosingDrawers((prev) => new Set([...prev, level])); // Set the drawer as closed to trigger exit animation setOpenDrawers((prev) => { const newOpen = [...prev]; newOpen[level] = false; return newOpen; }); // Remove from URL after animation completes setTimeout(() => { if (level === drawerStack.length - 1) { // Closing the top drawer popDrawerInternal(); } else { // Closing a drawer in the middle - close all above it closeAllDrawers(); } // Clean up closing state after URL update to prevent position jiggle setTimeout(() => { setClosingDrawers((prev) => { const newSet = new Set(prev); newSet.delete(level); return newSet; }); }, 50); }, 300); }; // Listen for animated close events useEffect(() => { const handlePopDrawerAnimated = (event) => { const { level } = event.detail; if (level >= 0 && level < drawerStack.length) { handleDrawerClose(level); } }; window.addEventListener("popDrawerAnimated", handlePopDrawerAnimated); return () => { window.removeEventListener("popDrawerAnimated", handlePopDrawerAnimated); }; }, [drawerStack.length, handleDrawerClose]); // Handle navigation within drawers const handleNavigateInDrawer = (path) => { pushDrawer(path); }; if (!hasDrawers) { return null; } return (_jsx(_Fragment, { children: drawerStack.map((drawer, index) => { const isClosing = closingDrawers.has(index); const isDragging = draggingDrawers.has(index); // Calculate effective stack excluding closing drawers const effectiveStack = drawerStack.filter((_, i) => !closingDrawers.has(i)); const effectiveIndex = effectiveStack.findIndex((d) => d.id === drawer.id); const zIndex = 50 + index; // Ensure proper stacking return (_jsx(Drawer.Root, { open: openDrawers[index] || false, onOpenChange: (open) => { if (!open) { handleDrawerClose(index); } }, handleOnly: true, onDrag: () => { // event, percentageDragged // Mark as dragging when drag starts setDraggingDrawers((prev) => new Set([...prev, index])); }, onRelease: () => { // event, open // Remove from dragging state when released setDraggingDrawers((prev) => { const newSet = new Set(prev); newSet.delete(index); return newSet; }); }, children: _jsxs(Drawer.Portal, { children: [_jsx(Drawer.Overlay, { className: "fixed inset-0 bg-black/40", style: { zIndex: zIndex } }), _jsxs(Drawer.Content, { className: "flex flex-col mt-24 fixed bottom-0 left-0 right-0", style: { borderTopRightRadius: borderRadius, borderTopLeftRadius: borderRadius, backgroundColor: backgroundColor, height: height, zIndex: zIndex + 1, // Remove focus ring outline: "none", // If closing or dragging, don't apply any custom transforms - let Vaul handle it transform: isClosing || isDragging ? "none" : `translateY(${(effectiveStack.length - 1 - effectiveIndex) * -STACK_GAP}px) scale(${1 - (effectiveStack.length - 1 - effectiveIndex) * STACK_SQUEEZE})`, // Only apply transition when not closing or dragging transition: isClosing || isDragging ? "none" : "transform 300ms ease-out", }, onPointerDownOutside: (event) => { const toastContainer = document.querySelector("[data-toast-container]"); const originalTarget = event?.target || event?.originalEvent?.target || event?.detail?.originalEvent?.target; if (toastContainer && originalTarget && toastContainer.contains(originalTarget)) { event.preventDefault(); } }, onInteractOutside: (event) => { const toastContainer = document.querySelector("[data-toast-container]"); const originalTarget = event?.target || event?.originalEvent?.target || event?.detail?.originalEvent?.target; if (toastContainer && originalTarget && toastContainer.contains(originalTarget)) { event.preventDefault(); } }, children: [handleClassName && (_jsx(Drawer.Handle, { className: handleClassName })), _jsx("div", { className: "flex-1 min-h-0", children: _jsx(DrawerContent, { path: drawer.path, level: drawer.level, onClose: () => handleDrawerClose(index), onNavigateInDrawer: handleNavigateInDrawer, routes: routes, closeButton: closeButton, borderRadius: borderRadius }) })] })] }) }, drawer.id)); }) })); }