From d62790cd78093b5b7149e3aa62402abd7ba23282 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Mon, 28 Apr 2025 09:41:08 -0500 Subject: [PATCH] Add ability to use the browser back button to close sheets and dialogs (#17932) --- .../overlay/detail/ReviewDetailDialog.tsx | 6 +- .../overlay/detail/SearchDetailDialog.tsx | 6 +- web/src/components/ui/dialog.tsx | 118 +++++++++++++---- web/src/components/ui/sheet.tsx | 123 ++++++++++++++---- 4 files changed, 196 insertions(+), 57 deletions(-) diff --git a/web/src/components/overlay/detail/ReviewDetailDialog.tsx b/web/src/components/overlay/detail/ReviewDetailDialog.tsx index c296866c4..a30ad8989 100644 --- a/web/src/components/overlay/detail/ReviewDetailDialog.tsx +++ b/web/src/components/overlay/detail/ReviewDetailDialog.tsx @@ -156,7 +156,11 @@ export default function ReviewDetailDialog({ return ( <> - + setUpload(undefined)} diff --git a/web/src/components/overlay/detail/SearchDetailDialog.tsx b/web/src/components/overlay/detail/SearchDetailDialog.tsx index 8cbddf63a..f8f79e021 100644 --- a/web/src/components/overlay/detail/SearchDetailDialog.tsx +++ b/web/src/components/overlay/detail/SearchDetailDialog.tsx @@ -187,7 +187,11 @@ export default function SearchDetailDialog({ const Description = isDesktop ? DialogDescription : MobilePageDescription; return ( - + { + const [internalOpen, setInternalOpen] = React.useState(open || false); + const historyStateRef = React.useRef void; + }>(null); -const DialogTrigger = DialogPrimitive.Trigger + React.useEffect(() => { + if (open !== undefined) { + setInternalOpen(open); + } + }, [open]); -const DialogPortal = DialogPrimitive.Portal + React.useEffect(() => { + if (enableHistoryBack) { + if (internalOpen) { + window.history.pushState({ dialogOpen: true }, ""); -const DialogClose = DialogPrimitive.Close + const listener = () => { + setInternalOpen(false); + if (onOpenChange) onOpenChange(false); + }; + + historyStateRef.current = { listener }; + window.addEventListener("popstate", listener); + + return () => { + if (internalOpen) { + window.removeEventListener("popstate", listener); + historyStateRef.current = null; + } + }; + } else if (historyStateRef.current) { + window.removeEventListener( + "popstate", + historyStateRef.current.listener, + ); + historyStateRef.current = null; + } + } + }, [enableHistoryBack, internalOpen, onOpenChange]); + + const handleOpenChange = (open: boolean) => { + setInternalOpen(open); + if (onOpenChange) { + onOpenChange(open); + } + }; + + return ( + + ); +}; + +Dialog.displayName = "Dialog"; + +const DialogTrigger = DialogPrimitive.Trigger; +const DialogPortal = DialogPrimitive.Portal; +const DialogClose = DialogPrimitive.Close; const DialogOverlay = React.forwardRef< React.ElementRef, @@ -20,12 +84,12 @@ const DialogOverlay = React.forwardRef< ref={ref} className={cn( "fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", - className + className, )} {...props} /> -)) -DialogOverlay.displayName = DialogPrimitive.Overlay.displayName +)); +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; const DialogContent = React.forwardRef< React.ElementRef, @@ -37,19 +101,19 @@ const DialogContent = React.forwardRef< ref={ref} className={cn( "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg", - className + className, )} {...props} > {children} - + Close -)) -DialogContent.displayName = DialogPrimitive.Content.displayName +)); +DialogContent.displayName = DialogPrimitive.Content.displayName; const DialogHeader = ({ className, @@ -58,12 +122,12 @@ const DialogHeader = ({
-) -DialogHeader.displayName = "DialogHeader" +); +DialogHeader.displayName = "DialogHeader"; const DialogFooter = ({ className, @@ -72,12 +136,12 @@ const DialogFooter = ({
-) -DialogFooter.displayName = "DialogFooter" +); +DialogFooter.displayName = "DialogFooter"; const DialogTitle = React.forwardRef< React.ElementRef, @@ -87,12 +151,12 @@ const DialogTitle = React.forwardRef< ref={ref} className={cn( "text-lg font-semibold leading-none tracking-tight", - className + className, )} {...props} /> -)) -DialogTitle.displayName = DialogPrimitive.Title.displayName +)); +DialogTitle.displayName = DialogPrimitive.Title.displayName; const DialogDescription = React.forwardRef< React.ElementRef, @@ -103,8 +167,8 @@ const DialogDescription = React.forwardRef< className={cn("text-sm text-muted-foreground", className)} {...props} /> -)) -DialogDescription.displayName = DialogPrimitive.Description.displayName +)); +DialogDescription.displayName = DialogPrimitive.Description.displayName; export { Dialog, @@ -117,4 +181,4 @@ export { DialogFooter, DialogTitle, DialogDescription, -} +}; diff --git a/web/src/components/ui/sheet.tsx b/web/src/components/ui/sheet.tsx index 34e5dcaf3..7ef05478f 100644 --- a/web/src/components/ui/sheet.tsx +++ b/web/src/components/ui/sheet.tsx @@ -1,17 +1,84 @@ -import * as React from "react" -import * as SheetPrimitive from "@radix-ui/react-dialog" -import { cva, type VariantProps } from "class-variance-authority" -import { X } from "lucide-react" +import * as React from "react"; +import * as SheetPrimitive from "@radix-ui/react-dialog"; +import { cva, type VariantProps } from "class-variance-authority"; +import { X } from "lucide-react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; -const Sheet = SheetPrimitive.Root +// Enhanced Sheet with History Support +interface HistorySheetProps extends SheetPrimitive.DialogProps { + enableHistoryBack?: boolean; +} -const SheetTrigger = SheetPrimitive.Trigger +const Sheet = ({ + enableHistoryBack = false, + open, + onOpenChange, + ...props +}: HistorySheetProps) => { + const [internalOpen, setInternalOpen] = React.useState(open || false); + const historyStateRef = React.useRef void; + }>(null); -const SheetClose = SheetPrimitive.Close + React.useEffect(() => { + if (open !== undefined) { + setInternalOpen(open); + } + }, [open]); -const SheetPortal = SheetPrimitive.Portal + React.useEffect(() => { + if (enableHistoryBack) { + if (internalOpen) { + window.history.pushState({ sheetOpen: true }, ""); + + const listener = () => { + setInternalOpen(false); + if (onOpenChange) onOpenChange(false); + }; + + historyStateRef.current = { listener }; + window.addEventListener("popstate", listener); + + return () => { + if (internalOpen) { + window.removeEventListener("popstate", listener); + historyStateRef.current = null; + } + }; + } else if (historyStateRef.current) { + window.removeEventListener( + "popstate", + historyStateRef.current.listener, + ); + historyStateRef.current = null; + } + } + }, [enableHistoryBack, internalOpen, onOpenChange]); + + const handleOpenChange = (open: boolean) => { + setInternalOpen(open); + if (onOpenChange) { + onOpenChange(open); + } + }; + + return ( + + ); +}; + +Sheet.displayName = "Sheet"; + +const SheetTrigger = SheetPrimitive.Trigger; + +const SheetClose = SheetPrimitive.Close; + +const SheetPortal = SheetPrimitive.Portal; const SheetOverlay = React.forwardRef< React.ElementRef, @@ -20,13 +87,13 @@ const SheetOverlay = React.forwardRef< -)) -SheetOverlay.displayName = SheetPrimitive.Overlay.displayName +)); +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", @@ -44,8 +111,8 @@ const sheetVariants = cva( defaultVariants: { side: "right", }, - } -) + }, +); interface SheetContentProps extends React.ComponentPropsWithoutRef, @@ -63,14 +130,14 @@ const SheetContent = React.forwardRef< {...props} > {children} - + Close -)) -SheetContent.displayName = SheetPrimitive.Content.displayName +)); +SheetContent.displayName = SheetPrimitive.Content.displayName; const SheetHeader = ({ className, @@ -79,12 +146,12 @@ const SheetHeader = ({
-) -SheetHeader.displayName = "SheetHeader" +); +SheetHeader.displayName = "SheetHeader"; const SheetFooter = ({ className, @@ -93,12 +160,12 @@ const SheetFooter = ({
-) -SheetFooter.displayName = "SheetFooter" +); +SheetFooter.displayName = "SheetFooter"; const SheetTitle = React.forwardRef< React.ElementRef, @@ -109,8 +176,8 @@ const SheetTitle = React.forwardRef< className={cn("text-lg font-semibold text-foreground", className)} {...props} /> -)) -SheetTitle.displayName = SheetPrimitive.Title.displayName +)); +SheetTitle.displayName = SheetPrimitive.Title.displayName; const SheetDescription = React.forwardRef< React.ElementRef, @@ -121,8 +188,8 @@ const SheetDescription = React.forwardRef< className={cn("text-sm text-muted-foreground", className)} {...props} /> -)) -SheetDescription.displayName = SheetPrimitive.Description.displayName +)); +SheetDescription.displayName = SheetPrimitive.Description.displayName; export { Sheet, @@ -135,4 +202,4 @@ export { SheetFooter, SheetTitle, SheetDescription, -} +};