ERP-node/frontend/components/ui/alert-dialog.tsx

230 lines
7.9 KiB
TypeScript
Raw Normal View History

2025-08-21 09:41:46 +09:00
"use client";
import * as React from "react";
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
import * as DialogPrimitive from "@radix-ui/react-dialog";
2025-08-21 09:41:46 +09:00
import { cn } from "@/lib/utils";
import { buttonVariants } from "@/components/ui/button";
import { useModalPortal } from "@/lib/modalPortalRef";
import { useTabId } from "@/contexts/TabIdContext";
import { useTabStore } from "@/stores/tabStore";
2025-08-21 09:41:46 +09:00
/**
* Context.
* scoped=true AlertDialogPrimitive DialogPrimitive .
*/
const ScopedAlertCtx = React.createContext(false);
const AlertDialog: React.FC<React.ComponentProps<typeof AlertDialogPrimitive.Root>> = ({
open,
children,
onOpenChange,
...props
}) => {
const autoContainer = useModalPortal();
const scoped = !!autoContainer;
const tabId = useTabId();
const activeTabId = useTabStore((s) => s[s.mode].activeTabId);
const isTabActive = !tabId || tabId === activeTabId;
const isTabActiveRef = React.useRef(isTabActive);
isTabActiveRef.current = isTabActive;
const effectiveOpen = open != null ? open && isTabActive : undefined;
const guardedOnOpenChange = React.useCallback(
(newOpen: boolean) => {
if (scoped && !newOpen && !isTabActiveRef.current) return;
onOpenChange?.(newOpen);
},
[scoped, onOpenChange],
);
if (scoped) {
return (
<ScopedAlertCtx.Provider value={true}>
<DialogPrimitive.Root open={effectiveOpen} onOpenChange={guardedOnOpenChange} modal={false}>
{children}
</DialogPrimitive.Root>
</ScopedAlertCtx.Provider>
);
}
return (
<ScopedAlertCtx.Provider value={false}>
<AlertDialogPrimitive.Root {...props} open={effectiveOpen} onOpenChange={guardedOnOpenChange}>
{children}
</AlertDialogPrimitive.Root>
</ScopedAlertCtx.Provider>
);
};
AlertDialog.displayName = "AlertDialog";
2025-08-21 09:41:46 +09:00
const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
const AlertDialogPortal = AlertDialogPrimitive.Portal;
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-1050 bg-black/80",
2025-08-21 09:41:46 +09:00
className,
)}
{...props}
ref={ref}
/>
));
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
interface ScopedAlertDialogContentProps
extends React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content> {
container?: HTMLElement | null;
hidden?: boolean;
}
2025-08-21 09:41:46 +09:00
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
ScopedAlertDialogContentProps
>(({ className, container: explicitContainer, hidden: hiddenProp, style, ...props }, ref) => {
const autoContainer = useModalPortal();
const container = explicitContainer !== undefined ? explicitContainer : autoContainer;
const scoped = React.useContext(ScopedAlertCtx);
const adjustedStyle = scoped && style
? { ...style, maxHeight: undefined, maxWidth: undefined }
: style;
const handleInteractOutside = React.useCallback(
(e: any) => {
if (scoped && container) {
const target = (e.detail?.originalEvent?.target ?? e.target) as HTMLElement | null;
if (target && !container.contains(target)) {
e.preventDefault();
return;
}
}
e.preventDefault();
},
[scoped, container],
);
if (scoped) {
return (
<DialogPrimitive.Portal container={container ?? undefined}>
<div
className="absolute inset-0 z-1050 flex items-center justify-center overflow-hidden p-4"
style={hiddenProp ? { display: "none" } : undefined}
>
<div className="absolute inset-0 bg-black/80" />
<DialogPrimitive.Content
ref={ref}
onInteractOutside={handleInteractOutside}
onFocusOutside={(e: any) => e.preventDefault()}
onEscapeKeyDown={(e) => e.preventDefault()}
className={cn(
"bg-background relative z-1 grid w-full max-w-lg max-h-full gap-4 border p-6 shadow-lg sm:rounded-lg",
className,
)}
style={adjustedStyle}
{...props}
/>
</div>
</DialogPrimitive.Portal>
);
}
return (
<AlertDialogPortal container={container ?? undefined}>
<div
style={hiddenProp ? { display: "none" } : undefined}
>
<AlertDialogPrimitive.Overlay className="fixed inset-0 z-1050 bg-black/80" />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"bg-background fixed top-[50%] left-[50%] z-1100 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg sm:rounded-lg",
className,
)}
style={adjustedStyle}
{...props}
/>
</div>
</AlertDialogPortal>
);
});
2025-08-21 09:41:46 +09:00
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
);
AlertDialogHeader.displayName = "AlertDialogHeader";
const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} {...props} />
);
AlertDialogFooter.displayName = "AlertDialogFooter";
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => {
const scoped = React.useContext(ScopedAlertCtx);
const Comp = scoped ? DialogPrimitive.Title : AlertDialogPrimitive.Title;
return <Comp ref={ref} className={cn("text-lg font-semibold", className)} {...props} />;
});
2025-08-21 09:41:46 +09:00
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => {
const scoped = React.useContext(ScopedAlertCtx);
const Comp = scoped ? DialogPrimitive.Description : AlertDialogPrimitive.Description;
return <Comp ref={ref} className={cn("text-muted-foreground text-sm", className)} {...props} />;
});
2025-08-21 09:41:46 +09:00
AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName;
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => {
const scoped = React.useContext(ScopedAlertCtx);
const Comp = scoped ? DialogPrimitive.Close : AlertDialogPrimitive.Action;
return <Comp ref={ref} className={cn(buttonVariants(), className)} {...props} />;
});
2025-08-21 09:41:46 +09:00
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => {
const scoped = React.useContext(ScopedAlertCtx);
const Comp = scoped ? DialogPrimitive.Close : AlertDialogPrimitive.Cancel;
return (
<Comp
ref={ref}
className={cn(buttonVariants({ variant: "outline" }), "mt-2 sm:mt-0", className)}
{...props}
/>
);
});
2025-08-21 09:41:46 +09:00
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
};