Skip to content

Commit

Permalink
Merge pull request #4 from Shunseii/feat/drag-close-mobile-menu
Browse files Browse the repository at this point in the history
feat: allow dragging to close mobile menu
  • Loading branch information
Shunseii authored Jul 1, 2024
2 parents 08b3ecc + dece9ee commit 9bec75c
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 28 deletions.
109 changes: 102 additions & 7 deletions apps/web/src/components/MobileHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,91 @@
import { NavLink } from "@/components/NavLink";
import { Button } from "@/components/ui/button";
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
import {
Sheet,
SheetContent,
SheetOverlay,
SheetTrigger,
} from "@/components/ui/sheet";
import { useDir } from "@/hooks/useDir";
import { trpc } from "@/lib/trpc";
import { Trans } from "@lingui/macro";
import { useQueryClient } from "@tanstack/react-query";
import { Link, useNavigate } from "@tanstack/react-router";
import { useClickAway } from "@uidotdev/usehooks";
import { Book, Home, PanelLeft, Settings } from "lucide-react";
import { FC, PropsWithChildren } from "react";
import React, { FC, PropsWithChildren } from "react";
import * as SheetPrimitive from "@radix-ui/react-dialog";
import { cn } from "@/lib/utils";
import { X } from "lucide-react";
import { sheetVariantsNoSlideAnimations } from "./ui/sheet/variants";
import { atom, useAtom } from "jotai";
import { motion } from "framer-motion";

const isOpenAtom = atom(false);

const DraggableSheetContent: typeof SheetContent = React.forwardRef(
({ side = "right", className, children, ...props }, ref) => {
const [isOpen, setIsOpen] = useAtom(isOpenAtom);
const dir = useDir();

return (
<div>
<SheetOverlay />

<motion.div
drag="x"
initial={{ x: 0 }}
animate={{ x: isOpen ? 0 : dir === "rtl" ? 1000 : -1000 }}
dragMomentum={false}
transition={{ type: "just" }}
dragElastic={{
left: dir === "rtl" ? 0 : 1,
right: dir === "rtl" ? 1 : 0,
}}
className="fixed sm:hidden top-0 ltr:left-0 rtl:right-0 z-[100] h-full w-screen pointer-events-none"
dragConstraints={{ left: 0, right: 0 }}
onDragEnd={(_e, info) => {
const { x: xOffset } = info.offset;
const { x: xVelocity } = info.velocity;

const isDraggedRight = xVelocity > 0 || xOffset > 0;
const isDraggedLeft = xVelocity < 0 || xOffset < 0;

if (
(dir === "rtl" && isDraggedRight) ||
(dir === "ltr" && isDraggedLeft)
) {
setIsOpen(false);
}
}}
>
<SheetPrimitive.Content
ref={ref}
className={cn(sheetVariantsNoSlideAnimations({ side }), className)}
{...props}
>
{children}

<button
className="absolute ltr:right-4 top-4 rtl:left-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary"
onClick={() => setIsOpen(false)}
>
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</button>
</SheetPrimitive.Content>
</motion.div>
</div>
);
},
);
DraggableSheetContent.displayName = SheetPrimitive.Content.displayName;

export const MobileHeader: FC<PropsWithChildren> = ({ children }) => {
const [isOpen, setIsOpen] = useAtom(isOpenAtom);
const ref = useClickAway(() => {
setIsOpen(false);
});
const navigate = useNavigate({ from: "/" });
const queryClient = useQueryClient();
const dir = useDir();
Expand All @@ -18,23 +94,33 @@ export const MobileHeader: FC<PropsWithChildren> = ({ children }) => {

return (
<header className="sticky top-0 z-30 flex h-14 items-center gap-4 border-b bg-background px-4 sm:static sm:h-auto sm:border-0 sm:bg-transparent sm:px-6">
<Sheet>
<Sheet open={isOpen}>
<SheetTrigger asChild>
<Button size="icon" variant="outline" className="sm:hidden">
<Button
size="icon"
variant="outline"
className="sm:hidden"
onClick={() => setIsOpen(!isOpen)}
>
<PanelLeft className="h-5 w-5" />
<span className="sr-only">
<Trans>Toggle Menu</Trans>
</span>
</Button>
</SheetTrigger>

<SheetContent
<DraggableSheetContent
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ref={ref as any}
side={dir === "rtl" ? "right" : "left"}
className="sm:max-w-xs"
>
<nav className="flex flex-col gap-y-6 text-lg font-medium">
<Link
href="/"
onClick={() => {
setIsOpen(false);
}}
className="group flex h-10 w-10 shrink-0 items-center justify-center gap-y-2 rounded-full bg-primary text-lg font-semibold text-primary-foreground md:text-base"
>
<Book className="h-5 w-5 transition-all group-hover:scale-110" />
Expand All @@ -44,13 +130,22 @@ export const MobileHeader: FC<PropsWithChildren> = ({ children }) => {
</Link>

<div className="flex flex-col gap-y-2">
<NavLink to="/" className="h-auto w-auto justify-start gap-x-2">
<NavLink
to="/"
className="h-auto w-auto justify-start gap-x-2"
onClick={() => {
setIsOpen(false);
}}
>
<Home className="w-5 h-5" />
<Trans>Home</Trans>
</NavLink>

<NavLink
to="/settings"
onClick={() => {
setIsOpen(false);
}}
className="h-auto w-auto justify-start gap-x-2"
>
<Settings className="w-5 h-5" />
Expand Down Expand Up @@ -78,7 +173,7 @@ export const MobileHeader: FC<PropsWithChildren> = ({ children }) => {
</p>
</Button>
</nav>
</SheetContent>
</DraggableSheetContent>
</Sheet>

{children}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import * as React from "react";
import * as SheetPrimitive from "@radix-ui/react-dialog";
import { cva, type VariantProps } from "class-variance-authority";
import { type VariantProps } from "class-variance-authority";
import { X } from "lucide-react";

import { cn } from "@/lib/utils";
import { sheetVariants } from "./variants";

const Sheet = SheetPrimitive.Root;

Expand All @@ -19,7 +20,7 @@ const SheetOverlay = React.forwardRef<
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...props}
Expand All @@ -28,25 +29,6 @@ const SheetOverlay = React.forwardRef<
));
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;

const sheetVariants = cva(
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
{
variants: {
side: {
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
bottom:
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
right:
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
},
},
defaultVariants: {
side: "right",
},
},
);

interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {}
Expand Down
37 changes: 37 additions & 0 deletions apps/web/src/components/ui/sheet/variants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { cva } from "class-variance-authority";

export const sheetVariants = cva(
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
{
variants: {
side: {
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
bottom:
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
right:
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
},
},
defaultVariants: {
side: "right",
},
},
);

export const sheetVariantsNoSlideAnimations = cva(
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
{
variants: {
side: {
top: "inset-x-0 top-0 border-b",
bottom: "inset-x-0 bottom-0 border-t",
left: "inset-y-0 left-0 h-full w-3/4 border-r ",
right: "inset-y-0 right-0 h-full w-3/4 border-l ",
},
},
defaultVariants: {
side: "right",
},
},
);

0 comments on commit 9bec75c

Please sign in to comment.