drawer-stack
Version:
Drawer stack for React
111 lines (110 loc) • 4.52 kB
JavaScript
import { useNavigate, useSearchParams, useLocation } from "react-router";
import { useCallback, useMemo } from "react";
export function useDrawerStack() {
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const location = useLocation();
// Parse drawer stack from URL
const drawerStack = useMemo(() => {
const drawerParams = searchParams.getAll("drawer");
return drawerParams.map((path, index) => ({
id: `drawer-${index}`,
path,
level: index,
}));
}, [searchParams]);
// Check if any drawers are open
const hasDrawers = drawerStack.length > 0;
// Get current (top) drawer
const currentDrawer = drawerStack[drawerStack.length - 1] || null;
// Push a new drawer onto the stack
const pushDrawer = useCallback((path) => {
// , title?: string
// Always read current URL state to ensure proper stacking
const currentSearchParams = new URLSearchParams(window.location.search);
currentSearchParams.append("drawer", path);
navigate({
pathname: location.pathname,
search: currentSearchParams.toString(),
}, { replace: false });
}, [navigate, location.pathname]);
// Internal function to actually update the URL (used by DrawerStack after animation)
const popDrawerInternal = useCallback(() => {
const drawerParams = searchParams.getAll("drawer");
if (drawerParams.length === 0)
return;
const newSearchParams = new URLSearchParams(searchParams);
// Remove all drawer params and re-add all but the last one
newSearchParams.delete("drawer");
drawerParams.slice(0, -1).forEach((path) => {
newSearchParams.append("drawer", path);
});
navigate({
pathname: location.pathname,
search: newSearchParams.toString(),
}, { replace: false });
}, [searchParams, navigate, location.pathname]);
// Pop the top drawer from the stack with animation
const popDrawer = useCallback(() => {
if (drawerStack.length === 0)
return;
// Dispatch a custom event that DrawerStack can listen to for animated close
window.dispatchEvent(new CustomEvent("popDrawerAnimated", {
detail: { level: drawerStack.length - 1 },
}));
}, [drawerStack.length]);
// Close all drawers
const closeAllDrawers = useCallback(() => {
const newSearchParams = new URLSearchParams(searchParams);
newSearchParams.delete("drawer");
navigate({
pathname: location.pathname,
search: newSearchParams.toString(),
}, { replace: false });
}, [searchParams, navigate, location.pathname]);
// Replace the current top drawer with a new path
const replaceDrawer = useCallback((path) => {
const drawerParams = searchParams.getAll("drawer");
const newSearchParams = new URLSearchParams(searchParams);
// Remove all drawer params
newSearchParams.delete("drawer");
if (drawerParams.length === 0) {
// No drawers exist, just add the new one
newSearchParams.append("drawer", path);
}
else {
// Replace the top drawer: keep all but the last, then add the new path
drawerParams.slice(0, -1).forEach((drawerPath) => {
newSearchParams.append("drawer", drawerPath);
});
newSearchParams.append("drawer", path);
}
navigate({
pathname: location.pathname,
search: newSearchParams.toString(),
}, { replace: false });
}, [searchParams, navigate, location.pathname]);
// Replace the entire drawer stack
const replaceDrawerStack = useCallback((paths) => {
const newSearchParams = new URLSearchParams(searchParams);
newSearchParams.delete("drawer");
paths.forEach((path) => {
newSearchParams.append("drawer", path);
});
navigate({
pathname: location.pathname,
search: newSearchParams.toString(),
}, { replace: false });
}, [searchParams, navigate, location.pathname]);
return {
drawerStack,
hasDrawers,
currentDrawer,
pushDrawer,
popDrawer,
popDrawerInternal, // For DrawerStack to use after animation
replaceDrawer,
closeAllDrawers,
replaceDrawerStack,
};
}