Action Drawer
Prerequisites
Ensure you have VizStats UI installed in your project. If not, follow the installation guide.
Click to switch between light and dark modes
Step 1: Install Dependencies
Install the necessary dependencies using your preferred package manager:
npm install zustand react-responsive
Step 2: Set Up the actionDrawerStore
Create a new file called actionDrawerStore.ts in your store directory:
store/actionDrawerStore.ts
import { create } from "zustand";
type FilterState = {
isDrawerOpen: boolean;
actionDrawerContent: React.ReactNode; // New state for dynamic content
toggleDrawer: () => void;
setDrawerContent: (content: React.ReactNode) => void;
openDrawer: () => void;
closeDrawer: () => void;
};
const useActionDrawerStore = create<FilterState>((set) => ({
isDrawerOpen: false,
actionDrawerContent: null,
toggleDrawer: () => set((state) => ({ isDrawerOpen: !state.isDrawerOpen })),
setDrawerContent: (content) => set({ actionDrawerContent: content }),
openDrawer: () => set({ isDrawerOpen: true }),
closeDrawer: () => set({ isDrawerOpen: false, actionDrawerContent: null }),
}));
export default useActionDrawerStore;
Step 3: Set Up hooks
Create a new file called useDeviceType.ts in your hooks directory:
hooks/useDeviceType.ts
import { useMediaQuery } from "react-responsive";
const useDeviceType = () => {
const isMobile = useMediaQuery({ maxWidth: 639, minWidth: 200 });
const isTablet = useMediaQuery({ minWidth: 640, maxWidth: 1023 });
const isDesktop = useMediaQuery({ minWidth: 1024 });
return { isMobile, isTablet, isDesktop };
};
export default useDeviceType;
Step 4: Create Action Drawer Component
ActionDrawer
Create a new component called action-drawer.tsx
"use client";
import { useCallback, ReactNode, useMemo } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Filter } from "lucide-react";
import { Cross2Icon } from "@radix-ui/react-icons";
import { Button, ButtonProps } from "@/components/ui/button";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import useDeviceType from "@/hooks/useDeviceType";
import useActionDrawerStore from "@/store/actionDrawerStore";
import { cn } from "@/lib/utils";
type ActionButtonProps = {
children?: ReactNode;
className?: string;
label?: string;
variant?: ButtonProps["variant"];
size?: ButtonProps["size"];
};
type ActionTooltipProps = ActionButtonProps & {
tooltip: string;
};
const sharedButtonStyles =
"gap-2 rounded-lg shadow-lg transition-all duration-300 active:scale-95";
export const ActionDrawerButton: React.FC<ActionButtonProps> = ({
children,
className,
label = "Button",
variant = "default",
size = "default",
}) => {
const { toggleDrawer, setDrawerContent } = useActionDrawerStore();
const handleClick = useCallback(() => {
setDrawerContent(children);
toggleDrawer();
}, [children, setDrawerContent, toggleDrawer]);
return (
<Button
variant={variant}
size={size}
className={cn(className, sharedButtonStyles)}
onClick={handleClick}
>
<Filter className="h-4 w-4" />
<span className="text-pxs sm:text-sm">{label}</span>
</Button>
);
};
export const ActionDrawerTooltip: React.FC<ActionTooltipProps> = ({
children,
label,
className,
tooltip = "Tooltip",
variant = "default",
size = "icon",
}) => {
const { toggleDrawer, setDrawerContent } = useActionDrawerStore();
const handleClick = useCallback(() => {
setDrawerContent(children);
toggleDrawer();
}, [children, setDrawerContent, toggleDrawer]);
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant={variant}
size={size}
className={cn(className, sharedButtonStyles)}
onClick={handleClick}
>
<Filter className="h-4 w-4" />
{label && <span className="text-pxs sm:text-sm">{label}</span>}
<span className="sr-only">{label}</span>
</Button>
</TooltipTrigger>
{tooltip && <TooltipContent side="top">{tooltip}</TooltipContent>}
</Tooltip>
</TooltipProvider>
);
};
export const ActionDrawer = () => {
const { isMobile } = useDeviceType();
const { isDrawerOpen, closeDrawer, actionDrawerContent } =
useActionDrawerStore();
const memoizedContent = useMemo(
() => actionDrawerContent,
[actionDrawerContent]
);
const handleOverlayClick = () => {
if (isDrawerOpen) {
closeDrawer();
}
};
const animationVariants = useMemo(
() => ({
initial: { x: "-100%" },
animate: { x: 0 },
exit: { x: "-100%" },
}),
[]
);
return (
<AnimatePresence>
{isDrawerOpen && (
<>
{isMobile && (
<div
className="fixed inset-0 z-[35] bg-black opacity-50"
aria-hidden="true"
tabIndex={-1}
onClick={handleOverlayClick}
/>
)}
<motion.div
className="fixed left-0 top-0 z-40 h-screen overflow-hidden bg-background shadow-lg sm:border-r dark:bg-background sm:py-10"
initial="initial"
animate="animate"
exit="exit"
variants={animationVariants}
transition={{
type: isDrawerOpen ? "tween" : "spring",
duration: 0.3,
}}
role="dialog"
aria-modal="true"
>
<div className="h-full w-64 sm:w-72">
<div className="flex h-full w-full flex-col gap-4">
{!isMobile && (
<Button
variant="ghost"
size="icon"
className="absolute right-3 top-3 h-4 w-4 rounded-full p-0 opacity-70 ring-offset-background transition-opacity data-[state=open]:bg-secondary hover:bg-transparent hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none"
onClick={closeDrawer}
>
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</Button>
)}
<div className="py-2 mx-auto">{memoizedContent}</div>
</div>
</div>
</motion.div>
</>
)}
</AnimatePresence>
);
};
Step 5: Use the Component
Add the ThemeToggle component to your layout or navbar:
import { ActionDrawer, ActionDrawerButton } from "@/components/elements/action-drawer";
export function Page() {
return (
<main>
<ActionDrawerButton label="Filter" variant="secondary">
{/* Other content */}
</ActionDrawerButton>
<ActionDrawer />
</main>
)
}