mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-12-06 13:34:13 +03:00
Revamp the history handling for dialog components
This commit is contained in:
parent
036a5d84cc
commit
36da8db6d0
@ -13,7 +13,7 @@ import { cn } from "@/lib/utils";
|
|||||||
import { isPWA } from "@/utils/isPWA";
|
import { isPWA } from "@/utils/isPWA";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useHistoryBack } from "@/hooks/use-history-back";
|
||||||
|
|
||||||
const MobilePageContext = createContext<{
|
const MobilePageContext = createContext<{
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@ -24,15 +24,16 @@ type MobilePageProps = {
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
open?: boolean;
|
open?: boolean;
|
||||||
onOpenChange?: (open: boolean) => void;
|
onOpenChange?: (open: boolean) => void;
|
||||||
|
enableHistoryBack?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function MobilePage({
|
export function MobilePage({
|
||||||
children,
|
children,
|
||||||
open: controlledOpen,
|
open: controlledOpen,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
|
enableHistoryBack = true,
|
||||||
}: MobilePageProps) {
|
}: MobilePageProps) {
|
||||||
const [uncontrolledOpen, setUncontrolledOpen] = useState(false);
|
const [uncontrolledOpen, setUncontrolledOpen] = useState(false);
|
||||||
const location = useLocation();
|
|
||||||
|
|
||||||
const open = controlledOpen ?? uncontrolledOpen;
|
const open = controlledOpen ?? uncontrolledOpen;
|
||||||
const setOpen = useCallback(
|
const setOpen = useCallback(
|
||||||
@ -46,33 +47,12 @@ export function MobilePage({
|
|||||||
[onOpenChange, setUncontrolledOpen],
|
[onOpenChange, setUncontrolledOpen],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
// Handle browser back button to close mobile page
|
||||||
let isActive = true;
|
useHistoryBack({
|
||||||
|
enabled: enableHistoryBack,
|
||||||
if (open && isActive) {
|
open,
|
||||||
window.history.pushState({ isMobilePage: true }, "", location.pathname);
|
onClose: () => setOpen(false),
|
||||||
}
|
});
|
||||||
|
|
||||||
const handlePopState = (event: PopStateEvent) => {
|
|
||||||
if (open && isActive) {
|
|
||||||
event.preventDefault();
|
|
||||||
setOpen(false);
|
|
||||||
// Delay replaceState to ensure state updates are processed
|
|
||||||
setTimeout(() => {
|
|
||||||
if (isActive) {
|
|
||||||
window.history.replaceState(null, "", location.pathname);
|
|
||||||
}
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener("popstate", handlePopState);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
isActive = false;
|
|
||||||
window.removeEventListener("popstate", handlePopState);
|
|
||||||
};
|
|
||||||
}, [open, setOpen, location.pathname]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MobilePageContext.Provider value={{ open, onOpenChange: setOpen }}>
|
<MobilePageContext.Provider value={{ open, onOpenChange: setOpen }}>
|
||||||
|
|||||||
@ -538,7 +538,7 @@ export default function SearchDetailDialog({
|
|||||||
<Overlay
|
<Overlay
|
||||||
open={isOpen}
|
open={isOpen}
|
||||||
onOpenChange={handleOpenChange}
|
onOpenChange={handleOpenChange}
|
||||||
enableHistoryBack={false}
|
enableHistoryBack={true}
|
||||||
>
|
>
|
||||||
{isDesktop && onPrevious && onNext && (
|
{isDesktop && onPrevious && onNext && (
|
||||||
<DialogPortal>
|
<DialogPortal>
|
||||||
|
|||||||
@ -113,7 +113,12 @@ export function PlatformAwareSheet({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sheet open={open} onOpenChange={onOpenChange} modal={false}>
|
<Sheet
|
||||||
|
open={open}
|
||||||
|
onOpenChange={onOpenChange}
|
||||||
|
modal={false}
|
||||||
|
enableHistoryBack
|
||||||
|
>
|
||||||
<SheetTrigger asChild className={triggerClassName}>
|
<SheetTrigger asChild className={triggerClassName}>
|
||||||
{trigger}
|
{trigger}
|
||||||
</SheetTrigger>
|
</SheetTrigger>
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import * as React from "react";
|
|||||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useHistoryBack } from "@/hooks/use-history-back";
|
||||||
|
|
||||||
// Enhanced Dialog with History Support
|
// Enhanced Dialog with History Support
|
||||||
interface HistoryDialogProps extends DialogPrimitive.DialogProps {
|
interface HistoryDialogProps extends DialogPrimitive.DialogProps {
|
||||||
@ -15,51 +16,28 @@ const Dialog = ({
|
|||||||
...props
|
...props
|
||||||
}: HistoryDialogProps) => {
|
}: HistoryDialogProps) => {
|
||||||
const [internalOpen, setInternalOpen] = React.useState(open || false);
|
const [internalOpen, setInternalOpen] = React.useState(open || false);
|
||||||
const historyStateRef = React.useRef<null | {
|
|
||||||
listener: (e: PopStateEvent) => void;
|
|
||||||
}>(null);
|
|
||||||
|
|
||||||
|
// Sync internal state with controlled open prop
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (open !== undefined) {
|
if (open !== undefined) {
|
||||||
setInternalOpen(open);
|
setInternalOpen(open);
|
||||||
}
|
}
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
const handleOpenChange = React.useCallback(
|
||||||
if (enableHistoryBack) {
|
(newOpen: boolean) => {
|
||||||
if (internalOpen) {
|
setInternalOpen(newOpen);
|
||||||
window.history.pushState({ dialogOpen: true }, "");
|
onOpenChange?.(newOpen);
|
||||||
|
},
|
||||||
const listener = () => {
|
[onOpenChange],
|
||||||
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) => {
|
// Handle browser back button to close dialog
|
||||||
setInternalOpen(open);
|
useHistoryBack({
|
||||||
if (onOpenChange) {
|
enabled: enableHistoryBack,
|
||||||
onOpenChange(open);
|
open: internalOpen,
|
||||||
}
|
onClose: () => handleOpenChange(false),
|
||||||
};
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogPrimitive.Root
|
<DialogPrimitive.Root
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority";
|
|||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useHistoryBack } from "@/hooks/use-history-back";
|
||||||
|
|
||||||
// Enhanced Sheet with History Support
|
// Enhanced Sheet with History Support
|
||||||
interface HistorySheetProps extends SheetPrimitive.DialogProps {
|
interface HistorySheetProps extends SheetPrimitive.DialogProps {
|
||||||
@ -17,51 +18,28 @@ const Sheet = ({
|
|||||||
...props
|
...props
|
||||||
}: HistorySheetProps) => {
|
}: HistorySheetProps) => {
|
||||||
const [internalOpen, setInternalOpen] = React.useState(open || false);
|
const [internalOpen, setInternalOpen] = React.useState(open || false);
|
||||||
const historyStateRef = React.useRef<null | {
|
|
||||||
listener: (e: PopStateEvent) => void;
|
|
||||||
}>(null);
|
|
||||||
|
|
||||||
|
// Sync internal state with controlled open prop
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (open !== undefined) {
|
if (open !== undefined) {
|
||||||
setInternalOpen(open);
|
setInternalOpen(open);
|
||||||
}
|
}
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
const handleOpenChange = React.useCallback(
|
||||||
if (enableHistoryBack) {
|
(newOpen: boolean) => {
|
||||||
if (internalOpen) {
|
setInternalOpen(newOpen);
|
||||||
window.history.pushState({ sheetOpen: true }, "");
|
onOpenChange?.(newOpen);
|
||||||
|
},
|
||||||
const listener = () => {
|
[onOpenChange],
|
||||||
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) => {
|
// Handle browser back button to close sheet
|
||||||
setInternalOpen(open);
|
useHistoryBack({
|
||||||
if (onOpenChange) {
|
enabled: enableHistoryBack,
|
||||||
onOpenChange(open);
|
open: internalOpen,
|
||||||
}
|
onClose: () => handleOpenChange(false),
|
||||||
};
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SheetPrimitive.Root
|
<SheetPrimitive.Root
|
||||||
|
|||||||
57
web/src/hooks/use-history-back.ts
Normal file
57
web/src/hooks/use-history-back.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
interface UseHistoryBackOptions {
|
||||||
|
enabled: boolean;
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook that manages browser history for overlay components (dialogs, sheets, etc.)
|
||||||
|
* When enabled, pressing the browser back button will close the overlay instead of navigating away.
|
||||||
|
*/
|
||||||
|
export function useHistoryBack({
|
||||||
|
enabled,
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
}: UseHistoryBackOptions): void {
|
||||||
|
const historyPushedRef = React.useRef(false);
|
||||||
|
const closedByBackRef = React.useRef(false);
|
||||||
|
|
||||||
|
// Keep onClose in a ref to avoid effect re-runs that cause multiple history pushes
|
||||||
|
const onCloseRef = React.useRef(onClose);
|
||||||
|
React.useLayoutEffect(() => {
|
||||||
|
onCloseRef.current = onClose;
|
||||||
|
});
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!enabled) return;
|
||||||
|
|
||||||
|
if (open) {
|
||||||
|
// Only push history state if we haven't already (prevents duplicates in strict mode)
|
||||||
|
if (!historyPushedRef.current) {
|
||||||
|
window.history.pushState({ overlayOpen: true }, "");
|
||||||
|
historyPushedRef.current = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePopState = () => {
|
||||||
|
closedByBackRef.current = true;
|
||||||
|
historyPushedRef.current = false;
|
||||||
|
onCloseRef.current();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("popstate", handlePopState);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("popstate", handlePopState);
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Overlay is closing - clean up history if we pushed and it wasn't via back button
|
||||||
|
if (historyPushedRef.current && !closedByBackRef.current) {
|
||||||
|
window.history.back();
|
||||||
|
}
|
||||||
|
historyPushedRef.current = false;
|
||||||
|
closedByBackRef.current = false;
|
||||||
|
}
|
||||||
|
}, [enabled, open]);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user