Skip to content

Commit

Permalink
Add vaul drawer
Browse files Browse the repository at this point in the history
  • Loading branch information
tommybarvaag committed Jul 25, 2023
1 parent 7fa473b commit 0d307bd
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 33 deletions.
2 changes: 1 addition & 1 deletion app/dashboard/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export default async function DashboardLayout({ children }: { children: ReactNod
</div>
</div>
</nav>
<main className="dark:bg-neutral-900">
<main vaul-drawer-wrapper="" className="dark:bg-neutral-900">
<div className="mx-auto max-w-5xl px-4 py-12 lg:py-24">{children}</div>
<Suspense fallback={<UserHeartbeatSkeleton />}>
<UserHeartbeat />
Expand Down
106 changes: 74 additions & 32 deletions components/ui/dialog.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,37 @@
"use client";

import * as DialogPrimitive from "@radix-ui/react-dialog";
import * as React from "react";

import { Icons } from "@/components/icons";
import { Button } from "@/components/ui/button";
import { useWindowSize } from "@/hooks/use-window-size";
import { cn } from "@/lib/utils";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import * as React from "react";
import { Drawer } from "vaul";

const Dialog = DialogPrimitive.Root;
const Dialog = ({ ...props }: DialogPrimitive.DialogProps) => {
const { isMobile } = useWindowSize();
const Component = isMobile ? Drawer.Root : DialogPrimitive.Root;

const DialogTrigger = DialogPrimitive.Trigger;
return <Component shouldScaleBackground {...props} />;
};

Dialog.displayName = DialogPrimitive.Root.displayName;

const DialogTrigger = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Trigger>
>(({ className, children, ...other }, ref) => {
const { isMobile } = useWindowSize();
const Component = isMobile ? Drawer.Trigger : DialogPrimitive.Trigger;

return (
<Component ref={ref} className={cn(className)} {...other}>
{children}
</Component>
);
});

DialogTrigger.displayName = DialogPrimitive.Trigger.displayName;

const DialogPortal = ({ className, children, ...other }: DialogPrimitive.DialogPortalProps) => (
<DialogPrimitive.Portal className={cn(className)} {...other}>
Expand All @@ -18,6 +40,7 @@ const DialogPortal = ({ className, children, ...other }: DialogPrimitive.DialogP
</div>
</DialogPrimitive.Portal>
);

DialogPortal.displayName = DialogPrimitive.Portal.displayName;

const DialogOverlay = React.forwardRef<
Expand All @@ -38,31 +61,50 @@ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...other }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed top-0 z-50 grid w-full gap-4 rounded-b-lg border border-neutral-700 bg-white p-6 animate-in data-[state=open]:fade-in-90 data-[state=open]:slide-in-from-top-full sm:max-w-lg sm:rounded-lg sm:zoom-in-90 data-[state=open]:sm:slide-in-from-top-0 lg:top-auto",
"dark:bg-neutral-900",
className
)}
{...other}
>
{children}
<DialogPrimitive.Close
asChild
className="absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-neutral-400 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-neutral-100 dark:focus:ring-neutral-400 dark:focus:ring-offset-neutral-900 dark:data-[state=open]:bg-neutral-800"
>(({ className, children, ...other }, ref) => {
const { isMobile } = useWindowSize();

if (isMobile) {
return (
<>
<Drawer.Overlay className="fixed inset-0 bg-black/40" />
<Drawer.Portal>
<Drawer.Content className="fixed inset-x-0 bottom-0 mt-24 flex h-full max-h-[96%] flex-col rounded-t-[10px] border border-neutral-700 bg-white px-6 dark:bg-neutral-900">
<div className="mx-auto my-3 h-1 w-12 rounded-full bg-gray-300" />
{children}
</Drawer.Content>
<Drawer.Overlay />
</Drawer.Portal>
</>
);
}

return (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed top-0 z-50 grid w-full gap-4 rounded-b-lg border border-neutral-700 bg-white p-6 animate-in data-[state=open]:fade-in-90 data-[state=open]:slide-in-from-top-full sm:max-w-lg sm:rounded-lg sm:zoom-in-90 data-[state=open]:sm:slide-in-from-top-0 lg:top-auto",
"dark:bg-neutral-900",
className
)}
{...other}
>
<Button className="absolute right-4 top-4" variant="ghost">
<Icons.Close className="h-4 w-4" />
<span className="sr-only">Close</span>
</Button>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
));
{children}
<DialogPrimitive.Close
asChild
className="absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-neutral-400 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-neutral-100 dark:focus:ring-neutral-400 dark:focus:ring-offset-neutral-900 dark:data-[state=open]:bg-neutral-800"
>
<Button className="absolute right-4 top-4" variant="ghost">
<Icons.Close className="h-4 w-4" />
<span className="sr-only">Close</span>
</Button>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
);
});
DialogContent.displayName = DialogPrimitive.Content.displayName;

const DialogHeader = ({ className, ...other }: React.HTMLAttributes<HTMLDivElement>) => (
Expand Down Expand Up @@ -104,10 +146,10 @@ DialogDescription.displayName = DialogPrimitive.Description.displayName;

export {
Dialog,
DialogTrigger,
DialogContent,
DialogHeader,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogDescription
DialogTrigger
};
47 changes: 47 additions & 0 deletions hooks/use-window-size.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { useEffect, useMemo, useState } from "react";

function useWindowSize(): {
width: number;
height: number;
isMobile: boolean;
isDesktop: boolean;
} {
// Initialize state with undefined width/height so server and client renders match
// Learn more here: https://joshwcomeau.com/react/the-perils-of-rehydration/
const [windowSize, setWindowSize] = useState<{
width: number;
height: number;
}>({
width: 0,
height: 0
});
useEffect(() => {
// Handler to call on window resize
function handleResize() {
// Set window width/height to state
setWindowSize({
width: window.innerWidth,
height: window.innerHeight
});
}
// Add event listener
window.addEventListener("resize", handleResize);
// Call handler right away so state gets updated with initial window size
handleResize();
// Remove event listener on cleanup
return () => window.removeEventListener("resize", handleResize);
}, []); // Empty array ensures that effect is only run on mount

// Memoize the result of the calculations using useMemo
const memoizedWindowSize = useMemo(() => {
return {
...windowSize,
isMobile: typeof windowSize?.width === "number" && windowSize.width < 768,
isDesktop: typeof windowSize?.width === "number" && windowSize.width >= 768
};
}, [windowSize]);

return memoizedWindowSize;
}

export { useWindowSize };
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"react-hook-form": "^7.45.1",
"server-only": "^0.0.1",
"tailwind-merge": "^1.13.2",
"vaul": "^0.0.6",
"zod": "^3.21.4"
},
"devDependencies": {
Expand Down
17 changes: 17 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 0d307bd

Please sign in to comment.